[
  {
    "path": ".gitattributes",
    "content": "/.gitattributes export-ignore\n/.github/ export-ignore\n/.gitignore export-ignore\n/docs/ export-ignore\n/phpcs.xml export-ignore\n/phpstan.neon export-ignore\n/phpunit.xml export-ignore\n/tests/ export-ignore\n"
  },
  {
    "path": ".github/DISCUSSION_TEMPLATE/bugs.yml",
    "content": "body:\n  - type: markdown\n    attributes:\n      value: |\n        **Before opening a bug report, please search the existing discussions.**\n\n  - type: input\n    id: deployer-version\n    attributes:\n      label: Deployer Version\n      description: Which version of Deployer are you using? Please provide the full version.\n      placeholder: v8.0.0\n    validations:\n      required: true\n\n  - type: input\n    id: target-os\n    attributes:\n      label: Target OS\n      description: Which operating system are you using? Please provide the full version.\n      placeholder: Ubuntu 24.04\n    validations:\n      required: true\n\n  - type: dropdown\n    id: php-version\n    attributes:\n      label: Which PHP version are you using?\n      options:\n        - PHP 8.5\n        - PHP 8.4\n        - PHP 8.3\n        - PHP 8.2\n        - PHP 8.1\n        - PHP 8.0\n        - PHP 7.4\n        - PHP 7.3\n        - PHP 7.2\n        - PHP 7.1\n        - PHP 7.0\n        - PHP 5.6\n        - PHP 5.5\n        - PHP 5.4\n        - PHP 5.3\n    validations:\n      required: true\n\n  - type: textarea\n    id: deploy-src\n    attributes:\n      label: Content of deploy.php or deploy.yaml\n      description: Please, provide a minimal reproducible example of deploy.php or deploy.yaml file.\n    validations:\n      required: false\n\n  - type: textarea\n    attributes:\n      label: Steps to reproduce\n      description: Please provide the steps to reproduce the bug.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: antonmedv\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Bug Report\n    url: https://github.com/deployphp/deployer/discussions/new?category=bugs\n    about: Submit a bug or an issue\n  - name: Feature request\n    url: https://github.com/deployphp/deployer/discussions/new?category=features\n    about: For ideas or feature requests\n  - name: Support questions & other\n    url: https://github.com/deployphp/deployer/discussions/new?category=help-needed\n    about: If you have a question or need help using the library\n  - name: General discussion\n    url: https://github.com/deployphp/deployer/discussions/new?category=general\n    about: Start a new discussion\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "- [ ] Bug fix #…?\n- [ ] New feature?\n- [ ] BC breaks?\n- [ ] Tests added?\n- [ ] Docs added?\n\n      Please, regenerate docs by running next command:\n      $ php bin/docgen\n"
  },
  {
    "path": ".github/labeler.yml",
    "content": "v7:\n- base-branch: \"7.x\""
  },
  {
    "path": ".github/workflows/check.yml",
    "content": "name: check\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  phpstan:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: '8.2'\n\n      - name: Validate composer.json and composer.lock\n        run: composer validate\n\n      - name: Get Composer Cache Directory\n        id: composer-cache\n        run: |\n          echo \"dir=$(composer config cache-files-dir)\" >> $GITHUB_OUTPUT\n\n      - uses: actions/cache@v4\n        with:\n          path: ${{ steps.composer-cache.outputs.dir }}\n          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-composer-\n\n      - name: Install dependencies\n        if: steps.composer-cache.outputs.cache-hit != 'true'\n        run: composer install --prefer-dist --no-progress\n\n      - name: Run test suite\n        run: composer phpstan\n\n  code-style:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: '8.2'\n\n      - name: Validate composer.json and composer.lock\n        run: composer validate\n\n      - name: Get Composer Cache Directory\n        id: composer-cache\n        run: |\n          echo \"dir=$(composer config cache-files-dir)\" >> $GITHUB_OUTPUT\n\n      - uses: actions/cache@v4\n        with:\n          path: ${{ steps.composer-cache.outputs.dir }}\n          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-composer-\n\n      - name: Install dependencies\n        if: steps.composer-cache.outputs.cache-hit != 'true'\n        run: composer install --prefer-dist --no-progress\n\n      - name: Run php-cs-fixer\n        run: composer check\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: docker\n\non:\n  release:\n    types: [ published ]\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version'\n        required: true\n\npermissions:\n  id-token: write\n  attestations: write\n\njobs:\n  build-and-push:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: '8.2'\n\n      - name: Get version\n        run: |\n          echo \"RELEASE_VERSION=${GITHUB_REF#refs/*/v}\" >> $GITHUB_ENV\n          if [ -n \"$VERSION\" ]; then\n            echo \"RELEASE_VERSION=${{ inputs.version }}\" >> $GITHUB_ENV\n          fi\n        env:\n          VERSION: ${{ inputs.version }}\n\n      - name: Build phar\n        run: php -d phar.readonly=0 bin/build -v\"$RELEASE_VERSION\"\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: deployphp/deployer\n          tags: |\n            type=raw,value=latest,enable={{is_default_branch}}\n            type=sha,format=long\n            type=sha\n            type=semver,pattern=v{{major}}.{{minor}}.{{patch}}\n            type=semver,pattern=v{{major}}.{{minor}}\n            type=semver,pattern=v{{major}}\n            type=ref,event=tag\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: deployphp\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Build and push\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm64\n          push: true\n          provenance: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/docs-sync.yml",
    "content": "name: doc-sync\n\non:\n  push:\n    branches: [ master ]\n\npermissions:\n  contents: write\n\njobs:\n  docgen-and-commit:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: '8.2'\n\n      - name: Get Composer Cache Directory\n        id: composer-cache\n        run: |\n          echo \"dir=$(composer config cache-files-dir)\" >> $GITHUB_OUTPUT\n\n      - uses: actions/cache@v4\n        with:\n          path: ${{ steps.composer-cache.outputs.dir }}\n          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-composer-\n\n      - name: Install dependencies\n        if: steps.composer-cache.outputs.cache-hit != 'true'\n        run: composer install --prefer-dist --no-progress\n\n      - name: Run docgen\n        run: php bin/docgen\n\n      - name: Add & Commit\n        uses: EndBug/add-and-commit@v9\n        with:\n          default_author: github_actions\n          add: 'docs'\n          message: '[automatic] Update docs with bin/docgen'\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: doc\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  docgen:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: '8.2'\n\n      - name: Get Composer Cache Directory\n        id: composer-cache\n        run: |\n          echo \"dir=$(composer config cache-files-dir)\" >> $GITHUB_OUTPUT\n\n      - uses: actions/cache@v4\n        with:\n          path: ${{ steps.composer-cache.outputs.dir }}\n          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-composer-\n\n      - name: Install dependencies\n        if: steps.composer-cache.outputs.cache-hit != 'true'\n        run: composer install --prefer-dist --no-progress\n\n      - name: Run docgen\n        run: php bin/docgen\n\n      - name: Check for uncommitted changes\n        run: |\n          status=$(git status --porcelain docs/);\n          [ -z \"$status\" ] || {\n            echo \"Please, run bin/docgen and commit next files:\";\n            echo $status;\n            exit 1;\n          }\n"
  },
  {
    "path": ".github/workflows/labeler.yml",
    "content": "name: labeler\n\non:\n- pull_request_target\n\njobs:\n  labeler:\n    permissions:\n      contents: read\n      pull-requests: write\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/labeler@v6\n  "
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: lint\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    types: [opened, synchronize, reopened, ready_for_review]\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        php-versions: [ '8.2' ]\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php-versions }}\n          tools: cs2pr, parallel-lint\n\n      - name: Lint sources\n        run: composer exec --no-interaction -- parallel-lint bin/ contrib/ recipe/ src/ tests/ --checkstyle | cs2pr\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: release\n\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version'\n        required: true\n\npermissions:\n  contents: write\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Get version\n        run: |\n          echo \"RELEASE_VERSION=${GITHUB_REF#refs/*/v}\" >> $GITHUB_ENV\n          if [ -n \"$VERSION\" ]; then\n            echo \"RELEASE_VERSION=${{ inputs.version }}\" >> $GITHUB_ENV\n          fi\n        env:\n          VERSION: ${{ inputs.version }}\n\n      - name: Build phar\n        run: php -d phar.readonly=0 bin/build -v\"$RELEASE_VERSION\"\n\n      - name: Sign phar\n        run: |\n          mkdir -p ~/.gnupg/\n          chmod 0700 ~/.gnupg/\n          echo \"$GPG_SIGNING_KEY\" > ~/.gnupg/private.key\n          gpg --import --no-tty --batch --yes ~/.gnupg/private.key\n          gpg -u anton@deployer.org --batch --pinentry-mode loopback --passphrase \"${GPG_PASSPHRASE}\" --detach-sign --output deployer.phar.asc deployer.phar\n        env:\n          GPG_SIGNING_KEY: |\n            ${{ secrets.GPG_SIGNING_KEY }}\n          GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}\n\n      - name: Upload phar\n        run: gh release upload v\"${RELEASE_VERSION}\" deployer.phar\n        env:\n          GH_TOKEN: ${{ github.token }}\n\n      - name: Upload signature\n        run: gh release upload v\"${RELEASE_VERSION}\" deployer.phar.asc\n        env:\n          GH_TOKEN: ${{ github.token }}\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: stale\non:\n  schedule:\n    - cron: \"* * * * *\"\n  workflow_dispatch:\n\njobs:\n  close-issues:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n    steps:\n      - uses: actions/stale@v9\n        with:\n          days-before-issue-stale: 0\n          days-before-issue-close: 0\n          ignore-updates: true\n          close-issue-message: |\n            This issue has been automatically closed. Please, open a discussion for bug reports and feature requests.\n            \n            Read more: https://github.com/deployphp/deployer/discussions/3888\n          days-before-pr-stale: -1\n          days-before-pr-close: -1\n          operations-per-run: 1440\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: test\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  unit:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        php-versions: [ '8.2', '8.3' ]\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php-versions }}\n          extensions: mbstring, intl\n          coverage: xdebug\n\n      - name: Validate composer.json and composer.lock\n        run: composer validate\n\n      - name: Get Composer Cache Directory\n        id: composer-cache\n        run: |\n          echo \"dir=$(composer config cache-files-dir)\" >> $GITHUB_OUTPUT\n\n      - uses: actions/cache@v4\n        with:\n          path: ${{ steps.composer-cache.outputs.dir }}\n          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-composer-\n\n      - name: Install dependencies\n        if: steps.composer-cache.outputs.cache-hit != 'true'\n        run: composer install --prefer-dist --no-progress\n\n      - name: Run test suite\n        run: composer test\n"
  },
  {
    "path": ".gitignore",
    "content": "/vendor/\n*.phar\n.phpunit.result.cache\ndocker-compose.override.yml\n.php-cs-fixer.cache\n.idea/\n"
  },
  {
    "path": ".php-cs-fixer.dist.php",
    "content": "<?php\n\n$finder = (new PhpCsFixer\\Finder())\n    ->in(__DIR__ . '/src')\n    ->in(__DIR__ . '/recipe')\n    ->in(__DIR__ . '/contrib')\n    ->in(__DIR__ . '/tests');\n\nreturn (new PhpCsFixer\\Config())\n    ->setRules([\n        '@PER-CS' => true,\n\n        // Due to historical reasons we have to keep this.\n        // Docs parser expects comment right after php tag.\n        'blank_line_after_opening_tag' => false,\n\n        // For PHP 7.4 compatibility.\n        'trailing_comma_in_multiline' => [\n            'elements' => ['arguments', 'array_destructuring', 'arrays']\n        ],\n    ])\n    ->setFinder($finder);\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM php:8.4-cli-alpine\n\nRUN apk add --no-cache bash git openssh-client rsync zip unzip libzip-dev \\\n\nRUN docker-php-ext-install mbstring mcrypt pcntl sockets curl zip\n\nCOPY --chmod=755 deployer.phar /bin/dep\n\nWORKDIR /app\n\nENTRYPOINT [\"/bin/dep\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright © 2013 Anton Medvedev\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<h1>\n    <a href=\"https://deployer.org\">\n        <picture>\n            <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://deployer.org/img/logo-white.svg\" height=\"30\">\n            <img src=\"https://deployer.org/img/logo.svg\" alt=\"Deployer Logo\" height=\"30\">\n        </picture>\n    </a>\n    Deployer\n</h1>\n<p>The PHP deployment tool with support for popular frameworks out of the box.</p>\n\n<p align=\"center\"><br><br><a href=\"https://deployer.org\"><img src=\"https://medv.io/assets/deployer/deployer.gif\" alt=\"Deployer Screenshot\" width=\"530\"></a><br><br><br></p>\n\n---\n\n<p style=\"font-size: 21px; color:black;\">\n    Browser testing via<br> \n    <a href=\"https://www.testmu.ai\" target=\"_blank\">\n        <picture>\n            <source media=\"(prefers-color-scheme: dark)\" srcset=\".github/testmu-black-logo.png\" width=\"800\">\n            <img src=\".github/testmu-white-logo.png\" alt=\"Testmu AI\" width=\"800\">\n        </picture>\n    </a>\n</p>\n\n---\n\n<a href=\"https://github.com/deployphp/deployer/actions?query=workflow%3Atest\"><img src=\"https://github.com/deployphp/deployer/workflows/test/badge.svg\" alt=\"Build Status\"></a>\n<a href=\"https://packagist.org/packages/deployer/deployer\"><img src=\"https://img.shields.io/packagist/v/deployer/deployer.svg?style=flat\" alt=\"Latest Stable Version\"></a>\n<a href=\"https://packagist.org/packages/deployer/deployer\"><img src=\"https://img.shields.io/badge/license-MIT-blue.svg?style=flat\" alt=\"License\"></a>\n\nSee [deployer.org](https://deployer.org) for more information and documentation.\n\n## Features\n\n- Automatic server **provisioning**.\n- **Zero downtime** deployments.\n- Ready to use recipes for **most frameworks**.\n\n## Additional resources\n\n* [GitHub Action for Deployer](https://github.com/deployphp/action)\n* [Deployer Docker Image](https://hub.docker.com/r/deployphp/deployer)\n\n## License\n[MIT](https://github.com/deployphp/deployer/blob/master/LICENSE)\n\n<p align=\"center\">\n  <a href=\"https://crow.watch/join/deployer\">\n    <img src=\"https://github.com/user-attachments/assets/37c84073-6533-4746-951d-d879f90a7fd2\" alt=\"Join Crow Watch\" width=\"900\" hight=\"600\">  \n  </a>\n</p>\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nDeployer is generally backwards compatible with very few exceptions, so we\nrecommend users to always use the latest version to experience stability,\nperformance and security.\n\nWe generally backport security issues to a single previous major version,\nunless this is not possible or feasible with a reasonable effort.\n\n| Version | Supported          |\n|---------|--------------------|\n| 8       | :white_check_mark: |\n| 7       | :white_check_mark: |\n| < 7     | :x:                |\n\n## Reporting a Vulnerability\n\nIf you believe you've discovered a serious vulnerability, please contact the\nExpr core team at anton+security@medv.io. We will evaluate your report and if\nnecessary issue a fix and an advisory. If the issue was previously undisclosed,\nwe'll also mention your name in the credits.\n"
  },
  {
    "path": "bin/build",
    "content": "#!/usr/bin/env php\n<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\nif (ini_get('phar.readonly') === '1') {\n    throw new \\Exception('Writing to phar files is disabled. Change your `php.ini` or append `-d phar.readonly=false` to the shebang, if supported by your `env` executable.');\n}\n\ndefine('__ROOT__', realpath(__DIR__ . '/..'));\nchdir(__ROOT__);\n\n$opt = getopt('v:', ['nozip']);\n\n$version = $opt['v'] ?? null;\nif (empty($version)) {\n    echo \"Please, specify version as \\\"-v8.0.0\\\".\\n\";\n    exit(1);\n}\nif (!preg_match('/^\\d+\\.\\d+\\.\\d+(\\-\\w+(\\.\\d+)?)?$/', $version)) {\n    echo \"Version must be \\\"7.0.0-beta.42\\\". Got \\\"$version\\\".\\n\";\n    exit(1);\n}\n\necho `set -x; composer install --no-dev --prefer-dist --optimize-autoloader`;\n\n$pharName = \"deployer.phar\";\n$pharFile = __ROOT__ . '/' . $pharName;\nif (file_exists($pharFile)) {\n    unlink($pharFile);\n}\n\n$ignore = [\n    '.anton',\n    '.git',\n    'Tests',\n    'tests',\n    'deploy.php',\n    '.php-cs-fixer.dist.php',\n];\n\n$phar = new \\Phar($pharFile, 0, $pharName);\n$phar->setSignatureAlgorithm(\\Phar::SHA1);\n$phar->startBuffering();\n$iterator = new RecursiveDirectoryIterator(__ROOT__, FilesystemIterator::SKIP_DOTS);\n$iterator = new RecursiveCallbackFilterIterator($iterator, function (SplFileInfo $fileInfo) use ($ignore) {\n    return !in_array($fileInfo->getBasename(), $ignore, true);\n});\n$iterator = new RecursiveIteratorIterator($iterator);\n$iterator = new CallbackFilterIterator($iterator, function (SplFileInfo $fileInfo) {\n    //'bash', 'fish', 'zsh' is a completion templates\n    return in_array($fileInfo->getExtension(), ['php', 'exe', 'bash', 'fish', 'zsh'], true);\n});\n\nforeach ($iterator as $fileInfo) {\n    $file = str_replace(__ROOT__, '', $fileInfo->getRealPath());\n    echo \"+ \" . $file . \"\\n\";\n    $phar->addFile($fileInfo->getRealPath(), $file);\n\n    if (!array_key_exists('nozip', $opt)) {\n        $phar[$file]->compress(Phar::GZ);\n\n        if (!$phar[$file]->isCompressed()) {\n            echo \"Could not compress File: $file\\n\";\n        }\n    }\n}\n\n// Add Caddyfile\necho \"+ /recipe/provision/Caddyfile\\n\";\n$phar->addFile(realpath(__DIR__ . '/../recipe/provision/Caddyfile'), '/recipe/provision/Caddyfile');\n\n// Add 404.html\necho \"+ /recipe/provision/404.html\\n\";\n$phar->addFile(realpath(__DIR__ . '/../recipe/provision/404.html'), '/recipe/provision/404.html');\n\n// Add bin/dep file\necho \"+ /bin/dep\\n\";\n$depContent = file_get_contents(__ROOT__ . '/bin/dep');\n$depContent = str_replace(\"#!/usr/bin/env php\\n\", '', $depContent);\n$depContent = str_replace('__FILE__', 'str_replace(\"phar://\", \"\", Phar::running())', $depContent);\n$depContent = preg_replace(\"/run\\('.+?'/\", \"run('$version'\", $depContent);\n$phar->addFromString('bin/dep', $depContent);\n\n$phar->setStub(\n    <<<STUB\n#!/usr/bin/env php\n<?php\nPhar::mapPhar('{$pharName}');\nrequire 'phar://{$pharName}/bin/dep';\n__HALT_COMPILER();\nSTUB\n);\n$phar->stopBuffering();\nunset($phar);\n\necho \"$pharName was created successfully.\\n\";\n"
  },
  {
    "path": "bin/dep",
    "content": "#!/usr/bin/env php\n<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\n// Check PHP version\nif (PHP_VERSION_ID < 80200) {\n    fwrite(STDERR, \"PHP 8.2 or higher is required.\\n\");\n    exit(1);\n}\n\n// Detect deploy.php location\n$deployFile = null;\nforeach ($argv as $i => $arg) {\n    if (preg_match('/^(-f|--file)$/', $arg, $match) && $i + 1 < count($argv)) {\n        $deployFile = $argv[$i + 1];\n        break;\n    }\n    if (preg_match('/^--file=(?<file>.+)$/', $arg, $match)) {\n        $deployFile = $match['file'];\n        break;\n    }\n    if (preg_match('/^-f=?(?<file>.+)$/', $arg, $match)) {\n        $deployFile = $match['file'];\n        break;\n    }\n}\nif (!empty($deployFile)) {\n    $deployFile = realpath($deployFile);\n}\n$lookUp = function (string $name): ?string {\n    $dir = getcwd();\n    for ($i = 0; $i < 10; $i++) {\n        $path = \"$dir/$name\";\n        if (is_readable($path)) {\n            return $path;\n        }\n        $dir = dirname($dir);\n    }\n    return '';\n};\nif (empty($deployFile)) {\n    $deployFile = $lookUp('deploy.php');\n}\nif (empty($deployFile)) {\n    $deployFile = $lookUp('deploy.yaml');\n}\nif (empty($deployFile)) {\n    $deployFile = $lookUp('deploy.yml');\n}\n\n// Detect autoload location\n$autoload = [\n    __DIR__ . '/../vendor/autoload.php', // The dep located at \"deployer.phar/bin\" or in development.\n    __DIR__ . '/../../../autoload.php', // The dep located at \"vendor/deployer/deployer/bin\".\n    __DIR__ . '/../autoload.php', // The dep located at \"vendor/bin\".\n];\n$includes = [\n    __DIR__ . '/..',\n    __DIR__ . '/../../../deployer/deployer',\n    __DIR__ . '/../deployer/deployer',\n];\n$includePath = false;\nfor ($i = 0; $i < count($autoload); $i++) {\n    if (file_exists($autoload[$i]) && is_dir($includes[$i])) {\n        require $autoload[$i];\n        $includePath = $includes[$i];\n        break;\n    }\n}\nif (empty($includePath)) {\n    fwrite(STDERR, \"Error: The `autoload.php` file not found in:\\n\");\n    for ($i = 0; $i < count($autoload); $i++) {\n        $a = file_exists($autoload[$i]) ? 'true' : 'false';\n        $b = is_dir($includes[$i]) ? 'true' : 'false';\n        fwrite(STDERR, \"  - file_exists($autoload[$i]) = $a\\n\");\n        fwrite(STDERR, \"    is_dir($includes[$i]) = $b\\n\");\n    }\n    exit(1);\n}\n\n// Errors to exception\nset_error_handler(function ($severity, $message, $filename, $lineno) {\n    if (error_reporting() == 0) {\n        return;\n    }\n    if (error_reporting() & $severity) {\n        throw new ErrorException($message, 0, $severity, $filename, $lineno);\n    }\n});\n\n// Enable recipe loading\nset_include_path($includePath . PATH_SEPARATOR . get_include_path());\n\n// Deployer constants\ndefine('DEPLOYER', true);\ndefine('DEPLOYER_BIN', __FILE__);\ndefine('DEPLOYER_DEPLOY_FILE', $deployFile);\n\nDeployer\\Deployer::run('master', $deployFile);\n"
  },
  {
    "path": "bin/docgen",
    "content": "#!/usr/bin/env php\n<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nuse Deployer\\Documentation\\ApiGen;\nuse Deployer\\Documentation\\DocGen;\nuse Symfony\\Component\\Console\\Application;\nuse Symfony\\Component\\Console\\Input\\ArgvInput;\nuse Symfony\\Component\\Console\\Output\\ConsoleOutput;\n\nrequire __DIR__ . '/../vendor/autoload.php';\n\nchdir(realpath(__DIR__ . '/..'));\n\n$input = new ArgvInput();\n$output = new ConsoleOutput();\n$app = new Application('DocGen', '1.0.0');\n$app->setDefaultCommand('all');\n\n$api = function () use ($output) {\n    $parser = new ApiGen();\n    $parser->parse(file_get_contents(__DIR__ . '/../src/functions.php'));\n    $md = $parser->markdown();\n    file_put_contents(__DIR__ . '/../docs/api.md', $md);\n    $output->writeln('API Reference documentation updated.');\n};\n\n$recipes = function () use ($input, $output) {\n    $docgen = new DocGen(__DIR__ . '/..');\n    $docgen->parse(__DIR__ . '/../recipe');\n    $docgen->parse(__DIR__ . '/../contrib');\n\n    if ($input->getOption('json')) {\n        echo json_encode($docgen->recipes, JSON_PRETTY_PRINT);\n        return;\n    }\n\n    $docgen->gen(__DIR__ . '/../docs');\n    $output->writeln('Recipes documentation updated.');\n};\n\n$app->register('api')->setCode($api);\n$app->register('recipes')->setCode($recipes)->addOption('json');\n$app->register('all')->setCode(function () use ($recipes, $api) {\n    $api();\n    $recipes();\n    echo `git status`;\n})->addOption('json');\n\n$app->run($input, $output);\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"deployer/deployer\",\n    \"description\": \"Deployment Tool\",\n    \"license\": \"MIT\",\n    \"homepage\": \"https://deployer.org\",\n    \"support\": {\n        \"docs\": \"https://deployer.org/docs\",\n        \"source\": \"https://github.com/deployphp/deployer\",\n        \"issues\": \"https://github.com/deployphp/deployer/issues\"\n    },\n    \"authors\": [\n        {\n            \"name\": \"Anton Medvedev\",\n            \"email\": \"anton@medv.io\"\n        }\n    ],\n    \"funding\": [\n        {\n            \"type\": \"github\",\n            \"url\": \"https://github.com/sponsors/antonmedv\"\n        }\n    ],\n    \"autoload\": {\n        \"psr-4\": {\n            \"Deployer\\\\\": \"src/\"\n        },\n        \"files\": [\n            \"src/functions.php\",\n            \"src/Support/helpers.php\"\n        ]\n    },\n    \"scripts\": {\n        \"test\": \"pest\",\n        \"test:e2e\": \"pest --config tests/e2e/phpunit-e2e.xml\",\n        \"check\": \"php-cs-fixer check\",\n        \"fix\": \"php-cs-fixer fix\",\n        \"phpstan\": \"phpstan analyse -c phpstan.neon --memory-limit 1G\",\n        \"phpstan:baseline\": \"@phpstan --generate-baseline tests/phpstan-baseline.neon\"\n    },\n    \"bin\": [\n        \"bin/dep\"\n    ],\n    \"require\": {\n        \"php\": \">=8.2\",\n        \"symfony/console\": \"^7.2\",\n        \"symfony/process\": \"^7.2\",\n        \"symfony/yaml\": \"^7.2\"\n    },\n    \"require-dev\": {\n        \"friendsofphp/php-cs-fixer\": \"^3.68\",\n        \"pestphp/pest\": \"^3.3\",\n        \"phpstan/phpstan\": \"^1.4\",\n        \"phpunit/php-code-coverage\": \"^11.0\",\n        \"phpunit/phpunit\": \"^11.4\"\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"process-timeout\": 0,\n        \"allow-plugins\": {\n            \"pestphp/pest-plugin\": true,\n            \"dealerdirect/phpcodesniffer-composer-installer\": true\n        }\n    }\n}\n"
  },
  {
    "path": "contrib/bugsnag.php",
    "content": "<?php\n/*\n\n## Configuration\n\n- *bugsnag_api_key* – the API Key associated with the project. Informs Bugsnag which project has been deployed. This is the only required field.\n- *bugsnag_provider* – the name of your source control provider. Required when repository is supplied and only for on-premise services.\n- *bugsnag_app_version* – the app version of the code you are currently deploying. Only set this if you tag your releases with semantic version numbers and deploy infrequently. (Optional.)\n\n## Usage\n\nSince you should only notify Bugsnag of a successful deployment, the `bugsnag:notify` task should be executed right at the end.\n\n```php\nafter('deploy', 'bugsnag:notify');\n```\n*/\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\ndesc('Notifies Bugsnag of deployment');\ntask('bugsnag:notify', function () {\n    $data = [\n        'apiKey'       => get('bugsnag_api_key'),\n        'releaseStage' => get('target'),\n        'repository'   => get('repository'),\n        'provider'     => get('bugsnag_provider', ''),\n        'branch'       => get('branch'),\n        'revision'     => runLocally('git log -n 1 --format=\"%h\"'),\n        'appVersion'   => get('bugsnag_app_version', ''),\n    ];\n\n    Httpie::post('https://notify.bugsnag.com/deploy')\n        ->jsonBody($data)\n        ->send();\n});\n"
  },
  {
    "path": "contrib/cachetool.php",
    "content": "<?php\n/*\n\n## Configuration\n\n- **cachetool** *(optional)*: accepts a *string* or an *array* of strings with the unix socket or ip address to php-fpm. If `cachetool` is not given, then the application will look for a configuration file. The file must be named .cachetool.yml or .cachetool.yaml. CacheTool will look for this file on the current directory and in any parent directory until it finds one. If the paths above fail it will try to load /etc/cachetool.yml or /etc/cachetool.yaml configuration file.\n\n    ```php\n    set('cachetool', '/var/run/php-fpm.sock');\n    // or\n    set('cachetool', '127.0.0.1:9000');\n    // or\n    set('cachetool', ['/var/run/php-fpm.sock', '/var/run/php-fpm-other.sock']);\n    ```\n\nYou can also specify different cachetool settings for each host:\n```php\nhost('staging')\n    ->set('cachetool', '127.0.0.1:9000');\n\nhost('production')\n    ->set('cachetool', '/var/run/php-fpm.sock');\n```\n\nBy default, if no `cachetool` parameter is provided, this recipe will fallback to the global setting.\n\nIf your deployment user does not have permission to access the php-fpm.sock, you can alternatively use\nthe web adapter that creates a temporary php file and makes a web request to it with a configuration like\n```php\nset('cachetool_args', '--web --web-path=./public --web-url=https://{{hostname}}');\n```\n\n## Usage\n\nSince APCu and OPcache deal with compiling and caching files, they should be executed right after the symlink is created for the new release:\n\n```php\nafter('deploy:symlink', 'cachetool:clear:opcache');\n// or\nafter('deploy:symlink', 'cachetool:clear:apcu');\n```\n\n## Read more\n\nRead more information about cachetool on the website:\nhttp://gordalina.github.io/cachetool/\n */\n\nnamespace Deployer;\n\nset('cachetool', '');\n/**\n * URL to download cachetool from if it is not available\n *\n * CacheTool 9.x works with PHP >=8.1\n * CacheTool 8.x works with PHP >=8.0\n * CacheTool 7.x works with PHP >=7.3\n */\nset('cachetool_url', 'https://github.com/gordalina/cachetool/releases/download/9.1.0/cachetool.phar');\nset('cachetool_args', '');\nset('bin/cachetool', function () {\n    if (!test('[ -f {{release_or_current_path}}/cachetool.phar ]')) {\n        run(\"cd {{release_or_current_path}} && curl -sLO {{cachetool_url}}\");\n    }\n    return '{{release_or_current_path}}/cachetool.phar';\n});\nset('cachetool_options', function () {\n    $options = (array) get('cachetool');\n    $fullOptions = (string) get('cachetool_args');\n    $return = [];\n\n    if ($fullOptions !== '') {\n        $return = [$fullOptions];\n    } elseif (count($options) > 0) {\n        foreach ($options as $option) {\n            if (is_string($option) && $option !== '') {\n                $return[] = \"--fcgi={$option}\";\n            }\n        }\n    }\n\n    return $return ?: [''];\n});\n\n/**\n * Clear opcache cache\n */\ndesc('Clears OPcode cache');\ntask('cachetool:clear:opcache', function () {\n    $options = get('cachetool_options');\n    foreach ($options as $option) {\n        run(\"cd {{release_or_current_path}} && {{bin/php}} {{bin/cachetool}} opcache:reset $option\");\n    }\n});\n\n/**\n * Clear APCu cache\n */\ndesc('Clears APCu system cache');\ntask('cachetool:clear:apcu', function () {\n    $options = get('cachetool_options');\n    foreach ($options as $option) {\n        run(\"cd {{release_or_current_path}} && {{bin/php}} {{bin/cachetool}} apcu:cache:clear $option\");\n    }\n});\n\n/**\n * Clear file status cache, including the realpath cache\n */\ndesc('Clears file status and realpath caches');\ntask('cachetool:clear:stat', function () {\n    $options = get('cachetool_options');\n    foreach ($options as $option) {\n        run(\"cd {{release_or_current_path}} && {{bin/php}} {{bin/cachetool}} stat:clear $option\");\n    }\n});\n"
  },
  {
    "path": "contrib/chatwork.php",
    "content": "<?php\n/*\n# Chatwork Recipe\n\n## Installing\n  1. Create chatwork account by any manual in the internet\n  2. Take chatwork token (Like: b29a700e2d15bef3f26ae6a5c142d1ea) and set `chatwork_token` parameter\n  3. Take chatwork room id from url after clicked on the room, and set `chatwork_room_id` parameter\n  4. If you want, you can edit `chatwork_notify_text`, `chatwork_success_text` or `chatwork_failure_text`\n  5. Require chatwork recipe in your `deploy.php` file\n\n```php\n# https://deployer.org/recipes.html\n\nrequire 'recipe/chatwork.php';\n```\n\nAdd hook on deploy:\n\n```php\nbefore('deploy', 'chatwork:notify');\n```\n\n## Configuration\n\n- `chatwork_token` – chatwork bot token, **required**\n- `chatwork_room_id` — chatwork room to push messages to **required**\n- `chatwork_notify_text` – notification message template\n  ```\n  [info]\n    [title](*) Deployment Status: Deploying[/title]\n    Repo: {{repository}}\n    Branch: {{branch}}\n    Server: {{hostname}}\n    Release Path: {{release_path}}\n    Current Path: {{current_path}}\n  [/info]\n  ```\n- `chatwork_success_text` – success template, default:\n  ```\n  [info]\n    [title](*) Deployment Status: Successfully[/title]\n    Repo: {{repository}}\n    Branch: {{branch}}\n    Server: {{hostname}}\n    Release Path: {{release_path}}\n    Current Path: {{current_path}}\n  [/info]\"\n  ```\n- `chatwork_failure_text` – failure template, default:\n  ```\n  [info]\n    [title](*) Deployment Status: Failed[/title]\n    Repo: {{repository}}\n    Branch: {{branch}}\n    Server: {{hostname}}\n    Release Path: {{release_path}}\n    Current Path: {{current_path}}\n  [/info]\"\n  ```\n\n## Tasks\n\n- `chatwork:notify` – send message to chatwork\n- `chatwork:notify:success` – send success message to chatwork\n- `chatwork:notify:failure` – send failure message to chatwork\n\n## Usage\n\nIf you want to notify only about beginning of deployment add this line only:\n\n```php\nbefore('deploy', 'chatwork:notify');\n```\n\nIf you want to notify about successful end of deployment add this too:\n\n```php\nafter('deploy:success', 'chatwork:notify:success');\n```\nIf you want to notify about failed deployment add this too:\n\n```php\nafter('deploy:failed', 'chatwork:notify:failure');\n```\n */\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\n// Chatwork settings\nset('chatwork_token', function () {\n    throw new \\RuntimeException('Please configure \"chatwork_token\" parameter.');\n});\nset('chatwork_room_id', function () {\n    throw new \\RuntimeException('Please configure \"chatwork_room_id\" parameter.');\n});\nset('chatwork_api', function () {\n    return 'https://api.chatwork.com/v2/rooms/' . get('chatwork_room_id') . '/messages';\n});\n\n// The Messages\nset('chatwork_notify_text', \"[info]\\n[title](*) Deployment Status: Deploying[/title]\\nRepo: {{repository}}\\nBranch: {{branch}}\\nServer: {{hostname}}\\nRelease Path: {{release_path}}\\nCurrent Path: {{current_path}}\\n[/info]\");\nset('chatwork_success_text', \"[info]\\n[title](*) Deployment Status: Successfully[/title]\\nRepo: {{repository}}\\nBranch: {{branch}}\\nServer: {{hostname}}\\nRelease Path: {{release_path}}\\nCurrent Path: {{current_path}}\\n[/info]\");\nset('chatwork_failure_text', \"[info]\\n[title](*) Deployment Status: Failed[/title]\\nRepo: {{repository}}\\nBranch: {{branch}}\\nServer: {{hostname}}\\nRelease Path: {{release_path}}\\nCurrent Path: {{current_path}}\\n[/info]\");\n\n// Helpers\ntask('chatwork_send_message', function () {\n    Httpie::post(get('chatwork_api'))\n        ->formBody(['body' => get('chatwork_message')])\n        ->header(\"X-ChatWorkToken\", get('chatwork_token'))\n        ->send();\n});\n\n// Tasks\ndesc('Tests messages');\ntask('chatwork:test', function () {\n    set('chatwork_message', get('chatwork_notify_text'));\n    invoke('chatwork_send_message');\n    set('chatwork_message', get('chatwork_success_text'));\n    invoke('chatwork_send_message');\n    set('chatwork_message', get('chatwork_failure_text'));\n    invoke('chatwork_send_message');\n})\n    ->once();\n\ndesc('Notifies Chatwork');\ntask('chatwork:notify', function () {\n    if (!get('chatwork_token', false)) {\n        return;\n    }\n\n    if (!get('chatwork_room_id', false)) {\n        return;\n    }\n    set('chatwork_message', get('chatwork_notify_text'));\n    invoke('chatwork_send_message');\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies Chatwork about deploy finish');\ntask('chatwork:notify:success', function () {\n    if (!get('chatwork_token', false)) {\n        return;\n    }\n\n    if (!get('chatwork_room_id', false)) {\n        return;\n    }\n\n    set('chatwork_message', get('chatwork_success_text'));\n    invoke('chatwork_send_message');\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies Chatwork about deploy failure');\ntask('chatwork:notify:failure', function () {\n    if (!get('chatwork_token', false)) {\n        return;\n    }\n\n    if (!get('chatwork_room_id', false)) {\n        return;\n    }\n\n    set('chatwork_message', get('chatwork_failure_text'));\n    invoke('chatwork_send_message');\n})\n    ->once()\n    ->hidden();\n"
  },
  {
    "path": "contrib/cimonitor.php",
    "content": "<?php\n/*\nMonitor your deployments on [CIMonitor](https://github.com/CIMonitor/CIMonitor).\n\n![CIMonitorGif](https://www.steefmin.xyz/deployer-example.gif)\n\n\nAdd tasks on deploy:\n\n```php\nbefore('deploy', 'cimonitor:notify');\nafter('deploy:success', 'cimonitor:notify:success');\nafter('deploy:failed', 'cimonitor:notify:failure');\n```\n\n## Configuration\n\n- `cimonitor_webhook` – CIMonitor server webhook url, **required**\n  ```\n  set('cimonitor_webhook', 'https://cimonitor.enrise.com/webhook/deployer');\n  ```\n- `cimonitor_title` – the title of application, default the username\\reponame combination from `{{repository}}`\n  ```\n  set('cimonitor_title', '');\n  ```\n- `cimonitor_user` – User object with name and email, default gets information from `git config`\n  ```\n  set('cimonitor_user', function () {\n    return [\n      'name' => 'John Doe',\n      'email' => 'john@enrise.com',\n    ];\n  });\n  ```\n\nVarious cimonitor statusses are set, in case you want to change these yourselves. See the [CIMonitor documentation](https://cimonitor.readthedocs.io/en/latest/) for the usages of different states.\n\n## Usage\n\nIf you want to notify only about beginning of deployment add this line only:\n\n```php\nbefore('deploy', 'cimonitor:notify');\n```\n\nIf you want to notify about successful end of deployment add this too:\n\n```php\nafter('deploy:success', 'cimonitor:notify:success');\n```\n\nIf you want to notify about failed deployment add this too:\n\n```php\nafter('deploy:failed', 'cimonitor:notify:failure');\n```\n */\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\n// Title of project based on git repo\nset('cimonitor_title', function () {\n    $repo = get('repository');\n    $pattern = '/\\w+\\/\\w+/';\n    return preg_match($pattern, $repo, $titles) ? $titles[0] : $repo;\n});\nset('cimonitor_user', function () {\n    return [\n        'name' => runLocally('git config --get user.name'),\n        'email' => runLocally('git config --get user.email'),\n    ];\n});\n\n// CI monitor status states and job states\nset('cimonitor_status_info', 'info');\nset('cimonitor_status_warning', 'warning');\nset('cimonitor_status_error', 'error');\nset('cimonitor_status_success', 'success');\nset('cimonitor_job_state_info', get('cimonitor_status_info'));\nset('cimonitor_job_state_pending', 'pending');\nset('cimonitor_job_state_running', 'running');\nset('cimonitor_job_state_warning', get('cimonitor_status_warning'));\nset('cimonitor_job_state_error', get('cimonitor_status_error'));\nset('cimonitor_job_state_success', get('cimonitor_status_success'));\n\ndesc('Notifies CIMonitor');\ntask('cimonitor:notify', function () {\n    if (!get('cimonitor_webhook', false)) {\n        return;\n    }\n\n    $body = [\n        'state' => get('cimonitor_status_warning'),\n        'branch' => get('branch'),\n        'title' => get('cimonitor_title'),\n        'user' => get('cimonitor_user'),\n        'stages' => [get('stage', '')],\n        'jobs' => [\n            [\n                'name' => 'Deploying...',\n                'stage' => '',\n                'state' => get('cimonitor_job_state_running'),\n            ],\n        ],\n    ];\n\n    Httpie::post(get('cimonitor_webhook'))->jsonBody($body)->send();\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies CIMonitor about deploy finish');\ntask('cimonitor:notify:success', function () {\n    if (!get('cimonitor_webhook', false)) {\n        return;\n    }\n\n    $depstage = 'Deployed to ' . get('stage', '');\n\n    $body = [\n        'state' => get('cimonitor_status_success'),\n        'branch' => get('branch'),\n        'title' => get('cimonitor_title'),\n        'user' => get('cimonitor_user'),\n        'stages' => [$depstage],\n        'jobs' => [\n            [\n                'name' => 'Deploy',\n                'stage' => $depstage,\n                'state' => get('cimonitor_job_state_success'),\n            ],\n        ],\n    ];\n\n    Httpie::post(get('cimonitor_webhook'))->jsonBody($body)->send();\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies CIMonitor about deploy failure');\ntask('cimonitor:notify:failure', function () {\n    if (!get('cimonitor_webhook', false)) {\n        return;\n    }\n\n    $body = [\n        'state' => get('cimonitor_status_error'),\n        'branch' => get('branch'),\n        'title' => get('cimonitor_title'),\n        'user' => get('cimonitor_user'),\n        'stages' => [get('stage', '')],\n        'jobs' => [\n            [\n                'name' => 'Deploy',\n                'stage' => '',\n                'state' => get('cimonitor_job_state_error'),\n            ],\n        ],\n    ];\n\n    Httpie::post(get('cimonitor_webhook'))->jsonBody($body)->send();\n})\n    ->once()\n    ->hidden();\n"
  },
  {
    "path": "contrib/cloudflare.php",
    "content": "<?php\n/*\n\n### Configuration\n\n- `cloudflare` – array with configuration for cloudflare\n    - `service_key` – Cloudflare Service Key. If this is not provided, use api_key and email.\n    - `api_key` – Cloudflare API key generated on the \"My Account\" page.\n    - `email` – Cloudflare Email address associated with your account.\n    - `api_token` – Cloudflare API Token generated on the \"My Account\" page.\n    - `domain` – The domain you want to clear (optional if zone_id is provided).\n    - `zone_id` – Cloudflare Zone ID (optional).\n\n### Usage\n\nSince the website should be built and some load is likely about to be applied to your server, this should be one of,\nif not the, last tasks before cleanup\n\n*/\n\nnamespace Deployer;\n\ndesc('Clears Cloudflare Cache');\ntask('deploy:cloudflare', function () {\n\n    $config = get('cloudflare', []);\n\n    // validate config and set headers\n    if (!empty($config['service_key'])) {\n        $headers = [\n            'X-Auth-User-Service-Key' => $config['service_key'],\n        ];\n    } elseif (!empty($config['email']) && !empty($config['api_key'])) {\n        $headers = [\n            'X-Auth-Key'   => $config['api_key'],\n            'X-Auth-Email' => $config['email'],\n        ];\n    } elseif (!empty($config['api_token'])) {\n        $headers = [\n            'Authorization' => 'Bearer ' . $config['api_token'],\n        ];\n    } else {\n        throw new \\RuntimeException(\"Set a service key or email / api key\");\n    }\n\n    $headers['Content-Type'] = 'application/json';\n\n    $makeRequest = function ($url, $opts = []) use ($headers) {\n        $ch = curl_init(\"https://api.cloudflare.com/client/v4/$url\");\n\n        $parsedHeaders = [];\n        foreach ($headers as $key => $value) {\n            $parsedHeaders[] = \"$key: $value\";\n        }\n\n        curl_setopt_array($ch, [\n            CURLOPT_HTTPHEADER     => $parsedHeaders,\n            CURLOPT_RETURNTRANSFER => true,\n        ]);\n\n        curl_setopt_array($ch, $opts);\n\n        $res = curl_exec($ch);\n\n        if (curl_errno($ch)) {\n            throw new \\RuntimeException(\"Error making curl request (result: $res)\");\n        }\n\n        if (PHP_MAJOR_VERSION < 8) {\n            curl_close($ch);\n        }\n\n        return $res;\n    };\n\n    $zoneId = $config['zone_id'];\n    if (empty($zoneId)) {\n        if (empty($config['domain'])) {\n            throw new \\RuntimeException(\"Set a domain\");\n        }\n\n        // get the mysterious zone id from Cloud Flare\n        $zones = json_decode($makeRequest(\n            \"zones?name={$config['domain']}\",\n        ), true);\n\n        if (!empty($zones['errors'])) {\n            throw new \\RuntimeException('Problem with zone data');\n        } else {\n            $zoneId = current($zones['result'])['id'];\n        }\n    }\n\n    // make purge request\n    $makeRequest(\n        \"zones/$zoneId/purge_cache\",\n        [\n            CURLOPT_CUSTOMREQUEST => 'DELETE',\n            CURLOPT_POSTFIELDS    => json_encode(\n                [\n                    'purge_everything' => true,\n                ],\n            ),\n        ],\n    );\n});\n"
  },
  {
    "path": "contrib/cpanel.php",
    "content": "<?php\n/*\n### Description\nThis is a recipe that uses the [cPanel 2 API](https://documentation.cPanel.net/display/DD/Guide+to+cPanel+API+2).\n\nUnfortunately the [UAPI](https://documentation.cPanel.net/display/DD/Guide+to+UAPI) that is recommended does not have support for creating addon domains.\nThe main idea behind is for staging purposes but I guess you can use it for other interesting concepts.\n\nThe idea is, every branch possibly has its own staging domain/subdomain (staging-neat-feature.project.com) and database db_neat-feature_project so it can be tested.\nThis recipe can make the domain/subdomain and database creation part of the deployment process so you don't have to manually create them through an interface.\n\n\n### Configuration\nThe example uses a .env file and Dotenv for configuration, but you can set the parameters as you wish\n```\nset('cpanel', [\n    'host' => getenv('CPANEL_HOST'),\n    'port' => getenv('CPANEL_PORT'),\n    'username' => getenv('CPANEL_USERNAME'),\n    'auth_type' => getenv('CPANEL_AUTH_TYPE'),\n    'token' => getenv('CPANEL_TOKEN'),\n    'user' => getenv('CPANEL_USER'),\n    'db_user' => getenv('CPANEL_DB_USER'),\n    'db_user_privileges' => getenv('CPANEL_DB_PRIVILEGES'),\n    'timeout' => 500,\n\n    'allowInStage' => ['staging', 'beta', 'alpha'],\n\n    'create_domain_format' => '%s-%s-%s',\n    'create_domain_values' => ['staging', 'master', get('application')],\n    'subdomain_prefix' => substr(md5(get('application')), 0,4) . '-',\n    'subdomain_suffix' => getenv('SUDOMAIN_SUFFIX'),\n\n\n    'create_db_format' => '%s_%s-%s-%s',\n    'create_db_values' => ['apps', 'staging','master', get('application')],\n\n]);\n```\n\n- `cpanel` – array with configuration for cPanel\n    - `username` – WHM account\n    - `user` – cPanel account that you want in charge of the domain\n    - `token` – WHM API token\n    - `create_domain_format` – Format for name creation of domain\n    - `create_domain_values` – The actual value reference for naming\n    - `subdomain_prefix` – cPanel has a weird way of dealing with addons and subdomains, you cannot create 2 addons with the same subdomain, so you need to change it in some way, example uses first 4 chars of md5(app_name)\n    - `subdomain_suffix` – cPanel has a weird way of dealing with addons and subdomains, so the suffix needs to be your main domain for that account for deletion purposes\n    - `addondir` – addon dir is different from the deploy path because cPanel \"injects\" /home/user/ into the path, so tilde cannot be used\n    - `allowInStage` – Define the stages that cPanel recipe actions are allowed in\n\n\n#### .env file example\n```\nCPANEL_HOST=xxx.xxx.xxx.xxx\nCPANEL_PORT=2087\nCPANEL_USERNAME=root\nCPANEL_TOKEN=xxxx\nCPANEL_USER=xxx\nCPANEL_AUTH_TYPE=hash\nCPANEL_DB_USER=db_user\nCPANEL_DB_PRIVILEGES=\"ALL PRIVILEGES\"\nSUDOMAIN_SUFFIX=.mymaindomain.com\n\n```\n\n### Tasks\n\n- `cpanel:createaddondomain` Creates an addon domain\n- `cpanel:deleteaddondomain` Removes an addon domain\n- `cpanel:createdb` Creates a new database\n\n### Usage\n\nA complete example with configs, staging and deployment\n\n```\n<?php\n\nnamespace Deployer;\nuse Dotenv\\Dotenv;\n\nrequire 'vendor/autoload.php';\n(Dotenv::create(__DIR__))->load(); // this is used just so an .env file can be used for credentials\n\nrequire 'cpanel.php';\n\n\n// Project name\nset('application', 'myproject.com');\n// Project repository\nset('repository', 'git@github.com:myorg/myproject.com');\n\n\n\n\n\nset('cpanel', [\n    'host' => getenv('CPANEL_HOST'),\n    'port' => getenv('CPANEL_PORT'),\n    'username' => getenv('CPANEL_USERNAME'),\n    'auth_type' => getenv('CPANEL_AUTH_TYPE'),\n    'token' => getenv('CPANEL_TOKEN'),\n    'user' => getenv('CPANEL_USER'),\n    'db_user' => getenv('CPANEL_DB_USER'),\n    'db_user_privileges' => getenv('CPANEL_DB_PRIVILEGES'),\n    'timeout' => 500,\n    'allowInStage' => ['staging', 'beta', 'alpha'],\n\n    'create_domain_format' => '%s-%s-%s',\n    'create_domain_values' => ['staging', 'master', get('application')],\n    'subdomain_prefix' => substr(md5(get('application')), 0,4) . '-',\n    'subdomain_suffix' => getenv('SUDOMAIN_SUFFIX'),\n\n\n    'create_db_format' => '%s_%s-%s-%s',\n    'create_db_values' => ['apps', 'staging','master', get('application')],\n\n]);\n\nhost('myproject.com')\n    ->stage('staging')\n    ->set('cpanel_createdb', vsprintf(get('cpanel')['create_db_format'], get('cpanel')['create_db_values']))\n    ->set('branch', 'dev-branch')\n    ->set('deploy_path',  '~/staging/' . vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values']))\n    ->set('addondir',  'staging/' . vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values']));\n// Tasks\ntask('build', function () {\n    run('cd {{release_path}} && build');\n});\n\nafter('deploy:prepare', 'cpanel:createaddondomain');\nafter('deploy:prepare', 'cpanel:createdb');\n```\n */\n\nnamespace Deployer;\n\nuse Deployer\\Task\\Context;\nuse Gufy\\CpanelPhp\\Cpanel;\n\n/**\n * @return Cpanel\n * @throws Exception\\Exception\n */\nfunction getCpanel()\n{\n    $config = get('cpanel', []);\n    $allowInStage = $config['allowInStage'];\n    $stage = input()->getArgument('stage');\n\n    if (!class_exists('\\Gufy\\CpanelPhp\\Cpanel')) {\n        throw new \\RuntimeException(\"<comment>Please install php package</comment> <info>gufy/cpanel-php</info> <comment>to use CPanel API</comment>\");\n    }\n\n    if (!in_array($stage, $allowInStage)) {\n        throw new \\RuntimeException(sprintf(\"Since it creates addon domains and databases, CPanel recipe is available only in the %s environments\", implode($allowInStage)));\n    }\n\n\n    if (!is_array($config) ||\n        !isset($config['host']) ||\n        !isset($config['port']) ||\n        !isset($config['username']) ||\n        !isset($config['token']) ||\n        !isset($config['user'])) {\n        throw new \\RuntimeException(\"<comment>Please configure CPanel config:</comment> <info>set('cpanel', array('host' => 'xxx.xxx.xxx.xxx:', 'port' => 2087 , 'username' => 'root', 'token' => 'asdfasdf', 'cpaneluser' => 'guy'));</info>\");\n    }\n\n    $cpanel = new Cpanel([\n        'host'        =>  'https://' . $config['host'] . ':' . $config['port'],\n        'username'    =>  $config['username'],\n        'auth_type'   =>  $config['auth_type'],\n        'password'    =>  $config['token'],\n    ]);\n\n    $cpanel->setTimeout($config['timeout']);\n\n    return $cpanel;\n}\n\nfunction getDomainInfo()\n{\n    $domain = vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values']);\n    $cleanDomain = str_replace(['.', ',', ' ', '/', '-'], '', $domain);\n    $subDomain = get('cpanel')['subdomain_prefix'] . $cleanDomain;\n\n    return [\n        'domain' => $domain,\n        'subDomain' => $subDomain,\n        'subDomainWithSuffix' => $subDomain . get('cpanel')['subdomain_suffix'],\n    ];\n}\n\ndesc('Creates database though CPanel API');\ntask('cpanel:createdb', function () {\n\n    $cpanel = getCPanel();\n    $config = get('cpanel', []);\n    if (!askConfirmation(sprintf('This will try to create the database %s on the host though CPanel API, ok?', get('cpanel_createdb')), true)) {\n        return;\n    }\n\n    $createDbDataResult = $cpanel->cpanel('MysqlFE', 'createdb', $config['user'], ['db' => get('cpanel_createdb')]);\n    $addPrivilegesDataResult = $cpanel->cpanel('MysqlFE', 'setdbuserprivileges', $config['user'], ['privileges' => $config['db_user_privileges'], 'db' => get('cpanel_createdb'), 'dbuser' => $config['db_user']]);\n\n    $createDbData = json_decode($createDbDataResult, true);\n    $addPrivilegesData = json_decode($addPrivilegesDataResult, true);\n\n    if (isset($createDbData['cpanelresult']['error'])) {\n        writeln($createDbData['cpanelresult']['error']);\n    } else {\n        writeln('Successfully created database!');\n    }\n\n    if (isset($addPrivilegesData['cpanelresult']['error'])) {\n        writeln($addPrivilegesData['cpanelresult']['error']);\n    } else {\n        writeln('Successfully added privileges to database!');\n    }\n});\n\ndesc('Creates addon domain though CPanel API');\ntask('cpanel:createaddondomain', function () {\n    $cpanel = getCPanel();\n    $config = get('cpanel', []);\n    $domain = getDomainInfo()['domain'];\n    $subDomain = getDomainInfo()['subDomain'];\n    if (!askConfirmation(sprintf('This will try to create the addon domain %s and point it to %s and subdomain %s, ok?', $domain, get('addondir'), $subDomain), true)) {\n        return;\n    }\n\n    writeln(sprintf('Creates addon domain %s and pointing it to %s', $domain, get('addondir')));\n\n    $addAddonDomainResult = $cpanel->cpanel('AddonDomain', 'addaddondomain', $config['user'], ['dir' => get('addondir'), 'newdomain' => $domain, 'subdomain' => $subDomain]);\n    $addAddonDomainData = json_decode($addAddonDomainResult, true);\n\n    if (isset($addAddonDomainResult['cpanelresult']['error'])) {\n        writeln($addAddonDomainData['cpanelresult']['error']);\n    } else {\n        writeln('Successfully created addon domain!');\n        writeln($addAddonDomainData['cpanelresult']['data'][0]['reason']);\n    }\n});\n\ndesc('Deletes addon domain though CPanel API');\ntask('cpanel:deleteaddondomain', function () {\n    $cpanel = getCPanel();\n    $config = get('cpanel', []);\n    $domain = getDomainInfo()['domain'];\n    $subDomain = getDomainInfo()['subDomain'];\n    $subDomainWithSuffix = getDomainInfo()['subDomainWithSuffix'];\n\n    if (!askConfirmation(sprintf('This will delete the addon domain %s with corresponding subdomain %s, ok?', $domain, $subDomain), true)) {\n        return;\n    }\n\n    writeln(sprintf('Deleting addon domain %s', $domain));\n\n    $delAddonDomainResult = $cpanel->cpanel('AddonDomain', 'deladdondomain', $config['user'], ['domain' => $domain, 'subdomain' => $subDomainWithSuffix]);\n    $delAddonDomainResult = json_decode($delAddonDomainResult, true);\n\n    if (isset($delAddonDomainResult['cpanelresult']['error'])) {\n        writeln($delAddonDomainResult['cpanelresult']['error']);\n    } else {\n        writeln('Successfully deleted addon domain!');\n        writeln($delAddonDomainResult['cpanelresult']['data'][0]['reason']);\n    }\n});\n"
  },
  {
    "path": "contrib/crontab.php",
    "content": "<?php\n/*\nRecipe for adding crontab jobs.\n\nThis recipe creates a new section in the crontab file with the configured jobs.\nThe section is identified by the *crontab:identifier* variable, by default the application name.\n\n## Configuration\n\n- *crontab:jobs* - An array of strings with crontab lines.\n\n## Usage\n\n```php\nrequire 'contrib/crontab.php';\n\nafter('deploy:success', 'crontab:sync');\n\nadd('crontab:jobs', [\n    '* * * * * cd {{current_path}} && {{bin/php}} artisan schedule:run >> /dev/null 2>&1',\n]);\n```\n */\n\nnamespace Deployer;\n\nuse function Deployer\\Support\\escape_shell_argument;\n\n// Get path to bin\nset('bin/crontab', function () {\n    return which('crontab');\n});\n\n// Set the identifier used in the crontab, application name by default\nset('crontab:identifier', function () {\n    return get('application', 'application');\n});\n\n// Use sudo to run crontab. When running crontab with sudo, you can use the `-u` parameter to change a crontab for a different user.\nset('crontab:use_sudo', false);\n\ndesc('Sync crontab jobs');\ntask('crontab:sync', function () {\n    $cronJobsLocal = array_map(\n        fn($job) => parse($job),\n        get('crontab:jobs', []),\n    );\n\n    if (count($cronJobsLocal) == 0) {\n        writeln(\"Nothing to sync - configure crontab:jobs\");\n        return;\n    }\n\n    $cronJobs = getRemoteCrontab();\n    $identifier = get('crontab:identifier');\n    $sectionStart = \"###< $identifier\";\n    $sectionEnd = \"###> $identifier\";\n\n    // find our cronjob section\n    $start = array_search($sectionStart, $cronJobs);\n    $end = array_search($sectionEnd, $cronJobs);\n\n    if ($start === false || $end === false) {\n        // Move the duplicates over when first generating the section\n        foreach ($cronJobs as $index => $cronJob) {\n            if (in_array($cronJob, $cronJobsLocal)) {\n                unset($cronJobs[$index]);\n                writeln(\"Crontab: Found existing job in crontab, moving it to the section\");\n            }\n        }\n\n        // Create the section\n        $cronJobs[] = $sectionStart;\n        $cronJobs = [...$cronJobs, ...$cronJobsLocal];\n        $cronJobs[] = $sectionEnd;\n        writeln(\"Crontab: Found no section, created the section with configured jobs\");\n    } else {\n        // Replace the existing section\n        array_splice($cronJobs, $start + 1, $end - $start - 1, $cronJobsLocal);\n        writeln(\"Crontab: Found existing section, replaced with configured jobs\");\n    }\n\n    setRemoteCrontab($cronJobs);\n});\n\ndesc('Remove crontab jobs');\ntask('crontab:remove', function () {\n    $cronJobsLocal = array_map(\n        fn($job) => parse($job),\n        get('crontab:jobs', []),\n    );\n\n    $cronJobs = getRemoteCrontab();\n    $identifier = get('crontab:identifier');\n    $sectionStart = \"###< $identifier\";\n    $sectionEnd = \"###> $identifier\";\n\n    // Find our cronjob section\n    $start = array_search($sectionStart, $cronJobs);\n    $end = array_search($sectionEnd, $cronJobs);\n\n    if ($start && $end) {\n        // Remove the existing section\n        array_splice($cronJobs, $start + 1, $end - $start - 1);\n        writeln(\"Crontab: Found existing section, removed jobs\");\n    } elseif (count($cronJobsLocal) > 0) {\n        $foundJobs = false;\n        // Remove individual jobs if no section is present\n        foreach ($cronJobs as $index => $cronJob) {\n            if (in_array($cronJob, $cronJobsLocal)) {\n                unset($cronJobs[$index]);\n                $foundJobs = true;\n            }\n        }\n        if ($foundJobs) {\n            writeln(\"Crontab: Found existing jobs in crontab, removed jobs\");\n        } else {\n            writeln(\"Crontab: No existing jobs in crontab, skipping\");\n            return;\n        }\n    } else {\n        writeln(\"Crontab: Found no section and crontab:jobs is not configured, skipping\");\n        return;\n    }\n\n    setRemoteCrontab($cronJobs);\n});\n\nfunction setRemoteCrontab(array $lines): void\n{\n    $sudo = get('crontab:use_sudo') ? 'sudo' : '';\n\n    $tmpCrontabPath = sprintf('/tmp/%s', uniqid('crontab_save_'));\n\n    if (test(\"[ -f '$tmpCrontabPath' ]\")) {\n        run(\"unlink '$tmpCrontabPath'\");\n    }\n\n    foreach ($lines as $line) {\n        run(\"echo \" . escape_shell_argument($line) . \" >> $tmpCrontabPath\");\n    }\n\n    run(\"$sudo {{bin/crontab}} \" . $tmpCrontabPath);\n    run('unlink ' . $tmpCrontabPath);\n}\n\nfunction getRemoteCrontab(): array\n{\n    $sudo = get('crontab:use_sudo') ? 'sudo' : '';\n\n    if (!test(\"$sudo {{bin/crontab}} -l >> /dev/null 2>&1\")) {\n        return [];\n    }\n\n    return explode(PHP_EOL, run(\"$sudo {{bin/crontab}} -l\"));\n}\n"
  },
  {
    "path": "contrib/directadmin.php",
    "content": "<?php\n/*\n### Configuration\n- `directadmin` – array with configuration for DirectAdmin\n    - `host` – DirectAdmin host\n    - `port` – DirectAdmin port (default: 2222, not required)\n    - `scheme` – DirectAdmin scheme (default: http, not required)\n    - `username` – DirectAdmin username\n    - `password` – DirectAdmin password (it is recommended to use login keys!)\n    - `db_user` – Database username (required when using directadmin:createdb or directadmin:deletedb)\n    - `db_name` – Database namse (required when using directadmin:createdb)\n    - `db_password` – Database password (required when using directadmin:createdb)\n    - `domain_name` – Domain to create, delete or edit (required when using directadmin:createdomain, directadmin:deletedomain, directadmin:symlink-private-html or directadmin:php-version)\n    - `domain_ssl` – Enable SSL, options: ON/OFF, default: ON (optional when using directadmin:createdb)\n    - `domain_cgi` – Enable CGI, options: ON/OFF, default: ON (optional when using directadmin:createdb)\n    - `domain_php` – Enable PHP, options: ON/OFF, default: ON (optional when using directadmin:createdb)\n    - `domain_php_version` – Domain PHP Version, default: 1 (required when using directadmin:php-version)\n\n */\n\nnamespace Deployer;\n\nuse Deployer\\Task\\Context;\nuse Deployer\\Utility\\Httpie;\n\n/**\n * getDirectAdminConfig\n *\n * @return array\n */\nfunction getDirectAdminConfig()\n{\n    $config = get('directadmin', []);\n\n    if (!is_array($config) ||\n        !isset($config['host']) ||\n        !isset($config['username']) ||\n        !isset($config['password'])) {\n        throw new \\RuntimeException(\"Please set the following DirectAdmin config:\" . PHP_EOL . \"set('directadmin', ['host' => '127.0.0.1', 'port' => 2222, 'username' => 'admin', 'password' => 'password']);\");\n    }\n\n    return $config;\n}\n\n/**\n * DirectAdmin\n *\n * @param string $action\n * @param array $data\n *\n * @return void\n */\nfunction DirectAdmin(string $action, array $data = [])\n{\n    $config = getDirectAdminConfig();\n    $scheme = $config['scheme'] ?? 'http';\n    $port = $config['port'] ?? 2222;\n\n    $result = Httpie::post(sprintf('%s://%s:%s/%s', $scheme, $config['host'], $port, $action))\n        ->formBody($data)\n        ->setopt(CURLOPT_USERPWD, $config['username'] . ':' . $config['password'])\n        ->send();\n\n    parse_str($result, $resultData);\n\n    if ($resultData['error'] === '1') {\n        $resultData['details'] = trim($resultData['details']);\n        $resultData['details'] = str_replace(['\\\\n', '\\\\r'], '', $resultData['details']);\n        $resultData['details'] = strip_tags($resultData['details']);\n\n        writeln('<error>DirectAdmin message: ' . $resultData['details'] . '</error>');\n    }\n}\n\ndesc('Creates a database on DirectAdmin');\ntask('directadmin:createdb', function () {\n    $config = getDirectAdminConfig();\n\n    if (!is_array($config) ||\n        !isset($config['db_name']) ||\n        !isset($config['db_user']) ||\n        !isset($config['db_password'])) {\n        throw new \\RuntimeException(\"Please add the following DirectAdmin config:\" . PHP_EOL . \"add('directadmin', ['db_name' => 'test', 'db_user' => 'test', 'db_password' => '123456']);\");\n    }\n\n    DirectAdmin('CMD_API_DATABASES', [\n        'action' => 'create',\n        'name' => $config['db_name'],\n        'user' => $config['db_user'],\n        'passwd' => $config['db_password'],\n        'passwd2' => $config['db_password'],\n    ]);\n});\n\ndesc('Deletes a database on DirectAdmin');\ntask('directadmin:deletedb', function () {\n    $config = getDirectAdminConfig();\n\n    if (!is_array($config) ||\n        !isset($config['db_user'])) {\n        throw new \\RuntimeException(\"Please add the following DirectAdmin config:\" . PHP_EOL . \"add('directadmin', ['db_user' => 'test_database']);\");\n    }\n\n    DirectAdmin('CMD_API_DATABASES', [\n        'action' => 'delete',\n        'select0' => $config['username'] . '_' . $config['db_user'],\n    ]);\n});\n\ndesc('Creates a domain on DirectAdmin');\ntask('directadmin:createdomain', function () {\n    $config = getDirectAdminConfig();\n\n    if (!is_array($config) ||\n        !isset($config['domain_name'])) {\n        throw new \\RuntimeException(\"Please add the following DirectAdmin config:\" . PHP_EOL . \"add('directadmin', ['domain_name' => 'test.example.com']);\");\n    }\n\n    DirectAdmin('CMD_API_DOMAIN', [\n        'action' => 'create',\n        'domain' => $config['domain_name'],\n        'ssl' => $config['domain_ssl'] ?? 'On',\n        'cgi' => $config['domain_cgi'] ?? 'ON',\n        'php' => $config['domain_php'] ?? 'ON',\n    ]);\n});\n\ndesc('Deletes a domain on DirectAdmin');\ntask('directadmin:deletedomain', function () {\n    $config = getDirectAdminConfig();\n\n    if (!is_array($config) ||\n        !isset($config['domain_name'])) {\n        throw new \\RuntimeException(\"Please add the following DirectAdmin config:\" . PHP_EOL . \"add('directadmin', ['domain_name' => 'test.example.com']);\");\n    }\n\n    DirectAdmin('CMD_API_DOMAIN', [\n        'delete' => 'anything',\n        'confirmed' => 'anything',\n        'select0' => $config['domain_name'],\n    ]);\n});\n\ndesc('Symlink your private_html to public_html');\ntask('directadmin:symlink-private-html', function () {\n    $config = getDirectAdminConfig();\n\n    if (!is_array($config) ||\n        !isset($config['domain_name'])) {\n        throw new \\RuntimeException(\"Please add the following DirectAdmin config:\" . PHP_EOL . \"add('directadmin', ['domain_name' => 'test.example.com']);\");\n    }\n\n    DirectAdmin('CMD_API_DOMAIN', [\n        'action' => 'private_html',\n        'domain' => $config['domain_name'],\n        'val' => 'symlink',\n    ]);\n});\n\ndesc('Changes the PHP version from a domain');\ntask('directadmin:php-version', function () {\n    $config = getDirectAdminConfig();\n\n    if (!is_array($config) ||\n        !isset($config['domain_name']) ||\n        !isset($config['domain_php_version'])) {\n        throw new \\RuntimeException(\"Please add the following DirectAdmin config:\" . PHP_EOL . \"add('directadmin', ['domain_name' => 'test.example.com', 'domain_php_version' => 1]);\");\n    }\n\n    DirectAdmin('CMD_API_DOMAIN', [\n        'action' => 'php_selector',\n        'domain' => $config['domain_name'],\n        'php1_select' => $config['domain_php_version'],\n    ]);\n});\n"
  },
  {
    "path": "contrib/discord.php",
    "content": "<?php\n/*\n## Installing\n\nAdd hook on deploy:\n\n```php\nbefore('deploy', 'discord:notify');\n```\n\n## Configuration\n\n- `discord_channel` – Discord channel ID, **required**\n- `discord_token` – Discord channel token, **required**\n\n- `discord_notify_text` – notification message template, markdown supported, default:\n  ```markdown\n  :information_source: **{{user}}** is deploying branch `{{branch}}` to _{{where}}_\n  ```\n- `discord_success_text` – success template, default:\n  ```markdown\n  :white_check_mark: Branch `{{branch}}` deployed to _{{where}}_ successfully\n  ```\n- `discord_failure_text` – failure template, default:\n  ```markdown\n  :no_entry_sign: Branch `{{branch}}` has failed to deploy to _{{where}}_\n\n## Usage\n\nIf you want to notify only about beginning of deployment add this line only:\n\n```php\nbefore('deploy', 'discord:notify');\n```\n\nIf you want to notify about successful end of deployment add this too:\n\n```php\nafter('deploy:success', 'discord:notify:success');\n```\n\nIf you want to notify about failed deployment add this too:\n\n```php\nafter('deploy:failed', 'discord:notify:failure');\n```\n */\n\nnamespace Deployer;\n\nuse Deployer\\Task\\Context;\nuse Deployer\\Utility\\Httpie;\n\nset('discord_webhook', function () {\n    return 'https://discordapp.com/api/webhooks/{{discord_channel}}/{{discord_token}}/slack';\n});\n\n// Deploy messages\nset('discord_notify_text', function () {\n    return [\n        'text' => parse(':information_source: **{{user}}** is deploying branch `{{what}}` to _{{where}}_'),\n    ];\n});\nset('discord_success_text', function () {\n    return [\n        'text' => parse(':white_check_mark: Branch `{{what}}` deployed to _{{where}}_ successfully'),\n    ];\n});\nset('discord_failure_text', function () {\n    return [\n        'text' => parse(':no_entry_sign: Branch `{{what}}` has failed to deploy to _{{where}}_'),\n    ];\n});\n\n// The message\nset('discord_message', 'discord_notify_text');\n\n// Helpers\ntask('discord_send_message', function () {\n    $message = get(get('discord_message'));\n\n    Httpie::post(get('discord_webhook'))->jsonBody($message)->send();\n});\n\n// Tasks\ndesc('Tests messages');\ntask('discord:test', function () {\n    set('discord_message', 'discord_notify_text');\n    invoke('discord_send_message');\n    set('discord_message', 'discord_success_text');\n    invoke('discord_send_message');\n    set('discord_message', 'discord_failure_text');\n    invoke('discord_send_message');\n})\n    ->once();\n\ndesc('Notifies Discord');\ntask('discord:notify', function () {\n    set('discord_message', 'discord_notify_text');\n    invoke('discord_send_message');\n})\n    ->once()\n    ->isHidden();\n\ndesc('Notifies Discord about deploy finish');\ntask('discord:notify:success', function () {\n    set('discord_message', 'discord_success_text');\n    invoke('discord_send_message');\n})\n    ->once()\n    ->isHidden();\n\ndesc('Notifies Discord about deploy failure');\ntask('discord:notify:failure', function () {\n    set('discord_message', 'discord_failure_text');\n    invoke('discord_send_message');\n})\n    ->once()\n    ->isHidden();\n"
  },
  {
    "path": "contrib/grafana.php",
    "content": "<?php\n/*\n\n## Configuration options\n\n- **url** *(required)*: the URL to the creates annotation api endpoint.\n- **token** *(required)*: authentication token. Can be created at Grafana Console.\n- **time** *(optional)* – set deploy time of annotation. specify epoch milliseconds. (Defaults is set to the current time in epoch milliseconds.)\n- **tags** *(optional)* – set tag of annotation.\n- **text** *(optional)* – set text of annotation. (Defaults is set to \"Deployed \" + git log -n 1 --format=\"%h\")\n\n```php\n// deploy.php\n\nset('grafana', [\n    'token' => 'eyJrIj...',\n    'url' => 'http://grafana/api/annotations',\n    'tags' => ['deploy', 'production'],\n]);\n\n```\n\n## Usage\n\nIf you want to create annotation about successful end of deployment.\n\n```php\nafter('deploy:success', 'grafana:annotation');\n```\n\n*/\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\ndesc('Creates Grafana annotation of deployment');\ntask('grafana:annotation', function () {\n    $defaultConfig = [\n        'url' => null,\n        'token' => null,\n        'time' => round(microtime(true) * 1000),\n        'tags' => [],\n        'text' => null,\n    ];\n\n    $config = array_merge($defaultConfig, (array) get('grafana'));\n    if (!is_array($config) || !isset($config['url']) || !isset($config['token'])) {\n        throw new \\RuntimeException(\"Please configure Grafana: set('grafana', ['url' => 'https://localhost/api/annotations', token' => 'eyJrIjo...']);\");\n    }\n\n    $params = [\n        'time' => $config['time'],\n        'isRegion' => false,\n        'tags' => $config['tags'],\n        'text' => $config['text'],\n    ];\n    if (!isset($params['text'])) {\n        $params['text'] = 'Deployed ' . trim(runLocally('git log -n 1 --format=\"%h\"'));\n    }\n\n    Httpie::post($config['url'])\n        ->header('Authorization', 'Bearer ' . $config['token'])\n        ->jsonBody($params)\n        ->send();\n});\n"
  },
  {
    "path": "contrib/hangouts.php",
    "content": "<?php\n/*\n\nAdd hook on deploy:\n\n```php\nbefore('deploy', 'chat:notify');\n```\n\n## Configuration\n\n- `chat_webhook` – chat incoming webhook url, **required**\n- `chat_title` – the title of your notification card, default `{{application}}`\n- `chat_subtitle` – the subtitle of your card, default `{{hostname}}`\n- `chat_favicon` – an image for the header of your card, default `http://{{hostname}}/favicon.png`\n- `chat_line1` – first line of the text in your card, default: `{{branch}}`\n- `chat_line2` – second line of the text in your card, default: `{{stage}}`\n\n## Usage\n\nIf you want to notify only about beginning of deployment add this line only:\n\n```php\nbefore('deploy', 'chat:notify');\n```\n\nIf you want to notify about successful end of deployment add this too:\n\n```php\nafter('deploy:success', 'chat:notify:success');\n```\n\nIf you want to notify about failed deployment add this too:\n\n```php\nafter('deploy:failed', 'chat:notify:failure');\n```\n\n */\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\n// Title of project\nset('chat_title', function () {\n    return get('application', 'Project');\n});\n\nset('chat_subtitle', get('hostname'));\n\n// If 'favicon' is set Google Hangouts Chat will decorate your card with an image.\nset('favicon', 'http://{{hostname}}/favicon.png');\n\n// Deploy messages\nset('chat_line1', '{{branch}}');\nset('chat_line2', '{{stage}}');\n\ndesc('Notifies Google Hangouts Chat');\ntask('chat:notify', function () {\n    if (!get('chat_webhook', false)) {\n        return;\n    }\n\n    $card = [\n        'header' => [\n            'title' => get('chat_title'),\n            'subtitle' => get('chat_subtitle'),\n            'imageUrl' => get('favicon'),\n            'imageStyle' => 'IMAGE',\n        ],\n        'sections' => [\n            'widgets' => [\n                'keyValue' => [\n                    'topLabel' => get('chat_line1'),\n                    'content' => get('chat_line2'),\n                    'contentMultiline' => false,\n                    'bottomLabel' => 'started',\n                    // Use 'iconUrl' to set a custom icon URL (png)\n                    'icon' => 'CLOCK',\n                    'button' => [\n                        'textButton' => [\n                            'text' => 'Visit site',\n                            'onClick' => [\n                                'openLink' => [\n                                    'url' => get('hostname'),\n                                ],\n                            ],\n                        ],\n                    ],\n                ],\n            ],\n        ],\n    ];\n\n    Httpie::post(get('chat_webhook'))->jsonBody(['cards' => $card])->send();\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies Google Hangouts Chat about deploy finish');\ntask('chat:notify:success', function () {\n    if (!get('chat_webhook', false)) {\n        return;\n    }\n\n    $card = [\n        'header' => [\n            'title' => get('chat_title'),\n            'subtitle' => get('chat_subtitle'),\n            'imageUrl' => get('favicon'),\n            'imageStyle' => 'IMAGE',\n        ],\n        'sections' => [\n            'widgets' => [\n                'keyValue' => [\n                    'topLabel' => get('chat_line1'),\n                    'content' => get('chat_line2'),\n                    'contentMultiline' => false,\n                    'bottomLabel' => 'succeeded',\n                    // Use 'iconUrl' to set a custom icon URL (png)\n                    'icon' => 'STAR',\n                    'button' => [\n                        'textButton' => [\n                            'text' => 'Visit site',\n                            'onClick' => [\n                                'openLink' => [\n                                    'url' => get('hostname'),\n                                ],\n                            ],\n                        ],\n                    ],\n                ],\n            ],\n        ],\n    ];\n\n    Httpie::post(get('chat_webhook'))->jsonBody(['cards' => $card])->send();\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies Google Hangouts Chat about deploy failure');\ntask('chat:notify:failure', function () {\n    if (!get('chat_webhook', false)) {\n        return;\n    }\n\n    $card = [\n        'header' => [\n            'title' => get('chat_title'),\n            'subtitle' => get('chat_subtitle'),\n            'imageUrl' => get('favicon'),\n            'imageStyle' => 'IMAGE',\n        ],\n        'sections' => [\n            'widgets' => [\n                'keyValue' => [\n                    'topLabel' => get('chat_line1'),\n                    'content' => get('chat_line2'),\n                    'contentMultiline' => false,\n                    'bottomLabel' => 'failed',\n                    // Use 'iconUrl' to set a custom icon URL (png)\n                    // or use 'icon' and pick from this list:\n                    // https://developers.google.com/hangouts/chat/reference/message-formats/cards#customicons\n                    'button' => [\n                        'textButton' => [\n                            'text' => 'Visit site',\n                            'onClick' => [\n                                'openLink' => [\n                                    'url' => get('hostname'),\n                                ],\n                            ],\n                        ],\n                    ],\n                ],\n            ],\n        ],\n    ];\n\n    Httpie::post(get('chat_webhook'))->jsonBody(['cards' => $card])->send();\n})\n    ->once()\n    ->hidden();\n"
  },
  {
    "path": "contrib/hipchat.php",
    "content": "<?php\n/*\n## Configuration\n\n- `hipchat_token` – Hipchat V1 auth token\n- `hipchat_room_id` – Room ID or name\n- `hipchat_message` –  Deploy message, default is `_{{user}}_ deploying `{{what}}` to *{{where}}*`\n- `hipchat_from` – Default to target\n- `hipchat_color` – Message color, default is **green**\n- `hipchat_url` –  The URL to the message endpoint, default is https://api.hipchat.com/v1/rooms/message\n\n## Usage\n\nSince you should only notify Hipchat room of a successful deployment, the `hipchat:notify` task should be executed right at the end.\n\n```php\nafter('deploy', 'hipchat:notify');\n```\n\n */\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\nset('hipchat_color', 'green');\nset('hipchat_from', '{{where}}');\nset('hipchat_message', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\nset('hipchat_url', 'https://api.hipchat.com/v1/rooms/message');\n\ndesc('Notifies Hipchat channel of deployment');\ntask('hipchat:notify', function () {\n    $params = [\n        'room_id' => get('hipchat_room_id'),\n        'from' => get('target'),\n        'message' => get('hipchat_message'),\n        'color' => get('hipchat_color'),\n        'auth_token' => get('hipchat_token'),\n        'notify' => 0,\n        'format' => 'json',\n    ];\n\n    Httpie::get(get('hipchat_url'))\n        ->query($params)\n        ->send();\n});\n"
  },
  {
    "path": "contrib/ispmanager.php",
    "content": "<?php\n/*\n * This recipe for work with ISPManager Lite panel by API.\n */\n\nnamespace Deployer;\n\nuse Deployer\\Exception\\Exception;\nuse Deployer\\Utility\\Httpie;\n\nset('ispmanager_owner', 'www-root');\nset('ispmanager_doc_root', '/var/www/' . get('ispmanager_owner') . '/data/');\n\n// ISPManager default configuration\nset('ispmanager', [\n    'api' => [\n        'dsn' => 'https://root:password@localhost:1500/ispmgr',\n        'secure' => true,\n    ],\n    'createDomain' => null,\n    'updateDomain' => null,\n    'deleteDomain' => null,\n    'createDatabase' => null,\n    'deleteDatabase' => null,\n    'phpSelect' => null,\n    'createAlias' => null,\n    'deleteAlias' => null,\n]);\n\n// Vhost default configuration\nset('vhost', [\n    'name' => '{{domain}}',\n    'php_enable' => 'on',\n    'aliases' => 'www.{{domain}}',\n    'home' => 'www/{{domain}}',\n    'owner' => get('ispmanager_owner'),\n    'email' => 'webmaster@{{domain}}',\n    'charset' => 'off',\n    'dirindex' => 'index.php uploaded.html',\n    'ssi' => 'on',\n    'php' => 'on',\n    'php_mode' => 'php_mode_mod',\n    'basedir' => 'on',\n    'php_apache_version' => 'native',\n    'cgi' => 'off',\n    'log_access' => 'on',\n    'log_error' => 'on',\n]);\n\n// Storage\nset('ispmanager_session', '');\nset('ispmanager_databases', [\n    'servers' => [],\n    'hosts' => [],\n    'dblist' => [],\n]);\n\nset('ispmanager_domains', []);\nset('ispmanager_phplist', []);\nset('ispmanager_aliaslist', []);\n\ndesc('Installs ispmanager');\ntask('ispmanager:init', function () {\n    $config = get('ispmanager');\n\n    if (!is_null($config['createDatabase']) || !is_null($config['deleteDatabase'])) {\n        invoke('ispmanager:db-server-list');\n        invoke('ispmanager:db-list');\n    }\n\n    if (!is_null($config['createDomain']) || !is_null($config['deleteDomain'])) {\n        invoke('ispmanager:domain-list');\n    }\n\n    if (!is_null($config['phpSelect'])) {\n        invoke('ispmanager:domain-list');\n        invoke('ispmanager:get-php-list');\n    }\n\n    if (!is_null($config['createAlias']) || !is_null($config['deleteAlias'])) {\n        invoke('ispmanager:domain-list');\n    }\n});\n\ndesc('Takes database servers list');\ntask('ispmanager:db-server-list', function () {\n    $response = ispmanagerRequest('get', [\n        'func' => 'db.server',\n    ]);\n\n    $hostList = [];\n    $serverList = [];\n\n    if (isset($response['doc']['elem']) && count($response['doc']['elem']) > 0) {\n        foreach ($response['doc']['elem'] as $dbServer) {\n            $serverList[$dbServer['name']['$']] = [\n                'host' => $dbServer['host']['$'],\n                'name' => $dbServer['name']['$'],\n                'version' => $dbServer['savedver']['$'],\n            ];\n\n            if (!strpos($dbServer['host']['$'], ':')) {\n                $dbHost = $dbServer['host']['$'] . ':3306';\n            } else {\n                $dbHost = $dbServer['host']['$'];\n            }\n\n            $hostList[$dbHost] = [\n                'host' => $dbHost,\n                'name' => $dbServer['name']['$'],\n                'version' => $dbServer['savedver']['$'],\n            ];\n        }\n    }\n\n    add('ispmanager_databases', [\n        'servers' => $serverList,\n        'hosts' => $hostList,\n    ]);\n});\n\ndesc('Takes databases list');\ntask('ispmanager:db-list', function () {\n    $response = ispmanagerRequest('get', [\n        'func' => 'db',\n    ]);\n\n    $dbList = [];\n    if (isset($response['doc']['elem']) && count($response['doc']['elem']) > 0) {\n        foreach ($response['doc']['elem'] as $db) {\n            $dbList[$db['pair']['$']] = [\n                'name' => $db['name']['$'],\n                'server' => $db['server']['$'],\n                'location' => $db['pair']['$'],\n            ];\n        }\n    }\n\n    add('ispmanager_databases', [\n        'dblist' => $dbList,\n    ]);\n});\n\ndesc('Takes domain list');\ntask('ispmanager:domain-list', function () {\n    $response = ispmanagerRequest('get', [\n        'func' => 'webdomain',\n    ]);\n\n    $domainList = [];\n    if (isset($response['doc']['elem']) && count($response['doc']['elem']) > 0) {\n        foreach ($response['doc']['elem'] as $domain) {\n            $domainList[] = $domain['name']['$'];\n        }\n    }\n\n    add('ispmanager_domains', $domainList);\n});\n\ndesc('Creates new database');\ntask('ispmanager:db-create', function () {\n    $config = get('ispmanager');\n\n    if (is_null($config['createDatabase'])) {\n        warning('Action for database create is not active');\n        return;\n    }\n\n    $dsnData = parse_url($config['createDatabase']['dsn']);\n\n    $dbInfo = get('ispmanager_databases');\n\n    $hostInfo = null;\n    foreach ($dbInfo['hosts'] as $hostData) {\n        if ($hostData['host'] == $dsnData['host'] . ':' . $dsnData['port']) {\n            $hostInfo = $hostData;\n            break;\n        }\n    }\n\n    if (is_null($hostInfo)) {\n        throw new Exception('Incorrect DB host');\n    }\n\n    $dbName = substr($dsnData['path'], 1);\n\n    $dbLocation = $dbName . '->' . $hostInfo['name'];\n\n    if (isset($dbInfo['dblist'][$dbLocation])) {\n        if (!isset($config['createDatabase']['skipIfExist']) || !$config['createDatabase']['skipIfExist']) {\n            throw new Exception('Database already exists!');\n        } else {\n            warning('Database already exists - skipping');\n            return;\n        }\n    }\n\n    $dbCreateRequest = [\n        'func' => 'db.edit',\n        'name' => $dbName,\n        'owner' => get('ispmanager_owner'),\n        'server' => $hostInfo['name'],\n        'charset' => $config['createDatabase']['charset'],\n        'sok' => 'ok',\n    ];\n\n    if ($dsnData['user'] == '*') {\n        $dbCreateRequest['user'] = '*';\n        $dbCreateRequest['username'] = $dbName;\n\n        if ($dsnData['pass'] == '*') {\n            $dbCreateRequest['password'] = generatePassword(8);\n        } else {\n            $dbCreateRequest['password'] = $dsnData['pass'];\n        }\n    } else {\n        $dbCreateRequest['user'] = $dsnData['user'];\n    }\n\n\n    $response = ispmanagerRequest('post', $dbCreateRequest);\n\n    if (isset($response['doc']['error']['msg']['$'])) {\n        throw new Exception($response['doc']['error']['msg']['$']);\n    } else {\n        info('Database successfully created');\n    }\n});\n\ndesc('Deletes database');\ntask('ispmanager:db-delete', function () {\n    $config = get('ispmanager');\n\n    if (is_null($config['deleteDatabase'])) {\n        warning('Action for database delete is not active');\n        return;\n    }\n\n    $dbInfo = get('ispmanager_databases');\n    $dsnData = parse_url($config['deleteDatabase']['dsn']);\n\n    $hostInfo = null;\n    foreach ($dbInfo['hosts'] as $hostData) {\n        if ($hostData['host'] == $dsnData['host'] . ':' . $dsnData['port']) {\n            $hostInfo = $hostData;\n            break;\n        }\n    }\n\n    if (is_null($hostInfo)) {\n        throw new Exception('Incorrect DB host');\n    }\n\n    $dbName = substr($dsnData['path'], 1);\n\n    $dbLocation = $dbName . '->' . $hostInfo['name'];\n\n    if (!isset($dbInfo['dblist'][$dbLocation])) {\n        if (!isset($config['deleteDatabase']['skipIfNotExist']) || !$config['deleteDatabase']['skipIfNotExist']) {\n            throw new Exception('Database not exist!');\n        } else {\n            warning('Database not exist - skipping');\n            return;\n        }\n    }\n\n    $dbDeleteRequest = [\n        'func' => 'db.delete',\n        'elid' => $dbLocation,\n    ];\n\n    $response = ispmanagerRequest('post', $dbDeleteRequest);\n\n    if (isset($response['doc']['error']['msg']['$'])) {\n        throw new Exception($response['doc']['error']['msg']['$']);\n    } else {\n        info('Database successfully deleted');\n    }\n});\n\ndesc('Creates new domain');\ntask('ispmanager:domain-create', function () {\n    $config = get('ispmanager');\n\n    if (is_null($config['createDomain'])) {\n        warning('Action for domain create is not active');\n        return;\n    }\n\n    if (!isset($config['createDomain']['name']) || $config['createDomain']['name'] == '') {\n        throw new Exception('Invalid domain name!');\n    }\n\n    // Check domain exists\n    $existDomains = get('ispmanager_domains');\n\n    if (in_array($config['createDomain']['name'], $existDomains)) {\n        if (!isset($config['createDomain']['skipIfExist']) || !$config['createDomain']['skipIfExist']) {\n            throw new Exception('Domain already exists!');\n        } else {\n            warning('Domain already exists - skipping');\n            return;\n        }\n    }\n\n    // Build vhost create request\n    $vhostTemplate = get('vhost');\n\n    $domainCreateRequest = [\n        'func' => 'webdomain.edit',\n        'sok' => 'ok',\n    ];\n\n    foreach ($vhostTemplate as $key => $value) {\n        $domainCreateRequest[$key] = str_replace('{{domain}}', $config['createDomain']['name'], $vhostTemplate[$key]);\n    }\n\n    $response = ispmanagerRequest('post', $domainCreateRequest);\n\n    if (isset($response['doc']['error']['msg']['$'])) {\n        throw new Exception($response['doc']['error']['msg']['$']);\n    } else {\n        info('Domain successfully created');\n    }\n});\n\ndesc('Gets allowed PHP modes and versions');\ntask('ispmanager:get-php-list', function () {\n    // Get www-root settings for fpm version\n    $response = ispmanagerRequest('get', [\n        'func' => 'user.edit',\n        'elid' => get('ispmanager_owner'),\n        'elname' => get('ispmanager_owner'),\n    ]);\n\n    $userFPMVersion = $response['doc']['limit_php_fpm_version']['$'] ?? null;\n\n    $response = ispmanagerRequest('get', [\n        'func' => 'phpversions',\n    ]);\n\n    $versions = [];\n    foreach ($response['doc']['elem'] as $phpVersion) {\n        $versions[$phpVersion['key']['$']] = [\n            'name' => $phpVersion['name']['$'],\n            'php_mode_mod' => false,\n            'php_mode_cgi' => false,\n            'php_mode_fcgi_apache' => false,\n            'php_mode_fcgi_nginxfpm' => false,\n        ];\n\n        if (isset($phpVersion['default_apache']) && $phpVersion['default_apache']['$'] == 'on') {\n            $versions[$phpVersion['key']['$']]['php_mode_mod'] = true;\n        }\n\n        if (isset($phpVersion['cgi']) && $phpVersion['cgi']['$'] == 'on') {\n            $versions[$phpVersion['key']['$']]['php_mode_cgi'] = true;\n        }\n\n        if (isset($phpVersion['apache']) && $phpVersion['apache']['$'] == 'on') {\n            $versions[$phpVersion['key']['$']]['php_mode_fcgi_apache'] = true;\n        }\n\n        if (isset($phpVersion['fpm']) && $phpVersion['fpm']['$'] == 'on' && $phpVersion['key']['$'] == $userFPMVersion) {\n            $versions[$phpVersion['key']['$']]['php_mode_fcgi_nginxfpm'] = true;\n        }\n\n    }\n\n    add('ispmanager_phplist', $versions);\n});\n\ndesc('Prints allowed PHP modes and versions');\ntask('ispmanager:print-php-list', function () {\n    invoke('ispmanager:get-php-list');\n\n    $versions = get('ispmanager_phplist');\n    writeln(\"PHP versions: \");\n    writeln(str_repeat('*', 32));\n    foreach ($versions as $versionKey => $versionData) {\n        writeln('PHP ' . $versionData['name'] . ' (ID: ' . $versionKey . ')');\n        writeln(str_repeat('*', 32));\n        if (!$versionData['php_mode_mod']) {\n            writeln('Apache module support (php_mode_mod) - <fg=red;options=bold>NO</>');\n        } else {\n            writeln('Apache module support (php_mode_mod) - <fg=green;options=bold>YES</>');\n        }\n\n        if (!$versionData['php_mode_cgi']) {\n            writeln('CGI support (php_mode_cgi) - <fg=red;options=bold>NO</>');\n        } else {\n            writeln('CGI support (php_mode_cgi) - <fg=green;options=bold>YES</>');\n        }\n\n        if (!$versionData['php_mode_fcgi_apache']) {\n            writeln('Apache fast-cgi support (php_mode_fcgi_apache) - <fg=red;options=bold>NO</>');\n        } else {\n            writeln('Apache fast-cgi support (php_mode_fcgi_apache) - <fg=green;options=bold>YES</>');\n        }\n\n        if (!$versionData['php_mode_fcgi_nginxfpm']) {\n            writeln('nginx fast-cgi support (php_mode_fcgi_nginxfpm) - <fg=red;options=bold>NO</>');\n        } else {\n            writeln('nginx fast-cgi support (php_mode_fcgi_nginxfpm) - <fg=green;options=bold>YES</>');\n        }\n\n        writeln(str_repeat('*', 32));\n    }\n});\n\ndesc('Switches PHP version for domain');\ntask('ispmanager:domain-php-select', function () {\n    $config = get('ispmanager');\n\n    if (is_null($config['phpSelect'])) {\n        warning('Action for domain update is not active');\n        return;\n    }\n\n    if (!isset($config['phpSelect']['name']) || $config['phpSelect']['name'] == '') {\n        throw new Exception('Invalid domain name!');\n    }\n\n    $existDomains = get('ispmanager_domains');\n\n    if (!in_array($config['phpSelect']['name'], $existDomains)) {\n        throw new Exception('Domain not exist!');\n    }\n\n    if (!isset($config['phpSelect']['mode']) || !isset($config['phpSelect']['version'])) {\n        throw new Exception('Incorrect settings for select php version');\n    }\n\n    $phpVersions = get('ispmanager_phplist');\n\n    $newVersion = $config['phpSelect']['version'];\n    $newMode = $config['phpSelect']['mode'];\n\n    if (!isset($phpVersions[$newVersion])) {\n        throw new Exception('Incorrect php version');\n    }\n\n    $versionData = $phpVersions[$newVersion];\n\n    if (!isset($versionData[$newMode]) || !$versionData[$newMode]) {\n        throw new Exception('Incorrect php mode');\n    }\n\n    $domainUpdateRequest = [\n        'func' => 'webdomain.edit',\n        'elid' => $config['phpSelect']['name'],\n        'name' => $config['phpSelect']['name'],\n        'php_mode' => $newMode,\n        'sok' => 'ok',\n    ];\n\n    if ($newMode == 'php_mode_mod') {\n        $domainUpdateRequest['php_apache_version'] = $newVersion;\n    } elseif ($newMode == 'php_mode_cgi') {\n        $domainUpdateRequest['php_cgi_version'] = $newVersion;\n    } elseif ($newMode == 'php_mode_fcgi_apache') {\n        $domainUpdateRequest['php_cgi_version'] = $newVersion;\n        $domainUpdateRequest['php_apache_version'] = $newVersion;\n    } elseif ($newMode == 'php_mode_fcgi_nginxfpm') {\n        $domainUpdateRequest['php_cgi_version'] = $newVersion;\n        $domainUpdateRequest['php_fpm_version'] = $newVersion;\n    } else {\n        throw new Exception('Unknown PHP mode!');\n    }\n\n    $response = ispmanagerRequest('post', $domainUpdateRequest);\n\n    if (isset($response['doc']['error']['msg']['$'])) {\n        throw new Exception($response['doc']['error']['msg']['$']);\n    } else {\n        info('PHP successfully selected');\n    }\n});\n\ndesc('Creates new domain alias');\ntask('ispmanager:domain-alias-create', function () {\n    $config = get('ispmanager');\n\n    if (is_null($config['createAlias'])) {\n        warning('Action for alias create is not active');\n        return;\n    }\n\n    if (!isset($config['createAlias']['name']) || $config['createAlias']['name'] == '') {\n        throw new Exception('Invalid domain name!');\n    }\n\n    $existDomains = get('ispmanager_domains');\n\n    if (!in_array($config['createAlias']['name'], $existDomains)) {\n        throw new Exception('Domain not exist!');\n    }\n\n    if (!isset($config['createAlias']['alias']) || $config['createAlias']['alias'] == '') {\n        throw new Exception('Invalid alias name!');\n    }\n\n    // Get current domain data\n    $response = ispmanagerRequest('get', [\n        'func' => 'webdomain.edit',\n        'elid' => $config['createAlias']['name'],\n        'elname' => $config['createAlias']['name'],\n    ]);\n\n    $existAliases = [];\n    if (isset($response['doc']['aliases']['$'])) {\n        $existAliases = explode(' ', $response['doc']['aliases']['$']);\n    }\n\n    $newAliasList = [];\n    $createAliasList = explode(' ', $config['createAlias']['alias']);\n    foreach ($createAliasList as $createAlias) {\n        if (in_array($createAlias, $existAliases)) {\n            if (!isset($config['createAlias']['skipIfExist']) || !$config['createAlias']['skipIfExist']) {\n                throw new Exception('Alias already exists!');\n            } else {\n                warning('Alias ' . $createAlias . ' already exists - skipping');\n                continue;\n            }\n        }\n\n        $newAliasList[] = $createAlias;\n    }\n\n    $saveAliases = array_merge($existAliases, $newAliasList);\n\n    $domainUpdateRequest = [\n        'func' => 'webdomain.edit',\n        'elid' => $config['createAlias']['name'],\n        'name' => $config['createAlias']['name'],\n        'aliases' => implode(' ', $saveAliases),\n        'sok' => 'ok',\n    ];\n\n    $response = ispmanagerRequest('post', $domainUpdateRequest);\n\n    if (isset($response['doc']['error']['msg']['$'])) {\n        throw new Exception($response['doc']['error']['msg']['$']);\n    } else {\n        info('Alias successfully created');\n    }\n});\n\ndesc('Deletes domain alias');\ntask('ispmanager:domain-alias-delete', function () {\n    $config = get('ispmanager');\n\n    if (is_null($config['deleteAlias'])) {\n        warning('Action for alias create is not active');\n        return;\n    }\n\n    if (!isset($config['deleteAlias']['name']) || $config['deleteAlias']['name'] == '') {\n        throw new Exception('Invalid domain name!');\n    }\n\n    $existDomains = get('ispmanager_domains');\n\n    if (!in_array($config['deleteAlias']['name'], $existDomains)) {\n        throw new Exception('Domain not exist!');\n    }\n\n    if (!isset($config['deleteAlias']['alias']) || $config['deleteAlias']['alias'] == '') {\n        throw new Exception('Invalid alias name!');\n    }\n\n    // Get current domain data\n    $response = ispmanagerRequest('get', [\n        'func' => 'webdomain.edit',\n        'elid' => $config['createAlias']['name'],\n        'elname' => $config['createAlias']['name'],\n    ]);\n\n    $existAliases = [];\n    if (isset($response['doc']['aliases']['$'])) {\n        $existAliases = explode(' ', $response['doc']['aliases']['$']);\n    }\n\n    $deleteAliasList = explode(' ', $config['deleteAlias']['alias']);\n    foreach ($deleteAliasList as $deleteAlias) {\n        if (!in_array($deleteAlias, $existAliases)) {\n            if (!isset($config['deleteAlias']['skipIfNotExist']) || !$config['deleteAlias']['skipIfNotExist']) {\n                throw new Exception('Alias not exist!');\n            } else {\n                warning('Alias ' . $deleteAlias . ' not exist - skipping');\n                continue;\n            }\n        }\n\n        if (($index = array_search($deleteAlias, $existAliases)) !== false) {\n            unset($existAliases[$index]);\n        }\n    }\n\n    $domainUpdateRequest = [\n        'func' => 'webdomain.edit',\n        'elid' => $config['deleteAlias']['name'],\n        'name' => $config['deleteAlias']['name'],\n        'aliases' => implode(' ', $existAliases),\n        'sok' => 'ok',\n    ];\n\n    $response = ispmanagerRequest('post', $domainUpdateRequest);\n\n    if (isset($response['doc']['error']['msg']['$'])) {\n        throw new Exception($response['doc']['error']['msg']['$']);\n    } else {\n        info('Alias successfully deleted');\n    }\n});\n\ndesc('Deletes domain');\ntask('ispmanager:domain-delete', function () {\n    $config = get('ispmanager');\n\n    if (is_null($config['deleteDomain'])) {\n        warning('Action for domain delete is not active');\n        return;\n    }\n\n    if (!isset($config['deleteDomain']['name']) || $config['deleteDomain']['name'] == '') {\n        throw new Exception('Invalid domain name!');\n    }\n\n    // Check domain exists\n    $existDomains = get('ispmanager_domains');\n\n    if (!in_array($config['deleteDomain']['name'], $existDomains)) {\n        if (!isset($config['deleteDomain']['skipIfNotExist']) || !$config['deleteDomain']['skipIfNotExist']) {\n            throw new Exception('Domain not exist!');\n        } else {\n            warning('Domain not exist - skipping');\n            return;\n        }\n    }\n\n    // Build request\n    $domainDeleteRequest = [\n        'func' => 'webdomain.delete.confirm',\n        'elid' => $config['deleteDomain']['name'],\n        'sok' => 'ok',\n    ];\n\n    if (!isset($config['deleteDomain']['removeDir']) || !$config['deleteDomain']['removeDir']) {\n        $domainDeleteRequest['remove_directory'] = 'off';\n    } else {\n        $domainDeleteRequest['remove_directory'] = 'on';\n    }\n\n    $response = ispmanagerRequest('post', $domainDeleteRequest);\n\n    if (isset($response['doc']['error']['msg']['$'])) {\n        throw new Exception($response['doc']['error']['msg']['$']);\n    } else {\n        info('Domain successfully deleted');\n    }\n});\n\ndesc('Auto task processing');\ntask('ispmanager:process', function () {\n    $config = get('ispmanager');\n\n    if (!is_null($config['createDatabase'])) {\n        invoke('ispmanager:db-create');\n    }\n\n    if (!is_null($config['deleteDatabase'])) {\n        invoke('ispmanager:db-delete');\n    }\n\n    if (!is_null($config['createDomain'])) {\n        invoke('ispmanager:domain-create');\n    }\n\n    if (!is_null($config['deleteDomain'])) {\n        invoke('ispmanager:domain-delete');\n    }\n\n    if (!is_null($config['phpSelect'])) {\n        invoke('ispmanager:domain-php-select');\n    }\n\n    if (!is_null($config['createAlias'])) {\n        invoke('ispmanager:domain-alias-create');\n    }\n\n    if (!is_null($config['deleteAlias'])) {\n        invoke('ispmanager:domain-alias-delete');\n    }\n});\n\nfunction ispmanagerRequest($method, $requestData)\n{\n    $config = get('ispmanager');\n    $dsnData = parse_url($config['api']['dsn']);\n\n    $requestUrl = $dsnData['scheme'] . '://' . $dsnData['host'] . ':' . $dsnData['port'] . $dsnData['path'];\n\n    if ($config['api']['secure'] && get('ispmanager_session') == '') {\n        ispmanagerAuthRequest($requestUrl, $dsnData['user'], $dsnData['pass']);\n    }\n\n    if ($method == 'post') {\n        return Httpie::post($requestUrl)\n            ->formBody(prepareRequest($requestData))\n            ->setopt(CURLOPT_SSL_VERIFYPEER, false)\n            ->setopt(CURLOPT_SSL_VERIFYHOST, false)\n            ->getJson();\n    } elseif ($method == 'get') {\n        return Httpie::get($requestUrl)\n            ->query(prepareRequest($requestData))\n            ->setopt(CURLOPT_SSL_VERIFYPEER, false)\n            ->setopt(CURLOPT_SSL_VERIFYHOST, false)\n            ->getJson();\n    } else {\n        throw new Exception('Unknown request method');\n    }\n}\n\nfunction ispmanagerAuthRequest($url, $login, $pass)\n{\n    $authRequestData = [\n        'func' => 'auth',\n        'username' => $login,\n        'password' => $pass,\n    ];\n\n    $responseData = Httpie::post($url)\n        ->setopt(CURLOPT_SSL_VERIFYPEER, false)\n        ->setopt(CURLOPT_SSL_VERIFYHOST, false)\n        ->formBody(prepareRequest($authRequestData))\n        ->getJson();\n\n    if (isset($responseData['doc']['auth']['$id'])) {\n        set('ispmanager_session', $responseData['doc']['auth']['$id']);\n    } else {\n        throw new Exception('Error while create auth session');\n    }\n}\n\nfunction prepareRequest($requestData)\n{\n    $config = get('ispmanager');\n    $dsnData = parse_url($config['api']['dsn']);\n\n    if (!isset($requestData['out'])) {\n        $requestData['out'] = 'json';\n    }\n\n    if (!$config['api']['secure']) {\n        $requestData['authinfo'] = $dsnData['user'] . ':' . $dsnData['pass'];\n    } else {\n        if (get('ispmanager_session') != '') {\n            $requestData['auth'] = get('ispmanager_session');\n        }\n    }\n\n    return $requestData;\n}\n\nfunction generatePassword($lenght)\n{\n    return substr(md5(uniqid()), 0, $lenght);\n}\n\n// Callbacks before actions under domains\nbefore('ispmanager:domain-alias-create', 'ispmanager:init');\nbefore('ispmanager:domain-alias-delete', 'ispmanager:init');\nbefore('ispmanager:domain-create', 'ispmanager:init');\nbefore('ispmanager:domain-delete', 'ispmanager:init');\nbefore('ispmanager:domain-php-select', 'ispmanager:init');\n\n// Callbacks before actions under databases\nbefore('ispmanager:db-create', 'ispmanager:init');\nbefore('ispmanager:db-delete', 'ispmanager:init');\n"
  },
  {
    "path": "contrib/mattermost.php",
    "content": "<?php\n/*\n## Installing\n\nCreate a Mattermost incoming webhook, through the administration panel.\n\nAdd hook on deploy:\n\n```\nbefore('deploy', 'mattermost:notify');\n```\n\n## Configuration\n\n - `mattermost_webhook` - incoming mattermost webook **required**\n   ```\n   set('mattermost_webook', 'https://{your-mattermost-site}/hooks/xxx-generatedkey-xxx');\n   ```\n\n - `mattermost_channel` - overrides the channel the message posts in\n   ```\n   set('mattermost_channel', 'town-square');\n   ```\n\n - `mattermost_username` - overrides the username the message posts as\n   ```\n   set('mattermost_username', 'deployer');\n   ```\n\n - `mattermost_icon_url` - overrides the profile picture the message posts with\n   ```\n   set('mattermost_icon_url', 'https://domain.com/your-icon.png');\n   ```\n\n - `mattermost_text` - notification message\n   ```\n   set('mattermost_text', '_{{user}}_ deploying `{{what}}` to **{{where}}**');\n   ```\n\n - `mattermost_success_text` – success template, default:\n   ```\n   set('mattermost_success_text', 'Deploy to **{{where}}** successful {{mattermost_success_emoji}}');\n   ```\n\n - `mattermost_failure_text` – failure template, default:\n   ```\n   set('mattermost_failure_text', 'Deploy to **{{where}}** failed {{mattermost_failure_emoji}}');\n   ```\n\n - `mattermost_success_emoji` – emoji added at the end of success text\n - `mattermost_failure_emoji` – emoji added at the end of failure text\n\n For detailed information about Mattermost hooks see: https://developers.mattermost.com/integrate/incoming-webhooks/\n\n## Usage\n\nIf you want to notify only about beginning of deployment add this line only:\n\n```php\nbefore('deploy', 'mattermost:notify');\n```\n\nIf you want to notify about successful end of deployment add this too:\n\n```php\nafter('deploy:success', 'mattermost:notify:success');\n```\n\nIf you want to notify about failed deployment add this too:\n\n```php\nafter('deploy:failed', 'mattermost:notify:failure');\n```\n\n */\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\nset('mattermost_webhook', null);\nset('mattermost_channel', null);\nset('mattermost_username', 'deployer');\nset('mattermost_icon_url', null);\n\nset('mattermost_success_emoji', ':white_check_mark:');\nset('mattermost_failure_emoji', ':x:');\n\nset('mattermost_text', '_{{user}}_ deploying `{{what}}` to **{{where}}**');\nset('mattermost_success_text', 'Deploy to **{{where}}** successful {{mattermost_success_emoji}}');\nset('mattermost_failure_text', 'Deploy to **{{where}}** failed {{mattermost_failure_emoji}}');\n\ndesc('Notifies mattermost');\ntask('mattermost:notify', function () {\n    if (null === get('mattermost_webhook')) {\n        return;\n    }\n\n    $body = [\n        'text' => get('mattermost_text'),\n        'username' => get('mattermost_username'),\n    ];\n\n    if (get('mattermost_channel')) {\n        $body['channel'] = get('mattermost_channel');\n    }\n    if (get('mattermost_icon_url')) {\n        $body['icon_url'] = get('mattermost_icon_url');\n    }\n\n    Httpie::post(get('mattermost_webhook'))->jsonBody($body)->send();\n});\n\ndesc('Notifies mattermost about deploy finish');\ntask('mattermost:notify:success', function () {\n    if (null === get('mattermost_webhook')) {\n        return;\n    }\n\n    $body = [\n        'text' => get('mattermost_success_text'),\n        'username' => get('mattermost_username'),\n    ];\n\n    if (get('mattermost_channel')) {\n        $body['channel'] = get('mattermost_channel');\n    }\n    if (get('mattermost_icon_url')) {\n        $body['icon_url'] = get('mattermost_icon_url');\n    }\n\n    Httpie::post(get('mattermost_webhook'))->jsonBody($body)->send();\n});\n\ndesc('Notifies mattermost about deploy failure');\ntask('mattermost:notify:failure', function () {\n    if (null === get('mattermost_webhook')) {\n        return;\n    }\n\n    $body = [\n        'text' => get('mattermost_failure_text'),\n        'username' => get('mattermost_username'),\n    ];\n\n    if (get('mattermost_channel')) {\n        $body['channel'] = get('mattermost_channel');\n    }\n    if (get('mattermost_icon_url')) {\n        $body['icon_url'] = get('mattermost_icon_url');\n    }\n\n    Httpie::post(get('mattermost_webhook'))->jsonBody($body)->send();\n});\n"
  },
  {
    "path": "contrib/ms-teams.php",
    "content": "<?php\n/*\n## Installing\n\nRequire ms-teams recipe in your `deploy.php` file:\n\nSetup:\n1. Open MS Teams\n2. Navigate to Teams section\n3. Select existing or create new team\n4. Select existing or create new channel\n5. Hover over channel to get three dots, click, in menu select \"Connectors\"\n6. Search for and configure \"Incoming Webhook\"\n7. Confirm/create and copy your Webhook URL\n8. Setup deploy.php\n    Add in header:\n```php\nrequire 'contrib/ms-teams.php';\nset('teams_webhook', 'https://outlook.office.com/webhook/...');\n```\nAdd in content:\n```php\nbefore('deploy', 'teams:notify');\nafter('deploy:success', 'teams:notify:success');\nafter('deploy:failed', 'teams:notify:failure');\n```\n9.) Sip your coffee\n\n## Configuration\n\n- `teams_webhook` – teams incoming webhook url, **required**\n  ```\n  set('teams_webhook', 'https://outlook.office.com/webhook/...');\n  ```\n- `teams_title` – the title of application, default `{{application}}`\n- `teams_text` – notification message template, markdown supported\n  ```\n  set('teams_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\n  ```\n- `teams_success_text` – success template, default:\n  ```\n  set('teams_success_text', 'Deploy to *{{where}}* successful');\n  ```\n- `teams_failure_text` – failure template, default:\n  ```\n  set('teams_failure_text', 'Deploy to *{{where}}* failed');\n  ```\n\n- `teams_color` – color's attachment\n- `teams_success_color` – success color's attachment\n- `teams_failure_color` – failure color's attachment\n\n## Usage\n\nIf you want to notify only about beginning of deployment add this line only:\n\n```php\nbefore('deploy', 'teams:notify');\n```\n\nIf you want to notify about successful end of deployment add this too:\n\n```php\nafter('deploy:success', 'teams:notify:success');\n```\n\nIf you want to notify about failed deployment add this too:\n\n```php\nafter('deploy:failed', 'teams:notify:failure');\n```\n */\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\n// Title of project\nset('teams_title', function () {\n    return get('application', 'Project');\n});\n\n// Allow Continue on Failure\nset('teams_failure_continue', false);\n\n// Deploy message\nset('teams_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\nset('teams_success_text', 'Deploy to *{{where}}* successful');\nset('teams_failure_text', 'Deploy to *{{where}}* failed');\n\n// Color of attachment\nset('teams_color', '#4d91f7');\nset('teams_success_color', '#00c100');\nset('teams_failure_color', '#ff0909');\n\ndesc('Notifies Teams');\ntask('teams:notify', function () {\n    if (!get('teams_webhook', false)) {\n        warning('No MS Teams webhook configured');\n        return;\n    }\n\n    try {\n        Httpie::post(get('teams_webhook'))->jsonBody([\n            \"themeColor\" => get('teams_color'),\n            'text'       => get('teams_text'),\n        ])->send();\n    } catch (\\Exception $e) {\n        if (get('teams_failure_continue', false)) {\n            warning('Error sending Teams Notification: ' . $e->getMessage());\n        } else {\n            throw $e;\n        }\n    }\n\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies Teams about deploy finish');\ntask('teams:notify:success', function () {\n    if (!get('teams_webhook', false)) {\n        warning('No MS Teams webhook configured');\n        return;\n    }\n\n    try {\n        Httpie::post(get('teams_webhook'))->jsonBody([\n            \"themeColor\" => get('teams_success_color'),\n            'text'       => get('teams_success_text'),\n        ])->send();\n    } catch (\\Exception $e) {\n        if (get('teams_failure_continue', false)) {\n            warning('Error sending Teams Notification: ' . $e->getMessage());\n        } else {\n            throw $e;\n        }\n    }\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies Teams about deploy failure');\ntask('teams:notify:failure', function () {\n    if (!get('teams_webhook', false)) {\n        warning('No MS Teams webhook configured');\n        return;\n    }\n\n    try {\n        Httpie::post(get('teams_webhook'))->jsonBody([\n            \"themeColor\" => get('teams_failure_color'),\n            'text'       => get('teams_failure_text'),\n        ])->send();\n    } catch (\\Exception $e) {\n        if (get('teams_failure_continue', false)) {\n            warning('Error sending Teams Notification: ' . $e->getMessage());\n        } else {\n            throw $e;\n        }\n    }\n})\n    ->once()\n    ->hidden();\n"
  },
  {
    "path": "contrib/newrelic.php",
    "content": "<?php\n/*\n## Configuration\n\n- `newrelic_app_id` – newrelic's app id\n- `newrelic_api_key` – newrelic's api key\n- `newrelic_description` – message to send\n- `newrelic_endpoint` – newrelic's REST API endpoint\n\n## Usage\n\nSince you should only notify New Relic of a successful deployment, the `newrelic:notify` task should be executed right at the end.\n\n```php\nafter('deploy', 'newrelic:notify');\n```\n\n */\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\nset('newrelic_app_id', function () {\n    throw new \\Exception('Please, configure \"newrelic_app_id\" parameter.');\n});\n\nset('newrelic_description', function () {\n    return runLocally('git log -n 1 --format=\"%an: %s\" | tr \\'\"\\' \"\\'\"');\n});\n\nset('newrelic_revision', function () {\n    return runLocally('git log -n 1 --format=\"%h\"');\n});\n\nset('newrelic_endpoint', 'api.newrelic.com');\n\ndesc('Notifies New Relic of deployment');\ntask('newrelic:notify', function () {\n    if (($appId = get('newrelic_app_id')) && ($apiKey = get('newrelic_api_key')) && ($endpoint = get('newrelic_endpoint'))) {\n        $data = [\n            'user' => get('user'),\n            'revision' => get('newrelic_revision'),\n            'description' => get('newrelic_description'),\n        ];\n\n        Httpie::post(\"https://$endpoint/v2/applications/$appId/deployments.json\")\n            ->header(\"X-Api-Key\", $apiKey)\n            ->query(['deployment' => $data])\n            ->send();\n    }\n})\n    ->once()\n    ->hidden();\n"
  },
  {
    "path": "contrib/npm.php",
    "content": "<?php\n/*\n## Configuration\n\n- `bin/npm` *(optional)*: set npm binary, automatically detected otherwise.\n\n## Usage\n\n```php\nafter('deploy:update_code', 'npm:install');\n```\n\n */\n\nnamespace Deployer;\n\nset('bin/npm', function () {\n    return which('npm');\n});\n\n// Uses `npm ci` command. This command is similar to npm install,\n// except it's meant to be used in automated environments such as\n// test platforms, continuous integration, and deployment -- or\n// any situation where you want to make sure you're doing a clean\n// install of your dependencies.\ndesc('Installs npm packages');\ntask('npm:install', function () {\n    run(\"cd {{release_path}} && {{bin/npm}} ci\");\n});\n"
  },
  {
    "path": "contrib/ntfy.php",
    "content": "<?php\n/*\n## Installing\n\nRequire ntfy.sh recipe in your `deploy.php` file:\n\nSetup:\n1. Setup deploy.php\n    Add in header:\n```php\nrequire 'contrib/ntfy.php';\nset('ntfy_topic', 'ntfy.sh/mytopic');\n```\nAdd in content:\n```php\nbefore('deploy', 'ntfy:notify');\nafter('deploy:success', 'ntfy:notify:success');\nafter('deploy:failed', 'ntfy:notify:failure');\n```\n9.) Sip your coffee\n\n## Configuration\n\n- `ntfy_server` – ntfy server url, default `ntfy.sh`\n  ```\n  set('ntfy_server', 'ntfy.sh');\n  ```\n- `ntfy_topic` – ntfy topic, **required**\n  ```\n  set('ntfy_topic', 'mysecrettopic');\n  ```\n- `ntfy_title` – the title of the message, default `{{application}}`\n- `ntfy_text` – notification message template\n  ```\n  set('ntfy_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\n  ```\n- `ntfy_tags` – notification message tags / emojis (comma separated)\n  ```\n  set('ntfy_tags', `information_source`);\n  ```\n- `ntfy_priority` – notification message priority (integer)\n  ```\n  set('ntfy_priority', 5);\n  ```\n- `ntfy_success_text` – success template, default:\n  ```\n  set('ntfy_success_text', 'Deploy to *{{where}}* successful');\n  ```\n- `ntfy_success_tags` – success tags / emojis (comma separated)\n  ```\n  set('ntfy_success_tags', `white_check_mark,champagne`);\n  ```\n- `ntfy_success_priority` – success notification message priority\n- `ntfy_failure_text` – failure template, default:\n  ```\n  set('ntfy_failure_text', 'Deploy to *{{where}}* failed');\n  ```\n- `ntfy_failure_tags` – failure tags / emojis (comma separated)\n  ```\n  set('ntfy_failure_tags', `warning,skull`);\n  ```\n- `ntfy_failure_priority` – failure notification message priority\n\n\n## Usage\n\nIf you want to notify only about beginning of deployment add this line only:\n\n```php\nbefore('deploy', 'ntfy:notify');\n```\n\nIf you want to notify about successful end of deployment add this too:\n\n```php\nafter('deploy:success', 'ntfy:notify:success');\n```\n\nIf you want to notify about failed deployment add this too:\n\n```php\nafter('deploy:failed', 'ntfy:notify:failure');\n```\n */\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\nset('ntfy_server', 'ntfy.sh');\n\n// Title of project\nset('ntfy_title', function () {\n    return get('application', 'Project');\n});\n\n// Deploy message\nset('ntfy_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\nset('ntfy_success_text', 'Deploy to *{{where}}* successful');\nset('ntfy_failure_text', 'Deploy to *{{where}}* failed');\n\n// Message tags\nset('ntfy_tags', '');\nset('ntfy_success_tags', '');\nset('ntfy_failure_tags', '');\n\ndesc('Notifies ntfy server');\ntask('ntfy:notify', function () {\n    if (!get('ntfy_topic', false)) {\n        warning('No ntfy topic configured');\n        return;\n    }\n\n    Httpie::post(get('ntfy_server'))->jsonBody([\n        \"topic\"     => get('ntfy_topic'),\n        \"title\"   => get('ntfy_title'),\n        \"message\"   => get('ntfy_text'),\n        \"tags\"   => explode(\",\", get('ntfy_tags')),\n        \"priority\"   => get('ntfy_priority'),\n    ])->send();\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies ntfy server about deploy finish');\ntask('ntfy:notify:success', function () {\n    if (!get('ntfy_topic', false)) {\n        warning('No ntfy topic configured');\n        return;\n    }\n\n    Httpie::post(get('ntfy_server'))->jsonBody([\n        \"topic\"     => get('ntfy_topic'),\n        \"title\"   => get('ntfy_title'),\n        \"message\"   => get('ntfy_success_text'),\n        \"tags\"   => explode(\",\", get('ntfy_success_tags')),\n        \"priority\"   => get('ntfy_success_priority'),\n    ])->send();\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies ntfy server about deploy failure');\ntask('ntfy:notify:failure', function () {\n    if (!get('ntfy_topic', false)) {\n        warning('No ntfy topic configured');\n        return;\n    }\n\n    Httpie::post(get('ntfy_server'))->jsonBody([\n        \"topic\"     => get('ntfy_topic'),\n        \"title\"   => get('ntfy_title'),\n        \"message\"   => get('ntfy_failure_text'),\n        \"tags\"   => explode(\",\", get('ntfy_failure_tags')),\n        \"priority\"   => get('ntfy_failure_priority'),\n    ])->send();\n})\n    ->once()\n    ->hidden();\n"
  },
  {
    "path": "contrib/phinx.php",
    "content": "<?php\n/*\n\n## Configuration options\n\nAll options are in the config parameter `phinx` specified as an array (instead of the `phinx_path` variable).\nAll parameters are *optional*, but you can specify them with a dictionary (to change all parameters)\nor by deployer dot notation (to change one option).\n\n### Phinx params\n\n- `phinx.environment`\n- `phinx.date`\n- `phinx.configuration` N.B. current directory is the project directory\n- `phinx.target`\n- `phinx.seed`\n- `phinx.parser`\n- `phinx.remove-all` (pass empty string as value)\n\n### Phinx path params\n\n- `phinx_path` Specify phinx path (by default phinx is searched for in $PATH, ./vendor/bin and ~/.composer/vendor/bin)\n\n### Example of usage\n\n```php\n$phinx_env_vars = [\n  'environment' => 'development',\n  'configuration' => './migration/.phinx.yml',\n  'target' => '20120103083322',\n  'remove-all' => '',\n];\n\nset('phinx_path', '/usr/local/phinx/bin/phinx');\nset('phinx', $phinx_env_vars);\n\nafter('cleanup', 'phinx:migrate');\n\n// or set it for a specific server\nhost('dev')\n    ->user('user')\n    ->set('deploy_path', '/var/www')\n    ->set('phinx', $phinx_env_vars)\n    ->set('phinx_path', '');\n```\n\n## Suggested Usage\n\nYou can run all tasks before or after any\ntasks (but you need to specify external configs for phinx).\nIf you use internal configs (which are in your project) you need\nto run it after the `deploy:update_code` task is completed.\n\n## Read more\n\nFor further reading see [phinx.org](https://phinx.org). Complete descriptions of all possible options can be found on the [commands page](http://docs.phinx.org/en/latest/commands.html).\n\n */\n\nnamespace Deployer;\n\nuse Deployer\\Exception\\RunException;\n\n/*\n * Phinx recipe for Deployer\n *\n * @author    Alexey Boyko <ket4yiit@gmail.com>\n * @contributor Security-Database <info@security-database.com>\n * @copyright 2016 Alexey Boyko\n * @license   MIT https://github.com/deployphp/recipes/blob/master/LICENSE\n *\n * @link https://github.com/deployphp/recipes\n *\n * @see http://deployer.org\n * @see https://phinx.org\n */\n\n/**\n * Path to Phinx\n */\nset('bin/phinx', function () {\n    try {\n        $phinxPath = which('phinx');\n    } catch (RunException $e) {\n        $phinxPath = null;\n    }\n\n    if ($phinxPath !== null) {\n        return \"phinx\";\n    } elseif (test('[ -f {{release_path}}/vendor/bin/phinx ]')) {\n        return \"{{release_path}}/vendor/bin/phinx\";\n    } elseif (test('[ -f ~/.composer/vendor/bin/phinx ]')) {\n        return '~/.composer/vendor/bin/phinx';\n    } else {\n        throw new \\RuntimeException('Cannot find phinx. Please specify path to phinx manually');\n    }\n});\n\n/**\n * Make Phinx command\n *\n * @param string $cmdName Name of command\n * @param array $conf Command options(config)\n *\n * @return string Phinx command to execute\n */\nfunction phinx_get_cmd($cmdName, $conf)\n{\n    $phinx = get('phinx_path') ?: get('bin/phinx');\n\n    $phinxCmd = \"$phinx $cmdName\";\n\n    $options = '';\n\n    foreach ($conf as $name => $value) {\n        $options .= \" --$name $value\";\n    }\n\n    $phinxCmd .= $options;\n\n    return $phinxCmd;\n}\n\n/**\n * Returns options array that allowed for command\n *\n * @param array $allowedOptions List of allowed options\n *\n * @return array Array of options\n */\nfunction phinx_get_allowed_config($allowedOptions)\n{\n    $opts = [];\n\n    try {\n        foreach (get('phinx') as $key => $val) {\n            if (in_array($key, $allowedOptions)) {\n                $opts[$key] = $val;\n            }\n        }\n    } catch (\\RuntimeException $e) {\n    }\n\n    return $opts;\n}\n\n\ndesc('Migrats database with phinx');\ntask('phinx:migrate', function () {\n    $ALLOWED_OPTIONS = [\n        'configuration',\n        'date',\n        'environment',\n        'target',\n        'parser',\n    ];\n\n    $conf = phinx_get_allowed_config($ALLOWED_OPTIONS);\n\n    cd('{{release_path}}');\n\n    $phinxCmd = phinx_get_cmd('migrate', $conf);\n\n    run($phinxCmd);\n\n    cd('{{deploy_path}}');\n});\n\ndesc('Rollbacks database migrations with phinx');\ntask('phinx:rollback', function () {\n    $ALLOWED_OPTIONS = [\n        'configuration',\n        'date',\n        'environment',\n        'target',\n        'parser',\n    ];\n\n    $conf = phinx_get_allowed_config($ALLOWED_OPTIONS);\n\n    cd('{{release_path}}');\n\n    $phinxCmd = phinx_get_cmd('rollback', $conf);\n\n    run($phinxCmd);\n\n    cd('{{deploy_path}}');\n});\n\ndesc('Seeds database with phinx');\ntask('phinx:seed', function () {\n    $ALLOWED_OPTIONS = [\n        'configuration',\n        'environment',\n        'parser',\n        'seed',\n    ];\n\n    $conf = phinx_get_allowed_config($ALLOWED_OPTIONS);\n\n    cd('{{release_path}}');\n\n    $phinxCmd = phinx_get_cmd('seed:run', $conf);\n\n    run($phinxCmd);\n\n    cd('{{deploy_path}}');\n});\n\ndesc('Sets a migrations breakpoint with phinx');\ntask('phinx:breakpoint', function () {\n    $ALLOWED_OPTIONS = [\n        'configuration',\n        'environment',\n        'remove-all',\n        'target',\n    ];\n\n    $conf = phinx_get_allowed_config($ALLOWED_OPTIONS);\n\n    cd('{{release_path}}');\n\n    $phinxCmd = phinx_get_cmd('breakpoint', $conf);\n\n    run($phinxCmd);\n\n    cd('{{deploy_path}}');\n});\n"
  },
  {
    "path": "contrib/php-fpm.php",
    "content": "<?php\n/*\n\n:::caution\nDo **not** reload php-fpm. Some user requests could fail or not complete in the\nprocess of reloading.\n\nInstead, configure your server [properly](avoid-php-fpm-reloading). If you're using Deployer's provision\nrecipe, it's already configured the right way and no php-fpm reload is needed.\n:::\n\n## Configuration\n\n- `php_fpm_version` – The PHP-fpm version. For example: `8.0`.\n- `php_fpm_service` – The full name of the PHP-fpm service. Defaults to `php{{php_fpm_version}}-fpm`.\n- `php_fpm_command` – The command to run to reload PHP-fpm. Defaults to `sudo systemctl reload {{php_fpm_service}}`.\n\n## Usage\n\nStart by explicitely providing the current version of PHP-version using the `php_fpm_version`.\nAlternatively, you may use any of the options above to configure how PHP-fpm should reload.\n\nThen, add the `php-fpm:reload` task at the end of your deployments by using the `after` method like so.\n\n```php\nset('php_fpm_version', '8.0');\nafter('deploy', 'php-fpm:reload');\n```\n\n */\n\nnamespace Deployer;\n\n// Automatically detects by using {{bin/php}}.\nset('php_fpm_version', function () {\n    return run('{{bin/php}} -r \"printf(\\'%d.%d\\', PHP_MAJOR_VERSION, PHP_MINOR_VERSION);\"');\n});\n\nset('php_fpm_service', 'php{{php_fpm_version}}-fpm');\n\ndesc('Reloads the php-fpm service');\ntask('php-fpm:reload', function () {\n    warning('Avoid reloading php-fpm [deployer.org/docs/8.x/avoid-php-fpm-reloading]');\n    run('sudo systemctl reload {{php_fpm_service}}');\n});\n"
  },
  {
    "path": "contrib/rabbit.php",
    "content": "<?php\n/*\n### Installing\n\n```php\n// deploy.php\n\nrequire 'recipe/rabbit.php';\n```\n\n### Configuration options\n\n- **rabbit** *(required)*: accepts an *array* with the connection information to [rabbitmq](http://www.rabbitmq.com) server token and team name.\n\n\nYou can provide also other configuration options:\n\n - *host* - default is localhost\n - *port* - default is 5672\n - *username* - default is *guest*\n - *password* - default is *guest*\n - *channel* - no default value, need to be specified via config\n - *message* - default is **Deployment to '$host' on *$prod* was successful\\n$releasePath**\n - *vhost* - default is\n\n\n```php\n// deploy.php\n\nset('rabbit', [\n    'host'     => 'localhost',\n    'port'     => '5672',\n    'username' => 'guest',\n    'password' => 'guest',\n    'channel'  => 'notify-channel',\n    'vhost'    => '/my-app'\n]);\n```\n\n### Suggested Usage\n\nSince you should only notify RabbitMQ channel of a successful deployment, the `deploy:rabbit` task should be executed right at the end.\n\n```php\n// deploy.php\n\nbefore('deploy:end', 'deploy:rabbit');\n```\n */\n\nnamespace Deployer;\n\nuse Deployer\\Task\\Context;\nuse PhpAmqpLib\\Connection\\AMQPConnection;\nuse PhpAmqpLib\\Message\\AMQPMessage;\n\ndesc('Notifies RabbitMQ channel about deployment');\ntask('deploy:rabbit', function () {\n\n    if (!class_exists('PhpAmqpLib\\Connection\\AMQPConnection')) {\n        throw new \\RuntimeException(\"<comment>Please install php package</comment> <info>videlalvaro/php-amqplib</info> <comment>to use rabbitmq</comment>\");\n    }\n\n    $config = get('rabbit', []);\n\n    if (!isset($config['message'])) {\n        $releasePath = get('release_path');\n        $host = Context::get()->getHost();\n\n        $stage = get('stage', false);\n        $stageInfo = ($stage) ? sprintf(' on *%s*', $stage) : '';\n\n        $message = \"Deployment to '%s'%s was successful\\n(%s)\";\n        $config['message'] = sprintf(\n            $message,\n            $host->getHostname(),\n            $stageInfo,\n            $releasePath,\n        );\n    }\n\n    $defaultConfig = [\n        'host' => 'localhost',\n        'port' => 5672,\n        'username' => 'guest',\n        'password' => 'guest',\n        'vhost' => '/',\n    ];\n\n    $config = array_merge($defaultConfig, $config);\n\n    if (!is_array($config) ||\n        !isset($config['channel']) ||\n        !isset($config['host']) ||\n        !isset($config['port']) ||\n        !isset($config['username']) ||\n        !isset($config['password']) ||\n        !isset($config['vhost'])) {\n        throw new \\RuntimeException(\"<comment>Please configure rabbit config:</comment> <info>set('rabbit', array('channel' => 'channel', 'host' => 'host', 'port' => 'port', 'username' => 'username', 'password' => 'password'));</info>\");\n    }\n\n    $connection = new AMQPConnection($config['host'], $config['port'], $config['username'], $config['password'], $config['vhost']);\n    $channel = $connection->channel();\n\n    $msg = new AMQPMessage($config['message']);\n    $channel->basic_publish($msg, $config['channel'], $config['channel']);\n\n    $channel->close();\n    $connection->close();\n\n});\n"
  },
  {
    "path": "contrib/raygun.php",
    "content": "<?php\n/*\n\n## Configuration\n\n- `raygun_api_key` – the API key of your Raygun application\n- `raygun_version` – the version of your application that this deployment is releasing\n- `raygun_owner_name` – the name of the person creating this deployment\n- `raygun_email` – the email of the person creating this deployment\n- `raygun_comment` – the deployment notes\n- `raygun_scm_identifier` – the commit that this deployment was built off\n- `raygun_scm_type` - the source control system you use\n\n## Usage\n\nTo notify Raygun of a successful deployment, you can use the 'raygun:notify' task after a deployment.\n\n```php\nafter('deploy', 'raygun:notify');\n```\n */\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\ndesc('Notifies Raygun of deployment');\ntask('raygun:notify', function () {\n    $data = [\n        'apiKey'       => get('raygun_api_key'),\n        'version' => get('raygun_version'),\n        'ownerName'   => get('raygun_owner_name'),\n        'emailAddress' => get('raygun_email'),\n        'comment' => get('raygun_comment'),\n        'scmIdentifier' => get('raygun_scm_identifier'),\n        'scmType' => get('raygun_scm_type'),\n    ];\n\n    Httpie::post('https://app.raygun.io/deployments')\n        ->jsonBody($data)\n        ->send();\n});\n"
  },
  {
    "path": "contrib/rocketchat.php",
    "content": "<?php\n/*\n## Installing\n\nCreate a RocketChat incoming webhook, through the administration panel.\n\nAdd hook on deploy:\n\n```\nbefore('deploy', 'rocketchat:notify');\n```\n\n## Configuration\n\n - `rocketchat_webhook` - incoming rocketchat webook **required**\n   ```\n   set('rocketchat_webhook', 'https://rocketchat.yourcompany.com/hooks/XXXXX');\n   ```\n\n - `rocketchat_title` - the title of the application, defaults to `{{application}}`\n - `rocketchat_text` - notification message\n   ```\n   set('rocketchat_text', '_{{user}}_ deploying {{what}} to {{where}}');\n   ```\n\n - `rocketchat_success_text` – success template, default:\n  ```\n  set('rocketchat_success_text', 'Deploy to *{{where}}* successful');\n  ```\n - `rocketchat_failure_text` – failure template, default:\n  ```\n  set('rocketchat_failure_text', 'Deploy to *{{where}}* failed');\n  ```\n\n - `rocketchat_color` – color's attachment\n - `rocketchat_success_color` – success color's attachment\n - `rocketchat_failure_color` – failure color's attachment\n\n## Usage\n\nIf you want to notify only about beginning of deployment add this line only:\n\n```php\nbefore('deploy', 'rocketchat:notify');\n```\n\nIf you want to notify about successful end of deployment add this too:\n\n```php\nafter('deploy:success', 'rocketchat:notify:success');\n```\n\nIf you want to notify about failed deployment add this too:\n\n```php\nafter('deploy:failed', 'rocketchat:notify:failure');\n```\n\n */\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\nset('rockchat_title', function () {\n    return get('application', 'Project');\n});\n\nset('rocketchat_icon_emoji', ':robot:');\nset('rocketchat_icon_url', null);\n\nset('rocketchat_channel', null);\nset('rocketchat_room_id', null);\nset('rocketchat_username', null);\nset('rocketchat_webhook', null);\n\nset('rocketchat_color', '#000000');\nset('rocketchat_success_color', '#00c100');\nset('rocketchat_failure_color', '#ff0909');\n\nset('rocketchat_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\nset('rocketchat_success_text', 'Deploy to *{{where}}* successful');\nset('rocketchat_failure_text', 'Deploy to *{{where}}* failed');\n\ndesc('Notifies RocketChat');\ntask('rocketchat:notify', function () {\n    if (null === get('rocketchat_webhook')) {\n        return;\n    }\n\n    $body = [\n        'text' => get('rockchat_title'),\n        'username' => get('rocketchat_username'),\n        'attachments' => [[\n            'text' => get('rocketchat_text'),\n            'color' => get('rocketchat_color'),\n        ]],\n    ];\n\n    if (get('rocketchat_channel')) {\n        $body['channel'] = get('rocketchat_channel');\n    }\n    if (get('rocketchat_room_id')) {\n        $body['roomId'] = get('rocketchat_room_id');\n    }\n    if (get('rocketchat_icon_url')) {\n        $body['avatar'] = get('rocketchat_icon_url');\n    } elseif (get('rocketchat_icon_emoji')) {\n        $body['emoji'] = get('rocketchat_icon_emoji');\n    }\n\n    Httpie::post(get('rocketchat_webhook'))->jsonBody($body)->send();\n});\n\ndesc('Notifies RocketChat about deploy finish');\ntask('rocketchat:notify:success', function () {\n    if (null === get('rocketchat_webhook')) {\n        return;\n    }\n\n    $body = [\n        'text' => get('rockchat_title'),\n        'username' => get('rocketchat_username'),\n        'attachments' => [[\n            'text' => get('rocketchat_success_text'),\n            'color' => get('rocketchat_success_color'),\n        ]],\n    ];\n\n    if (get('rocketchat_channel')) {\n        $body['channel'] = get('rocketchat_channel');\n    }\n    if (get('rocketchat_room_id')) {\n        $body['roomId'] = get('rocketchat_room_id');\n    }\n    if (get('rocketchat_icon_url')) {\n        $body['avatar'] = get('rocketchat_icon_url');\n    } elseif (get('rocketchat_icon_emoji')) {\n        $body['emoji'] = get('rocketchat_icon_emoji');\n    }\n\n    Httpie::post(get('rocketchat_webhook'))->jsonBody($body)->send();\n});\n\ndesc('Notifies RocketChat about deploy failure');\ntask('rocketchat:notify:failure', function () {\n    if (null === get('rocketchat_webhook')) {\n        return;\n    }\n\n    $body = [\n        'text' => get('rockchat_title'),\n        'username' => get('rocketchat_username'),\n        'attachments' => [[\n            'color' => get('rocketchat_failure_color'),\n            'text' => get('rocketchat_failure_text'),\n        ]],\n    ];\n\n    if (get('rocketchat_channel')) {\n        $body['channel'] = get('rocketchat_channel');\n    }\n    if (get('rocketchat_room_id')) {\n        $body['roomId'] = get('rocketchat_room_id');\n    }\n    if (get('rocketchat_icon_url')) {\n        $body['avatar'] = get('rocketchat_icon_url');\n    } elseif (get('rocketchat_icon_emoji')) {\n        $body['emoji'] = get('rocketchat_icon_emoji');\n    }\n\n    Httpie::post(get('rocketchat_webhook'))->jsonBody($body)->send();\n});\n"
  },
  {
    "path": "contrib/rollbar.php",
    "content": "<?php\n/*\n\n## Configuration\n\n- `rollbar_token` – access token to rollbar api\n- `rollbar_comment` – comment about deploy, default to\n  ```php\n  set('rollbar_comment', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\n  ```\n- `rollbar_username` – rollbar user name\n\n## Usage\n\nSince you should only notify Rollbar channel of a successful deployment, the `rollbar:notify` task should be executed right at the end.\n\n```php\nafter('deploy', 'rollbar:notify');\n```\n\n */\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\nset('rollbar_comment', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\n\ndesc('Notifies Rollbar of deployment');\ntask('rollbar:notify', function () {\n    if (!get('rollbar_token', false)) {\n        return;\n    }\n\n    $params = [\n        'access_token' => get('rollbar_token'),\n        'environment' => get('where'),\n        'revision' => runLocally('git log -n 1 --format=\"%h\"'),\n        'local_username' => get('user'),\n        'rollbar_username' => get('rollbar_username'),\n        'comment' => get('rollbar_comment'),\n    ];\n\n    Httpie::post('https://api.rollbar.com/api/1/deploy/')\n        ->formBody($params)\n        ->send();\n})\n    ->once();\n"
  },
  {
    "path": "contrib/rsync.php",
    "content": "<?php\n/*\n:::warning\nThis must not be confused with `/src/Utility/Rsync.php`, deployer's built-in rsync. Their configuration options are also very different, read carefully below.\n:::\n\n## Configuration options\n\n- **rsync**: Accepts an array with following rsync options (all are optional and defaults are ok):\n    - *exclude*: accepts an *array* with patterns to be excluded from sending to server\n    - *exclude-file*: accepts a *string* containing absolute path to file, which contains exclude patterns\n    - *include*: accepts an *array* with patterns to be included in sending to server\n    - *include-file*: accepts a *string* containing absolute path to file, which contains include patterns\n    - *filter*: accepts an *array* of rsync filter rules\n    - *filter-file*: accepts a *string* containing merge-file filename.\n    - *filter-perdir*: accepts a *string* containing merge-file filename to be scanned and merger per each directory in rsync list on files to send\n    - *flags*: accepts a *string* of flags to set when calling rsync command. Please **avoid** flags that accept params, and use *options* instead.\n    - *options*: accepts an *array* of options to set when calling rsync command. **DO NOT** prefix options with `--` as it's automatically added.\n    - *timeout*: accepts an *int* defining timeout for rsync command to run locally.\n\n### Sample Configuration:\n\nFollowing is default configuration. By default rsync ignores only git dir and `deploy.php` file.\n\n```php\n// deploy.php\n\nset('rsync',[\n    'exclude'      => [\n        '.git',\n        'deploy.php',\n    ],\n    'exclude-file' => false,\n    'include'      => [],\n    'include-file' => false,\n    'filter'       => [],\n    'filter-file'  => false,\n    'filter-perdir'=> false,\n    'flags'        => 'rz', // Recursive, with compress\n    'options'      => ['delete'],\n    'timeout'      => 60,\n]);\n```\n\nIf You have multiple excludes, You can put them in file and reference that instead. If You use `deploy:rsync_warmup` You could set additional options that could speed-up and/or affect way things are working. For example:\n\n```php\n// deploy.php\n\nset('rsync',[\n    'exclude'       => ['excludes_file'],\n    'exclude-file'  => '/tmp/localdeploys/excludes_file', //Use absolute path to avoid possible rsync problems\n    'include'       => [],\n    'include-file'  => false,\n    'filter'        => [],\n    'filter-file'   => false,\n    'filter-perdir' => false,\n    'flags'         => 'rzcE', // Recursive, with compress, check based on checksum rather than time/size, preserve Executable flag\n    'options'       => ['delete', 'delete-after', 'force'], //Delete after successful transfer, delete even if deleted dir is not empty\n    'timeout'       => 3600, //for those huge repos or crappy connection\n]);\n```\n\n\n### Parameter\n\n- **rsync_src**: per-host rsync source. This can be server, stage or whatever-dependent. By default it's set to current directory\n- **rsync_dest**: per-host rsync destination. This can be server, stage or whatever-dependent. by default it's equivalent to release deploy destination.\n\n### Sample configurations:\n\nThis is default configuration:\n\n```php\nset('rsync_src', __DIR__);\nset('rsync_dest','{{release_path}}');\n```\n\nIf You use local deploy recipe You can set src to local release:\n\n```php\nhost('hostname')\n    ->hostname('10.10.10.10')\n    ->port(22)\n    ->set('deploy_path','/your/remote/path/app')\n    ->set('rsync_src', '/your/local/path/app')\n    ->set('rsync_dest','{{release_path}}');\n```\n\n## Usage\n\n- `rsync` task\n\n    Set `rsync_src` to locally cloned repository and rsync to `rsync_dest`. Then set this task instead of `deploy:update_code` in Your `deploy` task if Your hosting provider does not allow git.\n\n- `rsync:warmup` task\n\n    If Your deploy task looks like:\n\n    ```php\n    task('deploy', [\n        'deploy:prepare',\n        'deploy:release',\n        'rsync',\n        'deploy:vendors',\n        'deploy:symlink',\n    ])->desc('Deploy your project');\n    ```\n\n    And Your `rsync_dest` is set to `{{release_path}}` then You could add this task to run before `rsync` task or after `deploy:release`, whatever is more convenient.\n\n */\n\nnamespace Deployer;\n\nuse Deployer\\Host\\Localhost;\nuse Deployer\\Task\\Context;\n\nset('rsync', [\n    'exclude' => [\n        '.git',\n        'deploy.php',\n    ],\n    'exclude-file' => false,\n    'include' => [],\n    'include-file' => false,\n    'filter' => [],\n    'filter-file' => false,\n    'filter-perdir' => false,\n    'flags' => 'rz',\n    'options' => ['delete'],\n    'timeout' => 300,\n]);\n\nset('rsync_src', __DIR__);\nset('rsync_dest', '{{release_path}}');\n\nset('rsync_excludes', function () {\n    $config = get('rsync');\n    $excludes = $config['exclude'];\n    $excludeFile = $config['exclude-file'];\n    $excludesRsync = '';\n    foreach ($excludes as $exclude) {\n        $excludesRsync .= ' --exclude=' . escapeshellarg($exclude);\n    }\n    if (!empty($excludeFile) && file_exists($excludeFile) && is_file($excludeFile) && is_readable($excludeFile)) {\n        $excludesRsync .= ' --exclude-from=' . escapeshellarg($excludeFile);\n    }\n\n    return $excludesRsync;\n});\n\nset('rsync_includes', function () {\n    $config = get('rsync');\n    $includes = $config['include'];\n    $includeFile = $config['include-file'];\n    $includesRsync = '';\n    foreach ($includes as $include) {\n        $includesRsync .= ' --include=' . escapeshellarg($include);\n    }\n    if (!empty($includeFile) && file_exists($includeFile) && is_file($includeFile) && is_readable($includeFile)) {\n        $includesRsync .= ' --include-from=' . escapeshellarg($includeFile);\n    }\n\n    return $includesRsync;\n});\n\nset('rsync_filter', function () {\n    $config = get('rsync');\n    $filters = $config['filter'];\n    $filterFile = $config['filter-file'];\n    $filterPerDir = $config['filter-perdir'];\n    $filtersRsync = '';\n    foreach ($filters as $filter) {\n        $filtersRsync .= \" --filter='$filter'\";\n    }\n    if (!empty($filterFile)) {\n        $filtersRsync .= \" --filter='merge $filterFile'\";\n    }\n    if (!empty($filterPerDir)) {\n        $filtersRsync .= \" --filter='dir-merge $filterPerDir'\";\n    }\n    return $filtersRsync;\n});\n\nset('rsync_options', function () {\n    $config = get('rsync');\n    $options = $config['options'];\n    $optionsRsync = [];\n    foreach ($options as $option) {\n        $optionsRsync[] = \"--$option\";\n    }\n    return implode(' ', $optionsRsync);\n});\n\n\ndesc('Warmups remote Rsync target');\ntask('rsync:warmup', function () {\n    $config = get('rsync');\n\n    $source = \"{{current_path}}\";\n    $destination = \"{{deploy_path}}/release\";\n\n    if (test(\"[ -d $(echo $source) ]\")) {\n        run(\"rsync -{$config['flags']} {{rsync_options}}{{rsync_excludes}}{{rsync_includes}}{{rsync_filter}} $source/ $destination/\");\n    } else {\n        writeln(\"<comment>No way to warmup rsync.</comment>\");\n    }\n});\n\n\ndesc('Rsync local->remote');\ntask('rsync', function () {\n    $config = get('rsync');\n\n    $src = get('rsync_src');\n    while (is_callable($src)) {\n        $src = $src();\n    }\n\n    if (!trim($src)) {\n        // if $src is not set here rsync is going to do a directory listing\n        // exiting with code 0, since only doing a directory listing clearly\n        // is not what we want to achieve we need to throw an exception\n        throw new \\RuntimeException('You need to specify a source path.');\n    }\n\n    $dst = get('rsync_dest');\n    while (is_callable($dst)) {\n        $dst = $dst();\n    }\n\n    if (!trim($dst)) {\n        // if $dst is not set here we are going to sync to root\n        // and even worse - depending on rsync flags and permission -\n        // might end up deleting everything we have write permission to\n        throw new \\RuntimeException('You need to specify a destination path.');\n    }\n\n    $rsyncFlags = (is_string($config['flags']) && trim($config['flags']) !== '') ? \"-{$config['flags']}\" : '';\n\n    $host = Context::get()->getHost();\n    if ($host instanceof Localhost) {\n        runLocally(\"rsync {$rsyncFlags} {{rsync_options}}{{rsync_includes}}{{rsync_excludes}}{{rsync_filter}} '$src/' '$dst/'\", timeout: $config['timeout']);\n        return;\n    }\n\n    $sshArguments = $host->connectionOptionsString();\n    runLocally(\"rsync {$rsyncFlags} -e 'ssh $sshArguments' {{rsync_options}}{{rsync_includes}}{{rsync_excludes}}{{rsync_filter}} '$src/' '{$host->connectionString()}:$dst/'\", timeout: $config['timeout']);\n});\n"
  },
  {
    "path": "contrib/sentry.php",
    "content": "<?php\n/*\n\n### Configuration options\n\n- **organization** *(required)*: the slug of the organization the release belongs to.\n- **projects** *(required)*: array of slugs of the projects to create a release for.\n- **token** *(required)*: authentication token. Can be created at [https://sentry.io/settings/account/api/auth-tokens/]\n- **version** *(required)* – a version identifier for this release.\nCan be a version number, a commit hash etc. (Defaults is set to git log -n 1 --format=\"%h\".)\n- **version_prefix** *(optional)* - a string prefixed to version.\nReleases are global per organization so indipentent projects needs to prefix version number with unique string to avoid conflicts\n- **environment** *(optional)* - the environment you’re deploying to. By default framework's environment is used.\nFor example for symfony, *symfony_env* configuration is read otherwise defaults to 'prod'.\n- **ref** *(optional)* – an optional commit reference. This is useful if a tagged version has been provided.\n- **refs** *(optional)* - array to indicate the start and end commits for each repository included in a release.\nHead commits must include parameters *repository* and *commit*) (the HEAD sha).\nThey can optionally include *previousCommit* (the sha of the HEAD of the previous release),\nwhich should be specified if this is the first time you’ve sent commit data.\n- **commits** *(optional)* - array commits data to be associated with the release.\nCommits must include parameters *id* (the sha of the commit), and can optionally include *repository*,\n*message*, *author_name*, *author_email* and *timestamp*. By default will send all new commits,\nunless it's a first release, then only first 200 will be sent.\n- **url** *(optional)* – a URL that points to the release. This can be the path to an online interface to the sourcecode for instance.\n- **date_released** *(optional)* – date that indicates when the release went live. If not provided the current time is assumed.\n- **sentry_server** *(optional)* – sentry server (if you host it yourself). defaults to hosted sentry service.\n- **date_deploy_started** *(optional)* - date that indicates when the deploy started. Defaults to current time.\n- **date_deploy_finished** *(optional)* - date that indicates when the deploy ended. If not provided, the current time is used.\n- **deploy_name** *(optional)* - name of the deploy\n- **git_version_command** *(optional)* - the command that retrieves the git version information (Defaults is set to git log -n 1 --format=\"%h\", other options are git describe --tags --abbrev=0)\n\n```php\n// deploy.php\n\nset('sentry', [\n    'organization' => 'exampleorg',\n    'projects' => [\n        'exampleproj'\n    ],\n    'token' => 'd47828...',\n    'version' => '0.0.1',\n\n]);\n```\n\n### Suggested Usage\n\nSince you should only notify Sentry of a successful deployment, the deploy:sentry task should be executed right at the end.\n\n```php\n// deploy.php\n\nafter('deploy', 'deploy:sentry');\n```\n\n */\n\nnamespace Deployer;\n\nuse Closure;\nuse DateTime;\nuse Deployer\\Exception\\ConfigurationException;\nuse Deployer\\Utility\\Httpie;\n\ndesc('Notifies Sentry of deployment');\ntask(\n    'deploy:sentry',\n    static function () {\n        $now = date('c');\n\n        $defaultConfig = [\n            'version' => getReleaseGitRef(),\n            'version_prefix' => null,\n            'refs' => [],\n            'ref' => null,\n            'commits' => getGitCommitsRefs(),\n            'url' => null,\n            'date_released' => $now,\n            'date_deploy_started' => $now,\n            'date_deploy_finished' => $now,\n            'sentry_server' => 'https://sentry.io',\n            'previous_commit' => null,\n            'environment' => get('symfony_env', 'prod'),\n            'deploy_name' => null,\n        ];\n\n        $config = array_merge($defaultConfig, (array) get('sentry'));\n        array_walk(\n            $config,\n            static function (&$value) use ($config) {\n                if (is_callable($value)) {\n                    $value = $value($config);\n                }\n            },\n        );\n\n        if (\n            !isset($config['organization'], $config['token'], $config['version'])\n            || (empty($config['projects']) || !is_array($config['projects']))\n        ) {\n            throw new \\RuntimeException(\n                <<<EXAMPLE\n                    Required data missing. Please configure sentry:\n                    set(\n                        'sentry',\n                        [\n                            'organization' => 'exampleorg',\n                            'projects' => [\n                                'exampleproj',\n                                'exampleproje2'\n                            ],\n                            'token' => 'd47828...',\n                        ]\n                    );\"\n                    EXAMPLE,\n            );\n        }\n\n        $releaseData = array_filter(\n            [\n                'version' => ($config['version_prefix'] ?? '') . $config['version'],\n                'refs' => $config['refs'],\n                'ref' => $config['ref'],\n                'url' => $config['url'],\n                'commits' => array_slice($config['commits'] ?? [], 0), // reset keys to serialize as array in json\n                'dateReleased' => $config['date_released'],\n                'projects' => $config['projects'],\n                'previousCommit' => $config['previous_commit'],\n            ],\n        );\n\n        $releasesApiUrl = $config['sentry_server'] . '/api/0/organizations/' . $config['organization'] . '/releases/';\n        $response = Httpie::post(\n            $releasesApiUrl,\n        )\n            ->setopt(CURLOPT_TIMEOUT, 10)\n            ->header('Authorization', sprintf('Bearer %s', $config['token']))\n            ->jsonBody($releaseData)\n            ->getJson();\n\n        if (!isset($response['version'], $response['projects'])) {\n            throw new \\RuntimeException(sprintf('Unable to create a release: %s', print_r($response, true)));\n        }\n\n        writeln(\n            sprintf(\n                '<info>Sentry:</info> Release of version <comment>%s</comment> ' .\n                'for projects: <comment>%s</comment> created successfully.',\n                $response['version'],\n                implode(', ', array_column($response['projects'], 'slug')),\n            ),\n        );\n\n        $deployData = array_filter(\n            [\n                'environment' => $config['environment'],\n                'name' => $config['deploy_name'],\n                'url' => $config['url'],\n                'dateStarted' => $config['date_deploy_started'],\n                'dateFinished' => $config['date_deploy_finished'],\n            ],\n        );\n\n        $response = Httpie::post(\n            $releasesApiUrl . $response['version'] . '/deploys/',\n        )\n            ->setopt(CURLOPT_TIMEOUT, 10)\n            ->header('Authorization', sprintf('Bearer %s', $config['token']))\n            ->jsonBody($deployData)\n            ->getJson();\n\n        if (!isset($response['id'], $response['environment'])) {\n            throw new \\RuntimeException(sprintf('Unable to create a deployment: %s', print_r($response, true)));\n        }\n\n        writeln(\n            sprintf(\n                '<info>Sentry:</info> Deployment <comment>%s</comment> ' .\n                'for environment <comment>%s</comment> created successfully',\n                $response['id'],\n                $response['environment'],\n            ),\n        );\n    },\n);\n\nfunction getPreviousReleaseRevision()\n{\n    switch (get('update_code_strategy')) {\n        case 'local_archive':\n        case 'archive':\n            if (has('previous_release')) {\n                return run('cat {{previous_release}}/REVISION');\n            }\n\n            return null;\n        case 'clone':\n            if (has('previous_release')) {\n                cd('{{previous_release}}');\n                return trim(run('git rev-parse HEAD'));\n            }\n\n            return null;\n        default:\n            throw new ConfigurationException(parse(\"Unknown `update_code_strategy` option: {{update_code_strategy}}.\"));\n    }\n}\n\nfunction getCurrentReleaseRevision()\n{\n    switch (get('update_code_strategy')) {\n        case 'local_archive':\n        case 'archive':\n            return run('cat {{release_path}}/REVISION');\n\n        case 'clone':\n            cd('{{release_path}}');\n            return trim(run('git rev-parse HEAD'));\n\n        default:\n            throw new ConfigurationException(parse(\"Unknown `update_code_strategy` option: {{update_code_strategy}}.\"));\n    }\n}\n\nfunction getReleaseGitRef(): Closure\n{\n    return static function ($config = []): string {\n        $strategy = get('update_code_strategy');\n\n        if ($strategy === 'archive') {\n            cd('{{deploy_path}}/.dep/repo');\n        } else {\n            cd('{{release_path}}');\n        }\n\n        if (isset($config['git_version_command'])) {\n            return trim(run($config['git_version_command']));\n        }\n\n        if ($strategy !== 'clone') {\n            return run('cat {{current_path}}/REVISION');\n        }\n\n        return trim(run('git log -n 1 --format=\"%h\"'));\n    };\n}\n\nfunction getGitCommitsRefs(): Closure\n{\n    return static function ($config = []): array {\n        $previousReleaseRevision = getPreviousReleaseRevision();\n        $currentReleaseRevision = getCurrentReleaseRevision() ?: 'HEAD';\n\n        if ($previousReleaseRevision === null) {\n            $commitRange = $currentReleaseRevision;\n        } else {\n            $commitRange = $previousReleaseRevision . '..' . $currentReleaseRevision;\n        }\n\n        try {\n            if (get('update_code_strategy') === 'archive') {\n                cd('{{deploy_path}}/.dep/repo');\n            } else {\n                cd('{{release_path}}');\n            }\n\n            $result = run(sprintf('git rev-list --pretty=\"%s\" %s', 'format:%H#%an#%ae#%at#%s', $commitRange));\n            $lines = array_filter(\n                // limit number of commits for first release with many commits\n                array_map('trim', array_slice(explode(\"\\n\", $result), 0, 200)),\n                static function (string $line): bool {\n                    return !empty($line) && strpos($line, 'commit') !== 0;\n                },\n            );\n\n            return array_map(\n                static function (string $line): array {\n                    [$ref, $authorName, $authorEmail, $timestamp, $message] = explode('#', $line, 5);\n\n                    return [\n                        'id' => $ref,\n                        'author_name' => $authorName,\n                        'author_email' => $authorEmail,\n                        'message' => $message,\n                        'timestamp' => date(\\DateTime::ATOM, (int) $timestamp),\n                    ];\n                },\n                $lines,\n            );\n\n        } catch (\\Deployer\\Exception\\RunException $e) {\n            writeln($e->getMessage());\n            return [];\n        }\n    };\n}\n"
  },
  {
    "path": "contrib/slack.php",
    "content": "<?php\n/*\n## Installing\n\n<a href=\"https://slack.com/oauth/authorize?&client_id=113734341365.225973502034&scope=incoming-webhook\"><img alt=\"Add to Slack\" height=\"40\" width=\"139\" src=\"https://platform.slack-edge.com/img/add_to_slack.png\" srcset=\"https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x\" /></a>\n\n\nAdd hook on deploy:\n\n```php\nbefore('deploy', 'slack:notify');\n```\n\n## Configuration\n\n- `slack_webhook` – slack incoming webhook url, **required**\n  ```\n  set('slack_webhook', 'https://hooks.slack.com/...');\n  ```\n- `slack_channel` - channel to send notification to. The default is the channel configured in the webhook\n- `slack_title` – the title of application, default `{{application}}`\n- `slack_text` – notification message template, markdown supported\n  ```\n  set('slack_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\n  ```\n- `slack_success_text` – success template, default:\n  ```\n  set('slack_success_text', 'Deploy to *{{where}}* successful');\n  ```\n- `slack_failure_text` – failure template, default:\n  ```\n  set('slack_failure_text', 'Deploy to *{{where}}* failed');\n  ```\n\n- `slack_color` – color's attachment\n- `slack_success_color` – success color's attachment\n- `slack_failure_color` – failure color's attachment\n- `slack_fields` - set attachments fields for pretty output in Slack, default:\n  ```\n  set('slack_fields', []);\n  ```\n\n## Usage\n\nIf you want to notify only about beginning of deployment add this line only:\n\n```php\nbefore('deploy', 'slack:notify');\n```\n\nIf you want to notify about successful end of deployment add this too:\n\n```php\nafter('deploy:success', 'slack:notify:success');\n```\n\nIf you want to notify about failed deployment add this too:\n\n```php\nafter('deploy:failed', 'slack:notify:failure');\n```\n\n */\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\n// Channel to publish to, when false the default channel the webhook will be used\nset('slack_channel', false);\n\n// Title of project\nset('slack_title', function () {\n    return get('application', 'Project');\n});\n\n// Deploy message\nset('slack_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\nset('slack_success_text', 'Deploy to *{{where}}* successful');\nset('slack_failure_text', 'Deploy to *{{where}}* failed');\nset('slack_rollback_text', '_{{user}}_ rolled back changes on *{{where}}*');\nset('slack_fields', []);\n\n// Color of attachment\nset('slack_color', '#4d91f7');\nset('slack_success_color', '#00c100');\nset('slack_failure_color', '#ff0909');\nset('slack_rollback_color', '#eba211');\n\nfunction checkSlackAnswer($result)\n{\n    if ('invalid_token' === $result) {\n        warning('Invalid Slack token');\n        return false;\n    }\n    return true;\n}\n\ndesc('Notifies Slack');\ntask('slack:notify', function () {\n    if (!get('slack_webhook', false)) {\n        warning('No Slack webhook configured');\n        return;\n    }\n\n    $attachment = [\n        'title' => get('slack_title'),\n        'text' => get('slack_text'),\n        'color' => get('slack_color'),\n        'mrkdwn_in' => ['text'],\n    ];\n\n    $result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send();\n    checkSlackAnswer($result);\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies Slack about deploy finish');\ntask('slack:notify:success', function () {\n    if (!get('slack_webhook', false)) {\n        warning('No Slack webhook configured');\n        return;\n    }\n\n    $attachment = [\n        'title' => get('slack_title'),\n        'text' => get('slack_success_text'),\n        'color' => get('slack_success_color'),\n        'fields' => get('slack_fields'),\n        'mrkdwn_in' => ['text'],\n    ];\n\n    $result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send();\n    checkSlackAnswer($result);\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies Slack about deploy failure');\ntask('slack:notify:failure', function () {\n    if (!get('slack_webhook', false)) {\n        warning('No Slack webhook configured');\n        return;\n    }\n\n    $attachment = [\n        'title' => get('slack_title'),\n        'text' => get('slack_failure_text'),\n        'color' => get('slack_failure_color'),\n        'mrkdwn_in' => ['text'],\n    ];\n\n    $result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send();\n    checkSlackAnswer($result);\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies Slack about rollback');\ntask('slack:notify:rollback', function () {\n    if (!get('slack_webhook', false)) {\n        warning('No Slack webhook configured');\n        return;\n    }\n\n    $attachment = [\n        'title' => get('slack_title'),\n        'text' => get('slack_rollback_text'),\n        'color' => get('slack_rollback_color'),\n        'mrkdwn_in' => ['text'],\n    ];\n\n    $result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send();\n    checkSlackAnswer($result);\n})\n    ->once()\n    ->hidden();\n"
  },
  {
    "path": "contrib/supervisord-monitor.php",
    "content": "<?php\n/*\n### Description\nThis is a recipe that uses the [Supervisord server monitoring project](https://github.com/mlazarov/supervisord-monitor).\n\nWith this recipe the possibility is created to restart a supervisord process through the Supervisor Monitor webtool, by using cURL. This workaround is particular usefull when the deployment user has unsuficient rights to restart a daemon process from the cli.\n\n### Configuration\n\n```\nset('supervisord', [\n    'uri' => 'https://youruri.xyz/supervisor',\n    'basic_auth_user' => 'username',\n    'basic_auth_password' => 'password',\n    'process_name' => 'process01',\n]);\n```\n\nor\n\n```\nset('supervisord_uri', 'https://youruri.xyz/supervisor');\nset('supervisord_basic_auth_user', 'username');\nset('supervisord_basic_auth_password', 'password');\nset('supervisord_process_name', 'process01');\n```\n\n- `supervisord` – array with configuration for Supervisord\n    - `uri` – URI to the Supervisord monitor page\n    - `basic_auth_user` – Basic auth username to access the URI\n    - `basic_auth_password` – Basic auth password to access the URI\n    - `process_name` – the process name, as visible in the Supervisord monitor page. Multiple processes can be listed here, comma separated\n\n### Task\n\n- `supervisord-monitor:restart` Restarts given processes\n- `supervisord-monitor:stop` Stops given processes\n- `supervisord-monitor:start` Starts given processes\n\n### Usage\n\nA complete example with configs, staging and deployment\n\n```\n<?php\n\nnamespace Deployer;\nuse Dotenv\\Dotenv;\n\nrequire 'vendor/autoload.php';\n\nrequire 'supervisord_monitor.php';\n\n// Project name\nset('application', 'myproject.com');\n\n// Project repository\nset('repository', 'git@github.com:myorg/myproject.com');\n\nset('supervisord', [\n    'uri' => 'https://youruri.xyz/supervisor',\n    'basic_auth_user' => 'username',\n    'basic_auth_password' => 'password',\n    'process_name' => 'process01',\n]);\n\nhost('staging.myproject.com')\n    ->set('branch', 'develop')\n    ->set('labels', ['stage' => 'staging']);\n\nhost('myproject.com')\n    ->set('branch', 'main')\n    ->set('labels', ['stage' => 'production']);\n\n// Tasks\ntask('build', function () {\n    run('cd {{release_path}} && build');\n});\n\ntask('deploy', [\n    'build',\n    'supervisord',\n]);\n\ntask('supervisord', ['supervisord-monitor:restart'])\n    ->select('stage=production');\n```\n*/\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\nfunction supervisordCheckConfig()\n{\n    $config = get('supervisord', []);\n    foreach ($config as $key => $value) {\n        if ($value) {\n            set('supervisord_' . $key, $value);\n        }\n    }\n\n    if (!get('supervisord_uri') ||\n        !get('supervisord_basic_auth_user') ||\n        !get('supervisord_basic_auth_password') ||\n        !get('supervisord_process_name')) {\n        throw new \\RuntimeException(\"<comment>Please configure Supervisord config:</comment> <info>set('supervisord', array('uri' => 'yourdomain.xyz/supervisor', 'basic_auth_user' => 'abc' , 'basic_auth_password' => 'xyz', 'process_name' => 'process01,process02'));</info> or <info>set('supervisord_uri', 'yourdomain.xyz/supervisor'); set('supervisord_basic_auth_user', 'abc'); etc</info>\");\n    }\n}\n\nfunction supervisordGetBasicAuthToken()\n{\n    return 'Basic ' . base64_encode(get('supervisord_basic_auth_user') . ':' . get('supervisord_basic_auth_password'));\n}\n\nfunction supervisordIsAuthenticated()\n{\n    supervisordCheckConfig();\n\n    $authResponseInfo = [];\n    Httpie::post(get('supervisord_uri'))->header('Authorization', supervisordGetBasicAuthToken())->send($authResponseInfo);\n\n    return $authResponseInfo['http_code'] === 200;\n}\n\nfunction supervisordControlAction($name, $action = 'stop')\n{\n    $stopResponseInfo = [];\n    Httpie::post(get('supervisord_uri') . '/control/' . $action . '/localhost/' . $name)->header('Authorization', supervisordGetBasicAuthToken())->send($stopResponseInfo);\n\n    return $stopResponseInfo['http_code'] === 200;\n}\n\ntask('supervisord-monitor:restart', function () {\n    if (supervisordIsAuthenticated()) {\n        $names = explode(',', get('supervisord_process_name'));\n        foreach ($names as $name) {\n            $name = trim($name);\n            if (supervisordControlAction($name, 'stop')) {\n                writeln('Daemon [' . $name . '] stopped');\n                if (supervisordControlAction($name, 'start')) {\n                    writeln('Daemon [' . $name . '] started');\n                }\n            }\n        }\n    } else {\n        writeln('Authentication failed');\n    }\n});\n\ntask('supervisord-monitor:stop', function () {\n    if (supervisordIsAuthenticated()) {\n        $names = explode(',', get('supervisord_process_name'));\n        foreach ($names as $name) {\n            $name = trim($name);\n            if (supervisordControlAction($name, 'stop')) {\n                writeln('Daemon [' . $name . '] stopped');\n            }\n        }\n    } else {\n        writeln('Authentication failed');\n    }\n});\n\ntask('supervisord-monitor:start', function () {\n    if (supervisordIsAuthenticated()) {\n        $names = explode(',', get('supervisord_process_name'));\n        foreach ($names as $name) {\n            $name = trim($name);\n            if (supervisordControlAction($name, 'start')) {\n                writeln('Daemon [' . $name . '] started');\n            }\n        }\n    } else {\n        writeln('Authentication failed');\n    }\n});\n"
  },
  {
    "path": "contrib/telegram.php",
    "content": "<?php\n/*\n## Installing\n  1. Create telegram bot with [BotFather](https://t.me/BotFather) and grab the token provided\n  2. Send `/start` to your bot and open https://api.telegram.org/bot{$TELEGRAM_TOKEN_HERE}/getUpdates\n  3. Take chat_id from response\n\n\nAdd hook on deploy:\n\n```php\nbefore('deploy', 'telegram:notify');\n```\n\n## Configuration\n\n- `telegram_token` – telegram bot token, **required**\n- `telegram_chat_id` — chat ID to push messages to\n- `telegram_proxy` - proxy connection string in [CURLOPT_PROXY](https://curl.haxx.se/libcurl/c/CURLOPT_PROXY.html) form like:\n  ```\n  http://proxy:80\n  socks5://user:password@host:3128\n   ```\n- `telegram_title` – the title of application, default `{{application}}`\n- `telegram_text` – notification message template\n  ```\n  _{{user}}_ deploying `{{what}}` to *{{where}}*\n  ```\n- `telegram_success_text` – success template, default:\n  ```\n  Deploy to *{{where}}* successful\n\n  ```\n- `telegram_failure_text` – failure template, default:\n  ```\n  Deploy to *{{where}}* failed\n  ```\n\n## Usage\n\nIf you want to notify only about beginning of deployment add this line only:\n\n```php\nbefore('deploy', 'telegram:notify');\n```\n\nIf you want to notify about successful end of deployment add this too:\n\n```php\nafter('deploy:success', 'telegram:notify:success');\n```\nIf you want to notify about failed deployment add this too:\n\n```php\nafter('deploy:failed', 'telegram:notify:failure');\n\n\n */\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\n// Title of project\nset('telegram_title', function () {\n    return get('application', 'Project');\n});\n\n// Telegram settings\nset('telegram_token', function () {\n    throw new \\Exception('Please, configure \"telegram_token\" parameter.');\n});\nset('telegram_chat_id', function () {\n    throw new \\Exception('Please, configure \"telegram_chat_id\" parameter.');\n});\nset('telegram_url', function () {\n    return 'https://api.telegram.org/bot' . get('telegram_token') . '/sendmessage';\n});\n\n// Deploy message\nset('telegram_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\nset('telegram_success_text', 'Deploy to *{{where}}* successful');\nset('telegram_failure_text', 'Deploy to *{{where}}* failed');\n\n\ndesc('Notifies Telegram');\ntask('telegram:notify', function () {\n    if (!get('telegram_token', false)) {\n        warning('No Telegram token configured');\n        return;\n    }\n\n    if (!get('telegram_chat_id', false)) {\n        warning('No Telegram chat id configured');\n        return;\n    }\n\n    $telegramUrl = get('telegram_url') . '?' . http_build_query(\n        [\n            'chat_id' => get('telegram_chat_id'),\n            'text' => get('telegram_text'),\n            'parse_mode' => 'Markdown',\n        ],\n    );\n\n    $httpie = Httpie::get($telegramUrl);\n\n    if (get('telegram_proxy', '') !== '') {\n        $httpie = $httpie->setopt(CURLOPT_PROXY, get('telegram_proxy'));\n    }\n\n    $httpie->send();\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies Telegram about deploy finish');\ntask('telegram:notify:success', function () {\n    if (!get('telegram_token', false)) {\n        warning('No Telegram token configured');\n        return;\n    }\n\n    if (!get('telegram_chat_id', false)) {\n        warning('No Telegram chat id configured');\n        return;\n    }\n\n    $telegramUrl = get('telegram_url') . '?' . http_build_query(\n        [\n            'chat_id' => get('telegram_chat_id'),\n            'text' => get('telegram_success_text'),\n            'parse_mode' => 'Markdown',\n        ],\n    );\n\n    $httpie = Httpie::get($telegramUrl);\n\n    if (get('telegram_proxy', '') !== '') {\n        $httpie = $httpie->setopt(CURLOPT_PROXY, get('telegram_proxy'));\n    }\n\n    $httpie->send();\n})\n  ->once()\n  ->hidden();\n\ndesc('Notifies Telegram about deploy failure');\ntask('telegram:notify:failure', function () {\n    if (!get('telegram_token', false)) {\n        warning('No Telegram token configured');\n        return;\n    }\n\n    if (!get('telegram_chat_id', false)) {\n        warning('No Telegram chat id configured');\n        return;\n    }\n\n    $telegramUrl = get('telegram_url') . '?' . http_build_query(\n        [\n            'chat_id' => get('telegram_chat_id'),\n            'text' => get('telegram_failure_text'),\n            'parse_mode' => 'Markdown',\n        ],\n    );\n\n    $httpie = Httpie::get($telegramUrl);\n\n    if (get('telegram_proxy', '') !== '') {\n        $httpie = $httpie->setopt(CURLOPT_PROXY, get('telegram_proxy'));\n    }\n\n    $httpie->send();\n})\n  ->once()\n  ->hidden();\n"
  },
  {
    "path": "contrib/webpack_encore.php",
    "content": "<?php\n/*\n\n## Configuration\n\n- **webpack_encore/package_manager** *(optional)*: set yarn or npm. We try to find if yarn or npm is available and used.\n\n## Usage\n\n```php\n// For Yarn\nafter('deploy:update_code', 'yarn:install');\n// For npm\nafter('deploy:update_code', 'npm:install');\n\nafter('deploy:update_code', 'webpack_encore:build');\n```\n */\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/npm.php';\nrequire_once __DIR__ . '/yarn.php';\n\nset('webpack_encore/package_manager', function () {\n    if (test('[ -f {{release_path}}/yarn.lock ]')) {\n        return 'yarn';\n    }\n\n    return 'npm';\n});\n\nset('webpack_encore/env', 'production');\n\ndesc('Runs webpack encore build');\ntask('webpack_encore:build', function () {\n    $packageManager = get('webpack_encore/package_manager');\n\n    if (!in_array($packageManager, ['npm', 'yarn'], true)) {\n        throw new \\Exception(sprintf('Package Manager \"%s\" is not supported', $packageManager));\n    }\n\n    run(\"cd {{release_path}} && {{bin/$packageManager}} run encore {{webpack_encore/env}}\");\n});\n"
  },
  {
    "path": "contrib/workplace.php",
    "content": "<?php\n/*\nThis recipes works with Custom Integrations and Publishing Bots.\n\n\nAdd hook on deploy:\n\n```\nbefore('deploy', 'workplace:notify');\n```\n\n## Configuration\n\n - `workplace_webhook` - incoming workplace webhook **required**\n   ```\n   // With custom integration\n   set('workplace_webhook', 'https://graph.facebook.com/<GROUP_ID>/feed?access_token=<ACCESS_TOKEN>');\n\n   // With publishing bot\n   set('workplace_webhook', 'https://graph.facebook.com/v3.0/group/feed?access_token=<ACCESS_TOKEN>');\n\n   // Use markdown on message\n   set('workplace_webhook', 'https://graph.facebook.com/<GROUP_ID>/feed?access_token=<ACCESS_TOKEN>&formatting=MARKDOWN');\n   ```\n\n - `workplace_text` - notification message\n   ```\n   set('workplace_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\n   ```\n\n - `workplace_success_text` – success template, default:\n  ```\n  set('workplace_success_text', 'Deploy to *{{where}}* successful');\n  ```\n - `workplace_failure_text` – failure template, default:\n  ```\n  set('workplace_failure_text', 'Deploy to *{{where}}* failed');\n  ```\n - `workplace_edit_post` – whether to create a new post for deploy result, or edit the first one created, default creates a new post:\n  ```\n  set('workplace_edit_post', false);\n  ```\n\n## Usage\n\nIf you want to notify only about beginning of deployment add this line only:\n\n```php\nbefore('deploy', 'workplace:notify');\n```\n\nIf you want to notify about successful end of deployment add this too:\n\n```php\nafter('deploy:success', 'workplace:notify:success');\n```\n\nIf you want to notify about failed deployment add this too:\n\n```php\nafter('deploy:failed', 'workplace:notify:failure');\n```\n\n */\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\n// Deploy message\nset('workplace_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\nset('workplace_success_text', 'Deploy to *{{where}}* successful');\nset('workplace_failure_text', 'Deploy to *{{where}}* failed');\n\n// By default, create a new post for every message\nset('workplace_edit_post', false);\n\ndesc('Notifies Workplace');\ntask('workplace:notify', function () {\n    if (!get('workplace_webhook', false)) {\n        return;\n    }\n    $url = get('workplace_webhook') . '&message=' . urlencode(get('workplace_text'));\n    $response = Httpie::post($url)->getJson();\n\n    if (get('workplace_edit_post', false)) {\n        // Endpoint will be something like: https//graph.facebook.com/<POST_ID>?<QUERY_PARAMS>\n        $url = sprintf(\n            '%s://%s/%s?%s',\n            parse_url(get('workplace_webhook'), PHP_URL_SCHEME),\n            parse_url(get('workplace_webhook'), PHP_URL_HOST),\n            $response['id'],\n            parse_url(get('workplace_webhook'), PHP_URL_QUERY),\n        );\n        // Replace the webhook with a url that points to the created post\n        set('workplace_webhook', $url);\n    }\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies Workplace about deploy finish');\ntask('workplace:notify:success', function () {\n    if (!get('workplace_webhook', false)) {\n        return;\n    }\n    $url = get('workplace_webhook') . '&message=' . urlencode(get('workplace_success_text'));\n    Httpie::post($url)->send();\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies Workplace about deploy failure');\ntask('workplace:notify:failure', function () {\n    if (!get('workplace_webhook', false)) {\n        return;\n    }\n    $url = get('workplace_webhook') . '&message=' . urlencode(get('workplace_failure_text'));\n    Httpie::post($url)->send();\n})\n    ->once()\n    ->hidden();\n"
  },
  {
    "path": "contrib/yammer.php",
    "content": "<?php\n/*\n\nAdd hook on deploy:\n\n```php\nbefore('deploy', 'yammer:notify');\n```\n\n## Configuration\n\n- `yammer_url` – The URL to the message endpoint, default is https://www.yammer.com/api/v1/messages.json\n- `yammer_token` *(required)* – Yammer auth token\n- `yammer_group_id` *(required)* - Group ID\n- `yammer_title` – the title of application, default `{{application}}`\n- `yammer_body` – notification message template, default:\n  ```\n  <em>{{user}}</em> deploying {{what}} to <strong>{{where}}</strong>\n  ```\n- `yammer_success_body` – success template, default:\n  ```\n  Deploy to <strong>{{where}}</strong> successful\n  ```\n- `yammer_failure_body` – failure template, default:\n  ```\n  Deploy to <strong>{{where}}</strong> failed\n  ```\n\n## Usage\n\nIf you want to notify only about beginning of deployment add this line only:\n\n```php\nbefore('deploy', 'yammer:notify');\n```\n\nIf you want to notify about successful end of deployment add this too:\n\n```php\nafter('deploy:success', 'yammer:notify:success');\n```\n\nIf you want to notify about failed deployment add this too:\n\n```php\nafter('deploy:failed', 'yammer:notify:failure');\n```\n\n */\n\nnamespace Deployer;\n\nuse Deployer\\Utility\\Httpie;\n\nset('yammer_url', 'https://www.yammer.com/api/v1/messages.json');\n\n// Title of project\nset('yammer_title', function () {\n    return get('application', 'Project');\n});\n\n// Deploy message\nset('yammer_body', '<em>{{user}}</em> deploying {{what}} to <strong>{{where}}</strong>');\nset('yammer_success_body', 'Deploy to <strong>{{where}}</strong> successful');\nset('yammer_failure_body', 'Deploy to <strong>{{where}}</strong> failed');\n\ndesc('Notifies Yammer');\ntask('yammer:notify', function () {\n    $params = [\n        'is_rich_text' => 'true',\n        'message_type' => 'announcement',\n        'group_id' => get('yammer_group_id'),\n        'title' => get('yammer_title'),\n        'body' => get('yammer_body'),\n    ];\n\n    Httpie::post(get('yammer_url'))\n        ->header('Authorization', 'Bearer ' . get('yammer_token'))\n        ->header('Content-type', 'application/json')\n        ->jsonBody($params)\n        ->send();\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies Yammer about deploy finish');\ntask('yammer:notify:success', function () {\n    $params = [\n        'is_rich_text' => 'true',\n        'message_type' => 'announcement',\n        'group_id' => get('yammer_group_id'),\n        'title' => get('yammer_title'),\n        'body' => get('yammer_success_body'),\n    ];\n\n    Httpie::post(get('yammer_url'))\n        ->header('Authorization', 'Bearer ' . get('yammer_token'))\n        ->header('Content-type', 'application/json')\n        ->jsonBody($params)\n        ->send();\n})\n    ->once()\n    ->hidden();\n\ndesc('Notifies Yammer about deploy failure');\ntask('yammer:notify:failure', function () {\n    $params = [\n        'is_rich_text' => 'true',\n        'message_type' => 'announcement',\n        'group_id' => get('yammer_group_id'),\n        'title' => get('yammer_title'),\n        'body' => get('yammer_failure_body'),\n    ];\n\n    Httpie::post(get('yammer_url'))\n        ->header('Authorization', 'Bearer ' . get('yammer_token'))\n        ->header('Content-type', 'application/json')\n        ->jsonBody($params)\n        ->send();\n})\n    ->once()\n    ->hidden();\n"
  },
  {
    "path": "contrib/yarn.php",
    "content": "<?php\n/*\n## Configuration\n\n- **bin/yarn** *(optional)*: set Yarn binary, automatically detected otherwise.\n\n## Usage\n\n```php\nafter('deploy:update_code', 'yarn:install');\n```\n */\n\nnamespace Deployer;\n\nset('bin/yarn', function () {\n    return which('yarn');\n});\n\n// In there is a {{previous_release}}, node_modules will be copied from it before installing deps with yarn.\ndesc('Installs Yarn packages');\ntask('yarn:install', function () {\n    if (has('previous_release')) {\n        if (test('[ -d {{previous_release}}/node_modules ]')) {\n            run('cp -R {{previous_release}}/node_modules {{release_path}}');\n        }\n    }\n    run(\"cd {{release_path}} && {{bin/yarn}}\");\n});\n"
  },
  {
    "path": "docs/KNOWN_BUGS.md",
    "content": "# Known Bugs\n\n## Ubuntu 14.04, Coreutils 8.21\n\nThere are known bugs with relative symlinks `ln --relative`, which may cause the rollback command to fail.\n\nAdd the following line to your _deploy.php_ file:\n\n```php\nset('use_relative_symlink', false);\n```\n\n## OpenSSH_7.2p2\n\nControlPersist causes stderr to be left open until the master connection times out.\n\n- https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=714526\n- https://bugzilla.mindrot.org/show_bug.cgi?id=1988\n\n## cURL 7.29.0\n\nCertificate verification fails with multiple https urls.\n\n- https://bugzilla.redhat.com/show_bug.cgi?id=1241172\n\n## Rsync (3.1.3)\n\nArtifact upload with `rsync` is interrupted after the first chunk of data upload.\n\n```\nThe command \"rsync -azP -e 'ssh -A -p *** -o UserKnownHostsFile=/dev/null\n  -o StrictHostKeyChecking=no' 'artifacts/artifact.tar.gz' 'deploy@ssh.XXX.io:/srv/releases/2009076181'\" failed.\n\nExit Code: 255(Unknown error)\n\nOutput:\n================\nsending incremental file list\nartifact.tar.gz\n     32,768   0%    0.00kB/s    0:00:00\n\nError Output:\n================\nclient_loop: send disconnect: Broken pipe\n\nrsync: [sender] write error: Broken pipe (32)\n```\n\nIn order to resolve (workaround) the issue, you need to add `--bwlimit=4096` to the list of options.\n\nExample:\n\n```php\ntask('artifact:upload', function () {\n    upload(get('artifact_path'), '{{release_path}}', ['options' => ['--bwlimit=4096']]);\n});\n```\n\nThe issue was also described in the [Github Action](https://github.com/deployphp/action/issues/35).\n"
  },
  {
    "path": "docs/UPGRADE.md",
    "content": "# Upgrade a major version\n\n## Upgrade from 7.x to 8.x\n\n- `run()` and `runLocally()` doesn't accept `options` parameter anymore. Use named arguments instead.\n   - `no_throw` is now `nothrow`.\n   - `real_time_output` is now `forceOutput`.\n   - `idle_timeout` is now `idleTimeout`.\n\n## Upgrade from 6.x to 7.x\n\n### Step 1: Update deploy.php\n\n1. Change config `hostname` to `alias`.\n2. Change config `real_hostname` to `hostname`.\n3. Change config `user` to `remote_user`.\n4. Update `host()` definitions:\n   1. Add `set` prefix to all setters: `identityFile` -> `setIdentityFile` or `set('identity_file')`\n   2. Update `host(...)->addSshOption('UserKnownHostsFile', '/dev/null')` to `host(...)->setSshArguments(['-o UserKnownHostsFile=/dev/null']);`\n   3. Replace _stage_ with labels, i.e.\n      ```php\n      host('deployer.org')\n          ->set('labels', ['stage' => 'prod']);\n      ```\n      When deploying instead of `dep deploy prod` use `dep deploy stage=prod`.\n   4. `alias()` is deleted, `host()` itself sets alias and hostname, to override hostname use `setHostname()`.\n5. Update `task()` definitions.\n   1. Replace `onRoles()` with `select()`:\n      ```php\n      task(...)\n          ->select('stage=prod');\n      ```\n   2. Don't use string-based task definition, it's not available anymore. Don't forget to set correct working directory.\n      ```php\n      # from\n      task('deploy:npm-install', 'npm clean-install');\n      \n      # to\n      task('deploy:npm-install', function() {\n          cd('{{release_path}}');\n          run('npm clean-install');\n      });\n      ```\n   3. Remove `shallow()` tasks options.      \n6. Third party recipes now live inside main Deployer repo in _contrib_:\n   ```php\n   require 'contrib/rsync.php';\n   ```\n7. Replace `inventory()` with `import()`. It now can import hosts, configs, tasks:\n\n   ```yaml\n   import: recipe/common.php\n\n   config:\n     application: deployer\n     shared_dirs:\n       - uploads\n       - storage/logs/\n       - storage/db\n     shared_files:\n       - .env\n       - config/test.yaml\n     keep_releases: 3\n     http_user: false\n\n   hosts:\n     prod:\n       local: true\n\n   tasks:\n     deploy:\n       - deploy:prepare\n       - deploy:vendors\n       - deploy:publish\n\n     deploy:vendors:\n       - run: \"cd {{release_path}} && echo {{bin/composer}} {{composer_options}} 2>&1\"\n   ```\n\n8. Rename task `success` to `deploy:success` and `cleanup` to `deploy:cleanup`.\n9. Verbosity functions (`isDebug()`, etc) got deleted. Use `output()->isDebug()` instead.\n10. `runLocally()` commands are executed relative to the recipe file directory. This behaviour can be overridden via an environment variable:\n    ```\n    DEPLOYER_ROOT=. vendor/bin/dep taskname\n    ```\n11. Replace `local()` tasks with combination of `once()` and `runLocally()` func.\n12. Replace `locateBinaryPath()` with `which()` func.\n13. Replace `default_stage` with `default_selector`, and adjust the value accordingly (for example: \"prod\" to \"stage=prod\").\n14. Replace `onHosts()` and `onStage()` with [labels & selectors](selector.md).\n15. Replace `setPrivate()` with [`hidden()`](tasks.md#hidden).\n16. Configuration property `writable_recursive` defaults to `false`. This behaviour can be overridden with:\n   ```php\n   set('writable_recursive', true);\n   ```\n17. `.git` directory is not present in release directory anymore. The previous behavior can be restored with:\n   ```php\n   set('update_code_strategy', 'clone');\n   ```\n\n### Step 2: Deploy\n\nSince the release history numbering is not compatible between v6 and v7, you need to specify the `release_name` manually for the first time. Otherwise you start with release 1.\n\n1. Find out next release name (ssh to the host, `ls` releases dir, find the biggest number). Example: `42`.\n2. Deploy with release_name:\n   ```\n   dep deploy -o release_name=43\n   ```\n\n:::note\nIn case a rollback is needed, manually change the `current` symlink:\n\n```\nln -nfs releases/42 current\n```\n\n:::\n\n:::note\nIn case there are multiple hosts with different release names, you should create a `{{deploy_path}}/.dep/latest_release` file in each host with the current release number of that particular host.\n:::\n\n## Upgrade from 5.x to 6.x\n\n1. Changed branch option priority\n\n   If you have host definition with `branch(...)` parameter, adding `--branch` option will not override it any more.\n   If no `branch(...)` parameter persists, branch will be fetched from current local git branch.\n\n   ```php\n   host('prod')\n       ->set('branch', 'production')\n   ```\n\n   In order to return to old behavior add checking of `--branch` option.\n\n   ```php\n   host('prod')\n       ->set('branch', function () {\n           return input()->getOption('branch') ?: 'production';\n       })\n   ```\n\n2. Add `deploy:info` task to the beginning to `deploy` task.\n3. `run` returns string instead of `Deployer\\Type\\Result`\n\n   Now `run` and `runLocally` returns `string` instead of `Deployer\\Type\\Result`.\n   Replace method calls as:\n\n   - `run('command')->toString()` → `run('command')`\n   - `run('if command; then echo \"true\"; fi;')->toBool()` → `test('command')`\n\n4. `env_vars` renamed to `env`\n\n   - `set('env_vars', 'FOO=bar');` → `set('env', ['FOO' => 'bar']);`\n\n   If your are using Symfony recipe, then you need to change `env` setting:\n\n   - `set('env', 'prod');` → `set('symfony_env', 'prod');`\n\n## Upgrade from 4.x to 5.x\n\n1. Servers to Hosts\n\n   - `server($hostname)` to `host($hostname)`, and `server($name, $hostname)` to `host($name)->hostname($hostname)`\n   - `localServer($name)` to `localhost()`\n   - `cluster($name, $nodes, $port)` to `hosts(...$hodes)`\n   - `serverList($file)` to `inventory($file)`\n\n   If you need to deploy to same server use [host aliases](https://deployer.org/docs/hosts#host-aliases):\n\n   ```php\n   host('domain.com/green', 'domain.com/blue')\n       ->set('deploy_path', '~/{{hostname}}')\n       ...\n   ```\n\n   Or you can define different hosts with same hostname:\n\n   ```php\n   host('production')\n       ->hostname('domain.com')\n       ->set('deploy_path', '~/production')\n       ...\n\n   host('beta')\n       ->hostname('domain.com')\n       ->set('deploy_path', '~/beta')\n       ...\n   ```\n\n2. Configuration options\n\n   - Rename `{{server.name}}` to `{{hostname}}`\n\n3. DotArray syntax\n\n   In v5 access to nested arrays in config via dot notation was removed.\n   If you was using it, consider to move to plain config options.\n\n   Refactor this:\n\n   ```php\n   set('a', ['b' => 1]);\n\n   // ...\n\n   get('a.b');\n   ```\n\n   To:\n\n   ```php\n   set('a_b', 1);\n\n   // ...\n\n   get('a_b');\n   ```\n\n4. Credentials\n\n   Best practice in new v5 is to omit credentials for connection in `deploy.php` and write them in `~/.ssh/config` instead.\n\n   - `identityFile($publicKeyFile,, $privateKeyFile, $passPhrase)` to `identityFile($privateKeyFile)`\n   - `pemFile($pemFile)` to `identityFile($pemFile)`\n   - `forwardAgent()` to `forwardAgent(true)`\n\n5. Tasks constraints\n\n   - `onlyOn` to `onHosts`\n   - `onlyOnStage` to `onStage`\n\n## Upgrade from 3.x to 4.x\n\n1. Namespace for functions\n\n   Add to beginning of _deploy.php_ next line:\n\n   ```php\n   use function Deployer\\{server, task, run, set, get, add, before, after};\n   ```\n\n   If you are using PHP version less than 5.6, you can use this:\n\n   ```php\n   namespace Deployer;\n   ```\n\n2. `env()` to `set()`/`get()`\n\n   Rename all calls `env($name, $value)` to `set($name, $value)`.\n\n   Rename all rvalue `env($name)` to `get($name)`.\n\n   Rename all `server(...)->env(...)` to `server(...)->set(...)`.\n\n3. Moved _NonFatalException_\n\n   Rename `Deployer\\Task\\NonFatalException` to `Deployer\\Exception\\NonFatalException`.\n\n4. Prior release cleanup\n\n   Due to changes in release management, the new cleanup task will ignore any prior releases deployed with 3.x. These will need to be manually removed after migrating to and successfully releasing via 4.x.\n\n## Upgrade from 2.x to 3.x\n\n1. ### `->path('...')`\n\n   Replace your server paths configuration:\n\n   ```php\n   server(...)\n     ->path(...);\n   ```\n\n   to:\n\n   ```php\n   server(...)\n     ->env('deploy_path', '...');\n   ```\n"
  },
  {
    "path": "docs/api.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit src/functions.php -->\n<!-- Then run bin/docgen -->\n\n# API Reference\n\n## host()\n\n```php\nhost(string ...$hostname): Host|ObjectProxy\n```\n\nDefines a host or hosts.\n```php\nhost('example.org');\nhost('prod.example.org', 'staging.example.org');\n```\n\nInside task can be used to get `Host` instance of an alias.\n```php\ntask('test', function () {\n    $port = host('example.org')->get('port');\n});\n```\n\n\n## localhost()\n\n```php\nlocalhost(string ...$hostnames): Localhost|ObjectProxy\n```\n\nDefine a local host.\nDeployer will not connect to this host, but will execute commands locally instead.\n\n```php\nlocalhost('ci'); // Alias and hostname will be \"ci\".\n```\n\n\n## currentHost()\n\n```php\ncurrentHost(): Host\n```\n\nReturns current host.\n\n\n## select()\n\n```php\nselect(string $selector): array\n```\n\nReturns hosts based on provided selector.\n\n```php\non(select('stage=prod, role=db'), function (Host $host) {\n    ...\n});\n```\n\n\n\n## selectedHosts()\n\n```php\nselectedHosts(): array\n```\n\nReturns array of hosts selected by user via CLI.\n\n\n\n## import()\n\n```php\nimport(string $file): void\n```\n\nImport other php or yaml recipes.\n\n```php\nimport('recipe/common.php');\n```\n\n```php\nimport(__DIR__ . '/config/hosts.yaml');\n```\n\n\n## desc()\n\n```php\ndesc(?string $title = null): ?string\n```\n\nSet task description.\n\n\n## task()\n\n```php\ntask(string $name, callable|array|null $body = null): Task\n```\n\nDefine a new task and save to tasks list.\n\nAlternatively get a defined task.\n\n\n\n| Argument | Type | Comment |\n|---|---|---|\n| `$name` | `string` | Name of current task. |\n| `$body` | `callable` or `array` or `null` | Callable task, array of other tasks names or nothing to get a defined tasks |\n\n## before()\n\n```php\nbefore(string $task, string|callable $do): ?Task\n```\n\nCall that task before specified task runs.\n\n\n\n\n| Argument | Type | Comment |\n|---|---|---|\n| `$task` | `string` | The task before $that should be run. |\n| `$do` | `string` or `callable` | The task to be run. |\n\n## after()\n\n```php\nafter(string $task, string|callable $do): ?Task\n```\n\nCall that task after specified task runs.\n\n\n\n\n| Argument | Type | Comment |\n|---|---|---|\n| `$task` | `string` | The task after $that should be run. |\n| `$do` | `string` or `callable` | The task to be run. |\n\n## fail()\n\n```php\nfail(string $task, string|callable $do): ?Task\n```\n\nSetup which task run on failure of $task.\nWhen called multiple times for a task, previous fail() definitions will be overridden.\n\n\n\n\n| Argument | Type | Comment |\n|---|---|---|\n| `$task` | `string` | The task which need to fail so $that should be run. |\n| `$do` | `string` or `callable` | The task to be run. |\n\n## option()\n\n```php\noption(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null): void\n```\n\nAdd users options.\n\n\n\n| Argument | Type | Comment |\n|---|---|---|\n| `$name` | `string` | The option name |\n| `$shortcut` | `string` or `array` or `null` | The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts |\n| `$mode` | `int` or `null` | The option mode: One of the VALUE_* constants |\n| `$description` | `string` | A description text |\n| `$default` | `string` or `string[]` or `int` or `bool` or `null` | The default value (must be null for self::VALUE_NONE) |\n\n## cd()\n\n```php\ncd(string $path): void\n```\n\nChange the current working directory.\n\n```php\ncd('~/myapp');\nrun('ls'); // Will run `ls` in ~/myapp.\n```\n\n\n## become()\n\n```php\nbecome(string $user): \\Closure\n```\n\nChange the current user.\n\nUsage:\n```php\n$restore = become('deployer');\n\n// do something\n\n$restore(); // revert back to the previous user\n```\n\n\n\n## within()\n\n```php\nwithin(string $path, callable $callback): mixed\n```\n\nExecute a callback within a specific directory and revert back to the initial working directory.\n\n\n\n## run()\n\n```php\nrun(\n    string  $command,\n    ?string $cwd = null,\n    ?array  $env = null,\n    #[\\SensitiveParameter]\n    ?string $secret = null,\n    ?bool   $nothrow = false,\n    ?bool   $forceOutput = false,\n    ?int    $timeout = null,\n    ?int    $idleTimeout = null,\n): string \n```\n\nExecutes given command on remote host.\n\nExamples:\n\n```php\nrun('echo hello world');\nrun('cd {{deploy_path}} && git status');\nrun('password %secret%', secret: getenv('CI_SECRET'));\nrun('curl medv.io', timeout: 5);\n```\n\n```php\n$path = run('readlink {{deploy_path}}/current');\nrun(\"echo $path\");\n```\n\n\n\n| Argument | Type | Comment |\n|---|---|---|\n| `$command` | `string` | Command to run on remote host. |\n| `$cwd` | `string` or `null` | Sets the process working directory. If not set {{working_path}} will be used. |\n| `$timeout` | `int` or `null` | Sets the process timeout (max. runtime). The timeout in seconds (default: 300 sec; see {{default_timeout}}, `null` to disable). |\n| `$idleTimeout` | `int` or `null` | Sets the process idle timeout (max. time since last output) in seconds. |\n| `$secret` | `string` or `null` | Placeholder `%secret%` can be used in command. Placeholder will be replaced with this value and will not appear in any logs. |\n| `$env` | `array` or `null` | Array of environment variables: `run('echo $KEY', env: ['key' => 'value']);` |\n| `$forceOutput` | `bool` or `null` | Print command output in real-time. |\n| `$nothrow` | `bool` or `null` | Don't throw an exception of non-zero exit code. |\n\n## runLocally()\n\n```php\nrunLocally(\n    string  $command,\n    ?string $cwd = null,\n    ?int    $timeout = null,\n    ?int    $idleTimeout = null,\n    #[\\SensitiveParameter]\n    ?string $secret = null,\n    ?array  $env = null,\n    ?bool   $forceOutput = false,\n    ?bool   $nothrow = false,\n    ?string $shell = null,\n): string \n```\n\nExecute commands on a local machine.\n\nExamples:\n\n```php\n$user = runLocally('git config user.name');\nrunLocally(\"echo $user\");\n```\n\n\n\n\n| Argument | Type | Comment |\n|---|---|---|\n| `$command` | `string` | Command to run on localhost. |\n| `$cwd` | `string` or `null` | Sets the process working directory. If not set {{working_path}} will be used. |\n| `$timeout` | `int` or `null` | Sets the process timeout (max. runtime). The timeout in seconds (default: 300 sec, `null` to disable). |\n| `$idleTimeout` | `int` or `null` | Sets the process idle timeout (max. time since last output) in seconds. |\n| `$secret` | `string` or `null` | Placeholder `%secret%` can be used in command. Placeholder will be replaced with this value and will not appear in any logs. |\n| `$env` | `array` or `null` | Array of environment variables: `runLocally('echo $KEY', env: ['key' => 'value']);` |\n| `$forceOutput` | `bool` or `null` | Print command output in real-time. |\n| `$nothrow` | `bool` or `null` | Don't throw an exception of non-zero exit code. |\n| `$shell` | `string` or `null` | Shell to run in. Default is `bash -s`. |\n\n## test()\n\n```php\ntest(string $command): bool\n```\n\nRun test command.\nExample:\n\n```php\nif (test('[ -d {{release_path}} ]')) {\n...\n}\n```\n\n\n\n## testLocally()\n\n```php\ntestLocally(string $command): bool\n```\n\nRun test command locally.\nExample:\n\n    testLocally('[ -d {{local_release_path}} ]')\n\n\n\n## on()\n\n```php\non($hosts, callable $callback): void\n```\n\nIterate other hosts, allowing to call run a func in callback.\n\n```php\non(select('stage=prod, role=db'), function ($host) {\n    ...\n});\n```\n\n```php\non(host('example.org'), function ($host) {\n    ...\n});\n```\n\n```php\non(Deployer::get()->hosts, function ($host) {\n    ...\n});\n```\n\n\n\n## invoke()\n\n```php\ninvoke(string $taskName): void\n```\n\nRuns a task.\n```php\ninvoke('deploy:symlink');\n```\n\n\n\n## upload()\n\n```php\nupload($source, string $destination, array $config = []): void\n```\n\nUpload files or directories to host.\n\n> To upload the _contents_ of a directory, include a trailing slash (eg `upload('build/', '{{release_path}}/public');`).\n> Without the trailing slash, the build directory itself will be uploaded (resulting in `{{release_path}}/public/build`).\n\n The `$config` array supports the following keys:\n\n- `flags` for overriding the default `-azP` passed to the `rsync` command\n- `options` with additional flags passed directly to the `rsync` command\n- `timeout` for `Process::fromShellCommandline()` (`null` by default)\n- `progress_bar` to display upload/download progress\n- `display_stats` to display rsync set of statistics\n\nNote: due to the way php escapes command line arguments, list-notation for the rsync `--exclude={'file','anotherfile'}` option will not work.\nA workaround is to add a separate `--exclude=file` argument for each exclude to `options` (also, _do not_ wrap the filename/filter in quotes).\nAn alternative might be to write the excludes to a temporary file (one per line) and use `--exclude-from=temporary_file` argument instead.\n\n\n\n\n## download()\n\n```php\ndownload(string $source, string $destination, array $config = []): void\n```\n\nDownload file or directory from host\n\n\n\n\n## info()\n\n```php\ninfo(string $message): void\n```\n\nWrites an info message.\n\n\n## warning()\n\n```php\nwarning(string $message): void\n```\n\nWrites an warning message.\n\n\n## writeln()\n\n```php\nwriteln(string $message, int $options = 0): void\n```\n\nWrites a message to the output and adds a newline at the end.\n\n\n## parse()\n\n```php\nparse(string $value): string\n```\n\nParse set values.\n\n\n## set()\n\n```php\nset(string $name, $value): void\n```\n\nSetup configuration option.\n\n\n## add()\n\n```php\nadd(string $name, array $array): void\n```\n\nMerge new config params to existing config array.\n\n\n\n## get()\n\n```php\nget(string $name, $default = null)\n```\n\nGet configuration value.\n\n\n\n\n## has()\n\n```php\nhas(string $name): bool\n```\n\nCheck if there is such configuration option.\n\n\n## ask()\n\n```php\nask(string $message, ?string $default = null, ?array $autocomplete = null): ?string\n```\n\n\n\n## askChoice()\n\n```php\naskChoice(string $message, array $availableChoices, $default = null, bool $multiselect = false)\n```\n\n\n\n## askConfirmation()\n\n```php\naskConfirmation(string $message, bool $default = false): bool\n```\n\n\n\n## askHiddenResponse()\n\n```php\naskHiddenResponse(string $message): string\n```\n\n\n\n## input()\n\n```php\ninput(): InputInterface\n```\n\n\n\n## output()\n\n```php\noutput(): OutputInterface\n```\n\n\n\n## commandExist()\n\n```php\ncommandExist(string $command): bool\n```\n\nCheck if command exists\n\n\n\n## commandSupportsOption()\n\n```php\ncommandSupportsOption(string $command, string $option): bool\n```\n\n\n\n## which()\n\n```php\nwhich(string $name): string\n```\n\n\n\n## remoteEnv()\n\n```php\nremoteEnv(): array\n```\n\nReturns remote environments variables as an array.\n```php\n$remotePath = remoteEnv()['PATH'];\nrun('echo $PATH', env: ['PATH' => \"/home/user/bin:$remotePath\"]);\n```\n\n\n## error()\n\n```php\nerror(string $message): Exception\n```\n\nCreates a new exception.\n\n\n## timestamp()\n\n```php\ntimestamp(): string\n```\n\nReturns current timestamp in UTC timezone in ISO8601 format.\n\n\n## fetch()\n\n```php\nfetch(string $url, string $method = 'get', array $headers = [], ?string $body = null, ?array &$info = null, bool $nothrow = false): string\n```\n\nExample usage:\n```php\n$result = fetch('{{domain}}', info: $info);\nvar_dump($info['http_code'], $result);\n```\n\n\n"
  },
  {
    "path": "docs/avoid-php-fpm-reloading.md",
    "content": "# Avoid PHP-FPM Reloading\n\nDeployer symlinks _current_ to latest release dir.\n\n```\ncurrent -> releases/3/\nreleases/\n    1/\n    2/\n    3/\n```\n\n## The problem\n\nPHP Opcodes get cached. And if `SCRIPT_FILENAME` contains _current_ symlink, on\nnew deploy nothing updates. Usually, a solution is simple to reload **php-fpm**\nafter deploy, but such reload can lead to **dropped** or **failed** requests.\nThe correct fix is to configure your server set `SCRIPT_FILENAME` to a resolved path.\nYou can check your server configuration by printing `SCRIPT_FILENAME`.\n\n```php\necho $_SERVER['SCRIPT_FILENAME'];\n```\n\nIf it prints something like `/home/deployer/example.com/current/index.php` with\n_current_ in the path, your server configured incorrectly.\n\n## Fix for Nginx\n\nNginx has special variable `$realpath_root`, use it to set up `SCRIPT_FILENAME` and `DOCUMENT_ROOT`:\n\n```diff\nlocation ~ \\.php$ {\n  include fastcgi_params;\n  fastcgi_pass unix:/var/run/php/php-fpm.sock;\n- fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n+ fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;\n- fastcgi_param DOCUMENT_ROOT $document_root;\n+ fastcgi_param DOCUMENT_ROOT $realpath_root;\n}\n```\n\n## Fix for Caddy\n\n:::tip\nIf you're already using servers provisioned by Deployer, you don't need to fix\nanything, as everything is already configured properly.\n:::\n\nUse `resolve_root_symlink`:\n\n```\nphp_fastcgi * unix//run/php/php-fpm.sock {\n    resolve_root_symlink\n}\n```\n\n## Fix for Apache\n\nEnable `revalidate_path` in `php.ini`:\n\n```ini\nopcache.revalidate_path=1\n```\n"
  },
  {
    "path": "docs/basics.md",
    "content": "# Basics\n\nDeployer operates around two main concepts: [**hosts**](hosts.md) and [**tasks**](tasks.md). These are defined within a\n**recipe**, which is simply a file containing **hosts** and **tasks** definitions.\n\nThe Deployer CLI requires two arguments:\n\n1. A **task** to execute.\n2. A **selector** to determine the hosts the task will run on.\n\nHere's an example:\n\n```sh\n$ dep deploy deployer.org\n      ------ ------------\n       task    selector\n```\n\nDeployer uses the [selector](selector.md) to choose which hosts to execute the task on. After selecting hosts, it\nprepares the environment (details later) and runs the task.\n\n### Host Selection\n\n- If no selector is specified, Deployer prompts you to choose a host.\n- If your recipe has only one host, it is automatically selected.\n- To run a task on all hosts, use the `all` selector.\n\nBy default, the `dep` CLI looks for a `deploy.php` or `deploy.yaml` file in the current directory. Alternatively, you\ncan specify a recipe file explicitly using the `-f` or `--file` option:\n\n```sh\n$ dep --file=deploy.php deploy deployer.org\n```\n\n---\n\n## Writing Your First Recipe\n\nHere's an example of a simple recipe:\n\n```php\nnamespace Deployer;\n\nhost('deployer.org');\n\ntask('my_task', function () {\n    run('whoami');\n});\n```\n\nTo execute this task on `deployer.org`:\n\n```sh\n$ dep my_task\ntask my_task\n```\n\n### Increasing Verbosity\n\nBy default, Deployer only shows task names. To see detailed output (e.g., the result of the `whoami` command), use the\n`-v` option:\n\n```sh\n$ dep my_task -v\ntask my_task\n[deployer.org] run whoami\n[deployer.org] deployer\n```\n\n---\n\n## Working with Multiple Hosts\n\nYou can define multiple hosts in your recipe:\n\n```php\nhost('deployer.org');\nhost('medv.io');\n```\n\nDeployer connects to hosts using the same `~/.ssh/config` file as the `ssh` command. Alternatively, you can\nspecify [connection options](hosts.md) directly in the recipe.\n\nRun a task on both hosts:\n\n```sh\n$ dep my_task -v all\ntask my_task\n[deployer.org] run whoami\n[medv.io] run whoami\n[deployer.org] deployer\n[medv.io] anton\n```\n\n### Controlling Parallelism\n\nBy default, tasks run in parallel on all selected hosts, which may mix the output. To limit execution to one host at a\ntime:\n\n```sh\n$ dep my_task -v all --limit 1\ntask my_task\n[deployer.org] run whoami\n[deployer.org] deployer\n[medv.io] run whoami\n[medv.io] deployer\n```\n\nYou can also specify a [limit level](tasks.md#limit) for individual tasks to control parallelism.\n\n---\n\n## Configuring Hosts\n\nEach host can have a set of key-value configuration options. Here's an example:\n\n```php\nhost('deployer.org')->set('my_config', 'foo');\nhost('medv.io')->set('my_config', 'bar');\n```\n\nAccess these options in a task using the [currentHost](api.md#currenthost) function:\n\n```php\ntask('my_task', function () {\n    $myConfig = currentHost()->get('my_config');\n    writeln(\"my_config: \" . $myConfig);\n});\n```\n\nOr more concisely with the [get](api.md#get) function:\n\n```php\ntask('my_task', function () {\n    $myConfig = get('my_config');\n    writeln(\"my_config: \" . $myConfig);\n});\n```\n\nOr using brackets syntax `{{` and `}}`:\n\n```php\ntask('my_task', function () {\n    writeln(\"my_config: {{my_config}}\");\n});\n```\n\n---\n\n## Global Configurations\n\nHost configurations inherit global options. Here's how to set a global configuration:\n\n```php\nset('my_config', 'global');\n\nhost('deployer.org');\nhost('medv.io');\n```\n\nBoth hosts will inherit `my_config` with the value `global`. You can override these values for individual hosts as\nneeded.\n\n\n```php\nset('my_config', 'global');\n\nhost('deployer.org');\nhost('medv.io')->set('my_config', 'bar');\n```\n\n---\n\n## Dynamic Configurations\n\nYou can define dynamic configuration values using callbacks. These are evaluated the first time they are accessed, and\nthe result is stored for subsequent use:\n\n```php\nset('whoami', function () {\n    return run('whoami');\n});\n\ntask('my_task', function () {\n    writeln('Who am I? {{whoami}}');\n});\n```\n\nWhen executed:\n\n```sh\n$ dep my_task all\ntask my_task\n[deployer.org] Who am I? deployer\n[medv.io] Who am I? anton\n```\n\n---\n\nDynamic configurations are cached after the first use:\n\n```php\nset('current_date', function () {\n    return run('date');\n});\n\ntask('my_task', function () {\n    writeln('What time is it? {{current_date}}');\n    run('sleep 5');\n    writeln('What time is it? {{current_date}}');\n});\n```\n\nRunning this task:\n\n```sh\n$ dep my_task deployer.org -v\ntask my_task\n[deployer.org] run date\n[deployer.org] Wed 03 Nov 2021 01:16:53 PM UTC\n[deployer.org] What time is it? Wed 03 Nov 2021 01:16:53 PM UTC\n[deployer.org] run sleep 5\n[deployer.org] What time is it? Wed 03 Nov 2021 01:16:53 PM UTC\n```\n\n---\n\n## Overriding Configurations via CLI\n\nYou can override configuration values using the `-o` option:\n\n```sh\n$ dep my_task deployer.org -v -o current_date=\"I don't know\"\ntask my_task\n[deployer.org] What time is it? I don't know\n[deployer.org] run sleep 5\n[deployer.org] What time is it? I don't know\n```\n\nSince `current_date` is overridden, the callback is never executed.\n\n:::note\nIf you need to create a new configuration option based on the overridden one, use dynamic configuration syntax:\n\n```php\n\nset('dir_name', 'test');\n\n// calling get during recipe initialization will give you the original value defined above\nset('uses_original_dir_name', '/path/to/' . get('dir_name'));\n\n// use dynamic configuration syntax if you need to get a value passed via -o option\nset('uses_overridden_dir_name', function () {\n    return '/path/to/' . get('dir_name');\n});\n\ntask('my_task', function () {\n    writeln('Path: {{uses_original_dir_name}}');\n    writeln('Path: {{uses_overridden_dir_name}}');\n});\n```\n\n```sh\n$ dep my_task deployer.org -v -o dir_name=\"prod\"\ntask my_task\n[deployer.org] Path: /path/to/test\n[deployer.org] Path: /path/to/prod\n```\n:::\n\n---\n\nBy now, you should have a solid understanding of Deployer’s basics, from defining tasks and hosts to working with\nconfigurations and dynamic values. Happy deploying!\n"
  },
  {
    "path": "docs/ci-cd.md",
    "content": "# CI/CD\n\n## GitHub Actions\n\nUse official [GitHub Action for Deployer](https://github.com/deployphp/action).\n\nCreate `.github/workflows/deploy.yml` file with following content:\n\n```yaml\nname: deploy\n\non:\n  push:\n    branches: [master]\n\nconcurrency: production_environment\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: \"8.1\"\n\n      - name: Install dependencies\n        run: composer install\n\n      - name: Deploy\n        uses: deployphp/action@v1\n        with:\n          private-key: ${{ secrets.PRIVATE_KEY }}\n          dep: deploy\n```\n\n:::warning\nThe `concurrency: production_environment` is important as it prevents concurrent\ndeploys.\n:::\n\n## GitLab CI/CD\n\nSet the following variables in your GitLab project:\n\n- `SSH_KNOWN_HOSTS`: Content of `~/.ssh/known_hosts` file.\n  The public SSH keys for a host may be obtained using the utility `ssh-keyscan`.\n  For example: `ssh-keyscan deployer.org`.\n- `SSH_PRIVATE_KEY`: Private key for connecting to remote hosts.\n  To generate a private key: `ssh-keygen -t ed25519 -C 'gitlab@deployer.org'`.\n\nCreate a .gitlab-ci.yml file with the following content:\n\n```yml\nstages:\n  - deploy\n\ndeploy:\n  stage: deploy\n  image:\n    name: deployphp/deployer:v7\n    entrypoint: [\"\"]\n  before_script:\n    - mkdir -p ~/.ssh\n    - eval $(ssh-agent -s)\n    - echo \"$SSH_KNOWN_HOSTS\" > ~/.ssh/known_hosts\n    - chmod 644 ~/.ssh/known_hosts\n    - echo \"$SSH_PRIVATE_KEY\" | tr -d '\\r' | ssh-add - > /dev/null\n  script:\n    - dep deploy -vvv\n  resource_group: production\n  only:\n    - master\n```\n\n## Bitbucket Pipelines\n\nFirstly, [generate a new SSH key and add it to your workspace for the server](https://support.atlassian.com/bitbucket-cloud/docs/configure-ssh-and-two-step-verification/). There are instructions on the SSH Keys page that can help you add this key to your server.\n\nYou may also need to [define your environment variables](https://support.atlassian.com/bitbucket-cloud/docs/set-up-and-monitor-deployments/#Step-1--Define-your-environments) that you need to use in your deploy commands.\n\nCreate a bitbucket-pipelines.yml file with the following content:\n\n```yml\npipelines:\n  branches:\n    develop:\n      - stage:\n          # this is the target deployment name and it will inherit the environment from it\n          deployment: staging\n          name: Deploy Staging\n          steps:\n            - step:\n              name: Composer Install\n              image: composer/composer:2.2\n              caches:\n                - composer\n              script:\n                - composer install --quiet\n              artifacts:\n                # we need to save all these files so that they can be picked up in the actual deployment\n                - vendor/**\n            - step:\n                name: NPM Install\n                image: node:22-bullseye-slim\n                caches:\n                  - node\n                script:\n                  - npm install --silent\n                artifacts:\n                  # we need to save all these files so that they can be picked up in the actual deployment\n                  - public/build/**\n            - step:\n              name: Deployer Deploy\n              timeout: 6m # if it takes longer than this, error out\n              # @see https://hub.docker.com/r/deployphp/deployer/tags?name=v7.5\n              image: deployphp/deployer:v7.5.8\n              script:\n                # pass $DEVELOP and $STAGING variables from the \"staging\" deployment environment\n                - php /bin/deployer.phar deploy --branch=$DEVELOP stage=$STAGING\n```\n\n### Deployment concurrency\n\nOnly one deployment job runs at a time with the [`resource_group` keyword](https://docs.gitlab.com/ee/ci/yaml/index.html#resource_group) in .gitlab-ci.yml.\n\nIn addition, you can ensure that older deployment jobs are cancelled automatically when a newer deployment runs by enabling the [skip outdated deployment jobs](https://docs.gitlab.com/ee/ci/pipelines/settings.html#prevent-outdated-deployment-jobs) feature (enabled by default).\n\n### Deploy secrets\n\nIt is not recommended to commit secrets to the repository, you could use a GitLab variable to store them instead.\n\nMany frameworks use dotenv to store secrets, let's create a GitLab file variable named `DOTENV`, so it can be deployed along with the code.\n\nSet up a deployer task to copy secrets to the server:\n\n```php\ntask('deploy:secrets', function () {\n    upload(getenv('DOTENV'), '{{deploy_path}}/shared/.env');\n});\n```\n\nRun the task immediately after updating the code.\n"
  },
  {
    "path": "docs/cli.md",
    "content": "# CLI Usage\n\nWe recommend adding the following alias to your .bashrc file:\n\n```bash\nalias dep='vendor/bin/dep'\n```\n\nIt is also recommended to install the completion script for Deployer. Completion supports:\n\n- tasks,\n- options,\n- host names,\n- and configs.\n\nFor example, on macOS run the following commands:\n\n```bash\nbrew install bash-completion\ndep completion bash > /usr/local/etc/bash_completion.d/deployer\n```\n\n## Overriding configuration options\n\nFor example, if your _deploy.php_ file contains this configuration:\n\n```php\nset('ssh_multiplexing', false);\n```\n\nAnd you want to enable [ssh multiplexing](https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Multiplexing) without modifying the recipe, you can pass the `-o` option to the `dep` command:\n\n```\ndep deploy -o ssh_multiplexing=true\n```\n\nTo override multiple config options, you can pass multiple `-o` args:\n\n```\ndep deploy -o ssh_multiplexing=true -o branch=master\n```\n\n## Running arbitrary commands\n\nRun any command on one or more hosts:\n\n```\ndep run 'uptime -p'\n```\n\n## Tree command\n\nDeployer supports [task grouping](tasks.md#task-grouping) and [before/after hooks](tasks.md#addbefore). \nTo visualize the task hierarchy, use the **dep tree** command.\n\n```\n$ dep tree deploy\nThe task-tree for deploy:\n└── deploy\n    ├── deploy:prepare\n    │   ├── deploy:info\n    │   ├── deploy:setup\n    │   ├── deploy:lock\n    │   ├── deploy:release\n    │   ├── deploy:update_code\n    │   ├── build  // after deploy:update_code\n    │   ├── deploy:shared\n    │   └── deploy:writable\n    ├── deploy:vendors\n    ├── artisan:storage:link\n    ├── artisan:config:cache\n    ├── artisan:route:cache\n    ├── artisan:view:cache\n    ├── artisan:migrate\n    └── deploy:publish\n        ├── deploy:symlink\n        ├── deploy:unlock\n        ├── deploy:cleanup\n        └── deploy:success\n```\n\n## Execution plan\n\nBefore executing tasks, Deployer needs to flatten the task tree and decide in which order it will be executing tasks\non which hosts. Use the `--plan` option to output a table with tasks/hosts:\n\n```\n$ dep deploy --plan all\n┌──────────────────────┬──────────────────────┬──────────────────────┬──────────────────────┐\n│ prod01               │ prod02               │ prod03               │ prod04               │\n├──────────────────────┼──────────────────────┼──────────────────────┼──────────────────────┤\n│ deploy:info          │ deploy:info          │ deploy:info          │ deploy:info          │\n│ deploy:setup         │ deploy:setup         │ deploy:setup         │ deploy:setup         │\n│ deploy:lock          │ deploy:lock          │ deploy:lock          │ deploy:lock          │\n│ deploy:release       │ deploy:release       │ deploy:release       │ deploy:release       │\n│ deploy:update_code   │ deploy:update_code   │ deploy:update_code   │ deploy:update_code   │\n│ build                │ build                │ build                │ build                │\n│ deploy:shared        │ deploy:shared        │ deploy:shared        │ deploy:shared        │\n│ deploy:writable      │ deploy:writable      │ deploy:writable      │ deploy:writable      │\n│ deploy:vendors       │ deploy:vendors       │ deploy:vendors       │ deploy:vendors       │\n│ artisan:storage:link │ artisan:storage:link │ artisan:storage:link │ artisan:storage:link │\n│ artisan:config:cache │ artisan:config:cache │ artisan:config:cache │ artisan:config:cache │\n│ artisan:route:cache  │ artisan:route:cache  │ artisan:route:cache  │ artisan:route:cache  │\n│ artisan:view:cache   │ artisan:view:cache   │ artisan:view:cache   │ artisan:view:cache   │\n│ artisan:migrate      │ artisan:migrate      │ artisan:migrate      │ artisan:migrate      │\n│ deploy:symlink       │ -                    │ -                    │ -                    │\n│ -                    │ deploy:symlink       │ -                    │ -                    │\n│ -                    │ -                    │ deploy:symlink       │ -                    │\n│ -                    │ -                    │ -                    │ deploy:symlink       │\n│ deploy:unlock        │ deploy:unlock        │ deploy:unlock        │ deploy:unlock        │\n│ deploy:cleanup       │ deploy:cleanup       │ deploy:cleanup       │ deploy:cleanup       │\n│ deploy:success       │ deploy:success       │ deploy:success       │ deploy:success       │\n└──────────────────────┴──────────────────────┴──────────────────────┴──────────────────────┘\n```\n\nThe **deploy.php**:\n\n```php\nhost('prod[01:04]');\ntask('deploy:symlink')->limit(1);\n```\n\n## The `runLocally` working dir\n\nBy default, `runLocally()` commands are executed relative to the recipe file directory.\nThis can be overridden globally by setting an environment variable:\n\n```\nDEPLOYER_ROOT=. dep taskname`\n```\n\nAlternatively, the root directory can be overridden per command via the cwd configuration.\n\n```php\nrunLocally('ls', ['cwd' => '/root/directory']);\n```\n\n## Play blackjack\n\n> Yeah, well. I'm gonna go build my own theme park... with blackjack and hookers!\n>\n> In fact, forget the park!\n>\n> — Bender\n\n```\ndep blackjack\n```\n"
  },
  {
    "path": "docs/contrib/README.md",
    "content": "# All Contrib Recipes\n\n* [Bugsnag Recipe](/docs/contrib/bugsnag.md)\n* [Cachetool Recipe](/docs/contrib/cachetool.md)\n* [Chatwork Recipe](/docs/contrib/chatwork.md)\n* [Cimonitor Recipe](/docs/contrib/cimonitor.md)\n* [Cloudflare Recipe](/docs/contrib/cloudflare.md)\n* [Cpanel Recipe](/docs/contrib/cpanel.md)\n* [Crontab Recipe](/docs/contrib/crontab.md)\n* [Directadmin Recipe](/docs/contrib/directadmin.md)\n* [Discord Recipe](/docs/contrib/discord.md)\n* [Grafana Recipe](/docs/contrib/grafana.md)\n* [Hangouts Recipe](/docs/contrib/hangouts.md)\n* [Hipchat Recipe](/docs/contrib/hipchat.md)\n* [Ispmanager Recipe](/docs/contrib/ispmanager.md)\n* [Mattermost Recipe](/docs/contrib/mattermost.md)\n* [Ms-teams Recipe](/docs/contrib/ms-teams.md)\n* [Newrelic Recipe](/docs/contrib/newrelic.md)\n* [Npm Recipe](/docs/contrib/npm.md)\n* [Ntfy Recipe](/docs/contrib/ntfy.md)\n* [Phinx Recipe](/docs/contrib/phinx.md)\n* [Php-fpm Recipe](/docs/contrib/php-fpm.md)\n* [Rabbit Recipe](/docs/contrib/rabbit.md)\n* [Raygun Recipe](/docs/contrib/raygun.md)\n* [Rocketchat Recipe](/docs/contrib/rocketchat.md)\n* [Rollbar Recipe](/docs/contrib/rollbar.md)\n* [Rsync Recipe](/docs/contrib/rsync.md)\n* [Sentry Recipe](/docs/contrib/sentry.md)\n* [Slack Recipe](/docs/contrib/slack.md)\n* [Supervisord-monitor Recipe](/docs/contrib/supervisord-monitor.md)\n* [Telegram Recipe](/docs/contrib/telegram.md)\n* [Webpack_encore Recipe](/docs/contrib/webpack_encore.md)\n* [Workplace Recipe](/docs/contrib/workplace.md)\n* [Yammer Recipe](/docs/contrib/yammer.md)\n* [Yarn Recipe](/docs/contrib/yarn.md)"
  },
  {
    "path": "docs/contrib/bugsnag.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/bugsnag.php -->\n<!-- Then run bin/docgen -->\n\n# Bugsnag Recipe\n\n```php\nrequire 'contrib/bugsnag.php';\n```\n\n[Source](/contrib/bugsnag.php)\n\n\n\n## Configuration\n- *bugsnag_api_key* – the API Key associated with the project. Informs Bugsnag which project has been deployed. This is the only required field.\n- *bugsnag_provider* – the name of your source control provider. Required when repository is supplied and only for on-premise services.\n- *bugsnag_app_version* – the app version of the code you are currently deploying. Only set this if you tag your releases with semantic version numbers and deploy infrequently. (Optional.)\n## Usage\nSince you should only notify Bugsnag of a successful deployment, the `bugsnag:notify` task should be executed right at the end.\n```php\nafter('deploy', 'bugsnag:notify');\n```\n\n\n\n## Tasks\n\n### bugsnag\\:notify {#bugsnag-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/bugsnag.php#L24)\n\nNotifies Bugsnag of deployment.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/cachetool.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/cachetool.php -->\n<!-- Then run bin/docgen -->\n\n# Cachetool Recipe\n\n```php\nrequire 'contrib/cachetool.php';\n```\n\n[Source](/contrib/cachetool.php)\n\n\n\n## Configuration\n- **cachetool** *(optional)*: accepts a *string* or an *array* of strings with the unix socket or ip address to php-fpm. If `cachetool` is not given, then the application will look for a configuration file. The file must be named .cachetool.yml or .cachetool.yaml. CacheTool will look for this file on the current directory and in any parent directory until it finds one. If the paths above fail it will try to load /etc/cachetool.yml or /etc/cachetool.yaml configuration file.\n    ```php\n    set('cachetool', '/var/run/php-fpm.sock');\n    // or\n    set('cachetool', '127.0.0.1:9000');\n    // or\n    set('cachetool', ['/var/run/php-fpm.sock', '/var/run/php-fpm-other.sock']);\n    ```\nYou can also specify different cachetool settings for each host:\n```php\nhost('staging')\n    ->set('cachetool', '127.0.0.1:9000');\nhost('production')\n    ->set('cachetool', '/var/run/php-fpm.sock');\n```\nBy default, if no `cachetool` parameter is provided, this recipe will fallback to the global setting.\nIf your deployment user does not have permission to access the php-fpm.sock, you can alternatively use\nthe web adapter that creates a temporary php file and makes a web request to it with a configuration like\n```php\nset('cachetool_args', '--web --web-path=./public --web-url=https://{{hostname}}');\n```\n## Usage\nSince APCu and OPcache deal with compiling and caching files, they should be executed right after the symlink is created for the new release:\n```php\nafter('deploy:symlink', 'cachetool:clear:opcache');\nor\nafter('deploy:symlink', 'cachetool:clear:apcu');\n```\n## Read more\nRead more information about cachetool on the website:\nhttp://gordalina.github.io/cachetool/\n\n\n## Configuration\n### cachetool\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L51)\n\n## Configuration\n- **cachetool** *(optional)*: accepts a *string* or an *array* of strings with the unix socket or ip address to php-fpm. If `cachetool` is not given, then the application will look for a configuration file. The file must be named .cachetool.yml or .cachetool.yaml. CacheTool will look for this file on the current directory and in any parent directory until it finds one. If the paths above fail it will try to load /etc/cachetool.yml or /etc/cachetool.yaml configuration file.\n    ```php\n    set('cachetool', '/var/run/php-fpm.sock');\n    // or\n    set('cachetool', '127.0.0.1:9000');\n    // or\n    set('cachetool', ['/var/run/php-fpm.sock', '/var/run/php-fpm-other.sock']);\n    ```\nYou can also specify different cachetool settings for each host:\n```php\nhost('staging')\n    ->set('cachetool', '127.0.0.1:9000');\nhost('production')\n    ->set('cachetool', '/var/run/php-fpm.sock');\n```\nBy default, if no `cachetool` parameter is provided, this recipe will fallback to the global setting.\nIf your deployment user does not have permission to access the php-fpm.sock, you can alternatively use\nthe web adapter that creates a temporary php file and makes a web request to it with a configuration like\n```php\nset('cachetool_args', '--web --web-path=./public --web-url=https://{{hostname}}');\n```\n## Usage\nSince APCu and OPcache deal with compiling and caching files, they should be executed right after the symlink is created for the new release:\n```php\nafter('deploy:symlink', 'cachetool:clear:opcache');\nor\nafter('deploy:symlink', 'cachetool:clear:apcu');\n```\n## Read more\nRead more information about cachetool on the website:\nhttp://gordalina.github.io/cachetool/\n\n\n\n### cachetool_url\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L59)\n\nURL to download cachetool from if it is not available\n\nCacheTool 9.x works with PHP >=8.1\nCacheTool 8.x works with PHP >=8.0\nCacheTool 7.x works with PHP >=7.3\n\n```php title=\"Default value\"\n'https://github.com/gordalina/cachetool/releases/download/9.1.0/cachetool.phar'\n```\n\n\n### cachetool_args\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L60)\n\n\n\n\n\n### bin/cachetool\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L61)\n\n\n\n```php title=\"Default value\"\nif (!test('[ -f {{release_or_current_path}}/cachetool.phar ]')) {\nrun(\"cd {{release_or_current_path}} && curl -sLO {{cachetool_url}}\");\n}\nreturn '{{release_or_current_path}}/cachetool.phar';\n```\n\n\n### cachetool_options\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L67)\n\n\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n\n## Tasks\n\n### cachetool\\:clear\\:opcache {#cachetool-clear-opcache}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L89)\n\nClears OPcode cache.\n\nClear opcache cache\n\n\n### cachetool\\:clear\\:apcu {#cachetool-clear-apcu}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L100)\n\nClears APCu system cache.\n\nClear APCu cache\n\n\n### cachetool\\:clear\\:stat {#cachetool-clear-stat}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L111)\n\nClears file status and realpath caches.\n\nClear file status cache, including the realpath cache\n\n\n"
  },
  {
    "path": "docs/contrib/chatwork.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/chatwork.php -->\n<!-- Then run bin/docgen -->\n\n# Chatwork Recipe\n\n```php\nrequire 'contrib/chatwork.php';\n```\n\n[Source](/contrib/chatwork.php)\n\n\n\n# Chatwork Recipe\n## Installing\n  1. Create chatwork account by any manual in the internet\n  2. Take chatwork token (Like: b29a700e2d15bef3f26ae6a5c142d1ea) and set `chatwork_token` parameter\n  3. Take chatwork room id from url after clicked on the room, and set `chatwork_room_id` parameter\n  4. If you want, you can edit `chatwork_notify_text`, `chatwork_success_text` or `chatwork_failure_text`\n  5. Require chatwork recipe in your `deploy.php` file\n```php\n# https://deployer.org/recipes.html\nrequire 'recipe/chatwork.php';\n```\nAdd hook on deploy:\n```php\nbefore('deploy', 'chatwork:notify');\n```\n## Configuration\n- `chatwork_token` – chatwork bot token, **required**\n- `chatwork_room_id` — chatwork room to push messages to **required**\n- `chatwork_notify_text` – notification message template\n  ```\n  [info]\n    [title](*) Deployment Status: Deploying[/title]\n    Repo: {{repository}}\n    Branch: {{branch}}\n    Server: {{hostname}}\n    Release Path: {{release_path}}\n    Current Path: {{current_path}}\n  [/info]\n  ```\n- `chatwork_success_text` – success template, default:\n  ```\n  [info]\n    [title](*) Deployment Status: Successfully[/title]\n    Repo: {{repository}}\n    Branch: {{branch}}\n    Server: {{hostname}}\n    Release Path: {{release_path}}\n    Current Path: {{current_path}}\n  [/info]\"\n  ```\n- `chatwork_failure_text` – failure template, default:\n  ```\n  [info]\n    [title](*) Deployment Status: Failed[/title]\n    Repo: {{repository}}\n    Branch: {{branch}}\n    Server: {{hostname}}\n    Release Path: {{release_path}}\n    Current Path: {{current_path}}\n  [/info]\"\n  ```\n## Tasks\n- `chatwork:notify` – send message to chatwork\n- `chatwork:notify:success` – send success message to chatwork\n- `chatwork:notify:failure` – send failure message to chatwork\n## Usage\nIf you want to notify only about beginning of deployment add this line only:\n```php\nbefore('deploy', 'chatwork:notify');\n```\nIf you want to notify about successful end of deployment add this too:\n```php\nafter('deploy:success', 'chatwork:notify:success');\n```\nIf you want to notify about failed deployment add this too:\n```php\nafter('deploy:failed', 'chatwork:notify:failure');\n```\n\n\n## Configuration\n### chatwork_token\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L93)\n\nChatwork settings\n:::info Required\nThrows exception if not set.\n:::\n\n\n\n\n### chatwork_room_id\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L96)\n\n\n:::info Required\nThrows exception if not set.\n:::\n\n\n\n\n### chatwork_api\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L99)\n\n\n\n```php title=\"Default value\"\nreturn 'https://api.chatwork.com/v2/rooms/' . get('chatwork_room_id') . '/messages';\n```\n\n\n### chatwork_notify_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L104)\n\nThe Messages\n\n```php title=\"Default value\"\n\"[info]\\n[title](*) Deployment Status: Deploying[/title]\\nRepo: {{repository}}\\nBranch: {{branch}}\\nServer: {{hostname}}\\nRelease Path: {{release_path}}\\nCurrent Path: {{current_path}}\\n[/info]\"\n```\n\n\n### chatwork_success_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L105)\n\n\n\n```php title=\"Default value\"\n\"[info]\\n[title](*) Deployment Status: Successfully[/title]\\nRepo: {{repository}}\\nBranch: {{branch}}\\nServer: {{hostname}}\\nRelease Path: {{release_path}}\\nCurrent Path: {{current_path}}\\n[/info]\"\n```\n\n\n### chatwork_failure_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L106)\n\n\n\n```php title=\"Default value\"\n\"[info]\\n[title](*) Deployment Status: Failed[/title]\\nRepo: {{repository}}\\nBranch: {{branch}}\\nServer: {{hostname}}\\nRelease Path: {{release_path}}\\nCurrent Path: {{current_path}}\\n[/info]\"\n```\n\n\n\n## Tasks\n\n### chatwork_send_message {#chatwork_send_message}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L109)\n\n\n\nHelpers\n\n\n### chatwork\\:test {#chatwork-test}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L118)\n\nTests messages.\n\nTasks\n\n\n### chatwork\\:notify {#chatwork-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L129)\n\nNotifies Chatwork.\n\n\n\n\n### chatwork\\:notify\\:success {#chatwork-notify-success}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L144)\n\nNotifies Chatwork about deploy finish.\n\n\n\n\n### chatwork\\:notify\\:failure {#chatwork-notify-failure}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L160)\n\nNotifies Chatwork about deploy failure.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/cimonitor.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/cimonitor.php -->\n<!-- Then run bin/docgen -->\n\n# Cimonitor Recipe\n\n```php\nrequire 'contrib/cimonitor.php';\n```\n\n[Source](/contrib/cimonitor.php)\n\n\n\nMonitor your deployments on [CIMonitor](https://github.com/CIMonitor/CIMonitor).\n![CIMonitorGif](https://www.steefmin.xyz/deployer-example.gif)\nAdd tasks on deploy:\n```php\nbefore('deploy', 'cimonitor:notify');\nafter('deploy:success', 'cimonitor:notify:success');\nafter('deploy:failed', 'cimonitor:notify:failure');\n```\n## Configuration\n- `cimonitor_webhook` – CIMonitor server webhook url, **required**\n  ```\n  set('cimonitor_webhook', 'https://cimonitor.enrise.com/webhook/deployer');\n  ```\n- `cimonitor_title` – the title of application, default the username\\reponame combination from `{{repository}}`\n  ```\n  set('cimonitor_title', '');\n  ```\n- `cimonitor_user` – User object with name and email, default gets information from `git config`\n  ```\n  set('cimonitor_user', function () {\n    return [\n      'name' => 'John Doe',\n      'email' => 'john@enrise.com',\n    ];\n  });\n  ```\nVarious cimonitor statusses are set, in case you want to change these yourselves. See the [CIMonitor documentation](https://cimonitor.readthedocs.io/en/latest/) for the usages of different states.\n## Usage\nIf you want to notify only about beginning of deployment add this line only:\n```php\nbefore('deploy', 'cimonitor:notify');\n```\nIf you want to notify about successful end of deployment add this too:\n```php\nafter('deploy:success', 'cimonitor:notify:success');\n```\nIf you want to notify about failed deployment add this too:\n```php\nafter('deploy:failed', 'cimonitor:notify:failure');\n```\n\n\n## Configuration\n### cimonitor_title\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L64)\n\nTitle of project based on git repo\n\n```php title=\"Default value\"\n$repo = get('repository');\n$pattern = '/\\w+\\/\\w+/';\nreturn preg_match($pattern, $repo, $titles) ? $titles[0] : $repo;\n```\n\n\n### cimonitor_user\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L69)\n\n\n\n```php title=\"Default value\"\nreturn [\n'name' => runLocally('git config --get user.name'),\n'email' => runLocally('git config --get user.email'),\n];\n```\n\n\n### cimonitor_status_info\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L77)\n\nCI monitor status states and job states\n\n```php title=\"Default value\"\n'info'\n```\n\n\n### cimonitor_status_warning\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L78)\n\n\n\n```php title=\"Default value\"\n'warning'\n```\n\n\n### cimonitor_status_error\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L79)\n\n\n\n```php title=\"Default value\"\n'error'\n```\n\n\n### cimonitor_status_success\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L80)\n\n\n\n```php title=\"Default value\"\n'success'\n```\n\n\n### cimonitor_job_state_info\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L81)\n\n\n\n```php title=\"Default value\"\nget('cimonitor_status_info')\n```\n\n\n### cimonitor_job_state_pending\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L82)\n\n\n\n```php title=\"Default value\"\n'pending'\n```\n\n\n### cimonitor_job_state_running\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L83)\n\n\n\n```php title=\"Default value\"\n'running'\n```\n\n\n### cimonitor_job_state_warning\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L84)\n\n\n\n```php title=\"Default value\"\nget('cimonitor_status_warning')\n```\n\n\n### cimonitor_job_state_error\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L85)\n\n\n\n```php title=\"Default value\"\nget('cimonitor_status_error')\n```\n\n\n### cimonitor_job_state_success\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L86)\n\n\n\n```php title=\"Default value\"\nget('cimonitor_status_success')\n```\n\n\n\n## Tasks\n\n### cimonitor\\:notify {#cimonitor-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L89)\n\nNotifies CIMonitor.\n\n\n\n\n### cimonitor\\:notify\\:success {#cimonitor-notify-success}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L115)\n\nNotifies CIMonitor about deploy finish.\n\n\n\n\n### cimonitor\\:notify\\:failure {#cimonitor-notify-failure}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L143)\n\nNotifies CIMonitor about deploy failure.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/cloudflare.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/cloudflare.php -->\n<!-- Then run bin/docgen -->\n\n# Cloudflare Recipe\n\n```php\nrequire 'contrib/cloudflare.php';\n```\n\n[Source](/contrib/cloudflare.php)\n\n\n\n### Configuration\n- `cloudflare` – array with configuration for cloudflare\n    - `service_key` – Cloudflare Service Key. If this is not provided, use api_key and email.\n    - `api_key` – Cloudflare API key generated on the \"My Account\" page.\n    - `email` – Cloudflare Email address associated with your account.\n    - `api_token` – Cloudflare API Token generated on the \"My Account\" page.\n    - `domain` – The domain you want to clear (optional if zone_id is provided).\n    - `zone_id` – Cloudflare Zone ID (optional).\n### Usage\nSince the website should be built and some load is likely about to be applied to your server, this should be one of,\nif not the, last tasks before cleanup\n\n\n\n## Tasks\n\n### deploy\\:cloudflare {#deploy-cloudflare}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cloudflare.php#L24)\n\nClears Cloudflare Cache.\n\n### Configuration\n- `cloudflare` – array with configuration for cloudflare\n    - `service_key` – Cloudflare Service Key. If this is not provided, use api_key and email.\n    - `api_key` – Cloudflare API key generated on the \"My Account\" page.\n    - `email` – Cloudflare Email address associated with your account.\n    - `api_token` – Cloudflare API Token generated on the \"My Account\" page.\n    - `domain` – The domain you want to clear (optional if zone_id is provided).\n    - `zone_id` – Cloudflare Zone ID (optional).\n### Usage\nSince the website should be built and some load is likely about to be applied to your server, this should be one of,\nif not the, last tasks before cleanup\n\n\n"
  },
  {
    "path": "docs/contrib/cpanel.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/cpanel.php -->\n<!-- Then run bin/docgen -->\n\n# Cpanel Recipe\n\n```php\nrequire 'contrib/cpanel.php';\n```\n\n[Source](/contrib/cpanel.php)\n\n\n\n### Description\nThis is a recipe that uses the [cPanel 2 API](https://documentation.cPanel.net/display/DD/Guide+to+cPanel+API+2).\nUnfortunately the [UAPI](https://documentation.cPanel.net/display/DD/Guide+to+UAPI) that is recommended does not have support for creating addon domains.\nThe main idea behind is for staging purposes but I guess you can use it for other interesting concepts.\nThe idea is, every branch possibly has its own staging domain/subdomain (staging-neat-feature.project.com) and database db_neat-feature_project so it can be tested.\nThis recipe can make the domain/subdomain and database creation part of the deployment process so you don't have to manually create them through an interface.\n### Configuration\nThe example uses a .env file and Dotenv for configuration, but you can set the parameters as you wish\n```\nset('cpanel', [\n    'host' => getenv('CPANEL_HOST'),\n    'port' => getenv('CPANEL_PORT'),\n    'username' => getenv('CPANEL_USERNAME'),\n    'auth_type' => getenv('CPANEL_AUTH_TYPE'),\n    'token' => getenv('CPANEL_TOKEN'),\n    'user' => getenv('CPANEL_USER'),\n    'db_user' => getenv('CPANEL_DB_USER'),\n    'db_user_privileges' => getenv('CPANEL_DB_PRIVILEGES'),\n    'timeout' => 500,\n    'allowInStage' => ['staging', 'beta', 'alpha'],\n    'create_domain_format' => '%s-%s-%s',\n    'create_domain_values' => ['staging', 'master', get('application')],\n    'subdomain_prefix' => substr(md5(get('application')), 0,4) . '-',\n    'subdomain_suffix' => getenv('SUDOMAIN_SUFFIX'),\n    'create_db_format' => '%s_%s-%s-%s',\n    'create_db_values' => ['apps', 'staging','master', get('application')],\n]);\n```\n- `cpanel` – array with configuration for cPanel\n    - `username` – WHM account\n    - `user` – cPanel account that you want in charge of the domain\n    - `token` – WHM API token\n    - `create_domain_format` – Format for name creation of domain\n    - `create_domain_values` – The actual value reference for naming\n    - `subdomain_prefix` – cPanel has a weird way of dealing with addons and subdomains, you cannot create 2 addons with the same subdomain, so you need to change it in some way, example uses first 4 chars of md5(app_name)\n    - `subdomain_suffix` – cPanel has a weird way of dealing with addons and subdomains, so the suffix needs to be your main domain for that account for deletion purposes\n    - `addondir` – addon dir is different from the deploy path because cPanel \"injects\" /home/user/ into the path, so tilde cannot be used\n    - `allowInStage` – Define the stages that cPanel recipe actions are allowed in\n#### .env file example\n```\nCPANEL_HOST=xxx.xxx.xxx.xxx\nCPANEL_PORT=2087\nCPANEL_USERNAME=root\nCPANEL_TOKEN=xxxx\nCPANEL_USER=xxx\nCPANEL_AUTH_TYPE=hash\nCPANEL_DB_USER=db_user\nCPANEL_DB_PRIVILEGES=\"ALL PRIVILEGES\"\nSUDOMAIN_SUFFIX=.mymaindomain.com\n```\n### Tasks\n- `cpanel:createaddondomain` Creates an addon domain\n- `cpanel:deleteaddondomain` Removes an addon domain\n- `cpanel:createdb` Creates a new database\n### Usage\nA complete example with configs, staging and deployment\n```\n<?php\nnamespace Deployer;\nuse Dotenv\\Dotenv;\nrequire 'vendor/autoload.php';\n(Dotenv::create(__DIR__))->load(); // this is used just so an .env file can be used for credentials\nrequire 'cpanel.php';\nProject name\nset('application', 'myproject.com');\nProject repository\nset('repository', 'git@github.com:myorg/myproject.com');\nset('cpanel', [\n    'host' => getenv('CPANEL_HOST'),\n    'port' => getenv('CPANEL_PORT'),\n    'username' => getenv('CPANEL_USERNAME'),\n    'auth_type' => getenv('CPANEL_AUTH_TYPE'),\n    'token' => getenv('CPANEL_TOKEN'),\n    'user' => getenv('CPANEL_USER'),\n    'db_user' => getenv('CPANEL_DB_USER'),\n    'db_user_privileges' => getenv('CPANEL_DB_PRIVILEGES'),\n    'timeout' => 500,\n    'allowInStage' => ['staging', 'beta', 'alpha'],\n    'create_domain_format' => '%s-%s-%s',\n    'create_domain_values' => ['staging', 'master', get('application')],\n    'subdomain_prefix' => substr(md5(get('application')), 0,4) . '-',\n    'subdomain_suffix' => getenv('SUDOMAIN_SUFFIX'),\n    'create_db_format' => '%s_%s-%s-%s',\n    'create_db_values' => ['apps', 'staging','master', get('application')],\n]);\nhost('myproject.com')\n    ->stage('staging')\n    ->set('cpanel_createdb', vsprintf(get('cpanel')['create_db_format'], get('cpanel')['create_db_values']))\n    ->set('branch', 'dev-branch')\n    ->set('deploy_path',  '~/staging/' . vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values']))\n    ->set('addondir',  'staging/' . vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values']));\nTasks\ntask('build', function () {\n    run('cd {{release_path}} && build');\n});\nafter('deploy:prepare', 'cpanel:createaddondomain');\nafter('deploy:prepare', 'cpanel:createdb');\n```\n\n\n\n## Tasks\n\n### cpanel\\:createdb {#cpanel-createdb}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cpanel.php#L196)\n\nCreates database though CPanel API.\n\n\n\n\n### cpanel\\:createaddondomain {#cpanel-createaddondomain}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cpanel.php#L224)\n\nCreates addon domain though CPanel API.\n\n\n\n\n### cpanel\\:deleteaddondomain {#cpanel-deleteaddondomain}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/cpanel.php#L247)\n\nDeletes addon domain though CPanel API.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/crontab.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/crontab.php -->\n<!-- Then run bin/docgen -->\n\n# Crontab Recipe\n\n```php\nrequire 'contrib/crontab.php';\n```\n\n[Source](/contrib/crontab.php)\n\n\n\nRecipe for adding crontab jobs.\nThis recipe creates a new section in the crontab file with the configured jobs.\nThe section is identified by the *crontab:identifier* variable, by default the application name.\n## Configuration\n- *crontab:jobs* - An array of strings with crontab lines.\n## Usage\n```php\nrequire 'contrib/crontab.php';\nafter('deploy:success', 'crontab:sync');\nadd('crontab:jobs', [\n    '* * * * * cd {{current_path}} && {{bin/php}} artisan schedule:run >> /dev/null 2>&1',\n]);\n```\n\n\n## Configuration\n### bin/crontab\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/crontab.php#L30)\n\nGet path to bin\n\n```php title=\"Default value\"\nreturn which('crontab');\n```\n\n\n### crontab:identifier\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/crontab.php#L35)\n\nSet the identifier used in the crontab, application name by default\n\n```php title=\"Default value\"\nreturn get('application', 'application');\n```\n\n\n### crontab:use_sudo\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/crontab.php#L40)\n\nUse sudo to run crontab. When running crontab with sudo, you can use the `-u` parameter to change a crontab for a different user.\n\n```php title=\"Default value\"\nfalse\n```\n\n\n\n## Tasks\n\n### crontab\\:sync {#crontab-sync}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/crontab.php#L43)\n\nSync crontab jobs.\n\n\n\n\n### crontab\\:remove {#crontab-remove}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/crontab.php#L87)\n\nRemove crontab jobs.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/directadmin.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/directadmin.php -->\n<!-- Then run bin/docgen -->\n\n# Directadmin Recipe\n\n```php\nrequire 'contrib/directadmin.php';\n```\n\n[Source](/contrib/directadmin.php)\n\n\n\n### Configuration\n- `directadmin` – array with configuration for DirectAdmin\n    - `host` – DirectAdmin host\n    - `port` – DirectAdmin port (default: 2222, not required)\n    - `scheme` – DirectAdmin scheme (default: http, not required)\n    - `username` – DirectAdmin username\n    - `password` – DirectAdmin password (it is recommended to use login keys!)\n    - `db_user` – Database username (required when using directadmin:createdb or directadmin:deletedb)\n    - `db_name` – Database namse (required when using directadmin:createdb)\n    - `db_password` – Database password (required when using directadmin:createdb)\n    - `domain_name` – Domain to create, delete or edit (required when using directadmin:createdomain, directadmin:deletedomain, directadmin:symlink-private-html or directadmin:php-version)\n    - `domain_ssl` – Enable SSL, options: ON/OFF, default: ON (optional when using directadmin:createdb)\n    - `domain_cgi` – Enable CGI, options: ON/OFF, default: ON (optional when using directadmin:createdb)\n    - `domain_php` – Enable PHP, options: ON/OFF, default: ON (optional when using directadmin:createdb)\n    - `domain_php_version` – Domain PHP Version, default: 1 (required when using directadmin:php-version)\n\n\n\n## Tasks\n\n### directadmin\\:createdb {#directadmin-createdb}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/directadmin.php#L76)\n\nCreates a database on DirectAdmin.\n\n\n\n\n### directadmin\\:deletedb {#directadmin-deletedb}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/directadmin.php#L96)\n\nDeletes a database on DirectAdmin.\n\n\n\n\n### directadmin\\:createdomain {#directadmin-createdomain}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/directadmin.php#L111)\n\nCreates a domain on DirectAdmin.\n\n\n\n\n### directadmin\\:deletedomain {#directadmin-deletedomain}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/directadmin.php#L129)\n\nDeletes a domain on DirectAdmin.\n\n\n\n\n### directadmin\\:symlink-private-html {#directadmin-symlink-private-html}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/directadmin.php#L145)\n\nSymlink your private_html to public_html.\n\n\n\n\n### directadmin\\:php-version {#directadmin-php-version}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/directadmin.php#L161)\n\nChanges the PHP version from a domain.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/discord.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/discord.php -->\n<!-- Then run bin/docgen -->\n\n# Discord Recipe\n\n```php\nrequire 'contrib/discord.php';\n```\n\n[Source](/contrib/discord.php)\n\n\n\n## Installing\nAdd hook on deploy:\n```php\nbefore('deploy', 'discord:notify');\n```\n## Configuration\n- `discord_channel` – Discord channel ID, **required**\n- `discord_token` – Discord channel token, **required**\n- `discord_notify_text` – notification message template, markdown supported, default:\n  ```markdown\n  :&#8203;information_source: **{{user}}** is deploying branch `{{branch}}` to _{{where}}_\n  ```\n- `discord_success_text` – success template, default:\n  ```markdown\n  :&#8203;white_check_mark: Branch `{{branch}}` deployed to _{{where}}_ successfully\n  ```\n- `discord_failure_text` – failure template, default:\n  ```markdown\n  :&#8203;no_entry_sign: Branch `{{branch}}` has failed to deploy to _{{where}}_\n## Usage\nIf you want to notify only about beginning of deployment add this line only:\n```php\nbefore('deploy', 'discord:notify');\n```\nIf you want to notify about successful end of deployment add this too:\n```php\nafter('deploy:success', 'discord:notify:success');\n```\nIf you want to notify about failed deployment add this too:\n```php\nafter('deploy:failed', 'discord:notify:failure');\n```\n\n\n## Configuration\n### discord_webhook\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L54)\n\n\n\n```php title=\"Default value\"\nreturn 'https://discordapp.com/api/webhooks/{{discord_channel}}/{{discord_token}}/slack';\n```\n\n\n### discord_notify_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L59)\n\nDeploy messages\n\n```php title=\"Default value\"\nreturn [\n'text' => parse(':&#8203;information_source: **{{user}}** is deploying branch `{{what}}` to _{{where}}_'),\n];\n```\n\n\n### discord_success_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L64)\n\n\n\n```php title=\"Default value\"\nreturn [\n'text' => parse(':&#8203;white_check_mark: Branch `{{what}}` deployed to _{{where}}_ successfully'),\n];\n```\n\n\n### discord_failure_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L69)\n\n\n\n```php title=\"Default value\"\nreturn [\n'text' => parse(':&#8203;no_entry_sign: Branch `{{what}}` has failed to deploy to _{{where}}_'),\n];\n```\n\n\n### discord_message\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L76)\n\nThe message\n\n```php title=\"Default value\"\n'discord_notify_text'\n```\n\n\n\n## Tasks\n\n### discord_send_message {#discord_send_message}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L79)\n\n\n\nHelpers\n\n\n### discord\\:test {#discord-test}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L87)\n\nTests messages.\n\nTasks\n\n\n### discord\\:notify {#discord-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L98)\n\nNotifies Discord.\n\n\n\n\n### discord\\:notify\\:success {#discord-notify-success}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L106)\n\nNotifies Discord about deploy finish.\n\n\n\n\n### discord\\:notify\\:failure {#discord-notify-failure}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L114)\n\nNotifies Discord about deploy failure.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/grafana.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/grafana.php -->\n<!-- Then run bin/docgen -->\n\n# Grafana Recipe\n\n```php\nrequire 'contrib/grafana.php';\n```\n\n[Source](/contrib/grafana.php)\n\n\n\n## Configuration options\n- **url** *(required)*: the URL to the creates annotation api endpoint.\n- **token** *(required)*: authentication token. Can be created at Grafana Console.\n- **time** *(optional)* – set deploy time of annotation. specify epoch milliseconds. (Defaults is set to the current time in epoch milliseconds.)\n- **tags** *(optional)* – set tag of annotation.\n- **text** *(optional)* – set text of annotation. (Defaults is set to \"Deployed \" + git log -n 1 --format=\"%h\")\n```php\ndeploy.php\nset('grafana', [\n    'token' => 'eyJrIj...',\n    'url' => 'http://grafana/api/annotations',\n    'tags' => ['deploy', 'production'],\n]);\n```\n## Usage\nIf you want to create annotation about successful end of deployment.\n```php\nafter('deploy:success', 'grafana:annotation');\n```\n\n\n\n## Tasks\n\n### grafana\\:annotation {#grafana-annotation}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/grafana.php#L38)\n\nCreates Grafana annotation of deployment.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/hangouts.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/hangouts.php -->\n<!-- Then run bin/docgen -->\n\n# Hangouts Recipe\n\n```php\nrequire 'contrib/hangouts.php';\n```\n\n[Source](/contrib/hangouts.php)\n\n\n\nAdd hook on deploy:\n```php\nbefore('deploy', 'chat:notify');\n```\n## Configuration\n- `chat_webhook` – chat incoming webhook url, **required**\n- `chat_title` – the title of your notification card, default `{{application}}`\n- `chat_subtitle` – the subtitle of your card, default `{{hostname}}`\n- `chat_favicon` – an image for the header of your card, default `http://{{hostname}}/favicon.png`\n- `chat_line1` – first line of the text in your card, default: `{{branch}}`\n- `chat_line2` – second line of the text in your card, default: `{{stage}}`\n## Usage\nIf you want to notify only about beginning of deployment add this line only:\n```php\nbefore('deploy', 'chat:notify');\n```\nIf you want to notify about successful end of deployment add this too:\n```php\nafter('deploy:success', 'chat:notify:success');\n```\nIf you want to notify about failed deployment add this too:\n```php\nafter('deploy:failed', 'chat:notify:failure');\n```\n\n\n## Configuration\n### chat_title\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L46)\n\nTitle of project\n\n```php title=\"Default value\"\nreturn get('application', 'Project');\n```\n\n\n### chat_subtitle\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L50)\n\n\n\n```php title=\"Default value\"\nget('hostname')\n```\n\n\n### favicon\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L53)\n\nIf 'favicon' is set Google Hangouts Chat will decorate your card with an image.\n\n```php title=\"Default value\"\n'http://{{hostname}}/favicon.png'\n```\n\n\n### chat_line1\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L56)\n\nDeploy messages\n\n```php title=\"Default value\"\n'{{branch}}'\n```\n\n\n### chat_line2\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L57)\n\n\n\n```php title=\"Default value\"\n'{{stage}}'\n```\n\n\n\n## Tasks\n\n### chat\\:notify {#chat-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L60)\n\nNotifies Google Hangouts Chat.\n\n\n\n\n### chat\\:notify\\:success {#chat-notify-success}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L102)\n\nNotifies Google Hangouts Chat about deploy finish.\n\n\n\n\n### chat\\:notify\\:failure {#chat-notify-failure}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L144)\n\nNotifies Google Hangouts Chat about deploy failure.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/hipchat.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/hipchat.php -->\n<!-- Then run bin/docgen -->\n\n# Hipchat Recipe\n\n```php\nrequire 'contrib/hipchat.php';\n```\n\n[Source](/contrib/hipchat.php)\n\n\n\n## Configuration\n- `hipchat_token` – Hipchat V1 auth token\n- `hipchat_room_id` – Room ID or name\n- `hipchat_message` –  Deploy message, default is `_{{user}}_ deploying `{{what}}` to *{{where}}*`\n- `hipchat_from` – Default to target\n- `hipchat_color` – Message color, default is **green**\n- `hipchat_url` –  The URL to the message endpoint, default is https://api.hipchat.com/v1/rooms/message\n## Usage\nSince you should only notify Hipchat room of a successful deployment, the `hipchat:notify` task should be executed right at the end.\n```php\nafter('deploy', 'hipchat:notify');\n```\n\n\n## Configuration\n### hipchat_color\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/hipchat.php#L26)\n\n\n\n```php title=\"Default value\"\n'green'\n```\n\n\n### hipchat_from\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/hipchat.php#L27)\n\n\n\n```php title=\"Default value\"\n'{{where}}'\n```\n\n\n### hipchat_message\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/hipchat.php#L28)\n\n\n\n```php title=\"Default value\"\n'_{{user}}_ deploying `{{what}}` to *{{where}}*'\n```\n\n\n### hipchat_url\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/hipchat.php#L29)\n\n\n\n```php title=\"Default value\"\n'https://api.hipchat.com/v1/rooms/message'\n```\n\n\n\n## Tasks\n\n### hipchat\\:notify {#hipchat-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/hipchat.php#L32)\n\nNotifies Hipchat channel of deployment.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/ispmanager.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/ispmanager.php -->\n<!-- Then run bin/docgen -->\n\n# Ispmanager Recipe\n\n```php\nrequire 'contrib/ispmanager.php';\n```\n\n[Source](/contrib/ispmanager.php)\n\n\n\nThis recipe for work with ISPManager Lite panel by API.\n\n\n## Configuration\n### ispmanager_owner\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L11)\n\n\n\n```php title=\"Default value\"\n'www-root'\n```\n\n\n### ispmanager_doc_root\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L12)\n\n\n\n```php title=\"Default value\"\n'/var/www/' . get('ispmanager_owner') . '/data/'\n```\n\n\n### ispmanager\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L15)\n\nISPManager default configuration\n\n```php title=\"Default value\"\n[\n    'api' => [\n        'dsn' => 'https://root:password@localhost:1500/ispmgr',\n        'secure' => true,\n    ],\n    'createDomain' => null,\n    'updateDomain' => null,\n    'deleteDomain' => null,\n    'createDatabase' => null,\n    'deleteDatabase' => null,\n    'phpSelect' => null,\n    'createAlias' => null,\n    'deleteAlias' => null,\n]\n```\n\n\n### vhost\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L31)\n\nVhost default configuration\n\n```php title=\"Default value\"\n[\n    'name' => '{{domain}}',\n    'php_enable' => 'on',\n    'aliases' => 'www.{{domain}}',\n    'home' => 'www/{{domain}}',\n    'owner' => get('ispmanager_owner'),\n    'email' => 'webmaster@{{domain}}',\n    'charset' => 'off',\n    'dirindex' => 'index.php uploaded.html',\n    'ssi' => 'on',\n    'php' => 'on',\n    'php_mode' => 'php_mode_mod',\n    'basedir' => 'on',\n    'php_apache_version' => 'native',\n    'cgi' => 'off',\n    'log_access' => 'on',\n    'log_error' => 'on',\n]\n```\n\n\n### ispmanager_session\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L51)\n\nStorage\n\n\n\n### ispmanager_databases\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L52)\n\n\n\n```php title=\"Default value\"\n[\n    'servers' => [],\n    'hosts' => [],\n    'dblist' => [],\n]\n```\n\n\n### ispmanager_domains\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L58)\n\n\n\n\n\n### ispmanager_phplist\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L59)\n\n\n\n\n\n### ispmanager_aliaslist\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L60)\n\n\n\n\n\n\n## Tasks\n\n### ispmanager\\:init {#ispmanager-init}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L63)\n\nInstalls ispmanager.\n\n\n\n\n### ispmanager\\:db-server-list {#ispmanager-db-server-list}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L86)\n\nTakes database servers list.\n\n\n\n\n### ispmanager\\:db-list {#ispmanager-db-list}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L123)\n\nTakes databases list.\n\n\n\n\n### ispmanager\\:domain-list {#ispmanager-domain-list}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L145)\n\nTakes domain list.\n\n\n\n\n### ispmanager\\:db-create {#ispmanager-db-create}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L161)\n\nCreates new database.\n\n\n\n\n### ispmanager\\:db-delete {#ispmanager-db-delete}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L231)\n\nDeletes database.\n\n\n\n\n### ispmanager\\:domain-create {#ispmanager-domain-create}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L282)\n\nCreates new domain.\n\n\n\n\n### ispmanager\\:get-php-list {#ispmanager-get-php-list}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L328)\n\nGets allowed PHP modes and versions.\n\n\n\n\n### ispmanager\\:print-php-list {#ispmanager-print-php-list}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L374)\n\nPrints allowed PHP modes and versions.\n\n\n\n\n### ispmanager\\:domain-php-select {#ispmanager-domain-php-select}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L412)\n\nSwitches PHP version for domain.\n\n\n\n\n### ispmanager\\:domain-alias-create {#ispmanager-domain-alias-create}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L481)\n\nCreates new domain alias.\n\n\n\n\n### ispmanager\\:domain-alias-delete {#ispmanager-domain-alias-delete}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L550)\n\nDeletes domain alias.\n\n\n\n\n### ispmanager\\:domain-delete {#ispmanager-domain-delete}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L618)\n\nDeletes domain.\n\n\n\n\n### ispmanager\\:process {#ispmanager-process}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L665)\n\nAuto task processing.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/mattermost.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/mattermost.php -->\n<!-- Then run bin/docgen -->\n\n# Mattermost Recipe\n\n```php\nrequire 'contrib/mattermost.php';\n```\n\n[Source](/contrib/mattermost.php)\n\n\n\n## Installing\nCreate a Mattermost incoming webhook, through the administration panel.\nAdd hook on deploy:\n```\nbefore('deploy', 'mattermost:notify');\n```\n## Configuration\n - `mattermost_webhook` - incoming mattermost webook **required**\n   ```\n   set('mattermost_webook', 'https://{your-mattermost-site}/hooks/xxx-generatedkey-xxx');\n   ```\n - `mattermost_channel` - overrides the channel the message posts in\n   ```\n   set('mattermost_channel', 'town-square');\n   ```\n - `mattermost_username` - overrides the username the message posts as\n   ```\n   set('mattermost_username', 'deployer');\n   ```\n - `mattermost_icon_url` - overrides the profile picture the message posts with\n   ```\n   set('mattermost_icon_url', 'https://domain.com/your-icon.png');\n   ```\n - `mattermost_text` - notification message\n   ```\n   set('mattermost_text', '_{{user}}_ deploying `{{what}}` to **{{where}}**');\n   ```\n - `mattermost_success_text` – success template, default:\n   ```\n   set('mattermost_success_text', 'Deploy to **{{where}}** successful {{mattermost_success_emoji}}');\n   ```\n - `mattermost_failure_text` – failure template, default:\n   ```\n   set('mattermost_failure_text', 'Deploy to **{{where}}** failed {{mattermost_failure_emoji}}');\n   ```\n - `mattermost_success_emoji` – emoji added at the end of success text\n - `mattermost_failure_emoji` – emoji added at the end of failure text\n For detailed information about Mattermost hooks see: https://developers.mattermost.com/integrate/incoming-webhooks/\n## Usage\nIf you want to notify only about beginning of deployment add this line only:\n```php\nbefore('deploy', 'mattermost:notify');\n```\nIf you want to notify about successful end of deployment add this too:\n```php\nafter('deploy:success', 'mattermost:notify:success');\n```\nIf you want to notify about failed deployment add this too:\n```php\nafter('deploy:failed', 'mattermost:notify:failure');\n```\n\n\n## Configuration\n### mattermost_webhook\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L81)\n\n\n\n```php title=\"Default value\"\nnull\n```\n\n\n### mattermost_channel\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L82)\n\n\n\n```php title=\"Default value\"\nnull\n```\n\n\n### mattermost_username\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L83)\n\n\n\n```php title=\"Default value\"\n'deployer'\n```\n\n\n### mattermost_icon_url\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L84)\n\n\n\n```php title=\"Default value\"\nnull\n```\n\n\n### mattermost_success_emoji\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L86)\n\n\n\n```php title=\"Default value\"\n':&#8203;white_check_mark:'\n```\n\n\n### mattermost_failure_emoji\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L87)\n\n\n\n```php title=\"Default value\"\n':&#8203;x:'\n```\n\n\n### mattermost_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L89)\n\n\n\n```php title=\"Default value\"\n'_{{user}}_ deploying `{{what}}` to **{{where}}**'\n```\n\n\n### mattermost_success_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L90)\n\n\n\n```php title=\"Default value\"\n'Deploy to **{{where}}** successful {{mattermost_success_emoji}}'\n```\n\n\n### mattermost_failure_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L91)\n\n\n\n```php title=\"Default value\"\n'Deploy to **{{where}}** failed {{mattermost_failure_emoji}}'\n```\n\n\n\n## Tasks\n\n### mattermost\\:notify {#mattermost-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L94)\n\nNotifies mattermost.\n\n\n\n\n### mattermost\\:notify\\:success {#mattermost-notify-success}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L115)\n\nNotifies mattermost about deploy finish.\n\n\n\n\n### mattermost\\:notify\\:failure {#mattermost-notify-failure}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L136)\n\nNotifies mattermost about deploy failure.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/ms-teams.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/ms-teams.php -->\n<!-- Then run bin/docgen -->\n\n# Ms-teams Recipe\n\n```php\nrequire 'contrib/ms-teams.php';\n```\n\n[Source](/contrib/ms-teams.php)\n\n\n\n## Installing\nRequire ms-teams recipe in your `deploy.php` file:\nSetup:\n1. Open MS Teams\n2. Navigate to Teams section\n3. Select existing or create new team\n4. Select existing or create new channel\n5. Hover over channel to get three dots, click, in menu select \"Connectors\"\n6. Search for and configure \"Incoming Webhook\"\n7. Confirm/create and copy your Webhook URL\n8. Setup deploy.php\n    Add in header:\n```php\nrequire 'contrib/ms-teams.php';\nset('teams_webhook', 'https://outlook.office.com/webhook/...');\n```\nAdd in content:\n```php\nbefore('deploy', 'teams:notify');\nafter('deploy:success', 'teams:notify:success');\nafter('deploy:failed', 'teams:notify:failure');\n```\n9.) Sip your coffee\n## Configuration\n- `teams_webhook` – teams incoming webhook url, **required**\n  ```\n  set('teams_webhook', 'https://outlook.office.com/webhook/...');\n  ```\n- `teams_title` – the title of application, default `{{application}}`\n- `teams_text` – notification message template, markdown supported\n  ```\n  set('teams_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\n  ```\n- `teams_success_text` – success template, default:\n  ```\n  set('teams_success_text', 'Deploy to *{{where}}* successful');\n  ```\n- `teams_failure_text` – failure template, default:\n  ```\n  set('teams_failure_text', 'Deploy to *{{where}}* failed');\n  ```\n- `teams_color` – color's attachment\n- `teams_success_color` – success color's attachment\n- `teams_failure_color` – failure color's attachment\n## Usage\nIf you want to notify only about beginning of deployment add this line only:\n```php\nbefore('deploy', 'teams:notify');\n```\nIf you want to notify about successful end of deployment add this too:\n```php\nafter('deploy:success', 'teams:notify:success');\n```\nIf you want to notify about failed deployment add this too:\n```php\nafter('deploy:failed', 'teams:notify:failure');\n```\n\n\n## Configuration\n### teams_title\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L79)\n\nTitle of project\n\n```php title=\"Default value\"\nreturn get('application', 'Project');\n```\n\n\n### teams_failure_continue\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L84)\n\nAllow Continue on Failure\n\n```php title=\"Default value\"\nfalse\n```\n\n\n### teams_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L87)\n\nDeploy message\n\n```php title=\"Default value\"\n'_{{user}}_ deploying `{{what}}` to *{{where}}*'\n```\n\n\n### teams_success_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L88)\n\n\n\n```php title=\"Default value\"\n'Deploy to *{{where}}* successful'\n```\n\n\n### teams_failure_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L89)\n\n\n\n```php title=\"Default value\"\n'Deploy to *{{where}}* failed'\n```\n\n\n### teams_color\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L92)\n\nColor of attachment\n\n```php title=\"Default value\"\n'#4d91f7'\n```\n\n\n### teams_success_color\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L93)\n\n\n\n```php title=\"Default value\"\n'#00c100'\n```\n\n\n### teams_failure_color\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L94)\n\n\n\n```php title=\"Default value\"\n'#ff0909'\n```\n\n\n\n## Tasks\n\n### teams\\:notify {#teams-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L97)\n\nNotifies Teams.\n\n\n\n\n### teams\\:notify\\:success {#teams-notify-success}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L121)\n\nNotifies Teams about deploy finish.\n\n\n\n\n### teams\\:notify\\:failure {#teams-notify-failure}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L144)\n\nNotifies Teams about deploy failure.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/newrelic.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/newrelic.php -->\n<!-- Then run bin/docgen -->\n\n# Newrelic Recipe\n\n```php\nrequire 'contrib/newrelic.php';\n```\n\n[Source](/contrib/newrelic.php)\n\n\n\n## Configuration\n- `newrelic_app_id` – newrelic's app id\n- `newrelic_api_key` – newrelic's api key\n- `newrelic_description` – message to send\n- `newrelic_endpoint` – newrelic's REST API endpoint\n## Usage\nSince you should only notify New Relic of a successful deployment, the `newrelic:notify` task should be executed right at the end.\n```php\nafter('deploy', 'newrelic:notify');\n```\n\n\n## Configuration\n### newrelic_app_id\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/newrelic.php#L24)\n\n\n:::info Required\nThrows exception if not set.\n:::\n\n\n\n\n### newrelic_description\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/newrelic.php#L28)\n\n\n\n```php title=\"Default value\"\nreturn runLocally('git log -n 1 --format=\"%an: %s\" | tr \\'\"\\' \"\\'\"');\n```\n\n\n### newrelic_revision\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/newrelic.php#L32)\n\n\n\n```php title=\"Default value\"\nreturn runLocally('git log -n 1 --format=\"%h\"');\n```\n\n\n### newrelic_endpoint\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/newrelic.php#L36)\n\n\n\n```php title=\"Default value\"\n'api.newrelic.com'\n```\n\n\n\n## Tasks\n\n### newrelic\\:notify {#newrelic-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/newrelic.php#L39)\n\nNotifies New Relic of deployment.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/npm.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/npm.php -->\n<!-- Then run bin/docgen -->\n\n# Npm Recipe\n\n```php\nrequire 'contrib/npm.php';\n```\n\n[Source](/contrib/npm.php)\n\n\n\n## Configuration\n- `bin/npm` *(optional)*: set npm binary, automatically detected otherwise.\n## Usage\n```php\nafter('deploy:update_code', 'npm:install');\n```\n\n\n## Configuration\n### bin/npm\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/npm.php#L17)\n\n## Configuration\n- `bin/npm` *(optional)*: set npm binary, automatically detected otherwise.\n## Usage\n```php\nafter('deploy:update_code', 'npm:install');\n```\n\n```php title=\"Default value\"\nreturn which('npm');\n```\n\n\n\n## Tasks\n\n### npm\\:install {#npm-install}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/npm.php#L27)\n\nInstalls npm packages.\n\nUses `npm ci` command. This command is similar to npm install,\nexcept it's meant to be used in automated environments such as\ntest platforms, continuous integration, and deployment -- or\nany situation where you want to make sure you're doing a clean\ninstall of your dependencies.\n\n\n"
  },
  {
    "path": "docs/contrib/ntfy.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/ntfy.php -->\n<!-- Then run bin/docgen -->\n\n# Ntfy Recipe\n\n```php\nrequire 'contrib/ntfy.php';\n```\n\n[Source](/contrib/ntfy.php)\n\n\n\n## Installing\nRequire ntfy.sh recipe in your `deploy.php` file:\nSetup:\n1. Setup deploy.php\n    Add in header:\n```php\nrequire 'contrib/ntfy.php';\nset('ntfy_topic', 'ntfy.sh/mytopic');\n```\nAdd in content:\n```php\nbefore('deploy', 'ntfy:notify');\nafter('deploy:success', 'ntfy:notify:success');\nafter('deploy:failed', 'ntfy:notify:failure');\n```\n9.) Sip your coffee\n## Configuration\n- `ntfy_server` – ntfy server url, default `ntfy.sh`\n  ```\n  set('ntfy_server', 'ntfy.sh');\n  ```\n- `ntfy_topic` – ntfy topic, **required**\n  ```\n  set('ntfy_topic', 'mysecrettopic');\n  ```\n- `ntfy_title` – the title of the message, default `{{application}}`\n- `ntfy_text` – notification message template\n  ```\n  set('ntfy_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\n  ```\n- `ntfy_tags` – notification message tags / emojis (comma separated)\n  ```\n  set('ntfy_tags', `information_source`);\n  ```\n- `ntfy_priority` – notification message priority (integer)\n  ```\n  set('ntfy_priority', 5);\n  ```\n- `ntfy_success_text` – success template, default:\n  ```\n  set('ntfy_success_text', 'Deploy to *{{where}}* successful');\n  ```\n- `ntfy_success_tags` – success tags / emojis (comma separated)\n  ```\n  set('ntfy_success_tags', `white_check_mark,champagne`);\n  ```\n- `ntfy_success_priority` – success notification message priority\n- `ntfy_failure_text` – failure template, default:\n  ```\n  set('ntfy_failure_text', 'Deploy to *{{where}}* failed');\n  ```\n- `ntfy_failure_tags` – failure tags / emojis (comma separated)\n  ```\n  set('ntfy_failure_tags', `warning,skull`);\n  ```\n- `ntfy_failure_priority` – failure notification message priority\n## Usage\nIf you want to notify only about beginning of deployment add this line only:\n```php\nbefore('deploy', 'ntfy:notify');\n```\nIf you want to notify about successful end of deployment add this too:\n```php\nafter('deploy:success', 'ntfy:notify:success');\n```\nIf you want to notify about failed deployment add this too:\n```php\nafter('deploy:failed', 'ntfy:notify:failure');\n```\n\n\n## Configuration\n### ntfy_server\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L90)\n\n\n\n```php title=\"Default value\"\n'ntfy.sh'\n```\n\n\n### ntfy_title\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L93)\n\nTitle of project\n\n```php title=\"Default value\"\nreturn get('application', 'Project');\n```\n\n\n### ntfy_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L98)\n\nDeploy message\n\n```php title=\"Default value\"\n'_{{user}}_ deploying `{{what}}` to *{{where}}*'\n```\n\n\n### ntfy_success_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L99)\n\n\n\n```php title=\"Default value\"\n'Deploy to *{{where}}* successful'\n```\n\n\n### ntfy_failure_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L100)\n\n\n\n```php title=\"Default value\"\n'Deploy to *{{where}}* failed'\n```\n\n\n### ntfy_tags\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L103)\n\nMessage tags\n\n\n\n### ntfy_success_tags\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L104)\n\n\n\n\n\n### ntfy_failure_tags\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L105)\n\n\n\n\n\n\n## Tasks\n\n### ntfy\\:notify {#ntfy-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L108)\n\nNotifies ntfy server.\n\n\n\n\n### ntfy\\:notify\\:success {#ntfy-notify-success}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L126)\n\nNotifies ntfy server about deploy finish.\n\n\n\n\n### ntfy\\:notify\\:failure {#ntfy-notify-failure}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L144)\n\nNotifies ntfy server about deploy failure.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/phinx.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/phinx.php -->\n<!-- Then run bin/docgen -->\n\n# Phinx Recipe\n\n```php\nrequire 'contrib/phinx.php';\n```\n\n[Source](/contrib/phinx.php)\n\n\n\n## Configuration options\nAll options are in the config parameter `phinx` specified as an array (instead of the `phinx_path` variable).\nAll parameters are *optional*, but you can specify them with a dictionary (to change all parameters)\nor by deployer dot notation (to change one option).\n### Phinx params\n- `phinx.environment`\n- `phinx.date`\n- `phinx.configuration` N.B. current directory is the project directory\n- `phinx.target`\n- `phinx.seed`\n- `phinx.parser`\n- `phinx.remove-all` (pass empty string as value)\n### Phinx path params\n- `phinx_path` Specify phinx path (by default phinx is searched for in $PATH, ./vendor/bin and ~/.composer/vendor/bin)\n### Example of usage\n```php\n$phinx_env_vars = [\n  'environment' => 'development',\n  'configuration' => './migration/.phinx.yml',\n  'target' => '20120103083322',\n  'remove-all' => '',\n];\nset('phinx_path', '/usr/local/phinx/bin/phinx');\nset('phinx', $phinx_env_vars);\nafter('cleanup', 'phinx:migrate');\nor set it for a specific server\nhost('dev')\n    ->user('user')\n    ->set('deploy_path', '/var/www')\n    ->set('phinx', $phinx_env_vars)\n    ->set('phinx_path', '');\n```\n## Suggested Usage\nYou can run all tasks before or after any\ntasks (but you need to specify external configs for phinx).\nIf you use internal configs (which are in your project) you need\nto run it after the `deploy:update_code` task is completed.\n## Read more\nFor further reading see [phinx.org](https://phinx.org). Complete descriptions of all possible options can be found on the [commands page](http://docs.phinx.org/en/latest/commands.html).\n\n\n## Configuration\n### bin/phinx\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/phinx.php#L81)\n\nPhinx recipe for Deployer\n\n@author    Alexey Boyko <ket4yiit@gmail.com>\n@contributor Security-Database <info@security-database.com>\n@copyright 2016 Alexey Boyko\n@license   MIT https://github.com/deployphp/recipes/blob/master/LICENSE\n\n@link https://github.com/deployphp/recipes\n\n@see http://deployer.org\n@see https://phinx.org\n\nPath to Phinx\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n\n## Tasks\n\n### phinx\\:migrate {#phinx-migrate}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/phinx.php#L149)\n\nMigrats database with phinx.\n\n\n\n\n### phinx\\:rollback {#phinx-rollback}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/phinx.php#L170)\n\nRollbacks database migrations with phinx.\n\n\n\n\n### phinx\\:seed {#phinx-seed}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/phinx.php#L191)\n\nSeeds database with phinx.\n\n\n\n\n### phinx\\:breakpoint {#phinx-breakpoint}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/phinx.php#L211)\n\nSets a migrations breakpoint with phinx.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/php-fpm.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/php-fpm.php -->\n<!-- Then run bin/docgen -->\n\n# Php-fpm Recipe\n\n```php\nrequire 'contrib/php-fpm.php';\n```\n\n[Source](/contrib/php-fpm.php)\n\n\n\n:::caution\nDo **not** reload php-fpm. Some user requests could fail or not complete in the\nprocess of reloading.\nInstead, configure your server [properly](avoid-php-fpm-reloading). If you're using Deployer's provision\nrecipe, it's already configured the right way and no php-fpm reload is needed.\n:::\n## Configuration\n- `php_fpm_version` – The PHP-fpm version. For example: `8.0`.\n- `php_fpm_service` – The full name of the PHP-fpm service. Defaults to `php{{php_fpm_version}}-fpm`.\n- `php_fpm_command` – The command to run to reload PHP-fpm. Defaults to `sudo systemctl reload {{php_fpm_service}}`.\n## Usage\nStart by explicitely providing the current version of PHP-version using the `php_fpm_version`.\nAlternatively, you may use any of the options above to configure how PHP-fpm should reload.\nThen, add the `php-fpm:reload` task at the end of your deployments by using the `after` method like so.\n```php\nset('php_fpm_version', '8.0');\nafter('deploy', 'php-fpm:reload');\n```\n\n\n## Configuration\n### php_fpm_version\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/php-fpm.php#L35)\n\n:::caution\nDo **not** reload php-fpm. Some user requests could fail or not complete in the\nprocess of reloading.\nInstead, configure your server [properly](avoid-php-fpm-reloading). If you're using Deployer's provision\nrecipe, it's already configured the right way and no php-fpm reload is needed.\n:::\n## Configuration\n- `php_fpm_version` – The PHP-fpm version. For example: `8.0`.\n- `php_fpm_service` – The full name of the PHP-fpm service. Defaults to `php[php_fpm_version](/docs/contrib/php-fpm.md#php_fpm_version)-fpm`.\n- `php_fpm_command` – The command to run to reload PHP-fpm. Defaults to `sudo systemctl reload [php_fpm_service](/docs/contrib/php-fpm.md#php_fpm_service)`.\n## Usage\nStart by explicitely providing the current version of PHP-version using the `php_fpm_version`.\nAlternatively, you may use any of the options above to configure how PHP-fpm should reload.\nThen, add the `php-fpm:reload` task at the end of your deployments by using the `after` method like so.\n```php\nset('php_fpm_version', '8.0');\nafter('deploy', 'php-fpm:reload');\n```\nAutomatically detects by using [bin/php](/docs/recipe/common.md#bin/php).\n\n```php title=\"Default value\"\nreturn run('{{bin/php}} -r \"printf(\\'%d.%d\\', PHP_MAJOR_VERSION, PHP_MINOR_VERSION);\"');\n```\n\n\n### php_fpm_service\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/php-fpm.php#L39)\n\n\n\n```php title=\"Default value\"\n'php{{php_fpm_version}}-fpm'\n```\n\n\n\n## Tasks\n\n### php-fpm\\:reload {#php-fpm-reload}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/php-fpm.php#L42)\n\nReloads the php-fpm service.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/rabbit.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/rabbit.php -->\n<!-- Then run bin/docgen -->\n\n# Rabbit Recipe\n\n```php\nrequire 'contrib/rabbit.php';\n```\n\n[Source](/contrib/rabbit.php)\n\n\n\n### Installing\n```php\ndeploy.php\nrequire 'recipe/rabbit.php';\n```\n### Configuration options\n- **rabbit** *(required)*: accepts an *array* with the connection information to [rabbitmq](http://www.rabbitmq.com) server token and team name.\nYou can provide also other configuration options:\n - *host* - default is localhost\n - *port* - default is 5672\n - *username* - default is *guest*\n - *password* - default is *guest*\n - *channel* - no default value, need to be specified via config\n - *message* - default is **Deployment to '$host' on *$prod* was successful\\n$releasePath**\n - *vhost* - default is\n```php\ndeploy.php\nset('rabbit', [\n    'host'     => 'localhost',\n    'port'     => '5672',\n    'username' => 'guest',\n    'password' => 'guest',\n    'channel'  => 'notify-channel',\n    'vhost'    => '/my-app'\n]);\n```\n### Suggested Usage\nSince you should only notify RabbitMQ channel of a successful deployment, the `deploy:rabbit` task should be executed right at the end.\n```php\ndeploy.php\nbefore('deploy:end', 'deploy:rabbit');\n```\n\n\n\n## Tasks\n\n### deploy\\:rabbit {#deploy-rabbit}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rabbit.php#L58)\n\nNotifies RabbitMQ channel about deployment.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/raygun.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/raygun.php -->\n<!-- Then run bin/docgen -->\n\n# Raygun Recipe\n\n```php\nrequire 'contrib/raygun.php';\n```\n\n[Source](/contrib/raygun.php)\n\n\n\n## Configuration\n- `raygun_api_key` – the API key of your Raygun application\n- `raygun_version` – the version of your application that this deployment is releasing\n- `raygun_owner_name` – the name of the person creating this deployment\n- `raygun_email` – the email of the person creating this deployment\n- `raygun_comment` – the deployment notes\n- `raygun_scm_identifier` – the commit that this deployment was built off\n- `raygun_scm_type` - the source control system you use\n## Usage\nTo notify Raygun of a successful deployment, you can use the 'raygun:notify' task after a deployment.\n```php\nafter('deploy', 'raygun:notify');\n```\n\n\n\n## Tasks\n\n### raygun\\:notify {#raygun-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/raygun.php#L28)\n\nNotifies Raygun of deployment.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/rocketchat.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/rocketchat.php -->\n<!-- Then run bin/docgen -->\n\n# Rocketchat Recipe\n\n```php\nrequire 'contrib/rocketchat.php';\n```\n\n[Source](/contrib/rocketchat.php)\n\n\n\n## Installing\nCreate a RocketChat incoming webhook, through the administration panel.\nAdd hook on deploy:\n```\nbefore('deploy', 'rocketchat:notify');\n```\n## Configuration\n - `rocketchat_webhook` - incoming rocketchat webook **required**\n   ```\n   set('rocketchat_webhook', 'https://rocketchat.yourcompany.com/hooks/XXXXX');\n   ```\n - `rocketchat_title` - the title of the application, defaults to `{{application}}`\n - `rocketchat_text` - notification message\n   ```\n   set('rocketchat_text', '_{{user}}_ deploying {{what}} to {{where}}');\n   ```\n - `rocketchat_success_text` – success template, default:\n  ```\n  set('rocketchat_success_text', 'Deploy to *{{where}}* successful');\n  ```\n - `rocketchat_failure_text` – failure template, default:\n  ```\n  set('rocketchat_failure_text', 'Deploy to *{{where}}* failed');\n  ```\n - `rocketchat_color` – color's attachment\n - `rocketchat_success_color` – success color's attachment\n - `rocketchat_failure_color` – failure color's attachment\n## Usage\nIf you want to notify only about beginning of deployment add this line only:\n```php\nbefore('deploy', 'rocketchat:notify');\n```\nIf you want to notify about successful end of deployment add this too:\n```php\nafter('deploy:success', 'rocketchat:notify:success');\n```\nIf you want to notify about failed deployment add this too:\n```php\nafter('deploy:failed', 'rocketchat:notify:failure');\n```\n\n\n## Configuration\n### rockchat_title\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L65)\n\n\n\n```php title=\"Default value\"\nreturn get('application', 'Project');\n```\n\n\n### rocketchat_icon_emoji\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L69)\n\n\n\n```php title=\"Default value\"\n':robot:'\n```\n\n\n### rocketchat_icon_url\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L70)\n\n\n\n```php title=\"Default value\"\nnull\n```\n\n\n### rocketchat_channel\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L72)\n\n\n\n```php title=\"Default value\"\nnull\n```\n\n\n### rocketchat_room_id\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L73)\n\n\n\n```php title=\"Default value\"\nnull\n```\n\n\n### rocketchat_username\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L74)\n\n\n\n```php title=\"Default value\"\nnull\n```\n\n\n### rocketchat_webhook\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L75)\n\n\n\n```php title=\"Default value\"\nnull\n```\n\n\n### rocketchat_color\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L77)\n\n\n\n```php title=\"Default value\"\n'#000000'\n```\n\n\n### rocketchat_success_color\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L78)\n\n\n\n```php title=\"Default value\"\n'#00c100'\n```\n\n\n### rocketchat_failure_color\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L79)\n\n\n\n```php title=\"Default value\"\n'#ff0909'\n```\n\n\n### rocketchat_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L81)\n\n\n\n```php title=\"Default value\"\n'_{{user}}_ deploying `{{what}}` to *{{where}}*'\n```\n\n\n### rocketchat_success_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L82)\n\n\n\n```php title=\"Default value\"\n'Deploy to *{{where}}* successful'\n```\n\n\n### rocketchat_failure_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L83)\n\n\n\n```php title=\"Default value\"\n'Deploy to *{{where}}* failed'\n```\n\n\n\n## Tasks\n\n### rocketchat\\:notify {#rocketchat-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L86)\n\nNotifies RocketChat.\n\n\n\n\n### rocketchat\\:notify\\:success {#rocketchat-notify-success}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L116)\n\nNotifies RocketChat about deploy finish.\n\n\n\n\n### rocketchat\\:notify\\:failure {#rocketchat-notify-failure}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L146)\n\nNotifies RocketChat about deploy failure.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/rollbar.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/rollbar.php -->\n<!-- Then run bin/docgen -->\n\n# Rollbar Recipe\n\n```php\nrequire 'contrib/rollbar.php';\n```\n\n[Source](/contrib/rollbar.php)\n\n\n\n## Configuration\n- `rollbar_token` – access token to rollbar api\n- `rollbar_comment` – comment about deploy, default to\n  ```php\n  set('rollbar_comment', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\n  ```\n- `rollbar_username` – rollbar user name\n## Usage\nSince you should only notify Rollbar channel of a successful deployment, the `rollbar:notify` task should be executed right at the end.\n```php\nafter('deploy', 'rollbar:notify');\n```\n\n\n## Configuration\n### rollbar_comment\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rollbar.php#L27)\n\n\n\n```php title=\"Default value\"\n'_{{user}}_ deploying `{{what}}` to *{{where}}*'\n```\n\n\n\n## Tasks\n\n### rollbar\\:notify {#rollbar-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rollbar.php#L30)\n\nNotifies Rollbar of deployment.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/rsync.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/rsync.php -->\n<!-- Then run bin/docgen -->\n\n# Rsync Recipe\n\n```php\nrequire 'contrib/rsync.php';\n```\n\n[Source](/contrib/rsync.php)\n\n\n\n:::warning\nThis must not be confused with `/src/Utility/Rsync.php`, deployer's built-in rsync. Their configuration options are also very different, read carefully below.\n:::\n## Configuration options\n- **rsync**: Accepts an array with following rsync options (all are optional and defaults are ok):\n    - *exclude*: accepts an *array* with patterns to be excluded from sending to server\n    - *exclude-file*: accepts a *string* containing absolute path to file, which contains exclude patterns\n    - *include*: accepts an *array* with patterns to be included in sending to server\n    - *include-file*: accepts a *string* containing absolute path to file, which contains include patterns\n    - *filter*: accepts an *array* of rsync filter rules\n    - *filter-file*: accepts a *string* containing merge-file filename.\n    - *filter-perdir*: accepts a *string* containing merge-file filename to be scanned and merger per each directory in rsync list on files to send\n    - *flags*: accepts a *string* of flags to set when calling rsync command. Please **avoid** flags that accept params, and use *options* instead.\n    - *options*: accepts an *array* of options to set when calling rsync command. **DO NOT** prefix options with `--` as it's automatically added.\n    - *timeout*: accepts an *int* defining timeout for rsync command to run locally.\n### Sample Configuration:\nFollowing is default configuration. By default rsync ignores only git dir and `deploy.php` file.\n```php\ndeploy.php\nset('rsync',[\n    'exclude'      => [\n        '.git',\n        'deploy.php',\n    ],\n    'exclude-file' => false,\n    'include'      => [],\n    'include-file' => false,\n    'filter'       => [],\n    'filter-file'  => false,\n    'filter-perdir'=> false,\n    'flags'        => 'rz', // Recursive, with compress\n    'options'      => ['delete'],\n    'timeout'      => 60,\n]);\n```\nIf You have multiple excludes, You can put them in file and reference that instead. If You use `deploy:rsync_warmup` You could set additional options that could speed-up and/or affect way things are working. For example:\n```php\ndeploy.php\nset('rsync',[\n    'exclude'       => ['excludes_file'],\n    'exclude-file'  => '/tmp/localdeploys/excludes_file', //Use absolute path to avoid possible rsync problems\n    'include'       => [],\n    'include-file'  => false,\n    'filter'        => [],\n    'filter-file'   => false,\n    'filter-perdir' => false,\n    'flags'         => 'rzcE', // Recursive, with compress, check based on checksum rather than time/size, preserve Executable flag\n    'options'       => ['delete', 'delete-after', 'force'], //Delete after successful transfer, delete even if deleted dir is not empty\n    'timeout'       => 3600, //for those huge repos or crappy connection\n]);\n```\n### Parameter\n- **rsync_src**: per-host rsync source. This can be server, stage or whatever-dependent. By default it's set to current directory\n- **rsync_dest**: per-host rsync destination. This can be server, stage or whatever-dependent. by default it's equivalent to release deploy destination.\n### Sample configurations:\nThis is default configuration:\n```php\nset('rsync_src', __DIR__);\nset('rsync_dest','{{release_path}}');\n```\nIf You use local deploy recipe You can set src to local release:\n```php\nhost('hostname')\n    ->hostname('10.10.10.10')\n    ->port(22)\n    ->set('deploy_path','/your/remote/path/app')\n    ->set('rsync_src', '/your/local/path/app')\n    ->set('rsync_dest','{{release_path}}');\n```\n## Usage\n- `rsync` task\n    Set `rsync_src` to locally cloned repository and rsync to `rsync_dest`. Then set this task instead of `deploy:update_code` in Your `deploy` task if Your hosting provider does not allow git.\n- `rsync:warmup` task\n    If Your deploy task looks like:\n    ```php\n    task('deploy', [\n        'deploy:prepare',\n        'deploy:release',\n        'rsync',\n        'deploy:vendors',\n        'deploy:symlink',\n    ])->desc('Deploy your project');\n    ```\n    And Your `rsync_dest` is set to `{{release_path}}` then You could add this task to run before `rsync` task or after `deploy:release`, whatever is more convenient.\n\n\n## Configuration\n### rsync\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L119)\n\n\n\n```php title=\"Default value\"\n[\n    'exclude' => [\n        '.git',\n        'deploy.php',\n    ],\n    'exclude-file' => false,\n    'include' => [],\n    'include-file' => false,\n    'filter' => [],\n    'filter-file' => false,\n    'filter-perdir' => false,\n    'flags' => 'rz',\n    'options' => ['delete'],\n    'timeout' => 300,\n]\n```\n\n\n### rsync_src\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L135)\n\n\n\n```php title=\"Default value\"\n__DIR__\n```\n\n\n### rsync_dest\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L136)\n\n\n\n```php title=\"Default value\"\n'{{release_path}}'\n```\n\n\n### rsync_excludes\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L138)\n\n\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### rsync_includes\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L153)\n\n\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### rsync_filter\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L168)\n\n\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### rsync_options\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L186)\n\n\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n\n## Tasks\n\n### rsync\\:warmup {#rsync-warmup}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L198)\n\nWarmups remote Rsync target.\n\n\n\n\n### rsync {#rsync}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L213)\n\nRsync local->remote.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/sentry.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/sentry.php -->\n<!-- Then run bin/docgen -->\n\n# Sentry Recipe\n\n```php\nrequire 'contrib/sentry.php';\n```\n\n[Source](/contrib/sentry.php)\n\n\n\n### Configuration options\n- **organization** *(required)*: the slug of the organization the release belongs to.\n- **projects** *(required)*: array of slugs of the projects to create a release for.\n- **token** *(required)*: authentication token. Can be created at [https://sentry.io/settings/account/api/auth-tokens/]\n- **version** *(required)* – a version identifier for this release.\nCan be a version number, a commit hash etc. (Defaults is set to git log -n 1 --format=\"%h\".)\n- **version_prefix** *(optional)* - a string prefixed to version.\nReleases are global per organization so indipentent projects needs to prefix version number with unique string to avoid conflicts\n- **environment** *(optional)* - the environment you’re deploying to. By default framework's environment is used.\nFor example for symfony, *symfony_env* configuration is read otherwise defaults to 'prod'.\n- **ref** *(optional)* – an optional commit reference. This is useful if a tagged version has been provided.\n- **refs** *(optional)* - array to indicate the start and end commits for each repository included in a release.\nHead commits must include parameters *repository* and *commit*) (the HEAD sha).\nThey can optionally include *previousCommit* (the sha of the HEAD of the previous release),\nwhich should be specified if this is the first time you’ve sent commit data.\n- **commits** *(optional)* - array commits data to be associated with the release.\nCommits must include parameters *id* (the sha of the commit), and can optionally include *repository*,\n*message*, *author_name*, *author_email* and *timestamp*. By default will send all new commits,\nunless it's a first release, then only first 200 will be sent.\n- **url** *(optional)* – a URL that points to the release. This can be the path to an online interface to the sourcecode for instance.\n- **date_released** *(optional)* – date that indicates when the release went live. If not provided the current time is assumed.\n- **sentry_server** *(optional)* – sentry server (if you host it yourself). defaults to hosted sentry service.\n- **date_deploy_started** *(optional)* - date that indicates when the deploy started. Defaults to current time.\n- **date_deploy_finished** *(optional)* - date that indicates when the deploy ended. If not provided, the current time is used.\n- **deploy_name** *(optional)* - name of the deploy\n- **git_version_command** *(optional)* - the command that retrieves the git version information (Defaults is set to git log -n 1 --format=\"%h\", other options are git describe --tags --abbrev=0)\n```php\ndeploy.php\nset('sentry', [\n    'organization' => 'exampleorg',\n    'projects' => [\n        'exampleproj'\n    ],\n    'token' => 'd47828...',\n    'version' => '0.0.1',\n]);\n```\n### Suggested Usage\nSince you should only notify Sentry of a successful deployment, the deploy:sentry task should be executed right at the end.\n```php\ndeploy.php\nafter('deploy', 'deploy:sentry');\n```\n\n\n\n"
  },
  {
    "path": "docs/contrib/slack.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/slack.php -->\n<!-- Then run bin/docgen -->\n\n# Slack Recipe\n\n```php\nrequire 'contrib/slack.php';\n```\n\n[Source](/contrib/slack.php)\n\n\n\n## Installing\n<a href=\"https://slack.com/oauth/authorize?&client_id=113734341365.225973502034&scope=incoming-webhook\"><img alt=\"Add to Slack\" height=\"40\" width=\"139\" src=\"https://platform.slack-edge.com/img/add_to_slack.png\" srcset=\"https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x\" /></a>\nAdd hook on deploy:\n```php\nbefore('deploy', 'slack:notify');\n```\n## Configuration\n- `slack_webhook` – slack incoming webhook url, **required**\n  ```\n  set('slack_webhook', 'https://hooks.slack.com/...');\n  ```\n- `slack_channel` - channel to send notification to. The default is the channel configured in the webhook\n- `slack_title` – the title of application, default `{{application}}`\n- `slack_text` – notification message template, markdown supported\n  ```\n  set('slack_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\n  ```\n- `slack_success_text` – success template, default:\n  ```\n  set('slack_success_text', 'Deploy to *{{where}}* successful');\n  ```\n- `slack_failure_text` – failure template, default:\n  ```\n  set('slack_failure_text', 'Deploy to *{{where}}* failed');\n  ```\n- `slack_color` – color's attachment\n- `slack_success_color` – success color's attachment\n- `slack_failure_color` – failure color's attachment\n- `slack_fields` - set attachments fields for pretty output in Slack, default:\n  ```\n  set('slack_fields', []);\n  ```\n## Usage\nIf you want to notify only about beginning of deployment add this line only:\n```php\nbefore('deploy', 'slack:notify');\n```\nIf you want to notify about successful end of deployment add this too:\n```php\nafter('deploy:success', 'slack:notify:success');\n```\nIf you want to notify about failed deployment add this too:\n```php\nafter('deploy:failed', 'slack:notify:failure');\n```\n\n\n## Configuration\n### slack_channel\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L70)\n\nChannel to publish to, when false the default channel the webhook will be used\n\n```php title=\"Default value\"\nfalse\n```\n\n\n### slack_title\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L73)\n\nTitle of project\n\n```php title=\"Default value\"\nreturn get('application', 'Project');\n```\n\n\n### slack_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L78)\n\nDeploy message\n\n```php title=\"Default value\"\n'_{{user}}_ deploying `{{what}}` to *{{where}}*'\n```\n\n\n### slack_success_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L79)\n\n\n\n```php title=\"Default value\"\n'Deploy to *{{where}}* successful'\n```\n\n\n### slack_failure_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L80)\n\n\n\n```php title=\"Default value\"\n'Deploy to *{{where}}* failed'\n```\n\n\n### slack_rollback_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L81)\n\n\n\n```php title=\"Default value\"\n'_{{user}}_ rolled back changes on *{{where}}*'\n```\n\n\n### slack_fields\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L82)\n\n\n\n\n\n### slack_color\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L85)\n\nColor of attachment\n\n```php title=\"Default value\"\n'#4d91f7'\n```\n\n\n### slack_success_color\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L86)\n\n\n\n```php title=\"Default value\"\n'#00c100'\n```\n\n\n### slack_failure_color\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L87)\n\n\n\n```php title=\"Default value\"\n'#ff0909'\n```\n\n\n### slack_rollback_color\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L88)\n\n\n\n```php title=\"Default value\"\n'#eba211'\n```\n\n\n\n## Tasks\n\n### slack\\:notify {#slack-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L100)\n\nNotifies Slack.\n\n\n\n\n### slack\\:notify\\:success {#slack-notify-success}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L120)\n\nNotifies Slack about deploy finish.\n\n\n\n\n### slack\\:notify\\:failure {#slack-notify-failure}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L141)\n\nNotifies Slack about deploy failure.\n\n\n\n\n### slack\\:notify\\:rollback {#slack-notify-rollback}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L161)\n\nNotifies Slack about rollback.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/supervisord-monitor.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/supervisord-monitor.php -->\n<!-- Then run bin/docgen -->\n\n# Supervisord-monitor Recipe\n\n```php\nrequire 'contrib/supervisord-monitor.php';\n```\n\n[Source](/contrib/supervisord-monitor.php)\n\n\n\n### Description\nThis is a recipe that uses the [Supervisord server monitoring project](https://github.com/mlazarov/supervisord-monitor).\nWith this recipe the possibility is created to restart a supervisord process through the Supervisor Monitor webtool, by using cURL. This workaround is particular usefull when the deployment user has unsuficient rights to restart a daemon process from the cli.\n### Configuration\n```\nset('supervisord', [\n    'uri' => 'https://youruri.xyz/supervisor',\n    'basic_auth_user' => 'username',\n    'basic_auth_password' => 'password',\n    'process_name' => 'process01',\n]);\n```\nor\n```\nset('supervisord_uri', 'https://youruri.xyz/supervisor');\nset('supervisord_basic_auth_user', 'username');\nset('supervisord_basic_auth_password', 'password');\nset('supervisord_process_name', 'process01');\n```\n- `supervisord` – array with configuration for Supervisord\n    - `uri` – URI to the Supervisord monitor page\n    - `basic_auth_user` – Basic auth username to access the URI\n    - `basic_auth_password` – Basic auth password to access the URI\n    - `process_name` – the process name, as visible in the Supervisord monitor page. Multiple processes can be listed here, comma separated\n### Task\n- `supervisord-monitor:restart` Restarts given processes\n- `supervisord-monitor:stop` Stops given processes\n- `supervisord-monitor:start` Starts given processes\n### Usage\nA complete example with configs, staging and deployment\n```\n<?php\nnamespace Deployer;\nuse Dotenv\\Dotenv;\nrequire 'vendor/autoload.php';\nrequire 'supervisord_monitor.php';\nProject name\nset('application', 'myproject.com');\nProject repository\nset('repository', 'git@github.com:myorg/myproject.com');\nset('supervisord', [\n    'uri' => 'https://youruri.xyz/supervisor',\n    'basic_auth_user' => 'username',\n    'basic_auth_password' => 'password',\n    'process_name' => 'process01',\n]);\nhost('staging.myproject.com')\n    ->set('branch', 'develop')\n    ->set('labels', ['stage' => 'staging']);\nhost('myproject.com')\n    ->set('branch', 'main')\n    ->set('labels', ['stage' => 'production']);\nTasks\ntask('build', function () {\n    run('cd {{release_path}} && build');\n});\ntask('deploy', [\n    'build',\n    'supervisord',\n]);\ntask('supervisord', ['supervisord-monitor:restart'])\n    ->select('stage=production');\n```\n\n\n\n## Tasks\n\n### supervisord-monitor\\:restart {#supervisord-monitor-restart}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/supervisord-monitor.php#L134)\n\n\n\n\n\n\n### supervisord-monitor\\:stop {#supervisord-monitor-stop}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/supervisord-monitor.php#L151)\n\n\n\n\n\n\n### supervisord-monitor\\:start {#supervisord-monitor-start}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/supervisord-monitor.php#L165)\n\n\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/telegram.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/telegram.php -->\n<!-- Then run bin/docgen -->\n\n# Telegram Recipe\n\n```php\nrequire 'contrib/telegram.php';\n```\n\n[Source](/contrib/telegram.php)\n\n\n\n## Installing\n  1. Create telegram bot with [BotFather](https://t.me/BotFather) and grab the token provided\n  2. Send `/start` to your bot and open https://api.telegram.org/bot{$TELEGRAM_TOKEN_HERE}/getUpdates\n  3. Take chat_id from response\nAdd hook on deploy:\n```php\nbefore('deploy', 'telegram:notify');\n```\n## Configuration\n- `telegram_token` – telegram bot token, **required**\n- `telegram_chat_id` — chat ID to push messages to\n- `telegram_proxy` - proxy connection string in [CURLOPT_PROXY](https://curl.haxx.se/libcurl/c/CURLOPT_PROXY.html) form like:\n  ```\n  http://proxy:80\n  socks5://user:password@host:3128\n   ```\n- `telegram_title` – the title of application, default `{{application}}`\n- `telegram_text` – notification message template\n  ```\n  _{{user}}_ deploying `{{what}}` to *{{where}}*\n  ```\n- `telegram_success_text` – success template, default:\n  ```\n  Deploy to *{{where}}* successful\n  ```\n- `telegram_failure_text` – failure template, default:\n  ```\n  Deploy to *{{where}}* failed\n  ```\n## Usage\nIf you want to notify only about beginning of deployment add this line only:\n```php\nbefore('deploy', 'telegram:notify');\n```\nIf you want to notify about successful end of deployment add this too:\n```php\nafter('deploy:success', 'telegram:notify:success');\n```\nIf you want to notify about failed deployment add this too:\n```php\nafter('deploy:failed', 'telegram:notify:failure');\n\n\n## Configuration\n### telegram_title\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L65)\n\nTitle of project\n\n```php title=\"Default value\"\nreturn get('application', 'Project');\n```\n\n\n### telegram_token\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L70)\n\nTelegram settings\n:::info Required\nThrows exception if not set.\n:::\n\n\n\n\n### telegram_chat_id\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L73)\n\n\n:::info Required\nThrows exception if not set.\n:::\n\n\n\n\n### telegram_url\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L76)\n\n\n\n```php title=\"Default value\"\nreturn 'https://api.telegram.org/bot' . get('telegram_token') . '/sendmessage';\n```\n\n\n### telegram_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L81)\n\nDeploy message\n\n```php title=\"Default value\"\n'_{{user}}_ deploying `{{what}}` to *{{where}}*'\n```\n\n\n### telegram_success_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L82)\n\n\n\n```php title=\"Default value\"\n'Deploy to *{{where}}* successful'\n```\n\n\n### telegram_failure_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L83)\n\n\n\n```php title=\"Default value\"\n'Deploy to *{{where}}* failed'\n```\n\n\n\n## Tasks\n\n### telegram\\:notify {#telegram-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L87)\n\nNotifies Telegram.\n\n\n\n\n### telegram\\:notify\\:success {#telegram-notify-success}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L118)\n\nNotifies Telegram about deploy finish.\n\n\n\n\n### telegram\\:notify\\:failure {#telegram-notify-failure}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L149)\n\nNotifies Telegram about deploy failure.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/webpack_encore.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/webpack_encore.php -->\n<!-- Then run bin/docgen -->\n\n# Webpack Encore Recipe\n\n```php\nrequire 'contrib/webpack_encore.php';\n```\n\n[Source](/contrib/webpack_encore.php)\n\n* Requires\n  * [npm](/docs/contrib/npm.md)\n  * [yarn](/docs/contrib/yarn.md)\n\n\n## Configuration\n- **webpack_encore/package_manager** *(optional)*: set yarn or npm. We try to find if yarn or npm is available and used.\n## Usage\n```php\nFor Yarn\nafter('deploy:update_code', 'yarn:install');\nFor npm\nafter('deploy:update_code', 'npm:install');\nafter('deploy:update_code', 'webpack_encore:build');\n```\n\n\n## Configuration\n### webpack_encore/package_manager\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/webpack_encore.php#L25)\n\n## Configuration\n- **webpack_encore/package_manager** *(optional)*: set yarn or npm. We try to find if yarn or npm is available and used.\n## Usage\n```php\nFor Yarn\nafter('deploy:update_code', 'yarn:install');\nFor npm\nafter('deploy:update_code', 'npm:install');\nafter('deploy:update_code', 'webpack_encore:build');\n```\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### webpack_encore/env\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/webpack_encore.php#L33)\n\n\n\n```php title=\"Default value\"\n'production'\n```\n\n\n\n## Tasks\n\n### webpack_encore\\:build {#webpack_encore-build}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/webpack_encore.php#L36)\n\nRuns webpack encore build.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/workplace.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/workplace.php -->\n<!-- Then run bin/docgen -->\n\n# Workplace Recipe\n\n```php\nrequire 'contrib/workplace.php';\n```\n\n[Source](/contrib/workplace.php)\n\n\n\nThis recipes works with Custom Integrations and Publishing Bots.\nAdd hook on deploy:\n```\nbefore('deploy', 'workplace:notify');\n```\n## Configuration\n - `workplace_webhook` - incoming workplace webhook **required**\n   ```\n   // With custom integration\n   set('workplace_webhook', 'https://graph.facebook.com/<GROUP_ID>/feed?access_token=<ACCESS_TOKEN>');\n   // With publishing bot\n   set('workplace_webhook', 'https://graph.facebook.com/v3.0/group/feed?access_token=<ACCESS_TOKEN>');\n   // Use markdown on message\n   set('workplace_webhook', 'https://graph.facebook.com/<GROUP_ID>/feed?access_token=<ACCESS_TOKEN>&formatting=MARKDOWN');\n   ```\n - `workplace_text` - notification message\n   ```\n   set('workplace_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*');\n   ```\n - `workplace_success_text` – success template, default:\n  ```\n  set('workplace_success_text', 'Deploy to *{{where}}* successful');\n  ```\n - `workplace_failure_text` – failure template, default:\n  ```\n  set('workplace_failure_text', 'Deploy to *{{where}}* failed');\n  ```\n - `workplace_edit_post` – whether to create a new post for deploy result, or edit the first one created, default creates a new post:\n  ```\n  set('workplace_edit_post', false);\n  ```\n## Usage\nIf you want to notify only about beginning of deployment add this line only:\n```php\nbefore('deploy', 'workplace:notify');\n```\nIf you want to notify about successful end of deployment add this too:\n```php\nafter('deploy:success', 'workplace:notify:success');\n```\nIf you want to notify about failed deployment add this too:\n```php\nafter('deploy:failed', 'workplace:notify:failure');\n```\n\n\n## Configuration\n### workplace_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/workplace.php#L71)\n\nDeploy message\n\n```php title=\"Default value\"\n'_{{user}}_ deploying `{{what}}` to *{{where}}*'\n```\n\n\n### workplace_success_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/workplace.php#L72)\n\n\n\n```php title=\"Default value\"\n'Deploy to *{{where}}* successful'\n```\n\n\n### workplace_failure_text\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/workplace.php#L73)\n\n\n\n```php title=\"Default value\"\n'Deploy to *{{where}}* failed'\n```\n\n\n### workplace_edit_post\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/workplace.php#L76)\n\nBy default, create a new post for every message\n\n```php title=\"Default value\"\nfalse\n```\n\n\n\n## Tasks\n\n### workplace\\:notify {#workplace-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/workplace.php#L79)\n\nNotifies Workplace.\n\n\n\n\n### workplace\\:notify\\:success {#workplace-notify-success}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/workplace.php#L103)\n\nNotifies Workplace about deploy finish.\n\n\n\n\n### workplace\\:notify\\:failure {#workplace-notify-failure}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/workplace.php#L114)\n\nNotifies Workplace about deploy failure.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/yammer.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/yammer.php -->\n<!-- Then run bin/docgen -->\n\n# Yammer Recipe\n\n```php\nrequire 'contrib/yammer.php';\n```\n\n[Source](/contrib/yammer.php)\n\n\n\nAdd hook on deploy:\n```php\nbefore('deploy', 'yammer:notify');\n```\n## Configuration\n- `yammer_url` – The URL to the message endpoint, default is https://www.yammer.com/api/v1/messages.json\n- `yammer_token` *(required)* – Yammer auth token\n- `yammer_group_id` *(required)* - Group ID\n- `yammer_title` – the title of application, default `{{application}}`\n- `yammer_body` – notification message template, default:\n  ```\n  <em>{{user}}</em> deploying {{what}} to <strong>{{where}}</strong>\n  ```\n- `yammer_success_body` – success template, default:\n  ```\n  Deploy to <strong>{{where}}</strong> successful\n  ```\n- `yammer_failure_body` – failure template, default:\n  ```\n  Deploy to <strong>{{where}}</strong> failed\n  ```\n## Usage\nIf you want to notify only about beginning of deployment add this line only:\n```php\nbefore('deploy', 'yammer:notify');\n```\nIf you want to notify about successful end of deployment add this too:\n```php\nafter('deploy:success', 'yammer:notify:success');\n```\nIf you want to notify about failed deployment add this too:\n```php\nafter('deploy:failed', 'yammer:notify:failure');\n```\n\n\n## Configuration\n### yammer_url\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L55)\n\n\n\n```php title=\"Default value\"\n'https://www.yammer.com/api/v1/messages.json'\n```\n\n\n### yammer_title\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L58)\n\nTitle of project\n\n```php title=\"Default value\"\nreturn get('application', 'Project');\n```\n\n\n### yammer_body\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L63)\n\nDeploy message\n\n```php title=\"Default value\"\n'<em>{{user}}</em> deploying {{what}} to <strong>{{where}}</strong>'\n```\n\n\n### yammer_success_body\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L64)\n\n\n\n```php title=\"Default value\"\n'Deploy to <strong>{{where}}</strong> successful'\n```\n\n\n### yammer_failure_body\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L65)\n\n\n\n```php title=\"Default value\"\n'Deploy to <strong>{{where}}</strong> failed'\n```\n\n\n\n## Tasks\n\n### yammer\\:notify {#yammer-notify}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L68)\n\nNotifies Yammer.\n\n\n\n\n### yammer\\:notify\\:success {#yammer-notify-success}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L87)\n\nNotifies Yammer about deploy finish.\n\n\n\n\n### yammer\\:notify\\:failure {#yammer-notify-failure}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L106)\n\nNotifies Yammer about deploy failure.\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/yarn.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit contrib/yarn.php -->\n<!-- Then run bin/docgen -->\n\n# Yarn Recipe\n\n```php\nrequire 'contrib/yarn.php';\n```\n\n[Source](/contrib/yarn.php)\n\n\n\n## Configuration\n- **bin/yarn** *(optional)*: set Yarn binary, automatically detected otherwise.\n## Usage\n```php\nafter('deploy:update_code', 'yarn:install');\n```\n\n\n## Configuration\n### bin/yarn\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/yarn.php#L16)\n\n## Configuration\n- **bin/yarn** *(optional)*: set Yarn binary, automatically detected otherwise.\n## Usage\n```php\nafter('deploy:update_code', 'yarn:install');\n```\n\n```php title=\"Default value\"\nreturn which('yarn');\n```\n\n\n\n## Tasks\n\n### yarn\\:install {#yarn-install}\n[Source](https://github.com/deployphp/deployer/blob/master/contrib/yarn.php#L22)\n\nInstalls Yarn packages.\n\nIn there is a {{previous_release}}, node_modules will be copied from it before installing deps with yarn.\n\n\n"
  },
  {
    "path": "docs/getting-started.md",
    "content": "# Getting Started\n\nThis tutorial will guide you through:\n\n- Setting up a new host with the [provision](recipe/provision.md) recipe.\n- Configuring a deployment and performing your first deploy.\n\n## Step 1: Install Deployer {#install}\n\nFirst, [install Deployer](installation.md). Once installed, navigate to your project directory and run:\n\n```sh\ndep init\n```\n\nDeployer will prompt you with a series of questions. After completing them, you'll have a **deploy.php** or \n**deploy.yaml** file—your deployment recipe. This file defines hosts, tasks, and dependencies on other recipes.\nFramework-specific recipes provided by Deployer are based on the [common](recipe/common.md) recipe.\n\n---\n\n## Step 2: Provision a New Server {#provision}\n\n:::note\nIf you already have a configured web server, skip to [deployment](#deploy).\n:::\n\n### Setting Up Your VPS\n\nCreate a new VPS with a provider like Linode, DigitalOcean, Vultr, AWS, or GCP. Use an **Ubuntu** image, as it's\nsupported by Deployer's [provision](recipe/provision.md) recipe.\n\nTo utilize Deployer for server provisioning, you must initially configure your server to permit key-based authentication for the root user (which is disabled by default for recent Ubuntu images). Once provisioning is complete, this root access via SSH can be disabled.\n\n:::tip\nSet up a DNS record pointing your domain to your server's IP address. This allows you to SSH into the server using your\ndomain name instead of its IP.\n:::\n\n### Configuring `deploy.php`\n\nYour **deploy.php** recipe should define your host with key parameters:\n\n- **`remote_user`**: The SSH username.\n- **`deploy_path`**: The file path where your project will be deployed.\n\nExample:\n\n```php\nhost('example.org')\n    ->set('remote_user', 'deployer')\n    ->set('deploy_path', '~/example');\n```\n\nIf your server only has a `root` user, the `provision` recipe will create and configure a `deployer` user for you.\n\n### Adding an Identity Key\n\nTo connect to your server, use an identity key or private key. Instead of defining it directly in your host\nconfiguration, add it to your **~/.ssh/config** file:\n\n```\nHost *\n  IdentityFile ~/.ssh/id_rsa\n```\n\n### Provisioning the Server\n\nRun the following command to provision your server:\n\n```sh\ndep provision\n```\n\n:::tip\n\n- To change the default `root` user, use:\n  ```sh\n  dep provision -o provision_user=your-user\n  ```\n- If your remote user can `sudo` to become root, use:\n  ```sh\n  dep provision -o become=root\n  ```\n\n:::\n\nDuring provisioning, Deployer will ask about PHP versions, database preferences, and more. It takes about **5 minutes**\nand installs everything required to run a website. The deployment path is configured\nas [deploy_path](recipe/common.md#deploy_path).\n\n---\n\n## Step 3: Deploy Your Project {#deploy}\n\nDeploy your project with:\n\n```sh\ndep deploy\n```\n\nIf the deployment fails, Deployer will display the error and the failed command. You may need to configure your `.env`\nfile or similar credentials. To edit files directly on the server:\n\n```sh\ndep ssh\n```\n\nIf needed, resume deployment from the last step:\n\n```sh\ndep deploy --start-from deploy:migrate\n```\n\n---\n\n## Step 4: Post-Deployment Configuration\n\nAfter the first successful deployment, the server directory structure looks like this:\n\n```\n~/example                      // deploy_path\n |- current -> releases/1      // Symlink to current release\n |- releases                   // Directory for all releases\n    |- 1                       // Latest release\n       |- ...\n       |- .env -> shared/.env  // Symlink to shared .env file\n |- shared                     // Shared files between releases\n    |- ...\n    |- .env                    // Shared .env file\n |- .dep                       // Deployer configuration files\n```\n\n### Web Server Setup\n\nConfigure your web server to serve from the `current` directory. Example for Nginx:\n\n```nginx\nroot /home/deployer/example/current/public;\nindex index.php;\nlocation / {\n    try_files $uri $uri/ /index.php?$query_string;\n}\n```\n\nFor those using the [provision recipe](recipe/provision.md), Deployer will automatically configure the Caddy web server\nto serve from the [public_path](recipe/provision/website.md#public_path).\n\n---\n\n## Step 5: Adding a Build Step\n\nTo automate build steps, add a task in your **deploy.php**:\n\n```php\ntask('build', function () {\n    cd('{{release_path}}');\n    run('npm install');\n    run('npm run prod');\n});\n\nafter('deploy:update_code', 'build');\n```\n\n---\n\n## Examining Deployments\n\nUse the `releases` task to view deployment details:\n\n```sh\ndep releases\n```\n\nExample output:\n\n```\n+---------------------+--------- deployer.org -------+--------+-----------+\n| Date (UTC)          | Release     | Author         | Target | Commit    |\n+---------------------+-------------+----------------+--------+-----------+\n| 2021-11-05 14:00:22 | 1 (current) | Anton Medvedev | HEAD   | 943ded2be |\n+---------------------+-------------+----------------+--------+-----------+\n```\n\n:::tip\nDuring development, the [dep push](recipe/deploy/push.md) task maybe useful\nto create a patch of local changes and push them to the host.\n:::\n\n--- \n\nWith Deployer, you're now ready to efficiently set up, provision, and manage deployments for your projects!\n"
  },
  {
    "path": "docs/hosts.md",
    "content": "# Hosts\n\nIn Deployer, you define hosts using the [host()](api.md#host) function.\n\n### Defining a Host\n\n```php\nhost('example.org');\n```\n\nEach host is associated with configuration key-value pairs. When you define a host, two key configurations are set:\n\n- **`hostname`**: Used for connecting to the remote host.\n- **`alias`**: A unique identifier for the host in recipe.\n\n### Example: Using Host Configurations\n\nYou can access host configurations within tasks with the [currentHost()](api.md#currenthost) function:\n\n```php\ntask('test', function () {\n    $hostname = currentHost()->get('hostname');\n    $alias = currentHost()->get('alias');\n    writeln(\"The $alias is $hostname\");\n});\n```\n\nOr using brackets syntax:\n\n```php\ntask('test', function () {\n    writeln('The {{alias}} is {{hostname}}');\n});\n```\n\nRunning the task:\n\n```sh\n$ dep test\n[example.org] The example.org is example.org\n```\n\n### Overriding Hostname\n\nYou can override the default hostname with the `set()` method:\n\n```php\nhost('example.org')\n    ->set('hostname', 'example.cloud.google.com');\n```\n\nNow the `hostname` is used for SSH connections, but the `alias` remains unchanged:\n\n```sh\n$ dep test\n[example.org] The example.org is example.cloud.google.com\n```\n\n### Configuring Remote User\n\nSpecify the `remote_user` to define which user to connect as:\n\n```php\nhost('example.org')\n    ->set('hostname', 'example.cloud.google.com')\n    ->set('remote_user', 'deployer');\n```\n\nDeployer will now connect using `ssh deployer@example.cloud.google.com`.\n\nAlternatively, you can use special setter methods for better IDE autocompletion:\n\n```php\nhost('example.org')\n    ->setHostname('example.cloud.google.com')\n    ->setRemoteUser('deployer');\n```\n\n---\n\n## Host Labels\n\nLabels allow you to group and identify hosts for specific deployments. Labels are defined as key-value pairs:\n\n```php\nhost('example.org')->setLabels(['stage' => 'prod']);\nhost('staging.example.org')->setLabels(['stage' => 'staging']);\n```\n\nLabels become powerful in multi-server setups:\n\n```php\nhost('admin.example.org')->setLabels(['stage' => 'prod', 'role' => 'web']);\nhost('web[1:5].example.org')->setLabels(['stage' => 'prod', 'role' => 'web']);\nhost('db[1:2].example.org')->setLabels(['stage' => 'prod', 'role' => 'db']);\nhost('test.example.org')->setLabels(['stage' => 'test', 'role' => 'web']);\nhost('special.example.org')->setLabels(['role' => 'special']);\n```\n\n### Filtering Hosts by Labels\n\nWhen deploying, you can filter hosts using label selectors:\n\n```sh\n$ dep deploy stage=prod&role=web,role=special\n```\n\n- Use `&` to specify multiple labels that must match on the same host.\n- Use `,` to separate multiple selections.\n\nSet a default selection string for convenience:\n\n```php\nset('default_selector', \"stage=prod&role=web,role=special\");\n```\n\n---\n\n## Host Configurations\n\n### Key Host Configurations\n\n| Config Key             | Description                                                                                    |\n|------------------------|------------------------------------------------------------------------------------------------|\n| **`alias`**            | Identifier for the host (e.g., `prod`, `staging`).                                             |\n| **`hostname`**         | Actual hostname or IP address used for SSH connections.                                        |\n| **`remote_user`**      | SSH username. Defaults to the current OS user or `~/.ssh/config`.                              |\n| **`port`**             | SSH port. Default is `22`.                                                                     |\n| **`config_file`**      | SSH config file location. Default is `~/.ssh/config`.                                          |\n| **`identity_file`**    | SSH private key file. E.g., `~/.ssh/id_rsa`.                                                   |\n| **`forward_agent`**    | Enable SSH agent forwarding. Default is `true`.                                                |\n| **`ssh_multiplexing`** | Enable SSH multiplexing for performance. Default is `true`.                                    |\n| **`shell`**            | Shell to use. Default is `bash -ls`.                                                           |\n| **`deploy_path`**      | Directory for deployments. E.g., `~/myapp`.                                                    |\n| **`labels`**           | Key-value pairs for host selection.                                                            |\n| **`ssh_arguments`**    | Additional SSH options. E.g., `['-o UserKnownHostsFile=/dev/null']`.                           |\n| **`ssh_control_path`** | Control path for SSH multiplexing. Default is `~/.ssh/%C` or `/dev/shm/%C` in CI environments. |\n\n### Best Practices\n\nAvoid storing sensitive SSH connection parameters in `deploy.php`. Instead, configure them in `~/.ssh/config`:\n\n```\nHost *\n  IdentityFile ~/.ssh/id_rsa\n```\n\n---\n\n## Advanced Host Definitions\n\n### Multiple Hosts\n\nDefine multiple hosts in one call:\n\n```php\nhost('example.org', 'deployer.org', 'another.org')->setRemoteUser('anton');\n```\n\n### Host Ranges\n\nFor patterns with many hosts, use ranges:\n\n```php\nhost('www[01:50].example.org'); // Will define hosts \"www01.example.org\", \"www02.example.org\", etc.\nhost('db[a:f].example.org'); // Will define hosts \"dba.example.org\", \"dbb.example.org\", etc.\n```\n\n- Numeric ranges can include leading zeros.\n- Alphabetic ranges are also supported.\n\n### Localhost\n\nUse the [localhost()](api.md#localhost) function for local execution:\n\n```php\nlocalhost(); // Alias and hostname are \"localhost\".\nlocalhost('ci'); // Alias is \"ci\", hostname is \"localhost\".\n```\n\nNow [run()](api.md#run) will execute on command locally. Alternatively, you can use [runLocally()](api.md#runlocally)\nfunction.\n\n### YAML Inventory\n\nSeparate host definitions into an external file using the [import()](api.md#import) function:\n\n```php title=\"deploy.php\"\nimport('inventory.yaml');\n```\n\n```yaml title=\"inventory.yaml\"\nhosts:\n  example.org:\n    remote_user: deployer\n  deployer.org:\n    remote_user: deployer\n```\n\n---\n\nWith these tools and configurations, you can manage and deploy to hosts effectively, whether it's a single server or a\ncomplex multi-host setup. Happy deploying!\n"
  },
  {
    "path": "docs/installation.md",
    "content": "# Installation\n\nThere are two ways to install Deployer: globally or locally. Global installation is recommended for most users, as it\nallows you to use Deployer from any directory.\nLocal (or project) installation is preferred for CI/CD pipelines, as it allows you to use the same version of Deployer\nacross all environments.\n\n## Global Installation\n\nTo install Deployer globally, use one of the following commands in your project directory:\n\n```sh\ncomposer global require deployer/deployer\n```\n\nOr:\n\n```sh\nphive install deployer\n```\n\n:::tip Path to Executable\n\nMake sure that Composer's global bin directory is in your `PATH`. Typically, you can add the following line to your\nshell configuration file (e.g., `.bashrc`, `.zshrc`):\n\n```sh\nexport PATH=\"$HOME/.composer/vendor/bin:$PATH\"\n\n```\n\nAfter adding this line, reload your shell configuration:\n\n```sh\nsource ~/.bashrc\n```\n\nor, for Zsh:\n\n```sh\nsource ~/.zshrc\n```\n\n:::\n\nTo set up Deployer in your project and create the `deploy.php` configuration file, run:\n\n```sh\ndep init\n```\n\n### Autocomplete Support\n\nDeployer includes support for autocompletion, helping you quickly find task names, options, and hosts. To enable\nautocomplete for various shells, use the following commands:\n\n\n- **Bash**:\n\n  ```sh\n  dep completion bash > /etc/bash_completion.d/deployer\n  ```\n\n  Make sure your `.bashrc` file sources the generated file so that bash completion works.\n\n- **Zsh**:\n\n  ```sh\n  dep completion zsh > ~/.zsh/completion/_deployer\n  ```\n\n  Ensure that your `.zshrc` file includes the directory where `_deployer` is located in the `fpath`.\n\n- **Fish**:\n\n  ```sh\n  dep completion fish > ~/.config/fish/completions/deployer.fish\n  ```\n\n  The generated file will be automatically loaded by Fish.\n\n## Project Installation\n\nThe project installation method is recommended for CI/CD pipelines, as it allows you to use the same version of Deployer\nacross all environments.\n\nTo install Deployer in your project, run the following command:\n\n```sh\ncomposer require --dev deployer/deployer\n```\n\n:::tip Configuring Shell Alias\nTo make using Deployer more convenient, you can set up a shell alias. This will allow you to run Deployer commands more\neasily. Add the following line to your shell configuration file (e.g., `.bashrc`, `.zshrc`):\n\n```sh\nalias dep='vendor/bin/dep'\n```\n\nThis alias lets you use `dep` instead of typing the full path each time.\n:::\n\nThen, to initialize Deployer in your project, use:\n\n```sh\nvendor/bin/dep init\n```\n\n## Downloading the Phar File\n\nAnother option for installing Deployer is to download the Phar file. You can find the latest version on\nthe [download page](/download).\n\nAdding `deployer.phar` to your project repository is recommended to ensure everyone, including your CI pipeline, uses\nthe same version of Deployer. This helps maintain consistency across all environments.\n\nOnce downloaded, run it in your project directory:\n\n```sh\nphp deployer.phar init\n```\n\nThis method provides a simple way to use Deployer without needing Composer.\n\n"
  },
  {
    "path": "docs/recipe/README.md",
    "content": "# All Recipes\n\n* [Cakephp Recipe](/docs/recipe/cakephp.md)\n* [Codeigniter 4 Recipe](/docs/recipe/codeigniter4.md)\n* [Codeigniter Recipe](/docs/recipe/codeigniter.md)\n* [Common Recipe](/docs/recipe/common.md)\n* [Composer Recipe](/docs/recipe/composer.md)\n* [Contao Recipe](/docs/recipe/contao.md)\n* [Craftcms Recipe](/docs/recipe/craftcms.md)\n* [Drupal 7 Recipe](/docs/recipe/drupal7.md)\n* [Drupal 8 Recipe](/docs/recipe/drupal8.md)\n* [Flow Framework Recipe](/docs/recipe/flow_framework.md)\n* [Fuelphp Recipe](/docs/recipe/fuelphp.md)\n* [Joomla Recipe](/docs/recipe/joomla.md)\n* [Laravel Recipe](/docs/recipe/laravel.md)\n* [Magento 2 Recipe](/docs/recipe/magento2.md)\n* [Magento Recipe](/docs/recipe/magento.md)\n* [Pimcore Recipe](/docs/recipe/pimcore.md)\n* [Prestashop Recipe](/docs/recipe/prestashop.md)\n* [Provision Recipe](/docs/recipe/provision.md)\n* [Shopware Recipe](/docs/recipe/shopware.md)\n* [Silverstripe Recipe](/docs/recipe/silverstripe.md)\n* [Spiral Recipe](/docs/recipe/spiral.md)\n* [Statamic Recipe](/docs/recipe/statamic.md)\n* [Sulu Recipe](/docs/recipe/sulu.md)\n* [Symfony Recipe](/docs/recipe/symfony.md)\n* [TYPO3 Recipe](/docs/recipe/typo3.md)\n* [WordPress Recipe](/docs/recipe/wordpress.md)\n* [Yii2 Recipe](/docs/recipe/yii.md)\n* [Zend Framework Recipe](/docs/recipe/zend_framework.md)"
  },
  {
    "path": "docs/recipe/cakephp.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/cakephp.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Cakephp Project\n\n```php\nrequire 'recipe/cakephp.php';\n```\n\n[Source](/recipe/cakephp.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Cakephp application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Cakephp** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [deploy:init](/docs/recipe/cakephp.md#deploy-init) – \n* [deploy:run_migrations](/docs/recipe/cakephp.md#deploy-run_migrations) – \n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe cakephp recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/cakephp.php#L14)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\nCakePHP 4 Project Template configuration\nCakePHP 4 Project Template shared dirs\n\n```php title=\"Default value\"\n[\n    'logs',\n    'tmp',\n]\n```\n\n\n### shared_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/cakephp.php#L20)\n\nOverrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`.\n\nCakePHP 4 Project Template shared files\n\n```php title=\"Default value\"\n[\n    'config/.env',\n    'config/app.php',\n]\n```\n\n\n\n## Tasks\n\n### deploy\\:init {#deploy-init}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/cakephp.php#L28)\n\n\n\nCreate plugins' symlinks\n\n\n### deploy\\:run_migrations {#deploy-run_migrations}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/cakephp.php#L35)\n\n\n\nRun migrations\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/cakephp.php#L43)\n\n\n\nMain task\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [deploy:init](/docs/recipe/cakephp.md#deploy-init)\n* [deploy:run_migrations](/docs/recipe/cakephp.md#deploy-run_migrations)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/codeigniter.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/codeigniter.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Codeigniter Project\n\n```php\nrequire 'recipe/codeigniter.php';\n```\n\n[Source](/recipe/codeigniter.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Codeigniter application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Codeigniter** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe codeigniter recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter.php#L10)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\nCodeIgniter shared dirs\n\n```php title=\"Default value\"\n['application/cache', 'application/logs']\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter.php#L13)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\nCodeIgniter writable dirs\n\n```php title=\"Default value\"\n['application/cache', 'application/logs']\n```\n\n\n\n## Tasks\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter.php#L19)\n\nDeploys your project.\n\nMain task\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/codeigniter4.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/codeigniter4.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Codeigniter 4 Project\n\n```php\nrequire 'recipe/codeigniter4.php';\n```\n\n[Source](/recipe/codeigniter4.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Codeigniter 4 application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Codeigniter 4** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [spark:optimize](/docs/recipe/codeigniter4.md#spark-optimize) – Optimize for production.\n* [spark:migrate](/docs/recipe/codeigniter4.md#spark-migrate) – Locates and runs all new migrations against the database.\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe codeigniter4 recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### public_path\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L10)\n\nOverrides [public_path](/docs/recipe/provision/website.md#public_path) from `recipe/provision/website.php`.\n\nDefault Configurations\n\n```php title=\"Default value\"\n'public'\n```\n\n\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L12)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n['writable']\n```\n\n\n### shared_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L14)\n\nOverrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n['.env']\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L16)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\n\n\n```php title=\"Default value\"\n[\n    'writable/cache',\n    'writable/debugbar',\n    'writable/logs',\n    'writable/session',\n    'writable/uploads',\n]\n```\n\n\n### log_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L24)\n\n\n\n```php title=\"Default value\"\n'writable/logs/*.log'\n```\n\n\n### codeigniter4_version\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L26)\n\n\n\n```php title=\"Default value\"\n$result = run('{{bin/php}} {{release_or_current_path}}/spark');\npreg_match_all('/(\\d+\\.?)+/', $result, $matches);\nreturn $matches[0][0] ?? 5.5;\n```\n\n\n\n## Tasks\n\n### spark\\:cache\\:info {#spark-cache-info}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L94)\n\nShows file cache information in the current system.\n\nDiscover & Checks\n\n\n### spark\\:config\\:check {#spark-config-check}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L97)\n\nCheck your Config values.\n\n\n\n\n### spark\\:env {#spark-env}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L100)\n\nRetrieves the current environment, or set a new one.\n\n\n\n\n### spark\\:filter\\:check {#spark-filter-check}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L103)\n\nCheck filters for a route.\n\n\n\n\n### spark\\:lang\\:find {#spark-lang-find}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L106)\n\nFind and save available phrases to translate.\n\n\n\n\n### spark\\:namespaces {#spark-namespaces}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L109)\n\nVerifies your namespaces are setup correctly.\n\n\n\n\n### spark\\:phpini\\:check {#spark-phpini-check}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L112)\n\nCheck your php.ini values.\n\n\n\n\n### spark\\:routes {#spark-routes}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L115)\n\nDisplays all routes.\n\n\n\n\n### spark\\:key\\:generate {#spark-key-generate}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L123)\n\nGenerates a new encryption key and writes it in an `.env` file.\n\nActions\n\n\n### spark\\:optimize {#spark-optimize}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L126)\n\nOptimize for production.\n\n\n\n\n### spark\\:publish {#spark-publish}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L129)\n\nDiscovers and executes all predefined Publisher classes.\n\n\n\n\n### spark\\:db\\:create {#spark-db-create}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L137)\n\nCreate a new database schema.\n\nDatabase and migrations.\n\n\n### spark\\:db\\:seed {#spark-db-seed}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L140)\n\nRuns the specified seeder to populate known data into the database.\n\n\n\n\n### spark\\:db\\:table {#spark-db-table}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L143)\n\nRetrieves information on the selected table.\n\n\n\n\n### spark\\:migrate {#spark-migrate}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L146)\n\nLocates and runs all new migrations against the database.\n\n\n\n\n### spark\\:migrate\\:refresh {#spark-migrate-refresh}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L149)\n\nDoes a rollback followed by a latest to refresh the current state of the database.\n\n\n\n\n### spark\\:migrate\\:rollback {#spark-migrate-rollback}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L152)\n\nRuns the \"down\" method for all migrations in the last batch.\n\n\n\n\n### spark\\:migrate\\:status {#spark-migrate-status}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L155)\n\nDisplays a list of all migrations and whether they\\'ve been run or not.\n\n\n\n\n### spark\\:cache\\:clear {#spark-cache-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L163)\n\nClears the current system caches.\n\nHousekeeping\n\n\n### spark\\:debugbar\\:clear {#spark-debugbar-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L166)\n\nClears all debugbar JSON files.\n\n\n\n\n### spark\\:logs\\:clear {#spark-logs-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L169)\n\nClears all log files.\n\n\n\n\n### spark\\:custom {#spark-custom}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L176)\n\nRun a custom spark command.\n\nCustom Spark Command for shield or setting packages\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L184)\n\nDeploys your project.\n\nMain deploy task.\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [spark:optimize](/docs/recipe/codeigniter4.md#spark-optimize)\n* [spark:migrate](/docs/recipe/codeigniter4.md#spark-migrate)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/common.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/common.php -->\n<!-- Then run bin/docgen -->\n\n# Common Recipe\n\n```php\nrequire 'recipe/common.php';\n```\n\n[Source](/recipe/common.php)\n\n* Requires\n  * [provision](/docs/recipe/provision.md)\n  * [check_remote](/docs/recipe/deploy/check_remote.md)\n  * [cleanup](/docs/recipe/deploy/cleanup.md)\n  * [clear_paths](/docs/recipe/deploy/clear_paths.md)\n  * [copy_dirs](/docs/recipe/deploy/copy_dirs.md)\n  * [env](/docs/recipe/deploy/env.md)\n  * [info](/docs/recipe/deploy/info.md)\n  * [lock](/docs/recipe/deploy/lock.md)\n  * [push](/docs/recipe/deploy/push.md)\n  * [release](/docs/recipe/deploy/release.md)\n  * [rollback](/docs/recipe/deploy/rollback.md)\n  * [setup](/docs/recipe/deploy/setup.md)\n  * [shared](/docs/recipe/deploy/shared.md)\n  * [symlink](/docs/recipe/deploy/symlink.md)\n  * [update_code](/docs/recipe/deploy/update_code.md)\n  * [vendors](/docs/recipe/deploy/vendors.md)\n  * [writable](/docs/recipe/deploy/writable.md)\n\n## Configuration\n### user\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L31)\n\nName of current user who is running deploy.\nIf not set will try automatically get git user name,\notherwise output of `whoami` command.\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### keep_releases\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L54)\n\nNumber of releases to preserve in releases folder.\n\n```php title=\"Default value\"\n10\n```\n\n\n### repository\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L57)\n\nRepository to deploy.\n\n\n\n### default_timeout\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L62)\n\nDefault timeout for `run()` and `runLocally()` functions.\n\nSet to `null` to disable timeout.\n\n```php title=\"Default value\"\n300\n```\n\n\n### env\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L78)\n\nRemote environment variables.\n```php\nset('env', [\n    'KEY' => 'something',\n]);\n```\n\nIt is possible to override it per `run()` call.\n\n```php\nrun('echo $KEY', env: ['KEY' => 'over']);\n```\n\n\n\n### dotenv\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L87)\n\nPath to `.env` file which will be used as environment variables for each command per `run()`.\n\n```php\nset('dotenv', '{{release_or_current_path}}/.env');\n```\n\n```php title=\"Default value\"\nfalse\n```\n\n\n### deploy_path\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L97)\n\nThe deploy path.\n\nFor example can be set for a bunch of host once as:\n```php\nset('deploy_path', '~/{{alias}}');\n```\n:::info Required\nThrows exception if not set.\n:::\n\n\n\n\n### current_path\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L107)\n\nReturn current release path. Default to [deploy_path](/docs/recipe/common.md#deploy_path)/`current`.\n```php\nset('current_path', '/var/public_html');\n```\n\n```php title=\"Default value\"\n'{{deploy_path}}/current'\n```\n\n\n### bin/php\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L110)\n\nPath to the `php` bin.\n\n```php title=\"Default value\"\nif (currentHost()->hasOwn('php_version')) {\nreturn '/usr/bin/php{{php_version}}';\n}\nreturn which('php');\n```\n\n\n### bin/git\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L118)\n\nPath to the `git` bin.\n\n```php title=\"Default value\"\nreturn which('git');\n```\n\n\n### use_relative_symlink\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L124)\n\nShould [bin/symlink](/docs/recipe/common.md#bin/symlink) use `--relative` option or not. Will detect\nautomatically.\n\n```php title=\"Default value\"\nreturn commandSupportsOption('ln', '--relative');\n```\n\n\n### bin/symlink\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L129)\n\nPath to the `ln` bin. With predefined options `-nfs`.\n\n```php title=\"Default value\"\nreturn get('use_relative_symlink') ? 'ln -nfs --relative' : 'ln -nfs';\n```\n\n\n### sudo_askpass\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L136)\n\nPath to a file which will store temp script with sudo password.\nDefaults to `.dep/sudo_pass`. This script is only temporary and will be deleted after\nsudo command executed.\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n\n## Tasks\n\n### deploy\\:prepare {#deploy-prepare}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L145)\n\nPrepares a new release.\n\n\n\n\nThis task is group task which contains next tasks:\n* [deploy:info](/docs/recipe/deploy/info.md#deploy-info)\n* [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup)\n* [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock)\n* [deploy:release](/docs/recipe/deploy/release.md#deploy-release)\n* [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code)\n* [deploy:env](/docs/recipe/deploy/env.md#deploy-env)\n* [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared)\n* [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable)\n\n\n### deploy\\:publish {#deploy-publish}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L157)\n\nPublishes the release.\n\n\n\n\nThis task is group task which contains next tasks:\n* [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink)\n* [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock)\n* [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup)\n* [deploy:success](/docs/recipe/common.md#deploy-success)\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L165)\n\nDeploys your project.\n\n\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n### deploy\\:success {#deploy-success}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L174)\n\nDeploys your project.\n\nPrints success message\n\n\n### deploy\\:failed {#deploy-failed}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L183)\n\n\n\nHook on deploy failure.\n\n\n### logs\\:app {#logs-app}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L192)\n\nShows application logs.\n\nFollows latest application logs.\n\n\n"
  },
  {
    "path": "docs/recipe/composer.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/composer.php -->\n<!-- Then run bin/docgen -->\n\n# Composer Recipe\n\n```php\nrequire 'recipe/composer.php';\n```\n\n[Source](/recipe/composer.php)\n\n* Requires\n  * [common](/docs/recipe/common.md)\n\n\n## Tasks\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/composer.php#L10)\n\nDeploys your project.\n\n\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/contao.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/contao.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Contao Project\n\n```php\nrequire 'recipe/contao.php';\n```\n\n[Source](/recipe/contao.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Contao application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Contao** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [contao:maintenance:enable](/docs/recipe/contao.md#contao-maintenance-enable) – Enable maintenance mode\n* [contao:migrate](/docs/recipe/contao.md#contao-migrate) – Run Contao migrations\n* [contao:maintenance:disable](/docs/recipe/contao.md#contao-maintenance-disable) – Disable maintenance mode\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe contao recipe is based on the [symfony](/docs/recipe/symfony.md) recipe.\n\n## Configuration\n### public_path\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L12)\n\nOverrides [public_path](/docs/recipe/provision/website.md#public_path) from `recipe/provision/website.php`.\n\nThe public path is the path to be set as DocumentRoot and is defined in the `composer.json` of the project\nbut defaults to `public` from Contao 5.0 on.\nThis path is relative from the [current_path](/docs/recipe/common.md#current_path), see [`recipe/provision/website.php`](/docs/recipe/provision/website.php#public_path).\n\n```php title=\"Default value\"\n$composerConfig = json_decode(file_get_contents('./composer.json'), true, 512, JSON_THROW_ON_ERROR);\n\nreturn $composerConfig['extra']['public-dir'] ?? 'public';\n```\n\n\n### bin/console\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L30)\n\nOverrides [bin/console](/docs/recipe/symfony.md#bin/console) from `recipe/symfony.php`.\n\n\n\n```php title=\"Default value\"\nreturn '{{bin/php}} {{release_or_current_path}}/vendor/bin/contao-console';\n```\n\n\n### contao_version\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L34)\n\n\n\n```php title=\"Default value\"\n$result = run('{{bin/console}} --version');\npreg_match_all('/(\\d+\\.?)+/', $result, $matches);\nreturn $matches[0][0] ?? 'n/a';\n```\n\n\n### symfony_version\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L40)\n\nOverrides [symfony_version](/docs/recipe/symfony.md#symfony_version) from `recipe/symfony.php`.\n\n\n\n```php title=\"Default value\"\n$result = run('{{bin/console}} about');\npreg_match_all('/(\\d+\\.?)+/', $result, $matches);\nreturn $matches[0][0] ?? 5.0;\n```\n\n\n\n## Tasks\n\n### contao\\:migrate {#contao-migrate}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L56)\n\nRun Contao migrations.\n\nThis task updates the database. A database backup is saved automatically as a default.\n\nTo automatically drop the obsolete database structures, you can override the task as follows:\n\n```php\ntask('contao:migrate', function () {\n    run('{{bin/php}} {{bin/console}} contao:migrate --with-deletes {{console_options}}');\n});\n```\n\n\n### contao\\:manager\\:download {#contao-manager-download}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L62)\n\nDownload the Contao Manager.\n\nDownloads the `contao-manager.phar.php` into the public path.\n\n\n### contao\\:install\\:lock {#contao-install-lock}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L68)\n\nLock the Contao Install Tool.\n\nLocks the Contao install tool which is useful if you don't use it.\n\n\n### contao\\:manager\\:lock {#contao-manager-lock}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L74)\n\nLock the Contao Manager.\n\nLocks the Contao Manager which is useful if you only need the API of the Manager rather than the UI.\n\n\n### contao\\:maintenance\\:enable {#contao-maintenance-enable}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L80)\n\nEnable maintenance mode.\n\n\n\n\n### contao\\:maintenance\\:disable {#contao-maintenance-disable}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L95)\n\nDisable maintenance mode.\n\n\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L107)\n\nDeploy the project.\n\n\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [contao:maintenance:enable](/docs/recipe/contao.md#contao-maintenance-enable)\n* [contao:migrate](/docs/recipe/contao.md#contao-migrate)\n* [contao:maintenance:disable](/docs/recipe/contao.md#contao-maintenance-disable)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/craftcms.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/craftcms.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Craftcms Project\n\n```php\nrequire 'recipe/craftcms.php';\n```\n\n[Source](/recipe/craftcms.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Craftcms application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Craftcms** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n\n\nThe craftcms recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### log_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/craftcms.php#L9)\n\n\n\n```php title=\"Default value\"\n'storage/logs/*.log'\n```\n\n\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/craftcms.php#L11)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n[\n    'storage',\n    'web/assets',\n]\n```\n\n\n### shared_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/craftcms.php#L16)\n\nOverrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n['.env']\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/craftcms.php#L18)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\n\n\n```php title=\"Default value\"\n[\n    'config/project',\n    'storage',\n    'web/assets',\n    'web/cpresources',\n]\n```\n\n\n\n## Tasks\n\n### craft\\:gc {#craft-gc}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/craftcms.php#L120)\n\nRuns garbage collection.\n\nGarbage collection\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/craftcms.php#L127)\n\nDeploys Craft CMS.\n\nMain deploy\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/check_remote.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/check_remote.php -->\n<!-- Then run bin/docgen -->\n\n# Check Remote Recipe\n\n```php\nrequire 'recipe/deploy/check_remote.php';\n```\n\n[Source](/recipe/deploy/check_remote.php)\n\n\n\n## Tasks\n\n### deploy\\:check_remote {#deploy-check_remote}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/check_remote.php#L11)\n\nChecks remote head.\n\nCancel deployment if there would be no change to the codebase.\nThis avoids unnecessary releases if the latest commit has already been deployed.\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/cleanup.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/cleanup.php -->\n<!-- Then run bin/docgen -->\n\n# Cleanup Recipe\n\n```php\nrequire 'recipe/deploy/cleanup.php';\n```\n\n[Source](/recipe/deploy/cleanup.php)\n\n\n## Configuration\n### cleanup_use_sudo\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/cleanup.php#L6)\n\nUse sudo in deploy:cleanup task for rm command.\n\n```php title=\"Default value\"\nfalse\n```\n\n\n\n## Tasks\n\n### deploy\\:cleanup {#deploy-cleanup}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/cleanup.php#L9)\n\nCleanup old releases.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/clear_paths.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/clear_paths.php -->\n<!-- Then run bin/docgen -->\n\n# Clear Paths Recipe\n\n```php\nrequire 'recipe/deploy/clear_paths.php';\n```\n\n[Source](/recipe/deploy/clear_paths.php)\n\n\n## Configuration\n### clear_paths\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/clear_paths.php#L6)\n\nList of paths to remove from [release_path](/docs/recipe/deploy/release.md#release_path).\n\n\n\n### clear_use_sudo\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/clear_paths.php#L9)\n\nUse sudo for deploy:clear_path task?\n\n```php title=\"Default value\"\nfalse\n```\n\n\n\n## Tasks\n\n### deploy\\:clear_paths {#deploy-clear_paths}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/clear_paths.php#L12)\n\nCleanup files and/or directories.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/copy_dirs.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/copy_dirs.php -->\n<!-- Then run bin/docgen -->\n\n# Copy Dirs Recipe\n\n```php\nrequire 'recipe/deploy/copy_dirs.php';\n```\n\n[Source](/recipe/deploy/copy_dirs.php)\n\n\n## Configuration\n### copy_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/copy_dirs.php#L7)\n\nList of dirs to copy between releases.\nFor example you can copy `node_modules` to speedup npm install.\n\n\n\n\n## Tasks\n\n### deploy\\:copy_dirs {#deploy-copy_dirs}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/copy_dirs.php#L10)\n\nCopies directories.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/env.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/env.php -->\n<!-- Then run bin/docgen -->\n\n# Env Recipe\n\n```php\nrequire 'recipe/deploy/env.php';\n```\n\n[Source](/recipe/deploy/env.php)\n\n\n## Configuration\n### dotenv_example\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/env.php#L5)\n\n\n\n```php title=\"Default value\"\n'.env.example'\n```\n\n\n\n## Tasks\n\n### deploy\\:env {#deploy-env}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/env.php#L8)\n\nConfigure .env file.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/info.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/info.php -->\n<!-- Then run bin/docgen -->\n\n# Info Recipe\n\n```php\nrequire 'recipe/deploy/info.php';\n```\n\n[Source](/recipe/deploy/info.php)\n\n\n## Configuration\n### what\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/info.php#L9)\n\nDefines \"what\" text for the 'deploy:info' task.\nUses one of the following sources:\n1. Repository name\n2. Application name\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### where\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/info.php#L25)\n\nDefines \"where\" text for the 'deploy:info' task.\nUses one of the following sources:\n1. Host's stage label\n2. Host's alias\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n\n## Tasks\n\n### deploy\\:info {#deploy-info}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/info.php#L34)\n\nDisplays info about deployment.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/lock.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/lock.php -->\n<!-- Then run bin/docgen -->\n\n# Lock Recipe\n\n```php\nrequire 'recipe/deploy/lock.php';\n```\n\n[Source](/recipe/deploy/lock.php)\n\n\n\n## Tasks\n\n### deploy\\:lock {#deploy-lock}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/lock.php#L8)\n\nLocks deploy.\n\n\n\n\n### deploy\\:unlock {#deploy-unlock}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/lock.php#L21)\n\nUnlocks deploy.\n\n\n\n\n### deploy\\:is_locked {#deploy-is_locked}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/lock.php#L26)\n\nChecks if deploy is locked.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/push.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/push.php -->\n<!-- Then run bin/docgen -->\n\n# Push Recipe\n\n```php\nrequire 'recipe/deploy/push.php';\n```\n\n[Source](/recipe/deploy/push.php)\n\n\n\n## Tasks\n\n### push {#push}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/push.php#L9)\n\nPushes local changes to remote host.\n\nCreates patch of local changes and pushes them on host.\nAnd applies to current_path. Push can be done many times.\nThe task purpose to be used only for development.\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/release.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/release.php -->\n<!-- Then run bin/docgen -->\n\n# Release Recipe\n\n```php\nrequire 'recipe/deploy/release.php';\n```\n\n[Source](/recipe/deploy/release.php)\n\n\n## Configuration\n### release_name\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L11)\n\nThe name of the release.\n\n```php title=\"Default value\"\nreturn within('{{deploy_path}}', function () {\n$latest = run('cat .dep/latest_release || echo 0');\nreturn strval(intval($latest) + 1);\n});\n```\n\n\n### releases_log\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L19)\n\nHolds releases log from `.dep/releases_log` file.\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### releases_list\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L34)\n\nReturn list of release names on host.\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### release_path\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L61)\n\nReturn release path.\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### release_revision\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L72)\n\nCurrent release revision. Usually a git hash.\n\n```php title=\"Default value\"\nreturn run('cat {{release_path}}/REVISION');\n```\n\n\n### release_or_current_path\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L78)\n\nReturn the release path during a deployment\nbut fallback to the current path otherwise.\n\n```php title=\"Default value\"\n$releaseExists = test('[ -h {{deploy_path}}/release ]');\nreturn $releaseExists ? get('release_path') : get('current_path');\n```\n\n\n\n## Tasks\n\n### deploy\\:release {#deploy-release}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L85)\n\nPrepares release.\n\nClean up unfinished releases and prepare next release\n\n\n### releases {#releases}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L160)\n\nShows releases list.\n\nExample output:\n```\n+---------------------+------example.org ------------+--------+-----------+\n| Date (UTC)          | Release     | Author         | Target | Commit    |\n+---------------------+-------------+----------------+--------+-----------+\n| 2021-11-06 20:51:45 | 1           | Anton Medvedev | HEAD   | 34d24192e |\n| 2021-11-06 21:00:50 | 2 (bad)     | Anton Medvedev | HEAD   | 392948a40 |\n| 2021-11-06 23:19:20 | 3           | Anton Medvedev | HEAD   | a4057a36c |\n| 2021-11-06 23:24:30 | 4 (current) | Anton Medvedev | HEAD   | s3wa45ca6 |\n+---------------------+-------------+----------------+--------+-----------+\n```\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/rollback.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/rollback.php -->\n<!-- Then run bin/docgen -->\n\n# Rollback Recipe\n\n```php\nrequire 'recipe/deploy/rollback.php';\n```\n\n[Source](/recipe/deploy/rollback.php)\n\n\n## Configuration\n### rollback_candidate\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/rollback.php#L20)\n\nRollback candidate will be automatically chosen by looking\nat output of `ls` command and content of `.dep/releases_log`.\n\nIf rollback candidate is marked as **BAD_RELEASE**, it will be skipped.\n\n:::tip\nYou can override rollback candidate via:\n```\ndep rollback -o rollback_candidate=123\n```\n:::\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n\n## Tasks\n\n### rollback {#rollback}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/rollback.php#L63)\n\nRollbacks to the previous release.\n\nUses [rollback_candidate](/docs/recipe/deploy/rollback.md#rollback_candidate) for symlinking. Current release will be marked as\nbad by creating file **BAD_RELEASE** with timestamp and [user](/docs/recipe/common.md#user).\n\n:::warning\nYou can always manually symlink [current_path](/docs/recipe/common.md#current_path) to proper release.\n```\ndep run '{{bin/symlink}} releases/123 {{current_path}}'\n```\n:::\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/setup.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/setup.php -->\n<!-- Then run bin/docgen -->\n\n# Setup Recipe\n\n```php\nrequire 'recipe/deploy/setup.php';\n```\n\n[Source](/recipe/deploy/setup.php)\n\n\n\n## Tasks\n\n### deploy\\:setup {#deploy-setup}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/setup.php#L6)\n\nPrepares host for deploy.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/shared.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/shared.php -->\n<!-- Then run bin/docgen -->\n\n# Shared Recipe\n\n```php\nrequire 'recipe/deploy/shared.php';\n```\n\n[Source](/recipe/deploy/shared.php)\n\n\n## Configuration\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/shared.php#L13)\n\nList of dirs what will be shared between releases.\nEach release will have symlink to those dirs stored in [deploy_path](/docs/recipe/common.md#deploy_path)/shared dir.\n```php\nset('shared_dirs', ['storage']);\n```\n\n\n\n### shared_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/shared.php#L20)\n\nList of files what will be shared between releases.\nEach release will have symlink to those files stored in [deploy_path](/docs/recipe/common.md#deploy_path)/shared dir.\n```php\nset('shared_files', ['.env']);\n```\n\n\n\n\n## Tasks\n\n### deploy\\:shared {#deploy-shared}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/shared.php#L23)\n\nCreates symlinks for shared files and dirs.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/symlink.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/symlink.php -->\n<!-- Then run bin/docgen -->\n\n# Symlink Recipe\n\n```php\nrequire 'recipe/deploy/symlink.php';\n```\n\n[Source](/recipe/deploy/symlink.php)\n\n\n## Configuration\n### use_atomic_symlink\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/symlink.php#L6)\n\nUse mv -T if available. Will check automatically.\n\n```php title=\"Default value\"\nreturn commandSupportsOption('mv', '--no-target-directory');\n```\n\n\n\n## Tasks\n\n### deploy\\:symlink {#deploy-symlink}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/symlink.php#L11)\n\nCreates symlink to release.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/update_code.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/update_code.php -->\n<!-- Then run bin/docgen -->\n\n# Update Code Recipe\n\n```php\nrequire 'recipe/deploy/update_code.php';\n```\n\n[Source](/recipe/deploy/update_code.php)\n\n\n## Configuration\n### branch\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/update_code.php#L12)\n\nDetermines which branch to deploy. Can be overridden with CLI option `--branch`.\nIf not specified, will get current git HEAD branch as default branch to deploy.\n\n```php title=\"Default value\"\n'HEAD'\n```\n\n\n### target\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/update_code.php#L19)\n\nThe deploy target: a branch, a tag or a revision.\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### update_code_strategy\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/update_code.php#L49)\n\nSets deploy:update_code strategy.\nCan be one of:\n- local_archive (copies the repository from local machine)\n- archive (default, fetches the code from the remote repository)\n- clone (if you need the origin repository `.git` dir in your [release_path](/docs/recipe/deploy/release.md#release_path), clones from remote repository)\n\n```php title=\"Default value\"\n'archive'\n```\n\n\n### git_ssh_command\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/update_code.php#L55)\n\nSets environment variable _GIT_SSH_COMMAND_ for `git clone` command.\nIf `StrictHostKeyChecking` flag is set to `accept-new` then ssh will\nautomatically add new host keys to the user known hosts files, but\nwill not permit connections to hosts with changed host keys.\n\n```php title=\"Default value\"\n'ssh -o StrictHostKeyChecking=accept-new'\n```\n\n\n### sub_directory\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/update_code.php#L67)\n\nSpecifies a sub directory within the repository to deploy.\nWorks only when [`update_code_strategy`](#update_code_strategy) is set to `archive` (default) or `local_archive`.\n\nExample:\n - set value to `src` if you want to deploy the folder that lives at `/src`.\n - set value to `src/api` if you want to deploy the folder that lives at `/src/api`.\n\nNote: do not use a leading `/`!\n\n```php title=\"Default value\"\nfalse\n```\n\n\n\n## Tasks\n\n### deploy\\:update_code {#deploy-update_code}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/update_code.php#L73)\n\nUpdates code.\n\nUpdate code at [release_path](/docs/recipe/deploy/release.md#release_path) on host.\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/vendors.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/vendors.php -->\n<!-- Then run bin/docgen -->\n\n# Vendors Recipe\n\n```php\nrequire 'recipe/deploy/vendors.php';\n```\n\n[Source](/recipe/deploy/vendors.php)\n\n\n## Configuration\n### composer_action\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/vendors.php#L5)\n\n\n\n```php title=\"Default value\"\n'install'\n```\n\n\n### composer_options\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/vendors.php#L6)\n\n\n\n```php title=\"Default value\"\n'--verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader'\n```\n\n\n### composer_version\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/vendors.php#L7)\n\n\n\n```php title=\"Default value\"\nnull\n```\n\n\n### bin/composer\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/vendors.php#L10)\n\nReturns Composer binary path if found. Otherwise, tries to install composer to `.dep/composer.phar`.\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n\n## Tasks\n\n### deploy\\:vendors {#deploy-vendors}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/vendors.php#L32)\n\nInstalls vendors.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/deploy/writable.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/deploy/writable.php -->\n<!-- Then run bin/docgen -->\n\n# Writable Recipe\n\n```php\nrequire 'recipe/deploy/writable.php';\n```\n\n[Source](/recipe/deploy/writable.php)\n\n\n## Configuration\n### http_user\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L9)\n\nUsed to make a writable directory by a server.\nUsed in `chown` and `acl` modes of [writable_mode](/docs/recipe/deploy/writable.md#writable_mode).\nAttempts automatically to detect http user in process list.\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### http_group\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L26)\n\nUsed to make a writable directory by a server.\nUsed in `chgrp` mode of [writable_mode](/docs/recipe/deploy/writable.md#writable_mode) only.\nAttempts automatically to detect http user in process list.\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L41)\n\nList of writable dirs.\n\n\n\n### writable_mode\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L50)\n\nOne of:\n- chown\n- chgrp\n- chmod\n- acl\n- sticky\n- skip\n\n```php title=\"Default value\"\n'acl'\n```\n\n\n### writable_use_sudo\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L53)\n\nUsing sudo in writable commands?\n\n```php title=\"Default value\"\nfalse\n```\n\n\n### writable_recursive\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L56)\n\nUse recursive mode (-R)?\n\n```php title=\"Default value\"\nfalse\n```\n\n\n### writable_chmod_mode\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L59)\n\nThe chmod mode.\n\n```php title=\"Default value\"\n'0755'\n```\n\n\n### writable_acl_groups\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L62)\n\nList of additional groups to give write permission to.\n\n\n\n### writable_acl_force\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L65)\n\nForce ACLs to be reapplied even if they already exist. Useful when recursive ACLs need to reach new nested paths but sudo isn't available. Slower, so enable only to fix writable dir permissions.\n\n```php title=\"Default value\"\nfalse\n```\n\n\n\n## Tasks\n\n### deploy\\:writable {#deploy-writable}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L68)\n\nMakes writable dirs.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/drupal7.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/drupal7.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Drupal 7 Project\n\n```php\nrequire 'recipe/drupal7.php';\n```\n\n[Source](/recipe/drupal7.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Drupal 7 application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Drupal 7** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe drupal7 recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### drupal_site\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal7.php#L15)\n\nSet Drupal 7 site. Change if you use different site\n\n```php title=\"Default value\"\n'default'\n```\n\n\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal7.php#L18)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\nDrupal 7 shared dirs\n\n```php title=\"Default value\"\n[\n    'sites/{{drupal_site}}/files',\n]\n```\n\n\n### shared_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal7.php#L23)\n\nOverrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`.\n\nDrupal 7 shared files\n\n```php title=\"Default value\"\n[\n    'sites/{{drupal_site}}/settings.php',\n]\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal7.php#L28)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\nDrupal 7 writable dirs\n\n```php title=\"Default value\"\n[\n    'sites/{{drupal_site}}/files',\n]\n```\n\n\n\n## Tasks\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal7.php#L9)\n\n\n\n\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n### drupal\\:settings {#drupal-settings}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal7.php#L34)\n\n\n\nCreate and upload Drupal 7 settings.php using values from secrets\n\n\n### drupal\\:upload_files {#drupal-upload_files}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal7.php#L76)\n\n\n\nUpload Drupal 7 files folder\n\n\n"
  },
  {
    "path": "docs/recipe/drupal8.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/drupal8.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Drupal 8 Project\n\n```php\nrequire 'recipe/drupal8.php';\n```\n\n[Source](/recipe/drupal8.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Drupal 8 application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Drupal 8** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe drupal8 recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### drupal_site\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal8.php#L15)\n\nSet drupal site. Change if you use different site\n\n```php title=\"Default value\"\n'default'\n```\n\n\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal8.php#L19)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\nDrupal 8 shared dirs\n\n```php title=\"Default value\"\n[\n    'sites/{{drupal_site}}/files',\n]\n```\n\n\n### shared_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal8.php#L24)\n\nOverrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`.\n\nDrupal 8 shared files\n\n```php title=\"Default value\"\n[\n    'sites/{{drupal_site}}/settings.php',\n    'sites/{{drupal_site}}/services.yml',\n]\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal8.php#L30)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\nDrupal 8 Writable dirs\n\n```php title=\"Default value\"\n[\n    'sites/{{drupal_site}}/files',\n]\n```\n\n\n\n## Tasks\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal8.php#L9)\n\n\n\n\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/flow_framework.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/flow_framework.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Flow Framework Project\n\n```php\nrequire 'recipe/flow_framework.php';\n```\n\n[Source](/recipe/flow_framework.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Flow Framework application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Flow Framework** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [deploy:run_migrations](/docs/recipe/flow_framework.md#deploy-run_migrations) – Applies database migrations\n* [deploy:publish_resources](/docs/recipe/flow_framework.md#deploy-publish_resources) – Publishes resources\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe flow_framework recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### flow_context\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/flow_framework.php#L10)\n\nFlow-Framework application-context\n\n```php title=\"Default value\"\n'Production'\n```\n\n\n### flow_command\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/flow_framework.php#L13)\n\nFlow-Framework cli-command\n\n```php title=\"Default value\"\n'flow'\n```\n\n\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/flow_framework.php#L16)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\nFlow-Framework shared directories\n\n```php title=\"Default value\"\n[\n    'Data/Persistent',\n    'Data/Logs',\n    'Configuration/{{flow_context}}',\n]\n```\n\n\n\n## Tasks\n\n### deploy\\:run_migrations {#deploy-run_migrations}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/flow_framework.php#L26)\n\nApplies database migrations.\n\nApply database migrations\n\n\n### deploy\\:publish_resources {#deploy-publish_resources}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/flow_framework.php#L34)\n\nPublishes resources.\n\nPublish resources\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/flow_framework.php#L42)\n\nDeploys your project.\n\nMain task\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [deploy:run_migrations](/docs/recipe/flow_framework.md#deploy-run_migrations)\n* [deploy:publish_resources](/docs/recipe/flow_framework.md#deploy-publish_resources)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/fuelphp.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/fuelphp.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Fuelphp Project\n\n```php\nrequire 'recipe/fuelphp.php';\n```\n\n[Source](/recipe/fuelphp.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Fuelphp application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Fuelphp** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe fuelphp recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/fuelphp.php#L10)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\nFuelPHP 1.x shared dirs\n\n```php title=\"Default value\"\n[\n    'fuel/app/cache', 'fuel/app/logs',\n]\n```\n\n\n\n## Tasks\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/fuelphp.php#L18)\n\nDeploys your project.\n\nMain task\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/joomla.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/joomla.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Joomla Project\n\n```php\nrequire 'recipe/joomla.php';\n```\n\n[Source](/recipe/joomla.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Joomla application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Joomla** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe joomla recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### shared_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/joomla.php#L9)\n\nOverrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n['configuration.php']\n```\n\n\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/joomla.php#L10)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n['images']\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/joomla.php#L11)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\n\n\n```php title=\"Default value\"\n['images']\n```\n\n\n\n## Tasks\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/joomla.php#L14)\n\nDeploys your project.\n\n\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/laravel.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/laravel.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Laravel Project\n\n```php\nrequire 'recipe/laravel.php';\n```\n\n[Source](/recipe/laravel.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Laravel application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Laravel** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [artisan:storage:link](/docs/recipe/laravel.md#artisan-storage-link) – Creates the symbolic links configured for the application\n* [artisan:optimize](/docs/recipe/laravel.md#artisan-optimize) – Cache the framework bootstrap files\n* [artisan:migrate](/docs/recipe/laravel.md#artisan-migrate) – Runs the database migrations\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n* [artisan:reload](/docs/recipe/laravel.md#artisan-reload) – Reload running services\n\n\nThe laravel recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L9)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n['storage']\n```\n\n\n### shared_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L10)\n\nOverrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n['.env']\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L11)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\n\n\n```php title=\"Default value\"\n[\n    'bootstrap/cache',\n    'storage',\n]\n```\n\n\n### writable_recursive\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L15)\n\nOverrides [writable_recursive](/docs/recipe/deploy/writable.md#writable_recursive) from `recipe/deploy/writable.php`.\n\n\n\n```php title=\"Default value\"\ntrue\n```\n\n\n### log_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L16)\n\n\n\n```php title=\"Default value\"\n'storage/logs/*.log'\n```\n\n\n### bin/artisan\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L17)\n\n\n\n```php title=\"Default value\"\n'{{release_or_current_path}}/artisan'\n```\n\n\n### laravel_version\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L18)\n\n\n\n```php title=\"Default value\"\n$result = run(\"{{bin/php}} {{bin/artisan}} --version\");\npreg_match_all('/(\\d+\\.?)+/', $result, $matches);\nreturn $matches[0][0] ?? 5.5;\n```\n\n\n### public_path\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L23)\n\nOverrides [public_path](/docs/recipe/provision/website.md#public_path) from `recipe/provision/website.php`.\n\n\n\n```php title=\"Default value\"\n'public'\n```\n\n\n\n## Tasks\n\n### artisan\\:down {#artisan-down}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L87)\n\nPuts the application into maintenance / demo mode.\n\nMaintenance mode.\n\n\n### artisan\\:up {#artisan-up}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L90)\n\nBrings the application out of maintenance mode.\n\n\n\n\n### artisan\\:key\\:generate {#artisan-key-generate}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L97)\n\nSets the application key.\n\nGenerate keys.\n\n\n### artisan\\:passport\\:keys {#artisan-passport-keys}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L100)\n\nCreates the encryption keys for API authentication.\n\n\n\n\n### artisan\\:db\\:seed {#artisan-db-seed}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L107)\n\nSeeds the database with records.\n\nDatabase and migrations.\n\n\n### artisan\\:migrate {#artisan-migrate}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L110)\n\nRuns the database migrations.\n\n\n\n\n### artisan\\:migrate\\:fresh {#artisan-migrate-fresh}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L113)\n\nDrops all tables and re-run all migrations.\n\n\n\n\n### artisan\\:migrate\\:rollback {#artisan-migrate-rollback}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L116)\n\nRollbacks the last database migration.\n\n\n\n\n### artisan\\:migrate\\:status {#artisan-migrate-status}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L119)\n\nShows the status of each migration.\n\n\n\n\n### artisan\\:cache\\:clear {#artisan-cache-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L126)\n\nFlushes the application cache.\n\nCache and optimizations.\n\n\n### artisan\\:config\\:cache {#artisan-config-cache}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L129)\n\nCreates a cache file for faster configuration loading.\n\n\n\n\n### artisan\\:config\\:clear {#artisan-config-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L132)\n\nRemoves the configuration cache file.\n\n\n\n\n### artisan\\:event\\:cache {#artisan-event-cache}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L135)\n\nDiscovers and cache the application\\'s events and listeners.\n\n\n\n\n### artisan\\:event\\:clear {#artisan-event-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L138)\n\nClears all cached events and listeners.\n\n\n\n\n### artisan\\:event\\:list {#artisan-event-list}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L141)\n\nLists the application\\'s events and listeners.\n\n\n\n\n### artisan\\:optimize {#artisan-optimize}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L144)\n\nCache the framework bootstrap files.\n\n\n\n\n### artisan\\:optimize\\:clear {#artisan-optimize-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L147)\n\nRemoves the cached bootstrap files.\n\n\n\n\n### artisan\\:reload {#artisan-reload}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L150)\n\nReload running services.\n\n\n\n\n### artisan\\:route\\:cache {#artisan-route-cache}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L153)\n\nCreates a route cache file for faster route registration.\n\n\n\n\n### artisan\\:route\\:clear {#artisan-route-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L156)\n\nRemoves the route cache file.\n\n\n\n\n### artisan\\:route\\:list {#artisan-route-list}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L159)\n\nLists all registered routes.\n\n\n\n\n### artisan\\:storage\\:link {#artisan-storage-link}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L162)\n\nCreates the symbolic links configured for the application.\n\n\n\n\n### artisan\\:view\\:cache {#artisan-view-cache}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L165)\n\nCompiles all of the application\\'s Blade templates.\n\n\n\n\n### artisan\\:view\\:clear {#artisan-view-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L168)\n\nClears all compiled view files.\n\n\n\n\n### artisan\\:queue\\:failed {#artisan-queue-failed}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L175)\n\nLists all of the failed queue jobs.\n\nQueue and Horizon.\n\n\n### artisan\\:queue\\:flush {#artisan-queue-flush}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L178)\n\nFlushes all of the failed queue jobs.\n\n\n\n\n### artisan\\:queue\\:restart {#artisan-queue-restart}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L181)\n\nRestarts queue worker daemons after their current job.\n\n\n\n\n### artisan\\:horizon {#artisan-horizon}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L184)\n\nStarts a master supervisor in the foreground.\n\n\n\n\n### artisan\\:horizon\\:clear {#artisan-horizon-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L187)\n\nDeletes all of the jobs from the specified queue.\n\n\n\n\n### artisan\\:horizon\\:continue {#artisan-horizon-continue}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L190)\n\nInstructs the master supervisor to continue processing jobs.\n\n\n\n\n### artisan\\:horizon\\:list {#artisan-horizon-list}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L193)\n\nLists all of the deployed machines.\n\n\n\n\n### artisan\\:horizon\\:pause {#artisan-horizon-pause}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L196)\n\nPauses the master supervisor.\n\n\n\n\n### artisan\\:horizon\\:purge {#artisan-horizon-purge}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L199)\n\nTerminates any rogue Horizon processes.\n\n\n\n\n### artisan\\:horizon\\:status {#artisan-horizon-status}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L202)\n\nGets the current status of Horizon.\n\n\n\n\n### artisan\\:horizon\\:terminate {#artisan-horizon-terminate}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L205)\n\nTerminates the master supervisor so it can be restarted.\n\n\n\n\n### artisan\\:horizon\\:publish {#artisan-horizon-publish}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L208)\n\nPublish all of the Horizon resources.\n\n\n\n\n### artisan\\:horizon\\:supervisors {#artisan-horizon-supervisors}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L211)\n\nLists all of the supervisors.\n\n\n\n\n### artisan\\:horizon\\:clear-metrics {#artisan-horizon-clear-metrics}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L214)\n\nDeletes metrics for all jobs and queues.\n\n\n\n\n### artisan\\:horizon\\:snapshot {#artisan-horizon-snapshot}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L217)\n\nStores a snapshot of the queue metrics.\n\n\n\n\n### artisan\\:schedule\\:interrupt {#artisan-schedule-interrupt}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L224)\n\nInterrupt in-progress schedule:run invocations.\n\nScheduler.\n\n\n### artisan\\:telescope\\:clear {#artisan-telescope-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L231)\n\nClears all entries from Telescope.\n\nTelescope.\n\n\n### artisan\\:telescope\\:prune {#artisan-telescope-prune}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L234)\n\nPrunes stale entries from the Telescope database.\n\n\n\n\n### artisan\\:octane {#artisan-octane}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L241)\n\nStarts the octane server.\n\nOctane.\n\n\n### artisan\\:octane\\:reload {#artisan-octane-reload}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L244)\n\nReloads the octane server.\n\n\n\n\n### artisan\\:octane\\:stop {#artisan-octane-stop}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L247)\n\nStops the octane server.\n\n\n\n\n### artisan\\:octane\\:status {#artisan-octane-status}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L250)\n\nCheck the status of the octane server.\n\n\n\n\n### artisan\\:nova\\:publish {#artisan-nova-publish}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L257)\n\nPublish all of the Laravel Nova resources.\n\nNova.\n\n\n### artisan\\:reverb\\:start {#artisan-reverb-start}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L264)\n\nStarts the Reverb server.\n\nReverb.\n\n\n### artisan\\:reverb\\:restart {#artisan-reverb-restart}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L267)\n\nRestarts the Reverb server.\n\n\n\n\n### artisan\\:pulse\\:check {#artisan-pulse-check}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L274)\n\nStarts the Pulse server.\n\nPulse.\n\n\n### artisan\\:pulse\\:restart {#artisan-pulse-restart}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L277)\n\nRestarts the Pulse server.\n\n\n\n\n### artisan\\:pulse\\:purge {#artisan-pulse-purge}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L280)\n\nPurges all Pulse data from storage.\n\n\n\n\n### artisan\\:pulse\\:work {#artisan-pulse-work}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L283)\n\nProcess incoming Pulse data from the ingest stream.\n\n\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L289)\n\nDeploys your project.\n\nMain deploy task.\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [artisan:storage:link](/docs/recipe/laravel.md#artisan-storage-link)\n* [artisan:optimize](/docs/recipe/laravel.md#artisan-optimize)\n* [artisan:migrate](/docs/recipe/laravel.md#artisan-migrate)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n* [artisan:reload](/docs/recipe/laravel.md#artisan-reload)\n\n\n"
  },
  {
    "path": "docs/recipe/magento.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/magento.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Magento Project\n\n```php\nrequire 'recipe/magento.php';\n```\n\n[Source](/recipe/magento.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Magento application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Magento** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:cache:clear](/docs/recipe/magento.md#deploy-cache-clear) – Clears cache\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe magento recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento.php#L14)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\nMagento Configuration\nMagento shared dirs\n\n```php title=\"Default value\"\n['var', 'media']\n```\n\n\n### shared_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento.php#L17)\n\nOverrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`.\n\nMagento shared files\n\n```php title=\"Default value\"\n['app/etc/local.xml']\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento.php#L20)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\nMagento writable dirs\n\n```php title=\"Default value\"\n['var', 'media']\n```\n\n\n\n## Tasks\n\n### deploy\\:cache\\:clear {#deploy-cache-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento.php#L26)\n\nClears cache.\n\nClear cache\n\n\n### deploy\\:clear_version {#deploy-clear_version}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento.php#L33)\n\n\n\nRemove files that can be used to compromise Magento\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento.php#L47)\n\nDeploys your project.\n\nMain task\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:cache:clear](/docs/recipe/magento.md#deploy-cache-clear)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/magento2.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/magento2.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Magento 2 Project\n\n```php\nrequire 'recipe/magento2.php';\n```\n\n[Source](/recipe/magento2.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Magento 2 application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Magento 2** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [deploy:clear_paths](/docs/recipe/deploy/clear_paths.md#deploy-clear_paths) – Cleanup files and/or directories\n* [deploy:magento](/docs/recipe/magento2.md#deploy-magento) – Magento2 deployment operations\n  * [magento:build](/docs/recipe/magento2.md#magento-build) – Magento2 build operations\n    * [magento:compile](/docs/recipe/magento2.md#magento-compile) – Compiles magento di\n    * [magento:deploy:assets](/docs/recipe/magento2.md#magento-deploy-assets) – Deploys assets\n  * [magento:maintenance:enable-if-needed](/docs/recipe/magento2.md#magento-maintenance-enable-if-needed) – Set maintenance mode if needed\n  * [magento:config:import](/docs/recipe/magento2.md#magento-config-import) – Config Import\n  * [magento:upgrade](/docs/recipe/magento2.md#magento-upgrade) – Run upgrades if needed\n  * [magento:maintenance:disable](/docs/recipe/magento2.md#magento-maintenance-disable) – Disables maintenance mode\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nIn addition the **Magento 2** recipe contains an artifact deployment.\nThis is a two step process where you first execute\n\n```php\nbin/dep artifact:build [options] [localhost]\n```\n\nto build an artifact, which then is deployed on a server with\n\n```php\nbin/dep artifact:deploy [host]\n```\n\nThe `localhost` to build the artifact on has to be declared local, so either add\n```php\nlocalhost()\n    ->set('local', true);\n```\nto your deploy.php or\n```yaml\nhosts:\n    localhost:\n        local: true\n```\nto your deploy yaml.\n\nThe [artifact:build](#artifact:build) command of **Magento 2** consists of: * [build:prepare](/docs/recipe/magento2.md#build-prepare) – Prepare local artifact build\n* [build:remove-generated](/docs/recipe/magento2.md#build-remove-generated) – Clears generated files prior to building.\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [magento:compile](/docs/recipe/magento2.md#magento-compile) – Compiles magento di\n* [magento:deploy:assets](/docs/recipe/magento2.md#magento-deploy-assets) – Deploys assets\n* [artifact:package](/docs/recipe/magento2.md#artifact-package) – Packages all relevant files in an artifact.\n\n\n The [artifact:deploy](#artifact:deploy) command of **Magento 2** consists of:\n* [artifact:prepare](/docs/recipe/magento2.md#artifact-prepare) – Prepares an artifact on the target server\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [artifact:upload](/docs/recipe/magento2.md#artifact-upload) – Uploads artifact in release folder for extraction.\n  * [artifact:extract](/docs/recipe/magento2.md#artifact-extract) – Extracts artifact in release path.\n  * [deploy:additional-shared](/docs/recipe/magento2.md#deploy-additional-shared) – Adds additional files and dirs to the list of shared files and dirs\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [magento:maintenance:enable-if-needed](/docs/recipe/magento2.md#magento-maintenance-enable-if-needed) – Set maintenance mode if needed\n* [magento:config:import](/docs/recipe/magento2.md#magento-config-import) – Config Import\n* [magento:upgrade](/docs/recipe/magento2.md#magento-upgrade) – Run upgrades if needed\n* [magento:maintenance:disable](/docs/recipe/magento2.md#magento-maintenance-disable) – Disables maintenance mode\n* [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n* [artifact:finish](/docs/recipe/magento2.md#artifact-finish) – Executes the tasks after artifact is released\n  * [magento:cache:flush](/docs/recipe/magento2.md#magento-cache-flush) – Flushes Magento Cache\n  * [cachetool:clear:opcache](/docs/contrib/cachetool.md#cachetool-clear-opcache) – Clears OPcode cache\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe magento2 recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### static_content_locales\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L26)\n\nConfiguration\nBy default setup:static-content:deploy uses `en_US`.\nTo change that, simply put `set('static_content_locales', 'en_US de_DE');`\nin you deployer script.\n\n```php title=\"Default value\"\n'en_US'\n```\n\n\n### magento_themes\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L43)\n\nConfiguration\nYou can also set the themes to run against. By default it'll deploy\nall themes - `add('magento_themes', ['Magento/luma', 'Magento/backend']);`\nIf the themes are set as a simple list of strings, then all languages defined in [static_content_locales](/docs/recipe/magento2.md#static_content_locales) are\ncompiled for the given themes.\nAlternatively The themes can be defined as an associative array, where the key represents the theme name and\nthe key contains the languages for the compilation (for this specific theme)\nExample:\nset('magento_themes', ['Magento/luma']); - Will compile this theme with every language from [static_content_locales](/docs/recipe/magento2.md#static_content_locales)\nset('magento_themes', [\n    'Magento/luma'   => null,                              - Will compile all languages from [static_content_locales](/docs/recipe/magento2.md#static_content_locales) for Magento/luma\n    'Custom/theme'   => 'en_US fr_FR'                      - Will compile only en_US and fr_FR for Custom/theme\n    'Custom/another' => '[static_content_locales](/docs/recipe/magento2.md#static_content_locales) it_IT' - Will compile all languages from [static_content_locales](/docs/recipe/magento2.md#static_content_locales) + it_IT for Custom/another\n]); - Will compile this theme with every language\n\n```php title=\"Default value\"\n[\n\n]\n```\n\n\n### static_deploy_options\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L48)\n\nStatic content deployment options, e.g. '--no-parent'\n\n\n\n### split_static_deployment\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L51)\n\nDeploy frontend and adminhtml together as default\n\n```php title=\"Default value\"\nfalse\n```\n\n\n### static_content_locales_backend\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L54)\n\nUse the default languages for the backend as default\n\n```php title=\"Default value\"\n'{{static_content_locales}}'\n```\n\n\n### magento_themes_backend\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L58)\n\nbackend themes to deploy. Only used if split_static_deployment=true\nThis setting supports the same options/structure as [magento_themes](/docs/recipe/magento2.md#magento_themes)\n\n```php title=\"Default value\"\n['Magento/backend' => null]\n```\n\n\n### static_content_jobs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L64)\n\nConfiguration\nAlso set the number of concurrent jobs to run. The default is 1\nUpdate using: `set('static_content_jobs', '1');`\n\n```php title=\"Default value\"\n'1'\n```\n\n\n### content_version\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L66)\n\n\n\n```php title=\"Default value\"\nreturn time();\n```\n\n\n### magento_dir\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L71)\n\nMagento directory relative to repository root. Use \".\" (default) if it is not located in a subdirectory\n\n```php title=\"Default value\"\n'.'\n```\n\n\n### shared_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L74)\n\nOverrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n[\n    '{{magento_dir}}/app/etc/env.php',\n    '{{magento_dir}}/var/.maintenance.ip',\n]\n```\n\n\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L78)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n[\n    '{{magento_dir}}/var/composer_home',\n    '{{magento_dir}}/var/log',\n    '{{magento_dir}}/var/export',\n    '{{magento_dir}}/var/report',\n    '{{magento_dir}}/var/import',\n    '{{magento_dir}}/var/import_history',\n    '{{magento_dir}}/var/session',\n    '{{magento_dir}}/var/importexport',\n    '{{magento_dir}}/var/backups',\n    '{{magento_dir}}/var/tmp',\n    '{{magento_dir}}/pub/sitemap',\n    '{{magento_dir}}/pub/media',\n    '{{magento_dir}}/pub/static/_cache',\n]\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L93)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\n\n\n```php title=\"Default value\"\n[\n    '{{magento_dir}}/var',\n    '{{magento_dir}}/pub/static',\n    '{{magento_dir}}/pub/media',\n    '{{magento_dir}}/generated',\n    '{{magento_dir}}/var/page_cache',\n]\n```\n\n\n### clear_paths\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L100)\n\nOverrides [clear_paths](/docs/recipe/deploy/clear_paths.md#clear_paths) from `recipe/deploy/clear_paths.php`.\n\n\n\n```php title=\"Default value\"\n[\n    '{{magento_dir}}/generated/*',\n    '{{magento_dir}}/pub/static/_cache/*',\n    '{{magento_dir}}/var/generation/*',\n    '{{magento_dir}}/var/cache/*',\n    '{{magento_dir}}/var/page_cache/*',\n    '{{magento_dir}}/var/view_preprocessed/*',\n]\n```\n\n\n### bin/magento\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L109)\n\n\n\n```php title=\"Default value\"\n'{{release_or_current_path}}/{{magento_dir}}/bin/magento'\n```\n\n\n### magento_version\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L111)\n\n\n\n```php title=\"Default value\"\n// detect version\n$versionOutput = run('{{bin/php}} {{bin/magento}} --version');\npreg_match('/(\\d+\\.?)+(-p\\d+)?$/', $versionOutput, $matches);\nreturn $matches[0] ?? '2.0';\n```\n\n\n### config_import_needed\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L118)\n\n\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### database_upgrade_needed\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L132)\n\n\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### full_upgrade_needed\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L147)\n\n\n\n```php title=\"Default value\"\n//Some conditions, such as new RabittMQ services require a full upgrade and are not detecet by setup:db:status\n//TODO: Add checks, once implemented, for detecting necessary full upgrade process. See future RabbitMQ Check: https://github.com/magento/magento2/pull/39698\nreturn false;\n```\n\n\n### upgrade_needed\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L153)\n\n\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### enable_zerodowntime\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L163)\n\nDeploy without setting maintenance mode if possible\n\n```php title=\"Default value\"\ntrue\n```\n\n\n### artifact_file\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L378)\n\nArtifact deployment section\nThe file the artifact is saved to\n\n```php title=\"Default value\"\n'artifact.tar.gz'\n```\n\n\n### artifact_dir\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L381)\n\nThe directory the artifact is saved in\n\n```php title=\"Default value\"\n'artifacts'\n```\n\n\n### artifact_excludes_file\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L385)\n\nPoints to a file with a list of files to exclude from packaging.\nThe format is as with the `tar --exclude-from=[file]` option\n\n```php title=\"Default value\"\n'artifacts/excludes'\n```\n\n\n### build_from_repo\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L388)\n\nIf set to true, the artifact is built from a clean copy of the project repository instead of the current working directory\n\n```php title=\"Default value\"\nfalse\n```\n\n\n### repository\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L391)\n\nOverrides [repository](/docs/recipe/common.md#repository) from `recipe/common.php`.\n\nSet this value if \"build_from_repo\" is set to true. The target to deploy must also be set with \"--branch\", \"--tag\" or \"--revision\"\n\n```php title=\"Default value\"\nnull\n```\n\n\n### artifact_path\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L394)\n\nThe relative path to the artifact file. If the directory does not exist, it will be created\n\n```php title=\"Default value\"\nif (!testLocally('[ -d {{artifact_dir}} ]')) {\nrunLocally('mkdir -p {{artifact_dir}}');\n}\nreturn get('artifact_dir') . '/' . get('artifact_file');\n```\n\n\n### bin/tar\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L402)\n\nThe location of the tar command. On MacOS you should have installed gtar, as it supports the required settings\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### additional_shared_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L474)\n\nArray of shared files that will be added to the default shared_files without overriding\n\n\n\n### additional_shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L476)\n\nArray of shared directories that will be added to the default shared_dirs without overriding\n\n\n\n\n## Tasks\n\n### magento\\:compile {#magento-compile}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L173)\n\nCompiles magento di.\n\nTasks\nTo work correctly with artifact deployment, it is necessary to set the MAGE_MODE correctly in `app/etc/config.php`\ne.g.\n```php\n'MAGE_MODE' => 'production'\n```\n\n\n### magento\\:deploy\\:assets {#magento-deploy-assets}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L199)\n\nDeploys assets.\n\nTo work correctly with artifact deployment it is necessary to set `system/dev/js` , `system/dev/css` and `system/dev/template`\nin `app/etc/config.php`, e.g.:\n```php\n'system' => [\n    'default' => [\n        'dev' => [\n            'js' => [\n                'merge_files' => '1',\n                'minify_files' => '1'\n            ],\n            'css' => [\n                'merge_files' => '1',\n                'minify_files' => '1'\n            ],\n            'template' => [\n                'minify_html' => '1'\n            ]\n        ]\n    ]\n```\n\n\n### magento\\:deploy\\:assets\\:adminhtml {#magento-deploy-assets-adminhtml}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L216)\n\nDeploys assets for backend only.\n\n\n\n\n### magento\\:deploy\\:assets\\:frontend {#magento-deploy-assets-frontend}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L221)\n\nDeploys assets for frontend only.\n\n\n\n\n### magento\\:sync\\:content_version {#magento-sync-content_version}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L284)\n\nSyncs content version.\n\n\n\n\n### magento\\:maintenance\\:enable {#magento-maintenance-enable}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L294)\n\nEnables maintenance mode.\n\n\n\n\n### magento\\:maintenance\\:disable {#magento-maintenance-disable}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L300)\n\nDisables maintenance mode.\n\n\n\n\n### magento\\:maintenance\\:enable-if-needed {#magento-maintenance-enable-if-needed}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L306)\n\nSet maintenance mode if needed.\n\n\n\n\n### magento\\:config\\:import {#magento-config-import}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L313)\n\nConfig Import.\n\n\n\n\n### magento\\:upgrade\\:db {#magento-upgrade-db}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L322)\n\nUpgrades magento database.\n\n\n\n\n### magento\\:upgrade {#magento-upgrade}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L334)\n\nRun upgrades if needed.\n\n\n\n\n### magento\\:cache\\:flush {#magento-cache-flush}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L343)\n\nFlushes Magento Cache.\n\n\n\n\n### deploy\\:magento {#deploy-magento}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L348)\n\nMagento2 deployment operations.\n\n\n\n\nThis task is group task which contains next tasks:\n* [magento:build](/docs/recipe/magento2.md#magento-build)\n* [magento:maintenance:enable-if-needed](/docs/recipe/magento2.md#magento-maintenance-enable-if-needed)\n* [magento:config:import](/docs/recipe/magento2.md#magento-config-import)\n* [magento:upgrade](/docs/recipe/magento2.md#magento-upgrade)\n* [magento:maintenance:disable](/docs/recipe/magento2.md#magento-maintenance-disable)\n\n\n### magento\\:build {#magento-build}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L357)\n\nMagento2 build operations.\n\n\n\n\nThis task is group task which contains next tasks:\n* [magento:compile](/docs/recipe/magento2.md#magento-compile)\n* [magento:deploy:assets](/docs/recipe/magento2.md#magento-deploy-assets)\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L363)\n\nDeploys your project.\n\n\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [deploy:clear_paths](/docs/recipe/deploy/clear_paths.md#deploy-clear_paths)\n* [deploy:magento](/docs/recipe/magento2.md#deploy-magento)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n### artifact\\:package {#artifact-package}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L413)\n\nPackages all relevant files in an artifact.\n\ntasks section\n\n\n### artifact\\:upload {#artifact-upload}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L423)\n\nUploads artifact in release folder for extraction.\n\n\n\n\n### artifact\\:extract {#artifact-extract}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L428)\n\nExtracts artifact in release path.\n\n\n\n\n### build\\:remove-generated {#build-remove-generated}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L434)\n\nClears generated files prior to building.\n\n\n\n\n### build\\:prepare {#build-prepare}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L439)\n\nPrepare local artifact build.\n\n\n\n\n### artifact\\:build {#artifact-build}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L464)\n\nBuilds an artifact.\n\n\n\n\nThis task is group task which contains next tasks:\n* [build:prepare](/docs/recipe/magento2.md#build-prepare)\n* [build:remove-generated](/docs/recipe/magento2.md#build-remove-generated)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [magento:compile](/docs/recipe/magento2.md#magento-compile)\n* [magento:deploy:assets](/docs/recipe/magento2.md#magento-deploy-assets)\n* [artifact:package](/docs/recipe/magento2.md#artifact-package)\n\n\n### deploy\\:additional-shared {#deploy-additional-shared}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L480)\n\nAdds additional files and dirs to the list of shared files and dirs.\n\n\n\n\n### magento\\:set_cache_prefix {#magento-set_cache_prefix}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L495)\n\nUpdate cache id_prefix.\n\nUpdate cache id_prefix on deploy so that you are compiling against a fresh cache\nReference Issue: https://github.com/davidalger/capistrano-magento2/issues/151\nTo use this feature, add the following to your deployer scripts:\n```php\nafter('deploy:shared', 'magento:set_cache_prefix');\nafter('deploy:magento', 'magento:cleanup_cache_prefix');\n```\n\n\n### magento\\:cleanup_cache_prefix {#magento-cleanup_cache_prefix}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L535)\n\nCleanup cache id_prefix env files.\n\nAfter successful deployment, move the tmp_env.php file to env.php ready for next deployment\n\n\n### magento\\:cron\\:stop {#magento-cron-stop}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L551)\n\nRemove cron from crontab and kill running cron jobs.\n\nRemove cron from crontab and kill running cron jobs\nTo use this feature, add the following to your deployer scripts:\n ```php\n after('magento:maintenance:enable-if-needed', 'magento:cron:stop');\n ```\n\n\n### magento\\:cron\\:install {#magento-cron-install}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L567)\n\nInstall cron in crontab.\n\nInstall cron in crontab\nTo use this feature, add the following to your deployer scripts:\n  ```php\n  after('magento:upgrade', 'magento:cron:install');\n  ```\n\n\n### artifact\\:prepare {#artifact-prepare}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L573)\n\nPrepares an artifact on the target server.\n\n\n\n\nThis task is group task which contains next tasks:\n* [deploy:info](/docs/recipe/deploy/info.md#deploy-info)\n* [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup)\n* [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock)\n* [deploy:release](/docs/recipe/deploy/release.md#deploy-release)\n* [artifact:upload](/docs/recipe/magento2.md#artifact-upload)\n* [artifact:extract](/docs/recipe/magento2.md#artifact-extract)\n* [deploy:additional-shared](/docs/recipe/magento2.md#deploy-additional-shared)\n* [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared)\n* [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable)\n\n\n### artifact\\:finish {#artifact-finish}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L586)\n\nExecutes the tasks after artifact is released.\n\n\n\n\nThis task is group task which contains next tasks:\n* [magento:cache:flush](/docs/recipe/magento2.md#magento-cache-flush)\n* [cachetool:clear:opcache](/docs/contrib/cachetool.md#cachetool-clear-opcache)\n* [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup)\n* [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock)\n* [deploy:success](/docs/recipe/common.md#deploy-success)\n\n\n### artifact\\:deploy {#artifact-deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L595)\n\nActually releases the artifact deployment.\n\n\n\n\nThis task is group task which contains next tasks:\n* [artifact:prepare](/docs/recipe/magento2.md#artifact-prepare)\n* [magento:maintenance:enable-if-needed](/docs/recipe/magento2.md#magento-maintenance-enable-if-needed)\n* [magento:config:import](/docs/recipe/magento2.md#magento-config-import)\n* [magento:upgrade](/docs/recipe/magento2.md#magento-upgrade)\n* [magento:maintenance:disable](/docs/recipe/magento2.md#magento-maintenance-disable)\n* [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink)\n* [artifact:finish](/docs/recipe/magento2.md#artifact-finish)\n\n\n"
  },
  {
    "path": "docs/recipe/pimcore.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/pimcore.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Pimcore Project\n\n```php\nrequire 'recipe/pimcore.php';\n```\n\n[Source](/recipe/pimcore.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Pimcore application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Pimcore** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [deploy:cache:clear](/docs/recipe/symfony.md#deploy-cache-clear) – Clears cache\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe pimcore recipe is based on the [symfony](/docs/recipe/symfony.md) recipe.\n\n\n## Tasks\n\n### pimcore\\:rebuild-classes {#pimcore-rebuild-classes}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/pimcore.php#L16)\n\nRebuilds Pimcore Classes.\n\n\n\n\n### pimcore\\:cache_clear {#pimcore-cache_clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/pimcore.php#L22)\n\nRemoves cache.\n\n\n\n\n### pimcore\\:deploy {#pimcore-deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/pimcore.php#L26)\n\n\n\n\n\n\nThis task is group task which contains next tasks:\n* [pimcore:rebuild-classes](/docs/recipe/pimcore.md#pimcore-rebuild-classes)\n* [pimcore:cache_clear](/docs/recipe/pimcore.md#pimcore-cache_clear)\n\n\n"
  },
  {
    "path": "docs/recipe/prestashop.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/prestashop.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Prestashop Project\n\n```php\nrequire 'recipe/prestashop.php';\n```\n\n[Source](/recipe/prestashop.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Prestashop application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Prestashop** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe prestashop recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### shared_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/prestashop.php#L9)\n\nOverrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n[\n    'config/settings.inc.php',\n    '.htaccess',\n]\n```\n\n\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/prestashop.php#L13)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n[\n    'img',\n    'log',\n    'download',\n    'upload',\n    'translations',\n    'mails',\n    'themes/default-bootstrap/lang',\n    'themes/default-bootstrap/mails',\n    'themes/default-bootstrap/pdf/lang',\n]\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/prestashop.php#L24)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\n\n\n```php title=\"Default value\"\n[\n    'img',\n    'log',\n    'cache',\n    'download',\n    'upload',\n    'translations',\n    'mails',\n    'themes/default-bootstrap/lang',\n    'themes/default-bootstrap/mails',\n    'themes/default-bootstrap/pdf/lang',\n    'themes/default-bootstrap/cache',\n]\n```\n\n\n\n"
  },
  {
    "path": "docs/recipe/provision/databases.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/provision/databases.php -->\n<!-- Then run bin/docgen -->\n\n# Databases Recipe\n\n```php\nrequire 'recipe/provision/databases.php';\n```\n\n[Source](/recipe/provision/databases.php)\n\n\n## Configuration\n### db_type\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L5)\n\n\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### db_name\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L15)\n\n\n\n```php title=\"Default value\"\nreturn ask(' DB name: ', 'prod');\n```\n\n\n### db_user\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L19)\n\n\n\n```php title=\"Default value\"\nreturn ask(' DB user: ', 'deployer');\n```\n\n\n### db_password\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L23)\n\n\n\n```php title=\"Default value\"\nreturn askHiddenResponse(' DB password: ');\n```\n\n\n\n## Tasks\n\n### provision\\:databases {#provision-databases}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L28)\n\nProvision databases.\n\n\n\n\n### provision\\:mysql {#provision-mysql}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L40)\n\nProvision MySQL.\n\n\n\n\n### provision\\:mariadb {#provision-mariadb}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L51)\n\nProvision MariaDB.\n\n\n\n\n### provision\\:postgresql {#provision-postgresql}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L62)\n\nProvision PostgreSQL.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/provision/nodejs.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/provision/nodejs.php -->\n<!-- Then run bin/docgen -->\n\n# Nodejs Recipe\n\n```php\nrequire 'recipe/provision/nodejs.php';\n```\n\n[Source](/recipe/provision/nodejs.php)\n\n\n## Configuration\n### node_version\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/nodejs.php#L7)\n\n\n\n```php title=\"Default value\"\n'--lts'\n```\n\n\n\n## Tasks\n\n### provision\\:node {#provision-node}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/nodejs.php#L10)\n\nInstalls npm packages.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/provision/php.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/provision/php.php -->\n<!-- Then run bin/docgen -->\n\n# Php Recipe\n\n```php\nrequire 'recipe/provision/php.php';\n```\n\n[Source](/recipe/provision/php.php)\n\n\n## Configuration\n### php_version\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/php.php#L5)\n\n\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n\n## Tasks\n\n### provision\\:php {#provision-php}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/php.php#L18)\n\nInstalls PHP packages.\n\n\n\n\n### logs\\:php-fpm {#logs-php-fpm}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/php.php#L73)\n\nShows php-fpm logs.\n\n\n\n\n### provision\\:composer {#provision-composer}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/php.php#L82)\n\nInstalls Composer.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/provision/user.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/provision/user.php -->\n<!-- Then run bin/docgen -->\n\n# User Recipe\n\n```php\nrequire 'recipe/provision/user.php';\n```\n\n[Source](/recipe/provision/user.php)\n\n\n## Configuration\n### sudo_password\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/user.php#L7)\n\n\n\n```php title=\"Default value\"\nreturn askHiddenResponse(' Password for sudo: ');\n```\n\n\n\n## Tasks\n\n### provision\\:user {#provision-user}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/user.php#L13)\n\nSetups a deployer user.\n\n\n\n\n### provision\\:ssh_copy_id {#provision-ssh_copy_id}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/user.php#L61)\n\nCopy public key to remote server.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/provision/website.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/provision/website.php -->\n<!-- Then run bin/docgen -->\n\n# Website Recipe\n\n```php\nrequire 'recipe/provision/website.php';\n```\n\n[Source](/recipe/provision/website.php)\n\n\n## Configuration\n### domain\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/website.php#L7)\n\n\n\n```php title=\"Default value\"\nreturn ask(' Domain: ', get('hostname'));\n```\n\n\n### public_path\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/website.php#L11)\n\n\n\n```php title=\"Default value\"\nreturn ask(' Public path: ', 'public');\n```\n\n\n\n## Tasks\n\n### provision\\:server {#provision-server}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/website.php#L16)\n\nConfigures a server.\n\n\n\n\n### provision\\:website {#provision-website}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/website.php#L25)\n\nProvision website.\n\n\n\n\n### logs\\:access {#logs-access}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/website.php#L69)\n\nShows access logs.\n\n\n\n\n### logs\\:caddy {#logs-caddy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/website.php#L74)\n\nShows caddy syslog.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/provision.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/provision.php -->\n<!-- Then run bin/docgen -->\n\n# Provision Recipe\n\n```php\nrequire 'recipe/provision.php';\n```\n\n[Source](/recipe/provision.php)\n\n* Requires\n  * [databases](/docs/recipe/provision/databases.md)\n  * [nodejs](/docs/recipe/provision/nodejs.md)\n  * [php](/docs/recipe/provision/php.md)\n  * [user](/docs/recipe/provision/user.md)\n  * [website](/docs/recipe/provision/website.md)\n\n## Configuration\n### lsb_release\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L19)\n\nName of lsb_release like: focal, bionic, etc.\nAs only Ubuntu 20.04 LTS is supported for provision should be the `focal`.\n\n```php title=\"Default value\"\nreturn run(\"lsb_release -s -c\");\n```\n\n\n### provision_user\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L43)\n\nDefault user to use for provisioning.\n\n```php title=\"Default value\"\n'root'\n```\n\n\n\n## Tasks\n\n### provision {#provision}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L24)\n\nProvision the server.\n\n\n\n\nThis task is group task which contains next tasks:\n* [provision:check](/docs/recipe/provision.md#provision-check)\n* [provision:configure](/docs/recipe/provision.md#provision-configure)\n* [provision:update](/docs/recipe/provision.md#provision-update)\n* [provision:upgrade](/docs/recipe/provision.md#provision-upgrade)\n* [provision:install](/docs/recipe/provision.md#provision-install)\n* [provision:ssh](/docs/recipe/provision.md#provision-ssh)\n* [provision:firewall](/docs/recipe/provision.md#provision-firewall)\n* [provision:user](/docs/recipe/provision/user.md#provision-user)\n* [provision:php](/docs/recipe/provision/php.md#provision-php)\n* [provision:node](/docs/recipe/provision/nodejs.md#provision-node)\n* [provision:databases](/docs/recipe/provision/databases.md#provision-databases)\n* [provision:composer](/docs/recipe/provision/php.md#provision-composer)\n* [provision:server](/docs/recipe/provision/website.md#provision-server)\n* [provision:website](/docs/recipe/provision/website.md#provision-website)\n* [provision:verify](/docs/recipe/provision.md#provision-verify)\n\n\n### provision\\:check {#provision-check}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L46)\n\nChecks pre-required state.\n\n\n\n\n### provision\\:configure {#provision-configure}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L71)\n\nCollects required params.\n\n\n\n\n### provision\\:update {#provision-update}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L123)\n\nAdds repositories and update.\n\n\n\n\n### provision\\:upgrade {#provision-upgrade}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L149)\n\nUpgrades all packages.\n\n\n\n\n### provision\\:install {#provision-install}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L157)\n\nInstalls packages.\n\n\n\n\n### provision\\:ssh {#provision-ssh}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L192)\n\nConfigures the ssh.\n\n\n\n\n### provision\\:firewall {#provision-firewall}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L204)\n\nSetups a firewall.\n\n\n\n\n### provision\\:verify {#provision-verify}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L213)\n\nVerifies what provision was successful.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/shopware.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/shopware.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Shopware Project\n\n```php\nrequire 'recipe/shopware.php';\n```\n\n[Source](/recipe/shopware.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Shopware application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Shopware** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [sw:writable:jwt](/docs/recipe/shopware.md#sw-writable-jwt) – \n* [sw:deploy](/docs/recipe/shopware.md#sw-deploy) – \n  * [sw:database:migrate](/docs/recipe/shopware.md#sw-database-migrate) – \n  * [sw:plugin:refresh](/docs/recipe/shopware.md#sw-plugin-refresh) – \n  * [sw:theme:refresh](/docs/recipe/shopware.md#sw-theme-refresh) – \n  * [sw:scheduled-task:register](/docs/recipe/shopware.md#sw-scheduled-task-register) – \n  * [sw:cache:clear](/docs/recipe/shopware.md#sw-cache-clear) – \n  * [sw:plugin:update:all](/docs/recipe/shopware.md#sw-plugin-update-all) – \n  * [sw:cache:clear](/docs/recipe/shopware.md#sw-cache-clear) – \n* [deploy:clear_paths](/docs/recipe/deploy/clear_paths.md#deploy-clear_paths) – Cleanup files and/or directories\n* [sw:cache:warmup](/docs/recipe/shopware.md#sw-cache-warmup) – \n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe shopware recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n\n## Usage\n\nAdd `repository` to your _deploy.php_ file:\n\n```php\nset('repository', 'git@github.com:shopware/production.git');\n```\n\nconfigure host:\n```php\nhost('SSH-HOSTNAME')\n    ->set('remote_user', 'SSH-USER')\n    ->set('deploy_path', '/var/www/shopware') // This is the path where deployer will create its directory structure\n    ->set('http_user', 'www-data') // Not needed, if the `user` is the same, the web server is running with\n    ->set('http_group', 'www-data')\n    ->set('writable_mode', 'chmod')\n    ->set('writable_recursive', true)\n    ->set('become', 'www-data'); // You might want to change user to execute remote tasks because of access rights of created cache files\n```\n\n:::note\nPlease remember that the installation must be modified so that it can be\n[build without database](https://developer.shopware.com/docs/guides/hosting/installation-updates/deployments/build-w-o-db#compiling-the-storefront-without-database).\n:::\n\n\n## Configuration\n### bin/console\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L35)\n\n\n\n```php title=\"Default value\"\n'{{bin/php}} {{release_or_current_path}}/bin/console'\n```\n\n\n### default_timeout\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L37)\n\nOverrides [default_timeout](/docs/recipe/common.md#default_timeout) from `recipe/common.php`.\n\n\n\n\n\n### shared_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L40)\n\nOverrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`.\n\nThese files are shared among all releases.\n\n```php title=\"Default value\"\n[\n    '.env.local',\n    'install.lock',\n    'public/.htaccess',\n    'public/.user.ini',\n]\n```\n\n\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L48)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\nThese directories are shared among all releases.\n\n```php title=\"Default value\"\n[\n    'config/jwt',\n    'files',\n    'var/log',\n    'public/media',\n    'public/plugins',\n    'public/thumbnail',\n    'public/sitemap',\n]\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L60)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\nThese directories are made writable (the definition of \"writable\" requires attention).\nPlease note that the files in `config/jwt/*` receive special attention in the `sw:writable:jwt` task.\n\n```php title=\"Default value\"\n[\n    'config/jwt',\n    'custom/plugins',\n    'files',\n    'public/bundles',\n    'public/css',\n    'public/fonts',\n    'public/js',\n    'public/media',\n    'public/plugins',\n    'public/sitemap',\n    'public/theme',\n    'public/thumbnail',\n    'var',\n]\n```\n\n\n### shopware_version\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L77)\n\nThis sets the Shopware version to the version of the Shopware console command.\n\n```php title=\"Default value\"\n$versionOutput = run('cd {{release_path}} && {{bin/console}} -V');\npreg_match('/(\\d+\\.\\d+\\.\\d+\\.\\d+)/', $versionOutput, $matches);\nreturn $matches[0] ?? '6.6.0';\n```\n\n\n\n## Tasks\n\n### sw\\:cache\\:clear {#sw-cache-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L84)\n\n\n\nThis task remotely executes the `cache:clear` console command on the target server.\n\n\n### sw\\:cache\\:warmup {#sw-cache-warmup}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L90)\n\n\n\nThis task remotely executes the cache warmup console commands on the target server, so that the first user, who\nvisits the website, doesn't have to wait for the cache to be built up.\n\n\n### sw\\:database\\:migrate {#sw-database-migrate}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L100)\n\n\n\nThis task remotely executes the `database:migrate` console command on the target server.\n\n\n### sw\\:plugin\\:refresh {#sw-plugin-refresh}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L104)\n\n\n\n\n\n\n### sw\\:scheduled-task\\:register {#sw-scheduled-task-register}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L108)\n\n\n\n\n\n\n### sw\\:theme\\:refresh {#sw-theme-refresh}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L112)\n\n\n\n\n\n\n### sw\\:theme\\:compile {#sw-theme-compile}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L118)\n\n\n\nThis task is not used by default, but can be used, e.g. in combination with `SHOPWARE_SKIP_THEME_COMPILE=1`,\nto build the theme remotely instead of locally.\n\n\n### sw\\:plugin\\:update\\:all {#sw-plugin-update-all}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L130)\n\n\n\n\n\n\n### sw\\:writable\\:jwt {#sw-writable-jwt}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L140)\n\n\n\n\n\n\n### sw\\:deploy {#sw-deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L150)\n\n\n\nGrouped SW deploy tasks.\n\n\nThis task is group task which contains next tasks:\n* [sw:database:migrate](/docs/recipe/shopware.md#sw-database-migrate)\n* [sw:plugin:refresh](/docs/recipe/shopware.md#sw-plugin-refresh)\n* [sw:theme:refresh](/docs/recipe/shopware.md#sw-theme-refresh)\n* [sw:scheduled-task:register](/docs/recipe/shopware.md#sw-scheduled-task-register)\n* [sw:cache:clear](/docs/recipe/shopware.md#sw-cache-clear)\n* [sw:plugin:update:all](/docs/recipe/shopware.md#sw-plugin-update-all)\n* [sw:cache:clear](/docs/recipe/shopware.md#sw-cache-clear)\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L161)\n\nDeploys your project.\n\n\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [sw:writable:jwt](/docs/recipe/shopware.md#sw-writable-jwt)\n* [sw:deploy](/docs/recipe/shopware.md#sw-deploy)\n* [deploy:clear_paths](/docs/recipe/deploy/clear_paths.md#deploy-clear_paths)\n* [sw:cache:warmup](/docs/recipe/shopware.md#sw-cache-warmup)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n### sw-build-without-db\\:get-remote-config {#sw-build-without-db-get-remote-config}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L180)\n\n\n\n\n\n\n### sw-build-without-db\\:build {#sw-build-without-db-build}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L193)\n\n\n\n\n\n\n### sw-build-without-db {#sw-build-without-db}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L197)\n\n\n\n\n\n\nThis task is group task which contains next tasks:\n* [sw-build-without-db:get-remote-config](/docs/recipe/shopware.md#sw-build-without-db-get-remote-config)\n* [sw-build-without-db:build](/docs/recipe/shopware.md#sw-build-without-db-build)\n\n\n"
  },
  {
    "path": "docs/recipe/silverstripe.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/silverstripe.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Silverstripe Project\n\n```php\nrequire 'recipe/silverstripe.php';\n```\n\n[Source](/recipe/silverstripe.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Silverstripe application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Silverstripe** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [silverstripe:buildflush](/docs/recipe/silverstripe.md#silverstripe-buildflush) – Runs /dev/build?flush=all\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe silverstripe recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### shared_assets\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/silverstripe.php#L13)\n\nSilverstripe configuration\n\n```php title=\"Default value\"\nif (test('[ -d {{release_or_current_path}}/public ]') || test('[ -d {{deploy_path}}/shared/public ]')) {\nreturn 'public/assets';\n}\nreturn 'assets';\n```\n\n\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/silverstripe.php#L22)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\nSilverstripe shared dirs\n\n```php title=\"Default value\"\n[\n    '{{shared_assets}}',\n]\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/silverstripe.php#L27)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\nSilverstripe writable dirs\n\n```php title=\"Default value\"\n[\n    '{{shared_assets}}',\n]\n```\n\n\n### silverstripe_cli_script\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/silverstripe.php#L32)\n\nSilverstripe cli script\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n\n## Tasks\n\n### silverstripe\\:build {#silverstripe-build}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/silverstripe.php#L48)\n\nRuns /dev/build.\n\nHelper tasks\n\n\n### silverstripe\\:buildflush {#silverstripe-buildflush}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/silverstripe.php#L53)\n\nRuns /dev/build?flush=all.\n\n\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/silverstripe.php#L61)\n\nDeploys your project.\n\nMain task\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [silverstripe:buildflush](/docs/recipe/silverstripe.md#silverstripe-buildflush)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/spiral.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/spiral.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Spiral Project\n\n```php\nrequire 'recipe/spiral.php';\n```\n\n[Source](/recipe/spiral.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Spiral application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Spiral** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [spiral:encrypt-key](/docs/recipe/spiral.md#spiral-encrypt-key) – Generate new encryption key, if it doesn\\'t exist\n* [spiral:configure](/docs/recipe/spiral.md#spiral-configure) – Configure project\n* [deploy:download-rr](/docs/recipe/spiral.md#deploy-download-rr) – Download RoadRunner\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n* [deploy:restart-rr](/docs/recipe/spiral.md#deploy-restart-rr) – Restart RoadRunner\n\n\nThe spiral recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L10)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\nSpiral shared dirs\n\n```php title=\"Default value\"\n['runtime']\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L13)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\nSpiral writable dirs\n\n```php title=\"Default value\"\n['runtime', 'public']\n```\n\n\n### roadrunner_path\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L16)\n\nPath to the RoadRunner server\n\n```php title=\"Default value\"\n'{{release_or_current_path}}'\n```\n\n\n### dotenv_example\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L18)\n\nOverrides [dotenv_example](/docs/recipe/deploy/env.md#dotenv_example) from `recipe/deploy/env.php`.\n\n\n\n```php title=\"Default value\"\n'.env.sample'\n```\n\n\n\n## Tasks\n\n### spiral\\:configure {#spiral-configure}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L58)\n\nConfigure project.\n\nSpiral Framework console commands\n\n\n### spiral\\:cycle {#spiral-cycle}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L61)\n\nUpdate (init) cycle schema from database and annotated classes.\n\n\n\n\n### spiral\\:migrate {#spiral-migrate}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L64)\n\nPerform all outstanding migrations.\n\n\n\n\n### spiral\\:update {#spiral-update}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L67)\n\nUpdate project state.\n\n\n\n\n### spiral\\:cache\\:clean {#spiral-cache-clean}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L70)\n\nClean application runtime cache.\n\n\n\n\n### spiral\\:i18n\\:reset {#spiral-i18n-reset}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L73)\n\nReset translation cache.\n\n\n\n\n### spiral\\:encrypt-key {#spiral-encrypt-key}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L76)\n\nGenerate new encryption key, if it doesn\\'t exist.\n\n\n\n\n### spiral\\:views\\:compile {#spiral-views-compile}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L79)\n\nWarm-up view cache.\n\n\n\n\n### spiral\\:views\\:reset {#spiral-views-reset}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L82)\n\nClear view cache.\n\n\n\n\n### cycle\\:migrate {#cycle-migrate}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L88)\n\nGenerate ORM schema migrations.\n\nCycle ORM and migrations console commands\n\n\n### cycle\\:render {#cycle-render}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L91)\n\nRender available CycleORM schemas.\n\n\n\n\n### cycle\\:sync {#cycle-sync}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L94)\n\nSync Cycle ORM schema with database without intermediate migration (risk operation).\n\n\n\n\n### migrate\\:init {#migrate-init}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L97)\n\nInit migrations component (create migrations table).\n\n\n\n\n### migrate\\:replay {#migrate-replay}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L100)\n\nReplay (down, up) one or multiple migrations.\n\n\n\n\n### migrate\\:rollback {#migrate-rollback}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L103)\n\nRollback one (default) or multiple migrations.\n\n\n\n\n### migrate\\:status {#migrate-status}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L106)\n\nGet list of all available migrations and their statuses.\n\n\n\n\n### roadrunner\\:serve {#roadrunner-serve}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L112)\n\nStart RoadRunner server.\n\nRoadRunner console commands\n\n\n### roadrunner\\:stop {#roadrunner-stop}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L117)\n\nStop RoadRunner server.\n\n\n\n\n### roadrunner\\:reset {#roadrunner-reset}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L120)\n\nReset workers of all services.\n\n\n\n\n### deploy\\:download-rr {#deploy-download-rr}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L126)\n\nDownload RoadRunner.\n\nDownload and restart RoadRunner\n\n\n### deploy\\:restart-rr {#deploy-restart-rr}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L132)\n\nRestart RoadRunner.\n\n\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L146)\n\nDeploys your project.\n\nMain task\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [spiral:encrypt-key](/docs/recipe/spiral.md#spiral-encrypt-key)\n* [spiral:configure](/docs/recipe/spiral.md#spiral-configure)\n* [deploy:download-rr](/docs/recipe/spiral.md#deploy-download-rr)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n* [deploy:restart-rr](/docs/recipe/spiral.md#deploy-restart-rr)\n\n\n"
  },
  {
    "path": "docs/recipe/statamic.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/statamic.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Statamic Project\n\n```php\nrequire 'recipe/statamic.php';\n```\n\n[Source](/recipe/statamic.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Statamic application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Statamic** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [artisan:storage:link](/docs/recipe/laravel.md#artisan-storage-link) – Creates the symbolic links configured for the application\n* [artisan:cache:clear](/docs/recipe/laravel.md#artisan-cache-clear) – Flushes the application cache\n* [statamic:stache:clear](/docs/recipe/statamic.md#statamic-stache-clear) – Clears the \"Stache\" cache\n* [statamic:stache:warm](/docs/recipe/statamic.md#statamic-stache-warm) – Builds the \"Stache\" cache\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe statamic recipe is based on the [laravel](/docs/recipe/laravel.md) recipe.\n\n## Configuration\n### statamic_version\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L16)\n\n\n\n```php title=\"Default value\"\n$result = run('{{bin/php}} {{release_or_current_path}}/please --version');\npreg_match_all('/(\\d+\\.?)+/', $result, $matches);\nreturn $matches[0][0] ?? 'unknown';\n```\n\n\n\n## Tasks\n\n### statamic\\:addons\\:discover {#statamic-addons-discover}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L27)\n\nRebuilds the cached addon package manifest.\n\nAddons\n\n\n### statamic\\:assets\\:generate-presets {#statamic-assets-generate-presets}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L34)\n\nGenerates asset preset manipulations.\n\nAssets\n\n\n### statamic\\:assets\\:meta {#statamic-assets-meta}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L37)\n\nGenerates asset metadata files.\n\n\n\n\n### statamic\\:git\\:commit {#statamic-git-commit}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L44)\n\nGit add and commit tracked content.\n\nGit\n\n\n### statamic\\:glide\\:clear {#statamic-glide-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L51)\n\nClears the Glide image cache.\n\nGlide\n\n\n### statamic\\:responsive\\:generate {#statamic-responsive-generate}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L58)\n\nGenerates responsive images.\n\nResponsive Images (not in the core)\n\n\n### statamic\\:responsive\\:regenerate {#statamic-responsive-regenerate}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L61)\n\nRegenerate responsive images.\n\n\n\n\n### statamic\\:search\\:insert {#statamic-search-insert}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L68)\n\nInserts an item into its search indexes.\n\nSearch\n\n\n### statamic\\:search\\:update {#statamic-search-update}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L71)\n\nUpdate a search index.\n\n\n\n\n### statamic\\:stache\\:clear {#statamic-stache-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L78)\n\nClears the \"Stache\" cache.\n\nStache\n\n\n### statamic\\:stache\\:doctor {#statamic-stache-doctor}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L81)\n\nDiagnose any problems with the Stache.\n\n\n\n\n### statamic\\:stache\\:refresh {#statamic-stache-refresh}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L84)\n\nClears and rebuild the \"Stache\" cache.\n\n\n\n\n### statamic\\:stache\\:warm {#statamic-stache-warm}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L87)\n\nBuilds the \"Stache\" cache.\n\n\n\n\n### statamic\\:static\\:clear {#statamic-static-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L94)\n\nClears the static page cache.\n\nStatic\n\n\n### statamic\\:static\\:warm {#statamic-static-warm}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L97)\n\nWarms the static cache by visiting all URLs.\n\n\n\n\n### statamic\\:support\\:details {#statamic-support-details}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L104)\n\nOutputs details helpful for support requests.\n\nSupport\n\n\n### statamic\\:updates\\:run {#statamic-updates-run}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L111)\n\nRuns update scripts from specific version.\n\nUpdated\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L119)\n\nDeploys your project.\n\nMain Deploy Script for Statamic, which\nwill overwrite the Laravel default.\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [artisan:storage:link](/docs/recipe/laravel.md#artisan-storage-link)\n* [artisan:cache:clear](/docs/recipe/laravel.md#artisan-cache-clear)\n* [statamic:stache:clear](/docs/recipe/statamic.md#statamic-stache-clear)\n* [statamic:stache:warm](/docs/recipe/statamic.md#statamic-stache-warm)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/sulu.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/sulu.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Sulu Project\n\n```php\nrequire 'recipe/sulu.php';\n```\n\n[Source](/recipe/sulu.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Sulu application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Sulu** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [deploy:cache:clear](/docs/recipe/symfony.md#deploy-cache-clear) – Clears cache\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe sulu recipe is based on the [symfony](/docs/recipe/symfony.md) recipe.\n\n## Configuration\n### bin/websiteconsole\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/sulu.php#L13)\n\n\n\n```php title=\"Default value\"\nreturn parse('{{bin/php}} {{release_or_current_path}}/bin/websiteconsole --no-interaction');\n```\n\n\n\n## Tasks\n\n### phpcr\\:migrate {#phpcr-migrate}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/sulu.php#L18)\n\nMigrates PHPCR.\n\n\n\n\n### deploy\\:website\\:cache\\:clear {#deploy-website-cache-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/sulu.php#L23)\n\nClears cache.\n\n\n\n\n### deploy\\:website\\:cache\\:warmup {#deploy-website-cache-warmup}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/sulu.php#L28)\n\nWarmups cache.\n\n\n\n\n"
  },
  {
    "path": "docs/recipe/symfony.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/symfony.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Symfony Application\n\n```php\nrequire 'recipe/symfony.php';\n```\n\n[Source](/recipe/symfony.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Symfony application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Symfony** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [deploy:cache:clear](/docs/recipe/symfony.md#deploy-cache-clear) – Clears cache\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe symfony recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### symfony_version\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L9)\n\n\n\n```php title=\"Default value\"\n$result = run('{{bin/console}} --version');\npreg_match_all('/(\\d+\\.?)+/', $result, $matches);\nreturn $matches[0][0] ?? 5.0;\n```\n\n\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L15)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n[\n    'var/log',\n]\n```\n\n\n### shared_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L19)\n\nOverrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n[\n    '.env.local',\n]\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L23)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\n\n\n```php title=\"Default value\"\n[\n    'var',\n    'var/cache',\n    'var/log',\n    'var/sessions',\n]\n```\n\n\n### log_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L30)\n\n\n\n```php title=\"Default value\"\n'var/log/*.log'\n```\n\n\n### migrations_config\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L32)\n\n\n\n\n\n### doctrine_schema_validate_config\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L34)\n\n\n\n\n\n### bin/console\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L36)\n\n\n\n```php title=\"Default value\"\n'{{bin/php}} {{release_or_current_path}}/bin/console'\n```\n\n\n### console_options\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L38)\n\n\n\n```php title=\"Default value\"\nreturn '--no-interaction';\n```\n\n\n\n## Tasks\n\n### database\\:migrate {#database-migrate}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L43)\n\nMigrates database.\n\n\n\n\n### doctrine\\:schema\\:validate {#doctrine-schema-validate}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L53)\n\nValidate the Doctrine mapping files.\n\n\n\n\n### deploy\\:cache\\:clear {#deploy-cache-clear}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L58)\n\nClears cache.\n\n\n\n\n### deploy\\:dump-env {#deploy-dump-env}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L67)\n\nOptimize environment variables.\n\n\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L74)\n\nDeploys project.\n\n\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [deploy:cache:clear](/docs/recipe/symfony.md#deploy-cache-clear)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/typo3.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/typo3.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a TYPO3 Project\n\n```php\nrequire 'recipe/typo3.php';\n```\n\n[Source](/recipe/typo3.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your TYPO3 application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **TYPO3** consists of:\n* [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n* [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n* [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n* [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n* [typo3:update_code](/docs/recipe/typo3.md#typo3-update_code) – \n* [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n* [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [typo3:install:fixfolderstructure](/docs/recipe/typo3.md#typo3-install-fixfolderstructure) – TYPO3 - Fix folder structure\n* [typo3:extension:setup](/docs/recipe/typo3.md#typo3-extension-setup) – TYPO3 - Set up all extensions\n* [typo3:language:update](/docs/recipe/typo3.md#typo3-language-update) – TYPO3 - Update the language files of all activated extensions\n* [typo3:cache:flush](/docs/recipe/typo3.md#typo3-cache-flush) – TYPO3 - Clear all caches\n* [typo3:cache:warmup](/docs/recipe/typo3.md#typo3-cache-warmup) – TYPO3 - Cache warmup for system caches\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe typo3 recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n\nTYPO3 Deployer Recipe\n\nUsage Examples:\n\nDeploy to production (using Git as source):\n    vendor/bin/dep deploy production\n\nDeploy to staging using rsync:\n    # In deploy.php or servers config, enable rsync\n    set('use_rsync', true);\n    vendor/bin/dep deploy staging\n\nCommon TYPO3 commands:\n    vendor/bin/dep typo3:cache:flush                     # Clear all TYPO3 caches\n    vendor/bin/dep typo3:cache:warmup                    # Warmup system caches\n    vendor/bin/dep typo3:language:update                 # Update extension language files\n    vendor/bin/dep typo3:extension:setup                 # Set up all extensions\n    vendor/bin/dep typo3:install:fixfolderstructure      # Automatically create required files and folders for TYPO3\n\n\n## Configuration\n### composer_config\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L35)\n\nParse composer.json and return its contents as an array.\nUsed for auto-detecting TYPO3 settings like public_dir and bin_dir.\n\n```php title=\"Default value\"\nreturn json_decode(file_get_contents('./composer.json'), true, 512, JSON_THROW_ON_ERROR);\n```\n\n\n### typo3/public_dir\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L44)\n\nTYPO3 public (web) directory.\nAutomatically determined from composer.json.\nDefaults to \"public\".\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### bin/typo3\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L58)\n\nPath to the TYPO3 CLI binary.\nDetermined from composer.json \"config.bin-dir\" or defaults to \"vendor/bin/typo3\".\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n\n\n\n\n### log_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L71)\n\nLog files to display when running `./vendor/bin/dep logs:app`\n\n```php title=\"Default value\"\n'var/log/typo3_*.log'\n```\n\n\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L77)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\nDirectories that persist between releases.\nShared via symlinks from the shared/ directory.\n\n```php title=\"Default value\"\n[\n    '{{typo3/public_dir}}/fileadmin',\n    '{{typo3/public_dir}}/typo3temp/assets',\n    'var/lock',\n    'var/log',\n    'var/session',\n    'var/spool',\n]\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L99)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\nWriteable directories\n\n```php title=\"Default value\"\n[\n    '{{typo3/public_dir}}/fileadmin',\n    '{{typo3/public_dir}}/typo3temp/assets',\n    'var/cache',\n    'var/lock',\n    'var/log',\n]\n```\n\n\n### composer_options\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L110)\n\nOverrides [composer_options](/docs/recipe/deploy/vendors.md#composer_options) from `recipe/deploy/vendors.php`.\n\nComposer install options for production.\n\n```php title=\"Default value\"\n' --no-dev --verbose --prefer-dist --no-progress --no-interaction --optimize-autoloader'\n```\n\n\n### use_rsync\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L116)\n\nIf set in the config this recipe uses rsync.\nDefault setting: false (uses the Git repository)\n\n```php title=\"Default value\"\nfalse\n```\n\n\n### update_code_task\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L118)\n\n\n\n```php title=\"Default value\"\nreturn get('use_rsync') ? 'rsync' : 'deploy:update_code';\n```\n\n\n### rsync\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L145)\n\n\n\n```php title=\"Default value\"\n[\n    'exclude' => array_merge(get('shared_dirs'), get('shared_files'), $exclude),\n    'exclude-file' => false,\n    'include' => ['vendor'],\n    'include-file' => false,\n    'filter' => ['dir-merge,-n /.gitignore'],\n    'filter-file' => false,\n    'filter-perdir' => false,\n    'flags' => 'avz',\n    'options' => ['delete', 'keep-dirlinks', 'links'],\n    'timeout' => 600,\n]\n```\n\n\n### typo3_updateschema_types\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L162)\n\nList of schema update types.\n`safe` includes all necessary operations, to add or change fields or tables.\n\n```php title=\"Default value\"\n'safe'\n```\n\n\n\n## Tasks\n\n### typo3\\:update_code {#typo3-update_code}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L122)\n\n\n\n\n\n\n### typo3\\:cache\\:flush {#typo3-cache-flush}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L171)\n\nTYPO3 - Clear all caches.\n\nTYPO3 Commands\nAll run via [bin/php](/docs/recipe/common.md#bin/php) [release_path](/docs/recipe/deploy/release.md#release_path)/[bin/typo3](/docs/recipe/typo3.md#bin/typo3) <command>\n\n\n### typo3\\:cache\\:warmup {#typo3-cache-warmup}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L176)\n\nTYPO3 - Cache warmup for system caches.\n\n\n\n\n### typo3\\:language\\:update {#typo3-language-update}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L181)\n\nTYPO3 - Update the language files of all activated extensions.\n\n\n\n\n### typo3\\:extension\\:setup {#typo3-extension-setup}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L186)\n\nTYPO3 - Set up all extensions.\n\n\n\n\n### typo3\\:install\\:fixfolderstructure {#typo3-install-fixfolderstructure}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L191)\n\nTYPO3 - Fix folder structure.\n\n\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L212)\n\nDeploys a TYPO3 project.\n\nMain deploy task for TYPO3.\n\n1. Lock deploy to avoid concurrent runs\n2. Create release directory\n3. Update code (Git or rsync)\n4. Symlink shared dirs/files\n5. Fix TYPO3 folder structure\n6. Ensure writable dirs\n7. Run extension setup & perform schema updates\n8. Update language files\n9. Install composer vendors\n10. Flush caches\n11. Warm up TYPO3 caches\n12. Unlock and clean up\n\n\nThis task is group task which contains next tasks:\n* [deploy:info](/docs/recipe/deploy/info.md#deploy-info)\n* [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup)\n* [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock)\n* [deploy:release](/docs/recipe/deploy/release.md#deploy-release)\n* [typo3:update_code](/docs/recipe/typo3.md#typo3-update_code)\n* [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared)\n* [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [typo3:install:fixfolderstructure](/docs/recipe/typo3.md#typo3-install-fixfolderstructure)\n* [typo3:extension:setup](/docs/recipe/typo3.md#typo3-extension-setup)\n* [typo3:language:update](/docs/recipe/typo3.md#typo3-language-update)\n* [typo3:cache:flush](/docs/recipe/typo3.md#typo3-cache-flush)\n* [typo3:cache:warmup](/docs/recipe/typo3.md#typo3-cache-warmup)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/wordpress.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/wordpress.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a WordPress Project\n\n```php\nrequire 'recipe/wordpress.php';\n```\n\n[Source](/recipe/wordpress.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your WordPress application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **WordPress** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe wordpress recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### shared_files\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/wordpress.php#L9)\n\nOverrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n['wp-config.php']\n```\n\n\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/wordpress.php#L10)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\n\n\n```php title=\"Default value\"\n['wp-content/uploads']\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/wordpress.php#L11)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\n\n\n```php title=\"Default value\"\n['wp-content/uploads']\n```\n\n\n\n## Tasks\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/wordpress.php#L14)\n\nDeploys your project.\n\n\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/yii.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/yii.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Yii2 Project\n\n```php\nrequire 'recipe/yii.php';\n```\n\n[Source](/recipe/yii.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Yii2 application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Yii2** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [deploy:migrate](/docs/recipe/yii.md#deploy-migrate) – Runs Yii2 migrations for your project\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe yii recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n## Configuration\n### shared_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/yii.php#L10)\n\nOverrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`.\n\nYii shared dirs\n\n```php title=\"Default value\"\n['runtime']\n```\n\n\n### writable_dirs\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/yii.php#L13)\n\nOverrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`.\n\nYii writable dirs\n\n```php title=\"Default value\"\n['runtime']\n```\n\n\n\n## Tasks\n\n### deploy\\:migrate {#deploy-migrate}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/yii.php#L16)\n\nRuns Yii2 migrations for your project.\n\n\n\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/yii.php#L24)\n\nDeploys your project.\n\nMain task\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [deploy:migrate](/docs/recipe/yii.md#deploy-migrate)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/recipe/zend_framework.md",
    "content": "<!-- DO NOT EDIT THIS FILE! -->\n<!-- Instead edit recipe/zend_framework.php -->\n<!-- Then run bin/docgen -->\n\n# How to Deploy a Zend Framework Project\n\n```php\nrequire 'recipe/zend_framework.php';\n```\n\n[Source](/recipe/zend_framework.php)\n\nDeployer is a free and open source deployment tool written in PHP. \nIt helps you to deploy your Zend Framework application to a server. \nIt is very easy to use and has a lot of features. \n\nThree main features of Deployer are:\n- **Provisioning** - provision your server for you.\n- **Zero downtime deployment** - deploy your application without a downtime.\n- **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\nAdditionally, Deployer has a lot of other features, like:\n- **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n- **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n- **Secure** - Deployer uses SSH to connect to your server.\n- **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\nYou can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\nThe [deploy](#deploy) task of **Zend Framework** consists of:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release\n  * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment\n  * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy\n  * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy\n  * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release\n  * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code\n  * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file\n  * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs\n  * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors\n* [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release\n  * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release\n  * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy\n  * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases\n  * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project\n\n\nThe zend_framework recipe is based on the [common](/docs/recipe/common.md) recipe.\n\n\n## Tasks\n\n### deploy {#deploy}\n[Source](https://github.com/deployphp/deployer/blob/master/recipe/zend_framework.php#L13)\n\nDeploys your project.\n\nMain task\n\n\nThis task is group task which contains next tasks:\n* [deploy:prepare](/docs/recipe/common.md#deploy-prepare)\n* [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors)\n* [deploy:publish](/docs/recipe/common.md#deploy-publish)\n\n\n"
  },
  {
    "path": "docs/selector.md",
    "content": "# Selector\n\nDeployer uses the selector to choose hosts. Each host can have a set of labels.\nLabels are key-value pairs.\n\nFor example, `stage: production` or `role: web`.\n\nYou can use labels to select hosts. For example, `dep deploy stage=production`\nwill deploy to all hosts with `stage: production` label.\n\nLet's define two labels, **type** and **env**, to our hosts:\n\n```php\nhost('web.example.com')\n    ->setLabels([\n        'type' => 'web',\n        'env' => 'prod',\n    ]);\n\nhost('db.example.com')\n    ->setLabels([\n        'type' => 'db',\n        'env' => 'prod',\n    ]);\n```\nor use `->addLabels()` method to add labels to the existing host.\n\nNow let's define a task to check labels:\n\n```php\ntask('info', function () {\n    writeln('type:' . get('labels')['type'] . ' env:' . get('labels')['env']);\n});\n```\n\nNow we can run this task with a selector:\n\n```bash\n$ dep info env=prod\ntask info\n[web.example.com] type:web env:prod\n[db.example.com] type:db env:prod\n```\n\nAs you can see, Deployer will run this task on all hosts with the `env: prod` label.\nAnd if we define only the `type` label, Deployer will run this task on the specified host.\n\n```bash\ndep info type=web\ntask info\n[web.example.com] type:web env:prod\n```\n\n## Selector syntax\n\nSelector syntax consists of a list of conditions, separated by `,` or `&`. There comma means **OR**\nand `&` means **AND**.\n\nFor example, `type=web,env=prod` is a selector of: `type=web` **OR** `env=prod`.\n\n```bash\n$ dep info 'type=web,env=prod'\ntask info\n[web.example.com] type:web env:prod\n[db.example.com] type:db env:prod\n```\n\nAs you can see, both hosts are selected (as both of them have the `env: prod` label).\n\nWe can use `&` to define **AND**. For example, `type=web & env=prod` is a selector\nfor hosts with `type: web` **AND** `env: prod` labels.\n\n```bash\n$ dep info 'type=web & env=prod'\ntask info\n[web.example.com] type:web env:prod\n```\n\nWe can use `|` to define **OR** in a subquery. For example, `type=web|db & env=prod` is a selector\nfor hosts with (`type: web` **OR** `type: db`) **AND** `env: prod` labels.\n\n```bash\n$ dep info 'type=web|db & env=prod'\ntask info\n[web.example.com] type:web env:prod\n[db.example.com] type:db env:prod\n```\n\nWe can also use `!=` to negate a label. For example, `type!=web` is a selector for\nall hosts which do not have a `type: web` label.\n\n```bash\n$ dep info 'type!=web'\ntask info\n[db.example.com] type:db env:prod\n```\n\n:::note\nDeployer CLI can take a few selectors as arguments. For example,\n`dep info type=web env=prod` is the same as `dep info 'type=web,env=prod'`.\n\nYou can install bash autocompletion for Deployer CLI, which will help you to\nwrite selectors. See [installation](installation.md) for more.\n:::\n\nDeployer also has a few special selectors:\n\n- `all` - select all hosts\n- `alias=...` - select host by alias\n\nIf a selector does not contain an `=` sign, Deployer will assume that it is an alias.\n\nFor example `dep info web.example.com` is the same as `dep info alias=web.example.com`.\n\n```bash\n$ dep info web.example.com\ntask info\n[web.example.com] type:web env:prod\n```\n\n```bash\n$ dep info 'web.example.com' 'db.example.com'\n$ # Same as: \n$ dep info 'alias=web.example.com,alias=db.example.com'\n````\n\n## Using the select() function\n\nYou can use the [select()](api.md#select) function to select hosts by selector in your PHP code.\n\n```php\ntask('info', function () {\n    $hosts = select('type=web|db,env=prod');\n    foreach ($hosts as $host) {\n        writeln('type:' . $host->get('labels')['type'] . ' env:' . $host->get('labels')['env']);\n    }\n});\n```\n\nOr you can use the [on()](api.md#on) function to run a task on selected hosts.\n\n```php\ntask('info', function () {\n    on(select('all'), function () {\n        writeln('type:' . get('labels')['type'] . ' env:' . get('labels')['env']);\n    });\n});\n```\n\n## Task selectors\n\nTo restrict a task to run only on selected hosts, you can use the [select()](tasks.md#select) method.\n\n```php\ntask('info', function () {\n    // ...\n})->select('type=web|db,env=prod');\n```\n\n## Labels in YAML\n\nYou can also define labels in a YAML recipe. For example:\n\n```yaml\nhosts:\n  web.example.com:\n    remote_user: deployer\n    env:\n      environment: production\n    labels:\n      env: prod\n```\n\nBut make sure to distinguish between the `env` and `labels.env` keys.\n`env` is a configuration key, and `labels.env` is a label.\n\n```php\ntask('info', function () {\n    writeln('env:' . get('env')['environment'] . ' labels.env:' . get('labels')['env']);\n});\n```\n\nWill print:\n\n```bash\n$ dep info env=prod\ntask info\n[web.example.com] env:production labels.env:prod\n```\n"
  },
  {
    "path": "docs/sidebar.js",
    "content": "module.exports = [\n  \"installation\",\n  \"getting-started\",\n  \"basics\",\n  {\n    type: \"category\",\n    label: \"Main Concepts\",\n    items: [\"hosts\", \"tasks\", \"selector\"],\n  },\n  \"ci-cd\",\n  \"yaml\",\n  \"cli\",\n  \"api\",\n  {\n    type: \"category\",\n    label: \"Other\",\n    items: [\"avoid-php-fpm-reloading\", \"UPGRADE\", \"KNOWN_BUGS\"],\n  },\n];\n"
  },
  {
    "path": "docs/tasks.md",
    "content": "# Tasks\n\nDefine a task by using the [task](api.md#task) function. Also, you can give a description\nfor a task with the [desc](api.md#desc) function called before _task_:\n\n```php\ndesc('My task');\ntask('my_task', function () {\n    ....\n});\n```\n\nTo get the task or override task config, call the _task_ function without the second argument:\n\n```php\ntask('my_task')->disable();\n```\n\n## Task config\n\n### desc()\n\nSets task's description.\n\n```php\ntask('deploy', function () {\n    // ...\n})->desc('Task description');\n```\n\nSame as using [desc()](api.md#desc) function helper:\n\n```php\ndesc('Task description');\ntask('deploy', function () {\n    // ...\n});\n```\n\n### once()\n\nSets the task to run only on one of the selected hosts.\n\n### oncePerNode()\n\nSets the task to run only on **one node** of the selected hosts.\n\nThe node is identified by its [hostname](hosts.md#hostname). For instance,\nmultiple hosts might deploy to a single physical machine (with a unique hostname).\n\n\n```php\nhost('foo')->setHostname('example.com');\nhost('bar')->setHostname('example.com');\nhost('pro')->setHostname('another.com');\n\ntask('apt:update', function () {\n    // This task will be executed twice, only on \"foo\" and \"pro\" hosts.\n    run('apt-get update');\n})->oncePerNode();\n```\n\n### hidden()\n\nHides the task from CLI usage page.\n\n### addBefore()\n\nAdds a before hook to the task.\n\n### addAfter()\n\nAdds an after hook to the task.\n\n### limit()\n\nLimits the number of hosts the task will be executed on in parallel.\n\nDefault is unlimited (runs the task on all hosts in parallel).\n\n### select()\n\nSets the task's host selector.\n\n### addSelector()\n\nAdds the task's selector.\n\n### verbose()\n\nMakes the task always verbose, as if the `-v` option is persistently enabled.\n\n### disable()\n\nDisables the task. the task will not be executed.\n\n### enable()\n\nEnables the task.\n\n## Task grouping\n\nYou can combine tasks in groups:\n\n```php\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:update_code',\n    'deploy:vendors',\n    'deploy:symlink',\n    'cleanup'\n]);\n```\n\n## Task hooks\n\nYou can define tasks to be run before or after specific tasks.\n\n```php\ntask('deploy:done', function () {\n    writeln('Deploy done!');\n});\n\nafter('deploy', 'deploy:done');\n```\n\nAfter the `deploy` task executed, `deploy:done` will be triggered.\n\n:::note\nYou can see which hooks are enabled via the **dep tree** command.\n\n```\ndep tree deploy\n```\n\n:::\n"
  },
  {
    "path": "docs/yaml.md",
    "content": "# YAML\n\nDeployer supports recipes written in YAML. For validating the structure, Deployer uses\nthe JSON Schema declared in [schema.json](https://github.com/deployphp/deployer/blob/master/src/schema.json).\n\nHere is an example of a YAML recipe:\n\n```yaml\nimport:\n  - recipe/laravel.php\n\nconfig:\n  repository: \"git@github.com:example/example.com.git\"\n  remote_user: deployer\n\nhosts:\n  example.com:\n    deploy_path: \"~/example\"\n\ntasks:\n  build:\n    - cd: \"{{release_path}}\"\n    - run: \"npm run build\"\n\nafter:\n  deploy:failed: deploy:unlock\n```\n\nYAML recipes can include recipes written in PHP. For example, some tasks maybe written in PHP and imported into YAML.\n\nConversely, it's also possible to import a YAML recipe from PHP using the [import()](api.md#import) function.\n"
  },
  {
    "path": "phpstan.neon",
    "content": "includes:\n    - tests/phpstan-baseline.neon\n\nparameters:\n    level: 5\n    paths:\n        - src\n        - recipe\n        - contrib\n\n    ignoreErrors:\n        - \"#^Constant DEPLOYER_VERSION not found\\\\.$#\"\n        - \"#^Constant DEPLOYER_BIN not found\\\\.$#\"\n        - \"#^Constant MASTER_ENDPOINT not found\\\\.$#\"\n        - \"#CpanelPhp#\"\n        - \"#AMQPMessage#\"\n\n    excludePaths:\n        - src/Component/PharUpdate/*\n"
  },
  {
    "path": "phpunit.xml",
    "content": "<?xml version=\"1.0\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" bootstrap=\"tests/bootstrap.php\" colors=\"true\" convertErrorsToExceptions=\"true\" convertNoticesToExceptions=\"true\" convertWarningsToExceptions=\"true\" stopOnError=\"false\" stopOnFailure=\"false\" verbose=\"true\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/9.3/phpunit.xsd\">\n  <coverage includeUncoveredFiles=\"true\">\n    <include>\n      <directory>src/</directory>\n      <directory>recipe/</directory>\n    </include>\n    <exclude>\n      <directory suffix=\".php\">vendor/</directory>\n      <directory>bin/</directory>\n    </exclude>\n  </coverage>\n  <testsuites>\n    <testsuite name=\"Src\">\n      <directory>tests/src/</directory>\n    </testsuite>\n    <testsuite name=\"Legacy\">\n      <directory>tests/legacy/</directory>\n    </testsuite>\n    <testsuite name=\"Joy\">\n      <directory>tests/joy/</directory>\n    </testsuite>\n  </testsuites>\n</phpunit>\n"
  },
  {
    "path": "recipe/cakephp.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['cakephp']);\n\n/**\n * CakePHP 4 Project Template configuration\n */\n\n// CakePHP 4 Project Template shared dirs\nset('shared_dirs', [\n    'logs',\n    'tmp',\n]);\n\n// CakePHP 4 Project Template shared files\nset('shared_files', [\n    'config/.env',\n    'config/app.php',\n]);\n\n/**\n * Create plugins' symlinks\n */\ntask('deploy:init', function () {\n    run('{{bin/php}} {{release_or_current_path}}/bin/cake.php plugin assets symlink');\n})->desc('Initialization');\n\n/**\n * Run migrations\n */\ntask('deploy:run_migrations', function () {\n    run('{{bin/php}} {{release_or_current_path}}/bin/cake.php migrations migrate --no-lock');\n    run('{{bin/php}} {{release_or_current_path}}/bin/cake.php schema_cache build');\n})->desc('Run migrations');\n\n/**\n * Main task\n */\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'deploy:init',\n    'deploy:run_migrations',\n    'deploy:publish',\n])->desc('Deploy your project');\n"
  },
  {
    "path": "recipe/codeigniter.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['codeigniter']);\n\n// CodeIgniter shared dirs\nset('shared_dirs', ['application/cache', 'application/logs']);\n\n// CodeIgniter writable dirs\nset('writable_dirs', ['application/cache', 'application/logs']);\n\n/**\n * Main task\n */\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'deploy:publish',\n]);\n"
  },
  {
    "path": "recipe/codeigniter4.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['codeigniter4']);\n\n// Default Configurations\nset('public_path', 'public');\n\nset('shared_dirs', ['writable']);\n\nset('shared_files', ['.env']);\n\nset('writable_dirs', [\n    'writable/cache',\n    'writable/debugbar',\n    'writable/logs',\n    'writable/session',\n    'writable/uploads',\n]);\n\nset('log_files', 'writable/logs/*.log');\n\nset('codeigniter4_version', function () {\n    $result = run('{{bin/php}} {{release_or_current_path}}/spark');\n    preg_match_all('/(\\d+\\.?)+/', $result, $matches);\n    return $matches[0][0] ?? 5.5;\n});\n\n/**\n * Run an spark command.\n *\n * Supported options:\n * - 'min' => #.#: The minimum Codeigniter4 version required (included).\n * - 'max' => #.#: The maximum Codeigniter4 version required (included).\n * - 'skipIfNoEnv': Skip and warn the user if `.env` file is inexistent or empty.\n * - 'failIfNoEnv': Fail the command if `.env` file is inexistent or empty.\n * - 'showOutput': Show the output of the command if given.\n *\n * @param string $command The spark command (with cli options if any).\n * @param array $options The options that define the behavior of the command.\n * @return callable A function that can be used as a task.\n */\nfunction spark($command, $options = [])\n{\n    return function () use ($command, $options) {\n\n        // Ensure the spark command is available on the current version.\n        $versionTooEarly = array_key_exists('min', $options)\n            && codeigniter4_version_compare($options['min'], '<');\n\n        $versionTooLate = array_key_exists('max', $options)\n            && codeigniter4_version_compare($options['max'], '>');\n\n        if ($versionTooEarly || $versionTooLate) {\n            return;\n        }\n\n        // Ensure we warn or fail when a command relies on the \".env\" file.\n        if (in_array('failIfNoEnv', $options) && !test('[ -s {{release_or_current_path}}/.env ]')) {\n            throw new \\Exception('Your .env file is empty! Cannot proceed.');\n        }\n\n        if (in_array('skipIfNoEnv', $options) && !test('[ -s {{release_or_current_path}}/.env ]')) {\n            warning(\"Your .env file is empty! Skipping...</>\");\n            return;\n        }\n\n        $spark = '{{release_or_current_path}}/spark';\n\n        // Run the spark command.\n        $output = run(\"{{bin/php}} $spark $command\");\n\n        // Output the results when appropriate.\n        if (in_array('showOutput', $options)) {\n            writeln(\"<info>$output</info>\");\n        }\n    };\n}\n\nfunction codeigniter4_version_compare($version, $comparator)\n{\n    return version_compare(get('codeigniter4_version'), $version, $comparator);\n}\n\n\n/**\n * Discover & Checks\n */\n\ndesc('Shows file cache information in the current system.');\ntask('spark:cache:info', spark('cache:info', ['showOutput']));\n\ndesc('Check your Config values.');\ntask('spark:config:check', spark('config:check', ['skipIfNoEnv', 'showOutput', 'min' => '4.5.0']));\n\ndesc('Retrieves the current environment, or set a new one.');\ntask('spark:env', spark('env', ['skipIfNoEnv', 'showOutput']));\n\ndesc('Check filters for a route.');\ntask('spark:filter:check', spark('filter:check', ['showOutput', 'min' => '4.3.0']));\n\ndesc('Find and save available phrases to translate.');\ntask('spark:lang:find', spark('lang:find', ['showOutput', 'min' => '4.5.0']));\n\ndesc('Verifies your namespaces are setup correctly.');\ntask('spark:namespaces', spark('namespaces', ['showOutput']));\n\ndesc('Check your php.ini values.');\ntask('spark:phpini:check', spark('phpini:check', ['showOutput', 'min' => '4.5.0']));\n\ndesc('Displays all routes.');\ntask('spark:routes', spark('routes', ['showOutput', 'min' => '4.3.0']));\n\n\n/**\n * Actions\n */\n\ndesc('Generates a new encryption key and writes it in an `.env` file.');\ntask('spark:key:generate', spark('key:generate', ['skipIfNoEnv']));\n\ndesc('Optimize for production.');\ntask('spark:optimize', spark('optimize', ['min' => '4.5.0']));\n\ndesc('Discovers and executes all predefined Publisher classes.');\ntask('spark:publish', spark('publish', ['skipIfNoEnv', 'showOutput']));\n\n\n/*\n * Database and migrations.\n */\n\ndesc('Create a new database schema.');\ntask('spark:db:create', spark('db:create', ['showOutput']));\n\ndesc('Runs the specified seeder to populate known data into the database.');\ntask('spark:db:seed', spark('db:seed', ['skipIfNoEnv']));\n\ndesc('Retrieves information on the selected table.');\ntask('spark:db:table', spark('db:table', ['skipIfNoEnv', 'showOutput', 'min' => '4.5.0']));\n\ndesc('Locates and runs all new migrations against the database.');\ntask('spark:migrate', spark('migrate --all', ['skipIfNoEnv']));\n\ndesc('Does a rollback followed by a latest to refresh the current state of the database.');\ntask('spark:migrate:refresh', spark('migrate:refresh -f --all', ['skipIfNoEnv']));\n\ndesc('Runs the \"down\" method for all migrations in the last batch.');\ntask('spark:migrate:rollback', spark('migrate:rollback -f', ['skipIfNoEnv', 'showOutput']));\n\ndesc('Displays a list of all migrations and whether they\\'ve been run or not.');\ntask('spark:migrate:status', spark('migrate:status', ['skipIfNoEnv', 'showOutput']));\n\n\n/**\n * Housekeeping\n */\n\ndesc('Clears the current system caches.');\ntask('spark:cache:clear', spark('cache:clear'));\n\ndesc('Clears all debugbar JSON files.');\ntask('spark:debugbar:clear', spark('debugbar:clear'));\n\ndesc('Clears all log files.');\ntask('spark:logs:clear', spark('logs:clear'));\n\n\n/**\n * Custom Spark Command for shield or setting packages\n */\ndesc('Run a custom spark command.');\ntask('spark:custom', spark('', ['showOutput']));\n\n\n\n/**\n * Main deploy task.\n */\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'spark:optimize',\n    'spark:migrate',\n    'deploy:publish',\n]);\n"
  },
  {
    "path": "recipe/common.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire __DIR__ . '/provision.php';\nrequire __DIR__ . '/deploy/check_remote.php';\nrequire __DIR__ . '/deploy/cleanup.php';\nrequire __DIR__ . '/deploy/clear_paths.php';\nrequire __DIR__ . '/deploy/copy_dirs.php';\nrequire __DIR__ . '/deploy/env.php';\nrequire __DIR__ . '/deploy/info.php';\nrequire __DIR__ . '/deploy/lock.php';\nrequire __DIR__ . '/deploy/push.php';\nrequire __DIR__ . '/deploy/release.php';\nrequire __DIR__ . '/deploy/rollback.php';\nrequire __DIR__ . '/deploy/setup.php';\nrequire __DIR__ . '/deploy/shared.php';\nrequire __DIR__ . '/deploy/symlink.php';\nrequire __DIR__ . '/deploy/update_code.php';\nrequire __DIR__ . '/deploy/vendors.php';\nrequire __DIR__ . '/deploy/writable.php';\n\nuse Deployer\\Exception\\ConfigurationException;\nuse Deployer\\Exception\\RunException;\n\nadd('recipes', ['common']);\n\n// Name of current user who is running deploy.\n// If not set will try automatically get git user name,\n// otherwise output of `whoami` command.\nset('user', function () {\n    if (getenv('CI') !== false) {\n        $ciUserVars = ['GITLAB_USER_NAME', 'GITHUB_ACTOR', 'CIRCLE_USERNAME', 'DRONE_BUILD_TRIGGER'];\n        foreach ($ciUserVars as $var) {\n            if (($ciUser = getenv($var)) !== false) {\n                return $ciUser;\n            }\n        }\n        return 'ci';\n    }\n\n    try {\n        return runLocally('git config --get user.name');\n    } catch (RunException $exception) {\n        try {\n            return runLocally('whoami');\n        } catch (RunException $exception) {\n            return 'no_user';\n        }\n    }\n});\n\n// Number of releases to preserve in releases folder.\nset('keep_releases', 10);\n\n// Repository to deploy.\nset('repository', '');\n\n// Default timeout for `run()` and `runLocally()` functions.\n//\n// Set to `null` to disable timeout.\nset('default_timeout', 300);\n\n/**\n * Remote environment variables.\n * ```php\n * set('env', [\n *     'KEY' => 'something',\n * ]);\n * ```\n *\n * It is possible to override it per `run()` call.\n *\n * ```php\n * run('echo $KEY', env: ['KEY' => 'over']);\n * ```\n */\nset('env', []);\n\n/**\n * Path to `.env` file which will be used as environment variables for each command per `run()`.\n *\n * ```php\n * set('dotenv', '{{release_or_current_path}}/.env');\n * ```\n */\nset('dotenv', false);\n\n/**\n * The deploy path.\n *\n * For example can be set for a bunch of host once as:\n * ```php\n * set('deploy_path', '~/{{alias}}');\n * ```\n */\nset('deploy_path', function () {\n    throw new ConfigurationException('Please, specify `deploy_path`.');\n});\n\n/**\n * Return current release path. Default to {{deploy_path}}/`current`.\n * ```php\n * set('current_path', '/var/public_html');\n * ```\n */\nset('current_path', '{{deploy_path}}/current');\n\n// Path to the `php` bin.\nset('bin/php', function () {\n    if (currentHost()->hasOwn('php_version')) {\n        return '/usr/bin/php{{php_version}}';\n    }\n    return which('php');\n});\n\n// Path to the `git` bin.\nset('bin/git', function () {\n    return which('git');\n});\n\n// Should {{bin/symlink}} use `--relative` option or not. Will detect\n// automatically.\nset('use_relative_symlink', function () {\n    return commandSupportsOption('ln', '--relative');\n});\n\n// Path to the `ln` bin. With predefined options `-nfs`.\nset('bin/symlink', function () {\n    return get('use_relative_symlink') ? 'ln -nfs --relative' : 'ln -nfs';\n});\n\n// Path to a file which will store temp script with sudo password.\n// Defaults to `.dep/sudo_pass`. This script is only temporary and will be deleted after\n// sudo command executed.\nset('sudo_askpass', function () {\n    if (test('[ -d {{deploy_path}}/.dep ]')) {\n        return '{{deploy_path}}/.dep/sudo_pass';\n    } else {\n        return '/tmp/dep_sudo_pass';\n    }\n});\n\ndesc('Prepares a new release');\ntask('deploy:prepare', [\n    'deploy:info',\n    'deploy:setup',\n    'deploy:lock',\n    'deploy:release',\n    'deploy:update_code',\n    'deploy:env',\n    'deploy:shared',\n    'deploy:writable',\n]);\n\ndesc('Publishes the release');\ntask('deploy:publish', [\n    'deploy:symlink',\n    'deploy:unlock',\n    'deploy:cleanup',\n    'deploy:success',\n]);\n\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:publish',\n]);\n\n\n/**\n * Prints success message\n */\ntask('deploy:success', function () {\n    info('successfully deployed!');\n})\n    ->hidden();\n\n\n/**\n * Hook on deploy failure.\n */\ntask('deploy:failed', function () {})\n    ->hidden();\n\nfail('deploy', 'deploy:failed');\n\n/**\n * Follows latest application logs.\n */\ndesc('Shows application logs');\ntask('logs:app', function () {\n    if (!has('log_files')) {\n        warning(\"Please, specify \\\"log_files\\\" option.\");\n        return;\n    }\n    cd('{{current_path}}');\n    run('tail -f {{log_files}}');\n})->verbose();\n"
  },
  {
    "path": "recipe/composer.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['composer']);\n\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'deploy:publish',\n]);\n"
  },
  {
    "path": "recipe/contao.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/symfony.php';\n\nadd('recipes', ['contao']);\n\n// The public path is the path to be set as DocumentRoot and is defined in the `composer.json` of the project\n// but defaults to `public` from Contao 5.0 on.\n// This path is relative from the {{current_path}}, see [`recipe/provision/website.php`](/docs/recipe/provision/website.php#public_path).\nset('public_path', function () {\n    $composerConfig = json_decode(file_get_contents('./composer.json'), true, 512, JSON_THROW_ON_ERROR);\n\n    return $composerConfig['extra']['public-dir'] ?? 'public';\n});\n\nadd('shared_files', ['config/parameters.yml']);\n\nadd('shared_dirs', [\n    'assets/images',\n    'contao-manager',\n    'files',\n    '{{public_path}}/share',\n    'system/config',\n    'var/backups',\n    'var/logs',\n]);\n\nset('bin/console', function () {\n    return '{{bin/php}} {{release_or_current_path}}/vendor/bin/contao-console';\n});\n\nset('contao_version', function () {\n    $result = run('{{bin/console}} --version');\n    preg_match_all('/(\\d+\\.?)+/', $result, $matches);\n    return $matches[0][0] ?? 'n/a';\n});\n\nset('symfony_version', function () {\n    $result = run('{{bin/console}} about');\n    preg_match_all('/(\\d+\\.?)+/', $result, $matches);\n    return $matches[0][0] ?? 5.0;\n});\n\n// This task updates the database. A database backup is saved automatically as a default.\n//\n// To automatically drop the obsolete database structures, you can override the task as follows:\n//\n// ```php\n// task('contao:migrate', function () {\n//     run('{{bin/php}} {{bin/console}} contao:migrate --with-deletes {{console_options}}');\n// });\n// ```\ndesc('Run Contao migrations');\ntask('contao:migrate', function () {\n    run('{{bin/console}} contao:migrate {{console_options}}');\n});\n\n// Downloads the `contao-manager.phar.php` into the public path.\ndesc('Download the Contao Manager');\ntask('contao:manager:download', function () {\n    run('curl -LsO https://download.contao.org/contao-manager/stable/contao-manager.phar && mv contao-manager.phar {{release_or_current_path}}/{{public_path}}/contao-manager.phar.php');\n});\n\n// Locks the Contao install tool which is useful if you don't use it.\ndesc('Lock the Contao Install Tool');\ntask('contao:install:lock', function () {\n    run('{{bin/console}} contao:install:lock {{console_options}}');\n});\n\n// Locks the Contao Manager which is useful if you only need the API of the Manager rather than the UI.\ndesc('Lock the Contao Manager');\ntask('contao:manager:lock', function () {\n    cd('{{release_or_current_path}}');\n    run('echo \"99\" > contao-manager/login.lock');\n});\n\ndesc('Enable maintenance mode');\ntask('contao:maintenance:enable', function () {\n    // Enable maintenance mode in both the current and release build, so that the maintenance mode will be enabled\n    // for the current installation before the symlink changes and the new installation after the symlink changed.\n    foreach (array_unique([parse('{{current_path}}'), parse('{{release_or_current_path}}')]) as $path) {\n        // The current path might not be present during first deploy.\n        if (!test(\"[ -d $path ]\")) {\n            continue;\n        }\n\n        cd($path);\n        run('{{bin/console}} contao:maintenance-mode enable {{console_options}}');\n    }\n});\n\ndesc('Disable maintenance mode');\ntask('contao:maintenance:disable', function () {\n    foreach (array_unique([parse('{{current_path}}'), parse('{{release_or_current_path}}')]) as $path) {\n        if (!test(\"[ -d $path ]\")) {\n            continue;\n        }\n\n        cd($path);\n        run('{{bin/console}} contao:maintenance-mode disable {{console_options}}');\n    }\n});\n\ndesc('Deploy the project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'contao:maintenance:enable',\n    'contao:migrate',\n    'contao:maintenance:disable',\n    'deploy:publish',\n]);\n"
  },
  {
    "path": "recipe/craftcms.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['craftcms']);\n\nset('log_files', 'storage/logs/*.log');\n\nset('shared_dirs', [\n    'storage',\n    'web/assets',\n]);\n\nset('shared_files', ['.env']);\n\nset('writable_dirs', [\n    'config/project',\n    'storage',\n    'web/assets',\n    'web/cpresources',\n]);\n\n/**\n * Run a craft command.\n *\n * Supported options:\n *  - 'showOutput': Show the output of the command if given.\n *  - 'interactive': Don't append the --interactive=0 flag to the command.\n *\n * @param string $command The craft command (with cli options if any).\n * @param array $options The options that define the behaviour of the command.\n *\n * @return callable A function that can be used as a task.\n */\nfunction craft($command, $options = [])\n{\n    return function () use ($command, $options) {\n        if (! test('[ -s {{release_path}}/.env ]')) {\n            throw new \\Exception('Your .env file is empty! Cannot proceed.');\n        }\n\n        // By default we don't want any command to be interactive\n        if (! in_array('interactive', $options)) {\n            $command .= ' --interactive=0';\n        }\n\n        $output = run(\"{{bin/php}} {{release_path}}/craft $command\");\n\n        if (in_array('showOutput', $options)) {\n            writeln(\"<info>$output</info>\");\n        }\n    };\n}\n\n/*\n * Migrations\n */\n\ndesc('Runs all pending Craft, plugin, and content migrations');\ntask('craft:migrate/all', craft('migrate/all'));\n\ndesc('Upgrades Craft by applying new migrations');\ntask('craft:migrate/up', craft('migrate/up'));\n\n/*\n * Generate keys\n */\n\ndesc('Generates a new application ID and saves it in the `.env` file');\ntask('craft:setup/app-id', craft('setup/app-id'));\n\ndesc('Generates a new security key and saves it in the `.env` file');\ntask('craft:setup/security-key', craft('setup/security-key'));\n\n/*\n * Project config\n */\n\ndesc('Applies project config file changes.');\ntask('craft:project-config/apply', craft('project-config/apply'));\n\n/*\n * Caches\n */\n\ndesc('Flushes all caches registered in the system');\ntask('craft:cache/flush-all', craft('cache/flush-all'));\n\ndesc('Clear all caches');\ntask('craft:clear-caches/all', craft('clear-caches/all'));\n\ndesc('Clear all Asset caches');\ntask('craft:clear-caches/asset', craft('clear-caches/asset'));\n\ndesc('Clear all Asset indexing data');\ntask('craft:clear-caches/asset-indexing-data', craft('clear-caches/asset-indexing-data'));\n\ndesc('Clear all compiled classes');\ntask('craft:clear-caches/compiled-classes', craft('clear-caches/compiled-classes'));\n\ndesc('Clear all compiled templates');\ntask('craft:clear-caches/compiled-templates', craft('clear-caches/compiled-templates'));\n\ndesc('Clear all control panel resources');\ntask('craft:clear-caches/cp-resources', craft('clear-caches/cp-resources'));\n\ndesc('Clear all data caches');\ntask('craft:clear-caches/data', craft('clear-caches/data'));\n\ndesc('Clear all temp files');\ntask('craft:clear-caches/temp-files', craft('clear-caches/temp-files'));\n\n/*\n * Garbage collection\n */\n\ndesc('Runs garbage collection');\ntask('craft:gc', craft('gc --delete-all-trashed=1 --silent-exit-on-exception=1', ['showOutput']));\n\n/*\n * Main deploy\n */\n\ndesc('Deploys Craft CMS');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'craft:clear-caches/compiled-classes',\n    'craft:migrate/all',\n    'craft:project-config/apply',\n    'craft:gc',\n    'craft:clear-caches/all',\n    'deploy:publish',\n]);\n"
  },
  {
    "path": "recipe/deploy/check_remote.php",
    "content": "<?php\n\nnamespace Deployer;\n\nuse Deployer\\Exception\\Exception;\nuse Deployer\\Exception\\GracefulShutdownException;\n\n// Cancel deployment if there would be no change to the codebase.\n// This avoids unnecessary releases if the latest commit has already been deployed.\ndesc('Checks remote head');\ntask('deploy:check_remote', function () {\n    $repository = get('repository');\n\n    // Skip if there is no current deployment to compare\n    if (!test('[ -d {{current_path}}/.git ]')) {\n        return;\n    }\n\n    // Determine the hash of the remote revision about to be deployed\n    $targetRevision = input()->getOption('revision');\n\n    if (!$targetRevision) {\n        $ref = 'HEAD';\n        $opt = '';\n        if ($tag = input()->getOption('tag')) {\n            $ref = $tag;\n            $opt = '--tags';\n        } elseif ($branch = get('branch')) {\n            $ref = $branch;\n            $opt = '--heads';\n        }\n        $remoteLs = runLocally(\"git ls-remote $opt $repository $ref\");\n        if (strstr($remoteLs, \"\\n\")) {\n            throw new Exception(\"Could not determine target revision. '$ref' matched multiple commits.\");\n        }\n        if (!$remoteLs) {\n            throw new Exception(\"Could not resolve a revision from '$ref'.\");\n        }\n        $targetRevision = substr($remoteLs, 0, strpos($remoteLs, \"\\t\"));\n    }\n\n    // Compare commit hashes. We use strpos to support short versions.\n    $targetRevision = trim($targetRevision);\n    $lastDeployedRevision = run('cat {{current_path}}/REVISION');\n    if ($targetRevision && strpos($lastDeployedRevision, $targetRevision) === 0) {\n        throw new GracefulShutdownException(\"Already up-to-date.\");\n    }\n\n    info(\"deployed different version\");\n});\n"
  },
  {
    "path": "recipe/deploy/cleanup.php",
    "content": "<?php\n\nnamespace Deployer;\n\n// Use sudo in deploy:cleanup task for rm command.\nset('cleanup_use_sudo', false);\n\ndesc('Cleanup old releases');\ntask('deploy:cleanup', function () {\n    $releases = get('releases_list');\n    $keep = get('keep_releases');\n    $sudo = get('cleanup_use_sudo') ? 'sudo' : '';\n\n    run(\"cd {{deploy_path}} && if [ -e release ]; then rm release; fi\");\n\n    if ($keep > 0) {\n        foreach (array_slice($releases, $keep) as $release) {\n            run(\"$sudo rm -rf {{deploy_path}}/releases/$release\");\n        }\n    }\n});\n"
  },
  {
    "path": "recipe/deploy/clear_paths.php",
    "content": "<?php\n\nnamespace Deployer;\n\n// List of paths to remove from {{release_path}}.\nset('clear_paths', []);\n\n// Use sudo for deploy:clear_path task?\nset('clear_use_sudo', false);\n\ndesc('Cleanup files and/or directories');\ntask('deploy:clear_paths', function () {\n    $paths = get('clear_paths');\n    $sudo = get('clear_use_sudo') ? 'sudo' : '';\n    $batch = 100;\n\n    $commands = [];\n    foreach ($paths as $path) {\n        $commands[] = \"$sudo rm -rf {{release_path}}/$path\";\n    }\n    $chunks = array_chunk($commands, $batch);\n    foreach ($chunks as $chunk) {\n        run(implode('; ', $chunk));\n    }\n});\n"
  },
  {
    "path": "recipe/deploy/copy_dirs.php",
    "content": "<?php\n\nnamespace Deployer;\n\n// List of dirs to copy between releases.\n// For example you can copy `node_modules` to speedup npm install.\nset('copy_dirs', []);\n\ndesc('Copies directories');\ntask('deploy:copy_dirs', function () {\n    if (has('previous_release')) {\n        foreach (get('copy_dirs') as $dir) {\n            // Make sure all path without tailing slash.\n            $dir = trim($dir, '/');\n\n            if (test(\"[ -d {{previous_release}}/$dir ]\")) {\n                $destinationDir = '';\n                if (strpos($dir, '/') !== false) {\n                    $destinationDir = substr($dir, 0, strrpos($dir, '/') + 1);\n                }\n                run(\"mkdir -p {{release_path}}/$dir\");\n                // -a, --archive\n                //        copy directories recursively, preserve all attributes,\n                //        never follow symbolic links in SOURCE\n                // -f, --force\n                //        if  an existing destination file cannot be opened, remove it and try again (this option is ignored when the -n\n                //        option is also used)\n                run(\"cp -af {{previous_release}}/$dir {{release_path}}/$destinationDir\");\n            }\n        }\n    }\n});\n"
  },
  {
    "path": "recipe/deploy/env.php",
    "content": "<?php\n\nnamespace Deployer;\n\nset('dotenv_example', '.env.example');\n\ndesc('Configure .env file');\ntask('deploy:env', function () {\n    cd('{{release_or_current_path}}');\n    if (test('[ ! -e .env ] && [ -f {{dotenv_example}} ]')) {\n        run('cp {{dotenv_example}} .env');\n    }\n});\n"
  },
  {
    "path": "recipe/deploy/info.php",
    "content": "<?php\n\nnamespace Deployer;\n\n// Defines \"what\" text for the 'deploy:info' task.\n// Uses one of the following sources:\n// 1. Repository name\n// 2. Application name\nset('what', function () {\n    $repo = get('repository');\n    if (!empty($repo)) {\n        return preg_replace('/\\.git$/', '', basename($repo));\n    }\n    $application = get('application');\n    if (!empty($application)) {\n        return $application;\n    }\n    return 'something';\n});\n\n// Defines \"where\" text for the 'deploy:info' task.\n// Uses one of the following sources:\n// 1. Host's stage label\n// 2. Host's alias\nset('where', function () {\n    $labels = get('labels');\n    if (isset($labels['stage'])) {\n        return $labels['stage'];\n    }\n    return currentHost()->getAlias();\n});\n\ndesc('Displays info about deployment');\ntask('deploy:info', function () {\n    $releaseName = test('[ -d {{deploy_path}}/.dep ]') ? get('release_name') : 1;\n\n    info(\"deploying <fg=green;options=bold>{{what}}</> to <fg=magenta;options=bold>{{where}}</> (release <fg=magenta;options=bold>{$releaseName}</>)\");\n});\n"
  },
  {
    "path": "recipe/deploy/lock.php",
    "content": "<?php\n\nnamespace Deployer;\n\nuse Deployer\\Exception\\GracefulShutdownException;\n\ndesc('Locks deploy');\ntask('deploy:lock', function () {\n    $user = escapeshellarg(get('user'));\n    $locked = run(\"[ -f {{deploy_path}}/.dep/deploy.lock ] && echo +locked || echo $user > {{deploy_path}}/.dep/deploy.lock\");\n    if ($locked === '+locked') {\n        $lockedUser = run(\"cat {{deploy_path}}/.dep/deploy.lock\");\n        throw new GracefulShutdownException(\n            \"Deploy locked by $lockedUser.\\n\" .\n            \"Execute \\\"deploy:unlock\\\" task to unlock.\",\n        );\n    }\n});\n\ndesc('Unlocks deploy');\ntask('deploy:unlock', function () {\n    run(\"rm -f {{deploy_path}}/.dep/deploy.lock\");//always success\n});\n\ndesc('Checks if deploy is locked');\ntask('deploy:is_locked', function () {\n    $locked = test(\"[ -f {{deploy_path}}/.dep/deploy.lock ]\");\n    if ($locked) {\n        $lockedUser = run(\"cat {{deploy_path}}/.dep/deploy.lock\");\n        throw new GracefulShutdownException(\"Deploy is locked by $lockedUser.\");\n    }\n    info('Deploy is unlocked.');\n});\n"
  },
  {
    "path": "recipe/deploy/push.php",
    "content": "<?php\n\nnamespace Deployer;\n\n// Creates patch of local changes and pushes them on host.\n// And applies to current_path. Push can be done many times.\n// The task purpose to be used only for development.\ndesc('Pushes local changes to remote host');\ntask('push', function () {\n    $files = explode(\"\\n\", runLocally(\"git diff --name-only HEAD\"));\n\n    info('uploading:');\n    foreach ($files as $file) {\n        writeln(\" - $file\");\n    }\n\n    upload(\n        $files,\n        '{{current_path}}',\n        ['progress_bar' => false, 'options' => ['--relative']],\n    );\n\n    // Mark this release as dirty.\n    run(\"echo '{{user}}' > {{current_path}}/DIRTY_RELEASE\");\n});\n"
  },
  {
    "path": "recipe/deploy/release.php",
    "content": "<?php\n\nnamespace Deployer;\n\nuse Deployer\\Exception\\Exception;\nuse Symfony\\Component\\Console\\Helper\\Table;\n\nuse function Deployer\\Support\\escape_shell_argument;\n\n// The name of the release.\nset('release_name', function () {\n    return within('{{deploy_path}}', function () {\n        $latest = run('cat .dep/latest_release || echo 0');\n        return strval(intval($latest) + 1);\n    });\n});\n\n// Holds releases log from `.dep/releases_log` file.\nset('releases_log', function () {\n    cd('{{deploy_path}}');\n\n    if (!test('[ -f .dep/releases_log ]')) {\n        return [];\n    }\n\n    $releaseLogs = array_map(function ($line) {\n        return json_decode($line, true);\n    }, explode(\"\\n\", run('tail -n 300 .dep/releases_log')));\n\n    return array_filter($releaseLogs); // Return all non-empty lines.\n});\n\n// Return list of release names on host.\nset('releases_list', function () {\n    cd('{{deploy_path}}');\n\n    // If there is no releases return empty list.\n    if (!test('[ -d releases ] && [ \"$(ls -A releases)\" ]')) {\n        return [];\n    }\n\n    // Will list only dirs in releases.\n    $ll = explode(\"\\n\", run('cd releases && ls -t -1 -d */'));\n    $ll = array_map(function ($release) {\n        return basename(rtrim(trim($release), '/'));\n    }, $ll);\n\n    // Return releases from newest to oldest.\n    $releasesLog = array_reverse(get('releases_log'));\n\n    $releases = [];\n    foreach ($releasesLog as $release) {\n        if (in_array($release['release_name'], $ll, true)) {\n            $releases[] = $release['release_name'];\n        }\n    }\n    return $releases;\n});\n\n// Return release path.\nset('release_path', function () {\n    $releaseExists = test('[ -h {{deploy_path}}/release ]');\n    if ($releaseExists) {\n        $link = run(\"readlink {{deploy_path}}/release\");\n        return substr($link, 0, 1) === '/' ? $link : get('deploy_path') . '/' . $link;\n    } else {\n        throw new Exception(parse('The \"release_path\" ({{deploy_path}}/release) does not exist.'));\n    }\n});\n\n// Current release revision. Usually a git hash.\nset('release_revision', function () {\n    return run('cat {{release_path}}/REVISION');\n});\n\n// Return the release path during a deployment\n// but fallback to the current path otherwise.\nset('release_or_current_path', function () {\n    $releaseExists = test('[ -h {{deploy_path}}/release ]');\n    return $releaseExists ? get('release_path') : get('current_path');\n});\n\n// Clean up unfinished releases and prepare next release\ndesc('Prepares release');\ntask('deploy:release', function () {\n    cd('{{deploy_path}}');\n\n    // Clean up if there is unfinished release.\n    if (test('[ -h release ]')) {\n        run('rm release'); // Delete symlink.\n    }\n\n    // We need to get releases_list at same point as release_name,\n    // as standard release_name's implementation depends on it and,\n    // if user overrides it, we need to get releases_list manually.\n    $releasesList = get('releases_list');\n    $releaseName = get('release_name');\n    $releasePath = \"releases/$releaseName\";\n\n    // Check what there is no such release path.\n    if (test(\"[ -d $releasePath ]\")) {\n        $freeReleaseName = '...';\n        // Check what $releaseName is integer.\n        if (ctype_digit($releaseName)) {\n            $freeReleaseName = intval($releaseName);\n            // Find free release name.\n            while (test(\"[ -d releases/$freeReleaseName ]\")) {\n                $freeReleaseName++;\n            }\n        }\n        throw new Exception(\"Release name \\\"$releaseName\\\" already exists.\\nRelease name can be overridden via:\\n dep deploy -o release_name=$freeReleaseName\");\n    }\n\n    // Save release_name.\n    if (is_numeric($releaseName) && is_integer(intval($releaseName))) {\n        run(\"echo $releaseName > .dep/latest_release\");\n    }\n\n    // Metainfo.\n    $timestamp = timestamp();\n    $metainfo = [\n        'created_at' => $timestamp,\n        'release_name' => $releaseName,\n        'user' => get('user'),\n        'target' => get('target'),\n    ];\n\n    // Save metainfo about release.\n    $json = escape_shell_argument(json_encode($metainfo));\n    run(\"echo $json >> .dep/releases_log\");\n\n    // Make new release.\n    run(\"mkdir -p $releasePath\");\n    run(\"{{bin/symlink}} $releasePath {{deploy_path}}/release\");\n\n    // Add to releases list.\n    array_unshift($releasesList, $releaseName);\n    set('releases_list', $releasesList);\n\n    // Set previous_release.\n    if (isset($releasesList[1])) {\n        set('previous_release', \"{{deploy_path}}/releases/{$releasesList[1]}\");\n    }\n});\n\ndesc('Shows releases list');\n/*\n * Example output:\n * ```\n * +---------------------+------example.org ------------+--------+-----------+\n * | Date (UTC)          | Release     | Author         | Target | Commit    |\n * +---------------------+-------------+----------------+--------+-----------+\n * | 2021-11-06 20:51:45 | 1           | Anton Medvedev | HEAD   | 34d24192e |\n * | 2021-11-06 21:00:50 | 2 (bad)     | Anton Medvedev | HEAD   | 392948a40 |\n * | 2021-11-06 23:19:20 | 3           | Anton Medvedev | HEAD   | a4057a36c |\n * | 2021-11-06 23:24:30 | 4 (current) | Anton Medvedev | HEAD   | s3wa45ca6 |\n * +---------------------+-------------+----------------+--------+-----------+\n * ```\n */\ntask('releases', function () {\n    cd('{{deploy_path}}');\n\n    $releasesLog = get('releases_log');\n    $currentRelease = basename(run('readlink {{current_path}}'));\n    $releasesList = get('releases_list');\n\n    $table = [];\n    $tz = !empty(getenv('TIMEZONE')) ? getenv('TIMEZONE') : date_default_timezone_get();\n\n    foreach ($releasesLog as &$metainfo) {\n        $date = \\DateTime::createFromFormat(\\DateTimeInterface::ISO8601, $metainfo['created_at']);\n        $date->setTimezone(new \\DateTimeZone($tz));\n        $status = $release = $metainfo['release_name'];\n        if (in_array($release, $releasesList, true)) {\n            if (test(\"[ -f releases/$release/BAD_RELEASE ]\")) {\n                $status = \"<error>$release</error> (bad)\";\n            } elseif (test(\"[ -f releases/$release/DIRTY_RELEASE ]\")) {\n                $status = \"<error>$release</error> (dirty)\";\n            } else {\n                $status = \"<info>$release</info>\";\n            }\n            try {\n                $revision = run(\"cat releases/$release/REVISION\");\n            } catch (\\Throwable $e) {\n                $revision = 'unknown';\n            }\n        } else {\n            $revision = 'unknown';\n        }\n        if ($release === $currentRelease) {\n            $status .= ' (current)';\n        }\n        $table[] = [\n            $date->format(\"Y-m-d H:i:s\"),\n            $status,\n            $metainfo['user'],\n            $metainfo['target'],\n            $revision,\n        ];\n    }\n\n    (new Table(output()))\n        ->setHeaderTitle(currentHost()->getAlias())\n        ->setHeaders([\"Date ($tz)\", 'Release', 'Author', 'Target', 'Commit'])\n        ->setRows($table)\n        ->render();\n});\n"
  },
  {
    "path": "recipe/deploy/rollback.php",
    "content": "<?php\n\nnamespace Deployer;\n\nuse Deployer\\Exception\\Exception;\n\n/*\n * Rollback candidate will be automatically chosen by looking\n * at output of `ls` command and content of `.dep/releases_log`.\n *\n * If rollback candidate is marked as **BAD_RELEASE**, it will be skipped.\n *\n * :::tip\n * You can override rollback candidate via:\n * ```\n * dep rollback -o rollback_candidate=123\n * ```\n * :::\n */\nset('rollback_candidate', function () {\n    $currentRelease = basename(run('readlink {{current_path}}'));\n    $releases = get('releases_list');\n\n    $releasesBeforeCurrent = [];\n    $foundCurrent = false;\n    foreach ($releases as $r) {\n        if ($r === $currentRelease) {\n            $foundCurrent = true;\n            continue;\n        }\n        if ($foundCurrent) {\n            $releasesBeforeCurrent[] = $r;\n        }\n    }\n\n    while (isset($releasesBeforeCurrent[0])) {\n        $candidate = $releasesBeforeCurrent[0];\n\n        // Skip all bad releases.\n        if (test(\"[ -f {{deploy_path}}/releases/$candidate/BAD_RELEASE ]\")) {\n            array_shift($releasesBeforeCurrent);\n            continue;\n        }\n\n        return $candidate;\n    }\n\n    throw new Exception(\"No more releases you can revert to.\");\n});\n\ndesc('Rollbacks to the previous release');\n/*\n * Uses {{rollback_candidate}} for symlinking. Current release will be marked as\n * bad by creating file **BAD_RELEASE** with timestamp and {{user}}.\n *\n * :::warning\n * You can always manually symlink {{current_path}} to proper release.\n * ```\n * dep run '{{bin/symlink}} releases/123 {{current_path}}'\n * ```\n * :::\n */\ntask('rollback', function () {\n    cd('{{deploy_path}}');\n\n    $currentRelease = basename(run('readlink {{current_path}}'));\n    $candidate = get('rollback_candidate');\n\n    writeln(\"Current release is <fg=red>$currentRelease</fg=red>.\");\n\n    if (!test(\"[ -d releases/$candidate ]\")) {\n        throw new \\RuntimeException(parse(\"Release \\\"$candidate\\\" not found in \\\"{{deploy_path}}/releases\\\".\"));\n    }\n    if (test(\"[ -f releases/$candidate/BAD_RELEASE ]\")) {\n        writeln(\"Candidate <fg=yellow>$candidate</> marked as <error>bad release</error>.\");\n        if (!askConfirmation(\"Continue rollback to $candidate?\")) {\n            writeln('Rollback aborted.');\n            return;\n        }\n    }\n    writeln(\"Rolling back to <info>$candidate</info> release.\");\n\n    // Symlink to old release.\n    run(\"{{bin/symlink}} releases/$candidate {{current_path}}\");\n\n    // Mark release as bad.\n    $timestamp = timestamp();\n    run(\"echo '$timestamp,{{user}}' > releases/$currentRelease/BAD_RELEASE\");\n\n    writeln(\"<info>rollback</info> to release <info>$candidate</info> was <success>successful</success>\");\n});\n"
  },
  {
    "path": "recipe/deploy/setup.php",
    "content": "<?php\n\nnamespace Deployer;\n\ndesc('Prepares host for deploy');\ntask('deploy:setup', function () {\n    run(\n        <<<EOF\n            [ -d {{deploy_path}} ] || mkdir -p {{deploy_path}};\n            cd {{deploy_path}};\n            [ -d .dep ] || mkdir .dep;\n            [ -d releases ] || mkdir releases;\n            [ -d shared ] || mkdir shared;\n            EOF,\n    );\n\n    // If current_path points to something like \"/var/www/html\", make sure it is\n    // a symlink and not a directory.\n    if (test('[ ! -L {{current_path}} ] && [ -d {{current_path}} ]')) {\n        throw error(\"There is a directory (not symlink) at {{current_path}}.\\n Remove this directory so it can be replaced with a symlink for atomic deployments.\");\n    }\n});\n"
  },
  {
    "path": "recipe/deploy/shared.php",
    "content": "<?php\n\nnamespace Deployer;\n\nuse Deployer\\Exception\\Exception;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\n// List of dirs what will be shared between releases.\n// Each release will have symlink to those dirs stored in {{deploy_path}}/shared dir.\n// ```php\n// set('shared_dirs', ['storage']);\n// ```\nset('shared_dirs', []);\n\n// List of files what will be shared between releases.\n// Each release will have symlink to those files stored in {{deploy_path}}/shared dir.\n// ```php\n// set('shared_files', ['.env']);\n// ```\nset('shared_files', []);\n\ndesc('Creates symlinks for shared files and dirs');\ntask('deploy:shared', function () {\n    $sharedPath = \"{{deploy_path}}/shared\";\n\n    // Validate shared_dir, find duplicates\n    foreach (get('shared_dirs') as $a) {\n        foreach (get('shared_dirs') as $b) {\n            if ($a !== $b && strpos(rtrim($a, '/') . '/', rtrim($b, '/') . '/') === 0) {\n                throw new Exception(\"Can not share same dirs `$a` and `$b`.\");\n            }\n        }\n    }\n\n    $copyVerbosity = output()->getVerbosity() === OutputInterface::VERBOSITY_DEBUG ? 'v' : '';\n\n    foreach (get('shared_dirs') as $dir) {\n        // Make sure all path without tailing slash.\n        $dir = trim($dir, '/');\n\n        // Check if shared dir does not exist.\n        if (!test(\"[ -d $sharedPath/$dir ]\")) {\n            // Create shared dir if it does not exist.\n            run(\"mkdir -p $sharedPath/$dir\");\n            // If release contains shared dir, copy that dir from release to shared.\n            if (test(\"[ -d $(echo {{release_path}}/$dir) ]\")) {\n                run(\"cp -r$copyVerbosity {{release_path}}/$dir $sharedPath/\" . dirname($dir));\n            }\n        }\n\n        // Remove from source.\n        run(\"rm -rf {{release_path}}/$dir\");\n\n        // Create path to shared dir in release dir if it does not exist.\n        // Symlink will not create the path and will fail otherwise.\n        run(\"mkdir -p `dirname {{release_path}}/$dir`\");\n\n        // Symlink shared dir to release dir\n        run(\"{{bin/symlink}} $sharedPath/$dir {{release_path}}/$dir\");\n    }\n\n    foreach (get('shared_files') as $file) {\n        $dirname = dirname(parse($file));\n\n        // Create dir of shared file if not existing\n        if (!test(\"[ -d $sharedPath/$dirname ]\")) {\n            run(\"mkdir -p $sharedPath/$dirname\");\n        }\n\n        // Check if shared file does not exist in shared.\n        // and file exist in release\n        if (!test(\"[ -f $sharedPath/$file ]\") && test(\"[ -f {{release_path}}/$file ]\")) {\n            // Copy file in shared dir if not present\n            run(\"cp -r$copyVerbosity {{release_path}}/$file $sharedPath/$file\");\n        }\n\n        // Remove from source.\n        run(\"if [ -f $(echo {{release_path}}/$file) ]; then rm -rf {{release_path}}/$file; fi\");\n\n        // Ensure dir is available in release\n        run(\"if [ ! -d $(echo {{release_path}}/$dirname) ]; then mkdir -p {{release_path}}/$dirname;fi\");\n\n        // Touch shared\n        run(\"[ -f $sharedPath/$file ] || touch $sharedPath/$file\");\n\n        // Symlink shared dir to release dir\n        run(\"{{bin/symlink}} $sharedPath/$file {{release_path}}/$file\");\n    }\n});\n"
  },
  {
    "path": "recipe/deploy/symlink.php",
    "content": "<?php\n\nnamespace Deployer;\n\n// Use mv -T if available. Will check automatically.\nset('use_atomic_symlink', function () {\n    return commandSupportsOption('mv', '--no-target-directory');\n});\n\ndesc('Creates symlink to release');\ntask('deploy:symlink', function () {\n    if (get('use_atomic_symlink')) {\n        run(\"mv -T {{deploy_path}}/release {{current_path}}\");\n    } else {\n        // Atomic symlink does not supported.\n        // Will use simple two steps switch.\n\n        run(\"cd {{deploy_path}} && {{bin/symlink}} {{release_path}} {{current_path}}\"); // Atomic override symlink.\n        run(\"cd {{deploy_path}} && rm release\"); // Remove release link.\n    }\n});\n"
  },
  {
    "path": "recipe/deploy/update_code.php",
    "content": "<?php\n\nnamespace Deployer;\n\nuse Deployer\\Exception\\ConfigurationException;\nuse Symfony\\Component\\Console\\Input\\InputOption;\n\n/**\n * Determines which branch to deploy. Can be overridden with CLI option `--branch`.\n * If not specified, will get current git HEAD branch as default branch to deploy.\n */\nset('branch', 'HEAD');\n\noption('tag', null, InputOption::VALUE_REQUIRED, 'Tag to deploy');\noption('revision', null, InputOption::VALUE_REQUIRED, 'Revision to deploy');\noption('branch', null, InputOption::VALUE_REQUIRED, 'Branch to deploy');\n\n// The deploy target: a branch, a tag or a revision.\nset('target', function () {\n    $target = '';\n\n    $branch = get('branch');\n    if (!empty($branch)) {\n        $target = $branch;\n    }\n\n    // Override target from CLI options.\n    if (input()->hasOption('branch') && !empty(input()->getOption('branch'))) {\n        $target = input()->getOption('branch');\n    }\n    if (input()->hasOption('tag') && !empty(input()->getOption('tag'))) {\n        $target = input()->getOption('tag');\n    }\n    if (input()->hasOption('revision') && !empty(input()->getOption('revision'))) {\n        $target = input()->getOption('revision');\n    }\n\n    if (empty($target)) {\n        $target = \"HEAD\";\n    }\n    return $target;\n});\n\n// Sets deploy:update_code strategy.\n// Can be one of:\n// - local_archive (copies the repository from local machine)\n// - archive (default, fetches the code from the remote repository)\n// - clone (if you need the origin repository `.git` dir in your {{release_path}}, clones from remote repository)\nset('update_code_strategy', 'archive');\n\n// Sets environment variable _GIT_SSH_COMMAND_ for `git clone` command.\n// If `StrictHostKeyChecking` flag is set to `accept-new` then ssh will\n// automatically add new host keys to the user known hosts files, but\n// will not permit connections to hosts with changed host keys.\nset('git_ssh_command', 'ssh -o StrictHostKeyChecking=accept-new');\n\n/**\n * Specifies a sub directory within the repository to deploy.\n * Works only when [`update_code_strategy`](#update_code_strategy) is set to `archive` (default) or `local_archive`.\n *\n * Example:\n *  - set value to `src` if you want to deploy the folder that lives at `/src`.\n *  - set value to `src/api` if you want to deploy the folder that lives at `/src/api`.\n *\n * Note: do not use a leading `/`!\n */\nset('sub_directory', false);\n\n/**\n * Update code at {{release_path}} on host.\n */\ndesc('Updates code');\ntask('deploy:update_code', function () {\n    $strategy = get('update_code_strategy');\n    $target = get('target');\n    $git = get('bin/git');\n\n    $targetWithDir = $target;\n    if (!empty(get('sub_directory'))) {\n        $targetWithDir .= ':{{sub_directory}}';\n    }\n\n    if ($strategy === 'local_archive') {\n        $gitRoot = runLocally(\"$git rev-parse --show-toplevel\");\n        runLocally(\"$git -C \" . escapeshellarg($gitRoot) . \" archive $targetWithDir -o archive.tar\");\n        upload(\"$gitRoot/archive.tar\", '{{release_path}}/archive.tar');\n        run(\"tar -xf {{release_path}}/archive.tar -C {{release_path}}\");\n        run(\"rm {{release_path}}/archive.tar\");\n        unlink(\"$gitRoot/archive.tar\");\n\n        $rev = escapeshellarg(runLocally(\"git rev-list $target -1\"));\n    } else {\n        $repository = get('repository');\n\n        if (empty($repository)) {\n            throw new ConfigurationException(\"Missing 'repository' configuration.\");\n        }\n\n        $bare = parse('{{deploy_path}}/.dep/repo');\n        $env = [\n            'GIT_TERMINAL_PROMPT' => '0',\n            'GIT_SSH_COMMAND' => get('git_ssh_command'),\n        ];\n\n        start:\n        // Clone the repository to a bare repo.\n        run(\"[ -d $bare ] || mkdir -p $bare\");\n        run(\"[ -f $bare/HEAD ] || $git clone --mirror $repository $bare 2>&1\", env: $env);\n\n        cd($bare);\n\n        // If remote url changed, drop `.dep/repo` and reinstall.\n        if (run(\"$git config --get remote.origin.url\") !== $repository) {\n            cd('{{deploy_path}}');\n            run(\"rm -rf $bare\");\n            goto start;\n        }\n\n        run(\"$git remote update 2>&1\", env: $env);\n\n        // Copy to release_path.\n        if ($strategy === 'archive') {\n            run(\"$git archive $targetWithDir | tar -x -f - -C {{release_path}} 2>&1\");\n        } elseif ($strategy === 'clone') {\n            cd('{{release_path}}');\n            run(\"$git clone -l $bare .\");\n            run(\"$git remote set-url origin $repository\", env: $env);\n            run(\"$git checkout --force $target\");\n        } else {\n            throw new ConfigurationException(parse(\"Unknown `update_code_strategy` option: {{update_code_strategy}}.\"));\n        }\n\n        $rev = escapeshellarg(run(\"$git rev-list $target -1\"));\n    }\n\n    // Save git revision in REVISION file.\n    run(\"echo $rev > {{release_path}}/REVISION\");\n});\n"
  },
  {
    "path": "recipe/deploy/vendors.php",
    "content": "<?php\n\nnamespace Deployer;\n\nset('composer_action', 'install');\nset('composer_options', '--verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader');\nset('composer_version', null);\n\n// Returns Composer binary path if found. Otherwise, tries to install composer to `.dep/composer.phar`.\nset('bin/composer', function () {\n    if (test('[ -f {{deploy_path}}/.dep/composer.phar ]')) {\n        if (empty(get('composer_version')) || preg_match(parse('/Composer.*{{composer_version}}/'), run('{{bin/php}} {{deploy_path}}/.dep/composer.phar --version'))) {\n            return '{{bin/php}} {{deploy_path}}/.dep/composer.phar';\n        }\n    }\n\n    if (commandExist('composer')) {\n        if (empty(get('composer_version')) || preg_match(parse('/Composer.*{{composer_version}}/'), run('{{bin/php}} ' . which('composer') . ' --version'))) {\n            return '{{bin/php}} ' . which('composer');\n        }\n    }\n\n    $versionAsName = get('composer_version') ? ' {{composer_version}}' : '';\n    $versionAsOption = get('composer_version') ? ' -- --version={{composer_version}}' : '';\n    warning(\"Composer{$versionAsName} wasn't found. Installing to \\\"{{deploy_path}}/.dep/composer.phar\\\".\");\n    run(\"cd {{deploy_path}} && curl -sS https://getcomposer.org/installer | {{bin/php}}{$versionAsOption}\");\n    run('mv {{deploy_path}}/composer.phar {{deploy_path}}/.dep/composer.phar');\n    return '{{bin/php}} {{deploy_path}}/.dep/composer.phar';\n});\n\ndesc('Installs vendors');\ntask('deploy:vendors', function () {\n    if (!commandExist('unzip')) {\n        warning('To speed up composer installation setup \"unzip\" command with PHP zip extension.');\n    }\n    run('cd {{release_or_current_path}} && {{bin/composer}} {{composer_action}} {{composer_options}} 2>&1');\n});\n"
  },
  {
    "path": "recipe/deploy/writable.php",
    "content": "<?php\n\nnamespace Deployer;\n\n// Used to make a writable directory by a server.\n// Used in `chown` and `acl` modes of {{writable_mode}}.\n// Attempts automatically to detect http user in process list.\n\nset('http_user', function () {\n    $candidates = explode(\"\\n\", run(\"ps axo comm,user | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | sort | awk '{print \\$NF}' | uniq\"));\n    $httpUser = array_shift($candidates);\n\n    if (empty($httpUser)) {\n        throw new \\RuntimeException(\n            \"Can't detect http user name.\\n\" .\n            \"Please setup `http_user` config parameter.\",\n        );\n    }\n\n    return $httpUser;\n});\n\n// Used to make a writable directory by a server.\n// Used in `chgrp` mode of {{writable_mode}} only.\n// Attempts automatically to detect http user in process list.\nset('http_group', function () {\n    $candidates = explode(\"\\n\", run(\"ps axo comm,group | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | sort | awk '{print \\$NF}' | uniq\"));\n    $httpGroup = array_shift($candidates);\n\n    if (empty($httpGroup)) {\n        throw new \\RuntimeException(\n            \"Can't detect http user name.\\n\" .\n            \"Please setup `http_group` config parameter.\",\n        );\n    }\n\n    return $httpGroup;\n});\n\n// List of writable dirs.\nset('writable_dirs', []);\n\n// One of:\n// - chown\n// - chgrp\n// - chmod\n// - acl\n// - sticky\n// - skip\nset('writable_mode', 'acl');\n\n// Using sudo in writable commands?\nset('writable_use_sudo', false);\n\n// Use recursive mode (-R)?\nset('writable_recursive', false);\n\n// The chmod mode.\nset('writable_chmod_mode', '0755');\n\n// List of additional groups to give write permission to.\nset('writable_acl_groups', []);\n\n// Force ACLs to be reapplied even if they already exist. Useful when recursive ACLs need to reach new nested paths but sudo isn't available. Slower, so enable only to fix writable dir permissions.\nset('writable_acl_force', false);\n\ndesc('Makes writable dirs');\ntask('deploy:writable', function () {\n    $dirs = join(' ', get('writable_dirs'));\n    $mode = get('writable_mode');\n    $recursive = get('writable_recursive') ? '-R' : '';\n    $sudo = get('writable_use_sudo') ? 'sudo' : '';\n\n    if (empty($dirs)) {\n        return;\n    }\n    // Check that we don't have absolute path\n    if (strpos($dirs, ' /') !== false) {\n        throw new \\RuntimeException('Absolute path not allowed in config parameter `writable_dirs`.');\n    }\n\n    cd('{{release_or_current_path}}');\n\n    // Create directories if they don't exist\n    run(\"mkdir -p $dirs\");\n\n    if ($mode === 'chown') {\n        $httpUser = get('http_user');\n        // Change owner.\n        // -L   traverse every symbolic link to a directory encountered\n        run(\"$sudo chown -L $recursive $httpUser $dirs\");\n    } elseif ($mode === 'chgrp') {\n        // Change group ownership.\n        // -L    traverse every symbolic link to a directory encountered\n        run(\"$sudo chgrp -L $recursive {{http_group}} $dirs\");\n        run(\"$sudo chmod $recursive g+rwx $dirs\");\n    } elseif ($mode === 'chmod') {\n        run(\"$sudo chmod $recursive {{writable_chmod_mode}} $dirs\");\n    } elseif ($mode === 'acl') {\n        $remoteUser = get('remote_user', false);\n        if (empty($remoteUser)) {\n            $remoteUser = run('whoami');\n        }\n        $httpUser = get('http_user');\n        if (run(\"uname -s\") === 'Darwin') {\n            // macOS supports chmod +a for ACL management\n\n            run(\"$sudo chmod +a \\\"$httpUser allow delete,write,append,file_inherit,directory_inherit\\\" $dirs\");\n            run(\"$sudo chmod +a \\\"$remoteUser allow delete,write,append,file_inherit,directory_inherit\\\" $dirs\");\n        } elseif (commandExist('setfacl')) {\n            $setFaclUsers = \"-m u:\\\"$httpUser\\\":rwX\";\n            $setFaclGroups = \"\";\n            foreach (get(\"writable_acl_groups\") as $index => $group) {\n                if ($index > 0) {\n                    $setFaclGroups .= \" \";\n                }\n                $setFaclGroups .= \"-m g:\\\"$group\\\":rwX\";\n            }\n            // Check if remote user exists, before adding it to setfacl\n            $remoteUserExists = test(\"id -u $remoteUser &>/dev/null 2>&1 || exit 0\");\n            if ($remoteUserExists === true) {\n                $setFaclUsers .= \" -m u:$remoteUser:rwX\";\n            }\n            if (empty($sudo)) {\n                // When running without sudo, exception may be thrown\n                // if executing setfacl on files created by http user (in directory that has been setfacl before).\n                // These directories/files should be skipped unless forcing ACL reset.\n                // Now, we will check each directory for ACL and only setfacl for which has not been set before,\n                // unless writable_acl_force is enabled.\n                $writeableDirs = get('writable_dirs');\n                $forceAcl = get('writable_acl_force');\n                foreach ($writeableDirs as $dir) {\n                    // Check if ACL has been set or not\n                    $hasfacl = run(\"getfacl -p $dir | grep \\\"^user:$httpUser:.*w\\\" | wc -l\");\n                    // Set ACL for directory if it has not been set before or if forcing ACL reset\n                    if ($forceAcl || !$hasfacl) {\n                        run(\"setfacl -L $recursive $setFaclUsers $setFaclGroups $dir\");\n                        run(\"setfacl -dL $recursive $setFaclUsers $setFaclGroups $dir\");\n                    }\n                }\n            } else {\n                run(\"$sudo setfacl -L $recursive $setFaclUsers $setFaclGroups $dirs\");\n                run(\"$sudo setfacl -dL $recursive $setFaclUsers $setFaclGroups $dirs\");\n            }\n        } else {\n            $alias = currentHost()->getAlias();\n            throw new \\RuntimeException(\"Can't set writable dirs with ACL.\\nInstall ACL with next command:\\ndep run 'sudo apt-get install acl' -- $alias\");\n        }\n    } elseif ($mode === 'sticky') {\n        // Changes the group of the files, sets sticky bit to the directories\n        // and add the writable bit for all files\n        run(\"for dir in $dirs;\" .\n            'do ' .\n            'chgrp -L -R {{http_group}} ${dir}; ' .\n            'find ${dir} -type d -exec chmod g+rwxs \\{\\} \\;;' .\n            'find ${dir} -type f -exec chmod g+rw \\{\\} \\;;' .\n            'done');\n    } elseif ($mode === 'skip') {\n        // Does nothing, saves time if no changes are required for some environments\n        return;\n    } else {\n        throw new \\RuntimeException(\"Unknown writable_mode `$mode`.\");\n    }\n});\n"
  },
  {
    "path": "recipe/drupal7.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['drupal7']);\n\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:publish',\n]);\n\n//Set Drupal 7 site. Change if you use different site\nset('drupal_site', 'default');\n\n//Drupal 7 shared dirs\nset('shared_dirs', [\n    'sites/{{drupal_site}}/files',\n]);\n\n//Drupal 7 shared files\nset('shared_files', [\n    'sites/{{drupal_site}}/settings.php',\n]);\n\n//Drupal 7 writable dirs\nset('writable_dirs', [\n    'sites/{{drupal_site}}/files',\n]);\n\n\n//Create and upload Drupal 7 settings.php using values from secrets\ntask('drupal:settings', function () {\n    if (askConfirmation('Are you sure to generate and upload settings.php file?')) {\n\n        //Get template\n        $template = get('settings_template');\n\n        //Import secrets\n        $secrets = get('settings');\n\n        //Prepare replacement variables\n        $iterator = new \\RecursiveIteratorIterator(\n            new \\RecursiveArrayIterator($secrets),\n        );\n\n        $replacements = [];\n        foreach ($iterator as $key => $value) {\n            $keys = [];\n            for ($i = $iterator->getDepth(); $i > 0; $i--) {\n                $keys[] = $iterator->getSubIterator($i - 1)->key();\n            }\n            $keys[] = $key;\n\n            $replacements['{{' . implode('.', $keys) . '}}'] = $value;\n        }\n\n        //Create settings from template\n        $settings = file_get_contents($template);\n\n        $settings = strtr($settings, $replacements);\n\n        writeln('settings.php created successfully');\n\n        $tmpFilename = tempnam(sys_get_temp_dir(), 'tmp_settings_');\n        file_put_contents($tmpFilename, $settings);\n\n        upload($tmpFilename, '{{deploy_path}}/shared/sites/{{drupal_site}}/settings.php');\n\n        unlink($tmpFilename);\n    }\n});\n\n//Upload Drupal 7 files folder\ntask('drupal:upload_files', function () {\n    if (askConfirmation('Are you sure?')) {\n        upload('sites/{{drupal_site}}/files', '{{deploy_path}}/shared/sites/{{drupal_site}}/files');\n    }\n});\n"
  },
  {
    "path": "recipe/drupal8.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['drupal8']);\n\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:publish',\n]);\n\n//Set drupal site. Change if you use different site\nset('drupal_site', 'default');\n\n\n//Drupal 8 shared dirs\nset('shared_dirs', [\n    'sites/{{drupal_site}}/files',\n]);\n\n//Drupal 8 shared files\nset('shared_files', [\n    'sites/{{drupal_site}}/settings.php',\n    'sites/{{drupal_site}}/services.yml',\n]);\n\n//Drupal 8 Writable dirs\nset('writable_dirs', [\n    'sites/{{drupal_site}}/files',\n]);\n"
  },
  {
    "path": "recipe/flow_framework.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['flow_framework']);\n\n// Flow-Framework application-context\nset('flow_context', 'Production');\n\n// Flow-Framework cli-command\nset('flow_command', 'flow');\n\n// Flow-Framework shared directories\nset('shared_dirs', [\n    'Data/Persistent',\n    'Data/Logs',\n    'Configuration/{{flow_context}}',\n]);\n\n/**\n * Apply database migrations\n */\ndesc('Applies database migrations');\ntask('deploy:run_migrations', function () {\n    run('FLOW_CONTEXT={{flow_context}} {{bin/php}} {{release_or_current_path}}/{{flow_command}} doctrine:migrate');\n});\n\n/**\n * Publish resources\n */\ndesc('Publishes resources');\ntask('deploy:publish_resources', function () {\n    run('FLOW_CONTEXT={{flow_context}} {{bin/php}} {{release_or_current_path}}/{{flow_command}} resource:publish');\n});\n\n/**\n * Main task\n */\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'deploy:run_migrations',\n    'deploy:publish_resources',\n    'deploy:publish',\n]);\n"
  },
  {
    "path": "recipe/fuelphp.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['fuelphp']);\n\n// FuelPHP 1.x shared dirs\nset('shared_dirs', [\n    'fuel/app/cache', 'fuel/app/logs',\n]);\n\n/**\n * Main task\n */\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'deploy:publish',\n]);\n"
  },
  {
    "path": "recipe/joomla.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['joomla']);\n\nset('shared_files', ['configuration.php']);\nset('shared_dirs', ['images']);\nset('writable_dirs', ['images']);\n\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:publish',\n]);\n"
  },
  {
    "path": "recipe/laravel.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['laravel']);\n\nset('shared_dirs', ['storage']);\nset('shared_files', ['.env']);\nset('writable_dirs', [\n    'bootstrap/cache',\n    'storage',\n]);\nset('writable_recursive', true);\nset('log_files', 'storage/logs/*.log');\nset('bin/artisan', '{{release_or_current_path}}/artisan');\nset('laravel_version', function () {\n    $result = run(\"{{bin/php}} {{bin/artisan}} --version\");\n    preg_match_all('/(\\d+\\.?)+/', $result, $matches);\n    return $matches[0][0] ?? 5.5;\n});\nset('public_path', 'public');\n\n/**\n * Run an artisan command.\n *\n * Supported options:\n * - 'min' => #.#: The minimum Laravel version required (included).\n * - 'max' => #.#: The maximum Laravel version required (included).\n * - 'skipIfNoEnv': Skip and warn the user if `.env` file is inexistant or empty.\n * - 'failIfNoEnv': Fail the command if `.env` file is inexistant or empty.\n * - 'showOutput': Show the output of the command if given.\n *\n * @param string $command The artisan command (with cli options if any).\n * @param array $options The options that define the behaviour of the command.\n * @return callable A function that can be used as a task.\n */\nfunction artisan($command, $options = [])\n{\n    return function () use ($command, $options) {\n\n        // Ensure the artisan command is available on the current version.\n        $versionTooEarly = array_key_exists('min', $options)\n            && laravel_version_compare($options['min'], '<');\n\n        $versionTooLate = array_key_exists('max', $options)\n            && laravel_version_compare($options['max'], '>');\n\n        if ($versionTooEarly || $versionTooLate) {\n            return;\n        }\n\n        // Get the dotenv path or use default.\n        $dotenv = get('dotenv', '{{release_or_current_path}}/.env');\n\n        // Ensure we warn or fail when a command relies on the \".env\" file.\n        if (in_array('failIfNoEnv', $options) && !test(\"[ -s $dotenv ]\")) {\n            throw new \\Exception('Your .env file is empty! Cannot proceed.');\n        }\n\n        if (in_array('skipIfNoEnv', $options) && !test(\"[ -s $dotenv ]\")) {\n            warning(\"Your .env file is empty! Skipping...</>\");\n            return;\n        }\n\n        // Run the artisan command.\n        $output = run(\"{{bin/php}} {{bin/artisan}} $command\");\n\n        // Output the results when appropriate.\n        if (in_array('showOutput', $options)) {\n            writeln(\"<info>$output</info>\");\n        }\n    };\n}\n\nfunction laravel_version_compare($version, $comparator)\n{\n    return version_compare(get('laravel_version'), $version, $comparator);\n}\n\n/*\n * Maintenance mode.\n */\n\ndesc('Puts the application into maintenance / demo mode');\ntask('artisan:down', artisan('down', ['showOutput']));\n\ndesc('Brings the application out of maintenance mode');\ntask('artisan:up', artisan('up', ['showOutput']));\n\n/*\n * Generate keys.\n */\n\ndesc('Sets the application key');\ntask('artisan:key:generate', artisan('key:generate'));\n\ndesc('Creates the encryption keys for API authentication');\ntask('artisan:passport:keys', artisan('passport:keys'));\n\n/*\n * Database and migrations.\n */\n\ndesc('Seeds the database with records');\ntask('artisan:db:seed', artisan('db:seed --force', ['skipIfNoEnv', 'showOutput']));\n\ndesc('Runs the database migrations');\ntask('artisan:migrate', artisan('migrate --force', ['skipIfNoEnv']));\n\ndesc('Drops all tables and re-run all migrations');\ntask('artisan:migrate:fresh', artisan('migrate:fresh --force', ['skipIfNoEnv']));\n\ndesc('Rollbacks the last database migration');\ntask('artisan:migrate:rollback', artisan('migrate:rollback --force', ['skipIfNoEnv', 'showOutput']));\n\ndesc('Shows the status of each migration');\ntask('artisan:migrate:status', artisan('migrate:status', ['skipIfNoEnv', 'showOutput']));\n\n/*\n * Cache and optimizations.\n */\n\ndesc('Flushes the application cache');\ntask('artisan:cache:clear', artisan('cache:clear'));\n\ndesc('Creates a cache file for faster configuration loading');\ntask('artisan:config:cache', artisan('config:cache'));\n\ndesc('Removes the configuration cache file');\ntask('artisan:config:clear', artisan('config:clear'));\n\ndesc('Discovers and cache the application\\'s events and listeners');\ntask('artisan:event:cache', artisan('event:cache', ['min' => '5.8.9']));\n\ndesc('Clears all cached events and listeners');\ntask('artisan:event:clear', artisan('event:clear', ['min' => '5.8.9']));\n\ndesc('Lists the application\\'s events and listeners');\ntask('artisan:event:list', artisan('event:list', ['showOutput', 'min' => '5.8.9']));\n\ndesc('Cache the framework bootstrap files');\ntask('artisan:optimize', artisan('optimize'));\n\ndesc('Removes the cached bootstrap files');\ntask('artisan:optimize:clear', artisan('optimize:clear'));\n\ndesc('Reload running services');\ntask('artisan:reload', artisan('reload', ['min' => '12.41']));\n\ndesc('Creates a route cache file for faster route registration');\ntask('artisan:route:cache', artisan('route:cache'));\n\ndesc('Removes the route cache file');\ntask('artisan:route:clear', artisan('route:clear'));\n\ndesc('Lists all registered routes');\ntask('artisan:route:list', artisan('route:list', ['showOutput']));\n\ndesc('Creates the symbolic links configured for the application');\ntask('artisan:storage:link', artisan('storage:link', ['min' => 5.3]));\n\ndesc('Compiles all of the application\\'s Blade templates');\ntask('artisan:view:cache', artisan('view:cache', ['min' => 5.6]));\n\ndesc('Clears all compiled view files');\ntask('artisan:view:clear', artisan('view:clear'));\n\n/**\n * Queue and Horizon.\n */\n\ndesc('Lists all of the failed queue jobs');\ntask('artisan:queue:failed', artisan('queue:failed', ['showOutput']));\n\ndesc('Flushes all of the failed queue jobs');\ntask('artisan:queue:flush', artisan('queue:flush'));\n\ndesc('Restarts queue worker daemons after their current job');\ntask('artisan:queue:restart', artisan('queue:restart'));\n\ndesc('Starts a master supervisor in the foreground');\ntask('artisan:horizon', artisan('horizon'));\n\ndesc('Deletes all of the jobs from the specified queue');\ntask('artisan:horizon:clear', artisan('horizon:clear --force'));\n\ndesc('Instructs the master supervisor to continue processing jobs');\ntask('artisan:horizon:continue', artisan('horizon:continue'));\n\ndesc('Lists all of the deployed machines');\ntask('artisan:horizon:list', artisan('horizon:list', ['showOutput']));\n\ndesc('Pauses the master supervisor');\ntask('artisan:horizon:pause', artisan('horizon:pause'));\n\ndesc('Terminates any rogue Horizon processes');\ntask('artisan:horizon:purge', artisan('horizon:purge'));\n\ndesc('Gets the current status of Horizon');\ntask('artisan:horizon:status', artisan('horizon:status', ['showOutput']));\n\ndesc('Terminates the master supervisor so it can be restarted');\ntask('artisan:horizon:terminate', artisan('horizon:terminate'));\n\ndesc('Publish all of the Horizon resources');\ntask('artisan:horizon:publish', artisan('horizon:publish'));\n\ndesc('Lists all of the supervisors');\ntask('artisan:horizon:supervisors', artisan('horizon:supervisors', ['showOutput']));\n\ndesc('Deletes metrics for all jobs and queues');\ntask('artisan:horizon:clear-metrics', artisan('horizon:clear-metrics'));\n\ndesc('Stores a snapshot of the queue metrics');\ntask('artisan:horizon:snapshot', artisan('horizon:snapshot'));\n\n/*\n * Scheduler.\n */\n\ndesc('Interrupt in-progress schedule:run invocations');\ntask('artisan:schedule:interrupt', artisan('schedule:interrupt'));\n\n/*\n * Telescope.\n */\n\ndesc('Clears all entries from Telescope');\ntask('artisan:telescope:clear', artisan('telescope:clear'));\n\ndesc('Prunes stale entries from the Telescope database');\ntask('artisan:telescope:prune', artisan('telescope:prune'));\n\n/*\n * Octane.\n */\n\ndesc('Starts the octane server');\ntask('artisan:octane', artisan('octane:start'));\n\ndesc('Reloads the octane server');\ntask('artisan:octane:reload', artisan('octane:reload'));\n\ndesc('Stops the octane server');\ntask('artisan:octane:stop', artisan('octane:stop'));\n\ndesc('Check the status of the octane server');\ntask('artisan:octane:status', artisan('octane:status'));\n\n/*\n * Nova.\n */\n\ndesc('Publish all of the Laravel Nova resources');\ntask('artisan:nova:publish', artisan('nova:publish'));\n\n/*\n * Reverb.\n */\n\ndesc('Starts the Reverb server');\ntask('artisan:reverb:start', artisan('reverb:start'));\n\ndesc('Restarts the Reverb server');\ntask('artisan:reverb:restart', artisan('reverb:restart'));\n\n/*\n * Pulse.\n */\n\ndesc('Starts the Pulse server');\ntask('artisan:pulse:check', artisan('pulse:check'));\n\ndesc('Restarts the Pulse server');\ntask('artisan:pulse:restart', artisan('pulse:restart'));\n\ndesc('Purges all Pulse data from storage');\ntask('artisan:pulse:purge', artisan('pulse:purge'));\n\ndesc('Process incoming Pulse data from the ingest stream');\ntask('artisan:pulse:work', artisan('pulse:work'));\n\n/**\n * Main deploy task.\n */\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'artisan:storage:link',\n    'artisan:optimize',\n    'artisan:migrate',\n    'deploy:publish',\n    'artisan:reload',\n]);\n"
  },
  {
    "path": "recipe/magento.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['magento']);\n\n/**\n * Magento Configuration\n */\n\n// Magento shared dirs\nset('shared_dirs', ['var', 'media']);\n\n// Magento shared files\nset('shared_files', ['app/etc/local.xml']);\n\n// Magento writable dirs\nset('writable_dirs', ['var', 'media']);\n\n/**\n * Clear cache\n */\ndesc('Clears cache');\ntask('deploy:cache:clear', function () {\n    run(\"cd {{release_or_current_path}} && php -r \\\"require_once 'app/Mage.php'; umask(0); Mage::app()->cleanCache();\\\"\");\n});\n\n/**\n * Remove files that can be used to compromise Magento\n */\ntask('deploy:clear_version', function () {\n    run(\"rm -f {{release_or_current_path}}/LICENSE.html\");\n    run(\"rm -f {{release_or_current_path}}/LICENSE.txt\");\n    run(\"rm -f {{release_or_current_path}}/LICENSE_AFL.txt\");\n    run(\"rm -f {{release_or_current_path}}/RELEASE_NOTES.txt\");\n})->hidden();\n\nafter('deploy:update_code', 'deploy:clear_version');\n\n\n/**\n * Main task\n */\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:cache:clear',\n    'deploy:publish',\n]);\n"
  },
  {
    "path": "recipe/magento2.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\nrequire_once __DIR__ . '/../contrib/cachetool.php';\n\nuse Deployer\\Exception\\ConfigurationException;\nuse Deployer\\Exception\\GracefulShutdownException;\nuse Deployer\\Exception\\RunException;\nuse Deployer\\Host\\Host;\n\nconst CONFIG_IMPORT_NEEDED_EXIT_CODE = 2;\nconst DB_UPDATE_NEEDED_EXIT_CODE = 2;\nconst MAINTENANCE_MODE_ACTIVE_OUTPUT_MSG = 'maintenance mode is active';\nconst ENV_CONFIG_FILE_PATH = 'app/etc/env.php';\nconst TMP_ENV_CONFIG_FILE_PATH = 'app/etc/env_tmp.php';\n\nadd('recipes', ['magento2']);\n\n// Configuration\n\n// By default setup:static-content:deploy uses `en_US`.\n// To change that, simply put `set('static_content_locales', 'en_US de_DE');`\n// in you deployer script.\nset('static_content_locales', 'en_US');\n\n// Configuration\n\n// You can also set the themes to run against. By default it'll deploy\n// all themes - `add('magento_themes', ['Magento/luma', 'Magento/backend']);`\n// If the themes are set as a simple list of strings, then all languages defined in {{static_content_locales}} are\n// compiled for the given themes.\n// Alternatively The themes can be defined as an associative array, where the key represents the theme name and\n// the key contains the languages for the compilation (for this specific theme)\n// Example:\n// set('magento_themes', ['Magento/luma']); - Will compile this theme with every language from {{static_content_locales}}\n// set('magento_themes', [\n//     'Magento/luma'   => null,                              - Will compile all languages from {{static_content_locales}} for Magento/luma\n//     'Custom/theme'   => 'en_US fr_FR'                      - Will compile only en_US and fr_FR for Custom/theme\n//     'Custom/another' => '{{static_content_locales}} it_IT' - Will compile all languages from {{static_content_locales}} + it_IT for Custom/another\n// ]); - Will compile this theme with every language\nset('magento_themes', [\n\n]);\n\n// Static content deployment options, e.g. '--no-parent'\nset('static_deploy_options', '');\n\n// Deploy frontend and adminhtml together as default\nset('split_static_deployment', false);\n\n// Use the default languages for the backend as default\nset('static_content_locales_backend', '{{static_content_locales}}');\n\n// backend themes to deploy. Only used if split_static_deployment=true\n// This setting supports the same options/structure as {{magento_themes}}\nset('magento_themes_backend', ['Magento/backend' => null]);\n\n// Configuration\n\n// Also set the number of concurrent jobs to run. The default is 1\n// Update using: `set('static_content_jobs', '1');`\nset('static_content_jobs', '1');\n\nset('content_version', function () {\n    return time();\n});\n\n// Magento directory relative to repository root. Use \".\" (default) if it is not located in a subdirectory\nset('magento_dir', '.');\n\n\nset('shared_files', [\n    '{{magento_dir}}/app/etc/env.php',\n    '{{magento_dir}}/var/.maintenance.ip',\n]);\nset('shared_dirs', [\n    '{{magento_dir}}/var/composer_home',\n    '{{magento_dir}}/var/log',\n    '{{magento_dir}}/var/export',\n    '{{magento_dir}}/var/report',\n    '{{magento_dir}}/var/import',\n    '{{magento_dir}}/var/import_history',\n    '{{magento_dir}}/var/session',\n    '{{magento_dir}}/var/importexport',\n    '{{magento_dir}}/var/backups',\n    '{{magento_dir}}/var/tmp',\n    '{{magento_dir}}/pub/sitemap',\n    '{{magento_dir}}/pub/media',\n    '{{magento_dir}}/pub/static/_cache',\n]);\nset('writable_dirs', [\n    '{{magento_dir}}/var',\n    '{{magento_dir}}/pub/static',\n    '{{magento_dir}}/pub/media',\n    '{{magento_dir}}/generated',\n    '{{magento_dir}}/var/page_cache',\n]);\nset('clear_paths', [\n    '{{magento_dir}}/generated/*',\n    '{{magento_dir}}/pub/static/_cache/*',\n    '{{magento_dir}}/var/generation/*',\n    '{{magento_dir}}/var/cache/*',\n    '{{magento_dir}}/var/page_cache/*',\n    '{{magento_dir}}/var/view_preprocessed/*',\n]);\n\nset('bin/magento', '{{release_or_current_path}}/{{magento_dir}}/bin/magento');\n\nset('magento_version', function () {\n    // detect version\n    $versionOutput = run('{{bin/php}} {{bin/magento}} --version');\n    preg_match('/(\\d+\\.?)+(-p\\d+)?$/', $versionOutput, $matches);\n    return $matches[0] ?? '2.0';\n});\n\nset('config_import_needed', function () {\n    // detect if app:config:import is needed\n    try {\n        run('{{bin/php}} {{bin/magento}} app:config:status');\n    } catch (RunException $e) {\n        if ($e->getExitCode() == CONFIG_IMPORT_NEEDED_EXIT_CODE) {\n            return true;\n        }\n\n        throw $e;\n    }\n    return false;\n});\n\nset('database_upgrade_needed', function () {\n    // detect if db upgrade is needed\n    try {\n        run('{{bin/php}} {{bin/magento}} setup:db:status');\n    } catch (RunException $e) {\n        if ($e->getExitCode() == DB_UPDATE_NEEDED_EXIT_CODE) {\n            return true;\n        }\n\n        throw $e;\n    }\n\n    return false;\n});\n\nset('full_upgrade_needed', function () {\n    //Some conditions, such as new RabittMQ services require a full upgrade and are not detecet by setup:db:status\n    //TODO: Add checks, once implemented, for detecting necessary full upgrade process. See future RabbitMQ Check: https://github.com/magento/magento2/pull/39698\n    return false;\n});\n\nset('upgrade_needed', function () {\n    // Detect necessary upgrade, partial db or full upgrade\n    try {\n        return get('database_upgrade_needed') || get('full_upgrade_needed');\n    } catch (RunException $e) {\n        throw $e;\n    }\n});\n\n// Deploy without setting maintenance mode if possible\nset('enable_zerodowntime', true);\n\n// Tasks\n\n// To work correctly with artifact deployment, it is necessary to set the MAGE_MODE correctly in `app/etc/config.php`\n// e.g.\n// ```php\n// 'MAGE_MODE' => 'production'\n// ```\ndesc('Compiles magento di');\ntask('magento:compile', function () {\n    run(\"{{bin/php}} {{bin/magento}} setup:di:compile\");\n    run('cd {{release_or_current_path}}/{{magento_dir}} && {{bin/composer}} dump-autoload -o');\n});\n\n// To work correctly with artifact deployment it is necessary to set `system/dev/js` , `system/dev/css` and `system/dev/template`\n// in `app/etc/config.php`, e.g.:\n// ```php\n// 'system' => [\n//     'default' => [\n//         'dev' => [\n//             'js' => [\n//                 'merge_files' => '1',\n//                 'minify_files' => '1'\n//             ],\n//             'css' => [\n//                 'merge_files' => '1',\n//                 'minify_files' => '1'\n//             ],\n//             'template' => [\n//                 'minify_html' => '1'\n//             ]\n//         ]\n//     ]\n// ```\ndesc('Deploys assets');\ntask('magento:deploy:assets', function () {\n    $themesToCompile = '';\n    if (get('split_static_deployment')) {\n        invoke('magento:deploy:assets:adminhtml');\n        invoke('magento:deploy:assets:frontend');\n    } else {\n        if (count(get('magento_themes')) > 0) {\n            $themes = array_is_list(get('magento_themes')) ? get('magento_themes') : array_keys(get('magento_themes'));\n            foreach ($themes as $theme) {\n                $themesToCompile .= ' -t ' . $theme;\n            }\n        }\n        run(\"{{bin/php}} {{bin/magento}} setup:static-content:deploy -f --content-version={{content_version}} {{static_deploy_options}} {{static_content_locales}} $themesToCompile -j {{static_content_jobs}}\");\n    }\n});\n\ndesc('Deploys assets for backend only');\ntask('magento:deploy:assets:adminhtml', function () {\n    magentoDeployAssetsSplit('backend');\n});\n\ndesc('Deploys assets for frontend only');\ntask('magento:deploy:assets:frontend', function () {\n    magentoDeployAssetsSplit('frontend');\n});\n\n/**\n * @phpstan-param 'frontend'|'backend' $area\n *\n * @throws ConfigurationException\n */\nfunction magentoDeployAssetsSplit(string $area)\n{\n    if (!in_array($area, ['frontend', 'backend'], true)) {\n        throw new ConfigurationException(\"\\$area must be either 'frontend' or 'backend', '$area' given\");\n    }\n\n    $maxProcesses = get('static_content_jobs');\n\n    $isFrontend = $area === 'frontend';\n    $suffix = $isFrontend\n        ? ''\n        : '_backend';\n\n    $themesConfig = get(\"magento_themes$suffix\");\n    $defaultLanguages = get(\"static_content_locales$suffix\");\n    $useDefaultLanguages = array_is_list($themesConfig);\n\n    /** @var list<string> $themes */\n    $themes = $useDefaultLanguages\n        ? array_values($themesConfig)\n        : array_keys($themesConfig);\n\n    $staticContentArea = $isFrontend\n        ? 'frontend'\n        : 'adminhtml';\n\n    // group themes by their languages to minimize number of static content deploy commands, and allow parallel jobs\n    $themeGroups = [];\n    if ($useDefaultLanguages) {\n        $themeGroups[$defaultLanguages] = $themes;\n    } else {\n        foreach ($themes as $theme) {\n            $locales = array_unique(array_filter(explode(' ', parse($themesConfig[$theme] ?? $defaultLanguages))));\n            sort($locales); // sort locales to ensure consistent grouping\n            $localesSorted = implode(' ', $locales);\n            $themeGroups[$localesSorted][] = $theme;\n        }\n    }\n\n    foreach ($themeGroups as $locales => $themes) {\n        $localeCount = substr_count($locales, ' ') + 1;\n        // how many themes can we process in parallel?\n        $maxConcurrentThemes = max(1, floor($maxProcesses / $localeCount));\n        // WARNING: when static_content_jobs>1, and it's doing more than 1 theme-locale per process, it can get stuck - so we do 1batch at a time\n        do {\n            $chunk = array_splice($themes, 0, $maxConcurrentThemes);\n            $themeArgs = '-t ' . implode(' -t ', $chunk);\n            run(\"{{bin/php}} {{bin/magento}} setup:static-content:deploy -f --area=$staticContentArea --content-version={{content_version}} {{static_deploy_options}} $locales $themeArgs -j {{static_content_jobs}}\");\n        } while (!empty($themes));\n    }\n\n}\n\ndesc('Syncs content version');\ntask('magento:sync:content_version', function () {\n    $timestamp = time();\n    on(select('all'), function (Host $host) use ($timestamp) {\n        $host->set('content_version', $timestamp);\n    });\n})->once();\n\nbefore('magento:deploy:assets', 'magento:sync:content_version');\n\ndesc('Enables maintenance mode');\ntask('magento:maintenance:enable', function () {\n    // do not use {{bin/magento}} because it would be in \"release\" but the maintenance mode must be set in \"current\"\n    run(\"if [ -d $(echo {{current_path}}) ]; then {{bin/php}} {{current_path}}/{{magento_dir}}/bin/magento maintenance:enable; fi\");\n});\n\ndesc('Disables maintenance mode');\ntask('magento:maintenance:disable', function () {\n    // do not use {{bin/magento}} because it would be in \"release\" but the maintenance mode must be set in \"current\"\n    run(\"if [ -d $(echo {{current_path}}) ]; then {{bin/php}} {{current_path}}/{{magento_dir}}/bin/magento maintenance:disable; fi\");\n});\n\ndesc('Set maintenance mode if needed');\ntask('magento:maintenance:enable-if-needed', function () {\n    ! get('enable_zerodowntime') || get('upgrade_needed') || get('config_import_needed') ?\n        invoke('magento:maintenance:enable') :\n        writeln('Config and database up to date => no maintenance mode');\n});\n\ndesc('Config Import');\ntask('magento:config:import', function () {\n    if (get('config_import_needed')) {\n        run('{{bin/php}} {{bin/magento}} app:config:import --no-interaction');\n    } else {\n        writeln('App config is up to date => import skipped');\n    }\n});\n\ndesc('Upgrades magento database');\ntask('magento:upgrade:db', function () {\n    if (get('database_upgrade_needed')) {\n        // clear config cache, so there is no error when a new MQ topic is introduced\n        run(\"{{bin/php}} {{bin/magento}} cache:clean config\");\n        run(\"{{bin/php}} {{bin/magento}} setup:db-schema:upgrade --no-interaction\");\n        run(\"{{bin/php}} {{bin/magento}} setup:db-data:upgrade --no-interaction\");\n    } else {\n        writeln('Database schema is up to date => upgrade skipped');\n    }\n});\n\ndesc('Run upgrades if needed');\ntask('magento:upgrade', function () {\n    if (get('full_upgrade_needed')) {\n        run(\"{{bin/php}} {{bin/magento}} setup:upgrade --keep-generated\");\n    } elseif (get('database_upgrade_needed')) {\n        invoke('magento:upgrade:db');\n    }\n})->once();\n\ndesc('Flushes Magento Cache');\ntask('magento:cache:flush', function () {\n    run(\"{{bin/php}} {{bin/magento}} cache:flush\");\n});\n\ndesc('Magento2 deployment operations');\ntask('deploy:magento', [\n    'magento:build',\n    'magento:maintenance:enable-if-needed',\n    'magento:config:import',\n    'magento:upgrade',\n    'magento:maintenance:disable',\n]);\n\ndesc('Magento2 build operations');\ntask('magento:build', [\n    'magento:compile',\n    'magento:deploy:assets',\n]);\n\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'deploy:clear_paths',\n    'deploy:magento',\n    'deploy:publish',\n]);\n\nafter('deploy:symlink', 'magento:cache:flush');\n\nafter('deploy:failed', 'magento:maintenance:disable');\n\n// Artifact deployment section\n\n// The file the artifact is saved to\nset('artifact_file', 'artifact.tar.gz');\n\n// The directory the artifact is saved in\nset('artifact_dir', 'artifacts');\n\n// Points to a file with a list of files to exclude from packaging.\n// The format is as with the `tar --exclude-from=[file]` option\nset('artifact_excludes_file', 'artifacts/excludes');\n\n// If set to true, the artifact is built from a clean copy of the project repository instead of the current working directory\nset('build_from_repo', false);\n\n// Set this value if \"build_from_repo\" is set to true. The target to deploy must also be set with \"--branch\", \"--tag\" or \"--revision\"\nset('repository', null);\n\n// The relative path to the artifact file. If the directory does not exist, it will be created\nset('artifact_path', function () {\n    if (!testLocally('[ -d {{artifact_dir}} ]')) {\n        runLocally('mkdir -p {{artifact_dir}}');\n    }\n    return get('artifact_dir') . '/' . get('artifact_file');\n});\n\n// The location of the tar command. On MacOS you should have installed gtar, as it supports the required settings\nset('bin/tar', function () {\n    if (commandExist('gtar')) {\n        return which('gtar');\n    } else {\n        return which('tar');\n    }\n});\n\n// tasks section\n\ndesc('Packages all relevant files in an artifact.');\ntask('artifact:package', function () {\n    if (!test('[ -f {{artifact_excludes_file}} ]')) {\n        throw new GracefulShutdownException(\n            \"No artifact excludes file provided, provide one at artifacts/excludes or change location\",\n        );\n    }\n    run('{{bin/tar}} --exclude-from={{artifact_excludes_file}} -czf {{artifact_path}} -C {{release_or_current_path}} .');\n});\n\ndesc('Uploads artifact in release folder for extraction.');\ntask('artifact:upload', function () {\n    upload(get('artifact_path'), '{{release_path}}');\n});\n\ndesc('Extracts artifact in release path.');\ntask('artifact:extract', function () {\n    run('{{bin/tar}} -xzpf {{release_path}}/{{artifact_file}} -C {{release_path}}');\n    run('rm -rf {{release_path}}/{{artifact_file}}');\n});\n\ndesc('Clears generated files prior to building.');\ntask('build:remove-generated', function () {\n    run('rm -rf generated/*');\n});\n\ndesc('Prepare local artifact build');\ntask('build:prepare', function () {\n    if (!currentHost()->get('local')) {\n        throw new GracefulShutdownException('Artifact can only be built locally, you provided a non local host');\n    }\n\n    $buildDir = get('build_from_repo') ? get('artifact_dir') . '/repo' : '.';\n    set('deploy_path', $buildDir);\n    set('release_path', $buildDir);\n    set('current_path', $buildDir);\n\n    if (!get('build_from_repo')) {\n        return;\n    }\n\n    $repository = (string) get('repository');\n    if ($repository === '') {\n        throw new GracefulShutdownException('You must specify the \"repository\" option.');\n    }\n\n    run('rm -rf {{release_or_current_path}}');\n    run('git clone {{repository}} {{release_or_current_path}}');\n    run('git -C {{release_or_current_path}} checkout --force {{target}}');\n});\n\ndesc('Builds an artifact.');\ntask('artifact:build', [\n    'build:prepare',\n    'build:remove-generated',\n    'deploy:vendors',\n    'magento:compile',\n    'magento:deploy:assets',\n    'artifact:package',\n]);\n\n// Array of shared files that will be added to the default shared_files without overriding\nset('additional_shared_files', []);\n// Array of shared directories that will be added to the default shared_dirs without overriding\nset('additional_shared_dirs', []);\n\n\ndesc('Adds additional files and dirs to the list of shared files and dirs');\ntask('deploy:additional-shared', function () {\n    add('shared_files', get('additional_shared_files'));\n    add('shared_dirs', get('additional_shared_dirs'));\n});\n\n/**\n * Update cache id_prefix on deploy so that you are compiling against a fresh cache\n * Reference Issue: https://github.com/davidalger/capistrano-magento2/issues/151\n * To use this feature, add the following to your deployer scripts:\n * ```php\n * after('deploy:shared', 'magento:set_cache_prefix');\n * after('deploy:magento', 'magento:cleanup_cache_prefix');\n * ```\n **/\ndesc('Update cache id_prefix');\ntask('magento:set_cache_prefix', function () {\n    //download current env config\n    $tmpConfigFile = tempnam(sys_get_temp_dir(), 'deployer_config');\n    download('{{deploy_path}}/shared/' . ENV_CONFIG_FILE_PATH, $tmpConfigFile);\n    $envConfigArray = include($tmpConfigFile);\n    //set prefix to `alias_releasename_`\n    $prefixUpdate = get('alias') . '_' . get('release_name') . '_';\n\n    //check for preload keys and update\n    if (isset($envConfigArray['cache']['frontend']['default']['backend_options']['preload_keys'])) {\n        $oldPrefix = $envConfigArray['cache']['frontend']['default']['id_prefix'];\n        $preloadKeys = $envConfigArray['cache']['frontend']['default']['backend_options']['preload_keys'];\n        $newPreloadKeys = [];\n        foreach ($preloadKeys as $preloadKey) {\n            $newPreloadKeys[] = preg_replace('/^' . $oldPrefix . '/', $prefixUpdate, $preloadKey);\n        }\n        $envConfigArray['cache']['frontend']['default']['backend_options']['preload_keys'] = $newPreloadKeys;\n    }\n\n    //update id_prefix to include release name\n    $envConfigArray['cache']['frontend']['default']['id_prefix'] = $prefixUpdate;\n    $envConfigArray['cache']['frontend']['page_cache']['id_prefix'] = $prefixUpdate;\n\n    //Generate configuration array as string\n    $envConfigStr = '<?php return ' . var_export($envConfigArray, true) . ';';\n    file_put_contents($tmpConfigFile, $envConfigStr);\n    //upload updated config to server\n    upload($tmpConfigFile, '{{deploy_path}}/shared/' . TMP_ENV_CONFIG_FILE_PATH);\n    //cleanup tmp file\n    unlink($tmpConfigFile);\n    //delete the symlink for env.php\n    run('rm {{release_or_current_path}}/' . ENV_CONFIG_FILE_PATH);\n    //link the env to the tmp version\n    run('{{bin/symlink}} {{deploy_path}}/shared/' . TMP_ENV_CONFIG_FILE_PATH . ' {{release_path}}/' . ENV_CONFIG_FILE_PATH);\n});\n\n/**\n * After successful deployment, move the tmp_env.php file to env.php ready for next deployment\n */\ndesc('Cleanup cache id_prefix env files');\ntask('magento:cleanup_cache_prefix', function () {\n    run('rm {{deploy_path}}/shared/' . ENV_CONFIG_FILE_PATH);\n    run('rm {{release_or_current_path}}/' . ENV_CONFIG_FILE_PATH);\n    run('mv {{deploy_path}}/shared/' . TMP_ENV_CONFIG_FILE_PATH . ' {{deploy_path}}/shared/' . ENV_CONFIG_FILE_PATH);\n    // Symlink shared dir to release dir\n    run('{{bin/symlink}} {{deploy_path}}/shared/' . ENV_CONFIG_FILE_PATH . ' {{release_path}}/' . ENV_CONFIG_FILE_PATH);\n});\n\n/**\n * Remove cron from crontab and kill running cron jobs\n * To use this feature, add the following to your deployer scripts:\n *  ```php\n *  after('magento:maintenance:enable-if-needed', 'magento:cron:stop');\n *  ```\n */\ndesc('Remove cron from crontab and kill running cron jobs');\ntask('magento:cron:stop', function () {\n    if (has('previous_release')) {\n        run('{{bin/php}} {{previous_release}}/{{magento_dir}}/bin/magento cron:remove');\n    }\n\n    run('pgrep -U \"$(id -u)\" -f \"bin/magento +(cron:run|queue:consumers:start)\" | xargs -r kill');\n});\n\n/**\n * Install cron in crontab\n * To use this feature, add the following to your deployer scripts:\n *   ```php\n *   after('magento:upgrade', 'magento:cron:install');\n *   ```\n */\ndesc('Install cron in crontab');\ntask('magento:cron:install', function () {\n    run('cd {{release_or_current_path}}');\n    run('{{bin/php}} {{bin/magento}} cron:install');\n});\n\ndesc('Prepares an artifact on the target server');\ntask('artifact:prepare', [\n    'deploy:info',\n    'deploy:setup',\n    'deploy:lock',\n    'deploy:release',\n    'artifact:upload',\n    'artifact:extract',\n    'deploy:additional-shared',\n    'deploy:shared',\n    'deploy:writable',\n]);\n\ndesc('Executes the tasks after artifact is released');\ntask('artifact:finish', [\n    'magento:cache:flush',\n    'cachetool:clear:opcache',\n    'deploy:cleanup',\n    'deploy:unlock',\n    'deploy:success',\n]);\n\ndesc('Actually releases the artifact deployment');\ntask('artifact:deploy', [\n    'artifact:prepare',\n    'magento:maintenance:enable-if-needed',\n    'magento:config:import',\n    'magento:upgrade',\n    'magento:maintenance:disable',\n    'deploy:symlink',\n    'artifact:finish',\n]);\n\nfail('artifact:deploy', 'deploy:failed');\n"
  },
  {
    "path": "recipe/pimcore.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/symfony.php';\n\nadd('recipes', ['pimcore']);\n\nadd('shared_dirs', ['public/var', 'var/email', 'var/recyclebin', 'var/versions']);\n\nadd('shared_files', ['config/local/database.yaml']);\n\nadd('writable_dirs', ['public/var', 'var/cache/dev']);\n\ndesc('Rebuilds Pimcore Classes');\ntask('pimcore:rebuild-classes', function () {\n    run('{{bin/console}} pimcore:build:classes');\n    run('{{bin/console}} pimcore:deployment:classes-rebuild --create-classes --delete-classes --no-interaction');\n});\n\ndesc('Removes cache');\ntask('pimcore:cache_clear', function () {\n    run('rm -rf {{release_or_current_path}}/var/cache/dev/*');\n});\n\ntask('pimcore:deploy', [\n    'pimcore:rebuild-classes',\n    'pimcore:cache_clear',\n]);\n\nafter('deploy:vendors', 'pimcore:deploy');\n"
  },
  {
    "path": "recipe/prestashop.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['prestashop']);\n\nset('shared_files', [\n    'config/settings.inc.php',\n    '.htaccess',\n]);\nset('shared_dirs', [\n    'img',\n    'log',\n    'download',\n    'upload',\n    'translations',\n    'mails',\n    'themes/default-bootstrap/lang',\n    'themes/default-bootstrap/mails',\n    'themes/default-bootstrap/pdf/lang',\n]);\nset('writable_dirs', [\n    'img',\n    'log',\n    'cache',\n    'download',\n    'upload',\n    'translations',\n    'mails',\n    'themes/default-bootstrap/lang',\n    'themes/default-bootstrap/mails',\n    'themes/default-bootstrap/pdf/lang',\n    'themes/default-bootstrap/cache',\n]);\n\ndesc('Deploys your project');\ntask(\n    'deploy',\n    [\n        'deploy:prepare',\n        'deploy:vendors',\n        'deploy:publish',\n    ],\n);\n"
  },
  {
    "path": "recipe/provision/404.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>404 Not Found</title>\n    <style>\n      body {\n        -moz-osx-font-smoothing: grayscale;\n        -webkit-font-smoothing: antialiased;\n        align-content: center;\n        background: #343434;\n        color: #fff;\n        display: grid;\n        font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n        font-size: 20px;\n        justify-content: center;\n        margin: 0;\n        min-height: 100vh;\n      }\n\n      main {\n        padding: 0 30px;\n      }\n\n      svg {\n        animation: 2s ease-in-out infinite hover;\n      }\n\n      @keyframes hover {\n        0%, 100% {\n          transform: translateY(0)\n        }\n        50% {\n          transform: translateY(-8px)\n        }\n      }\n    </style>\n</head>\n<body>\n<main>\n    <svg width=\"68\" viewBox=\"0 0 48 40\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path\n            d=\"M41.8253 0.269788C44.8519 -0.884712 47.9239 1.84927 47.1284 4.98944L39.3219 35.8043C38.615 38.5948 35.2805 39.7475 33.0012 37.9892L19.9291 27.905C17.8765 26.3215 17.8492 23.2338 19.8735 21.6144L35.3856 9.20469C36.2481 8.51467 37.5067 8.65451 38.1967 9.51704C38.8868 10.3796 38.7469 11.6381 37.8844 12.3282L22.8693 24.3402C22.6163 24.5427 22.6197 24.9286 22.8762 25.1266L34.8414 34.3568C35.1263 34.5766 35.5431 34.4325 35.6315 34.0837L43.0145 4.94009C43.1139 4.54757 42.7299 4.20583 42.3516 4.35014L5.41328 18.44C4.96316 18.6117 4.99234 19.2581 5.4561 19.3885L12.954 21.4973C14.4463 21.917 15.5617 23.1612 15.8166 24.6903L17.2908 33.5357C17.3714 34.0193 18.0276 34.11 18.2364 33.6664L18.8254 32.4148C19.2957 31.4154 20.4872 30.9865 21.4866 31.4568C22.486 31.9271 22.915 33.1186 22.4446 34.118L21.1735 36.8193C19.5032 40.3685 14.2535 39.6429 13.6086 35.7737L11.9232 25.6611C11.8913 25.4699 11.7519 25.3144 11.5654 25.2619L2.9172 22.8296C-0.792895 21.7862 -1.02636 16.6153 2.5746 15.2417L41.8253 0.269788Z\"\n            fill=\"white\"/>\n    </svg>\n    <h1>Not Found</h1>\n    <p>The requested URL was not found on this server.</p>\n</main>\n</body>\n</html>\n"
  },
  {
    "path": "recipe/provision/Caddyfile",
    "content": "{{domain}} {\n  root * {{deploy_path}}/current/{{public_path}}\n  encode zstd gzip\n  file_server\n  php_fastcgi * unix//run/php/php{{php_version}}-fpm.sock {\n    resolve_root_symlink\n  }\n\n  log {\n    output file {{deploy_path}}/log/access.log {\n      mode 0644\n    }\n  }\n\n  handle_errors {\n    @404 {\n      expression {http.error.status_code} == 404\n    }\n    rewrite @404 /404.html\n    encode zstd gzip\n    file_server {\n      root /var/deployer\n    }\n  }\n}\n"
  },
  {
    "path": "recipe/provision/databases.php",
    "content": "<?php\n\nnamespace Deployer;\n\nset('db_type', function () {\n    $supportedDbTypes = [\n        'none',\n        'mysql',\n        'mariadb',\n        'postgresql',\n    ];\n    return askChoice(' What DB to install? ', $supportedDbTypes, 0);\n});\n\nset('db_name', function () {\n    return ask(' DB name: ', 'prod');\n});\n\nset('db_user', function () {\n    return ask(' DB user: ', 'deployer');\n});\n\nset('db_password', function () {\n    return askHiddenResponse(' DB password: ');\n});\n\ndesc('Provision databases');\ntask('provision:databases', function () {\n    set('remote_user', get('provision_user'));\n\n    $dbType = get('db_type');\n    if ($dbType === 'none') {\n        return;\n    }\n    invoke('provision:' . $dbType);\n})\n    ->limit(1);\n\ndesc('Provision MySQL');\ntask('provision:mysql', function () {\n    run('apt-get install -y mysql-server', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900);\n    run(\"mysql --user=\\\"root\\\" -e \\\"CREATE USER IF NOT EXISTS '{{db_user}}'@'0.0.0.0' IDENTIFIED BY '%secret%';\\\"\", secret: get('db_password'));\n    run(\"mysql --user=\\\"root\\\" -e \\\"CREATE USER IF NOT EXISTS '{{db_user}}'@'%' IDENTIFIED BY '%secret%';\\\"\", secret: get('db_password'));\n    run(\"mysql --user=\\\"root\\\" -e \\\"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'0.0.0.0' WITH GRANT OPTION;\\\"\");\n    run(\"mysql --user=\\\"root\\\" -e \\\"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'%' WITH GRANT OPTION;\\\"\");\n    run(\"mysql --user=\\\"root\\\" -e \\\"FLUSH PRIVILEGES;\\\"\");\n    run(\"mysql --user=\\\"root\\\" -e \\\"CREATE DATABASE IF NOT EXISTS {{db_name}} character set UTF8mb4 collate utf8mb4_bin;\\\"\");\n});\n\ndesc('Provision MariaDB');\ntask('provision:mariadb', function () {\n    run('apt-get install -y mariadb-server', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900);\n    run(\"mysql --user=\\\"root\\\" -e \\\"CREATE USER IF NOT EXISTS '{{db_user}}'@'0.0.0.0' IDENTIFIED BY '%secret%';\\\"\", secret: get('db_password'));\n    run(\"mysql --user=\\\"root\\\" -e \\\"CREATE USER IF NOT EXISTS '{{db_user}}'@'%' IDENTIFIED BY '%secret%';\\\"\", secret: get('db_password'));\n    run(\"mysql --user=\\\"root\\\" -e \\\"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'0.0.0.0' WITH GRANT OPTION;\\\"\");\n    run(\"mysql --user=\\\"root\\\" -e \\\"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'%' WITH GRANT OPTION;\\\"\");\n    run(\"mysql --user=\\\"root\\\" -e \\\"FLUSH PRIVILEGES;\\\"\");\n    run(\"mysql --user=\\\"root\\\" -e \\\"CREATE DATABASE IF NOT EXISTS {{db_name}} character set UTF8mb4 collate utf8mb4_bin;\\\"\");\n});\n\ndesc('Provision PostgreSQL');\ntask('provision:postgresql', function () {\n    run('apt-get install -y postgresql postgresql-contrib', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900);\n    run(\"sudo -u postgres psql <<< $'CREATE DATABASE {{db_name}};'\");\n    run(\"sudo -u postgres psql <<< $'CREATE USER {{db_user}} WITH ENCRYPTED PASSWORD \\'%secret%\\';'\", secret: get('db_password'));\n    run(\"sudo -u postgres psql <<< $'GRANT ALL PRIVILEGES ON DATABASE {{db_name}} TO {{db_user}};'\");\n});\n"
  },
  {
    "path": "recipe/provision/nodejs.php",
    "content": "<?php\n\nnamespace Deployer;\n\nuse function Deployer\\Support\\escape_shell_argument;\n\nset('node_version', '--lts');\n\ndesc('Installs npm packages');\ntask('provision:node', function () {\n    set('remote_user', get('provision_user'));\n\n    if (has('nodejs_version')) {\n        throw new \\RuntimeException('nodejs_version is deprecated, use node_version instead.');\n    }\n    $arch = run('uname -m');\n\n    if ($arch === 'arm' || str_starts_with($arch, 'armv7')) {\n        $filename = 'fnm-arm32';\n    } elseif (str_starts_with($arch, 'aarch') || str_starts_with($arch, 'armv8')) {\n        $filename = 'fnm-arm64';\n    } else {\n        $filename = 'fnm-linux';\n    }\n\n    $url = \"https://github.com/Schniz/fnm/releases/latest/download/$filename.zip\";\n    run(\"rm -rf /tmp/$filename.zip\");\n    run(\"curl -sSL $url --output /tmp/$filename.zip\");\n\n    run(\"unzip /tmp/$filename.zip -d /tmp\");\n\n    run(\"mv /tmp/fnm /usr/local/bin/fnm\");\n    run('chmod +x /usr/local/bin/fnm');\n\n    run('fnm install {{node_version}}');\n    run(\"echo \" . escape_shell_argument('eval \"`fnm env`\"') . \" >> /etc/profile.d/fnm.sh\");\n})\n    ->oncePerNode();\n"
  },
  {
    "path": "recipe/provision/php.php",
    "content": "<?php\n\nnamespace Deployer;\n\nset('php_version', function () {\n    $defaultPhpVersion = file_exists('composer.json')\n        ? explode('|', preg_replace('/[^0-9.|]+/', '', json_decode(file_get_contents('composer.json'), true)['require']['php'] ?? '8.3'))[0]\n        : '8.3';\n\n    if (count(($parts = explode('.', $defaultPhpVersion))) > 2) {\n        $defaultPhpVersion = \"$parts[0].$parts[1]\";\n    }\n\n    return ask(' What PHP version to install? ', $defaultPhpVersion, ['5.6', '7.4', '8.0', '8.1', '8.2', '8.3']);\n});\n\ndesc('Installs PHP packages');\ntask('provision:php', function () {\n    set('remote_user', get('provision_user'));\n\n    $version = get('php_version');\n    info(\"Installing PHP $version\");\n    $packages = [\n        \"php$version-bcmath\",\n        \"php$version-cli\",\n        \"php$version-curl\",\n        \"php$version-dev\",\n        \"php$version-fpm\",\n        \"php$version-gd\",\n        \"php$version-imap\",\n        \"php$version-intl\",\n        \"php$version-mbstring\",\n        \"php$version-mysql\",\n        \"php$version-pgsql\",\n        \"php$version-readline\",\n        \"php$version-soap\",\n        \"php$version-sqlite3\",\n        \"php$version-xml\",\n        \"php$version-zip\",\n    ];\n    run('apt-get install -y ' . implode(' ', $packages), env: ['DEBIAN_FRONTEND' => 'noninteractive']);\n\n    // Configure PHP-CLI\n    run(\"sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/cli/php.ini\");\n    run(\"sed -i 's/display_errors = .*/display_errors = On/' /etc/php/$version/cli/php.ini\");\n    run(\"sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/$version/cli/php.ini\");\n    run(\"sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/$version/cli/php.ini\");\n    run(\"sed -i 's/;date.timezone.*/date.timezone = UTC/' /etc/php/$version/cli/php.ini\");\n\n    // Configure PHP-FPM\n    run(\"sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/fpm/php.ini\");\n    run(\"sed -i 's/display_errors = .*/display_errors = On/' /etc/php/$version/fpm/php.ini\");\n    run(\"sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/$version/fpm/php.ini\");\n    run(\"sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/$version/fpm/php.ini\");\n    run(\"sed -i 's/;date.timezone.*/date.timezone = UTC/' /etc/php/$version/fpm/php.ini\");\n    run(\"sed -i 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/' /etc/php/$version/fpm/php.ini\");\n\n    // Configure FPM Pool\n    run(\"sed -i 's/;request_terminate_timeout = .*/request_terminate_timeout = 60/' /etc/php/$version/fpm/pool.d/www.conf\");\n    run(\"sed -i 's/;catch_workers_output = .*/catch_workers_output = yes/' /etc/php/$version/fpm/pool.d/www.conf\");\n    run(\"sed -i 's/;php_flag\\[display_errors\\] = .*/php_flag[display_errors] = yes/' /etc/php/$version/fpm/pool.d/www.conf\");\n    run(\"sed -i 's/;php_admin_value\\[error_log\\] = .*/php_admin_value[error_log] = \\/var\\/log\\/fpm-php.www.log/' /etc/php/$version/fpm/pool.d/www.conf\");\n    run(\"sed -i 's/;php_admin_flag\\[log_errors\\] = .*/php_admin_flag[log_errors] = on/' /etc/php/$version/fpm/pool.d/www.conf\");\n\n    // Configure PHP sessions directory\n    run('chmod 733 /var/lib/php/sessions');\n    run('chmod +t /var/lib/php/sessions');\n})\n    ->verbose()\n    ->limit(1);\n\ndesc('Shows php-fpm logs');\ntask('logs:php-fpm', function () {\n    $fpmLogs = run(\"ls -1 /var/log | grep fpm\");\n    if (empty($fpmLogs)) {\n        throw new \\RuntimeException('No PHP-FPM logs found.');\n    }\n    run(\"sudo tail -f /var/log/$fpmLogs\");\n})->verbose();\n\ndesc('Installs Composer');\ntask('provision:composer', function () {\n    run('curl -sS https://getcomposer.org/installer | php');\n    run('mv composer.phar /usr/local/bin/composer');\n})->oncePerNode();\n"
  },
  {
    "path": "recipe/provision/user.php",
    "content": "<?php\n\nnamespace Deployer;\n\nuse function Deployer\\Support\\parse_home_dir;\n\nset('sudo_password', function () {\n    return askHiddenResponse(' Password for sudo: ');\n});\n\n\ndesc('Setups a deployer user');\ntask('provision:user', function () {\n    set('remote_user', get('provision_user'));\n\n    if (test('id deployer >/dev/null 2>&1')) {\n        // TODO: Check what created deployer user configured correctly.\n        // TODO: Update sudo_password of deployer user.\n        // TODO: Copy ssh_copy_id to deployer ssh dir.\n        info('deployer user already exist');\n    } else {\n        run('useradd deployer');\n        run('mkdir -p /home/deployer/.ssh');\n        run('mkdir -p /home/deployer/.deployer');\n        run('adduser deployer sudo');\n\n        run('chsh -s /bin/bash deployer');\n        run('cp /root/.profile /home/deployer/.profile');\n        run('cp /root/.bashrc /home/deployer/.bashrc');\n        run('touch /home/deployer/.sudo_as_admin_successful');\n\n        // Make color prompt.\n        run(\"sed -i 's/#force_color_prompt=yes/force_color_prompt=yes/' /home/deployer/.bashrc\");\n\n        $password = run(\"mkpasswd -m sha-512 '%secret%'\", secret: get('sudo_password'));\n        run(\"usermod --password '%secret%' deployer\", secret: $password);\n\n        // Copy root public key to deployer user so user can login without password.\n        run('cp /root/.ssh/authorized_keys /home/deployer/.ssh/authorized_keys');\n\n        // Create ssh key if not already exists.\n        run('ssh-keygen -f /home/deployer/.ssh/id_ed25519 -t ed25519 -N \"\"');\n\n        try {\n            run('chown -R deployer:deployer /home/deployer');\n            run('chmod -R 755 /home/deployer');\n            run('chmod 700 /home/deployer/.ssh');\n            run('chmod 600 /home/deployer/.ssh/id_ed25519');\n            run('chmod 600 /home/deployer/.ssh/authorized_keys');\n        } catch (\\Throwable $e) {\n            warning($e->getMessage());\n        }\n\n        run('usermod -a -G www-data deployer');\n        run('usermod -a -G caddy deployer');\n    }\n})->oncePerNode();\n\n\ndesc('Copy public key to remote server');\ntask('provision:ssh_copy_id', function () {\n    $defaultKeys = [\n        '~/.ssh/id_rsa.pub',\n        '~/.ssh/id_ed25519.pub',\n        '~/.ssh/id_ecdsa.pub',\n        '~/.ssh/id_dsa.pub',\n    ];\n\n    $publicKeyContent = false;\n    foreach ($defaultKeys as $key) {\n        $file = parse_home_dir($key);\n        if (file_exists($file)) {\n            $publicKeyContent = file_get_contents($file);\n            break;\n        }\n    }\n\n    if (!$publicKeyContent) {\n        $publicKeyContent = ask(' Public key: ', '');\n    }\n\n    if (empty($publicKeyContent)) {\n        info('Skipping public key copy as no public key was found or provided.');\n        return;\n    }\n\n    run('echo \"$PUBLIC_KEY\" >> /home/deployer/.ssh/authorized_keys', env: ['PUBLIC_KEY' => $publicKeyContent]);\n});\n"
  },
  {
    "path": "recipe/provision/website.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer;\n\nset('domain', function () {\n    return ask(' Domain: ', get('hostname'));\n});\n\nset('public_path', function () {\n    return ask(' Public path: ', 'public');\n});\n\ndesc('Configures a server');\ntask('provision:server', function () {\n    set('remote_user', get('provision_user'));\n    run('usermod -a -G www-data caddy');\n    run(\"mkdir -p /var/deployer\");\n    $html = file_get_contents(__DIR__ . '/404.html');\n    run(\"echo $'$html' > /var/deployer/404.html\");\n})->oncePerNode();\n\ndesc('Provision website');\ntask('provision:website', function () {\n    $restoreBecome = become('deployer');\n\n    run(\"[ -d {{deploy_path}} ] || mkdir -p {{deploy_path}}\");\n    run(\"chown -R deployer:deployer {{deploy_path}}\");\n\n    set('deploy_path', run(\"realpath {{deploy_path}}\"));\n    cd('{{deploy_path}}');\n\n    run(\"[ -d log ] || mkdir log\");\n    run(\"chgrp caddy log\");\n\n    $caddyfile = parse(file_get_contents(__DIR__ . '/Caddyfile'));\n\n    if (test('[ -f Caddyfile ]')) {\n        run(\"echo $'$caddyfile' > Caddyfile.new\");\n        $diff = run('diff -U5 --color=always Caddyfile Caddyfile.new', nothrow: true);\n        if (empty($diff)) {\n            run('rm Caddyfile.new');\n        } else {\n            info('Found Caddyfile changes');\n            writeln(\"\\n\" . $diff);\n            $answer = askChoice(' Which Caddyfile to save? ', ['old', 'new'], 0);\n            if ($answer === 'old') {\n                run('rm Caddyfile.new');\n            } else {\n                run('mv Caddyfile.new Caddyfile');\n            }\n        }\n    } else {\n        run(\"echo $'$caddyfile' > Caddyfile\");\n    }\n\n    $restoreBecome();\n\n    if (!test(\"grep -q 'import {{deploy_path}}/Caddyfile' /etc/caddy/Caddyfile\")) {\n        run(\"echo 'import {{deploy_path}}/Caddyfile' >> /etc/caddy/Caddyfile\");\n    }\n    run('service caddy reload');\n\n    info(\"Website {{domain}} configured!\");\n})->limit(1);\n\ndesc('Shows access logs');\ntask('logs:access', function () {\n    run('tail -f {{deploy_path}}/log/access.log');\n})->verbose();\n\ndesc('Shows caddy syslog');\ntask('logs:caddy', function () {\n    run('sudo journalctl -u caddy -f');\n})->verbose();\n"
  },
  {
    "path": "recipe/provision.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire __DIR__ . '/provision/databases.php';\nrequire __DIR__ . '/provision/nodejs.php';\nrequire __DIR__ . '/provision/php.php';\nrequire __DIR__ . '/provision/user.php';\nrequire __DIR__ . '/provision/website.php';\n\nuse Deployer\\Task\\Context;\n\nuse function Deployer\\Support\\parse_home_dir;\n\nadd('recipes', ['provision']);\n\n// Name of lsb_release like: focal, bionic, etc.\n// As only Ubuntu 20.04 LTS is supported for provision should be the `focal`.\nset('lsb_release', function () {\n    return run(\"lsb_release -s -c\");\n});\n\ndesc('Provision the server');\ntask('provision', [\n    'provision:check',\n    'provision:configure',\n    'provision:update',\n    'provision:upgrade',\n    'provision:install',\n    'provision:ssh',\n    'provision:firewall',\n    'provision:user',\n    'provision:php',\n    'provision:node',\n    'provision:databases',\n    'provision:composer',\n    'provision:server',\n    'provision:website',\n    'provision:verify',\n]);\n\n// Default user to use for provisioning.\nset('provision_user', 'root');\n\ndesc('Checks pre-required state');\ntask('provision:check', function () {\n    set('remote_user', get('provision_user'));\n\n    $release = run('cat /etc/os-release');\n    ['NAME' => $name, 'VERSION_ID' => $version] = parse_ini_string($release);\n    if ($name !== 'Ubuntu') {\n        warning('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');\n        warning('!!                                    !!');\n        warning('!!      Only Ubuntu is supported!     !!');\n        warning('!!                                    !!');\n        warning('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');\n        if (!askConfirmation(' Do you want to continue? (Not recommended)', false)) {\n            throw new \\RuntimeException('Provision aborted due to incompatible OS.');\n        }\n    }\n    // Also only version 20 and older are supported.\n    if (version_compare($version, '20', '<')) {\n        warning(\"Ubuntu $version is not supported. Use Ubuntu 20 or newer.\");\n        if (!askConfirmation(' Do you want to continue? (Not recommended)', false)) {\n            throw new \\RuntimeException('Provision aborted due to incompatible OS.');\n        }\n    }\n})->oncePerNode();\n\ndesc('Collects required params');\ntask('provision:configure', function () {\n    set('remote_user', get('provision_user'));\n\n    $params = [\n        'sudo_password',\n        'domain',\n        'public_path',\n        'php_version',\n        'db_type',\n    ];\n    $dbparams = [\n        'db_user',\n        'db_name',\n        'db_password',\n    ];\n\n    $showCode = false;\n\n    foreach ($params as $name) {\n        if (!Context::get()->getConfig()->hasOwn($name)) {\n            $showCode = true;\n        }\n        get($name);\n    }\n\n    if (get('db_type') !== 'none') {\n        foreach ($dbparams as $name) {\n            if (!Context::get()->getConfig()->hasOwn($name)) {\n                $showCode = true;\n            }\n            get($name);\n        }\n    }\n\n    if ($showCode) {\n        $code = \"\\n\\n<comment>====== Configuration Start ======</comment>\";\n        $code .= \"\\nhost(<info>'{{alias}}'</info>)\";\n        $codeParams = $params;\n        if (get('db_type') !== 'none') {\n            $codeParams = array_merge($codeParams, $dbparams);\n        }\n        foreach ($codeParams as $name) {\n            $code .= \"\\n    ->set(<info>'$name'</info>, <info>'\" . get($name) . \"'</info>)\";\n        }\n        $code .= \";\\n\";\n        $code .= \"<comment>====== Configuration End ======</comment>\\n\\n\";\n        writeln($code);\n    }\n});\n\n\ndesc('Adds repositories and update');\ntask('provision:update', function () {\n    set('remote_user', get('provision_user'));\n\n    // Update before installing anything\n    run('apt-get update', env: ['DEBIAN_FRONTEND' => 'noninteractive']);\n\n    // Pre-requisites\n    run('apt install -y curl gpg software-properties-common', env: ['DEBIAN_FRONTEND' => 'noninteractive']);\n\n    // PHP\n    run('apt-add-repository ppa:ondrej/php -y', env: [\n        'DEBIAN_FRONTEND' => 'noninteractive',\n        'LC_ALL' => 'C.UTF-8',\n    ]);\n\n    // Caddy\n    run(\"curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor --yes -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg\");\n    run(\"curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' > /etc/apt/sources.list.d/caddy-stable.list\");\n\n    // Update\n    run('apt-get update', env: ['DEBIAN_FRONTEND' => 'noninteractive']);\n})\n    ->oncePerNode()\n    ->verbose();\n\ndesc('Upgrades all packages');\ntask('provision:upgrade', function () {\n    set('remote_user', get('provision_user'));\n    run('apt-get upgrade -y', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900);\n})\n    ->oncePerNode()\n    ->verbose();\n\ndesc('Installs packages');\ntask('provision:install', function () {\n    set('remote_user', get('provision_user'));\n    $packages = [\n        'acl',\n        'apt-transport-https',\n        'build-essential',\n        'caddy',\n        'curl',\n        'debian-archive-keyring',\n        'debian-keyring',\n        'fail2ban',\n        'gcc',\n        'git',\n        'libmcrypt4',\n        'libpcre3-dev',\n        'libsqlite3-dev',\n        'make',\n        'ncdu',\n        'nodejs',\n        'pkg-config',\n        'python-is-python3',\n        'redis',\n        'sendmail',\n        'sqlite3',\n        'ufw',\n        'unzip',\n        'uuid-runtime',\n        'whois',\n    ];\n    run('apt-get install -y ' . implode(' ', $packages), env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900);\n})\n    ->verbose()\n    ->oncePerNode();\n\ndesc('Configures the ssh');\ntask('provision:ssh', function () {\n    set('remote_user', get('provision_user'));\n    run(\"sed -i 's/PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config\");\n    run('ssh-keygen -A');\n    run('service ssh restart');\n    if (test('[ ! -d /root/.ssh ]')) {\n        run('mkdir -p /root/.ssh');\n        run('touch /root/.ssh/authorized_keys');\n    }\n})->oncePerNode();\n\ndesc('Setups a firewall');\ntask('provision:firewall', function () {\n    set('remote_user', get('provision_user'));\n    run('ufw allow 22');\n    run('ufw allow 80');\n    run('ufw allow 443');\n    run('ufw --force enable');\n})->oncePerNode();\n\ndesc('Verifies what provision was successful');\ntask('provision:verify', function () {\n    fetch('{{domain}}', 'get', [], null, $info, true);\n    if ($info['http_code'] === 404) {\n        info(\"provisioned successfully!\");\n    }\n});\n"
  },
  {
    "path": "recipe/shopware.php",
    "content": "<?php\n/**\n * ## Usage\n *\n * Add `repository` to your _deploy.php_ file:\n *\n * ```php\n * set('repository', 'git@github.com:shopware/production.git');\n * ```\n *\n * configure host:\n * ```php\n * host('SSH-HOSTNAME')\n *     ->set('remote_user', 'SSH-USER')\n *     ->set('deploy_path', '/var/www/shopware') // This is the path where deployer will create its directory structure\n *     ->set('http_user', 'www-data') // Not needed, if the `user` is the same, the web server is running with\n *     ->set('http_group', 'www-data')\n *     ->set('writable_mode', 'chmod')\n *     ->set('writable_recursive', true)\n *     ->set('become', 'www-data'); // You might want to change user to execute remote tasks because of access rights of created cache files\n * ```\n *\n * :::note\n * Please remember that the installation must be modified so that it can be\n * [build without database](https://developer.shopware.com/docs/guides/hosting/installation-updates/deployments/build-w-o-db#compiling-the-storefront-without-database).\n * :::\n */\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['shopware']);\n\nset('bin/console', '{{bin/php}} {{release_or_current_path}}/bin/console');\n\nset('default_timeout', 3600); // Increase when tasks take longer than that.\n\n// These files are shared among all releases.\nset('shared_files', [\n    '.env.local',\n    'install.lock',\n    'public/.htaccess',\n    'public/.user.ini',\n]);\n\n// These directories are shared among all releases.\nset('shared_dirs', [\n    'config/jwt',\n    'files',\n    'var/log',\n    'public/media',\n    'public/plugins',\n    'public/thumbnail',\n    'public/sitemap',\n]);\n\n// These directories are made writable (the definition of \"writable\" requires attention).\n// Please note that the files in `config/jwt/*` receive special attention in the `sw:writable:jwt` task.\nset('writable_dirs', [\n    'config/jwt',\n    'custom/plugins',\n    'files',\n    'public/bundles',\n    'public/css',\n    'public/fonts',\n    'public/js',\n    'public/media',\n    'public/plugins',\n    'public/sitemap',\n    'public/theme',\n    'public/thumbnail',\n    'var',\n]);\n\n// This sets the Shopware version to the version of the Shopware console command.\nset('shopware_version', function () {\n    $versionOutput = run('cd {{release_path}} && {{bin/console}} -V');\n    preg_match('/(\\d+\\.\\d+\\.\\d+\\.\\d+)/', $versionOutput, $matches);\n    return $matches[0] ?? '6.6.0';\n});\n\n// This task remotely executes the `cache:clear` console command on the target server.\ntask('sw:cache:clear', static function () {\n    run('cd {{release_path}} && {{bin/console}} cache:clear --no-warmup');\n});\n\n// This task remotely executes the cache warmup console commands on the target server, so that the first user, who\n// visits the website, doesn't have to wait for the cache to be built up.\ntask('sw:cache:warmup', static function () {\n    run('cd {{release_path}} && {{bin/console}} cache:warmup');\n\n    // Shopware 6.6+ dropped support for the http:cache:warmup command, so only execute it if the version is less than 6.6\n    if (version_compare(get('shopware_version'), '6.6.0') < 0) {\n        run('cd {{release_path}} && {{bin/console}} http:cache:warm:up');\n    }\n});\n\n// This task remotely executes the `database:migrate` console command on the target server.\ntask('sw:database:migrate', static function () {\n    run('cd {{release_path}} && {{bin/console}} database:migrate --all');\n});\n\ntask('sw:plugin:refresh', function () {\n    run('cd {{release_path}} && {{bin/console}} plugin:refresh');\n});\n\ntask('sw:scheduled-task:register', function () {\n    run('cd {{release_path}} && {{bin/console}} scheduled-task:register');\n});\n\ntask('sw:theme:refresh', function () {\n    run('cd {{release_path}} && {{bin/console}} theme:refresh');\n});\n\n// This task is not used by default, but can be used, e.g. in combination with `SHOPWARE_SKIP_THEME_COMPILE=1`,\n// to build the theme remotely instead of locally.\ntask('sw:theme:compile', function () {\n    run('cd {{release_path}} && {{bin/console}} theme:compile');\n});\n\nfunction getPlugins(): array\n{\n    $output = run('cd {{release_path}} && {{bin/console}} plugin:list --json');\n    $plugins = json_decode($output);\n\n    return $plugins;\n}\n\ntask('sw:plugin:update:all', static function () {\n    $plugins = getPlugins();\n    foreach ($plugins as $plugin) {\n        if ($plugin->installedAt && $plugin->upgradeVersion) {\n            writeln(\"<info>Running plugin update for \" . $plugin->name . \"</info>\\n\");\n            run(\"cd {{release_path}} && {{bin/console}} plugin:update \" . $plugin->name);\n        }\n    }\n});\n\ntask('sw:writable:jwt', static function () {\n    if (!test('[ -d {{deploy_path}}/config/jwt/ ]')) {\n        return;\n    }\n    run('cd {{release_path}} && find config/jwt/ -type f -exec chmod -R 660 {} +');\n});\n\n/**\n * Grouped SW deploy tasks.\n */\ntask('sw:deploy', [\n    'sw:database:migrate',\n    'sw:plugin:refresh',\n    'sw:theme:refresh',\n    'sw:scheduled-task:register',\n    'sw:cache:clear',\n    'sw:plugin:update:all',\n    'sw:cache:clear',\n]);\n\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'sw:writable:jwt',\n    'sw:deploy',\n    'deploy:clear_paths',\n    'sw:cache:warmup',\n    'deploy:publish',\n]);\n\ntask('deploy:update_code')->setCallback(static function () {\n    upload('.', '{{release_path}}', [\n        'options' => [\n            '--exclude=.git',\n            '--exclude=deploy.php',\n            '--exclude=node_modules',\n        ],\n    ]);\n});\n\ntask('sw-build-without-db:get-remote-config', static function () {\n    if (!test('[ -d {{current_path}} ]')) {\n        return;\n    }\n    within('{{current_path}}', function () {\n        run('{{bin/php}} ./bin/console bundle:dump');\n        download('{{current_path}}/var/plugins.json', './var/');\n\n        run('{{bin/php}} ./bin/console theme:dump -n');\n        download('{{current_path}}/files/theme-config', './files/');\n    });\n});\n\ntask('sw-build-without-db:build', static function () {\n    runLocally('CI=1 SHOPWARE_SKIP_BUNDLE_DUMP=1 ./bin/build-js.sh');\n});\n\ntask('sw-build-without-db', [\n    'sw-build-without-db:get-remote-config',\n    'sw-build-without-db:build',\n]);\n\nbefore('deploy:update_code', 'sw-build-without-db');\n"
  },
  {
    "path": "recipe/silverstripe.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['silverstripe']);\n\n/**\n * Silverstripe configuration\n */\n\nset('shared_assets', function () {\n    if (test('[ -d {{release_or_current_path}}/public ]') || test('[ -d {{deploy_path}}/shared/public ]')) {\n        return 'public/assets';\n    }\n    return 'assets';\n});\n\n\n// Silverstripe shared dirs\nset('shared_dirs', [\n    '{{shared_assets}}',\n]);\n\n// Silverstripe writable dirs\nset('writable_dirs', [\n    '{{shared_assets}}',\n]);\n\n// Silverstripe cli script\nset('silverstripe_cli_script', function () {\n    $paths = [\n        'framework/cli-script.php',\n        'vendor/silverstripe/framework/cli-script.php',\n    ];\n    foreach ($paths as $path) {\n        if (test('[ -f {{release_or_current_path}}/' . $path . ' ]')) {\n            return $path;\n        }\n    }\n});\n\n/**\n * Helper tasks\n */\ndesc('Runs /dev/build');\ntask('silverstripe:build', function () {\n    run('{{bin/php}} {{release_or_current_path}}/{{silverstripe_cli_script}} /dev/build');\n});\n\ndesc('Runs /dev/build?flush=all');\ntask('silverstripe:buildflush', function () {\n    run('{{bin/php}} {{release_or_current_path}}/{{silverstripe_cli_script}} /dev/build flush=all');\n});\n\n/**\n * Main task\n */\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'silverstripe:buildflush',\n    'deploy:publish',\n]);\n"
  },
  {
    "path": "recipe/spiral.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['spiral']);\n\n// Spiral shared dirs\nset('shared_dirs', ['runtime']);\n\n// Spiral writable dirs\nset('writable_dirs', ['runtime', 'public']);\n\n// Path to the RoadRunner server\nset('roadrunner_path', '{{release_or_current_path}}');\n\nset('dotenv_example', '.env.sample');\n\n/**\n * Run a console command.\n *\n * Supported options:\n * - 'showOutput': Show the output of the command if given.\n */\nfunction command(string $command, array $options = []): \\Closure\n{\n    return function () use ($command, $options): void {\n        $output = run(\"cd {{release_or_current_path}} && {{bin/php}} app.php $command\");\n\n        if (\\in_array('showOutput', $options, true)) {\n            writeln(\"<info>$output</info>\");\n        }\n    };\n}\n\n/**\n * Run a RoadRunner command.\n *\n * Supported options:\n * - 'showOutput': Show the output of the command if given.\n */\nfunction rr(string $command, array $options = []): \\Closure\n{\n    return function () use ($command, $options): void {\n        $output = run(\"cd {{roadrunner_path}} && ./rr $command\");\n\n        if (\\in_array('showOutput', $options, true)) {\n            writeln(\"<info>$output</info>\");\n        }\n    };\n}\n\n/**\n * Spiral Framework console commands\n */\ndesc('Configure project');\ntask('spiral:configure', command('configure', ['showOutput']));\n\ndesc('Update (init) cycle schema from database and annotated classes');\ntask('spiral:cycle', command('cycle', ['showOutput']));\n\ndesc('Perform all outstanding migrations');\ntask('spiral:migrate', command('migrate', ['showOutput']));\n\ndesc('Update project state');\ntask('spiral:update', command('update', ['showOutput']));\n\ndesc('Clean application runtime cache');\ntask('spiral:cache:clean', command('cache:clean', ['showOutput']));\n\ndesc('Reset translation cache');\ntask('spiral:i18n:reset', command('i18n:reset', ['showOutput']));\n\ndesc('Generate new encryption key, if it doesn\\'t exist');\ntask('spiral:encrypt-key', command('encrypt:key -m .env -p', ['showOutput']));\n\ndesc('Warm-up view cache');\ntask('spiral:views:compile', command('views:compile', ['showOutput']));\n\ndesc('Clear view cache');\ntask('spiral:views:reset', command('views:reset', ['showOutput']));\n\n/**\n * Cycle ORM and migrations console commands\n */\ndesc('Generate ORM schema migrations');\ntask('cycle:migrate', command('cycle:migrate', ['showOutput']));\n\ndesc('Render available CycleORM schemas');\ntask('cycle:render', command('cycle:render', ['showOutput']));\n\ndesc('Sync Cycle ORM schema with database without intermediate migration (risk operation)');\ntask('cycle:sync', command('cycle:sync', ['showOutput']));\n\ndesc('Init migrations component (create migrations table)');\ntask('migrate:init', command('migrate:init', ['showOutput']));\n\ndesc('Replay (down, up) one or multiple migrations');\ntask('migrate:replay', command('migrate:replay', ['showOutput']));\n\ndesc('Rollback one (default) or multiple migrations');\ntask('migrate:rollback', command('migrate:rollback', ['showOutput']));\n\ndesc('Get list of all available migrations and their statuses');\ntask('migrate:status', command('migrate:status', ['showOutput']));\n\n/**\n * RoadRunner console commands\n */\ndesc('Start RoadRunner server');\ntask('roadrunner:serve', function (): void {\n    exec(parse('cd {{roadrunner_path}} && ./rr serve -p > /dev/null 2>&1 &'));\n});\n\ndesc('Stop RoadRunner server');\ntask('roadrunner:stop', rr('stop', ['showOutput']));\n\ndesc('Reset workers of all services');\ntask('roadrunner:reset', rr('reset', ['showOutput']));\n\n/**\n * Download and restart RoadRunner\n */\ndesc('Download RoadRunner');\ntask('deploy:download-rr', function (): void {\n    $output = run(\"cd {{release_or_current_path}} && {{bin/php}} ./vendor/bin/rr get-binary -l {{roadrunner_path}}\");\n    writeln(\"<info>$output</info>\");\n});\n\ndesc('Restart RoadRunner');\ntask('deploy:restart-rr', function (): void {\n    try {\n        invoke('roadrunner:reset');\n        writeln(\"<info>Roadrunner successfully restarted.</info>\");\n    } catch (\\Throwable $e) {\n        invoke('roadrunner:serve');\n        writeln(\"<info>Roadrunner successfully started.</info>\");\n    }\n});\n\n/**\n * Main task\n */\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'spiral:encrypt-key',\n    'spiral:configure',\n    'deploy:download-rr',\n    'deploy:publish',\n    'deploy:restart-rr',\n]);\n"
  },
  {
    "path": "recipe/statamic.php",
    "content": "<?php\n\nnamespace Deployer;\n\n/*\n * As Statamic is a Laravel Package, we will extend the Laravel\n * recipe and simply add Statamic specific commands.\n */\nrequire_once __DIR__ . '/laravel.php';\n\nadd('recipes', ['statamic']);\nadd('writable_dirs', [\n    'storage/statamic',\n]);\n\nset('statamic_version', function () {\n    $result = run('{{bin/php}} {{release_or_current_path}}/please --version');\n    preg_match_all('/(\\d+\\.?)+/', $result, $matches);\n    return $matches[0][0] ?? 'unknown';\n});\n\n/*\n * Addons\n */\n\ndesc('Rebuilds the cached addon package manifest');\ntask('statamic:addons:discover', artisan('statamic:addons:discover'));\n\n/*\n * Assets\n */\n\ndesc('Generates asset preset manipulations');\ntask('statamic:assets:generate-presets', artisan('statamic:assets:generate-presets'));\n\ndesc('Generates asset metadata files');\ntask('statamic:assets:meta', artisan('statamic:assets:meta'));\n\n/*\n * Git\n */\n\ndesc('Git add and commit tracked content');\ntask('statamic:git:commit', artisan('statamic:git:commit'));\n\n/*\n * Glide\n */\n\ndesc('Clears the Glide image cache');\ntask('statamic:glide:clear', artisan('statamic:glide:clear'));\n\n/*\n * Responsive Images (not in the core)\n */\n\ndesc('Generates responsive images');\ntask('statamic:responsive:generate', artisan('statamic:responsive:generate'));\n\ndesc('Regenerate responsive images');\ntask('statamic:responsive:regenerate', artisan('statamic:responsive:regenerate'));\n\n/*\n * Search\n */\n\ndesc('Inserts an item into its search indexes');\ntask('statamic:search:insert', artisan('statamic:search:insert'));\n\ndesc('Update a search index');\ntask('statamic:search:update', artisan('statamic:search:update'));\n\n/*\n * Stache\n */\n\ndesc('Clears the \"Stache\" cache');\ntask('statamic:stache:clear', artisan('statamic:stache:clear'));\n\ndesc('Diagnose any problems with the Stache');\ntask('statamic:stache:doctor', artisan('statamic:stache:doctor'));\n\ndesc('Clears and rebuild the \"Stache\" cache');\ntask('statamic:stache:refresh', artisan('statamic:stache:refresh'));\n\ndesc('Builds the \"Stache\" cache');\ntask('statamic:stache:warm', artisan('statamic:stache:warm'));\n\n/*\n * Static\n */\n\ndesc('Clears the static page cache');\ntask('statamic:static:clear', artisan('statamic:static:clear'));\n\ndesc('Warms the static cache by visiting all URLs');\ntask('statamic:static:warm', artisan('statamic:static:warm'));\n\n/*\n * Support\n */\n\ndesc('Outputs details helpful for support requests');\ntask('statamic:support:details', artisan('statamic:support:details'));\n\n/*\n * Updated\n */\n\ndesc('Runs update scripts from specific version');\ntask('statamic:updates:run', artisan('statamic:updates:run'));\n\n/*\n * Main Deploy Script for Statamic, which\n * will overwrite the Laravel default.\n */\n\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'artisan:storage:link',\n    'artisan:cache:clear',\n    'statamic:stache:clear',\n    'statamic:stache:warm',\n    'deploy:publish',\n]);\n"
  },
  {
    "path": "recipe/sulu.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/symfony.php';\n\nadd('recipes', ['sulu']);\n\nadd('shared_dirs', ['var/indexes', 'var/sitemaps', 'var/uploads', 'public/uploads']);\n\nadd('writable_dirs', ['public/uploads']);\n\nset('bin/websiteconsole', function () {\n    return parse('{{bin/php}} {{release_or_current_path}}/bin/websiteconsole --no-interaction');\n});\n\ndesc('Migrates PHPCR');\ntask('phpcr:migrate', function () {\n    run('{{bin/console}} phpcr:migrations:migrate');\n});\n\ndesc('Clears cache');\ntask('deploy:website:cache:clear', function () {\n    run('{{bin/websiteconsole}} cache:clear --no-warmup');\n});\n\ndesc('Warmups cache');\ntask('deploy:website:cache:warmup', function () {\n    run('{{bin/websiteconsole}} cache:warmup');\n});\n\nafter('deploy:cache:clear', 'deploy:website:cache:clear');\nafter('deploy:website:cache:clear', 'deploy:website:cache:warmup');\n"
  },
  {
    "path": "recipe/symfony.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['symfony']);\n\nset('symfony_version', function () {\n    $result = run('{{bin/console}} --version');\n    preg_match_all('/(\\d+\\.?)+/', $result, $matches);\n    return $matches[0][0] ?? 5.0;\n});\n\nset('shared_dirs', [\n    'var/log',\n]);\n\nset('shared_files', [\n    '.env.local',\n]);\n\nset('writable_dirs', [\n    'var',\n    'var/cache',\n    'var/log',\n    'var/sessions',\n]);\n\nset('log_files', 'var/log/*.log');\n\nset('migrations_config', '');\n\nset('doctrine_schema_validate_config', '');\n\nset('bin/console', '{{bin/php}} {{release_or_current_path}}/bin/console');\n\nset('console_options', function () {\n    return '--no-interaction';\n});\n\ndesc('Migrates database');\ntask('database:migrate', function () {\n    $options = '--allow-no-migration';\n    if (get('migrations_config') !== '') {\n        $options = \"$options --configuration={{release_or_current_path}}/{{migrations_config}}\";\n    }\n\n    run(\"cd {{release_or_current_path}} && {{bin/console}} doctrine:migrations:migrate $options {{console_options}}\");\n});\n\ndesc('Validate the Doctrine mapping files');\ntask('doctrine:schema:validate', function () {\n    run(\"cd {{release_or_current_path}} && {{bin/console}} doctrine:schema:validate {{doctrine_schema_validate_config}} {{console_options}}\");\n});\n\ndesc('Clears cache');\ntask('deploy:cache:clear', function () {\n    // composer install scripts usually clear and warmup symfony cache\n    // so we only need to do it if composer install was run with --no-scripts\n    if (false !== strpos(get('composer_options', ''), '--no-scripts')) {\n        run('{{bin/console}} cache:clear {{console_options}}');\n    }\n});\n\ndesc('Optimize environment variables');\ntask('deploy:dump-env', function () {\n    within('{{release_or_current_path}}', function () {\n        run('{{bin/composer}} dump-env \"${APP_ENV:-prod}\"');\n    });\n});\n\ndesc('Deploys project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'deploy:cache:clear',\n    'deploy:publish',\n]);\n"
  },
  {
    "path": "recipe/typo3.php",
    "content": "<?php\n\n/**\n * TYPO3 Deployer Recipe\n *\n * Usage Examples:\n *\n * Deploy to production (using Git as source):\n *     vendor/bin/dep deploy production\n *\n * Deploy to staging using rsync:\n *     # In deploy.php or servers config, enable rsync\n *     set('use_rsync', true);\n *     vendor/bin/dep deploy staging\n *\n * Common TYPO3 commands:\n *     vendor/bin/dep typo3:cache:flush                     # Clear all TYPO3 caches\n *     vendor/bin/dep typo3:cache:warmup                    # Warmup system caches\n *     vendor/bin/dep typo3:language:update                 # Update extension language files\n *     vendor/bin/dep typo3:extension:setup                 # Set up all extensions\n *     vendor/bin/dep typo3:install:fixfolderstructure      # Automatically create required files and folders for TYPO3\n */\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\nrequire_once 'contrib/rsync.php';\n\nadd('recipes', ['typo3']);\n\n/**\n * Parse composer.json and return its contents as an array.\n * Used for auto-detecting TYPO3 settings like public_dir and bin_dir.\n */\nset('composer_config', function () {\n    return json_decode(file_get_contents('./composer.json'), true, 512, JSON_THROW_ON_ERROR);\n});\n\n/**\n * TYPO3 public (web) directory.\n * Automatically determined from composer.json.\n * Defaults to \"public\".\n */\nset('typo3/public_dir', function () {\n    $composerConfig = get('composer_config');\n\n    if ($composerConfig['extra']['typo3/cms']['web-dir'] ?? false) {\n        return $composerConfig['extra']['typo3/cms']['web-dir'];\n    }\n\n    return 'public';\n});\n\n/**\n * Path to the TYPO3 CLI binary.\n * Determined from composer.json \"config.bin-dir\" or defaults to \"vendor/bin/typo3\".\n */\nset('bin/typo3', function () {\n    $composerConfig = get('composer_config');\n\n    if ($composerConfig['config']['bin-dir'] ?? false) {\n        return $composerConfig['config']['bin-dir'] . '/typo3';\n    }\n\n    return 'vendor/bin/typo3';\n});\n\n/**\n * Log files to display when running `./vendor/bin/dep logs:app`\n */\nset('log_files', 'var/log/typo3_*.log');\n\n/**\n * Directories that persist between releases.\n * Shared via symlinks from the shared/ directory.\n */\nset('shared_dirs', [\n    '{{typo3/public_dir}}/fileadmin',\n    '{{typo3/public_dir}}/typo3temp/assets',\n    'var/lock',\n    'var/log',\n    'var/session',\n    'var/spool',\n]);\n\n/**\n * Files that persist between releases.\n * By default: config/system/settings.php\n */\nif (!has('shared_files') || empty(get('shared_files'))) {\n    set('shared_files', [\n        'config/system/settings.php',\n    ]);\n}\n\n/**\n * Writeable directories\n */\nset('writable_dirs', [\n    '{{typo3/public_dir}}/fileadmin',\n    '{{typo3/public_dir}}/typo3temp/assets',\n    'var/cache',\n    'var/lock',\n    'var/log',\n]);\n\n/**\n * Composer install options for production.\n */\nset('composer_options', ' --no-dev --verbose --prefer-dist --no-progress --no-interaction --optimize-autoloader');\n\n/**\n * If set in the config this recipe uses rsync.\n * Default setting: false (uses the Git repository)\n */\nset('use_rsync', false);\n\nset('update_code_task', function () {\n    return get('use_rsync') ? 'rsync' : 'deploy:update_code';\n});\n\ntask('typo3:update_code', function () {\n    invoke(get('update_code_task'));\n});\n\n$exclude = [\n    '.Build',\n    '.git',\n    '.gitlab',\n    '.ddev',\n    '.deployer',\n    '.idea',\n    '.DS_Store',\n    '.gitlab-ci.yml',\n    '.npm',\n    'deploy.yaml',\n    'package.json',\n    'package-lock.json',\n    'node_modules/',\n    'var/',\n    '/{{typo3/public_dir}}/fileadmin',\n    '/{{typo3/public_dir}}/typo3temp',\n];\n\nset('rsync', [\n    'exclude' => array_merge(get('shared_dirs'), get('shared_files'), $exclude),\n    'exclude-file' => false,\n    'include' => ['vendor'],\n    'include-file' => false,\n    'filter' => ['dir-merge,-n /.gitignore'],\n    'filter-file' => false,\n    'filter-perdir' => false,\n    'flags' => 'avz',\n    'options' => ['delete', 'keep-dirlinks', 'links'],\n    'timeout' => 600,\n]);\n\n/**\n * List of schema update types.\n * `safe` includes all necessary operations, to add or change fields or tables.\n */\nset('typo3_updateschema_types', 'safe');\n\n\n/**\n * TYPO3 Commands\n * All run via {{bin/php}} {{release_path}}/{{bin/typo3}} <command>\n */\n\ndesc('TYPO3 - Clear all caches');\ntask('typo3:cache:flush', function () {\n    run('{{bin/php}} {{release_path}}/{{bin/typo3}} cache:flush');\n});\n\ndesc('TYPO3 - Cache warmup for system caches');\ntask('typo3:cache:warmup', function () {\n    run('{{bin/php}} {{release_path}}/{{bin/typo3}} cache:warmup --group system');\n});\n\ndesc('TYPO3 - Update the language files of all activated extensions');\ntask('typo3:language:update', function () {\n    run('{{bin/php}} {{release_path}}/{{bin/typo3}} language:update');\n});\n\ndesc('TYPO3 - Set up all extensions');\ntask('typo3:extension:setup', function () {\n    run('{{bin/php}} {{release_path}}/{{bin/typo3}} extension:setup');\n});\n\ndesc('TYPO3 - Fix folder structure');\ntask('typo3:install:fixfolderstructure', function () {\n    run('{{bin/php}} {{release_path}}/{{bin/typo3}} install:fixfolderstructure');\n});\n\n/**\n * Main deploy task for TYPO3.\n *\n * 1. Lock deploy to avoid concurrent runs\n * 2. Create release directory\n * 3. Update code (Git or rsync)\n * 4. Symlink shared dirs/files\n * 5. Fix TYPO3 folder structure\n * 6. Ensure writable dirs\n * 7. Run extension setup & perform schema updates\n * 8. Update language files\n * 9. Install composer vendors\n * 10. Flush caches\n * 11. Warm up TYPO3 caches\n * 12. Unlock and clean up\n */\ndesc('Deploys a TYPO3 project');\ntask('deploy', [\n    'deploy:info',\n    'deploy:setup',\n    'deploy:lock',\n    'deploy:release',\n    'typo3:update_code',\n    'deploy:shared',\n    'deploy:writable',\n    'deploy:vendors',\n    'typo3:install:fixfolderstructure',\n    'typo3:extension:setup',\n    'typo3:language:update',\n    'typo3:cache:flush',\n    'typo3:cache:warmup',\n    'deploy:publish',\n]);\n\nafter('deploy:failed', 'deploy:unlock');\n"
  },
  {
    "path": "recipe/wordpress.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['wordpress']);\n\nset('shared_files', ['wp-config.php']);\nset('shared_dirs', ['wp-content/uploads']);\nset('writable_dirs', ['wp-content/uploads']);\n\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:publish',\n]);\n"
  },
  {
    "path": "recipe/yii.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['yii']);\n\n// Yii shared dirs\nset('shared_dirs', ['runtime']);\n\n// Yii writable dirs\nset('writable_dirs', ['runtime']);\n\ndesc('Runs Yii2 migrations for your project');\ntask('deploy:migrate', function () {\n    run('cd {{release_or_current_path}} && {{bin/php}} yii migrate --interactive=0');\n});\n\n/**\n * Main task\n */\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'deploy:migrate',\n    'deploy:publish',\n]);\n"
  },
  {
    "path": "recipe/zend_framework.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire_once __DIR__ . '/common.php';\n\nadd('recipes', ['zend_framework']);\n\n/**\n * Main task\n */\ndesc('Deploys your project');\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'deploy:publish',\n]);\n"
  },
  {
    "path": "src/Collection/Collection.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Collection;\n\nuse Countable;\nuse IteratorAggregate;\n\nclass Collection implements Countable, IteratorAggregate\n{\n    protected array $values = [];\n\n    public function all(): array\n    {\n        return $this->values;\n    }\n\n    public function get(string $name): mixed\n    {\n        if ($this->has($name)) {\n            return $this->values[$name];\n        }\n        throw $this->notFound($name);\n    }\n\n    public function has(string $name): bool\n    {\n        return array_key_exists($name, $this->values);\n    }\n\n    public function set(string $name, mixed $object)\n    {\n        $this->values[$name] = $object;\n    }\n\n    public function remove(string $name): void\n    {\n        if ($this->has($name)) {\n            unset($this->values[$name]);\n        }\n        throw $this->notFound($name);\n    }\n\n    public function count(): int\n    {\n        return count($this->values);\n    }\n\n    public function select(callable $callback): array\n    {\n        $values = [];\n\n        foreach ($this->values as $key => $value) {\n            if ($callback($value, $key)) {\n                $values[$key] = $value;\n            }\n        }\n\n        return $values;\n    }\n\n    /**\n     * @return \\ArrayIterator|\\Traversable\n     */\n    #[\\ReturnTypeWillChange]\n    public function getIterator()\n    {\n        return new \\ArrayIterator($this->values);\n    }\n\n    protected function notFound(string $name): \\InvalidArgumentException\n    {\n        return new \\InvalidArgumentException(\"Element \\\"$name\\\" not found in collection.\");\n    }\n}\n"
  },
  {
    "path": "src/Command/BlackjackCommand.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Command;\n\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Input\\InputInterface as Input;\nuse Symfony\\Component\\Console\\Output\\OutputInterface as Output;\nuse Symfony\\Component\\Console\\Question\\ChoiceQuestion;\nuse Symfony\\Component\\Console\\Style\\SymfonyStyle;\n\nuse function Deployer\\Support\\array_flatten;\n\nclass BlackjackCommand extends Command\n{\n    use CommandCommon;\n\n    /**\n     * @var Input\n     */\n    private $input;\n\n    /**\n     * @var Output\n     */\n    private $output;\n\n    public function __construct()\n    {\n        parent::__construct('blackjack');\n        $this->setDescription('Play blackjack');\n    }\n\n    protected function execute(Input $input, Output $output): int\n    {\n        $this->input = $input;\n        $this->output = $output;\n        $this->telemetry();\n        $io = new SymfonyStyle($this->input, $this->output);\n\n        if (getenv('COLORTERM') === 'truecolor') {\n            $this->print(\"\\x1b[38;2;255;95;109m╭\\x1b[39m\\x1b[38;2;255;95;107m─\\x1b[39m\\x1b[38;2;255;96;106m─\\x1b[39m\\x1b[38;2;255;96;104m─\\x1b[39m\\x1b[38;2;255;96;103m─\\x1b[39m\\x1b[38;2;255;97;101m─\\x1b[39m\\x1b[38;2;255;97;100m─\\x1b[39m\\x1b[38;2;255;97;99m─\\x1b[39m\\x1b[38;2;255;98;97m─\\x1b[39m\\x1b[38;2;255;100;98m─\\x1b[39m\\x1b[38;2;255;102;98m─\\x1b[39m\\x1b[38;2;255;104;98m─\\x1b[39m\\x1b[38;2;255;106;99m─\\x1b[39m\\x1b[38;2;255;108;99m─\\x1b[39m\\x1b[38;2;255;110;99m─\\x1b[39m\\x1b[38;2;255;112;100m─\\x1b[39m\\x1b[38;2;255;114;100m─\\x1b[39m\\x1b[38;2;255;116;100m─\\x1b[39m\\x1b[38;2;255;118;100m─\\x1b[39m\\x1b[38;2;255;120;101m─\\x1b[39m\\x1b[38;2;255;122;101m─\\x1b[39m\\x1b[38;2;255;124;101m─\\x1b[39m\\x1b[38;2;255;126;102m╮\\x1b[39m\");\n            $this->print(\"\\x1b[38;2;255;128;102m│\\x1b[39m                     \\x1b[38;2;255;130;102m│\\x1b[39m\");\n            $this->print(\"\\x1b[38;2;255;132;103m│\\x1b[39m      \\x1b[38;2;255;134;103mW\\x1b[39m\\x1b[38;2;255;136;103me\\x1b[39m\\x1b[38;2;255;138;104ml\\x1b[39m\\x1b[38;2;255;140;104mc\\x1b[39m\\x1b[38;2;255;142;104mo\\x1b[39m\\x1b[38;2;255;144;104mm\\x1b[39m\\x1b[38;2;255;146;105me\\x1b[39m\\x1b[38;2;255;148;105m!\\x1b[39m       \\x1b[38;2;255;150;105m│\\x1b[39m\");\n            $this->print(\"\\x1b[38;2;255;152;106m│\\x1b[39m                     \\x1b[38;2;255;153;106m│\\x1b[39m\");\n            $this->print(\"\\x1b[38;2;255;155;106m╰\\x1b[39m\\x1b[38;2;255;157;107m─\\x1b[39m\\x1b[38;2;255;159;107m─\\x1b[39m\\x1b[38;2;255;161;107m─\\x1b[39m\\x1b[38;2;255;163;108m─\\x1b[39m\\x1b[38;2;255;165;108m─\\x1b[39m\\x1b[38;2;255;166;108m─\\x1b[39m\\x1b[38;2;255;168;108m─\\x1b[39m\\x1b[38;2;255;170;109m─\\x1b[39m\\x1b[38;2;255;172;109m─\\x1b[39m\\x1b[38;2;255;174;109m─\\x1b[39m\\x1b[38;2;255;176;110m─\\x1b[39m\\x1b[38;2;255;177;110m─\\x1b[39m\\x1b[38;2;255;179;110m─\\x1b[39m\\x1b[38;2;255;181;111m─\\x1b[39m\\x1b[38;2;255;183;111m─\\x1b[39m\\x1b[38;2;255;185;111m─\\x1b[39m\\x1b[38;2;255;186;111m─\\x1b[39m\\x1b[38;2;255;188;112m─\\x1b[39m\\x1b[38;2;255;190;112m─\\x1b[39m\\x1b[38;2;255;192;112m─\\x1b[39m\\x1b[38;2;255;193;113m─\\x1b[39m\\x1b[38;2;255;195;113m╯\\x1b[0m\");\n        } else {\n            $this->print(\"╭─────────────────────╮\");\n            $this->print(\"│                     │\");\n            $this->print(\"│      Welcome!       │\");\n            $this->print(\"│                     │\");\n            $this->print(\"╰─────────────────────╯\");\n        }\n\n        $money = 100;\n\n        if (md5(strval(getenv('MONEY'))) === '5a7c2f336d0cc43b68951e75cdffe333') {\n            $money += 25;\n            $this->print('<fg=cyan>You got an extra $25.</>');\n        } elseif (md5(strval(getenv('MONEY'))) === '530029252abcbda4a2a2069036ccc7fc') {\n            $money += 100;\n            $this->print('<fg=cyan>You got an extra $100.</>');\n        } elseif (md5(strval(getenv('MONEY'))) === '1aa827a06ecbfa5d6fa7c62ad245f3a3') {\n            $money = 100000;\n        }\n\n        $hasWatch = true;\n        $orderWhiskey = false;\n        $whiskeyLevel = 0;\n\n        $deck = $this->newDeck();\n        $graveyard = [];\n        $dealersHand = [];\n        $playersHand = [];\n        shuffle($deck);\n        $deal = function () use (&$deck, &$graveyard) {\n            if (count($deck) == 0) {\n                shuffle($graveyard);\n                $deck = $graveyard;\n                $graveyard = [];\n            }\n            return array_pop($deck);\n        };\n\n        start:\n        $this->print(\"You have <info>$</info><info>$money</info>.\");\n        if ($money > 0) {\n            $bet = (int) $io->ask('Your bet', '5');\n            if ($bet <= 0) {\n                goto start;\n            }\n            if ($bet > $money) {\n                goto start;\n            }\n        } elseif ($hasWatch) { // @phpstan-ignore-line\n            $answer = $io->askQuestion(new ChoiceQuestion('?', ['leave', '- Here, take my watch! [$25]'], 0));\n            if ($answer == 'leave') {\n                goto leave;\n            } else {\n                $hasWatch = false;\n                $money = 25;\n                $bet = 25;\n            }\n        } else {\n            goto leave;\n        }\n\n        $graveyard = array_merge($graveyard, $dealersHand);\n        $dealersHand = [];\n        $dealersHand[] = $deal();\n        $this->print(\"Dealers hand:\");\n        $this->printHand($dealersHand);\n\n        $graveyard = array_merge($graveyard, $playersHand);\n        $playersHand = [];\n        $playersHand[] = $deal();\n        $playersHand[] = $deal();\n        $this->print(\"Your hand:\");\n        $this->printHand($playersHand, 2);\n\n        while (true) {\n            $question = new ChoiceQuestion('Your turn', ['hit', 'stand'], 0);\n            $answer = $io->askQuestion($question);\n\n            if ($answer === 'hit') {\n                $playersHand[] = $deal();\n                usleep(200000);\n            }\n\n            if ($answer === 'stand') {\n                break;\n            }\n\n            $this->printHand($playersHand);\n            $handValue = self::handValue($playersHand);\n\n            if ($handValue > 21) {\n                $this->print(\"You got <comment>$handValue</comment>.\");\n                $this->print(\"<fg=cyan>Bust!</>\");\n                $this->print(\"-<info>$</info><info>$bet</info>\");\n                $money -= $bet;\n                goto nextRound;\n            }\n        }\n\n        $this->printHand($dealersHand);\n        $this->print(\"Dealer: \" . self::handValue($dealersHand));\n        sleep(1);\n\n        while (self::handValue($dealersHand) <= 17) {\n            $dealersHand[] = $deal();\n            $this->printHand($dealersHand);\n            $this->print(\"Dealer: \" . self::handValue($dealersHand));\n            sleep(1);\n        }\n\n        $d = self::handValue($dealersHand);\n        $p = self::handValue($playersHand);\n        $this->print(\"You got <comment>$p</comment> and dealer <comment>$d</comment>.\");\n\n        if ($d > 21 || $p > $d) {\n            $this->print(\"<fg=cyan>You won!</>\");\n            $this->print(\"+<info>$</info><info>$bet</info>\");\n            $money += $bet;\n        } elseif ($p < $d) {\n            $this->print(\"<fg=cyan>You lose!</>\");\n            $this->print(\"-<info>$</info><info>$bet</info>\");\n            $money -= $bet;\n        } else {\n            $this->print(\"<fg=cyan>Push!</>\");\n        }\n\n        nextRound:\n        $choices = ['continue', 'leave'];\n        if ($orderWhiskey) {\n            $orderWhiskey = false;\n            $whiskeyLevel = 4;\n            $this->print();\n            $this->print('The waitress brought whiskey and says:');\n            $this->print(' - Your whiskey, sir.');\n            if ($money >= 5) {\n                array_push($choices, 'tip the waitress [$5]');\n            }\n        } elseif ($money >= 5) {\n            array_push($choices, 'order whiskey [$5]');\n        }\n\n        if ($whiskeyLevel > 0) {\n            $this->printWhiskey($whiskeyLevel);\n            $whiskeyLevel--;\n        }\n        $answer = $io->askQuestion(new ChoiceQuestion('?', $choices, 0));\n\n        if ($answer == 'leave') {\n            goto leave;\n        } elseif ($money >= 5 && $answer == 'order whiskey [$5]') {\n            $orderWhiskey = true;\n            $this->print('You say:');\n            $this->print(' - Whiskey, please.');\n            $money -= 5;\n        } elseif ($money >= 5 && $answer == 'tip the waitress [$5]') {\n            $this->print('The waitress says:');\n            $this->print(' - Thank you, sir!');\n            $money -= 5;\n        }\n        $this->print();\n        $this->print(\"=====> Next round <=====\");\n        goto start;\n\n        leave:\n        if ($money >= 5) {\n            $answer = $io->ask('Leave a $5 tip to the dealer?', 'yes');\n            if ($answer === 'yes') {\n                $this->print(\"You can leave a tip here:\");\n                $this->print();\n                $this->print(\"- https://github.com/sponsors/antonmedv\");\n                $this->print(\"- https://paypal.me/antonmedv\");\n                $this->print();\n            }\n        }\n        $this->print('Thanks for playing, Come again!');\n        return 0;\n    }\n\n    private function newDeck(): array\n    {\n        $deck = [];\n        foreach (['♠', '♣', '♥', '♦'] as $suit) {\n            for ($i = 2; $i <= 10; $i++) {\n                $deck[] = [strval($i), $suit];\n            }\n            $deck[] = ['J', $suit];\n            $deck[] = ['Q', $suit];\n            $deck[] = ['K', $suit];\n            $deck[] = ['A', $suit];\n        }\n        return $deck;\n    }\n\n    public static function handValue(array $hand): int\n    {\n        $aces = 0;\n        $value = 0;\n        foreach ($hand as [$rank]) {\n            switch ($rank) {\n                case '2':\n                    $value += 2;\n                    break;\n                case '3':\n                    $value += 3;\n                    break;\n                case '4':\n                    $value += 4;\n                    break;\n                case '5':\n                    $value += 5;\n                    break;\n                case '6':\n                    $value += 6;\n                    break;\n                case '7':\n                    $value += 7;\n                    break;\n                case '8':\n                    $value += 8;\n                    break;\n                case '9':\n                    $value += 9;\n                    break;\n                case '10':\n                case 'J':\n                case 'Q':\n                case 'K':\n                    $value += 10;\n                    break;\n                case 'A':\n                    $aces++;\n                    break;\n            }\n        }\n        $variants = [$value];\n        while ($aces-- > 0) {\n            $variants = array_flatten(array_map(function ($v) {\n                return [$v + 1, $v + 11];\n            }, $variants));\n        }\n        $sum = $variants[0];\n        for ($i = 1; $i < count($variants); $i++) {\n            if ($variants[$i] <= 21) {\n                $sum = $variants[$i];\n            } else {\n                break;\n            }\n        }\n        return $sum;\n    }\n\n    private function print(string $text = \"\")\n    {\n        $this->output->writeln(\" $text\");\n    }\n\n    private function printHand(array $hand, int $offset = 1)\n    {\n        $cards = [];\n        for ($i = 0; $i < count($hand) - $offset; $i++) {\n            [$rank] = $hand[$i];\n            $cards[] = [\n                \"┌───\",\n                \"│\" . str_pad($rank, 3),\n                \"│   \",\n                \"│   \",\n                \"│   \",\n                \"│   \",\n                \"└───\",\n            ];\n        }\n\n        for (; $i < count($hand); $i++) {\n            [$rank, $suit] = $hand[$i];\n            $cards[] = [\n                \"┌───────┐\",\n                \"│\" . str_pad($rank, 7) . \"│\",\n                \"│       │\",\n                \"│   \" . $suit . \"   │\",\n                \"│       │\",\n                \"│\" . str_pad($rank, 7, \" \", STR_PAD_LEFT) . \"│\",\n                \"└───────┘\",\n            ];\n        }\n\n        for ($i = 0; $i < 7; $i++) {\n            $this->output->write(\" \");\n            foreach ($cards as $lines) {\n                $this->output->write($lines[$i]);\n            }\n            $this->output->write(\"\\n\");\n        }\n    }\n\n    private function printWhiskey(int $whiskeyLevel)\n    {\n        if ($whiskeyLevel == 4) {\n            echo <<<ASCII\n\n                 |          |\n                 |__________|\n                 |          |\n                 | /\\ / /\\ /|\n                 |/_/__/__\\_|\n\n\n                ASCII;\n        }\n        if ($whiskeyLevel == 3) {\n            echo <<<ASCII\n\n                 |          |\n                 |          |\n                 |__________|\n                 | /\\ / /\\ /|\n                 |/_/__/__\\_|\n\n\n                ASCII;\n        }\n        if ($whiskeyLevel == 2) {\n            echo <<<ASCII\n\n                 |          |\n                 |          |\n                 |          |\n                 |_/\\_/_/\\_/|\n                 |/_/__/__\\_|\n\n\n                ASCII;\n        }\n        if ($whiskeyLevel == 1) {\n            echo <<<ASCII\n\n                 |          |\n                 |          |\n                 |          |\n                 | /\\ / /\\ /|\n                 |/_/__/__\\_|\n\n\n                ASCII;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Command/CommandCommon.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Command;\n\nuse Deployer\\Deployer;\nuse Deployer\\Support\\Reporter;\n\ntrait CommandCommon\n{\n    /**\n     * Collecting anonymous stat helps Deployer team improve developer experience.\n     * If you are not comfortable with this, you will always be able to disable this\n     * by setting DO_NOT_TRACK environment variable to `1`.\n     * @codeCoverageIgnore\n     */\n    protected function telemetry(array $data = []): void\n    {\n        if (getenv('DO_NOT_TRACK') === 'true') {\n            return;\n        }\n        try {\n            Reporter::report(array_merge([\n                'command_name' => $this->getName(),\n                'deployer_version' => DEPLOYER_VERSION,\n                'deployer_phar' => Deployer::isPharArchive(),\n                'php_version' => phpversion(),\n                'os' => defined('PHP_OS_FAMILY') ? PHP_OS_FAMILY : (stristr(PHP_OS, 'DAR') ? 'OSX' : (stristr(PHP_OS, 'WIN') ? 'WIN' : (stristr(PHP_OS, 'LINUX') ? 'LINUX' : PHP_OS))),\n            ], $data));\n        } catch (\\Throwable $e) {\n            return;\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/Command/ConfigCommand.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Command;\n\nuse Deployer\\Deployer;\nuse Deployer\\Exception\\WillAskUser;\nuse Deployer\\Task\\Context;\nuse Symfony\\Component\\Console\\Input\\InputInterface as Input;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\NullOutput;\nuse Symfony\\Component\\Console\\Output\\OutputInterface as Output;\nuse Symfony\\Component\\Yaml\\Yaml;\n\nclass ConfigCommand extends SelectCommand\n{\n    public function __construct(Deployer $deployer)\n    {\n        parent::__construct('config', $deployer);\n        $this->setDescription('Get all configuration options for hosts');\n    }\n\n    protected function configure()\n    {\n        parent::configure();\n        $this->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (json, yaml)', 'yaml');\n        $this->getDefinition()->getArgument('selector')->setDefault(['all']);\n    }\n\n    protected function execute(Input $input, Output $output): int\n    {\n        $this->deployer->input = $input;\n        $this->deployer->output = new NullOutput();\n        $hosts = $this->selectHosts($input, $output);\n        $data = [];\n        $keys = $this->deployer->config->keys();\n        define('DEPLOYER_NO_ASK', true);\n        foreach ($hosts as $host) {\n            Context::push(new Context($host));\n            $values = [];\n            foreach ($keys as $key) {\n                try {\n                    $values[$key] = $host->get($key);\n                } catch (WillAskUser $exception) {\n                    $values[$key] = ['ask' => $exception->getMessage()];\n                } catch (\\Throwable $exception) {\n                    $values[$key] = ['error' => $exception->getMessage()];\n                }\n            }\n            foreach ($host->config()->persist() as $k => $v) {\n                $values[$k] = $v;\n            }\n            ksort($values);\n            $data[$host->getAlias()] = $values;\n            Context::pop();\n        }\n        $format = $input->getOption('format');\n        switch ($format) {\n            case 'json':\n                $output->writeln(json_encode($data, JSON_PRETTY_PRINT));\n                break;\n\n            case 'yaml':\n                $output->write(Yaml::dump($data));\n                break;\n\n            default:\n                throw new \\Exception(\"Unknown format: $format.\");\n        }\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/Command/CustomOption.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Command;\n\nuse Deployer\\Host\\Host;\n\ntrait CustomOption\n{\n    /**\n     * @param Host[] $hosts\n     * @param string[] $options\n     */\n    protected function applyOverrides(array $hosts, array $options)\n    {\n        $override = [];\n        foreach ($options as $option) {\n            [$name, $value] = explode('=', $option);\n            $value = $this->castValueToPhpType(trim($value));\n            $override[trim($name)] = $value;\n        }\n\n        foreach ($hosts as $host) {\n            foreach ($override as $key => $value) {\n                $host->set($key, $value);\n            }\n        }\n    }\n\n    /**\n     * @param mixed $value\n     * @return bool|mixed\n     */\n    protected function castValueToPhpType($value)\n    {\n        switch ($value) {\n            case 'true':\n                return true;\n            case 'false':\n                return false;\n            default:\n                return $value;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Command/InitCommand.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Command;\n\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Console\\Style\\SymfonyStyle;\nuse Symfony\\Component\\Process\\Exception\\RuntimeException;\nuse Symfony\\Component\\Process\\PhpProcess;\nuse Symfony\\Component\\Process\\Process;\n\nclass InitCommand extends Command\n{\n    use CommandCommon;\n\n    protected function configure()\n    {\n        $this\n            ->setName('init')\n            ->setDescription('Initialize deployer in your project')\n            ->addOption('path', 'p', InputOption::VALUE_REQUIRED, 'Recipe path');\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        if (getenv('COLORTERM') === 'truecolor') {\n            $output->write(\n                <<<EOF\n                    ╭───────────────────────────────────────╮\n                    │                                       │\n                    │                                       │\n                    │    \\e[38;2;94;231;223m_\\e[39m\\e[38;2;95;231;226m_\\e[39m\\e[38;2;96;230;228m_\\e[39m\\e[38;2;96;229;230m_\\e[39m          \\e[38;2;97;226;230m_\\e[39m                    │\n                    │   \\e[38;2;98;223;229m|\\e[39m    \\e[38;2;98;220;229m\\\\\\e[39m \\e[38;2;99;216;228m_\\e[39m\\e[38;2;100;213;228m_\\e[39m\\e[38;2;101;210;228m_\\e[39m \\e[38;2;101;208;227m_\\e[39m\\e[38;2;102;205;227m_\\e[39m\\e[38;2;103;202;227m_\\e[39m\\e[38;2;104;199;226m|\\e[39m \\e[38;2;104;196;226m|\\e[39m\\e[38;2;105;194;225m_\\e[39m\\e[38;2;106;191;225m_\\e[39m\\e[38;2;106;188;225m_\\e[39m \\e[38;2;107;186;224m_\\e[39m \\e[38;2;108;183;224m_\\e[39m \\e[38;2;109;181;224m_\\e[39m\\e[38;2;109;178;223m_\\e[39m\\e[38;2;110;176;223m_\\e[39m \\e[38;2;111;174;222m_\\e[39m\\e[38;2;111;171;222m_\\e[39m\\e[38;2;112;169;222m_\\e[39m    │\n                    │   \\e[38;2;113;167;221m|\\e[39m  \\e[38;2;113;165;221m|\\e[39m  \\e[38;2;114;163;221m|\\e[39m \\e[38;2;115;160;220m-\\e[39m\\e[38;2;115;158;220m_\\e[39m\\e[38;2;116;156;219m|\\e[39m \\e[38;2;117;155;219m.\\e[39m \\e[38;2;117;153;219m|\\e[39m \\e[38;2;118;151;218m|\\e[39m \\e[38;2;119;149;218m.\\e[39m \\e[38;2;119;147;218m|\\e[39m \\e[38;2;120;145;217m|\\e[39m \\e[38;2;121;144;217m|\\e[39m \\e[38;2;121;142;216m-\\e[39m\\e[38;2;122;140;216m_\\e[39m\\e[38;2;123;139;216m|\\e[39m  \\e[38;2;123;137;215m_\\e[39m\\e[38;2;124;136;215m|\\e[39m   │\n                    │   \\e[38;2;124;134;215m|\\e[39m\\e[38;2;125;133;214m_\\e[39m\\e[38;2;126;132;214m_\\e[39m\\e[38;2;126;130;214m_\\e[39m\\e[38;2;127;129;213m_\\e[39m\\e[38;2;127;128;213m/\\e[39m\\e[38;2;130;128;212m|\\e[39m\\e[38;2;132;129;212m_\\e[39m\\e[38;2;134;129;212m_\\e[39m\\e[38;2;137;130;211m_\\e[39m\\e[38;2;139;131;211m|\\e[39m  \\e[38;2;141;131;211m_\\e[39m\\e[38;2;143;132;210m|\\e[39m\\e[38;2;145;132;210m_\\e[39m\\e[38;2;147;133;209m|\\e[39m\\e[38;2;149;133;209m_\\e[39m\\e[38;2;151;134;209m_\\e[39m\\e[38;2;153;135;208m_\\e[39m\\e[38;2;155;135;208m|\\e[39m\\e[38;2;157;136;208m_\\e[39m  \\e[38;2;159;136;207m|\\e[39m\\e[38;2;161;137;207m_\\e[39m\\e[38;2;162;137;206m_\\e[39m\\e[38;2;164;138;206m_\\e[39m\\e[38;2;166;139;206m|\\e[39m\\e[38;2;167;139;205m_\\e[39m\\e[38;2;169;140;205m|\\e[39m     │\n                    │             \\e[38;2;170;140;205m|\\e[39m\\e[38;2;172;141;204m_\\e[39m\\e[38;2;173;141;204m|\\e[39m       \\e[38;2;175;142;203m|\\e[39m\\e[38;2;176;142;203m_\\e[39m\\e[38;2;177;143;203m_\\e[39m\\e[38;2;179;143;202m_\\e[39m\\e[38;2;180;144;202m|\\e[39m           │\n                    │                                       │\n                    │                                       │\n                    ╰───────────────────────────────────────╯\n\n                    EOF,\n            );\n        } else {\n            $output->write(\n                <<<EOF\n                    ╭───────────────────────────────────────╮\n                    │                                       │\n                    │                                       │\n                    │    ____          _                    │\n                    │   |    \\ ___ ___| |___ _ _ ___ ___    │\n                    │   |  |  | -_| . | | . | | | -_|  _|   │\n                    │   |____/|___|  _|_|___|_  |___|_|     │\n                    │             |_|       |___|           │\n                    │                                       │\n                    │                                       │\n                    ╰───────────────────────────────────────╯\n\n                    EOF,\n            );\n        }\n\n        $io = new SymfonyStyle($input, $output);\n        $recipePath = $input->getOption('path');\n\n        $language = $io->choice('Select recipe language', ['php', 'yaml'], 'php');\n        if (empty($recipePath)) {\n            $recipePath = \"deploy.$language\";\n        }\n\n        // Avoid accidentally override of existing file.\n        if (file_exists($recipePath)) {\n            $io->warning(\"$recipePath already exists\");\n            if (!$io->confirm(\"Do you want to override the existing file?\", false)) {\n                $io->block('👍🏻');\n                exit(1);\n            }\n        }\n\n        // Template\n        $template = $io->choice('Select project template', $this->recipes(), 'common');\n\n        // Repo\n        $default = '';\n        try {\n            $process = Process::fromShellCommandline('git remote get-url origin');\n            $default = $process->mustRun()->getOutput();\n            $default = trim($default);\n        } catch (RuntimeException $e) {\n        }\n        $repository = $io->ask('Repository', $default);\n\n        // Guess host\n        if (preg_match('/github.com:(?<org>[A-Za-z0-9_.\\-]+)\\//', $repository, $m)) {\n            $org = $m['org'];\n            $tempHostFile = tempnam(sys_get_temp_dir(), 'temp-host-file');\n            $php = new PhpProcess(\n                <<<EOF\n                    <?php\n                    \\$ch = curl_init('https://api.github.com/orgs/$org');\n                    curl_setopt(\\$ch, CURLOPT_USERAGENT, 'Deployer');\n                    curl_setopt(\\$ch, CURLOPT_CUSTOMREQUEST, 'GET');\n                    curl_setopt(\\$ch, CURLOPT_RETURNTRANSFER, true);\n                    curl_setopt(\\$ch, CURLOPT_FOLLOWLOCATION, true);\n                    curl_setopt(\\$ch, CURLOPT_MAXREDIRS, 10);\n                    curl_setopt(\\$ch, CURLOPT_CONNECTTIMEOUT, 5);\n                    curl_setopt(\\$ch, CURLOPT_TIMEOUT, 5);\n                    \\$result = curl_exec(\\$ch);\n                    if (PHP_MAJOR_VERSION < 8) {\n                        curl_close(\\$ch);\n                    }\n                    \\$json = json_decode(\\$result);\n                    \\$host = parse_url(\\$json->blog, PHP_URL_HOST);\n                    file_put_contents('$tempHostFile', \\$host);\n                    EOF,\n            );\n            $php->start();\n        }\n\n        // Project\n        $default = '';\n        try {\n            $process = Process::fromShellCommandline('basename \"$PWD\"');\n            $default = $process->mustRun()->getOutput();\n            $default = trim($default);\n        } catch (RuntimeException $e) {\n        }\n        $project = $io->ask('Project name', $default);\n\n        // Hosts\n        $host = null;\n        if (isset($tempHostFile)) {\n            $host = file_get_contents($tempHostFile);\n        }\n        $hostsString = $io->ask('Hosts (comma separated)', $host);\n        if ($hostsString !== null) {\n            $hosts = explode(',', $hostsString);\n        } else {\n            $hosts = [];\n        }\n\n        file_put_contents($recipePath, $this->$language($template, $project, $repository, $hosts));\n\n        $this->telemetry();\n        $output->writeln(sprintf(\n            '<info>Successfully created</info> <comment>%s</comment>',\n            $recipePath,\n        ));\n        return 0;\n    }\n\n    private function php(string $template, string $project, string $repository, array $hosts): string\n    {\n        $h = \"\";\n        foreach ($hosts as $host) {\n            $h .= \"host('{$host}')\\n\" .\n                \"    ->set('remote_user', 'deployer')\\n\" .\n                \"    ->set('deploy_path', '~/{$project}');\\n\";\n        }\n\n        return <<<PHP\n            <?php\n            namespace Deployer;\n\n            require 'recipe/$template.php';\n\n            // Config\n\n            set('repository', '{$repository}');\n\n            add('shared_files', []);\n            add('shared_dirs', []);\n            add('writable_dirs', []);\n\n            // Hosts\n\n            {$h}\n            // Hooks\n\n            after('deploy:failed', 'deploy:unlock');\n\n            PHP;\n    }\n\n    private function yaml(string $template, string $project, string $repository, array $hosts): string\n    {\n        $h = \"\";\n        foreach ($hosts as $host) {\n            $h .= \"  $host:\\n\" .\n                \"    remote_user: deployer\\n\" .\n                \"    deploy_path: '~/{$project}'\\n\";\n        }\n\n        $additionalConfigs = $this->getAdditionalConfigs($template);\n\n        return <<<YAML\n            import: \n              - recipe/$template.php\n\n            config:\n              repository: '$repository'\n            $additionalConfigs\n            hosts:\n            $h\n            tasks:\n              build:\n                - run: uptime  \n\n            after:\n              deploy:failed: deploy:unlock\n\n            YAML;\n    }\n\n    private function getAdditionalConfigs(string $template): string\n    {\n        if ($template !== 'common') {\n            return '';\n        }\n\n        return <<<YAML\n              shared_files:\n                - .env\n              shared_dirs:\n                - uploads\n              writable_dirs:\n                - uploads\n              \n            YAML;\n    }\n\n    private function recipes(): array\n    {\n        $recipes = [];\n        $dir = new \\DirectoryIterator(__DIR__ . '/../../recipe');\n        foreach ($dir as $fileinfo) {\n            if ($fileinfo->isDot()) {\n                continue;\n            }\n            if ($fileinfo->isDir()) {\n                continue;\n            }\n\n            $recipe = pathinfo($fileinfo->getFilename(), PATHINFO_FILENAME);\n\n            if ($recipe === 'README') {\n                continue;\n            }\n\n            $recipes[] = $recipe;\n        }\n\n        sort($recipes);\n        return $recipes;\n    }\n}\n"
  },
  {
    "path": "src/Command/MainCommand.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Command;\n\nuse Deployer\\Deployer;\nuse Deployer\\Exception\\Exception;\nuse Deployer\\Exception\\GracefulShutdownException;\nuse Deployer\\Executor\\Planner;\nuse Deployer\\Utility\\Httpie;\nuse Symfony\\Component\\Console\\Completion\\CompletionInput;\nuse Symfony\\Component\\Console\\Completion\\CompletionSuggestions;\nuse Symfony\\Component\\Console\\Input\\InputInterface as Input;\nuse Symfony\\Component\\Console\\Input\\InputOption as Option;\nuse Symfony\\Component\\Console\\Output\\OutputInterface as Output;\n\nclass MainCommand extends SelectCommand\n{\n    use CustomOption;\n    use CommandCommon;\n\n    public function __construct(string $name, ?string $description, Deployer $deployer)\n    {\n        parent::__construct($name, $deployer);\n        if ($description) {\n            $this->setDescription($description);\n        }\n    }\n\n    protected function configure()\n    {\n        parent::configure();\n\n        // Add global options defined with `option()` func.\n        $this->getDefinition()->addOptions($this->deployer->inputDefinition->getOptions());\n\n        $this->addOption(\n            'option',\n            'o',\n            Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY,\n            'Set configuration option',\n        );\n        $this->addOption(\n            'limit',\n            'l',\n            Option::VALUE_REQUIRED,\n            'How many tasks to run in parallel?',\n        );\n        $this->addOption(\n            'no-hooks',\n            null,\n            Option::VALUE_NONE,\n            'Run tasks without after/before hooks',\n        );\n        $this->addOption(\n            'plan',\n            null,\n            Option::VALUE_NONE,\n            'Show execution plan',\n        );\n        $this->addOption(\n            'start-from',\n            null,\n            Option::VALUE_REQUIRED,\n            'Start execution from this task',\n        );\n        $this->addOption(\n            'log',\n            null,\n            Option::VALUE_REQUIRED,\n            'Write log to a file',\n        );\n        $this->addOption(\n            'profile',\n            null,\n            Option::VALUE_REQUIRED,\n            'Write profile to a file',\n        );\n    }\n\n    protected function execute(Input $input, Output $output): int\n    {\n        $this->deployer->input = $input;\n        $this->deployer->output = $output;\n        $this->deployer['log'] = $input->getOption('log');\n        $this->telemetry([\n            'project_hash' => empty($this->deployer->config['repository']) ? null : sha1($this->deployer->config['repository']),\n            'hosts_count' => $this->deployer->hosts->count(),\n            'recipes' => $this->deployer->config->get('recipes', []),\n        ]);\n\n        $hosts = $this->selectHosts($input, $output);\n        $this->applyOverrides($hosts, $input->getOption('option'));\n\n        // Save selected_hosts for selectedHosts() func.\n        $hostsAliases = [];\n        foreach ($hosts as $host) {\n            $hostsAliases[] = $host->getAlias();\n        }\n        // Save selected_hosts per each host, and not globally. Otherwise it will\n        // not be accessible for workers.\n        foreach ($hosts as $host) {\n            $host->set('selected_hosts', $hostsAliases);\n        }\n\n        $plan = $input->getOption('plan') ? new Planner($output, $hosts) : null;\n\n        $this->deployer->scriptManager->setHooksEnabled(!$input->getOption('no-hooks'));\n        $startFrom = $input->getOption('start-from');\n        if ($startFrom && !$this->deployer->tasks->has($startFrom)) {\n            throw new Exception(\"Task $startFrom does not exist.\");\n        }\n        $skippedTasks = [];\n        $tasks = $this->deployer->scriptManager->getTasks($this->getName(), $startFrom, $skippedTasks);\n\n        if (empty($tasks)) {\n            throw new Exception('No task will be executed, because the selected hosts do not meet the conditions of the tasks');\n        }\n\n        if (!$plan) {\n            $this->checkUpdates();\n            if (!empty($skippedTasks)) {\n                foreach ($skippedTasks as $taskName) {\n                    $output->writeln(\"<fg=yellow;options=bold>skip</> $taskName\");\n                }\n            }\n        }\n        $exitCode = $this->deployer->master->run($tasks, $hosts, $plan);\n\n        if ($plan) {\n            $plan->render();\n            return 0;\n        }\n\n        if ($exitCode === 0) {\n            $this->showBanner();\n            return 0;\n        }\n        if ($exitCode === GracefulShutdownException::EXIT_CODE) {\n            return 1;\n        }\n\n        // Check if we have tasks to execute on failure.\n        if ($this->deployer['fail']->has($this->getName())) {\n            $taskName = $this->deployer['fail']->get($this->getName());\n            $tasks = $this->deployer->scriptManager->getTasks($taskName);\n            $this->deployer->master->run($tasks, $hosts);\n        }\n\n        return $exitCode;\n    }\n\n    private function checkUpdates()\n    {\n        try {\n            fwrite(STDERR, Httpie::get('https://deployer.org/check-updates/' . DEPLOYER_VERSION)->send());\n        } catch (\\Throwable $e) {\n            // Meh\n        }\n    }\n\n    private function showBanner()\n    {\n        if (getenv('DO_NOT_SHOW_BANNER') === 'true') {\n            return;\n        }\n\n        try {\n            $withColors = '';\n            if (function_exists('posix_isatty') && posix_isatty(STDOUT)) {\n                $withColors = '_with_colors';\n            }\n            fwrite(STDERR, Httpie::get(\"https://deployer.medv.io/banners/\" . $this->getName() . $withColors)->send());\n        } catch (\\Throwable $e) {\n            // Meh\n        }\n    }\n\n    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void\n    {\n        parent::complete($input, $suggestions);\n        if ($input->mustSuggestOptionValuesFor('start-from')) {\n            $taskNames = [];\n            foreach ($this->deployer->scriptManager->getTasks($this->getName()) as $task) {\n                $taskNames[] = $task->getName();\n            }\n            $suggestions->suggestValues($taskNames);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Command/RunCommand.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Command;\n\nuse Deployer\\Deployer;\nuse Deployer\\Task\\Context;\nuse Deployer\\Task\\Task;\nuse Symfony\\Component\\Console\\Input\\InputArgument;\nuse Symfony\\Component\\Console\\Input\\InputInterface as Input;\nuse Symfony\\Component\\Console\\Input\\InputOption as Option;\nuse Symfony\\Component\\Console\\Output\\OutputInterface as Output;\n\nuse function Deployer\\cd;\nuse function Deployer\\get;\nuse function Deployer\\has;\nuse function Deployer\\run;\nuse function Deployer\\test;\n\nclass RunCommand extends SelectCommand\n{\n    use CustomOption;\n\n    public function __construct(Deployer $deployer)\n    {\n        parent::__construct('run', $deployer);\n        $this->setDescription('Run any arbitrary command on hosts');\n    }\n\n    protected function configure()\n    {\n        $this->addArgument(\n            'command-to-run',\n            InputArgument::REQUIRED,\n            'Command to run on a remote host',\n        );\n        parent::configure();\n        $this->addOption(\n            'option',\n            'o',\n            Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY,\n            'Set configuration option',\n        );\n        $this->addOption(\n            'timeout',\n            't',\n            Option::VALUE_REQUIRED,\n            'Command timeout in seconds',\n        );\n    }\n\n    protected function execute(Input $input, Output $output): int\n    {\n        $this->deployer->input = $input;\n        $this->deployer->output = $output;\n\n        $command = $input->getArgument('command-to-run') ?? '';\n        $hosts = $this->selectHosts($input, $output);\n        $this->applyOverrides($hosts, $input->getOption('option'));\n\n        $task = new Task($command, function () use ($input, $command) {\n            if (has('current_path')) {\n                $path = get('current_path');\n                if (test(\"[ -d $path ]\")) {\n                    cd($path);\n                }\n            }\n            run(\n                $command,\n                timeout: intval($input->getOption('timeout')),\n                forceOutput: true,\n            );\n        });\n\n        foreach ($hosts as $host) {\n            try {\n                $task->run(new Context($host));\n            } catch (\\Throwable $exception) {\n                $this->deployer->messenger->renderException($exception, $host);\n            }\n        }\n\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/Command/SelectCommand.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Command;\n\nuse Deployer\\Deployer;\nuse Deployer\\Exception\\ConfigurationException;\nuse Deployer\\Exception\\Exception;\nuse Deployer\\Host\\Host;\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Completion\\CompletionInput;\nuse Symfony\\Component\\Console\\Completion\\CompletionSuggestions;\nuse Symfony\\Component\\Console\\Formatter\\OutputFormatterStyle;\nuse Symfony\\Component\\Console\\Helper\\QuestionHelper;\nuse Symfony\\Component\\Console\\Input\\InputArgument;\nuse Symfony\\Component\\Console\\Input\\InputInterface as Input;\nuse Symfony\\Component\\Console\\Output\\OutputInterface as Output;\nuse Symfony\\Component\\Console\\Question\\ChoiceQuestion;\n\nabstract class SelectCommand extends Command\n{\n    /**\n     * @var Deployer\n     */\n    protected $deployer;\n\n    public function __construct(string $name, Deployer $deployer)\n    {\n        $this->deployer = $deployer;\n        parent::__construct($name);\n    }\n\n    protected function configure()\n    {\n        $this->addArgument('selector', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Host selector');\n    }\n\n    /**\n     * @return Host[]\n     */\n    protected function selectHosts(Input $input, Output $output): array\n    {\n        $output->getFormatter()->setStyle('success', new OutputFormatterStyle('green'));\n        if (!$output->isDecorated() && !defined('NO_ANSI')) {\n            define('NO_ANSI', 'true');\n        }\n        $selector = $input->getArgument('selector');\n        $selector = empty($selector) ? Deployer::get()->config->get('default_selector', '') : $selector;\n        $selectExpression = is_array($selector) ? implode(',', $selector) : $selector;\n\n        if (empty($selectExpression)) {\n            if (count($this->deployer->hosts) === 0) {\n                throw new ConfigurationException(\"No host configured.\\nSpecify at least one host: `localhost();`.\");\n            } elseif (count($this->deployer->hosts) === 1) {\n                $hosts = $this->deployer->hosts->all();\n            } elseif ($input->isInteractive()) {\n                $hostsAliases = [];\n                foreach ($this->deployer->hosts as $host) {\n                    $hostsAliases[] = $host->getAlias();\n                }\n                /** @var QuestionHelper $helper */\n                $helper = $this->getHelper('question');\n                $question = new ChoiceQuestion(\n                    '<question>Select hosts:</question> (comma separated)',\n                    $hostsAliases,\n                );\n                $question->setMultiselect(true);\n                $question->setErrorMessage('There is no \"%s\" host.');\n                $answer = $helper->ask($input, $output, $question);\n                $answer = array_unique($answer);\n                $hosts = $this->deployer->hosts->select(function (Host $host) use ($answer) {\n                    return in_array($host->getAlias(), $answer, true);\n                });\n            }\n        } else {\n            $hosts = $this->deployer->selector->select($selectExpression);\n        }\n\n        if (empty($hosts)) {\n            $message = 'No host selected.';\n            if (!empty($selectExpression)) {\n                $message .= \" Please, check your selector:\\n\\n    $selectExpression\";\n            }\n            throw new Exception($message);\n        }\n\n        return $hosts;\n    }\n\n    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void\n    {\n        parent::complete($input, $suggestions);\n        if ($input->mustSuggestArgumentValuesFor('selector')) {\n            $selectors = ['all'];\n            $configs = [];\n            foreach ($this->deployer->hosts as $host) {\n                $configs[$host->getAlias()] = $host->config()->persist();\n            }\n            foreach ($configs as $alias => $c) {\n                $selectors[] = $alias;\n                foreach ($c['labels'] ?? [] as $label => $value) {\n                    $selectors[] = \"$label=$value\";\n                }\n            }\n            $selectors = array_unique($selectors);\n            $suggestions->suggestValues($selectors);\n        }\n        if ($input->mustSuggestOptionValuesFor('option')) {\n            $values = [];\n            foreach ($this->deployer->config->keys() as $key) {\n                $values[] = $key . '=';\n            }\n            $suggestions->suggestValues($values);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Command/SshCommand.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Command;\n\nuse Deployer\\Deployer;\nuse Deployer\\Host\\Localhost;\nuse Deployer\\Task\\Context;\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Completion\\CompletionInput;\nuse Symfony\\Component\\Console\\Completion\\CompletionSuggestions;\nuse Symfony\\Component\\Console\\Helper\\QuestionHelper;\nuse Symfony\\Component\\Console\\Input\\InputArgument;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Console\\Question\\ChoiceQuestion;\n\n/**\n * @codeCoverageIgnore\n */\nclass SshCommand extends Command\n{\n    use CommandCommon;\n\n    /**\n     * @var Deployer\n     */\n    private $deployer;\n\n    public function __construct(Deployer $deployer)\n    {\n        parent::__construct('ssh');\n        $this->setDescription('Connect to host through ssh');\n        $this->deployer = $deployer;\n    }\n\n    protected function configure()\n    {\n        $this->addArgument(\n            'hostname',\n            InputArgument::OPTIONAL,\n            'Hostname',\n        );\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        $this->telemetry();\n        $hostname = $input->getArgument('hostname');\n        if (!empty($hostname)) {\n            $host = $this->deployer->hosts->get($hostname);\n        } else {\n            $hostsAliases = [];\n            foreach ($this->deployer->hosts as $host) {\n                if ($host instanceof Localhost) {\n                    continue;\n                }\n                $hostsAliases[] = $host->getAlias();\n            }\n\n            if (count($hostsAliases) === 0) {\n                $output->writeln('No remote hosts.');\n                return 2; // Because there are no hosts.\n            }\n\n            if (count($hostsAliases) === 1) {\n                $host = $this->deployer->hosts->get($hostsAliases[0]);\n            } else {\n                /** @var QuestionHelper $helper */\n                $helper = $this->getHelper('question');\n                $question = new ChoiceQuestion(\n                    '<question>Select host:</question>',\n                    $hostsAliases,\n                );\n                $question->setErrorMessage('There is no \"%s\" host.');\n\n                $hostname = $helper->ask($input, $output, $question);\n                $host = $this->deployer->hosts->get($hostname);\n            }\n        }\n\n        $shell_path = 'exec $SHELL -l';\n        if ($host->has('shell_path')) {\n            $shell_path = 'exec ' . $host->get('shell_path') . ' -l';\n        }\n\n        Context::push(new Context($host));\n        $host->setSshMultiplexing(false);\n        $options = $host->connectionOptionsString();\n        $deployPath = $host->get('deploy_path', '~');\n\n        if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {\n            passthru(\"ssh -t $options {$host->connectionString()} \\\"cd $deployPath/current 2>/dev/null || cd $deployPath; $shell_path\\\"\");\n        } else {\n            passthru(\"ssh -t $options {$host->connectionString()} 'cd $deployPath/current 2>/dev/null || cd $deployPath; $shell_path'\");\n        }\n        return 0;\n    }\n\n    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void\n    {\n        parent::complete($input, $suggestions);\n        if ($input->mustSuggestArgumentValuesFor('hostname')) {\n            $suggestions->suggestValues(array_keys($this->deployer->hosts->all()));\n        }\n    }\n}\n"
  },
  {
    "path": "src/Command/TreeCommand.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Command;\n\nuse Deployer\\Deployer;\nuse Deployer\\Task\\GroupTask;\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Completion\\CompletionInput;\nuse Symfony\\Component\\Console\\Completion\\CompletionSuggestions;\nuse Symfony\\Component\\Console\\Input\\InputArgument;\nuse Symfony\\Component\\Console\\Input\\InputInterface as Input;\nuse Symfony\\Component\\Console\\Output\\OutputInterface as Output;\n\nclass TreeCommand extends Command\n{\n    /**\n     * @var Output\n     */\n    protected $output;\n    /**\n     * @var Deployer\n     */\n    private $deployer;\n    /**\n     * @var array\n     */\n    private $tree;\n    /**\n     * @var int\n     */\n    private $depth = 0;\n    /**\n     * @var array\n     */\n    private $openGroupDepths = [];\n\n    public function __construct(Deployer $deployer)\n    {\n        parent::__construct('tree');\n        $this->setDescription('Display the task-tree for a given task');\n        $this->deployer = $deployer;\n        $this->tree = [];\n    }\n\n    protected function configure()\n    {\n        $this->addArgument(\n            'task',\n            InputArgument::REQUIRED,\n            'Task to display the tree for',\n        );\n    }\n\n    protected function execute(Input $input, Output $output): int\n    {\n        $this->output = $output;\n\n        $rootTaskName = $input->getArgument('task');\n\n        $this->buildTree($rootTaskName);\n        $this->outputTree($rootTaskName);\n        return 0;\n    }\n\n    private function buildTree(string $taskName)\n    {\n        $this->createTreeFromTaskName($taskName, '', true);\n    }\n\n    private function createTreeFromTaskName(string $taskName, string $postfix = '', bool $isLast = false)\n    {\n        $task = $this->deployer->tasks->get($taskName);\n\n        if (!$task->isEnabled()) {\n            if (empty($postfix)) {\n                $postfix = '  // disabled';\n            } else {\n                $postfix .= '; disabled';\n            }\n        }\n\n        if ($task->getBefore()) {\n            $beforePostfix = sprintf(\"  // before %s\", $task->getName());\n\n            foreach ($task->getBefore() as $beforeTask) {\n                $this->createTreeFromTaskName($beforeTask, $beforePostfix);\n            }\n        }\n\n        if ($task instanceof GroupTask) {\n            $isLast = $isLast && empty($task->getAfter());\n\n            $this->addTaskToTree($task->getName() . $postfix, $isLast);\n\n            if (!$isLast) {\n                $this->openGroupDepths[] = $this->depth;\n            }\n\n            $this->depth++;\n\n            $taskGroup = $task->getGroup();\n            foreach ($taskGroup as $subtask) {\n                $isLastSubtask = $subtask === end($taskGroup);\n                $this->createTreeFromTaskName($subtask, '', $isLastSubtask);\n            }\n\n            if (!$isLast) {\n                array_pop($this->openGroupDepths);\n            }\n\n            $this->depth--;\n        } else {\n            $this->addTaskToTree($task->getName() . $postfix, $isLast);\n        }\n\n        if ($task->getAfter()) {\n            $afterPostfix = sprintf(\"  // after %s\", $task->getName());\n\n            foreach ($task->getAfter() as $afterTask) {\n                $this->createTreeFromTaskName($afterTask, $afterPostfix);\n            }\n        }\n    }\n\n    private function addTaskToTree(string $taskName, bool $isLast = false)\n    {\n        $this->tree[] = [\n            'taskName' => $taskName,\n            'depth' => $this->depth,\n            'isLast' => $isLast,\n            'openDepths' => $this->openGroupDepths,\n        ];\n    }\n\n    private function outputTree(string $taskName)\n    {\n        $this->output->writeln(\"The task-tree for <info>$taskName</info>:\");\n\n        /**\n         * @var int number of spaces for each depth increase\n         */\n        $REPEAT_COUNT = 4;\n\n        foreach ($this->tree as $treeItem) {\n            $depth = $treeItem['depth'];\n\n            $startSymbol = $treeItem['isLast'] || $treeItem === end($this->tree) ? '└' : '├';\n\n            $prefix = '';\n\n            for ($i = 0; $i < $depth; $i++) {\n                if (in_array($i, $treeItem['openDepths'])) {\n                    $prefix .= '│' . str_repeat(' ', $REPEAT_COUNT - 1);\n                } else {\n                    $prefix .= str_repeat(' ', $REPEAT_COUNT);\n                }\n            }\n\n            $prefix .= $startSymbol . '──';\n\n            $this->output->writeln(sprintf('%s %s', $prefix, $treeItem['taskName']));\n        }\n    }\n\n    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void\n    {\n        parent::complete($input, $suggestions);\n        if ($input->mustSuggestArgumentValuesFor('task')) {\n            $suggestions->suggestValues(array_keys($this->deployer->tasks->all()));\n        }\n    }\n}\n"
  },
  {
    "path": "src/Command/WorkerCommand.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Command;\n\nuse Deployer\\Deployer;\nuse Deployer\\Executor\\Worker;\nuse Deployer\\Host\\Localhost;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption as Option;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nuse function Deployer\\localhost;\n\nclass WorkerCommand extends MainCommand\n{\n    public function __construct(Deployer $deployer)\n    {\n        parent::__construct('worker', null, $deployer);\n        $this->setHidden(true);\n    }\n\n    protected function configure()\n    {\n        parent::configure();\n        $this->addOption('task', null, Option::VALUE_REQUIRED);\n        $this->addOption('host', null, Option::VALUE_REQUIRED);\n        $this->addOption('port', null, Option::VALUE_REQUIRED);\n        $this->addOption('decorated', null, Option::VALUE_NONE);\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        $this->deployer->input = $input;\n        $this->deployer->output = $output;\n        $this->deployer['log'] = $input->getOption('log');\n        $output->setDecorated($input->getOption('decorated'));\n        if (!$output->isDecorated() && !defined('NO_ANSI')) {\n            define('NO_ANSI', 'true');\n        }\n\n        define('MASTER_ENDPOINT', 'http://localhost:' . $input->getOption('port'));\n\n        $task = $this->deployer->tasks->get($input->getOption('task'));\n        $host = $this->deployer->hosts->get($input->getOption('host'));\n        $host->config()->load();\n\n        $worker = new Worker($this->deployer);\n        $exitCode = $worker->execute($task, $host);\n\n        $host->config()->save();\n        return $exitCode;\n    }\n}\n"
  },
  {
    "path": "src/Component/PharUpdate/Console/Command.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Console;\n\nuse Deployer\\Component\\PharUpdate\\Manager;\nuse LogicException;\nuse Symfony\\Component\\Console\\Command\\Command as Base;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\n/**\n * Manages updating or upgrading the Phar.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass Command extends Base\n{\n    /**\n     * Disable the ability to upgrade?\n     *\n     * @var boolean\n     */\n    private $disableUpgrade = false;\n\n    /**\n     * The manifest file URI.\n     *\n     * @var string\n     */\n    private $manifestUri;\n\n    /**\n     * The running file (the Phar that will be updated).\n     *\n     * @var string\n     */\n    private $runningFile;\n\n    /**\n     * @param string $name The command name.\n     * @param boolean $disable Disable upgrading?\n     */\n    public function __construct(string $name, bool $disable = false)\n    {\n        $this->disableUpgrade = $disable;\n\n        parent::__construct($name);\n    }\n\n    /**\n     * Sets the manifest URI.\n     *\n     * @param string $uri The URI.\n     */\n    public function setManifestUri(string $uri)\n    {\n        $this->manifestUri = $uri;\n    }\n\n    /**\n     * Sets the running file (the Phar that will be updated).\n     *\n     * @param string $file The file name or path.\n     */\n    public function setRunningFile(string $file): void\n    {\n        $this->runningFile = $file;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function configure()\n    {\n        $this->setDescription('Updates the application.');\n        $this->addOption(\n            'pre',\n            'p',\n            InputOption::VALUE_NONE,\n            'Allow pre-release updates.',\n        );\n        $this->addOption(\n            'redo',\n            'r',\n            InputOption::VALUE_NONE,\n            'Redownload update if already using current version.',\n        );\n\n        if (false === $this->disableUpgrade) {\n            $this->addOption(\n                'upgrade',\n                'u',\n                InputOption::VALUE_NONE,\n                'Upgrade to next major release, if available.',\n            );\n        }\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        if (null === $this->manifestUri) {\n            throw new LogicException(\n                'No manifest URI has been configured.',\n            );\n        }\n\n        $output->writeln('Looking for updates...');\n\n        /** @var Helper */\n        $pharUpdate = $this->getHelper('phar-update');\n        /** @var Manager $manager */\n        $manager = $pharUpdate->getManager($this->manifestUri);\n        $manager->setRunningFile($this->runningFile);\n\n        if ($manager->update(\n            $this->getApplication()->getVersion(),\n            $this->disableUpgrade ?: (false === $input->getOption('upgrade')),\n            $input->getOption('pre'),\n        )) {\n            $output->writeln('<info>Update successful!</info>');\n        } else {\n            $output->writeln('<comment>Already up-to-date.</comment>');\n        }\n\n        return self::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "src/Component/PharUpdate/Console/Helper.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Console;\n\nuse Deployer\\Component\\PharUpdate\\Manager;\nuse Deployer\\Component\\PharUpdate\\Manifest;\nuse Symfony\\Component\\Console\\Helper\\Helper as Base;\n\n/**\n * The helper provides a Manager factory.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass Helper extends Base\n{\n    /**\n     * Returns the update manager.\n     *\n     * @param string $uri The manifest file URI.\n     *\n     * @return Manager The update manager.\n     */\n    public function getManager(string $uri): Manager\n    {\n        return new Manager(Manifest::loadFile($uri));\n    }\n\n    public function getName(): string\n    {\n        return 'phar-update';\n    }\n}\n"
  },
  {
    "path": "src/Component/PharUpdate/Exception/Exception.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Exception;\n\n/**\n * Provides additional functional to the Exception class.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass Exception extends \\Exception implements ExceptionInterface\n{\n    /**\n     * Creates a new exception using a format and values.\n     *\n     * @param mixed  $value,... The value(s).\n     */\n    public static function create(string $format, $value = null): self\n    {\n        if (0 < func_num_args()) {\n            $format = vsprintf($format, array_slice(func_get_args(), 1));\n        }\n\n        return new static($format);\n    }\n\n    /**\n     * Creates an exception for the last error message.\n     */\n    public static function lastError(): self\n    {\n        $error = error_get_last();\n\n        return new static($error['message']);\n    }\n}\n"
  },
  {
    "path": "src/Component/PharUpdate/Exception/ExceptionInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Exception;\n\n/**\n * Indicates that the exception came from this library.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\ninterface ExceptionInterface {}\n"
  },
  {
    "path": "src/Component/PharUpdate/Exception/FileException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Exception;\n\n/**\n * Used for errors when using the file system.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass FileException extends Exception {}\n"
  },
  {
    "path": "src/Component/PharUpdate/Exception/InvalidArgumentException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Exception;\n\n/**\n * Used if an invalid argument is given.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass InvalidArgumentException extends Exception {}\n"
  },
  {
    "path": "src/Component/PharUpdate/Exception/LogicException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Exception;\n\n/**\n * Used if developer did something stupid (or overlooked something).\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass LogicException extends Exception {}\n"
  },
  {
    "path": "src/Component/PharUpdate/Manager.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate;\n\nuse Deployer\\Component\\PharUpdate\\Exception\\InvalidArgumentException;\nuse Deployer\\Component\\PharUpdate\\Version\\Parser;\nuse Deployer\\Component\\PharUpdate\\Version\\Version;\n\n/**\n * Manages the Phar update process.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass Manager\n{\n    /**\n     * The update manifest.\n     *\n     * @var Manifest\n     */\n    private $manifest;\n\n    /**\n     * The running file (the Phar that will be updated).\n     *\n     * @var string\n     */\n    private $runningFile;\n\n    /**\n     * Sets the update manifest.\n     *\n     * @param Manifest $manifest The manifest.\n     */\n    public function __construct(Manifest $manifest)\n    {\n        $this->manifest = $manifest;\n    }\n\n    /**\n     * Returns the manifest.\n     *\n     * @return Manifest The manifest.\n     */\n    public function getManifest(): Manifest\n    {\n        return $this->manifest;\n    }\n\n    /**\n     * Returns the running file (the Phar that will be updated).\n     *\n     * @return string The file.\n     */\n    public function getRunningFile(): string\n    {\n        if (null === $this->runningFile) {\n            $this->runningFile = realpath($_SERVER['argv'][0]);\n        }\n\n        return $this->runningFile;\n    }\n\n    /**\n     * Sets the running file (the Phar that will be updated).\n     *\n     * @param string $file The file name or path.\n     *\n     * @throws Exception\\Exception\n     * @throws InvalidArgumentException If the file path is invalid.\n     */\n    public function setRunningFile(string $file): void\n    {\n        if (false === is_file($file)) {\n            throw InvalidArgumentException::create(\n                'The file \"%s\" is not a file or it does not exist.',\n                $file,\n            );\n        }\n\n        $this->runningFile = $file;\n    }\n\n    /**\n     * Updates the running Phar if any is available.\n     *\n     * @param string|Version $version  The current version.\n     * @param boolean        $major    Lock to current major version?\n     * @param boolean        $pre      Allow pre-releases?\n     *\n     * @return boolean TRUE if an update was performed, FALSE if none available.\n     */\n    public function update($version, bool $major = false, bool $pre = false): bool\n    {\n        if (false === ($version instanceof Version)) {\n            $version = Parser::toVersion($version);\n        }\n\n        if (null !== ($update = $this->manifest->findRecent(\n            $version,\n            $major,\n            $pre,\n        ))) {\n            $update->getFile();\n            $update->copyTo($this->getRunningFile());\n\n            return true;\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/Component/PharUpdate/Manifest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate;\n\nuse Deployer\\Component\\PharUpdate\\Version\\Comparator;\nuse Deployer\\Component\\PharUpdate\\Version\\Parser;\nuse Deployer\\Component\\PharUpdate\\Version\\Version;\n\n/**\n * Manages the contents of an updates manifest file.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass Manifest\n{\n    /**\n     * The list of updates in the manifest.\n     *\n     * @var Update[]\n     */\n    private $updates;\n\n    /**\n     * Sets the list of updates from the manifest.\n     *\n     * @param Update[] $updates The updates.\n     */\n    public function __construct(array $updates = [])\n    {\n        $this->updates = $updates;\n    }\n\n    /**\n     * Finds the most recent update and returns it.\n     *\n     * @param Version $version The current version.\n     * @param boolean $major   Lock to major version?\n     * @param boolean $pre     Allow pre-releases?\n     */\n    public function findRecent(Version $version, bool $major = false, bool $pre = false): ?Update\n    {\n        /** @var Update|null */\n        $current = null;\n        $major = $major ? $version->getMajor() : null;\n\n        foreach ($this->updates as $update) {\n            if ($major && ($major !== $update->getVersion()->getMajor())) {\n                continue;\n            }\n\n            if ((false === $pre)\n                && !$update->getVersion()->isStable()) {\n                continue;\n            }\n\n            $test = $current ? $current->getVersion() : $version;\n\n            if (false === $update->isNewer($test)) {\n                continue;\n            }\n\n            $current = $update;\n        }\n\n        return $current;\n    }\n\n    /**\n     * Returns the list of updates in the manifest.\n     *\n     * @return Update[] The updates.\n     */\n    public function getUpdates(): array\n    {\n        return $this->updates;\n    }\n\n    /**\n     * Loads the manifest from a JSON encoded string.\n     *\n     * @param string $json The JSON encoded string.\n     */\n    public static function load(string $json): self\n    {\n        return self::create(json_decode($json));\n    }\n\n    /**\n     * Loads the manifest from a JSON encoded file.\n     *\n     * @param string $file The JSON encoded file.\n     */\n    public static function loadFile(string $file): self\n    {\n        return self::create(json_decode(file_get_contents($file)));\n    }\n\n    /**\n     * Validates the data, processes it, and returns a new instance of Manifest.\n     *\n     * @param array $decoded The decoded JSON data.\n     *\n     * @return static The new instance.\n     */\n    private static function create(array $decoded): self\n    {\n        $updates = [];\n\n        foreach ($decoded as $update) {\n            $updates[] = new Update(\n                $update->name,\n                $update->sha1,\n                $update->url,\n                Parser::toVersion($update->version),\n                $update->publicKey ?? null,\n            );\n        }\n\n        usort(\n            $updates,\n            function (Update $a, Update $b) {\n                return Comparator::isGreaterThan(\n                    $a->getVersion(),\n                    $b->getVersion(),\n                ) ? 1 : 0;\n            },\n        );\n\n        return new static($updates);\n    }\n}\n"
  },
  {
    "path": "src/Component/PharUpdate/Update.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate;\n\nuse Deployer\\Component\\PharUpdate\\Exception\\FileException;\nuse Deployer\\Component\\PharUpdate\\Exception\\LogicException;\nuse Deployer\\Component\\PharUpdate\\Version\\Comparator;\nuse Deployer\\Component\\PharUpdate\\Version\\Version;\nuse Phar;\nuse SplFileObject;\nuse UnexpectedValueException;\n\n/**\n * Manages an individual update.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass Update\n{\n    /**\n     * The temporary file path.\n     *\n     * @var string|null\n     */\n    private $file;\n\n    /**\n     * The name of the update file.\n     *\n     * @var string\n     */\n    private $name;\n\n    /**\n     * The URL where the public key can be downloaded from.\n     *\n     * @var string\n     */\n    private $publicKey;\n\n    /**\n     * The SHA1 file checksum.\n     *\n     * @var string\n     */\n    private $sha1;\n\n    /**\n     * The URL where the update can be downloaded from.\n     *\n     * @var string\n     */\n    private $url;\n\n    /**\n     * The version of the update.\n     *\n     * @var Version\n     */\n    private $version;\n\n    /**\n     * Sets the update information.\n     *\n     * @param string  $name    The name of the update file.\n     * @param string  $sha1    The SHA1 file checksum.\n     * @param string  $url     The URL where the update can be downloaded from.\n     * @param Version $version The version of the update.\n     * @param string  $key     The URL where the public key can be downloaded\n     *                         from.\n     */\n    public function __construct(\n        string $name,\n        string $sha1,\n        string $url,\n        Version $version,\n        string $key = null,\n    ) {\n        $this->name = $name;\n        $this->publicKey = $key;\n        $this->sha1 = $sha1;\n        $this->url = $url;\n        $this->version = $version;\n    }\n\n    /**\n     * Copies the update file to the destination.\n     *\n     * @param string $file The target file.\n     *\n     * @throws Exception\\Exception\n     * @throws FileException If the file could not be replaced.\n     */\n    public function copyTo(string $file): void\n    {\n        if (null === $this->file) {\n            throw LogicException::create(\n                'The update file has not been downloaded.',\n            );\n        }\n\n        $mode = 0o755;\n\n        if (file_exists($file)) {\n            $mode = fileperms($file) & 511;\n        }\n\n        if (false === @copy($this->file, $file)) {\n            throw FileException::lastError();\n        }\n\n        if (false === @chmod($file, $mode)) {\n            throw FileException::lastError();\n        }\n\n        $key = $file . '.pubkey';\n\n        if (file_exists($this->file . '.pubkey')) {\n            if (false === @copy($this->file . '.pubkey', $key)) {\n                throw FileException::lastError();\n            }\n        } elseif (file_exists($key)) {\n            if (false === @unlink($key)) {\n                throw FileException::lastError();\n            }\n        }\n    }\n\n    /**\n     * Cleans up by deleting the temporary update file.\n     *\n     * @throws FileException If the file could not be deleted.\n     */\n    public function deleteFile(): void\n    {\n        if ($this->file) {\n            if (file_exists($this->file)) {\n                if (false === @unlink($this->file)) {\n                    throw FileException::lastError();\n                }\n            }\n\n            if (file_exists($this->file . '.pubkey')) {\n                if (false === @unlink($this->file . '.pubkey')) {\n                    throw FileException::lastError();\n                }\n            }\n\n            $dir = dirname($this->file);\n\n            if (file_exists($dir)) {\n                if (false === @rmdir($dir)) {\n                    throw FileException::lastError();\n                }\n            }\n\n            $this->file = null;\n        }\n    }\n\n    /**\n     * Downloads the update file to a temporary location.\n     *\n     * @return string The temporary file path.\n     *\n     * @throws Exception\\Exception\n     * @throws FileException            If the SHA1 checksum differs.\n     * @throws UnexpectedValueException If the Phar is corrupt.\n     */\n    public function getFile(): ?string\n    {\n        if (null === $this->file) {\n            unlink($this->file = tempnam(sys_get_temp_dir(), 'upd'));\n            mkdir($this->file);\n\n            $this->file .= DIRECTORY_SEPARATOR . $this->name;\n\n            $in = new SplFileObject($this->url, 'rb', false);\n            $out = new SplFileObject($this->file, 'wb', false);\n\n            while (false === $in->eof()) {\n                $out->fwrite($in->fgets());\n            }\n\n            unset($in, $out);\n\n            if ($this->publicKey) {\n                $in = new SplFileObject($this->publicKey, 'r', false);\n                $out = new SplFileObject($this->file . '.pubkey', 'w', false);\n\n                while (false === $in->eof()) {\n                    $out->fwrite($in->fgets());\n                }\n\n                unset($in, $out);\n            }\n\n            if ($this->sha1 !== ($sha1 = sha1_file($this->file))) {\n                $this->deleteFile();\n\n                throw FileException::create(\n                    'Mismatch of the SHA1 checksum (%s) of the downloaded file (%s).',\n                    $this->sha1,\n                    $sha1,\n                );\n            }\n\n            // double check\n            try {\n                new Phar($this->file);\n            } catch (UnexpectedValueException $exception) {\n                $this->deleteFile();\n\n                throw $exception;\n            }\n        }\n\n        return $this->file;\n    }\n\n    /**\n     * Returns name of the update file.\n     */\n    public function getName(): string\n    {\n        return $this->name;\n    }\n\n    /**\n     * Returns the URL where the public key can be downloaded from.\n     */\n    public function getPublicKey(): string\n    {\n        return $this->publicKey;\n    }\n\n    /**\n     * Returns the SHA1 file checksum.\n     */\n    public function getSha1(): string\n    {\n        return $this->sha1;\n    }\n\n    /**\n     * Returns the URL where the update can be downloaded from.\n     */\n    public function getUrl(): string\n    {\n        return $this->url;\n    }\n\n    /**\n     * Returns the version of the update.\n     */\n    public function getVersion(): Version\n    {\n        return $this->version;\n    }\n\n    /**\n     * Checks if this update is newer than the version given.\n     *\n     * @param Version $version The current version.\n     *\n     * @return boolean TRUE if the update is newer, FALSE if not.\n     */\n    public function isNewer(Version $version): bool\n    {\n        return Comparator::isGreaterThan($this->version, $version);\n    }\n}\n"
  },
  {
    "path": "src/Component/PharUpdate/Version/Builder.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Version;\n\nuse Deployer\\Component\\PharUpdate\\Version\\Exception\\InvalidIdentifierException;\nuse Deployer\\Component\\PharUpdate\\Version\\Exception\\InvalidNumberException;\n\n/**\n * Builds a new version number.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass Builder extends Version\n{\n    /**\n     * Removes the build metadata identifiers.\n     */\n    public function clearBuild(): void\n    {\n        $this->build = [];\n    }\n\n    /**\n     * Removes the pre-release version identifiers.\n     */\n    public function clearPreRelease(): void\n    {\n        $this->preRelease = [];\n    }\n\n    /**\n     * Creates a new Version builder.\n     *\n     * @return Builder The Version builder.\n     */\n    public static function create(): Builder\n    {\n        return new Builder();\n    }\n\n    /**\n     * Returns a readonly Version instance.\n     *\n     * @return Version The readonly Version instance.\n     */\n    public function getVersion(): Version\n    {\n        return new Version(\n            $this->major,\n            $this->minor,\n            $this->patch,\n            $this->preRelease,\n            $this->build,\n        );\n    }\n\n    /**\n     * Imports the version components.\n     *\n     * @param array $components The components.\n     *\n     * @return Builder The Version builder.\n     */\n    public function importComponents(array $components): self\n    {\n        if (isset($components[Parser::BUILD])) {\n            $this->build = $components[Parser::BUILD];\n        } else {\n            $this->build = [];\n        }\n\n        if (isset($components[Parser::MAJOR])) {\n            $this->major = $components[Parser::MAJOR];\n        } else {\n            $this->major = 0;\n        }\n\n        if (isset($components[Parser::MINOR])) {\n            $this->minor = $components[Parser::MINOR];\n        } else {\n            $this->minor = 0;\n        }\n\n        if (isset($components[Parser::PATCH])) {\n            $this->patch = $components[Parser::PATCH];\n        } else {\n            $this->patch = 0;\n        }\n\n        if (isset($components[Parser::PRE_RELEASE])) {\n            $this->preRelease = $components[Parser::PRE_RELEASE];\n        } else {\n            $this->preRelease = [];\n        }\n\n        return $this;\n    }\n\n    /**\n     * Imports the version string representation.\n     *\n     * @param string $version The string representation.\n     *\n     * @return Builder The Version builder.\n     */\n    public function importString(string $version): self\n    {\n        return $this->importComponents(Parser::toComponents($version));\n    }\n\n    /**\n     * Imports an existing Version instance.\n     *\n     * @param Version $version A Version instance.\n     *\n     * @return Builder The Version builder.\n     */\n    public function importVersion(Version $version): self\n    {\n        return $this\n            ->setMajor($version->getMajor())\n            ->setMinor($version->getMinor())\n            ->setPatch($version->getPatch())\n            ->setPreRelease($version->getPreRelease())\n            ->setBuild($version->getBuild());\n    }\n\n    /**\n     * Increments the major version number and resets the minor and patch\n     * version numbers to zero.\n     *\n     * @param int $amount Increment by what amount?\n     *\n     * @return Builder The Version builder.\n     */\n    public function incrementMajor(int $amount = 1): self\n    {\n        $this->major += $amount;\n        $this->minor = 0;\n        $this->patch = 0;\n\n        return $this;\n    }\n\n    /**\n     * Increments the minor version number and resets the patch version number\n     * to zero.\n     *\n     * @param int $amount Increment by what amount?\n     *\n     * @return Builder The Version builder.\n     */\n    public function incrementMinor(int $amount = 1): self\n    {\n        $this->minor += $amount;\n        $this->patch = 0;\n\n        return $this;\n    }\n\n    /**\n     * Increments the patch version number.\n     *\n     * @param int $amount Increment by what amount?\n     *\n     * @return Builder The Version builder.\n     */\n    public function incrementPatch(int $amount = 1): self\n    {\n        $this->patch += $amount;\n\n        return $this;\n    }\n\n    /**\n     * Sets the build metadata identifiers.\n     *\n     * @param array $identifiers The build metadata identifiers.\n     *\n     * @return Builder The Version builder.\n     *\n     * @throws InvalidIdentifierException If an identifier is invalid.\n     */\n    public function setBuild(array $identifiers): self\n    {\n        foreach ($identifiers as $identifier) {\n            if (!Validator::isIdentifier($identifier)) {\n                throw new InvalidIdentifierException($identifier);\n            }\n        }\n\n        $this->build = $identifiers;\n\n        return $this;\n    }\n\n    /**\n     * Sets the major version number.\n     *\n     * @param int $number The major version number.\n     *\n     * @return Builder The Version builder.\n     *\n     * @throws InvalidNumberException If the number is invalid.\n     */\n    public function setMajor(int $number): self\n    {\n        if (!Validator::isNumber($number)) {\n            throw new InvalidNumberException($number);\n        }\n\n        $this->major = intval($number);\n\n        return $this;\n    }\n\n    /**\n     * Sets the minor version number.\n     *\n     * @param int $number The minor version number.\n     *\n     * @return Builder The Version builder.\n     *\n     * @throws InvalidNumberException If the number is invalid.\n     */\n    public function setMinor(int $number): self\n    {\n        if (!Validator::isNumber($number)) {\n            throw new InvalidNumberException($number);\n        }\n\n        $this->minor = intval($number);\n\n        return $this;\n    }\n\n    /**\n     * Sets the patch version number.\n     *\n     * @param int $number The patch version number.\n     *\n     * @return Builder The Version builder.\n     *\n     * @throws InvalidNumberException If the number is invalid.\n     */\n    public function setPatch(int $number): self\n    {\n        if (!Validator::isNumber($number)) {\n            throw new InvalidNumberException($number);\n        }\n\n        $this->patch = intval($number);\n\n        return $this;\n    }\n\n    /**\n     * Sets the pre-release version identifiers.\n     *\n     * @param array $identifiers The pre-release version identifiers.\n     *\n     * @return Builder The Version builder.\n     *\n     * @throws InvalidIdentifierException If an identifier is invalid.\n     */\n    public function setPreRelease(array $identifiers): self\n    {\n        foreach ($identifiers as $identifier) {\n            if (!Validator::isIdentifier($identifier)) {\n                throw new InvalidIdentifierException($identifier);\n            }\n        }\n\n        $this->preRelease = $identifiers;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Component/PharUpdate/Version/Comparator.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Version;\n\n/**\n * Compares two Version instances.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass Comparator\n{\n    /**\n     * The version is equal to another.\n     */\n    public const EQUAL_TO = 0;\n\n    /**\n     * The version is greater than another.\n     */\n    public const GREATER_THAN = 1;\n\n    /**\n     * The version is less than another.\n     */\n    public const LESS_THAN = -1;\n\n    /**\n     * Compares one version with another.\n     *\n     * @param Version $left  The left version to compare.\n     * @param Version $right The right version to compare.\n     *\n     * @return integer Returns Comparator::EQUAL_TO if the two versions are\n     *                 equal. If the left version is less than the right\n     *                 version, Comparator::LESS_THAN is returned. If the left\n     *                 version is greater than the right version,\n     *                 Comparator::GREATER_THAN is returned.\n     */\n    public static function compareTo(Version $left, Version $right)\n    {\n        switch (true) {\n            case ($left->getMajor() < $right->getMajor()):\n                return self::LESS_THAN;\n            case ($left->getMajor() > $right->getMajor()):\n                return self::GREATER_THAN;\n            case ($left->getMinor() > $right->getMinor()):\n                return self::GREATER_THAN;\n            case ($left->getMinor() < $right->getMinor()):\n                return self::LESS_THAN;\n            case ($left->getPatch() > $right->getPatch()):\n                return self::GREATER_THAN;\n            case ($left->getPatch() < $right->getPatch()):\n                return self::LESS_THAN;\n                // @codeCoverageIgnoreStart\n        }\n        // @codeCoverageIgnoreEnd\n\n        return self::compareIdentifiers(\n            $left->getPreRelease(),\n            $right->getPreRelease(),\n        );\n    }\n\n    /**\n     * Checks if the left version is equal to the right.\n     *\n     * @param Version $left  The left version to compare.\n     * @param Version $right The right version to compare.\n     *\n     * @return boolean TRUE if the left version is equal to the right, FALSE\n     *                 if not.\n     */\n    public static function isEqualTo(Version $left, Version $right)\n    {\n        return (self::EQUAL_TO === self::compareTo($left, $right));\n    }\n\n    /**\n     * Checks if the left version is greater than the right.\n     *\n     * @param Version $left  The left version to compare.\n     * @param Version $right The right version to compare.\n     *\n     * @return boolean TRUE if the left version is greater than the right,\n     *                 FALSE if not.\n     */\n    public static function isGreaterThan(Version $left, Version $right)\n    {\n        return (self::GREATER_THAN === self::compareTo($left, $right));\n    }\n\n    /**\n     * Checks if the left version is less than the right.\n     *\n     * @param Version $left  The left version to compare.\n     * @param Version $right The right version to compare.\n     *\n     * @return boolean TRUE if the left version is less than the right,\n     *                 FALSE if not.\n     */\n    public static function isLessThan(Version $left, Version $right)\n    {\n        return (self::LESS_THAN === self::compareTo($left, $right));\n    }\n\n    /**\n     * Compares the identifier components of the left and right versions.\n     *\n     * @param array $left  The left identifiers.\n     * @param array $right The right identifiers.\n     *\n     * @return integer Returns Comparator::EQUAL_TO if the two identifiers are\n     *                 equal. If the left identifiers is less than the right\n     *                 identifiers, Comparator::LESS_THAN is returned. If the\n     *                 left identifiers is greater than the right identifiers,\n     *                 Comparator::GREATER_THAN is returned.\n     */\n    public static function compareIdentifiers(array $left, array $right)\n    {\n        if ($left && empty($right)) {\n            return self::LESS_THAN;\n        } elseif (empty($left) && $right) {\n            return self::GREATER_THAN;\n        }\n\n        $l = $left;\n        $r = $right;\n        $x = self::GREATER_THAN;\n        $y = self::LESS_THAN;\n\n        if (count($l) < count($r)) {\n            $l = $right;\n            $r = $left;\n            $x = self::LESS_THAN;\n            $y = self::GREATER_THAN;\n        }\n\n        foreach (array_keys($l) as $i) {\n            if (!isset($r[$i])) {\n                return $x;\n            }\n\n            if ($l[$i] === $r[$i]) {\n                continue;\n            }\n\n            if (true === ($li = (false != preg_match('/^\\d+$/', $l[$i])))) {\n                $l[$i] = intval($l[$i]);\n            }\n\n            if (true === ($ri = (false != preg_match('/^\\d+$/', $r[$i])))) {\n                $r[$i] = intval($r[$i]);\n            }\n\n            if ($li && $ri) {\n                return ($l[$i] > $r[$i]) ? $x : $y;\n            } elseif (!$li && $ri) {\n                return $x;\n            } elseif ($li && !$ri) {\n                return $y;\n            }\n\n            $result = strcmp($l[$i], $r[$i]);\n\n            if ($result > 0) {\n                return $x;\n            } elseif ($result < 0) {\n                return $y;\n            }\n        }\n\n        return self::EQUAL_TO;\n    }\n}\n"
  },
  {
    "path": "src/Component/PharUpdate/Version/Dumper.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Version;\n\n/**\n * Dumps the Version instance to a variety of formats.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass Dumper\n{\n    /**\n     * Returns the components of a Version instance.\n     *\n     * @param Version $version A version.\n     *\n     * @return array The components.\n     */\n    public static function toComponents(Version $version)\n    {\n        return [\n            Parser::MAJOR => $version->getMajor(),\n            Parser::MINOR => $version->getMinor(),\n            Parser::PATCH => $version->getPatch(),\n            Parser::PRE_RELEASE => $version->getPreRelease(),\n            Parser::BUILD => $version->getBuild(),\n        ];\n    }\n\n    /**\n     * Returns the string representation of a Version instance.\n     *\n     * @param Version $version A version.\n     *\n     * @return string The string representation.\n     */\n    public static function toString(Version $version)\n    {\n        return sprintf(\n            '%d.%d.%d%s%s',\n            $version->getMajor(),\n            $version->getMinor(),\n            $version->getPatch(),\n            $version->getPreRelease()\n                ? '-' . join('.', $version->getPreRelease())\n                : '',\n            $version->getBuild()\n                ? '+' . join('.', $version->getBuild())\n                : '',\n        );\n    }\n}\n"
  },
  {
    "path": "src/Component/PharUpdate/Version/Exception/InvalidIdentifierException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Version\\Exception;\n\n/**\n * Thrown if an invalid identifier is used.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass InvalidIdentifierException extends VersionException\n{\n    /**\n     * The invalid identifier.\n     *\n     * @var string\n     */\n    private $identifier;\n\n    /**\n     * Sets the invalid identifier.\n     *\n     * @param string $identifier The invalid identifier.\n     */\n    public function __construct(string $identifier)\n    {\n        parent::__construct(\n            sprintf(\n                'The identifier \"%s\" is invalid.',\n                $identifier,\n            ),\n        );\n\n        $this->identifier = $identifier;\n    }\n\n    /**\n     * Returns the invalid identifier.\n     *\n     * @return string The invalid identifier.\n     */\n    public function getIdentifier(): string\n    {\n        return $this->identifier;\n    }\n}\n"
  },
  {
    "path": "src/Component/PharUpdate/Version/Exception/InvalidNumberException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Version\\Exception;\n\n/**\n * Thrown if an invalid version number is used.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass InvalidNumberException extends VersionException\n{\n    /**\n     * The invalid version number.\n     *\n     * @var mixed\n     */\n    private $number;\n\n    /**\n     * Sets the invalid version number.\n     *\n     * @param mixed $number The invalid version number.\n     */\n    public function __construct($number)\n    {\n        parent::__construct(\n            sprintf(\n                'The version number \"%s\" is invalid.',\n                $number,\n            ),\n        );\n\n        $this->number = $number;\n    }\n\n    /**\n     * Returns the invalid version number.\n     *\n     * @return mixed The invalid version number.\n     */\n    public function getNumber()\n    {\n        return $this->number;\n    }\n}\n"
  },
  {
    "path": "src/Component/PharUpdate/Version/Exception/InvalidStringRepresentationException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Version\\Exception;\n\n/**\n * Throw if an invalid version string representation is used.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass InvalidStringRepresentationException extends VersionException\n{\n    /**\n     * The invalid string representation.\n     *\n     * @var string\n     */\n    private $version;\n\n    /**\n     * Sets the invalid string representation.\n     *\n     * @param string $version The string representation.\n     */\n    public function __construct(string $version)\n    {\n        parent::__construct(\n            sprintf(\n                'The version string representation \"%s\" is invalid.',\n                $version,\n            ),\n        );\n\n        $this->version = $version;\n    }\n\n    /**\n     * Returns the invalid string representation.\n     *\n     * @return string The invalid string representation.\n     */\n    public function getVersion(): string\n    {\n        return $this->version;\n    }\n}\n"
  },
  {
    "path": "src/Component/PharUpdate/Version/Exception/VersionException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Version\\Exception;\n\nuse Exception;\n\n/**\n * The base library exception class.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass VersionException extends Exception {}\n"
  },
  {
    "path": "src/Component/PharUpdate/Version/Parser.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Version;\n\nuse Deployer\\Component\\PharUpdate\\Version\\Exception\\InvalidStringRepresentationException;\n\n/**\n * Parses the string representation of a version number.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass Parser\n{\n    /**\n     * The build metadata component.\n     */\n    public const BUILD = 'build';\n\n    /**\n     * The major version number component.\n     */\n    public const MAJOR = 'major';\n\n    /**\n     * The minor version number component.\n     */\n    public const MINOR = 'minor';\n\n    /**\n     * The patch version number component.\n     */\n    public const PATCH = 'patch';\n\n    /**\n     * The pre-release version number component.\n     */\n    public const PRE_RELEASE = 'pre';\n\n    /**\n     * Returns a Version builder for the string representation.\n     *\n     * @param string $version The string representation.\n     *\n     * @return Builder A Version builder.\n     */\n    public static function toBuilder(string $version): Builder\n    {\n        return Builder::create()->importComponents(\n            self::toComponents($version),\n        );\n    }\n\n    /**\n     * Returns the components of the string representation.\n     *\n     * @param string $version The string representation.\n     *\n     * @return array The components of the version.\n     *\n     * @throws InvalidStringRepresentationException If the string representation\n     *                                              is invalid.\n     */\n    public static function toComponents(string $version): array\n    {\n        if (!Validator::isVersion($version)) {\n            throw new InvalidStringRepresentationException($version);\n        }\n\n        if (false !== strpos($version, '+')) {\n            [$version, $build] = explode('+', $version);\n\n            $build = explode('.', $build);\n        }\n\n        if (false !== strpos($version, '-')) {\n            [$version, $pre] = explode('-', $version);\n\n            $pre = explode('.', $pre);\n        }\n\n        [\n            $major,\n            $minor,\n            $patch,\n        ] = explode('.', $version);\n\n        return [\n            self::MAJOR => intval($major),\n            self::MINOR => intval($minor),\n            self::PATCH => intval($patch),\n            self::PRE_RELEASE => $pre ?? [],\n            self::BUILD => $build ?? [],\n        ];\n    }\n\n    /**\n     * Returns a Version instance for the string representation.\n     *\n     * @param string $version The string representation.\n     *\n     * @return Version A Version instance.\n     */\n    public static function toVersion(string $version): Version\n    {\n        $components = self::toComponents($version);\n\n        return new Version(\n            $components['major'],\n            $components['minor'],\n            $components['patch'],\n            $components['pre'],\n            $components['build'],\n        );\n    }\n}\n"
  },
  {
    "path": "src/Component/PharUpdate/Version/Validator.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Version;\n\n/**\n * Validates version information.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass Validator\n{\n    /**\n     * The regular expression for a valid identifier.\n     */\n    public const IDENTIFIER_REGEX = '/^[0-9A-Za-z\\-]+$/';\n\n    /**\n     * The regular expression for a valid semantic version number.\n     */\n    public const VERSION_REGEX = '/^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$/';\n\n    /**\n     * Checks if a identifier is valid.\n     *\n     * @param string $identifier A identifier.\n     *\n     * @return boolean TRUE if the identifier is valid, FALSE If not.\n     */\n    public static function isIdentifier(string $identifier): bool\n    {\n        return (true == preg_match(self::IDENTIFIER_REGEX, $identifier));\n    }\n\n    /**\n     * Checks if a number is a valid version number.\n     *\n     * @param integer $number A number.\n     *\n     * @return boolean TRUE if the number is valid, FALSE If not.\n     */\n    public static function isNumber(int $number): bool\n    {\n        return (true == preg_match('/^(0|[1-9]\\d*)$/', (string) $number));\n    }\n\n    /**\n     * Checks if the string representation of a version number is valid.\n     *\n     * @param string $version The string representation.\n     *\n     * @return boolean TRUE if the string representation is valid, FALSE if not.\n     */\n    public static function isVersion(string $version): bool\n    {\n        return (true == preg_match(self::VERSION_REGEX, $version));\n    }\n}\n"
  },
  {
    "path": "src/Component/PharUpdate/Version/Version.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Component\\PharUpdate\\Version;\n\n/**\n * Stores and returns the version information.\n *\n * @author Kevin Herrera <kevin@herrera.io>\n */\nclass Version\n{\n    /**\n     * The build metadata identifiers.\n     *\n     * @var array\n     */\n    protected $build;\n\n    /**\n     * The major version number.\n     *\n     * @var integer\n     */\n    protected $major;\n\n    /**\n     * The minor version number.\n     *\n     * @var integer\n     */\n    protected $minor;\n\n    /**\n     * The patch version number.\n     *\n     * @var integer\n     */\n    protected $patch;\n\n    /**\n     * The pre-release version identifiers.\n     *\n     * @var array\n     */\n    protected $preRelease;\n\n    /**\n     * Sets the version information.\n     *\n     * @param int $major The major version number.\n     * @param int $minor The minor version number.\n     * @param int $patch The patch version number.\n     * @param array   $pre   The pre-release version identifiers.\n     * @param array   $build The build metadata identifiers.\n     */\n    public function __construct(\n        int $major = 0,\n        int $minor = 0,\n        int $patch = 0,\n        array $pre = [],\n        array $build = [],\n    ) {\n        $this->build = $build;\n        $this->major = $major;\n        $this->minor = $minor;\n        $this->patch = $patch;\n        $this->preRelease = $pre;\n    }\n\n    /**\n     * Returns the build metadata identifiers.\n     *\n     * @return array The build metadata identifiers.\n     */\n    public function getBuild(): array\n    {\n        return $this->build;\n    }\n\n    /**\n     * Returns the major version number.\n     *\n     * @return int The major version number.\n     */\n    public function getMajor(): int\n    {\n        return $this->major;\n    }\n\n    /**\n     * Returns the minor version number.\n     *\n     * @return int The minor version number.\n     */\n    public function getMinor(): int\n    {\n        return $this->minor;\n    }\n\n    /**\n     * Returns the patch version number.\n     *\n     * @return int The patch version number.\n     */\n    public function getPatch(): int\n    {\n        return $this->patch;\n    }\n\n    /**\n     * Returns the pre-release version identifiers.\n     *\n     * @return array The pre-release version identifiers.\n     */\n    public function getPreRelease(): array\n    {\n        return $this->preRelease;\n    }\n\n    /**\n     * Checks if the version number is stable.\n     *\n     * @return boolean TRUE if it is stable, FALSE if not.\n     */\n    public function isStable(): bool\n    {\n        return empty($this->preRelease) && $this->major !== 0;\n    }\n\n    /**\n     * Returns string representation.\n     *\n     * @return string The string representation.\n     */\n    public function __toString(): string\n    {\n        return Dumper::toString($this);\n    }\n}\n"
  },
  {
    "path": "src/Component/Pimple/Container.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Component\\Pimple;\n\nuse Deployer\\Component\\Pimple\\Exception\\ExpectedInvokableException;\nuse Deployer\\Component\\Pimple\\Exception\\FrozenServiceException;\nuse Deployer\\Component\\Pimple\\Exception\\InvalidServiceIdentifierException;\nuse Deployer\\Component\\Pimple\\Exception\\UnknownIdentifierException;\n\n/**\n * Container main class.\n *\n * @author Fabien Potencier\n */\nclass Container implements \\ArrayAccess\n{\n    /**\n     * @var array\n     */\n    private $values = [];\n    /**\n     * @var \\SplObjectStorage\n     */\n    private $factories;\n    /**\n     * @var \\SplObjectStorage\n     */\n    private $protected;\n    /**\n     * @var array\n     */\n    private $frozen = [];\n    /**\n     * @var array\n     */\n    private $raw = [];\n    /**\n     * @var array\n     */\n    private $keys = [];\n\n    /**\n     * Instantiates the container.\n     *\n     * Objects and parameters can be passed as argument to the constructor.\n     *\n     * @param array $values The parameters or objects\n     */\n    public function __construct(array $values = [])\n    {\n        $this->factories = new \\SplObjectStorage();\n        $this->protected = new \\SplObjectStorage();\n\n        foreach ($values as $key => $value) {\n            $this->offsetSet($key, $value);\n        }\n    }\n\n    /**\n     * Sets a parameter or an object.\n     *\n     * Objects must be defined as Closures.\n     *\n     * Allowing any PHP callable leads to difficult to debug problems\n     * as function names (strings) are callable (creating a function with\n     * the same name as an existing parameter would break your container).\n     *\n     * @param string $id    The unique identifier for the parameter or object\n     * @param mixed  $value The value of the parameter or a closure to define an object\n     *\n     * @throws FrozenServiceException Prevent override of a frozen service\n     * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint\n     */\n    #[\\ReturnTypeWillChange]\n    public function offsetSet($id, $value)\n    {\n        if (isset($this->frozen[$id])) {\n            throw new FrozenServiceException($id);\n        }\n\n        $this->values[$id] = $value;\n        $this->keys[$id] = true;\n    }\n\n    /**\n     * Gets a parameter or an object.\n     *\n     * @param string $id The unique identifier for the parameter or object\n     *\n     * @return mixed The value of the parameter or an object\n     *\n     * @throws UnknownIdentifierException If the identifier is not defined\n     * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint\n     */\n    #[\\ReturnTypeWillChange]\n    public function offsetGet($id)\n    {\n        if (!isset($this->keys[$id])) {\n            throw new UnknownIdentifierException($id);\n        }\n\n        if (\n            isset($this->raw[$id])\n            || !\\is_object($this->values[$id])\n            || isset($this->protected[$this->values[$id]])\n            || !\\method_exists($this->values[$id], '__invoke')\n        ) {\n            return $this->values[$id];\n        }\n\n        if (isset($this->factories[$this->values[$id]])) {\n            return $this->values[$id]($this);\n        }\n\n        $raw = $this->values[$id];\n        $val = $this->values[$id] = $raw($this);\n        $this->raw[$id] = $raw;\n\n        $this->frozen[$id] = true;\n\n        return $val;\n    }\n\n    /**\n     * Checks if a parameter or an object is set.\n     *\n     * @param string $id The unique identifier for the parameter or object\n     *\n     * @return bool\n     * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint\n     */\n    #[\\ReturnTypeWillChange]\n    public function offsetExists($id)\n    {\n        return isset($this->keys[$id]);\n    }\n\n    /**\n     * Unsets a parameter or an object.\n     *\n     * @param string $id The unique identifier for the parameter or object\n     * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint\n     */\n    #[\\ReturnTypeWillChange]\n    public function offsetUnset($id)\n    {\n        if (isset($this->keys[$id])) {\n            if (\\is_object($this->values[$id])) {\n                unset($this->factories[$this->values[$id]], $this->protected[$this->values[$id]]);\n            }\n\n            unset($this->values[$id], $this->frozen[$id], $this->raw[$id], $this->keys[$id]);\n        }\n    }\n\n    /**\n     * Marks a callable as being a factory service.\n     *\n     * @param callable $callable A service definition to be used as a factory\n     *\n     * @return callable The passed callable\n     *\n     * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object\n     */\n    public function factory(callable $callable)\n    {\n        if (!\\method_exists($callable, '__invoke')) {\n            throw new ExpectedInvokableException('Service definition is not a Closure or invokable object.');\n        }\n\n        $this->factories->attach($callable);\n\n        return $callable;\n    }\n\n    /**\n     * Protects a callable from being interpreted as a service.\n     *\n     * This is useful when you want to store a callable as a parameter.\n     *\n     * @param callable $callable A callable to protect from being evaluated\n     *\n     * @return callable The passed callable\n     *\n     * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object\n     */\n    public function protect(callable $callable)\n    {\n        if (!\\method_exists($callable, '__invoke')) {\n            throw new ExpectedInvokableException('Callable is not a Closure or invokable object.');\n        }\n\n        $this->protected->attach($callable);\n\n        return $callable;\n    }\n\n    /**\n     * Gets a parameter or the closure defining an object.\n     *\n     * @param string $id The unique identifier for the parameter or object\n     *\n     * @return mixed The value of the parameter or the closure defining an object\n     *\n     * @throws UnknownIdentifierException If the identifier is not defined\n     */\n    public function raw(string $id)\n    {\n        if (!isset($this->keys[$id])) {\n            throw new UnknownIdentifierException($id);\n        }\n\n        if (isset($this->raw[$id])) {\n            return $this->raw[$id];\n        }\n\n        return $this->values[$id];\n    }\n\n    /**\n     * Extends an object definition.\n     *\n     * Useful when you want to extend an existing object definition,\n     * without necessarily loading that object.\n     *\n     * @param string   $id       The unique identifier for the object\n     * @param callable $callable A service definition to extend the original\n     *\n     * @return callable The wrapped callable\n     *\n     * @throws UnknownIdentifierException        If the identifier is not defined\n     * @throws FrozenServiceException            If the service is frozen\n     * @throws InvalidServiceIdentifierException If the identifier belongs to a parameter\n     * @throws ExpectedInvokableException        If the extension callable is not a closure or an invokable object\n     */\n    public function extend(string $id, callable $callable)\n    {\n        if (!isset($this->keys[$id])) {\n            throw new UnknownIdentifierException($id);\n        }\n\n        if (isset($this->frozen[$id])) {\n            throw new FrozenServiceException($id);\n        }\n\n        if (!\\is_object($this->values[$id]) || !\\method_exists($this->values[$id], '__invoke')) {\n            throw new InvalidServiceIdentifierException($id);\n        }\n\n        if (isset($this->protected[$this->values[$id]])) {\n            @\\trigger_error(\\sprintf('How Pimple behaves when extending protected closures will be fixed in Pimple 4. Are you sure \"%s\" should be protected?', $id), E_USER_DEPRECATED);\n        }\n\n        if (!\\is_object($callable) || !\\method_exists($callable, '__invoke')) {\n            throw new ExpectedInvokableException('Extension service definition is not a Closure or invokable object.');\n        }\n\n        $factory = $this->values[$id];\n\n        $extended = function ($c) use ($callable, $factory) {\n            return $callable($factory($c), $c);\n        };\n\n        if (isset($this->factories[$factory])) {\n            $this->factories->detach($factory);\n            $this->factories->attach($extended);\n        }\n\n        return $this[$id] = $extended;\n    }\n\n    /**\n     * Returns all defined value names.\n     *\n     * @return array An array of value names\n     */\n    public function keys()\n    {\n        return \\array_keys($this->values);\n    }\n}\n"
  },
  {
    "path": "src/Component/Pimple/Exception/ExpectedInvokableException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Component\\Pimple\\Exception;\n\nuse Psr\\Container\\ContainerExceptionInterface;\n\n/**\n * A closure or invokable object was expected.\n *\n * @author Pascal Luna <skalpa@zetareticuli.org>\n */\nclass ExpectedInvokableException extends \\InvalidArgumentException implements ContainerExceptionInterface {}\n"
  },
  {
    "path": "src/Component/Pimple/Exception/FrozenServiceException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Component\\Pimple\\Exception;\n\nuse Psr\\Container\\ContainerExceptionInterface;\n\n/**\n * An attempt to modify a frozen service was made.\n *\n * @author Pascal Luna <skalpa@zetareticuli.org>\n */\nclass FrozenServiceException extends \\RuntimeException implements ContainerExceptionInterface\n{\n    /**\n     * @param string $id Identifier of the frozen service\n     */\n    public function __construct(string $id)\n    {\n        parent::__construct(\\sprintf('Cannot override frozen service \"%s\".', $id));\n    }\n}\n"
  },
  {
    "path": "src/Component/Pimple/Exception/InvalidServiceIdentifierException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Component\\Pimple\\Exception;\n\nuse Psr\\Container\\NotFoundExceptionInterface;\n\n/**\n * An attempt to perform an operation that requires a service identifier was made.\n *\n * @author Pascal Luna <skalpa@zetareticuli.org>\n */\nclass InvalidServiceIdentifierException extends \\InvalidArgumentException implements NotFoundExceptionInterface\n{\n    /**\n     * @param string $id The invalid identifier\n     */\n    public function __construct(string $id)\n    {\n        parent::__construct(\\sprintf('Identifier \"%s\" does not contain an object definition.', $id));\n    }\n}\n"
  },
  {
    "path": "src/Component/Pimple/Exception/UnknownIdentifierException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Component\\Pimple\\Exception;\n\nuse Psr\\Container\\NotFoundExceptionInterface;\n\n/**\n * The identifier of a valid service or parameter was expected.\n *\n * @author Pascal Luna <skalpa@zetareticuli.org>\n */\nclass UnknownIdentifierException extends \\InvalidArgumentException implements NotFoundExceptionInterface\n{\n    /**\n     * @param string $id The unknown identifier\n     */\n    public function __construct(string $id)\n    {\n        parent::__construct(\\sprintf('Identifier \"%s\" is not defined.', $id));\n    }\n}\n"
  },
  {
    "path": "src/Configuration.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nuse Deployer\\Exception\\ConfigurationException;\nuse Deployer\\Utility\\Httpie;\n\nuse function Deployer\\Support\\array_merge_alternate;\nuse function Deployer\\Support\\is_closure;\nuse function Deployer\\Support\\normalize_line_endings;\n\nclass Configuration implements \\ArrayAccess\n{\n    private ?Configuration $parent;\n    private array $values = [];\n\n    public function __construct(?Configuration $parent = null)\n    {\n        $this->parent = $parent;\n    }\n\n    public function update(array $values): void\n    {\n        $this->values = array_merge($this->values, $values);\n    }\n\n    public function bind(Configuration $parent): void\n    {\n        $this->parent = $parent;\n    }\n\n    public function set(string $name, mixed $value): void\n    {\n        $this->values[$name] = $value;\n    }\n\n    public function has(string $name): bool\n    {\n        $ok = array_key_exists($name, $this->values);\n        if ($ok) {\n            return true;\n        }\n        if ($this->parent) {\n            return $this->parent->has($name);\n        }\n        return false;\n    }\n\n    public function hasOwn(string $name): bool\n    {\n        return array_key_exists($name, $this->values);\n    }\n\n    public function add(string $name, array $array): void\n    {\n        if ($this->has($name)) {\n            $config = $this->get($name);\n            if (!is_array($config)) {\n                throw new ConfigurationException(\"Config option \\\"$name\\\" isn't array.\");\n            }\n            $this->set($name, array_merge_alternate($config, $array));\n        } else {\n            $this->set($name, $array);\n        }\n    }\n\n    public function get(string $name, mixed $default = null): mixed\n    {\n        if (array_key_exists($name, $this->values)) {\n            if (is_closure($this->values[$name])) {\n                return $this->values[$name] = $this->parse(call_user_func($this->values[$name]));\n            } else {\n                return $this->parse($this->values[$name]);\n            }\n        }\n\n        if ($this->parent) {\n            $rawValue = $this->parent->fetch($name);\n            if ($rawValue !== null) {\n                if (is_closure($rawValue)) {\n                    return $this->values[$name] = $this->parse(call_user_func($rawValue));\n                } else {\n                    return $this->values[$name] = $this->parse($rawValue);\n                }\n            }\n        }\n\n        if (func_num_args() >= 2) {\n            return $this->parse($default);\n        }\n\n        throw new ConfigurationException(\"Config option \\\"$name\\\" does not exist.\");\n    }\n\n    protected function fetch(string $name): mixed\n    {\n        if (array_key_exists($name, $this->values)) {\n            return $this->values[$name];\n        }\n        if ($this->parent) {\n            return $this->parent->fetch($name);\n        }\n        return null;\n    }\n\n    public function parse(mixed $value): mixed\n    {\n        if (is_string($value)) {\n            $normalizedValue = normalize_line_endings($value);\n            return preg_replace_callback('/\\{\\{\\s*([\\w\\.\\/-]+)\\s*\\}\\}/', function (array $matches) {\n                return $this->get($matches[1]);\n            }, $normalizedValue);\n        }\n\n        return $value;\n    }\n\n    public function keys(): array\n    {\n        return array_keys($this->values);\n    }\n\n    /**\n     * @param string $offset\n     * @return bool\n     */\n    #[\\ReturnTypeWillChange]\n    public function offsetExists($offset)\n    {\n        return $this->has($offset);\n    }\n\n    /**\n     * @param string $offset\n     * @return mixed\n     */\n    #[\\ReturnTypeWillChange]\n    public function offsetGet($offset)\n    {\n        return $this->get($offset);\n    }\n\n    /**\n     * @param string $offset\n     * @param mixed $value\n     */\n    #[\\ReturnTypeWillChange]\n    public function offsetSet($offset, $value): void\n    {\n        $this->set($offset, $value);\n    }\n\n    /**\n     * @param mixed $offset\n     */\n    #[\\ReturnTypeWillChange]\n    public function offsetUnset($offset): void\n    {\n        unset($this->values[$offset]);\n    }\n\n    public function load(): void\n    {\n        if (!Deployer::isWorker()) {\n            return;\n        }\n\n        $values = Httpie::get(MASTER_ENDPOINT . '/load')\n            ->setopt(CURLOPT_CONNECTTIMEOUT, 0)\n            ->setopt(CURLOPT_TIMEOUT, 0)\n            ->jsonBody([\n                'host' => $this->get('alias'),\n            ])\n            ->getJson();\n        $this->update($values);\n    }\n\n    public function save(): void\n    {\n        if (!Deployer::isWorker()) {\n            return;\n        }\n\n        Httpie::get(MASTER_ENDPOINT . '/save')\n            ->setopt(CURLOPT_CONNECTTIMEOUT, 0)\n            ->setopt(CURLOPT_TIMEOUT, 0)\n            ->jsonBody([\n                'host' => $this->get('alias'),\n                'config' => $this->persist(),\n            ])\n            ->getJson();\n    }\n\n    public function persist(): array\n    {\n        $values = [];\n        foreach ($this->values as $key => $value) {\n            if (is_closure($value)) {\n                continue;\n            }\n            $values[$key] = $value;\n        }\n        return $values;\n    }\n}\n"
  },
  {
    "path": "src/Deployer.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nuse Deployer\\Collection\\Collection;\nuse Deployer\\Command\\BlackjackCommand;\nuse Deployer\\Command\\ConfigCommand;\nuse Deployer\\Command\\InitCommand;\nuse Deployer\\Command\\MainCommand;\nuse Deployer\\Command\\RunCommand;\nuse Deployer\\Command\\SshCommand;\nuse Deployer\\Command\\TreeCommand;\nuse Deployer\\Command\\WorkerCommand;\nuse Deployer\\Component\\PharUpdate\\Console\\Command as PharUpdateCommand;\nuse Deployer\\Component\\PharUpdate\\Console\\Helper as PharUpdateHelper;\nuse Deployer\\Component\\Pimple\\Container;\nuse Deployer\\ProcessRunner\\Printer;\nuse Deployer\\ProcessRunner\\ProcessRunner;\nuse Deployer\\Ssh\\SshClient;\nuse Deployer\\Configuration;\nuse Deployer\\Executor\\Master;\nuse Deployer\\Executor\\Messenger;\nuse Deployer\\Host\\Host;\nuse Deployer\\Host\\HostCollection;\nuse Deployer\\Host\\Localhost;\nuse Deployer\\Importer\\Importer;\nuse Deployer\\Logger\\Handler\\FileHandler;\nuse Deployer\\Logger\\Handler\\NullHandler;\nuse Deployer\\Logger\\Logger;\nuse Deployer\\Selector\\Selector;\nuse Deployer\\Task\\ScriptManager;\nuse Deployer\\Task\\TaskCollection;\nuse Deployer\\Utility\\Httpie;\nuse Deployer\\Utility\\Rsync;\nuse Symfony\\Component\\Console;\nuse Symfony\\Component\\Console\\Application;\nuse Symfony\\Component\\Console\\Input\\ArgvInput;\nuse Symfony\\Component\\Console\\Input\\InputDefinition;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\ConsoleOutput;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Throwable;\n\n/**\n * @property Application $console\n * @property InputInterface $input\n * @property OutputInterface $output\n * @property Task\\TaskCollection|Task\\Task[] $tasks\n * @property HostCollection|Host[] $hosts\n * @property Configuration $config\n * @property Rsync $rsync\n * @property SshClient $sshClient\n * @property ProcessRunner $processRunner\n * @property Task\\ScriptManager $scriptManager\n * @property Selector $selector\n * @property Master $master\n * @property Messenger $messenger\n * @property Messenger $logger\n * @property Printer $pop\n * @property Collection $fail\n * @property InputDefinition $inputDefinition\n * @property Importer $importer\n */\nclass Deployer extends Container\n{\n    private static Deployer $instance;\n\n    public function __construct(Application $console)\n    {\n        parent::__construct();\n\n        /******************************\n         *           Console          *\n         ******************************/\n\n        $console->getDefinition()->addOption(\n            new InputOption('file', 'f', InputOption::VALUE_REQUIRED, 'Recipe file path'),\n        );\n\n        $this['console'] = function () use ($console) {\n            return $console;\n        };\n        $this['input'] = function () {\n            throw new \\RuntimeException('Uninitialized \"input\" in Deployer container.');\n        };\n        $this['output'] = function () {\n            throw new \\RuntimeException('Uninitialized \"output\" in Deployer container.');\n        };\n        $this['inputDefinition'] = function () {\n            return new InputDefinition();\n        };\n        $this['questionHelper'] = function () {\n            return $this->getHelper('question');\n        };\n\n        /******************************\n         *           Config           *\n         ******************************/\n\n        $this['config'] = function () {\n            return new Configuration();\n        };\n        // -l  act as if it had been invoked as a login shell (i.e. source ~/.profile file)\n        // -s  commands are read from the standard input (no arguments should remain after this option)\n        $this->config['shell'] = function () {\n            if (currentHost() instanceof Localhost) {\n                return 'bash -s'; // Non-login shell for localhost.\n            }\n            return 'bash -ls';\n        };\n        $this->config['forward_agent'] = true;\n        $this->config['ssh_multiplexing'] = true;\n\n        /******************************\n         *            Core            *\n         ******************************/\n\n        $this['pop'] = function ($c) {\n            return new Printer($c['output']);\n        };\n        $this['sshClient'] = function ($c) {\n            return new SshClient($c['output'], $c['pop'], $c['logger']);\n        };\n        $this['rsync'] = function ($c) {\n            return new Rsync($c['pop'], $c['output']);\n        };\n        $this['processRunner'] = function ($c) {\n            return new ProcessRunner($c['pop'], $c['logger']);\n        };\n        $this['tasks'] = function () {\n            return new TaskCollection();\n        };\n        $this['hosts'] = function () {\n            return new HostCollection();\n        };\n        $this['scriptManager'] = function ($c) {\n            return new ScriptManager($c['tasks']);\n        };\n        $this['selector'] = function ($c) {\n            return new Selector($c['hosts']);\n        };\n        $this['fail'] = function () {\n            return new Collection();\n        };\n        $this['messenger'] = function ($c) {\n            return new Messenger($c['input'], $c['output'], $c['logger']);\n        };\n        $this['master'] = function ($c) {\n            return new Master(\n                $c['hosts'],\n                $c['input'],\n                $c['output'],\n                $c['messenger'],\n            );\n        };\n        $this['importer'] = function () {\n            return new Importer();\n        };\n\n        /******************************\n         *           Logger           *\n         ******************************/\n\n        $this['log_handler'] = function () {\n            return !empty($this['log'])\n                ? new FileHandler($this['log'])\n                : new NullHandler();\n        };\n        $this['logger'] = function () {\n            return new Logger($this['log_handler']);\n        };\n\n        self::$instance = $this;\n    }\n\n    public static function get(): self\n    {\n        return self::$instance;\n    }\n\n    public function init(): void\n    {\n        $this->addTaskCommands();\n        $this->getConsole()->add(new BlackjackCommand());\n        $this->getConsole()->add(new ConfigCommand($this));\n        $this->getConsole()->add(new WorkerCommand($this));\n        $this->getConsole()->add(new InitCommand());\n        $this->getConsole()->add(new TreeCommand($this));\n        $this->getConsole()->add(new SshCommand($this));\n        $this->getConsole()->add(new RunCommand($this));\n        if (self::isPharArchive()) {\n            $selfUpdate = new PharUpdateCommand('self-update');\n            $selfUpdate->setDescription('Updates deployer.phar to the latest version');\n            $selfUpdate->setManifestUri('https://deployer.org/manifest.json');\n            $selfUpdate->setRunningFile(DEPLOYER_BIN);\n            $this->getConsole()->add($selfUpdate);\n            $this->getConsole()->getHelperSet()->set(new PharUpdateHelper());\n        }\n    }\n\n    /**\n     * Transform tasks to console commands.\n     */\n    public function addTaskCommands(): void\n    {\n        foreach ($this->tasks as $name => $task) {\n            $command = new MainCommand($name, $task->getDescription(), $this);\n            $command->setHidden($task->isHidden());\n\n            $this->getConsole()->add($command);\n        }\n    }\n\n    public function __get(string $name): mixed\n    {\n        if (isset($this[$name])) {\n            return $this[$name];\n        } else {\n            throw new \\InvalidArgumentException(\"Property \\\"$name\\\" does not exist.\");\n        }\n    }\n\n    public function __set(string $name, mixed $value): void\n    {\n        $this[$name] = $value;\n    }\n\n    public function getConsole(): Application\n    {\n        return $this['console'];\n    }\n\n    public function getHelper(string $name): Console\\Helper\\HelperInterface\n    {\n        return $this->getConsole()->getHelperSet()->get($name);\n    }\n\n    public static function run(string $version, ?string $deployFile): void\n    {\n        if (str_contains($version, 'master')) {\n            // Get version from composer.lock\n            $lockFile = __DIR__ . '/../../../../composer.lock';\n            if (file_exists($lockFile)) {\n                $content = file_get_contents($lockFile);\n                $json = json_decode($content);\n                foreach ($json->packages as $package) {\n                    if ($package->name === 'deployer/deployer') {\n                        $version = $package->version;\n                    }\n                }\n            }\n        }\n\n        // Version must be without \"v\" prefix.\n        //    Incorrect: v7.0.0\n        //    Correct: 7.0.0\n        // But deployphp/deployer uses tags with \"v\", and it gets passed to\n        // the composer.json file. Let's manually remove it from the version.\n        if (preg_match(\"/^v/\", $version)) {\n            $version = substr($version, 1);\n        }\n\n        if (!defined('DEPLOYER_VERSION')) {\n            define('DEPLOYER_VERSION', $version);\n        }\n\n        $input = new ArgvInput();\n        $output = new ConsoleOutput();\n\n        try {\n            $console = new Application('Deployer', $version);\n            $deployer = new self($console);\n\n            // Import recipe file\n            if (is_readable($deployFile ?? '')) {\n                $deployer->importer->import($deployFile);\n            }\n\n            $deployer->init();\n            $console->run($input, $output);\n\n        } catch (Throwable $exception) {\n            if (str_contains(\"$input\", \"-vvv\")) {\n                $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);\n            }\n            self::printException($output, $exception);\n\n            exit(1);\n        }\n    }\n\n    public static function printException(OutputInterface $output, Throwable $exception): void\n    {\n        $class = get_class($exception);\n        $file = basename($exception->getFile());\n        $output->writeln([\n            \"<fg=white;bg=red> {$class} </> <comment>in {$file} on line {$exception->getLine()}:</>\",\n            \"\",\n            implode(\"\\n\", array_map(function ($line) {\n                return \"  \" . $line;\n            }, explode(\"\\n\", $exception->getMessage()))),\n            \"\",\n        ]);\n        if ($output->isDebug()) {\n            $output->writeln($exception->getTraceAsString());\n        }\n\n        if ($exception->getPrevious()) {\n            self::printException($output, $exception->getPrevious());\n        }\n    }\n\n    public static function isWorker(): bool\n    {\n        return defined('MASTER_ENDPOINT');\n    }\n\n    /**\n     * @return array|bool|string\n     */\n    public static function masterCall(Host $host, string $func, mixed ...$arguments): mixed\n    {\n        // As request to master will stop master permanently, wait a little bit\n        // in order for ticker gather worker outputs and print it to user.\n        usleep(100_000); // Sleep 100ms.\n\n        return Httpie::get(MASTER_ENDPOINT . '/proxy')\n            ->setopt(CURLOPT_CONNECTTIMEOUT, 0) // no timeout\n            ->setopt(CURLOPT_TIMEOUT, 0) // no timeout\n            ->jsonBody([\n                'host' => $host->getAlias(),\n                'func' => $func,\n                'arguments' => $arguments,\n            ])\n            ->getJson();\n    }\n\n    public static function isPharArchive(): bool\n    {\n        return str_starts_with(__FILE__, 'phar:');\n    }\n}\n"
  },
  {
    "path": "src/Documentation/ApiGen.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Documentation;\n\nclass ApiGen\n{\n    /**\n     * @var array\n     */\n    private $fns = [];\n\n    public function parse(string $source): void\n    {\n        $comment = '';\n        $params = '';\n        $signature = '';\n\n        $source = str_replace(\"\\r\\n\", \"\\n\", $source);\n\n        $state = 'root';\n        foreach (explode(\"\\n\", $source) as $lineNumber => $line) {\n            switch ($state) {\n                case 'root':\n                    if (str_starts_with($line, '/**')) {\n                        $state = 'comment';\n                        break;\n                    }\n                    if (str_starts_with($line, 'function')) {\n                        $signature = preg_replace('/^function\\s+/', '', $line);\n                        $funcName = preg_replace('/\\(.*$/', '', $signature);\n                        $this->fns[] = [\n                            'comment' => $comment,\n                            'params' => $params,\n                            'funcName' => $funcName,\n                            'signature' => $signature,\n                        ];\n                        $comment = '';\n                        $params = '';\n\n                        if (str_ends_with($signature, '(')) {\n                            $state = 'params';\n                        } else {\n                            $signature = '';\n                        }\n                    }\n                    break;\n\n                case 'comment':\n                    if (str_ends_with($line, '*/')) {\n                        $state = 'root';\n                        break;\n                    }\n                    if (preg_match('/^\\s\\*\\s@param\\s(?<type>.+?)\\$(?<name>.+?)\\s(?<comment>.+)$/', $line, $matches)) {\n                        if (empty($params)) {\n                            $params = \"| Argument | Type | Comment |\\n|---|---|---|\\n\";\n                        }\n                        $type = implode(' or ', array_map(function ($t) {\n                            $t = trim($t, ' ');\n                            return \"`$t`\";\n                        }, explode('|', $matches['type'])));\n                        $params .= \"| `\\${$matches['name']}` | $type | {$matches['comment']} |\\n\";\n                        break;\n                    }\n                    if (str_starts_with($line, ' * @')) {\n                        break;\n                    }\n                    $comment .= preg_replace('/^\\s\\*\\s?/', '', $line) . \"\\n\";\n                    break;\n\n                case 'params':\n                    if (preg_match('/^\\).+\\{$/', $line, $matches)) {\n                        $signature .= \"\\n\" . preg_replace('/\\{$/', '', $line);\n                        $this->fns[count($this->fns) - 1]['signature'] = $signature;\n                        $state = 'root';\n                    } else {\n                        $signature .= \"\\n\" . $line;\n                    }\n                    break;\n            }\n        }\n    }\n\n    public function markdown(): string\n    {\n        $output = <<<MD\n            <!-- DO NOT EDIT THIS FILE! -->\n            <!-- Instead edit src/functions.php -->\n            <!-- Then run bin/docgen -->\n\n            # API Reference\n\n\n            MD;\n\n        foreach ($this->fns as $fn) {\n            [\n                'comment' => $comment,\n                'params' => $params,\n                'funcName' => $funcName,\n                'signature' => $signature,\n            ] = $fn;\n\n            if (!empty($params)) {\n                $params = \"\\n$params\";\n            }\n\n            $output .= <<<MD\n                ## $funcName()\n\n                ```php\n                $signature\n                ```\n\n                $comment\n                $params\n\n                MD;\n        }\n        return $output;\n    }\n}\n"
  },
  {
    "path": "src/Documentation/DocConfig.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Documentation;\n\nclass DocConfig\n{\n    /**\n     * @var string\n     */\n    public $name;\n    /**\n     * @var string\n     */\n    public $defaultValue;\n    /**\n     * @var string\n     */\n    public $comment;\n    /**\n     * @var string\n     */\n    public $recipePath;\n    /**\n     * @var int\n     */\n    public $lineNumber;\n}\n"
  },
  {
    "path": "src/Documentation/DocGen.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Documentation;\n\nuse RecursiveDirectoryIterator;\nuse RecursiveIteratorIterator;\nuse RecursiveRegexIterator;\nuse RegexIterator;\n\nclass DocGen\n{\n    /**\n     * @var string\n     */\n    public $root;\n    /**\n     * @var DocRecipe[]\n     */\n    public $recipes = [];\n\n    public function __construct(string $root)\n    {\n        $this->root = str_replace(DIRECTORY_SEPARATOR, '/', realpath($root));\n    }\n\n    public function parse(string $source): void\n    {\n        $directory = new RecursiveDirectoryIterator($source);\n        $iterator = new RegexIterator(new RecursiveIteratorIterator($directory), '/^.+\\.php$/i', RecursiveRegexIterator::GET_MATCH);\n        foreach ($iterator as [$path]) {\n            $realPath = str_replace(DIRECTORY_SEPARATOR, '/', realpath($path));\n            $recipePath = str_replace($this->root . '/', '', $realPath);\n            $recipeName = preg_replace('/\\.php$/i', '', basename($recipePath));\n            $recipe = new DocRecipe($recipeName, $recipePath);\n            $recipe->parse(file_get_contents($path));\n            $this->recipes[$recipePath] = $recipe;\n        }\n    }\n\n    public function gen(string $destination): ?string\n    {\n        foreach ($this->recipes as $recipe) {\n            // $find will try to return DocConfig for a given config $name.\n            $findConfig = function (string $name) use ($recipe): ?DocConfig {\n                if (array_key_exists($name, $recipe->config)) {\n                    return $recipe->config[$name];\n                }\n                foreach ($recipe->require as $r) {\n                    if (array_key_exists($r, $this->recipes)) {\n                        if (array_key_exists($name, $this->recipes[$r]->config)) {\n                            return $this->recipes[$r]->config[$name];\n                        }\n                    }\n                }\n                foreach ($this->recipes as $r) {\n                    if (array_key_exists($name, $r->config)) {\n                        return $r->config[$name];\n                    }\n                }\n                return null;\n            };\n            $findConfigOverride = function (DocRecipe $recipe, string $name) use (&$findConfigOverride): ?DocConfig {\n                foreach ($recipe->require as $r) {\n                    if (array_key_exists($r, $this->recipes)) {\n                        if (array_key_exists($name, $this->recipes[$r]->config)) {\n                            return $this->recipes[$r]->config[$name];\n                        }\n                    }\n                }\n                foreach ($recipe->require as $r) {\n                    if (array_key_exists($r, $this->recipes)) {\n                        return $findConfigOverride($this->recipes[$r], $name);\n                    }\n                }\n                return null;\n            };\n            // Replace all {{name}} with link to correct config declaration.\n            $replaceLinks = function (string $comment) use ($findConfig): string {\n                $output = '';\n                $code = false;\n                foreach (explode(\"\\n\", $comment) as $i => $line) {\n                    if (str_starts_with($line, '```') || str_starts_with($line, '~~~')) {\n                        $code = !$code;\n                    }\n                    if ($code) {\n                        $output .= $line;\n                        $output .= \"\\n\";\n                        continue;\n                    }\n                    $output .= preg_replace_callback('#(\\{\\{(?<name>[\\w_:\\-/]+)\\}\\})#', function ($m) use ($findConfig) {\n                        $name = $m['name'];\n                        $config = $findConfig($name);\n                        if ($config !== null) {\n                            $md = php_to_md($config->recipePath);\n                            $anchor = anchor($name);\n                            return \"[$name](/docs/$md#$anchor)\";\n                        }\n                        return \"{{\" . $name . \"}}\";\n                    }, $line);\n                    $output .= \"\\n\";\n                }\n                return $output;\n            };\n            $findTask = function (string $name, bool $searchOtherRecipes = true) use ($recipe): ?DocTask {\n                if (array_key_exists($name, $recipe->tasks)) {\n                    return $recipe->tasks[$name];\n                }\n                foreach ($recipe->require as $r) {\n                    if (array_key_exists($r, $this->recipes)) {\n                        if (array_key_exists($name, $this->recipes[$r]->tasks)) {\n                            return $this->recipes[$r]->tasks[$name];\n                        }\n                    }\n                }\n                if ($searchOtherRecipes) {\n                    foreach ($this->recipes as $r) {\n                        if (array_key_exists($name, $r->tasks)) {\n                            return $r->tasks[$name];\n                        }\n                    }\n                }\n                return null;\n            };\n\n            $title = join(' ', array_map('ucfirst', explode('_', $recipe->recipeName))) . ' Recipe';\n            $config = '';\n            $tasks = '';\n            $intro = <<<MD\n                ```php\n                require '$recipe->recipePath';\n                ```\n\n                [Source](/$recipe->recipePath)\n\n\n                MD;\n            if (is_framework_recipe($recipe)) {\n                $brandName = framework_brand_name($recipe->recipeName);\n                $typeOfProject = preg_match('/^symfony/i', $recipe->recipeName) ? 'Application' : 'Project';\n                $title = \"How to Deploy a $brandName $typeOfProject\";\n\n                $intro .= <<<MARKDOWN\n                    Deployer is a free and open source deployment tool written in PHP. \n                    It helps you to deploy your $brandName application to a server. \n                    It is very easy to use and has a lot of features. \n\n                    Three main features of Deployer are:\n                    - **Provisioning** - provision your server for you.\n                    - **Zero downtime deployment** - deploy your application without a downtime.\n                    - **Rollbacks** - rollback your application to a previous version, if something goes wrong.\n\n                    Additionally, Deployer has a lot of other features, like:\n                    - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.\n                    - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.\n                    - **Secure** - Deployer uses SSH to connect to your server.\n                    - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.\n\n                    You can read more about Deployer in [Getting Started](/docs/getting-started.md).\n\n\n                    MARKDOWN;\n\n                $map = function (DocTask $task, $ident = '') use (&$map, $findTask, &$intro): void {\n                    foreach ($task->group as $taskName) {\n                        $t = $findTask($taskName);\n                        if ($t !== null) {\n                            $intro .= \"$ident* {$t->mdLink()} – $t->desc\\n\";\n                            if ($t->group !== null) {\n                                $map($t, $ident . '  ');\n                            }\n                        }\n                    }\n                };\n                $deployTask = $findTask('deploy');\n                if ($deployTask !== null) {\n                    $intro .= \"The [deploy](#deploy) task of **$brandName** consists of:\\n\";\n                    $map($deployTask);\n                }\n\n                $intro .= \"\\n\\n\";\n\n                $artifactBuildTask = $findTask('artifact:build', false);\n                $artifactDeployTask = $findTask('artifact:deploy', false);\n                if ($artifactDeployTask !== null && $artifactBuildTask !== null) {\n                    $intro .= \"In addition the **$brandName** recipe contains an artifact deployment.\\n\";\n                    $intro .= <<<MD\n                        This is a two step process where you first execute\n\n                        ```php\n                        bin/dep artifact:build [options] [localhost]\n                        ```\n\n                        to build an artifact, which then is deployed on a server with\n\n                        ```php\n                        bin/dep artifact:deploy [host]\n                        ```\n\n                        The `localhost` to build the artifact on has to be declared local, so either add\n                        ```php\n                        localhost()\n                            ->set('local', true);\n                        ```\n                        to your deploy.php or\n                        ```yaml\n                        hosts:\n                            localhost:\n                                local: true\n                        ```\n                        to your deploy yaml.\n\n                        The [artifact:build](#artifact:build) command of **$brandName** consists of: \n                        MD;\n                    $map($artifactBuildTask);\n\n                    $intro .= \"\\n\\n The [artifact:deploy](#artifact:deploy) command of **$brandName** consists of:\\n\";\n\n                    $map($artifactDeployTask);\n\n                    $intro .= \"\\n\\n\";\n                }\n            }\n            if (count($recipe->require) > 0) {\n                if (is_framework_recipe($recipe)) {\n                    $link = recipe_to_md_link($recipe->require[0]);\n                    $intro .= \"The $recipe->recipeName recipe is based on the $link recipe.\\n\";\n                } else {\n                    $intro .= \"* Requires\\n\";\n                    foreach ($recipe->require as $r) {\n                        $link = recipe_to_md_link($r);\n                        $intro .= \"  * {$link}\\n\";\n                    }\n                }\n            }\n            if (!empty($recipe->comment)) {\n                $intro .= \"\\n$recipe->comment\\n\";\n            }\n            if (count($recipe->config) > 0) {\n                $config .= \"## Configuration\\n\";\n                foreach ($recipe->config as $c) {\n                    $config .= \"### {$c->name}\\n\";\n                    $config .= \"[Source](https://github.com/deployphp/deployer/blob/master/{$c->recipePath}#L{$c->lineNumber})\\n\\n\";\n                    $o = $findConfigOverride($recipe, $c->name);\n                    if ($o !== null) {\n                        $md = php_to_md($o->recipePath);\n                        $anchor = anchor($c->name);\n                        $config .= \"Overrides [{$c->name}](/docs/$md#$anchor) from `$o->recipePath`.\\n\\n\";\n                    }\n                    $config .= $replaceLinks($c->comment);\n                    $config .= \"\\n\";\n                    if (\n                        !empty($c->defaultValue)\n                        && $c->defaultValue !== \"''\"\n                        && $c->defaultValue !== '[]'\n                    ) {\n                        $config .= \"```php title=\\\"Default value\\\"\\n\";\n                        $config .= $c->defaultValue;\n                        $config .= \"\\n\";\n                        $config .= \"```\\n\";\n                    }\n                    $config .= \"\\n\\n\";\n                }\n            }\n            if (count($recipe->tasks) > 0) {\n                $tasks .= \"## Tasks\\n\\n\";\n                foreach ($recipe->tasks as $t) {\n                    $anchorTag = '{#' . anchor($t->name) . '}';\n                    $name = title($t->name);\n                    $tasks .= \"### $name $anchorTag\\n\";\n                    $tasks .= \"[Source](https://github.com/deployphp/deployer/blob/master/{$t->recipePath}#L{$t->lineNumber})\\n\\n\";\n                    $tasks .= add_tailing_dot($t->desc) . \"\\n\\n\";\n                    $tasks .= $replaceLinks($t->comment);\n                    if (is_array($t->group)) {\n                        $tasks .= \"\\n\\n\";\n                        $tasks .= \"This task is group task which contains next tasks:\\n\";\n                        foreach ($t->group as $taskName) {\n                            $t = $findTask($taskName);\n                            if ($t !== null) {\n                                $tasks .= \"* {$t->mdLink()}\\n\";\n                            } else {\n                                $tasks .= \"* `$taskName`\\n\";\n                            }\n                        }\n                    }\n                    $tasks .= \"\\n\\n\";\n                }\n            }\n\n            $output = <<<MD\n                <!-- DO NOT EDIT THIS FILE! -->\n                <!-- Instead edit $recipe->recipePath -->\n                <!-- Then run bin/docgen -->\n\n                # $title\n\n                $intro\n                $config\n                $tasks\n                MD;\n\n            $filePath = \"$destination/\" . php_to_md($recipe->recipePath);\n            if (!file_exists(dirname($filePath))) {\n                mkdir(dirname($filePath), 0o755, true);\n            }\n            $output = remove_text_emoji($output);\n            file_put_contents($filePath, $output);\n        }\n        $this->generateRecipesIndex($destination);\n        $this->generateContribIndex($destination);\n        return null;\n    }\n\n    public function generateRecipesIndex(string $destination)\n    {\n        $index = \"# All Recipes\\n\\n\";\n        $list = [];\n        foreach ($this->recipes as $recipe) {\n            if (preg_match('/^recipe\\/[^\\/]+\\.php$/', $recipe->recipePath)) {\n                $name = framework_brand_name($recipe->recipeName);\n                $list[] = \"* [$name Recipe](/docs/recipe/{$recipe->recipeName}.md)\";\n            }\n        }\n        sort($list);\n        $index .= implode(\"\\n\", $list);\n        file_put_contents(\"$destination/recipe/README.md\", $index);\n    }\n\n    public function generateContribIndex(string $destination)\n    {\n        $index = \"# All Contrib Recipes\\n\\n\";\n        $list = [];\n        foreach ($this->recipes as $recipe) {\n            if (preg_match('/^contrib\\/[^\\/]+\\.php$/', $recipe->recipePath)) {\n                $name = ucfirst($recipe->recipeName);\n                $list[] = \"* [$name Recipe](/docs/contrib/$recipe->recipeName.md)\";\n            }\n        }\n        sort($list);\n        $index .= implode(\"\\n\", $list);\n        file_put_contents(\"$destination/contrib/README.md\", $index);\n    }\n}\n\nfunction trim_comment(string $line): string\n{\n    return preg_replace('#^(/\\*\\*?\\s?|\\s\\*\\s?|//\\s?)#', '', $line);\n}\n\nfunction indent(string $text): string\n{\n    return implode(\"\\n\", array_map(function ($line) {\n        return \"  \" . $line;\n    }, explode(\"\\n\", $text)));\n}\n\nfunction php_to_md(string $file): string\n{\n    return preg_replace('#\\.php$#', '.md', $file);\n}\n\nfunction title(string $s): string\n{\n    return str_replace(':', '\\\\:', $s);\n}\n\nfunction anchor(string $s): string\n{\n    return strtolower(str_replace(':', '-', $s));\n}\n\nfunction remove_text_emoji(string $text): string\n{\n    return preg_replace('/:(bowtie|smile|laughing|blush|smiley|relaxed|smirk|heart_eyes|kissing_heart|kissing_closed_eyes|flushed|relieved|satisfied|grin|wink|stuck_out_tongue_winking_eye|stuck_out_tongue_closed_eyes|grinning|kissing|kissing_smiling_eyes|stuck_out_tongue|sleeping|worried|frowning|anguished|open_mouth|grimacing|confused|hushed|expressionless|unamused|sweat_smile|sweat|disappointed_relieved|weary|pensive|disappointed|confounded|fearful|cold_sweat|persevere|cry|sob|joy|astonished|scream|neckbeard|tired_face|angry|rage|triumph|sleepy|yum|mask|sunglasses|dizzy_face|imp|smiling_imp|neutral_face|no_mouth|innocent|alien|yellow_heart|blue_heart|purple_heart|heart|green_heart|broken_heart|heartbeat|heartpulse|two_hearts|revolving_hearts|cupid|sparkling_heart|sparkles|star|star2|dizzy|boom|collision|anger|exclamation|question|grey_exclamation|grey_question|zzz|dash|sweat_drops|notes|musical_note|fire|hankey|poop|shit|\\+1|thumbsup|\\-1|thumbsdown|ok_hand|punch|facepunch|fist|v|wave|hand|raised_hand|open_hands|point_up|point_down|point_left|point_right|raised_hands|pray|point_up_2|clap|muscle|metal|fu|walking|runner|running|couple|family|two_men_holding_hands|two_women_holding_hands|dancer|dancers|ok_woman|no_good|information_desk_person|raising_hand|bride_with_veil|person_with_pouting_face|person_frowning|bow|couplekiss|couple_with_heart|massage|haircut|nail_care|boy|girl|woman|man|baby|older_woman|older_man|person_with_blond_hair|man_with_gua_pi_mao|man_with_turban|construction_worker|cop|angel|princess|smiley_cat|smile_cat|heart_eyes_cat|kissing_cat|smirk_cat|scream_cat|crying_cat_face|joy_cat|pouting_cat|japanese_ogre|japanese_goblin|see_no_evil|hear_no_evil|speak_no_evil|guardsman|skull|feet|lips|kiss|droplet|ear|eyes|nose|tongue|love_letter|bust_in_silhouette|busts_in_silhouette|speech_balloon|thought_balloon|feelsgood|finnadie|goberserk|godmode|hurtrealbad|rage1|rage2|rage3|rage4|suspect|trollface|sunny|umbrella|cloud|snowflake|snowman|zap|cyclone|foggy|ocean|cat|dog|mouse|hamster|rabbit|wolf|frog|tiger|koala|bear|pig|pig_nose|cow|boar|monkey_face|monkey|horse|racehorse|camel|sheep|elephant|panda_face|snake|bird|baby_chick|hatched_chick|hatching_chick|chicken|penguin|turtle|bug|honeybee|ant|beetle|snail|octopus|tropical_fish|fish|whale|whale2|dolphin|cow2|ram|rat|water_buffalo|tiger2|rabbit2|dragon|goat|rooster|dog2|pig2|mouse2|ox|dragon_face|blowfish|crocodile|dromedary_camel|leopard|cat2|poodle|paw_prints|bouquet|cherry_blossom|tulip|four_leaf_clover|rose|sunflower|hibiscus|maple_leaf|leaves|fallen_leaf|herb|mushroom|cactus|palm_tree|evergreen_tree|deciduous_tree|chestnut|seedling|blossom|ear_of_rice|shell|globe_with_meridians|sun_with_face|full_moon_with_face|new_moon_with_face|new_moon|waxing_crescent_moon|first_quarter_moon|waxing_gibbous_moon|full_moon|waning_gibbous_moon|last_quarter_moon|waning_crescent_moon|last_quarter_moon_with_face|first_quarter_moon_with_face|moon|earth_africa|earth_americas|earth_asia|volcano|milky_way|partly_sunny|octocat|squirrel|bamboo|gift_heart|dolls|school_satchel|mortar_board|flags|fireworks|sparkler|wind_chime|rice_scene|jack_o_lantern|ghost|santa|christmas_tree|gift|bell|no_bell|tanabata_tree|tada|confetti_ball|balloon|crystal_ball|cd|dvd|floppy_disk|camera|video_camera|movie_camera|computer|tv|iphone|phone|telephone|telephone_receiver|pager|fax|minidisc|vhs|sound|speaker|mute|loudspeaker|mega|hourglass|hourglass_flowing_sand|alarm_clock|watch|radio|satellite|loop|mag|mag_right|unlock|lock|lock_with_ink_pen|closed_lock_with_key|key|bulb|flashlight|high_brightness|low_brightness|electric_plug|battery|calling|email|mailbox|postbox|bath|bathtub|shower|toilet|wrench|nut_and_bolt|hammer|seat|moneybag|yen|dollar|pound|euro|credit_card|money_with_wings|e-mail|inbox_tray|outbox_tray|envelope|incoming_envelope|postal_horn|mailbox_closed|mailbox_with_mail|mailbox_with_no_mail|door|smoking|bomb|gun|hocho|pill|syringe|page_facing_up|page_with_curl|bookmark_tabs|bar_chart|chart_with_upwards_trend|chart_with_downwards_trend|scroll|clipboard|calendar|date|card_index|file_folder|open_file_folder|scissors|pushpin|paperclip|black_nib|pencil2|straight_ruler|triangular_ruler|closed_book|green_book|blue_book|orange_book|notebook|notebook_with_decorative_cover|ledger|books|bookmark|name_badge|microscope|telescope|newspaper|football|basketball|soccer|baseball|tennis|8ball|rugby_football|bowling|golf|mountain_bicyclist|bicyclist|horse_racing|snowboarder|swimmer|surfer|ski|spades|hearts|clubs|diamonds|gem|ring|trophy|musical_score|musical_keyboard|violin|space_invader|video_game|black_joker|flower_playing_cards|game_die|dart|mahjong|clapper|memo|pencil|book|art|microphone|headphones|trumpet|saxophone|guitar|shoe|sandal|high_heel|lipstick|boot|shirt|tshirt|necktie|womans_clothes|dress|running_shirt_with_sash|jeans|kimono|bikini|ribbon|tophat|crown|womans_hat|mans_shoe|closed_umbrella|briefcase|handbag|pouch|purse|eyeglasses|fishing_pole_and_fish|coffee|tea|sake|baby_bottle|beer|beers|cocktail|tropical_drink|wine_glass|fork_and_knife|pizza|hamburger|fries|poultry_leg|meat_on_bone|spaghetti|curry|fried_shrimp|bento|sushi|fish_cake|rice_ball|rice_cracker|rice|ramen|stew|oden|dango|egg|bread|doughnut|custard|icecream|ice_cream|shaved_ice|birthday|cake|cookie|chocolate_bar|candy|lollipop|honey_pot|apple|green_apple|tangerine|lemon|cherries|grapes|watermelon|strawberry|peach|melon|banana|pear|pineapple|sweet_potato|eggplant|tomato|corn|house|house_with_garden|school|office|post_office|hospital|bank|convenience_store|love_hotel|hotel|wedding|church|department_store|european_post_office|city_sunrise|city_sunset|japanese_castle|european_castle|tent|factory|tokyo_tower|japan|mount_fuji|sunrise_over_mountains|sunrise|stars|statue_of_liberty|bridge_at_night|carousel_horse|rainbow|ferris_wheel|fountain|roller_coaster|ship|speedboat|boat|sailboat|rowboat|anchor|rocket|airplane|helicopter|steam_locomotive|tram|mountain_railway|bike|aerial_tramway|suspension_railway|mountain_cableway|tractor|blue_car|oncoming_automobile|car|red_car|taxi|oncoming_taxi|articulated_lorry|bus|oncoming_bus|rotating_light|police_car|oncoming_police_car|fire_engine|ambulance|minibus|truck|train|station|train2|bullettrain_front|bullettrain_side|light_rail|monorail|railway_car|trolleybus|ticket|fuelpump|vertical_traffic_light|traffic_light|warning|construction|beginner|atm|slot_machine|busstop|barber|hotsprings|checkered_flag|crossed_flags|izakaya_lantern|moyai|circus_tent|performing_arts|round_pushpin|triangular_flag_on_post|jp|kr|cn|us|fr|es|it|ru|gb|uk|de|one|two|three|four|five|six|seven|eight|nine|keycap_ten|1234|zero|hash|symbols|arrow_backward|arrow_down|arrow_forward|arrow_left|capital_abcd|abcd|abc|arrow_lower_left|arrow_lower_right|arrow_right|arrow_up|arrow_upper_left|arrow_upper_right|arrow_double_down|arrow_double_up|arrow_down_small|arrow_heading_down|arrow_heading_up|leftwards_arrow_with_hook|arrow_right_hook|left_right_arrow|arrow_up_down|arrow_up_small|arrows_clockwise|arrows_counterclockwise|rewind|fast_forward|information_source|ok|twisted_rightwards_arrows|repeat|repeat_one|new|top|up|cool|free|ng|cinema|koko|signal_strength|u5272|u5408|u55b6|u6307|u6708|u6709|u6e80|u7121|u7533|u7a7a|u7981|sa|restroom|mens|womens|baby_symbol|no_smoking|parking|wheelchair|metro|baggage_claim|accept|wc|potable_water|put_litter_in_its_place|secret|congratulations|m|passport_control|left_luggage|customs|ideograph_advantage|cl|sos|id|no_entry_sign|underage|no_mobile_phones|do_not_litter|non-potable_water|no_bicycles|no_pedestrians|children_crossing|no_entry|eight_spoked_asterisk|eight_pointed_black_star|heart_decoration|vs|vibration_mode|mobile_phone_off|chart|currency_exchange|aries|taurus|gemini|cancer|leo|virgo|libra|scorpius|sagittarius|capricorn|aquarius|pisces|ophiuchus|six_pointed_star|negative_squared_cross_mark|a|b|ab|o2|diamond_shape_with_a_dot_inside|recycle|end|on|soon|clock1|clock130|clock10|clock1030|clock11|clock1130|clock12|clock1230|clock2|clock230|clock3|clock330|clock4|clock430|clock5|clock530|clock6|clock630|clock7|clock730|clock8|clock830|clock9|clock930|heavy_dollar_sign|copyright|registered|tm|x|heavy_exclamation_mark|bangbang|interrobang|o|heavy_multiplication_x|heavy_plus_sign|heavy_minus_sign|heavy_division_sign|white_flower|100|heavy_check_mark|ballot_box_with_check|radio_button|link|curly_loop|wavy_dash|part_alternation_mark|trident|black_square|white_square|white_check_mark|black_square_button|white_square_button|black_circle|white_circle|red_circle|large_blue_circle|large_blue_diamond|large_orange_diamond|small_blue_diamond|small_orange_diamond|small_red_triangle|small_red_triangle_down|shipit):/i', ':&#8203;\\1:', $text);\n}\n\nfunction add_tailing_dot(string $sentence): string\n{\n    if (empty($sentence)) {\n        return $sentence;\n    }\n    if (str_ends_with($sentence, '.')) {\n        return $sentence;\n    }\n    return $sentence . '.';\n}\n\nfunction recipe_to_md_link(string $recipe): string\n{\n    $md = php_to_md($recipe);\n    $basename = basename($recipe, '.php');\n    return \"[$basename](/docs/$md)\";\n}\n\nfunction is_framework_recipe(DocRecipe $recipe): bool\n{\n    return preg_match('/recipe\\/[\\w_\\d]+\\.php$/', $recipe->recipePath) &&\n    !in_array($recipe->recipeName, ['common', 'composer', 'provision'], true);\n}\n\nfunction framework_brand_name(string $brandName): string\n{\n    $brandName = preg_replace('/(\\w+)(\\d)/', '$1 $2', $brandName);\n    $brandName = preg_replace('/typo 3/', 'TYPO3', $brandName);\n    $brandName = preg_replace('/yii/', 'Yii2', $brandName);\n    $brandName = preg_replace('/wordpress/', 'WordPress', $brandName);\n    $brandName = preg_replace('/_/', ' ', $brandName);\n    $brandName = preg_replace('/framework/', 'Framework', $brandName);\n    return ucfirst($brandName);\n}\n"
  },
  {
    "path": "src/Documentation/DocRecipe.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Documentation;\n\nclass DocRecipe\n{\n    /**\n     * @var string\n     */\n    public $recipeName;\n    /**\n     * @var string\n     */\n    public $recipePath;\n    /**\n     * @var string\n     */\n    public $comment;\n    /**\n     * @var string[]\n     */\n    public $require = [];\n    /**\n     * @var DocConfig[]\n     */\n    public $config = [];\n    /**\n     * @var DocTask[]\n     */\n    public $tasks = [];\n\n    public function __construct(string $recipeName, string $recipePath)\n    {\n        $this->recipeName = $recipeName;\n        $this->recipePath = $recipePath;\n    }\n\n    /**\n     * @return bool|int\n     */\n    public function parse(string $content)\n    {\n        $comment = '';\n        $desc = '';\n        $currentTask = null;\n\n        $content = str_replace(\"\\r\\n\", \"\\n\", $content);\n\n        $state = 'root';\n        $lines = explode(\"\\n\", $content);\n\n        for ($i = 0; $i < count($lines); $i++) {\n            $line = $lines[$i];\n\n            if (empty($line)) {\n                continue; // Skip empty lines\n            }\n\n            $m = [];\n            $match = function ($regexp) use ($line, &$m) {\n                return preg_match(\"#$regexp#\", $line, $m);\n            };\n            switch ($state) {\n                case 'root':\n                    if ($match('^/\\*\\*?')) {\n                        $state = 'comment';\n                        $comment .= trim_comment($line) . \"\\n\";\n                        break;\n                    }\n                    if ($match('^//')) {\n                        $comment .= trim_comment($line) . \"\\n\";\n                        break;\n                    }\n                    if ($match('^require.+?[\\'\"](?<recipe>.+?)[\\'\"]')) {\n                        $this->require[] = dirname($this->recipePath) . $m['recipe'];\n                        break;\n                    }\n                    if ($match('^set\\([\\'\"](?<config_name>[\\w_:\\-/]+?)[\\'\"]')) {\n                        $set = new DocConfig();\n                        $set->name = $m['config_name'];\n                        $set->comment = trim($comment);\n                        $comment = '';\n                        $set->recipePath = $this->recipePath;\n                        $set->lineNumber = $i + 1;\n                        if (preg_match('#^set\\(.+?,\\s(?<value>.+?)\\);$#', $line, $m)) {\n                            $set->defaultValue = $m['value'];\n                        }\n                        if (preg_match('#^set\\(.+?,\\s\\[$#', $line, $m)) {\n                            $multiLineArray = \"[\\n\";\n                            $line = $lines[++$i];\n                            while (!preg_match('/^]/', $line)) {\n                                $multiLineArray .= $line . \"\\n\";\n                                $line = $lines[++$i];\n                            }\n                            $multiLineArray .= \"]\";\n                            $set->defaultValue = $multiLineArray;\n                        }\n                        if (preg_match('/^set\\(.+?, function/', $line, $m)) {\n                            $body = [];\n                            $line = $lines[++$i];\n                            while (!preg_match('/^}\\);$/', $line)) {\n                                $body[] = trim($line);\n                                $line = $lines[++$i];\n                            }\n                            if (count($body) === 1 && preg_match('/throw new/', $body[0])) {\n                                $set->comment .= \"\\n:::info Required\\nThrows exception if not set.\\n:::\\n\";\n                            } elseif (count($body) <= 4) {\n                                $set->defaultValue = implode(\"\\n\", $body);\n                            } else {\n                                $set->comment .= \"\\n:::info Autogenerated\\nThe value of this configuration is autogenerated on access.\\n:::\\n\";\n                            }\n                        }\n                        $this->config[$set->name] = $set;\n                        break;\n                    }\n                    if ($match('^desc\\([\\'\"](?<desc>.+?)[\\'\"]\\);$')) {\n                        $desc = $m['desc'];\n                        break;\n                    }\n                    if ($match('^task\\([\\'\"](?<task_name>[\\w_:-]+?)[\\'\"],\\s\\[$')) {\n                        $task = new DocTask();\n                        $task->name = $m['task_name'];\n                        $task->desc = $desc;\n                        $task->comment = trim($comment);\n                        $comment = '';\n                        $task->group = [];\n                        $task->recipePath = $this->recipePath;\n                        $task->lineNumber = $i + 1;\n                        $this->tasks[$task->name] = $task;\n                        $state = 'group_task';\n                        $currentTask = $task;\n                        break;\n                    }\n                    if ($match('^task\\([\\'\"](?<task_name>[\\w_:-]+?)[\\'\"],')) {\n                        $task = new DocTask();\n                        $task->name = $m['task_name'];\n                        $task->desc = $desc;\n                        $task->comment = trim($comment);\n                        $comment = '';\n                        $task->recipePath = $this->recipePath;\n                        $task->lineNumber = $i + 1;\n                        $this->tasks[$task->name] = $task;\n                        break;\n                    }\n                    if ($match('^<\\?php')) {\n                        break;\n                    }\n                    if ($match('^namespace Deployer;$')) {\n                        $this->comment = $comment;\n                        break;\n                    }\n\n                    $desc = '';\n                    $comment = '';\n                    break;\n\n                case 'comment':\n                    if ($match('\\*/\\s*$')) {\n                        $state = 'root';\n                        break;\n                    }\n                    $comment .= trim_comment($line) . \"\\n\";\n                    break;\n\n                case 'group_task':\n                    if ($match('^\\s+\\'(?<task_name>[\\w_:-]+?)\\',$')) {\n                        $currentTask->group[] = $m['task_name'];\n                        break;\n                    }\n                    $state = 'root';\n                    break;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/Documentation/DocTask.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Documentation;\n\nclass DocTask\n{\n    /**\n     * @var string\n     */\n    public $name;\n    /**\n     * @var string\n     */\n    public $desc;\n    /**\n     * @var string\n     */\n    public $comment;\n    /**\n     * @var array\n     */\n    public $group;\n    /**\n     * @var string\n     */\n    public $recipePath;\n    /**\n     * @var int\n     */\n    public $lineNumber;\n\n    public function mdLink(): string\n    {\n        $md = php_to_md($this->recipePath);\n        $anchor = anchor($this->name);\n        return \"[$this->name](/docs/$md#$anchor)\";\n    }\n}\n"
  },
  {
    "path": "src/Exception/ConfigurationException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Exception;\n\nclass ConfigurationException extends \\RuntimeException {}\n"
  },
  {
    "path": "src/Exception/Exception.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Exception;\n\nuse Throwable;\n\nclass Exception extends \\Exception\n{\n    /**\n     * @var string\n     */\n    private static $taskSourceLocation = '';\n    /**\n     * @var string\n     */\n    private $taskFilename = '';\n    /**\n     * @var int|mixed\n     */\n    private $taskLineNumber = 0;\n\n    public function __construct(string $message = \"\", int $code = 0, ?Throwable $previous = null)\n    {\n        if (function_exists('debug_backtrace')) {\n            $trace = debug_backtrace();\n            foreach ($trace as $t) {\n                if (!empty($t['file']) && $t['file'] === self::$taskSourceLocation) {\n                    $this->taskFilename = basename($t['file']);\n                    $this->taskLineNumber = $t['line'];\n                    break;\n                }\n            }\n        }\n        parent::__construct($message, $code, $previous);\n    }\n\n    public static function setTaskSourceLocation(string $filepath): void\n    {\n        self::$taskSourceLocation = $filepath;\n    }\n\n    public function getTaskFilename(): string\n    {\n        return $this->taskFilename;\n    }\n\n    public function getTaskLineNumber(): int\n    {\n        return $this->taskLineNumber;\n    }\n\n    public function setTaskFilename(string $taskFilename): void\n    {\n        $this->taskFilename = $taskFilename;\n    }\n\n    public function setTaskLineNumber(int $taskLineNumber): void\n    {\n        $this->taskLineNumber = $taskLineNumber;\n    }\n}\n"
  },
  {
    "path": "src/Exception/GracefulShutdownException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Exception;\n\n/**\n * Then this exception thrown, it will not trigger \"fail\" callback.\n *\n *     fail('deploy', 'deploy:failed');\n *\n *     task('deploy', function () {\n *         throw new GracefulShutdownException(...);\n *     });\n *\n * In example above task `deploy:failed` will not be called.\n */\nclass GracefulShutdownException extends Exception\n{\n    public const EXIT_CODE = 42;\n}\n"
  },
  {
    "path": "src/Exception/HttpieException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Exception;\n\nclass HttpieException extends \\RuntimeException {}\n"
  },
  {
    "path": "src/Exception/RunException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Exception;\n\nuse Deployer\\Host\\Host;\nuse Symfony\\Component\\Process\\Process;\n\nclass RunException extends Exception\n{\n    /**\n     * @var Host\n     */\n    private $host;\n    /**\n     * @var string\n     */\n    private $command;\n    /**\n     * @var int\n     */\n    private $exitCode;\n    /**\n     * @var string\n     */\n    private $output;\n    /**\n     * @var string\n     */\n    private $errorOutput;\n\n    public function __construct(\n        Host $host,\n        string $command,\n        int $exitCode,\n        string $output,\n        string $errorOutput,\n    ) {\n        $this->host = $host;\n        $this->command = $command;\n        $this->exitCode = $exitCode;\n        $this->output = $output;\n        $this->errorOutput = $errorOutput;\n\n        $message = sprintf('The command \"%s\" failed.', $command);\n        parent::__construct($message, $exitCode);\n    }\n\n    public function getHost(): Host\n    {\n        return $this->host;\n    }\n\n    public function getCommand(): string\n    {\n        return $this->command;\n    }\n\n    public function getExitCode(): int\n    {\n        return $this->exitCode;\n    }\n\n    public function getExitCodeText(): string\n    {\n        return Process::$exitCodes[$this->exitCode] ?? 'Unknown error';\n    }\n\n    public function getOutput(): string\n    {\n        return $this->output;\n    }\n\n    public function getErrorOutput(): string\n    {\n        return $this->errorOutput;\n    }\n}\n"
  },
  {
    "path": "src/Exception/TimeoutException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Exception;\n\nclass TimeoutException extends Exception\n{\n    public function __construct(\n        string $command,\n        ?float $timeout,\n    ) {\n        $message = sprintf('The command \"%s\" exceeded the timeout of %s seconds.', $command, $timeout);\n        parent::__construct($message, 1);\n    }\n}\n"
  },
  {
    "path": "src/Exception/WillAskUser.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Exception;\n\nclass WillAskUser extends Exception\n{\n    public function __construct(string $message)\n    {\n        parent::__construct($message);\n    }\n}\n"
  },
  {
    "path": "src/Executor/Master.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Executor;\n\nuse Deployer\\Deployer;\nuse Deployer\\Host\\Host;\nuse Deployer\\Host\\HostCollection;\nuse Deployer\\Selector\\Selector;\nuse Deployer\\Ssh\\IOArguments;\nuse Deployer\\Task\\Context;\nuse Deployer\\Task\\Task;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Process\\PhpExecutableFinder;\nuse Symfony\\Component\\Process\\Process;\n\nconst FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];\n\nfunction spinner(string $message = ''): string\n{\n    $frame = FRAMES[(int) ((int) (new \\DateTime())->format('u') / 1e5) % count(FRAMES)];\n    return \"  $frame $message\\r\";\n}\n\nclass Master\n{\n    private HostCollection $hosts;\n    private InputInterface $input;\n    private OutputInterface $output;\n    private Messenger $messenger;\n    private string|false $phpBin;\n\n    public function __construct(\n        HostCollection  $hosts,\n        InputInterface  $input,\n        OutputInterface $output,\n        Messenger       $messenger,\n    ) {\n        $this->hosts = $hosts;\n        $this->input = $input;\n        $this->output = $output;\n        $this->messenger = $messenger;\n        $this->phpBin = (new PhpExecutableFinder())->find();\n    }\n\n    /**\n     * @param Task[] $tasks\n     * @param Host[] $hosts\n     */\n    public function run(array $tasks, array $hosts, ?Planner $plan = null): int\n    {\n        $globalLimit = (int) $this->input->getOption('limit') ?: count($hosts);\n\n        foreach ($tasks as $task) {\n            if (!$plan) {\n                $this->messenger->startTask($task);\n            }\n\n            $plannedHosts = $hosts;\n\n            $limit = min($globalLimit, $task->getLimit() ?? $globalLimit);\n\n            if ($task->isOnce()) {\n                $plannedHosts = [];\n                foreach ($hosts as $currentHost) {\n                    if (Selector::apply($task->getSelector(), $currentHost)) {\n                        $plannedHosts[] = $currentHost;\n                        break;\n                    }\n                }\n            } elseif ($task->isOncePerNode()) {\n                $plannedHosts = [];\n                foreach ($hosts as $currentHost) {\n                    if (Selector::apply($task->getSelector(), $currentHost)) {\n                        $nodeLabel = $currentHost->getHostname();\n                        $labels = $currentHost->config()->get('labels', []);\n                        if (is_array($labels) && array_key_exists('node', $labels)) {\n                            $nodeLabel = $labels['node'];\n                        }\n                        if (array_key_exists($nodeLabel, $plannedHosts)) {\n                            continue;\n                        }\n                        $plannedHosts[$nodeLabel] = $currentHost;\n                    }\n                }\n            }\n\n            if ($limit === 1 || count($plannedHosts) === 1) {\n                foreach ($plannedHosts as $currentHost) {\n                    if (!Selector::apply($task->getSelector(), $currentHost)) {\n                        if ($plan) {\n                            $plan->commit([], $task);\n                        }\n                        continue;\n                    }\n\n                    if ($plan) {\n                        $plan->commit([$currentHost], $task);\n                        continue;\n                    }\n\n                    $exitCode = $this->runTask($task, [$currentHost]);\n                    if ($exitCode !== 0) {\n                        return $exitCode;\n                    }\n                }\n            } else {\n                foreach (array_chunk($plannedHosts, $limit) as $chunk) {\n                    $selectedHosts = [];\n                    foreach ($chunk as $currentHost) {\n                        if (Selector::apply($task->getSelector(), $currentHost)) {\n                            $selectedHosts[] = $currentHost;\n                        }\n                    }\n\n                    if ($plan) {\n                        $plan->commit($selectedHosts, $task);\n                        continue;\n                    }\n\n                    $exitCode = $this->runTask($task, $selectedHosts);\n                    if ($exitCode !== 0) {\n                        return $exitCode;\n                    }\n                }\n            }\n\n            if (!$plan) {\n                $this->messenger->endTask($task);\n            }\n        }\n\n        return 0;\n    }\n\n    /**\n     * @param Host[] $hosts\n     */\n    private function runTask(Task $task, array $hosts): int\n    {\n        if (getenv('DEPLOYER_LOCAL_WORKER') === 'true') {\n            // This allows to code coverage all recipe,\n            // as well as speedup tests by not spawning\n            // lots of processes. Also there is a few tests\n            // what runs with workers for tests subprocess\n            // communications.\n            foreach ($hosts as $host) {\n                $worker = new Worker(Deployer::get());\n                $exitCode = $worker->execute($task, $host);\n                if ($exitCode !== 0) {\n                    $this->messenger->endTask($task, true);\n                    return $exitCode;\n                }\n            }\n            return 0;\n        }\n\n        $server = new Server('127.0.0.1', 0, $this->output);\n\n        /** @var Process[] $processes */\n        $processes = [];\n\n        $server->afterRun(function (int $port) use (&$processes, $hosts, $task) {\n            foreach ($hosts as $host) {\n                $processes[] = $this->createProcess($host, $task, $port);\n            }\n\n            foreach ($processes as $process) {\n                $process->start();\n            }\n        });\n\n        $echoCallback = function (string $output) {\n            $output = preg_replace('/\\n$/', '', $output);\n            if (strlen($output) !== 0) {\n                $this->output->writeln($output);\n            }\n        };\n\n        $server->ticker(function () use (&$processes, $server, $echoCallback) {\n            $this->gatherOutput($processes, $echoCallback);\n            if ($this->output->isDecorated() && !getenv('CI')) {\n                $this->output->write(spinner());\n            }\n            if ($this->allFinished($processes)) {\n                $server->stop();\n            }\n        });\n\n        $server->router(function (string $path, array $payload) {\n            switch ($path) {\n                case '/load':\n                    ['host' => $host] = $payload;\n\n                    $host = $this->hosts->get($host);\n                    $config = $host->config()->persist();\n\n                    return new Response(200, $config);\n\n                case '/save':\n                    ['host' => $host, 'config' => $config] = $payload;\n\n                    $host = $this->hosts->get($host);\n                    $host->config()->update($config);\n\n                    return new Response(200, true);\n\n                case '/proxy':\n                    ['host' => $host, 'func' => $func, 'arguments' => $arguments] = $payload;\n\n                    Context::push(new Context($this->hosts->get($host)));\n                    $answer = call_user_func($func, ...$arguments);\n                    Context::pop();\n\n                    return new Response(200, $answer);\n\n                default:\n                    return new Response(404, null);\n            }\n        });\n\n        $server->run();\n\n        if ($this->output->isDecorated() && !getenv('CI')) {\n            $this->output->write(\"    \\r\"); // clear spinner\n        }\n        $this->gatherOutput($processes, $echoCallback);\n\n        if ($this->cumulativeExitCode($processes) !== 0) {\n            $this->messenger->endTask($task, true);\n        }\n\n        return $this->cumulativeExitCode($processes);\n    }\n\n    protected function createProcess(Host $host, Task $task, int $port): Process\n    {\n        $command = [\n            $this->phpBin, DEPLOYER_BIN,\n            'worker', '--port', $port,\n            '--task', $task,\n            '--host', $host->getAlias(),\n        ];\n        $command = array_merge($command, IOArguments::collect($this->input, $this->output));\n        if ($task->isVerbose() && $this->output->getVerbosity() === OutputInterface::VERBOSITY_NORMAL) {\n            $command[] = '-v';\n        }\n        if ($this->output->isDebug()) {\n            $this->output->writeln(\"[$host] \" . join(' ', $command));\n        }\n        return new Process($command);\n    }\n\n    /**\n     * @param Process[] $processes\n     */\n    protected function allFinished(array $processes): bool\n    {\n        foreach ($processes as $process) {\n            if (!$process->isTerminated()) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * @param Process[] $processes\n     */\n    protected function gatherOutput(array $processes, callable $callback): void\n    {\n        foreach ($processes as $process) {\n            $output = $process->getIncrementalOutput();\n            if (strlen($output) !== 0) {\n                $callback($output);\n            }\n\n            $errorOutput = $process->getIncrementalErrorOutput();\n            if (strlen($errorOutput) !== 0) {\n                $callback($errorOutput);\n            }\n        }\n    }\n\n    /**\n     * @param Process[] $processes\n     */\n    protected function cumulativeExitCode(array $processes): int\n    {\n        foreach ($processes as $process) {\n            if ($process->getExitCode() > 0) {\n                return $process->getExitCode();\n            }\n        }\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/Executor/Messenger.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Executor;\n\nuse Deployer\\Exception\\Exception;\nuse Deployer\\Exception\\RunException;\nuse Deployer\\Host\\Host;\nuse Deployer\\Logger\\Logger;\nuse Deployer\\Task\\Task;\nuse Symfony\\Component\\Console\\Input\\Input;\nuse Symfony\\Component\\Console\\Output\\Output;\nuse Throwable;\n\nclass Messenger\n{\n    /**\n     * @var Input\n     */\n    private $input;\n\n    /**\n     * @var Output\n     */\n    private $output;\n\n    /**\n     * @var Logger\n     */\n    private $logger;\n\n    /**\n     * @var int|double\n     */\n    private $startTime;\n\n    public function __construct(Input $input, Output $output, Logger $logger)\n    {\n        $this->input = $input;\n        $this->output = $output;\n        $this->logger = $logger;\n    }\n\n    public function startTask(Task $task): void\n    {\n        $this->startTime = round(microtime(true) * 1000);\n        if (getenv('GITHUB_WORKFLOW')) {\n            $this->output->writeln(\"::group::task {$task->getName()}\");\n        } elseif (getenv('GITLAB_CI')) {\n            $sectionId = md5($task->getName());\n            $start = round($this->startTime / 1000);\n            $this->output->writeln(\"\\e[0Ksection_start:{$start}:{$sectionId}\\r\\e[0K{$task->getName()}\");\n        } else {\n            $this->output->writeln(\"<fg=cyan;options=bold>task</> {$task->getName()}\");\n        }\n        $this->logger->log(\"task {$task->getName()}\");\n    }\n\n    /*\n     * Print task was ok.\n     */\n    public function endTask(Task $task, bool $error = false): void\n    {\n        if (empty($this->startTime)) {\n            $this->startTime = round(microtime(true) * 1000);\n        }\n\n        $endTime = round(microtime(true) * 1000);\n        $millis = $endTime - $this->startTime;\n        $seconds = floor($millis / 1000);\n        $millis = $millis - $seconds * 1000;\n        $taskTime = ($seconds > 0 ? \"{$seconds}s \" : \"\") . \"{$millis}ms\";\n\n        if (getenv('GITHUB_WORKFLOW')) {\n            $this->output->writeln(\"::endgroup::\");\n        } elseif (getenv('GITLAB_CI')) {\n            $sectionId = md5($task->getName());\n            $endTime = round($endTime / 1000);\n            $this->output->writeln(\"\\e[0Ksection_end:{$endTime}:{$sectionId}\\r\\e[0K\");\n        } elseif ($this->output->isVeryVerbose()) {\n            $this->output->writeln(\"<fg=yellow;options=bold>done</> {$task->getName()} $taskTime\");\n        }\n        if ($error) {\n            $this->output->writeln(\"\\e[0K\\e[31;1mERROR: Task {$task->getName()} failed!\\e[0;m\");\n            return;\n        }\n        $this->logger->log(\"done {$task->getName()} $taskTime\");\n\n        if (!empty($this->input->getOption('profile'))) {\n            $line = sprintf(\"%s\\t%s\\n\", $task->getName(), $taskTime);\n            file_put_contents($this->input->getOption('profile'), $line, FILE_APPEND);\n        }\n    }\n\n    public function endOnHost(Host $host): void\n    {\n        if ($this->output->isVeryVerbose()) {\n            $this->output->writeln(\"<fg=yellow;options=bold>done</> on $host\");\n        }\n    }\n\n    public function renderException(Throwable $exception, Host $host): void\n    {\n        if ($exception instanceof RunException) {\n\n            $message = \"\";\n            $message .= \"[$host] <fg=white;bg=red> error </> <comment>in {$exception->getTaskFilename()} on line {$exception->getTaskLineNumber()}:</>\\n\";\n            if ($this->output->getVerbosity() === Output::VERBOSITY_NORMAL) {\n                $message .= \"[$host] <fg=green;options=bold>run</> {$exception->getCommand()}\\n\";\n                foreach (explode(\"\\n\", $exception->getErrorOutput()) as $line) {\n                    $line = trim($line);\n                    if ($line !== \"\") {\n                        $message .= \"[$host] <fg=red>err</> $line\\n\";\n                    }\n                }\n                foreach (explode(\"\\n\", $exception->getOutput()) as $line) {\n                    $line = trim($line);\n                    if ($line !== \"\") {\n                        $message .= \"[$host] $line\\n\";\n                    }\n                }\n            }\n            $message .= \"[$host] <fg=red>exit code</> {$exception->getExitCode()} ({$exception->getExitCodeText()})\\n\";\n            $this->output->write($message);\n\n        } else {\n            $message = \"\";\n            $class = get_class($exception);\n            $file = basename($exception->getFile());\n            $line = $exception->getLine();\n            if ($exception instanceof Exception) {\n                $file = $exception->getTaskFilename();\n                $line = $exception->getTaskLineNumber();\n            }\n            $message .= \"[$host] <fg=white;bg=red> $class </> <comment>in $file on line $line:</>\\n\";\n            $message .= \"[$host]\\n\";\n            foreach (explode(\"\\n\", $exception->getMessage()) as $line) {\n                $line = trim($line);\n                if ($line !== \"\") {\n                    $message .= \"[$host]   <comment>$line</comment>\\n\";\n                }\n            }\n            $message .= \"[$host]\\n\";\n            if ($this->output->isDebug()) {\n                foreach (explode(\"\\n\", $exception->getTraceAsString()) as $line) {\n                    $line = trim($line);\n                    if ($line !== \"\") {\n                        $message .= \"[$host] $line\\n\";\n                    }\n                }\n            }\n            $this->output->write($message);\n        }\n\n        $this->logger->log($exception->__toString());\n\n        if ($exception->getPrevious()) {\n            $this->renderException($exception->getPrevious(), $host);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Executor/Planner.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Executor;\n\nuse Deployer\\Host\\Host;\nuse Deployer\\Task\\Task;\nuse Symfony\\Component\\Console\\Helper\\Table;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass Planner\n{\n    /**\n     * @var Table\n     */\n    private $table;\n    /**\n     * @var array\n     */\n    private $template;\n\n    /**\n     * Planner constructor.\n     *\n     * @param Host[] $hosts\n     */\n    public function __construct(OutputInterface $output, array $hosts)\n    {\n        $headers = [];\n        $this->template = [];\n        foreach ($hosts as $host) {\n            $headers[] = $host->getTag();\n            $this->template[] = $host->getAlias();\n        }\n        $this->table = new Table($output);\n        $this->table->setHeaders($headers);\n        $this->table->setStyle('box');\n    }\n\n    /**\n     * @param Host[] $hosts\n     */\n    public function commit(array $hosts, Task $task): void\n    {\n        $row = [];\n        foreach ($this->template as $alias) {\n            $on = \"-\";\n            foreach ($hosts as $host) {\n                if ($alias === $host->getAlias()) {\n                    $on = $task->getName();\n                    break;\n                }\n            }\n            $row[] = $on;\n        }\n        $this->table->addRow($row);\n    }\n\n    public function render()\n    {\n        $this->table->render();\n    }\n}\n"
  },
  {
    "path": "src/Executor/Response.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Executor;\n\nclass Response\n{\n    private int $status;\n    private mixed $body;\n\n    public function __construct(int $status, mixed $body)\n    {\n        $this->status = $status;\n        $this->body = $body;\n    }\n\n    public function getStatus(): int\n    {\n        return $this->status;\n    }\n\n    public function getBody(): mixed\n    {\n        return $this->body;\n    }\n}\n"
  },
  {
    "path": "src/Executor/Server.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Executor;\n\nuse Closure;\nuse Deployer\\Exception\\Exception;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass Server\n{\n    private string $host;\n    private int $port;\n\n    private OutputInterface $output;\n\n    private bool $stop = false;\n\n    /**\n     * @var ?resource\n     */\n    private $socket;\n\n    /**\n     * @var resource[]\n     */\n    private array $clientSockets = [];\n\n    private Closure $afterCallback;\n    private Closure $tickerCallback;\n    private Closure $routerCallback;\n\n    public function __construct($host, $port, OutputInterface $output)\n    {\n        self::checkRequiredExtensionsExists();\n        $this->host = $host;\n        $this->port = $port;\n        $this->output = $output;\n    }\n\n    public static function checkRequiredExtensionsExists(): void\n    {\n        if (!function_exists('socket_import_stream')) {\n            throw new Exception('Required PHP extension \"sockets\" is not loaded');\n        }\n        if (!function_exists('stream_set_blocking')) {\n            throw new Exception('Required PHP extension \"stream\" is not loaded');\n        }\n    }\n\n    public function run(): void\n    {\n        try {\n            $this->socket = $this->createServerSocket();\n            $this->updatePort();\n            if ($this->output->isDebug()) {\n                $this->output->writeln(\"[master] Starting server at http://{$this->host}:{$this->port}\");\n            }\n\n            ($this->afterCallback)($this->port);\n\n            while (true) {\n                $this->acceptNewConnections();\n                $this->handleClientRequests();\n\n                // Prevent CPU exhaustion and 60fps ticker.\n                usleep(16_000); // 16ms\n\n                ($this->tickerCallback)();\n\n                if ($this->stop) {\n                    break;\n                }\n            }\n\n            if ($this->output->isDebug()) {\n                $this->output->writeln(\"[master] Stopping server at http://{$this->host}:{$this->port}\");\n            }\n        } finally {\n            if (isset($this->socket)) {\n                fclose($this->socket);\n            }\n        }\n    }\n\n    /**\n     * @return resource\n     * @throws Exception\n     */\n    private function createServerSocket()\n    {\n        $server = stream_socket_server(\"tcp://{$this->host}:{$this->port}\", $errno, $errstr);\n        if (!$server) {\n            throw new Exception(\"Socket creation failed: $errstr ($errno)\");\n        }\n\n        if (!stream_set_blocking($server, false)) {\n            throw new Exception(\"Failed to set server socket to non-blocking mode\");\n        }\n\n        return $server;\n    }\n\n    private function updatePort(): void\n    {\n        $name = stream_socket_get_name($this->socket, false);\n        if ($name) {\n            list(, $port) = explode(':', $name);\n            $this->port = (int) $port;\n        } else {\n            throw new Exception(\"Failed to get the assigned port\");\n        }\n    }\n\n    private function acceptNewConnections(): void\n    {\n        $newClientSocket = @stream_socket_accept($this->socket, 0);\n        if ($newClientSocket) {\n            if (!stream_set_blocking($newClientSocket, false)) {\n                throw new Exception(\"Failed to set client socket to non-blocking mode\");\n            }\n            $this->clientSockets[] = $newClientSocket;\n        }\n    }\n\n    private function handleClientRequests(): void\n    {\n        foreach ($this->clientSockets as $key => $clientSocket) {\n            if (feof($clientSocket)) {\n                $this->closeClientSocket($clientSocket, $key);\n                continue;\n            }\n\n            $request = $this->readClientRequest($clientSocket);\n            list($path, $payload) = $this->parseRequest($request);\n            $response = ($this->routerCallback)($path, $payload);\n\n            $this->sendResponse($clientSocket, $response);\n            $this->closeClientSocket($clientSocket, $key);\n        }\n    }\n\n    private function readClientRequest($clientSocket)\n    {\n        $request = stream_get_contents($clientSocket);\n\n        if ($request === false) {\n            throw new Exception('Socket read failed');\n        }\n\n        return $request;\n    }\n\n    private function parseRequest($request)\n    {\n        $lines = explode(\"\\r\\n\", $request);\n        $requestLine = $lines[0];\n        $parts = explode(' ', $requestLine);\n        if (count($parts) !== 3) {\n            throw new Exception(\"Malformed request line: $requestLine\");\n        }\n        $path = $parts[1];\n\n        $headers = [];\n        for ($i = 1; $i < count($lines); $i++) {\n            $line = $lines[$i];\n            if (empty($line)) {\n                break;\n            }\n            [$key, $value] = explode(':', $line, 2);\n            $headers[$key] = trim($value);\n        }\n        if (empty($headers['Content-Type']) || $headers['Content-Type'] !== 'application/json') {\n            throw new Exception(\"Malformed request: invalid Content-Type\");\n        }\n\n        $payload = json_decode(implode(\"\\n\", array_slice($lines, $i + 1)), true, flags: JSON_THROW_ON_ERROR);\n        return [$path, $payload];\n    }\n\n    private function sendResponse($clientSocket, Response $response)\n    {\n        $code = $response->getStatus();\n        $content = json_encode($response->getBody(), flags: JSON_PRETTY_PRINT);\n        $headers = \"HTTP/1.1 $code OK\\r\\n\" .\n            \"Content-Type: application/json\\r\\n\" .\n            \"Content-Length: \" . strlen($content) . \"\\r\\n\" .\n            \"Connection: close\\r\\n\\r\\n\";\n        fwrite($clientSocket, $headers . $content);\n    }\n\n    private function closeClientSocket($clientSocket, $key): void\n    {\n        fclose($clientSocket);\n        unset($this->clientSockets[$key]);\n    }\n\n    public function afterRun(Closure $param): void\n    {\n        $this->afterCallback = $param;\n    }\n\n    public function ticker(Closure $param): void\n    {\n        $this->tickerCallback = $param;\n    }\n\n    public function router(Closure $param)\n    {\n        $this->routerCallback = $param;\n    }\n\n    public function stop(): void\n    {\n        $this->stop = true;\n    }\n}\n"
  },
  {
    "path": "src/Executor/Worker.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Executor;\n\nuse Deployer\\Deployer;\nuse Deployer\\Exception\\Exception;\nuse Deployer\\Exception\\GracefulShutdownException;\nuse Deployer\\Exception\\RunException;\nuse Deployer\\Host\\Host;\nuse Deployer\\Task\\Context;\nuse Deployer\\Task\\Task;\nuse Throwable;\n\nclass Worker\n{\n    private Deployer $deployer;\n\n    public function __construct(Deployer $deployer)\n    {\n        $this->deployer = $deployer;\n    }\n\n    public function execute(Task $task, Host $host): int\n    {\n        try {\n            Exception::setTaskSourceLocation($task->getSourceLocation());\n\n            $context = new Context($host);\n            $task->run($context);\n\n            if ($task->getName() !== 'connect') {\n                $this->deployer->messenger->endOnHost($host);\n            }\n            return 0;\n        } catch (Throwable $e) {\n            $this->deployer->messenger->renderException($e, $host);\n            if ($e instanceof GracefulShutdownException) {\n                return GracefulShutdownException::EXIT_CODE;\n            }\n            if ($e instanceof RunException) {\n                return $e->getExitCode();\n            }\n            return 255;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Host/Host.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Host;\n\nuse Deployer\\Configuration;\nuse Deployer\\Deployer;\nuse Deployer\\Exception\\ConfigurationException;\nuse Deployer\\Exception\\Exception;\nuse Deployer\\Task\\Context;\n\nuse function Deployer\\Support\\colorize_host;\nuse function Deployer\\Support\\parse_home_dir;\n\nclass Host\n{\n    /**\n     * @var Configuration $config\n     */\n    private $config;\n\n    public function __construct(string $hostname)\n    {\n        $parent = null;\n        if (Deployer::get()) {\n            $parent = Deployer::get()->config;\n        }\n        $this->config = new Configuration($parent);\n        $this->set('#alias', $hostname);\n        $this->set('hostname', preg_replace('/\\/.+$/', '', $hostname));\n    }\n\n    public function __toString(): string\n    {\n        return $this->getTag();\n    }\n\n    public function config(): Configuration\n    {\n        return $this->config;\n    }\n\n    /**\n     * @param mixed $value\n     */\n    public function set(string $name, $value): self\n    {\n        if ($name === 'alias') {\n            throw new ConfigurationException(\"Can not update alias of the host.\\nThis will change only host own alias,\\nbut not the key it is stored in HostCollection.\");\n        }\n        if ($name === '#alias') {\n            $name = 'alias';\n        }\n        $this->config->set($name, $value);\n        return $this;\n    }\n\n    public function add(string $name, array $value): self\n    {\n        $this->config->add($name, $value);\n        return $this;\n    }\n\n    public function has(string $name): bool\n    {\n        return $this->config->has($name);\n    }\n\n    public function hasOwn(string $name): bool\n    {\n        return $this->config->hasOwn($name);\n    }\n\n    /**\n     * @param mixed|null $default\n     * @return mixed|null\n     */\n    public function get(string $name, $default = null)\n    {\n        return $this->config->get($name, $default);\n    }\n\n    public function getAlias(): ?string\n    {\n        return $this->config->get('alias', null);\n    }\n\n    public function setTag(string $tag): self\n    {\n        $this->config->set('tag', $tag);\n        return $this;\n    }\n\n    public function getTag(): ?string\n    {\n        return $this->config->get('tag', colorize_host($this->getAlias()));\n    }\n\n    public function setHostname(string $hostname): self\n    {\n        $this->config->set('hostname', $hostname);\n        return $this;\n    }\n\n    public function getHostname(): ?string\n    {\n        return $this->config->get('hostname', null);\n    }\n\n    public function setRemoteUser(string $user): self\n    {\n        $this->config->set('remote_user', $user);\n        return $this;\n    }\n\n    public function getRemoteUser(): ?string\n    {\n        return $this->config->get('remote_user', null);\n    }\n\n    /**\n     * @param string|int|null $port\n     * @return $this\n     */\n    public function setPort($port): self\n    {\n        $this->config->set('port', $port);\n        return $this;\n    }\n\n    /**\n     * @return string|int|null\n     */\n    public function getPort()\n    {\n        return $this->config->get('port', null);\n    }\n\n    public function setConfigFile(string $file): self\n    {\n        $this->config->set('config_file', $file);\n        return $this;\n    }\n\n    public function getConfigFile(): ?string\n    {\n        return $this->config->get('config_file', null);\n    }\n\n    public function setIdentityFile(string $file): self\n    {\n        $this->config->set('identity_file', $file);\n        return $this;\n    }\n\n    public function getIdentityFile(): ?string\n    {\n        return $this->config->get('identity_file', null);\n    }\n\n    public function setForwardAgent(bool $on): self\n    {\n        $this->config->set('forward_agent', $on);\n        return $this;\n    }\n\n    public function getForwardAgent(): ?bool\n    {\n        return $this->config->get('forward_agent', null);\n    }\n\n    public function setSshMultiplexing(bool $on): self\n    {\n        $this->config->set('ssh_multiplexing', $on);\n        return $this;\n    }\n\n    public function getSshMultiplexing(): ?bool\n    {\n        return $this->config->get('ssh_multiplexing', null);\n    }\n\n    public function setShell(string $command): self\n    {\n        $this->config->set('shell', $command);\n        return $this;\n    }\n\n    public function getShell(): ?string\n    {\n        return $this->config->get('shell', null);\n    }\n\n    public function setShellPath(string $path): self\n    {\n        $this->config->set('shell_path', $path);\n        return $this;\n    }\n\n    public function getShellPath(): ?string\n    {\n        return $this->config->get('shell_path', null);\n    }\n\n    public function setDeployPath(string $path): self\n    {\n        $this->config->set('deploy_path', $path);\n        return $this;\n    }\n\n    public function getDeployPath(): ?string\n    {\n        return $this->config->get('deploy_path', null);\n    }\n\n    public function setLabels(array $labels): self\n    {\n        $this->config->set('labels', $labels);\n        return $this;\n    }\n\n    public function addLabels(array $labels): self\n    {\n        $existingLabels = $this->getLabels() ?? [];\n        $this->setLabels(array_replace_recursive($existingLabels, $labels));\n        return $this;\n    }\n\n    public function getLabels(): ?array\n    {\n        return $this->config->get('labels', null);\n    }\n\n    public function setSshArguments(array $args): self\n    {\n        $this->config->set('ssh_arguments', $args);\n        return $this;\n    }\n\n    public function getSshArguments(): ?array\n    {\n        return $this->config->get('ssh_arguments', null);\n    }\n\n    public function setSshControlPath(string $path): self\n    {\n        $this->config->set('ssh_control_path', $path);\n        return $this;\n    }\n\n    public function getSshControlPath(): string\n    {\n        return $this->config->get('ssh_control_path', $this->generateControlPath());\n    }\n\n    private function generateControlPath(): string\n    {\n        $C = $this->getHostname();\n        if ($this->has('remote_user')) {\n            $C = $this->getRemoteUser() . '@' . $C;\n        }\n        if ($this->has('port')) {\n            $C .= ':' . $this->getPort();\n        }\n\n        // In case of CI environment, lets use shared memory.\n        if (getenv('CI') && is_writable('/dev/shm')) {\n            return \"/dev/shm/$C\";\n        }\n\n        return \"~/.ssh/$C\";\n    }\n\n    public function connectionString(): string\n    {\n        if ($this->get('remote_user', '') !== '') {\n            return $this->get('remote_user') . '@' . $this->get('hostname');\n        }\n        return $this->get('hostname');\n    }\n\n    public function connectionOptionsString(): string\n    {\n        return implode(' ', array_map('escapeshellarg', $this->connectionOptionsArray()));\n    }\n\n    /**\n     * @return string[]\n     */\n    public function connectionOptionsArray(): array\n    {\n        $options = [];\n        if ($this->has('ssh_arguments')) {\n            foreach ($this->getSshArguments() as $arg) {\n                $options = array_merge($options, explode(' ', $arg));\n            }\n        }\n        if ($this->has('port')) {\n            $options = array_merge($options, ['-p', $this->getPort()]);\n        }\n        if ($this->has('config_file')) {\n            $options = array_merge($options, ['-F', parse_home_dir($this->getConfigFile())]);\n        }\n        if ($this->has('identity_file')) {\n            $options = array_merge($options, ['-i', parse_home_dir($this->getIdentityFile())]);\n        }\n        if ($this->has('forward_agent') && $this->getForwardAgent()) {\n            $options = array_merge($options, ['-A']);\n        }\n        if ($this->has('ssh_multiplexing') && $this->getSshMultiplexing()) {\n            $options = array_merge($options, [\n                '-o', 'ControlMaster=auto',\n                '-o', 'ControlPersist=60',\n                '-o', 'ControlPath=' . $this->getSshControlPath(),\n            ]);\n        }\n        return $options;\n    }\n}\n"
  },
  {
    "path": "src/Host/HostCollection.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Host;\n\nuse Deployer\\Collection\\Collection;\n\n/**\n * @method Host get($name)\n * @method Host[] getIterator()\n */\nclass HostCollection extends Collection\n{\n    protected function notFound(string $name): \\InvalidArgumentException\n    {\n        return new \\InvalidArgumentException(\"Host \\\"$name\\\" not found.\");\n    }\n}\n"
  },
  {
    "path": "src/Host/Localhost.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Host;\n\nclass Localhost extends Host\n{\n    public function __construct(string $hostname = 'localhost')\n    {\n        parent::__construct($hostname);\n    }\n}\n"
  },
  {
    "path": "src/Host/Range.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Host;\n\nclass Range\n{\n    public const PATTERN = '/\\[(.+?)\\]/';\n\n    public static function expand(array $hostnames): array\n    {\n        $expanded = [];\n        foreach ($hostnames as $hostname) {\n            if (preg_match(self::PATTERN, $hostname, $matches)) {\n                [$start, $end] = explode(':', $matches[1]);\n                $zeroBased = (bool) preg_match('/^0[1-9]/', $start);\n\n                foreach (range($start, $end) as $i) {\n                    $expanded[] = preg_replace(self::PATTERN, self::format((string) $i, $zeroBased), $hostname);\n                }\n            } else {\n                $expanded[] = $hostname;\n            }\n        }\n\n        return $expanded;\n    }\n\n    private static function format(string $i, bool $zeroBased): string\n    {\n        if ($zeroBased) {\n            return strlen($i) === 1 ? \"0$i\" : $i;\n        } else {\n            return $i;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Importer/Importer.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Importer;\n\nuse Deployer\\Exception\\ConfigurationException;\nuse Deployer\\Exception\\Exception;\nuse Symfony\\Component\\Yaml\\Yaml;\n\nuse function array_filter;\nuse function array_keys;\nuse function Deployer\\after;\nuse function Deployer\\before;\nuse function Deployer\\cd;\nuse function Deployer\\download;\nuse function Deployer\\host;\nuse function Deployer\\localhost;\nuse function Deployer\\run;\nuse function Deployer\\runLocally;\nuse function Deployer\\set;\nuse function Deployer\\Support\\find_line_number;\nuse function Deployer\\task;\nuse function Deployer\\upload;\n\nuse const ARRAY_FILTER_USE_KEY;\n\nclass Importer\n{\n    /**\n     * @var string\n     */\n    private static $recipeFilename;\n    /**\n     * @var string\n     */\n    private static $recipeSource;\n\n    /**\n     * @param string|string[] $paths\n     */\n    public static function import($paths)\n    {\n        if (!is_array($paths)) {\n            $paths = [$paths];\n        }\n        foreach ($paths as $path) {\n            if (preg_match('/\\.php$/i', $path)) {\n                // Prevent variable leak into deploy.php file\n                call_user_func(function () use ($path) {\n                    // Reorder autoload stack\n                    $originStack = spl_autoload_functions();\n\n                    require $path;\n\n                    $newStack = spl_autoload_functions();\n                    if ($originStack[0] !== $newStack[0]) {\n                        foreach (array_reverse($originStack) as $loader) {\n                            spl_autoload_unregister($loader);\n                            spl_autoload_register($loader, true, true);\n                        }\n                    }\n                });\n            } elseif (preg_match('/\\.ya?ml$/i', $path)) {\n                self::$recipeFilename = basename($path);\n                self::$recipeSource = file_get_contents($path, true);\n\n                $root = array_filter(Yaml::parse(self::$recipeSource), static function (string $key) {\n                    return !str_starts_with($key, '.');\n                }, ARRAY_FILTER_USE_KEY);\n\n                foreach (array_keys($root) as $key) {\n                    static::$key($root[$key]);\n                }\n            } else {\n                throw new Exception(\"Unknown file format: $path\\nOnly .php and .yaml supported.\");\n            }\n        }\n    }\n\n    protected static function hosts(array $hosts)\n    {\n        foreach ($hosts as $alias => $config) {\n            if ($config['local'] ?? false) {\n                $host = localhost($alias);\n            } else {\n                $host = host($alias);\n            }\n            if (is_array($config)) {\n                foreach ($config as $key => $value) {\n                    $host->set($key, $value);\n                }\n            }\n        }\n    }\n\n    protected static function config(array $config)\n    {\n        foreach ($config as $key => $value) {\n            set($key, $value);\n        }\n    }\n\n    protected static function tasks(array $tasks)\n    {\n        $buildTask = function ($name, $steps) {\n            $body = function () {};\n            $task = task($name, $body);\n\n            foreach ($steps as $step) {\n                $buildStep = function ($step) use (&$body, $task) {\n                    extract($step);\n\n                    if (isset($cd)) {\n                        $prev = $body;\n                        $body = function () use ($cd, $prev) {\n                            $prev();\n                            cd($cd);\n                        };\n                    }\n\n                    if (isset($run)) {\n                        $has = 'run';\n                        $prev = $body;\n                        $body = function () use ($run, $prev) {\n                            $prev();\n                            try {\n                                run($run);\n                            } catch (Exception $e) {\n                                $e->setTaskFilename(self::$recipeFilename);\n                                $e->setTaskLineNumber(find_line_number(self::$recipeSource, $run));\n                                throw $e;\n                            }\n                        };\n                    }\n\n                    if (isset($run_locally)) {\n                        if (isset($has)) {\n                            throw new ConfigurationException(\"Task step can not have both $has and run_locally.\");\n                        }\n                        $has = 'run_locally';\n                        $prev = $body;\n                        $body = function () use ($run_locally, $prev) {\n                            $prev();\n                            try {\n                                runLocally($run_locally);\n                            } catch (Exception $e) {\n                                $e->setTaskFilename(self::$recipeFilename);\n                                $e->setTaskLineNumber(find_line_number(self::$recipeSource, $run_locally));\n                                throw $e;\n                            }\n                        };\n                    }\n\n                    if (isset($upload)) {\n                        if (isset($has)) {\n                            throw new ConfigurationException(\"Task step can not have both $has and upload.\");\n                        }\n                        $has = 'upload';\n                        $prev = $body;\n                        $body = function () use ($upload, $prev) {\n                            $prev();\n                            upload($upload['src'], $upload['dest']);\n                        };\n                    }\n\n                    if (isset($download)) {\n                        if (isset($has)) {\n                            throw new ConfigurationException(\"Task step can not have both $has and download.\");\n                        }\n                        $has = 'download';\n                        $prev = $body;\n                        $body = function () use ($download, $prev) {\n                            $prev();\n                            download($download['src'], $download['dest']);\n                        };\n                    }\n\n                    $methods = [\n                        'desc',\n                        'once',\n                        'hidden',\n                        'limit',\n                        'select',\n                    ];\n                    foreach ($methods as $method) {\n                        if (isset($$method)) {\n                            $task->$method($$method);\n                        }\n                    }\n                };\n\n                $buildStep($step);\n                $task->setCallback($body);\n            }\n        };\n\n        foreach ($tasks as $name => $config) {\n            foreach ($config as $key => $value) {\n                if (!is_int($key) || !is_string($value)) {\n                    goto not_a_group_task;\n                }\n            }\n\n            // Create a group task.\n            task($name, $config);\n            continue;\n\n            not_a_group_task:\n            $buildTask($name, $config);\n        }\n    }\n\n    protected static function after(array $after)\n    {\n        foreach ($after as $key => $value) {\n            if (is_array($value)) {\n                foreach (array_reverse($value) as $v) {\n                    after($key, $v);\n                }\n            } else {\n                after($key, $value);\n            }\n        }\n    }\n\n    protected static function before(array $before)\n    {\n        foreach ($before as $key => $value) {\n            if (is_array($value)) {\n                foreach (array_reverse($value) as $v) {\n                    before($key, $v);\n                }\n            } else {\n                before($key, $value);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Logger/Handler/FileHandler.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Logger\\Handler;\n\nclass FileHandler implements HandlerInterface\n{\n    /**\n     * @var string\n     */\n    private $filePath;\n\n    public function __construct(string $filePath)\n    {\n        $this->filePath = $filePath;\n    }\n\n    public function log(string $message): void\n    {\n        file_put_contents($this->filePath, $message, FILE_APPEND);\n    }\n}\n"
  },
  {
    "path": "src/Logger/Handler/HandlerInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Logger\\Handler;\n\ninterface HandlerInterface\n{\n    public function log(string $message): void;\n}\n"
  },
  {
    "path": "src/Logger/Handler/NullHandler.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Logger\\Handler;\n\nclass NullHandler implements HandlerInterface\n{\n    public function log(string $message): void {}\n}\n"
  },
  {
    "path": "src/Logger/Logger.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Logger;\n\nuse Deployer\\ProcessRunner\\Printer;\nuse Deployer\\Host\\Host;\nuse Deployer\\Logger\\Handler\\HandlerInterface;\n\nclass Logger\n{\n    /**\n     * @var HandlerInterface\n     */\n    private $handler;\n\n    public function __construct(HandlerInterface $handler)\n    {\n        $this->handler = $handler;\n    }\n\n    public function log(string $message): void\n    {\n        $this->handler->log(\"$message\\n\");\n    }\n\n    public function callback(Host $host): \\Closure\n    {\n        return function ($type, $buffer) use ($host) {\n            $this->printBuffer($host, $type, $buffer);\n        };\n    }\n\n    public function printBuffer(Host $host, string $type, string $buffer): void\n    {\n        foreach (explode(\"\\n\", rtrim($buffer)) as $line) {\n            $this->writeln($host, $type, $line);\n        }\n    }\n\n    public function writeln(Host $host, string $type, string $line): void\n    {\n        // Omit empty lines\n        if (empty($line)) {\n            return;\n        }\n\n        $this->log(\"[{$host->getAlias()}] $line\");\n    }\n}\n"
  },
  {
    "path": "src/ProcessRunner/Printer.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\ProcessRunner;\n\nuse Deployer\\Host\\Host;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass Printer\n{\n    private OutputInterface $output;\n\n    public function __construct(OutputInterface $output)\n    {\n        $this->output = $output;\n    }\n\n    public function command(Host $host, string $type, string $command): void\n    {\n        // -v for run command\n        if ($this->output->isVerbose()) {\n            $this->output->writeln(\"[$host] <fg=green;options=bold>$type</> $command\");\n        }\n    }\n\n    /**\n     * Returns a callable for use with the symfony Process->run($callable) method.\n     *\n     * @return callable A function expecting a int $type (e.g. Process::OUT or Process::ERR) and string $buffer parameters.\n     */\n    public function callback(Host $host, bool $forceOutput): callable\n    {\n        return function ($type, $buffer) use ($forceOutput, $host) {\n            if ($this->output->isVerbose() || $forceOutput) {\n                $this->printBuffer($type, $host, $buffer);\n            }\n        };\n    }\n\n    /**\n     * @param string $type Process::OUT or Process::ERR\n     */\n    public function printBuffer(string $type, Host $host, string $buffer): void\n    {\n        foreach (explode(\"\\n\", rtrim($buffer)) as $line) {\n            $this->writeln($type, $host, $line);\n        }\n    }\n\n    public function writeln(string $type, Host $host, string $line): void\n    {\n        // Omit empty lines\n        if (empty($line)) {\n            return;\n        }\n\n        $this->output->writeln(\"[$host] $line\");\n    }\n}\n"
  },
  {
    "path": "src/ProcessRunner/ProcessRunner.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\ProcessRunner;\n\nuse Deployer\\Exception\\RunException;\nuse Deployer\\Exception\\TimeoutException;\nuse Deployer\\Host\\Host;\nuse Deployer\\Logger\\Logger;\nuse Deployer\\Ssh\\RunParams;\nuse Symfony\\Component\\Process\\Exception\\ProcessFailedException;\nuse Symfony\\Component\\Process\\Exception\\ProcessTimedOutException;\nuse Symfony\\Component\\Process\\Process;\n\nuse function Deployer\\Support\\deployer_root;\nuse function Deployer\\Support\\env_stringify;\n\nclass ProcessRunner\n{\n    private Printer $pop;\n    private Logger $logger;\n\n    public function __construct(Printer $pop, Logger $logger)\n    {\n        $this->pop = $pop;\n        $this->logger = $logger;\n    }\n\n    public function run(Host $host, string $command, RunParams $params): string\n    {\n        $this->pop->command($host, 'run', $command);\n\n        $terminalOutput = $this->pop->callback($host, $params->forceOutput);\n        $callback = function ($type, $buffer) use ($host, $terminalOutput) {\n            $this->logger->printBuffer($host, $type, $buffer);\n            $terminalOutput($type, $buffer);\n        };\n\n        if (!empty($params->secrets)) {\n            foreach ($params->secrets as $key => $value) {\n                $command = str_replace('%' . $key . '%', $value, $command);\n            }\n        }\n\n        if (!empty($params->env)) {\n            $env = env_stringify($params->env);\n            $command = \"export $env; $command\";\n        }\n\n        if (!empty($params->dotenv)) {\n            $command = \"source $params->dotenv; $command\";\n        }\n\n        $process = Process::fromShellCommandline($params->shell)\n            ->setInput($command)\n            ->setTimeout($params->timeout)\n            ->setIdleTimeout($params->idleTimeout)\n            ->setWorkingDirectory($params->cwd ?? deployer_root());\n\n        try {\n            $process->mustRun($callback);\n            return $process->getOutput();\n        } catch (ProcessFailedException) {\n            if ($params->nothrow) {\n                return '';\n            }\n            throw new RunException(\n                $host,\n                $command,\n                $process->getExitCode(),\n                $process->getOutput(),\n                $process->getErrorOutput(),\n            );\n        } catch (ProcessTimedOutException $exception) { // @phpstan-ignore-line PHPStan doesn't know about ProcessTimedOutException for some reason.\n            throw new TimeoutException(\n                $command,\n                $exception->getExceededTimeout(),\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/Selector/Selector.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Selector;\n\nuse Deployer\\Host\\Host;\nuse Deployer\\Host\\HostCollection;\n\nuse function Deployer\\Support\\array_all;\n\nclass Selector\n{\n    /**\n     * @var HostCollection\n     */\n    private $hosts;\n\n    public function __construct(HostCollection $hosts)\n    {\n        $this->hosts = $hosts;\n    }\n\n    /**\n     * @return Host[]\n     */\n    public function select(string $selectExpression)\n    {\n        $conditions = self::parse($selectExpression);\n\n        $hosts = [];\n        foreach ($this->hosts as $host) {\n            if (self::apply($conditions, $host)) {\n                $hosts[] = $host;\n            }\n        }\n\n        return $hosts;\n    }\n\n    public static function apply(?array $conditions, Host $host): bool\n    {\n        if (empty($conditions)) {\n            return true;\n        }\n\n        $labels = $host->get('labels', []);\n        $labels['alias'] = $host->getAlias();\n        $labels['true'] = 'true';\n        $isTrue = function ($value) {\n            return $value;\n        };\n\n        foreach ($conditions as $hmm) {\n            $ok = [];\n            foreach ($hmm as [$op, $var, $value]) {\n                if (is_array($value)) {\n                    $orOk = [];\n                    foreach ($value as $val) {\n                        $orOk[] = self::compare($op, $labels[$var] ?? null, $val);\n                    }\n                    $ok[] = count(array_filter($orOk, $isTrue)) > 0;\n                } else {\n                    $ok[] = self::compare($op, $labels[$var] ?? null, $value);\n                }\n            }\n            if (count($ok) > 0 && array_all($ok, $isTrue)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * @param string|string[] $a\n     */\n    private static function compare(string $op, $a, ?string $b): bool\n    {\n        $matchFunction = function ($a, ?string $b) {\n            foreach ((array) $a as $item) {\n                if ($item === $b) {\n                    return true;\n                }\n            }\n\n            return false;\n        };\n\n        if ($op === '=') {\n            return $matchFunction($a, $b);\n        }\n        if ($op === '!=') {\n            return !$matchFunction($a, $b);\n        }\n        return false;\n    }\n\n    public static function parse(string $expression): array\n    {\n        $all = [];\n        foreach (explode(',', $expression) as $sub) {\n            $conditions = [];\n            foreach (explode('&', $sub) as $part) {\n                $part = trim($part);\n                if ($part === 'all') {\n                    $conditions[] = ['=', 'true', 'true'];\n                    continue;\n                }\n                if (preg_match('/(?<var>.+?)(?<op>!?=)(?<value>.+)/', $part, $match)) {\n                    $values = array_map('trim', explode('|', trim($match['value'])));\n                    $conditions[] = [$match['op'], trim($match['var']), $values];\n                } else {\n                    $conditions[] = ['=', 'alias', trim($part)];\n                }\n            }\n            $all[] = $conditions;\n        }\n        return $all;\n    }\n}\n"
  },
  {
    "path": "src/Ssh/IOArguments.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Ssh;\n\nuse Deployer\\Exception\\Exception;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass IOArguments\n{\n    public static function collect(InputInterface $input, OutputInterface $output): array\n    {\n        $arguments = [];\n        foreach ($input->getOptions() as $name => $value) {\n            if (!$input->getOption($name)) {\n                continue;\n            }\n            if ($name === 'file') {\n                $arguments[] = \"--file\";\n                $arguments[] = ltrim($value, '=');\n                continue;\n            }\n            if (in_array($name, ['verbose'], true)) {\n                continue;\n            }\n            if (!is_array($value)) {\n                $value = [$value];\n            }\n            foreach ($value as $v) {\n                if (is_bool($v)) {\n                    $arguments[] = \"--$name\";\n                    continue;\n                }\n\n                $arguments[] = \"--$name\";\n                $arguments[] = $v;\n            }\n        }\n\n        if ($output->isDecorated()) {\n            $arguments[] = '--decorated';\n        }\n        $verbosity = self::verbosity($output->getVerbosity());\n        if (!empty($verbosity)) {\n            $arguments[] = $verbosity;\n        }\n        return $arguments;\n    }\n\n    private static function verbosity(int $verbosity): string\n    {\n        switch ($verbosity) {\n            case OutputInterface::VERBOSITY_QUIET:\n                return '-q';\n            case OutputInterface::VERBOSITY_NORMAL:\n                return '';\n            case OutputInterface::VERBOSITY_VERBOSE:\n                return '-v';\n            case OutputInterface::VERBOSITY_VERY_VERBOSE:\n                return '-vv';\n            case OutputInterface::VERBOSITY_DEBUG:\n                return '-vvv';\n            default:\n                throw new Exception('Unknown verbosity level: ' . $verbosity);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ssh/RunParams.php",
    "content": "<?php\n\nnamespace Deployer\\Ssh;\n\nclass RunParams\n{\n    public function __construct(\n        public ?string  $shell = null,\n        public ?string $cwd = null,\n        public ?array  $env = null,\n        public ?string $dotenv = null,\n        public bool    $nothrow = false,\n        public ?int    $timeout = null,\n        public ?int    $idleTimeout = null,\n        public bool    $forceOutput = false,\n        #[\\SensitiveParameter]\n        public ?array  $secrets = null,\n    ) {}\n\n    public function with(\n        #[\\SensitiveParameter]\n        ?array $secrets = null,\n        ?int $timeout = null,\n    ): self {\n        $params = clone $this;\n        $params->secrets = array_merge($params->secrets ?? [], $secrets ?? []);\n        $params->timeout = $timeout ?? $params->timeout;\n        return $params;\n    }\n}\n"
  },
  {
    "path": "src/Ssh/SshClient.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Ssh;\n\nuse Deployer\\ProcessRunner\\Printer;\nuse Deployer\\Exception\\RunException;\nuse Deployer\\Exception\\TimeoutException;\nuse Deployer\\Host\\Host;\nuse Deployer\\Logger\\Logger;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Process\\Exception\\ProcessTimedOutException;\nuse Symfony\\Component\\Process\\Process;\n\nuse function Deployer\\Support\\env_stringify;\n\nclass SshClient\n{\n    private OutputInterface $output;\n    private Printer $pop;\n    private Logger $logger;\n\n    public function __construct(OutputInterface $output, Printer $pop, Logger $logger)\n    {\n        $this->output = $output;\n        $this->pop = $pop;\n        $this->logger = $logger;\n    }\n\n    public function run(Host $host, string $command, RunParams $params): string\n    {\n        $shellId = 'id$' . bin2hex(random_bytes(10));\n        $shellCommand = $host->getShell();\n        if ($host->has('become') && !empty($host->get('become'))) {\n            $shellCommand = \"sudo -H -u {$host->get('become')} \" . $shellCommand;\n        }\n\n        $ssh = array_merge(['ssh'], $host->connectionOptionsArray(), [$host->connectionString(), \": $shellId; $shellCommand\"]);\n\n        // -vvv for ssh command\n        if ($this->output->isDebug()) {\n            $sshString = $ssh[0];\n            for ($i = 1; $i < count($ssh); $i++) {\n                $sshString .= ' ' . escapeshellarg((string) $ssh[$i]);\n            }\n            $this->output->writeln(\"[$host] $sshString\");\n        }\n\n        if (!empty($params->cwd)) {\n            $command = \"cd $params->cwd && ($command)\";\n        }\n\n        if (!empty($params->env)) {\n            $env = env_stringify($params->env);\n            $command = \"export $env; $command\";\n        }\n\n        if (!empty($params->secrets)) {\n            foreach ($params->secrets as $key => $value) {\n                $command = str_replace('%' . $key . '%', strval($value), $command);\n            }\n        }\n\n        $this->pop->command($host, 'run', $command);\n        $this->logger->log(\"[{$host->getAlias()}] run $command\");\n\n\n        $process = new Process($ssh);\n        $process\n            ->setInput($command)\n            ->setTimeout($params->timeout)\n            ->setIdleTimeout($params->idleTimeout);\n\n        $callback = function ($type, $buffer) use ($params, $host) {\n            $this->logger->printBuffer($host, $type, $buffer);\n            $this->pop->callback($host, $params->forceOutput)($type, $buffer);\n        };\n\n        try {\n            $process->run($callback);\n        } catch (ProcessTimedOutException $exception) {\n            // Let's try to kill all processes started by this command.\n            $pid = $this->run($host, \"ps x | grep $shellId | grep -v grep | awk '{print \\$1}'\", $params->with(timeout: 10));\n            // Minus before pid means all processes in this group.\n            $this->run($host, \"kill -9 -$pid\", $params->with(timeout: 20));\n            throw new TimeoutException(\n                $command,\n                $exception->getExceededTimeout(),\n            );\n        }\n\n        $output = $process->getOutput();\n        $exitCode = $process->getExitCode();\n\n        if ($exitCode !== 0 && !$params->nothrow) {\n            throw new RunException(\n                $host,\n                $command,\n                $exitCode,\n                $output,\n                $process->getErrorOutput(),\n            );\n        }\n\n        return $output;\n    }\n}\n"
  },
  {
    "path": "src/Support/ObjectProxy.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Support;\n\nclass ObjectProxy\n{\n    /**\n     * @var array\n     */\n    private $objects;\n\n    public function __construct(array $objects)\n    {\n        $this->objects = $objects;\n    }\n\n    public function __call(string $name, array $arguments): self\n    {\n        foreach ($this->objects as $object) {\n            $object->$name(...$arguments);\n        }\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Support/Reporter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Support;\n\nuse Deployer\\Utility\\Httpie;\nuse Symfony\\Component\\Process\\PhpProcess;\n\n/**\n * @codeCoverageIgnore\n */\nclass Reporter\n{\n    public static function report(array $stats): void\n    {\n        $version = DEPLOYER_VERSION;\n        $body = json_encode($stats);\n        $length = strlen($body);\n        $php = new PhpProcess(<<<EOF\n            <?php\n            \\$ch = curl_init('https://deployer.org/api/stats');\n            curl_setopt(\\$ch, CURLOPT_USERAGENT, 'Deployer/$version');\n            curl_setopt(\\$ch, CURLOPT_CUSTOMREQUEST, 'POST');\n            curl_setopt(\\$ch, CURLOPT_HTTPHEADER, [\n                'Content-Type: application/json',\n                'Content-Length: $length',\n            ]);\n            curl_setopt(\\$ch, CURLOPT_POSTFIELDS, '$body');\n            curl_setopt(\\$ch, CURLOPT_SSL_VERIFYPEER, false);\n            curl_setopt(\\$ch, CURLOPT_RETURNTRANSFER, true);\n            curl_setopt(\\$ch, CURLOPT_FOLLOWLOCATION, true);\n            curl_setopt(\\$ch, CURLOPT_MAXREDIRS, 10);\n            curl_setopt(\\$ch, CURLOPT_CONNECTTIMEOUT, 5);\n            curl_setopt(\\$ch, CURLOPT_TIMEOUT, 5);\n            \\$result = curl_exec(\\$ch);\n            if (PHP_MAJOR_VERSION < 8) {\n                curl_close(\\$ch);\n            }\n            EOF);\n        $php->start();\n    }\n}\n"
  },
  {
    "path": "src/Support/helpers.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Support;\n\nfunction array_flatten(array $array): array\n{\n    $flatten = [];\n    array_walk_recursive($array, function ($value) use (&$flatten) {\n        $flatten[] = $value;\n    });\n    return $flatten;\n}\n\n/**\n * Recursively merge two config arrays with a specific behavior:\n *\n * 1. scalar values are overridden\n * 2. array values are extended uniquely if all keys are numeric\n * 3. all other array values are merged\n */\nfunction array_merge_alternate(array $original, array $override): array\n{\n    foreach ($override as $key => $value) {\n        if (isset($original[$key])) {\n            if (!is_array($original[$key])) {\n                if (is_numeric($key)) {\n                    // Append scalar value\n                    $original[] = $value;\n                } else {\n                    // Override scalar value\n                    $original[$key] = $value;\n                }\n            } elseif (array_keys($original[$key]) === range(0, count($original[$key]) - 1)) {\n                // Uniquely append to array with numeric keys\n                $original[$key] = array_unique(array_merge($original[$key], $value));\n            } else {\n                // Merge all other arrays\n                $original[$key] = array_merge_alternate($original[$key], $value);\n            }\n        } else {\n            // Simply add new key/value\n            $original[$key] = $value;\n        }\n    }\n\n    return $original;\n}\n\nfunction env_stringify(array $array): string\n{\n    return implode(' ', array_map(\n        function ($key, $value) {\n            return sprintf(\"%s=%s\", $key, escapeshellarg((string) $value));\n        },\n        array_keys($array),\n        $array,\n    ));\n}\n\nfunction is_closure(mixed $var): bool\n{\n    return is_object($var) && ($var instanceof \\Closure);\n}\n\n/**\n * Check if all elements satisfy predicate.\n */\nfunction array_all(array $array, callable $predicate): bool\n{\n    foreach ($array as $key => $value) {\n        if (!$predicate($value, $key)) {\n            return false;\n        }\n    }\n    return true;\n}\n\n/**\n * Cleanup CRLF new line endings.\n */\nfunction normalize_line_endings(string $string): string\n{\n    return str_replace([\"\\r\\n\", \"\\r\"], \"\\n\", $string);\n}\n\n/**\n * Expand leading tilde (~) symbol in given path.\n */\nfunction parse_home_dir(string $path): string\n{\n    if ('~' === $path || str_starts_with($path, '~/')) {\n        if (isset($_SERVER['HOME'])) {\n            $home = $_SERVER['HOME'];\n        } elseif (isset($_SERVER['HOMEDRIVE'], $_SERVER['HOMEPATH'])) {\n            $home = $_SERVER['HOMEDRIVE'] . $_SERVER['HOMEPATH'];\n        } else {\n            return $path;\n        }\n\n        return $home . substr($path, 1);\n    }\n\n    return $path;\n}\n\nfunction find_line_number(string $source, string $string): int\n{\n    $string = explode(PHP_EOL, $string)[0];\n    $before = strstr($source, $string, true);\n    if (false !== $before) {\n        return count(explode(PHP_EOL, $before));\n    }\n    return 1;\n}\n\nfunction colorize_host(string $alias): string\n{\n    if (defined('NO_ANSI')) {\n        return $alias;\n    }\n\n    if (in_array($alias, ['localhost', 'local'], true)) {\n        return $alias;\n    }\n\n    if (getenv('COLORTERM') === 'truecolor') {\n        $hsv = function ($h, $s, $v) {\n            $r = $g = $b = $i = $f = $p = $q = $t = 0;\n            $i = floor($h * 6);\n            $f = $h * 6 - $i;\n            $p = $v * (1 - $s);\n            $q = $v * (1 - $f * $s);\n            $t = $v * (1 - (1 - $f) * $s);\n            switch ($i % 6) {\n                case 0:\n                    $r = $v;\n                    $g = $t;\n                    $b = $p;\n                    break;\n                case 1:\n                    $r = $q;\n                    $g = $v;\n                    $b = $p;\n                    break;\n                case 2:\n                    $r = $p;\n                    $g = $v;\n                    $b = $t;\n                    break;\n                case 3:\n                    $r = $p;\n                    $g = $q;\n                    $b = $v;\n                    break;\n                case 4:\n                    $r = $t;\n                    $g = $p;\n                    $b = $v;\n                    break;\n                case 5:\n                    $r = $v;\n                    $g = $p;\n                    $b = $q;\n                    break;\n            }\n            $r = round($r * 255);\n            $g = round($g * 255);\n            $b = round($b * 255);\n            return \"\\x1b[38;2;{$r};{$g};{$b}m\";\n        };\n        $total = 100;\n        $colors = [];\n        for ($i = 0; $i < $total; $i++) {\n            $colors[] = $hsv($i / $total, .5, .9);\n        }\n        if ($alias === 'prod' || $alias === 'production') {\n            return \"$colors[99]$alias\\x1b[0m\";\n        }\n        if ($alias === 'beta') {\n            return \"$colors[14]$alias\\x1b[0m\";\n        }\n        $tag = $colors[abs(crc32($alias)) % count($colors)];\n        return \"$tag$alias\\x1b[0m\";\n    }\n\n    $colors = [\n        'fg=cyan;options=bold',\n        'fg=green;options=bold',\n        'fg=yellow;options=bold',\n        'fg=cyan',\n        'fg=blue',\n        'fg=yellow',\n        'fg=magenta',\n        'fg=blue;options=bold',\n        'fg=green',\n        'fg=magenta;options=bold',\n        'fg=red;options=bold',\n    ];\n    $tag = $colors[abs(crc32($alias)) % count($colors)];\n    return \"<$tag>$alias</>\";\n}\n\nfunction escape_shell_argument(string $argument): string\n{\n    return \"'\" . str_replace(\"'\", \"'\\\\''\", $argument) . \"'\";\n}\n\nfunction deployer_root(): string\n{\n    if (getenv('DEPLOYER_ROOT') !== false) {\n        return getenv('DEPLOYER_ROOT');\n    } else {\n        if (defined('DEPLOYER_DEPLOY_FILE')) {\n            return dirname(DEPLOYER_DEPLOY_FILE);\n        } else {\n            return getcwd();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Task/Context.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Task;\n\nuse Deployer\\Configuration;\nuse Deployer\\Exception\\Exception;\nuse Deployer\\Host\\Host;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass Context\n{\n    private Host $host;\n\n    /**\n     * @var Context[]\n     */\n    private static array $contexts = [];\n\n    public function __construct(Host $host)\n    {\n        $this->host = $host;\n    }\n\n    public static function push(Context $context): void\n    {\n        self::$contexts[] = $context;\n    }\n\n    public static function has(): bool\n    {\n        return !empty(self::$contexts);\n    }\n\n    public static function get(): Context\n    {\n        if (empty(self::$contexts)) {\n            throw new Exception(\"Context was requested but was not available.\");\n        }\n        return end(self::$contexts);\n    }\n\n    public static function pop(): ?Context\n    {\n        return array_pop(self::$contexts);\n    }\n\n    /**\n     * Throws a Exception when not called within a task-context and therefore no Context is available.\n     *\n     * This method provides a useful error to the end-user to make him/her aware\n     * to use a function in the required task-context.\n     *\n     * @throws Exception\n     */\n    public static function required(string $callerName): void\n    {\n        if (empty(self::$contexts)) {\n            throw new Exception(\"'$callerName' can only be used within a task.\");\n        }\n    }\n\n    public function getConfig(): Configuration\n    {\n        return $this->host->config();\n    }\n\n    public function getHost(): Host\n    {\n        return $this->host;\n    }\n}\n"
  },
  {
    "path": "src/Task/GroupTask.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Task;\n\nuse function Deployer\\invoke;\n\nclass GroupTask extends Task\n{\n    /**\n     * List of tasks.\n     *\n     * @var string[]\n     */\n    private $group;\n\n    /**\n     * @param string[] $group\n     */\n    public function __construct(string $name, array $group)\n    {\n        parent::__construct($name);\n        $this->group = $group;\n    }\n\n    public function run(Context $context): void\n    {\n        foreach ($this->group as $item) {\n            invoke($item);\n        }\n    }\n\n    /**\n     * List of dependent tasks names\n     *\n     * @return string[]\n     */\n    public function getGroup(): array\n    {\n        return $this->group;\n    }\n\n    public function setGroup(array $group): void\n    {\n        $this->group = $group;\n    }\n}\n"
  },
  {
    "path": "src/Task/ScriptManager.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Task;\n\nuse Deployer\\Exception\\Exception;\n\nuse function Deployer\\Support\\array_flatten;\n\nclass ScriptManager\n{\n    /**\n     * @var TaskCollection\n     */\n    private $tasks;\n    /**\n     * @var bool\n     */\n    private $hooksEnabled = true;\n    /**\n     * @var array\n     */\n    private $visitedTasks = [];\n\n    public function __construct(TaskCollection $tasks)\n    {\n        $this->tasks = $tasks;\n    }\n\n    /**\n     * Return tasks to run.\n     *\n     * @return Task[]\n     */\n    public function getTasks(string $name, ?string $startFrom = null, array &$skipped = []): array\n    {\n        $tasks = [];\n        $this->visitedTasks = [];\n        $allTasks = $this->doGetTasks($name);\n\n        if ($startFrom === null) {\n            $tasks = $allTasks;\n        } else {\n            $skip = true;\n            foreach ($allTasks as $task) {\n                if ($skip) {\n                    if ($task->getName() === $startFrom) {\n                        $skip = false;\n                    } else {\n                        $skipped[] = $task->getName();\n                        continue;\n                    }\n                }\n                $tasks[] = $task;\n            }\n            if (count($tasks) === 0) {\n                throw new Exception('All tasks skipped via --start-from option. Nothing to run.');\n            }\n        }\n\n        $enabledTasks = [];\n        foreach ($tasks as $task) {\n            if ($task->isEnabled()) {\n                $enabledTasks[] = $task;\n            }\n        }\n\n        return $enabledTasks;\n    }\n\n    /**\n     * @return Task[]\n     */\n    public function doGetTasks(string $name): array\n    {\n        if (array_key_exists($name, $this->visitedTasks)) {\n            if ($this->visitedTasks[$name] >= 100) {\n                throw new Exception(\"Looks like a circular dependency with \\\"$name\\\" task.\");\n            }\n            $this->visitedTasks[$name]++;\n        } else {\n            $this->visitedTasks[$name] = 1;\n        }\n\n        $tasks = [];\n        $task = $this->tasks->get($name);\n        if ($this->hooksEnabled) {\n            $tasks = array_merge(array_map([$this, 'doGetTasks'], $task->getBefore()), $tasks);\n        }\n        if ($task instanceof GroupTask) {\n            foreach ($task->getGroup() as $taskName) {\n                $subTasks = $this->doGetTasks($taskName);\n                foreach ($subTasks as $subTask) {\n                    $subTask->addSelector($task->getSelector());\n                    if ($task->isOnce()) {\n                        $subTask->once();\n                    }\n                    $tasks[] = $subTask;\n                }\n            }\n        } else {\n            $tasks[] = $task;\n        }\n        if ($this->hooksEnabled) {\n            $tasks = array_merge($tasks, array_map([$this, 'doGetTasks'], $task->getAfter()));\n        }\n        return array_flatten($tasks);\n    }\n\n    public function getHooksEnabled(): bool\n    {\n        return $this->hooksEnabled;\n    }\n\n    public function setHooksEnabled(bool $hooksEnabled): void\n    {\n        $this->hooksEnabled = $hooksEnabled;\n    }\n}\n"
  },
  {
    "path": "src/Task/Task.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Task;\n\nuse Deployer\\Selector\\Selector;\n\nclass Task\n{\n    /**\n     * @var string\n     */\n    private $name;\n    /**\n     * @var callable|null\n     */\n    private $callback;\n    /**\n     * @var string\n     */\n    private $description;\n    /**\n     * @var string\n     */\n    private $sourceLocation = '';\n    /**\n     * @var array\n     */\n    private $before = [];\n    /**\n     * @var array\n     */\n    private $after = [];\n    /**\n     * @var bool\n     */\n    private $hidden = false;\n    /**\n     * @var bool\n     */\n    private $once = false;\n    /**\n     * @var bool\n     */\n    private $oncePerNode = false;\n    /**\n     * @var int|null\n     */\n    private $limit = null;\n    /**\n     * @var array|null\n     */\n    private $selector = null;\n    /**\n     * @var bool\n     */\n    private $verbose = false;\n    /**\n     * @var bool\n     */\n    private $enabled = true;\n\n    /**\n     * @param callable():void $callback\n     */\n    public function __construct(string $name, ?callable $callback = null)\n    {\n        $this->name = $name;\n        $this->callback = $callback;\n    }\n\n    /**\n     * @param callable():void $callback\n     */\n    public function setCallback(callable $callback): void\n    {\n        $this->callback = $callback;\n    }\n\n    public function run(Context $context): void\n    {\n        Context::push($context);\n\n        try {\n            call_user_func($this->callback); // call task\n        } finally {\n            if ($context->getConfig() !== null) {\n                $context->getConfig()->set('working_path', null);\n            }\n\n            Context::pop();\n        }\n    }\n\n    public function getName(): string\n    {\n        return $this->name;\n    }\n\n    public function __toString(): string\n    {\n        return $this->getName();\n    }\n\n    public function getDescription(): ?string\n    {\n        return $this->description;\n    }\n\n    public function desc(string $description): self\n    {\n        $this->description = $description;\n        return $this;\n    }\n\n    public function getSourceLocation(): string\n    {\n        return $this->sourceLocation;\n    }\n\n    public function setSourceLocation(string $path): void\n    {\n        $this->sourceLocation = $path;\n    }\n\n    public function saveSourceLocation(): void\n    {\n        if (function_exists('debug_backtrace')) {\n            $trace = debug_backtrace();\n            $this->sourceLocation = $trace[1]['file'];\n        }\n    }\n\n    /**\n     * Mark this task to run only once on one of hosts.\n     */\n    public function once(bool $once = true): self\n    {\n        $this->once = $once;\n        return $this;\n    }\n\n    public function isOnce(): bool\n    {\n        return $this->once;\n    }\n\n    /**\n     * Mark task to only run once per node.\n     * Node is a group of hosts with same hostname or with same node label.\n     */\n    public function oncePerNode(bool $once = true): self\n    {\n        $this->oncePerNode = $once;\n        return $this;\n    }\n\n    public function isOncePerNode(): bool\n    {\n        return $this->oncePerNode;\n    }\n\n    /**\n     * Mark task as hidden and not accessible from CLI.\n     */\n    public function hidden(bool $hidden = true): self\n    {\n        $this->hidden = $hidden;\n        return $this;\n    }\n\n    public function isHidden(): bool\n    {\n        return $this->hidden;\n    }\n\n    /**\n     * Make $task being run before this task.\n     */\n    public function addBefore(string $task): self\n    {\n        array_unshift($this->before, $task);\n        return $this;\n    }\n\n    /**\n     * Make $task being run after this task\n     */\n    public function addAfter(string $task): self\n    {\n        array_push($this->after, $task);\n        return $this;\n    }\n\n    public function getBefore(): array\n    {\n        return $this->before;\n    }\n\n    public function getAfter(): array\n    {\n        return $this->after;\n    }\n\n    public function getLimit(): ?int\n    {\n        return $this->limit;\n    }\n\n    public function limit(?int $limit): self\n    {\n        $this->limit = $limit;\n        return $this;\n    }\n\n    public function select(string $selector): self\n    {\n        $this->selector = Selector::parse($selector);\n        return $this;\n    }\n\n    /**\n     * @return array\n     */\n    public function getSelector(): ?array\n    {\n        return $this->selector;\n    }\n\n    public function addSelector(?array $newSelector): void\n    {\n        if ($newSelector !== null) {\n            if ($this->selector === null) {\n                $this->selector = $newSelector;\n            } else {\n                $this->selector = array_merge($this->selector, $newSelector);\n            }\n        }\n    }\n\n    public function isVerbose(): bool\n    {\n        return $this->verbose;\n    }\n\n    public function verbose(bool $verbose = true): self\n    {\n        $this->verbose = $verbose;\n        return $this;\n    }\n\n    public function isEnabled(): bool\n    {\n        return $this->enabled;\n    }\n\n    public function disable(): self\n    {\n        $this->enabled = false;\n        return $this;\n    }\n\n    public function enable(): self\n    {\n        $this->enabled = true;\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Task/TaskCollection.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Task;\n\nuse Deployer\\Collection\\Collection;\n\n/**\n * @method Task get($name)\n * @method Task[] getIterator()\n */\nclass TaskCollection extends Collection\n{\n    protected function notFound(string $name): \\InvalidArgumentException\n    {\n        return new \\InvalidArgumentException(\"Task `$name` not found.\");\n    }\n\n    public function add(Task $task): void\n    {\n        $this->set($task->getName(), $task);\n    }\n}\n"
  },
  {
    "path": "src/Utility/Httpie.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Utility;\n\nuse Deployer\\Exception\\HttpieException;\n\nclass Httpie\n{\n    private string $method = 'GET';\n    private string $url = '';\n    private array $headers = [];\n    private string $body = '';\n    private array $curlopts = [];\n    private bool $nothrow = false;\n\n    public function __construct()\n    {\n        if (!extension_loaded('curl')) {\n            throw new \\Exception(\n                \"Please, install curl extension.\\n\" .\n                \"https://php.net/curl.installation\",\n            );\n        }\n    }\n\n    public static function get(string $url): Httpie\n    {\n        $http = new self();\n        $http->method = 'GET';\n        $http->url = $url;\n        return $http;\n    }\n\n    public static function post(string $url): Httpie\n    {\n        $http = new self();\n        $http->method = 'POST';\n        $http->url = $url;\n        return $http;\n    }\n\n    public static function patch(string $url): Httpie\n    {\n        $http = new self();\n        $http->method = 'PATCH';\n        $http->url = $url;\n        return $http;\n    }\n\n\n    public static function put(string $url): Httpie\n    {\n        $http = new self();\n        $http->method = 'PUT';\n        $http->url = $url;\n        return $http;\n    }\n\n    public static function delete(string $url): Httpie\n    {\n        $http = new self();\n        $http->method = 'DELETE';\n        $http->url = $url;\n        return $http;\n    }\n\n    public function query(array $params): self\n    {\n        $this->url .= '?' . http_build_query($params);\n        return $this;\n    }\n\n    public function header(string $header, string $value): self\n    {\n        $this->headers[$header] = $value;\n        return $this;\n    }\n\n    public function body(string $body): self\n    {\n        $this->body = $body;\n        $this->headers = array_merge($this->headers, [\n            'Content-Length' => strlen($this->body),\n        ]);\n        return $this;\n    }\n\n    public function jsonBody(array $data): self\n    {\n        $this->body = json_encode($data, JSON_PRETTY_PRINT);\n        $this->headers = array_merge($this->headers, [\n            'Content-Type' => 'application/json',\n            'Content-Length' => strlen($this->body),\n        ]);\n        return $this;\n    }\n\n    public function formBody(array $data): self\n    {\n        $this->body = http_build_query($data);\n        $this->headers = array_merge($this->headers, [\n            'Content-type' => 'application/x-www-form-urlencoded',\n            'Content-Length' => strlen($this->body),\n        ]);\n        return $this;\n    }\n\n    /**\n     * @param mixed $value\n     */\n    public function setopt(int $key, $value): self\n    {\n        $this->curlopts[$key] = $value;\n        return $this;\n    }\n\n    public function nothrow(bool $on = true): self\n    {\n        $this->nothrow = $on;\n        return $this;\n    }\n\n    public function send(?array &$info = null): string\n    {\n        if ($this->url === '') {\n            throw new \\RuntimeException('URL must not be empty to Httpie::send()');\n        }\n        $ch = curl_init($this->url);\n        curl_setopt($ch, CURLOPT_USERAGENT, 'Deployer ' . DEPLOYER_VERSION);\n        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method);\n        $headers = [];\n        foreach ($this->headers as $key => $value) {\n            $headers[] = \"$key: $value\";\n        }\n        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);\n        curl_setopt($ch, CURLOPT_POSTFIELDS, $this->body);\n        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);\n        curl_setopt($ch, CURLOPT_MAXREDIRS, 10);\n        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);\n        curl_setopt($ch, CURLOPT_TIMEOUT, 5);\n        foreach ($this->curlopts as $key => $value) {\n            curl_setopt($ch, $key, $value);\n        }\n        $result = curl_exec($ch);\n        $info = curl_getinfo($ch);\n        if ($result === false) {\n            if ($this->nothrow) {\n                $result = '';\n            } else {\n                $error = curl_error($ch);\n                $errno = curl_errno($ch);\n                if (PHP_MAJOR_VERSION < 8) {\n                    curl_close($ch);\n                }\n                throw new HttpieException($error, $errno);\n            }\n        }\n        if (PHP_MAJOR_VERSION < 8) {\n            curl_close($ch);\n        }\n        return $result;\n    }\n\n    public function getJson(): mixed\n    {\n        $result = $this->send();\n        $response = json_decode($result, true);\n        if (json_last_error() !== JSON_ERROR_NONE) {\n            throw new HttpieException(\n                'JSON Error: ' . json_last_error_msg() . '\\n' .\n                'Response: ' . $result,\n            );\n        }\n        return $response;\n    }\n}\n"
  },
  {
    "path": "src/Utility/Rsync.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Utility;\n\nuse Deployer\\ProcessRunner\\Printer;\nuse Deployer\\Exception\\RunException;\nuse Deployer\\Host\\Host;\nuse Symfony\\Component\\Console\\Helper\\ProgressBar;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Process\\Exception\\ProcessFailedException;\nuse Symfony\\Component\\Process\\Process;\n\nuse function Deployer\\writeln;\n\nclass Rsync\n{\n    /**\n     * @var Printer\n     */\n    private $pop;\n    /**\n     * @var OutputInterface\n     */\n    private $output;\n\n    public function __construct(Printer $pop, OutputInterface $output)\n    {\n        $this->pop = $pop;\n        $this->output = $output;\n    }\n\n    /**\n     * Start rsync process.\n     *\n     * @param string|string[] $source\n     * @phpstan-param array{flags?: string, options?: array, timeout?: int|null, progress_bar?: bool, display_stats?: bool} $config\n     * @throws RunException\n     */\n    public function call(Host $host, $source, string $destination, array $config = []): void\n    {\n        $defaults = [\n            'timeout' => null,\n            'options' => [],\n            'flags' => '-azP',\n            'progress_bar' => true,\n            'display_stats' => false,\n        ];\n        $config = array_merge($defaults, $config);\n\n        $options = $config['options'];\n        $flags = $config['flags'];\n        $displayStats = $config['display_stats'] || in_array('--stats', $options, true);\n\n        if ($displayStats && !in_array('--stats', $options, true)) {\n            $options[] = '--stats';\n        }\n\n        $connectionOptions = $host->connectionOptionsString();\n        if ($connectionOptions !== '') {\n            $options = array_merge($options, ['-e', \"ssh $connectionOptions\"]);\n        }\n        if ($host->has('become') && !empty($host->get('become'))) {\n            $options = array_merge($options, ['--rsync-path', \"sudo -H -u {$host->get('become')} rsync\"]);\n        }\n        if (!is_array($source)) {\n            $source = [$source];\n        }\n        $command = array_values(array_filter(\n            array_merge(['rsync', $flags], $options, $source, [$destination]),\n            function (string $value) {\n                return $value !== '';\n            },\n        ));\n\n        $commandString = $command[0];\n        for ($i = 1; $i < count($command); $i++) {\n            $commandString .= ' ' . escapeshellarg($command[$i]);\n        }\n        if ($this->output->isVerbose()) {\n            $this->output->writeln(\"[$host] $commandString\");\n        }\n\n        $progressBar = null;\n        if ($this->output->getVerbosity() === OutputInterface::VERBOSITY_NORMAL && $config['progress_bar']) {\n            $progressBar = new ProgressBar($this->output);\n            $progressBar->setBarCharacter('<info>≡</info>');\n            $progressBar->setProgressCharacter('>');\n            $progressBar->setEmptyBarCharacter('-');\n        }\n\n        $fullOutput = '';\n\n        $callback = function ($type, $buffer) use ($host, $progressBar, &$fullOutput) {\n            $fullOutput .= $buffer;\n            if ($progressBar) {\n                foreach (explode(\"\\n\", $buffer) as $line) {\n                    if (preg_match('/(to-chk|to-check)=(\\d+?)\\/(\\d+)/', $line, $match)) {\n                        $max = intval($match[3]);\n                        $step = $max - intval($match[2]);\n                        $progressBar->setMaxSteps($max);\n                        $progressBar->setFormat(\"[$host] %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%\");\n                        $progressBar->setProgress($step);\n                    }\n                }\n                return;\n            }\n            if ($this->output->isVerbose()) {\n                $this->pop->printBuffer($type, $host, $buffer);\n            }\n        };\n\n        $process = new Process($command);\n        $process->setTimeout($config['timeout']);\n        try {\n            $process->mustRun($callback);\n\n            if ($displayStats) {\n                $stats = [];\n\n                $statsStarted = false;\n                foreach (explode(\"\\n\", $fullOutput) as $line) {\n                    if (strpos($line, 'Number of files') === 0) {\n                        $statsStarted = true;\n                    }\n\n                    if ($statsStarted) {\n                        if (empty($line)) {\n                            break;\n                        }\n                        $stats[] = $line;\n                    }\n                }\n\n                writeln(\"Rsync operation stats\\n\" . '<comment>' . implode(\"\\n\", $stats) . '</comment>');\n            }\n\n        } catch (ProcessFailedException $exception) {\n            throw new RunException(\n                $host,\n                $commandString,\n                $process->getExitCode(),\n                $process->getOutput(),\n                $process->getErrorOutput(),\n            );\n        } finally {\n            if ($progressBar) {\n                $progressBar->clear();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/functions.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nuse Deployer\\Exception\\Exception;\nuse Deployer\\Exception\\GracefulShutdownException;\nuse Deployer\\Exception\\RunException;\nuse Deployer\\Exception\\TimeoutException;\nuse Deployer\\Exception\\WillAskUser;\nuse Deployer\\Host\\Host;\nuse Deployer\\Host\\Localhost;\nuse Deployer\\Host\\Range;\nuse Deployer\\Importer\\Importer;\nuse Deployer\\Ssh\\RunParams;\nuse Deployer\\Support\\ObjectProxy;\nuse Deployer\\Task\\Context;\nuse Deployer\\Task\\GroupTask;\nuse Deployer\\Task\\Task;\nuse Deployer\\Utility\\Httpie;\nuse Symfony\\Component\\Console\\Helper\\QuestionHelper;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Console\\Question\\ChoiceQuestion;\nuse Symfony\\Component\\Console\\Question\\ConfirmationQuestion;\nuse Symfony\\Component\\Console\\Question\\Question;\n\nuse function Deployer\\Support\\array_merge_alternate;\nuse function Deployer\\Support\\is_closure;\n\n/**\n * Defines a host or hosts.\n * ```php\n * host('example.org');\n * host('prod.example.org', 'staging.example.org');\n * ```\n *\n * Inside task can be used to get `Host` instance of an alias.\n * ```php\n * task('test', function () {\n *     $port = host('example.org')->get('port');\n * });\n * ```\n */\nfunction host(string ...$hostname): Host|ObjectProxy\n{\n    $deployer = Deployer::get();\n    if (count($hostname) === 1 && $deployer->hosts->has($hostname[0])) {\n        return $deployer->hosts->get($hostname[0]);\n    }\n    $aliases = Range::expand($hostname);\n\n    foreach ($aliases as $alias) {\n        if ($deployer->hosts->has($alias)) {\n            $host = $deployer->hosts->get($alias);\n            throw new \\InvalidArgumentException(\"Host \\\"$host\\\" already exists.\");\n        }\n    }\n\n    if (count($aliases) === 1) {\n        $host = new Host($aliases[0]);\n        $deployer->hosts->set($aliases[0], $host);\n        return $host;\n    } else {\n        $hosts = array_map(function ($hostname) use ($deployer): Host {\n            $host = new Host($hostname);\n            $deployer->hosts->set($hostname, $host);\n            return $host;\n        }, $aliases);\n        return new ObjectProxy($hosts);\n    }\n}\n\n/**\n * Define a local host.\n * Deployer will not connect to this host, but will execute commands locally instead.\n *\n * ```php\n * localhost('ci'); // Alias and hostname will be \"ci\".\n * ```\n */\nfunction localhost(string ...$hostnames): Localhost|ObjectProxy\n{\n    $deployer = Deployer::get();\n    $hostnames = Range::expand($hostnames);\n\n    if (count($hostnames) <= 1) {\n        $host = count($hostnames) === 1 ? new Localhost($hostnames[0]) : new Localhost();\n        $deployer->hosts->set($host->getAlias(), $host);\n        return $host;\n    } else {\n        $hosts = array_map(function ($hostname) use ($deployer): Localhost {\n            $host = new Localhost($hostname);\n            $deployer->hosts->set($host->getAlias(), $host);\n            return $host;\n        }, $hostnames);\n        return new ObjectProxy($hosts);\n    }\n}\n\n/**\n * Returns current host.\n */\nfunction currentHost(): Host\n{\n    return Context::get()->getHost();\n}\n\n/**\n * Returns hosts based on provided selector.\n *\n * ```php\n * on(select('stage=prod, role=db'), function (Host $host) {\n *     ...\n * });\n * ```\n *\n * @return Host[]\n */\nfunction select(string $selector): array\n{\n    return Deployer::get()->selector->select($selector);\n}\n\n/**\n * Returns array of hosts selected by user via CLI.\n *\n * @return Host[]\n */\nfunction selectedHosts(): array\n{\n    $hosts = [];\n    foreach (get('selected_hosts', []) as $alias) {\n        $hosts[] = Deployer::get()->hosts->get($alias);\n    }\n    return $hosts;\n}\n\n/**\n * Import other php or yaml recipes.\n *\n * ```php\n * import('recipe/common.php');\n * ```\n *\n * ```php\n * import(__DIR__ . '/config/hosts.yaml');\n * ```\n */\nfunction import(string $file): void\n{\n    Importer::import($file);\n}\n\n/**\n * Set task description.\n */\nfunction desc(?string $title = null): ?string\n{\n    static $store = null;\n\n    if ($title === null) {\n        return $store;\n    } else {\n        return $store = $title;\n    }\n}\n\n/**\n * Define a new task and save to tasks list.\n *\n * Alternatively get a defined task.\n *\n * @param string $name Name of current task.\n * @param callable|array|null $body Callable task, array of other tasks names or nothing to get a defined tasks\n * @return Task\n */\nfunction task(string $name, callable|array|null $body = null): Task\n{\n    $deployer = Deployer::get();\n\n    if (empty($body)) {\n        return $deployer->tasks->get($name);\n    }\n\n    if (is_callable($body)) {\n        $task = new Task($name, $body);\n    } elseif (is_array($body)) {\n        $task = new GroupTask($name, $body);\n    } else {\n        throw new \\InvalidArgumentException('Task body should be a function or an array.');\n    }\n\n    if ($deployer->tasks->has($name)) {\n        // If task already exists, try to replace.\n        $existingTask = $deployer->tasks->get($name);\n        if (get_class($existingTask) !== get_class($task)) {\n            // There is no \"up\" or \"down\"casting in PHP.\n            throw new \\Exception('Tried to replace Task \\'' . $name . '\\' with a GroupTask or vice-versa. This is not supported. If you are sure you want to do that, remove the old task `Deployer::get()->tasks->remove(<taskname>)` and then re-add the task.');\n        }\n        if ($existingTask instanceof GroupTask) {\n            $existingTask->setGroup($body);\n        } elseif ($existingTask instanceof Task) {\n            $existingTask->setCallback($body);\n        }\n        $task = $existingTask;\n    } else {\n        // If task does not exist, add it to the Collection.\n        $deployer->tasks->set($name, $task);\n    }\n\n    $task->saveSourceLocation();\n\n    if (!empty(desc())) {\n        $task->desc(desc());\n        desc(''); // Clear title.\n    }\n\n    return $task;\n}\n\n/**\n * Call that task before specified task runs.\n *\n * @param string $task The task before $that should be run.\n * @param string|callable $do The task to be run.\n *\n * @return ?Task\n */\nfunction before(string $task, string|callable $do): ?Task\n{\n    if (is_closure($do)) {\n        $newTask = task(\"before:$task\", $do);\n        before($task, \"before:$task\");\n        return $newTask;\n    }\n    task($task)->addBefore($do);\n\n    return null;\n}\n\n/**\n * Call that task after specified task runs.\n *\n * @param string $task The task after $that should be run.\n * @param string|callable $do The task to be run.\n *\n * @return ?Task\n */\nfunction after(string $task, string|callable $do): ?Task\n{\n    if (is_closure($do)) {\n        $newTask = task(\"after:$task\", $do);\n        after($task, \"after:$task\");\n        return $newTask;\n    }\n    task($task)->addAfter($do);\n\n    return null;\n}\n\n/**\n * Setup which task run on failure of $task.\n * When called multiple times for a task, previous fail() definitions will be overridden.\n *\n * @param string $task The task which need to fail so $that should be run.\n * @param string|callable $do The task to be run.\n *\n * @return ?Task\n */\nfunction fail(string $task, string|callable $do): ?Task\n{\n    if (is_callable($do)) {\n        $newTask = task(\"fail:$task\", $do);\n        fail($task, \"fail:$task\");\n        return $newTask;\n    }\n    $deployer = Deployer::get();\n    $deployer->fail->set($task, $do);\n\n    return null;\n}\n\n/**\n * Add users options.\n *\n * @param string $name The option name\n * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts\n * @param int|null $mode The option mode: One of the VALUE_* constants\n * @param string $description A description text\n * @param string|string[]|int|bool|null $default The default value (must be null for self::VALUE_NONE)\n */\nfunction option(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null): void\n{\n    Deployer::get()->inputDefinition->addOption(\n        new InputOption($name, $shortcut, $mode, $description, $default),\n    );\n}\n\n/**\n * Change the current working directory.\n *\n * ```php\n * cd('~/myapp');\n * run('ls'); // Will run `ls` in ~/myapp.\n * ```\n */\nfunction cd(string $path): void\n{\n    set('working_path', parse($path));\n}\n\n/**\n * Change the current user.\n *\n * Usage:\n * ```php\n * $restore = become('deployer');\n *\n * // do something\n *\n * $restore(); // revert back to the previous user\n * ```\n *\n * @param string $user\n * @return \\Closure\n */\nfunction become(string $user): \\Closure\n{\n    $currentBecome = get('become');\n    set('become', $user);\n    return function () use ($currentBecome) {\n        set('become', $currentBecome);\n    };\n}\n\n/**\n * Execute a callback within a specific directory and revert back to the initial working directory.\n *\n * @return mixed Return value of the $callback function or null if callback doesn't return anything\n * @throws Exception\n */\nfunction within(string $path, callable $callback): mixed\n{\n    $lastWorkingPath = get('working_path', '');\n    try {\n        set('working_path', parse($path));\n        return $callback();\n    } finally {\n        set('working_path', $lastWorkingPath);\n    }\n}\n\n/**\n * Executes given command on remote host.\n *\n * Examples:\n *\n * ```php\n * run('echo hello world');\n * run('cd {{deploy_path}} && git status');\n * run('password %secret%', secret: getenv('CI_SECRET'));\n * run('curl medv.io', timeout: 5);\n * ```\n *\n * ```php\n * $path = run('readlink {{deploy_path}}/current');\n * run(\"echo $path\");\n * ```\n *\n * @param string $command Command to run on remote host.\n * @param string|null $cwd Sets the process working directory. If not set {{working_path}} will be used.\n * @param int|null $timeout Sets the process timeout (max. runtime). The timeout in seconds (default: 300 sec; see {{default_timeout}}, `null` to disable).\n * @param int|null $idleTimeout Sets the process idle timeout (max. time since last output) in seconds.\n * @param string|null $secret Placeholder `%secret%` can be used in command. Placeholder will be replaced with this value and will not appear in any logs.\n * @param array|null $env Array of environment variables: `run('echo $KEY', env: ['key' => 'value']);`\n * @param bool|null $forceOutput Print command output in real-time.\n * @param bool|null $nothrow Don't throw an exception of non-zero exit code.\n * @return string\n * @throws RunException\n * @throws TimeoutException\n * @throws WillAskUser\n */\nfunction run(\n    string  $command,\n    ?string $cwd = null,\n    ?array  $env = null,\n    #[\\SensitiveParameter]\n    ?string $secret = null,\n    ?bool   $nothrow = false,\n    ?bool   $forceOutput = false,\n    ?int    $timeout = null,\n    ?int    $idleTimeout = null,\n): string {\n    $runParams = new RunParams(\n        shell: currentHost()->getShell(),\n        cwd: $cwd ?? has('working_path') ? get('working_path') : null,\n        env: array_merge_alternate(get('env', []), $env ?? []),\n        nothrow: $nothrow,\n        timeout: $timeout ?? get('default_timeout', 300),\n        idleTimeout: $idleTimeout,\n        forceOutput: $forceOutput,\n        secrets: empty($secret) ? null : ['secret' => $secret],\n    );\n\n    $dotenv = get('dotenv', false);\n    if (!empty($dotenv)) {\n        $runParams->dotenv = $dotenv;\n    }\n\n    $run = function (string $command, ?RunParams $params = null) use ($runParams): string {\n        $params = $params ?? $runParams;\n        $host = currentHost();\n        $command = parse($command);\n        if ($host instanceof Localhost) {\n            $process = Deployer::get()->processRunner;\n            $output = $process->run($host, $command, $params);\n        } else {\n            $client = Deployer::get()->sshClient;\n            $output = $client->run($host, $command, $params);\n        }\n        return rtrim($output);\n    };\n\n    if (preg_match('/^sudo\\b/', $command)) {\n        try {\n            return $run($command);\n        } catch (RunException) {\n            $askpass = get('sudo_askpass', '/tmp/dep_sudo_pass');\n            $password = get('sudo_pass', false);\n            if ($password === false) {\n                writeln(\"<fg=green;options=bold>run</> $command\");\n                $password = askHiddenResponse(\" [sudo] password for {{remote_user}}: \");\n            }\n            $run(\"echo -e '#!/bin/sh\\necho \\\"\\$PASSWORD\\\"' > $askpass\");\n            $run(\"chmod a+x $askpass\");\n            $command = preg_replace('/^sudo\\b/', 'sudo -A', $command);\n            $output = $run(\" SUDO_ASKPASS=$askpass PASSWORD=%sudo_pass% $command\", $runParams->with(\n                secrets: ['sudo_pass' => escapeshellarg($password)],\n            ));\n            $run(\"rm $askpass\");\n            return $output;\n        }\n    } else {\n        return $run($command);\n    }\n}\n\n\n/**\n * Execute commands on a local machine.\n *\n * Examples:\n *\n * ```php\n * $user = runLocally('git config user.name');\n * runLocally(\"echo $user\");\n * ```\n *\n * @param string $command Command to run on localhost.\n * @param string|null $cwd Sets the process working directory. If not set {{working_path}} will be used.\n * @param int|null $timeout Sets the process timeout (max. runtime). The timeout in seconds (default: 300 sec, `null` to disable).\n * @param int|null $idleTimeout Sets the process idle timeout (max. time since last output) in seconds.\n * @param string|null $secret Placeholder `%secret%` can be used in command. Placeholder will be replaced with this value and will not appear in any logs.\n * @param array|null $env Array of environment variables: `runLocally('echo $KEY', env: ['key' => 'value']);`\n * @param bool|null $forceOutput Print command output in real-time.\n * @param bool|null $nothrow Don't throw an exception of non-zero exit code.\n * @param string|null $shell Shell to run in. Default is `bash -s`.\n *\n * @return string\n * @throws RunException\n * @throws TimeoutException\n */\nfunction runLocally(\n    string  $command,\n    ?string $cwd = null,\n    ?int    $timeout = null,\n    ?int    $idleTimeout = null,\n    #[\\SensitiveParameter]\n    ?string $secret = null,\n    ?array  $env = null,\n    ?bool   $forceOutput = false,\n    ?bool   $nothrow = false,\n    ?string $shell = null,\n): string {\n    $runParams = new RunParams(\n        shell: $shell ?? 'bash -s',\n        cwd: $cwd,\n        env: $env,\n        nothrow: $nothrow,\n        timeout: $timeout,\n        idleTimeout: $idleTimeout,\n        forceOutput: $forceOutput,\n        secrets: empty($secret) ? null : ['secret' => $secret],\n    );\n\n    $process = Deployer::get()->processRunner;\n    $command = parse($command);\n\n    $output = $process->run(new Localhost(), $command, $runParams);\n    return rtrim($output);\n}\n\n/**\n * Run test command.\n * Example:\n *\n * ```php\n * if (test('[ -d {{release_path}} ]')) {\n * ...\n * }\n * ```\n *\n */\nfunction test(string $command): bool\n{\n    $true = '+' . array_rand(array_flip(['accurate', 'appropriate', 'correct', 'legitimate', 'precise', 'right', 'true', 'yes', 'indeed']));\n    return trim(run(\"if $command; then echo $true; fi\")) === $true;\n}\n\n/**\n * Run test command locally.\n * Example:\n *\n *     testLocally('[ -d {{local_release_path}} ]')\n *\n */\nfunction testLocally(string $command): bool\n{\n    return runLocally(\"if $command; then echo +true; fi\") === '+true';\n}\n\n/**\n * Iterate other hosts, allowing to call run a func in callback.\n *\n * ```php\n * on(select('stage=prod, role=db'), function ($host) {\n *     ...\n * });\n * ```\n *\n * ```php\n * on(host('example.org'), function ($host) {\n *     ...\n * });\n * ```\n *\n * ```php\n * on(Deployer::get()->hosts, function ($host) {\n *     ...\n * });\n * ```\n *\n * @param Host|Host[] $hosts\n */\nfunction on($hosts, callable $callback): void\n{\n    if (!is_array($hosts) && !($hosts instanceof \\Traversable)) {\n        $hosts = [$hosts];\n    }\n\n    foreach ($hosts as $host) {\n        if ($host instanceof Host) {\n            $host->config()->load();\n            Context::push(new Context($host));\n            try {\n                $callback($host);\n                $host->config()->save();\n            } catch (GracefulShutdownException $e) {\n                Deployer::get()->messenger->renderException($e, $host);\n            } finally {\n                Context::pop();\n            }\n        } else {\n            throw new \\InvalidArgumentException(\"Function on can iterate only on Host instances.\");\n        }\n    }\n}\n\n/**\n * Runs a task.\n * ```php\n * invoke('deploy:symlink');\n * ```\n *\n * @throws Exception\n */\nfunction invoke(string $taskName): void\n{\n    $task = Deployer::get()->tasks->get($taskName);\n    Deployer::get()->messenger->startTask($task);\n    $task->run(Context::get());\n    Deployer::get()->messenger->endTask($task);\n}\n\n/**\n * Upload files or directories to host.\n *\n * > To upload the _contents_ of a directory, include a trailing slash (eg `upload('build/', '{{release_path}}/public');`).\n * > Without the trailing slash, the build directory itself will be uploaded (resulting in `{{release_path}}/public/build`).\n *\n *  The `$config` array supports the following keys:\n *\n * - `flags` for overriding the default `-azP` passed to the `rsync` command\n * - `options` with additional flags passed directly to the `rsync` command\n * - `timeout` for `Process::fromShellCommandline()` (`null` by default)\n * - `progress_bar` to display upload/download progress\n * - `display_stats` to display rsync set of statistics\n *\n * Note: due to the way php escapes command line arguments, list-notation for the rsync `--exclude={'file','anotherfile'}` option will not work.\n * A workaround is to add a separate `--exclude=file` argument for each exclude to `options` (also, _do not_ wrap the filename/filter in quotes).\n * An alternative might be to write the excludes to a temporary file (one per line) and use `--exclude-from=temporary_file` argument instead.\n *\n * @param string|string[] $source\n * @param array $config\n * @phpstan-param array{flags?: string, options?: array, timeout?: int|null, progress_bar?: bool, display_stats?: bool} $config\n *\n * @throws RunException\n */\nfunction upload($source, string $destination, array $config = []): void\n{\n    $rsync = Deployer::get()->rsync;\n    $host = currentHost();\n    $source = is_array($source) ? array_map('Deployer\\parse', $source) : parse($source);\n    $destination = parse($destination);\n\n    if ($host instanceof Localhost) {\n        $rsync->call($host, $source, $destination, $config);\n    } else {\n        $rsync->call($host, $source, \"{$host->connectionString()}:$destination\", $config);\n    }\n}\n\n/**\n * Download file or directory from host\n *\n * @param array $config\n *\n * @throws RunException\n */\nfunction download(string $source, string $destination, array $config = []): void\n{\n    $rsync = Deployer::get()->rsync;\n    $host = currentHost();\n    $source = parse($source);\n    $destination = parse($destination);\n\n    if ($host instanceof Localhost) {\n        $rsync->call($host, $source, $destination, $config);\n    } else {\n        $rsync->call($host, \"{$host->connectionString()}:$source\", $destination, $config);\n    }\n}\n\n/**\n * Writes an info message.\n */\nfunction info(string $message): void\n{\n    writeln(\"<fg=green;options=bold>info</> \" . parse($message));\n}\n\n/**\n * Writes an warning message.\n */\nfunction warning(string $message): void\n{\n    $message = \"<fg=yellow;options=bold>warning</> <comment>$message</comment>\";\n\n    if (Context::has()) {\n        writeln($message);\n    } else {\n        Deployer::get()->output->writeln($message);\n    }\n}\n\n/**\n * Writes a message to the output and adds a newline at the end.\n */\nfunction writeln(string $message, int $options = 0): void\n{\n    $host = currentHost();\n    output()->writeln(\"[$host] \" . parse($message), $options);\n}\n\n/**\n * Parse set values.\n */\nfunction parse(string $value): string\n{\n    return Context::get()->getConfig()->parse($value);\n}\n\n/**\n * Setup configuration option.\n * @param mixed $value\n * @throws Exception\n */\nfunction set(string $name, $value): void\n{\n    if (!Context::has()) {\n        Deployer::get()->config->set($name, $value);\n    } else {\n        Context::get()->getConfig()->set($name, $value);\n    }\n}\n\n/**\n * Merge new config params to existing config array.\n *\n * @param array $array\n */\nfunction add(string $name, array $array): void\n{\n    if (!Context::has()) {\n        Deployer::get()->config->add($name, $array);\n    } else {\n        Context::get()->getConfig()->add($name, $array);\n    }\n}\n\n/**\n * Get configuration value.\n *\n * @param mixed|null $default\n *\n * @return mixed\n */\nfunction get(string $name, $default = null)\n{\n    if (!Context::has()) {\n        return Deployer::get()->config->get($name, $default);\n    } else {\n        return Context::get()->getConfig()->get($name, $default);\n    }\n}\n\n/**\n * Check if there is such configuration option.\n */\nfunction has(string $name): bool\n{\n    if (!Context::has()) {\n        return Deployer::get()->config->has($name);\n    } else {\n        return Context::get()->getConfig()->has($name);\n    }\n}\n\nfunction ask(string $message, ?string $default = null, ?array $autocomplete = null): ?string\n{\n    if (defined('DEPLOYER_NO_ASK')) {\n        throw new WillAskUser($message);\n    }\n    Context::required(__FUNCTION__);\n\n    if (output()->isQuiet()) {\n        return $default;\n    }\n\n    if (Deployer::isWorker()) {\n        return Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args());\n    }\n\n    /** @var QuestionHelper */\n    $helper = Deployer::get()->getHelper('question');\n\n    $tag = currentHost()->getTag();\n    $message = parse($message);\n    $message = \"[$tag] <question>$message</question> \" . (($default === null) ? \"\" : \"(default: $default) \");\n\n    $question = new Question($message, $default);\n    if (!empty($autocomplete)) {\n        $question->setAutocompleterValues($autocomplete);\n    }\n\n    return $helper->ask(input(), output(), $question);\n}\n\n/**\n * @param mixed $default\n * @return mixed\n * @throws Exception\n */\nfunction askChoice(string $message, array $availableChoices, $default = null, bool $multiselect = false)\n{\n    if (defined('DEPLOYER_NO_ASK')) {\n        throw new WillAskUser($message);\n    }\n    Context::required(__FUNCTION__);\n\n    if (empty($availableChoices)) {\n        throw new \\InvalidArgumentException('Available choices should not be empty');\n    }\n\n    if ($default !== null && !array_key_exists($default, $availableChoices)) {\n        throw new \\InvalidArgumentException('Default choice is not available');\n    }\n\n    if (output()->isQuiet()) {\n        if ($default === null) {\n            $default = key($availableChoices);\n        }\n        return [$default => $availableChoices[$default]];\n    }\n\n    if (Deployer::isWorker()) {\n        return Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args());\n    }\n\n    /** @var QuestionHelper */\n    $helper = Deployer::get()->getHelper('question');\n\n    $tag = currentHost()->getTag();\n    $message = parse($message);\n    $message = \"[$tag] <question>$message</question> \" . (($default === null) ? \"\" : \"(default: $default) \");\n\n    $question = new ChoiceQuestion($message, $availableChoices, $default);\n    $question->setMultiselect($multiselect);\n\n    return $helper->ask(input(), output(), $question);\n}\n\nfunction askConfirmation(string $message, bool $default = false): bool\n{\n    if (defined('DEPLOYER_NO_ASK')) {\n        throw new WillAskUser($message);\n    }\n    Context::required(__FUNCTION__);\n\n    if (output()->isQuiet()) {\n        return $default;\n    }\n\n    if (Deployer::isWorker()) {\n        return Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args());\n    }\n\n    /** @var QuestionHelper */\n    $helper = Deployer::get()->getHelper('question');\n\n    $yesOrNo = $default ? 'Y/n' : 'y/N';\n    $tag = currentHost()->getTag();\n    $message = parse($message);\n    $message = \"[$tag] <question>$message</question> [$yesOrNo] \";\n\n    $question = new ConfirmationQuestion($message, $default);\n\n    return $helper->ask(input(), output(), $question);\n}\n\nfunction askHiddenResponse(string $message): string\n{\n    if (defined('DEPLOYER_NO_ASK')) {\n        throw new WillAskUser($message);\n    }\n    Context::required(__FUNCTION__);\n\n    if (output()->isQuiet()) {\n        return '';\n    }\n\n    if (Deployer::isWorker()) {\n        return (string) Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args());\n    }\n\n    /** @var QuestionHelper */\n    $helper = Deployer::get()->getHelper('question');\n\n    $tag = currentHost()->getTag();\n    $message = parse($message);\n    $message = \"[$tag] <question>$message</question> \";\n\n    $question = new Question($message);\n    $question->setHidden(true);\n    $question->setHiddenFallback(false);\n\n    return (string) $helper->ask(input(), output(), $question);\n}\n\nfunction input(): InputInterface\n{\n    return Deployer::get()->input;\n}\n\nfunction output(): OutputInterface\n{\n    return Deployer::get()->output;\n}\n\n/**\n * Check if command exists\n *\n * @throws RunException\n */\nfunction commandExist(string $command): bool\n{\n    return test(\"hash $command 2>/dev/null\");\n}\n\n/**\n * @throws RunException\n */\nfunction commandSupportsOption(string $command, string $option): bool\n{\n    $man = run(\"(man $command 2>&1 || $command -h 2>&1 || $command --help 2>&1) | grep -- $option || true\");\n    if (empty($man)) {\n        return false;\n    }\n    return str_contains($man, $option);\n}\n\n/**\n * @throws RunException\n */\nfunction which(string $name): string\n{\n    $nameEscaped = escapeshellarg($name);\n\n    // Try `command`, should cover all Bourne-like shells\n    // Try `which`, should cover most other cases\n    // Fallback to `type` command, if the rest fails\n    $path = run(\"command -v $nameEscaped || which $nameEscaped || type -p $nameEscaped\");\n    if (empty($path)) {\n        throw new \\RuntimeException(\"Can't locate [$nameEscaped] - neither of [command|which|type] commands are available\");\n    }\n\n    // Deal with issue when `type -p` outputs something like `type -ap` in some implementations\n    return trim(str_replace(\"$name is\", \"\", $path));\n\n}\n\n/**\n * Returns remote environments variables as an array.\n * ```php\n * $remotePath = remoteEnv()['PATH'];\n * run('echo $PATH', env: ['PATH' => \"/home/user/bin:$remotePath\"]);\n * ```\n */\nfunction remoteEnv(): array\n{\n    $vars = [];\n    $data = run('env');\n    foreach (explode(\"\\n\", $data) as $line) {\n        [$name, $value] = explode('=', $line, 2);\n        $vars[$name] = $value;\n    }\n    return $vars;\n}\n\n/**\n * Creates a new exception.\n */\nfunction error(string $message): Exception\n{\n    return new Exception(parse($message));\n}\n\n/**\n * Returns current timestamp in UTC timezone in ISO8601 format.\n */\nfunction timestamp(): string\n{\n    return (new \\DateTime('now', new \\DateTimeZone('UTC')))->format(\\DateTime::ISO8601);\n}\n\n/**\n * Example usage:\n * ```php\n * $result = fetch('{{domain}}', info: $info);\n * var_dump($info['http_code'], $result);\n * ```\n */\nfunction fetch(string $url, string $method = 'get', array $headers = [], ?string $body = null, ?array &$info = null, bool $nothrow = false): string\n{\n    $url = parse($url);\n    if (strtolower($method) === 'get') {\n        $http = Httpie::get($url);\n    } elseif (strtolower($method) === 'post') {\n        $http = Httpie::post($url);\n    } else {\n        throw new \\InvalidArgumentException(\"Unknown method \\\"$method\\\".\");\n    }\n    $http = $http->nothrow($nothrow);\n    foreach ($headers as $key => $value) {\n        $http = $http->header($key, $value);\n    }\n    if ($body !== null) {\n        $http = $http->body($body);\n    }\n    return $http->send($info);\n}\n"
  },
  {
    "path": "src/schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"http://deployer.org/schema.json#\",\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"version\": {\n      \"type\": \"string\"\n    },\n    \"import\": {\n      \"oneOf\": [\n        {\n          \"type\": \"string\"\n        },\n        {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      ]\n    },\n    \"config\": {\n      \"type\": \"object\"\n    },\n    \"hosts\": {\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \"^\": {\n          \"oneOf\": [\n            {\n              \"type\": \"object\",\n              \"properties\": {\n                \"local\": {\n                  \"type\": \"boolean\"\n                }\n              }\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"tasks\": {\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \"^\": {\n          \"oneOf\": [\n            {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"cd\": {\n                    \"type\": \"string\"\n                  },\n                  \"run\": {\n                    \"type\": \"string\"\n                  },\n                  \"run_locally\": {\n                    \"type\": \"string\"\n                  },\n                  \"upload\": {\n                    \"type\": \"object\",\n                    \"required\": [\n                      \"src\",\n                      \"dest\"\n                    ],\n                    \"properties\": {\n                      \"src\": {\n                        \"oneOf\": [\n                          {\n                          \"type\": \"string\"\n                          },\n                          {\n                            \"type\": \"array\",\n                            \"items\": {\n                              \"type\": \"string\"\n                            }\n                          }\n                        ]\n                      },\n                      \"dest\": {\n                        \"type\": \"string\"\n                      }\n                    }\n                  },\n                  \"download\": {\n                    \"type\": \"object\",\n                    \"required\": [\n                      \"src\",\n                      \"dest\"\n                    ],\n                    \"properties\": {\n                      \"src\": {\n                        \"type\": \"string\"\n                      },\n                      \"dest\": {\n                        \"type\": \"string\"\n                      }\n                    }\n                  },\n                  \"desc\": {\n                    \"type\": \"string\"\n                  },\n                  \"once\": {\n                    \"type\": \"boolean\"\n                  },\n                  \"hidden\": {\n                    \"type\": \"boolean\"\n                  },\n                  \"limit\": {\n                    \"type\": \"number\"\n                  },\n                  \"select\": {\n                    \"type\": \"string\"\n                  }\n                }\n              }\n            },\n            {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          ]\n        }\n      }\n    },\n    \"before\": {\n      \"type\": \"object\"\n    },\n    \"after\": {\n      \"type\": \"object\"\n    }\n  }\n}\n"
  },
  {
    "path": "tests/bootstrap.php",
    "content": "<?php\n\n$loaded = false;\n\nforeach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) {\n    if (file_exists($file)) {\n        require $file;\n        $loaded = true;\n        break;\n    }\n}\n\nif (!$loaded) {\n    die(\n        'You need to set up the project dependencies using the following commands:' . PHP_EOL .\n        'composer install' . PHP_EOL\n    );\n}\n\n// For loading recipes\nset_include_path(__DIR__ . '/..' . PATH_SEPARATOR . get_include_path());\n\nputenv('DEPLOYER_LOCAL_WORKER=true');\ndefine('DEPLOYER_BIN', __DIR__ . '/../bin/dep');\ndefine('__FIXTURES__', __DIR__ . '/fixtures');\ndefine('__REPOSITORY__', __DIR__ . '/fixtures/repository');\ndefine('__TEMP_DIR__', sys_get_temp_dir() . '/deployer');\n\nrequire_once __DIR__ . '/legacy/AbstractTest.php';\nrequire_once __DIR__ . '/joy/JoyTest.php';\n\n// Init repository\n$repository = __REPOSITORY__;\n\n`cd $repository && git init`;\n$branch = trim(`git rev-parse --abbrev-ref HEAD`);\n`cd $repository && git checkout -B $branch 2>&1`;\n`cd $repository && git add .`;\n`cd $repository && git config user.name 'Anton Medvedev'`;\n`cd $repository && git config user.email 'anton.medv@example.com'`;\n`cd $repository && git commit -m 'first commit'`;\n"
  },
  {
    "path": "tests/fixtures/project/uploaded.html",
    "content": ""
  },
  {
    "path": "tests/fixtures/repository/README.md",
    "content": "# Example repository\n\n\n"
  },
  {
    "path": "tests/fixtures/repository/composer.json",
    "content": "{\n  \"name\": \"ಠ_ಠ\",\n  \"require\": {\n    \"php\": \"^7.3\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/repository/uploads/poem.txt",
    "content": "Night, street, lamp, drugstore,\nA dull and meaningless light.\nGo on and live another quarter century -\nNothing will change. There's no way out.\n\nYou'll die, then start from the beginning,\nIt will repeat, just like before:\nNight, icy ripples on a canal,\nDrugstore, street, lamp.\n\n    A. A. Blok\n    10 October 1912\n"
  },
  {
    "path": "tests/joy/HostDefaultConfigTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace joy;\n\nclass HostDefaultConfigTest extends JoyTest\n{\n    protected function recipe(): string\n    {\n        return <<<'PHP'\n            <?php\n            namespace Deployer;\n            localhost();\n\n            task('test', function () {\n                $port = currentHost()->getPort();\n                writeln(empty($port) ? 'empty' : \"port:$port\");\n            });\n            PHP;\n    }\n\n    public function testOnFunc()\n    {\n        $this->dep('test');\n        $display = $this->tester->getDisplay();\n        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n        self::assertStringContainsString('empty', $display);\n    }\n}\n"
  },
  {
    "path": "tests/joy/JoyTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace joy;\n\nuse Deployer\\Deployer;\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Application;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Console\\Tester\\ApplicationTester;\n\nuse const __TEMP_DIR__;\n\nabstract class JoyTest extends TestCase\n{\n    /**\n     * @var ApplicationTester\n     */\n    protected $tester;\n\n    /**\n     * @var Deployer\n     */\n    protected $deployer;\n\n    public static function setUpBeforeClass(): void\n    {\n        self::cleanUp();\n        mkdir(__TEMP_DIR__);\n    }\n\n    public static function tearDownAfterClass(): void\n    {\n        self::cleanUp();\n    }\n\n    protected static function cleanUp()\n    {\n        if (is_dir(__TEMP_DIR__)) {\n            exec('rm -rf ' . __TEMP_DIR__);\n        }\n    }\n\n    protected function init(string $recipe)\n    {\n        $console = new Application();\n        $console->setAutoExit(false);\n        $this->tester = new ApplicationTester($console);\n\n        $this->deployer = new Deployer($console);\n        $this->deployer->importer->import($recipe);\n        $this->deployer->init();\n        $this->deployer->config->set('deploy_path', __TEMP_DIR__ . '/{{hostname}}');\n    }\n\n    protected function dep(string $task, array $args = []): int\n    {\n        $recipe = __TEMP_DIR__ . '/' . get_called_class() . '.php';\n        file_put_contents($recipe, $this->recipe());\n        $this->init($recipe);\n        return $this->tester->run(array_merge([\n            $task,\n            'selector' => 'all',\n            '--file' => $recipe,\n            '--limit' => 1,\n        ], $args), [\n            'verbosity' => OutputInterface::VERBOSITY_VERBOSE,\n            'interactive' => false,\n        ]);\n    }\n\n    abstract protected function recipe(): string;\n}\n"
  },
  {
    "path": "tests/joy/OnFuncTest.php",
    "content": "<?php\n\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace joy;\n\nclass OnFuncTest extends JoyTest\n{\n    protected function recipe(): string\n    {\n        return <<<'PHP'\n            <?php\n            namespace Deployer;\n            localhost('prod');\n            localhost('beta');\n\n            task('test', [\n                'first',\n                'second',\n            ]);\n\n            task('first', function () {\n                set('foo', '{{alias}}');\n            });\n\n            task('second', function () {\n                on(selectedHosts(), function () {\n                    writeln('foo = {{foo}}');\n                }); \n            })->once();\n            PHP;\n    }\n\n    public function testOnFunc()\n    {\n        putenv('DEPLOYER_LOCAL_WORKER=false');\n        $this->dep('test');\n        putenv('DEPLOYER_LOCAL_WORKER=true');\n\n        $display = $this->tester->getDisplay();\n        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n        self::assertStringContainsString('[prod] foo = prod', $display);\n        self::assertStringContainsString('[beta] foo = beta', $display);\n    }\n}\n"
  },
  {
    "path": "tests/legacy/AbstractTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Application;\nuse Symfony\\Component\\Console\\Output\\Output;\nuse Symfony\\Component\\Console\\Tester\\ApplicationTester;\n\n/**\n * @deprecated Use JoyTest instead.\n */\nabstract class AbstractTest extends TestCase\n{\n    /**\n     * @var ApplicationTester\n     */\n    protected $tester;\n\n    /**\n     * @var Deployer\n     */\n    protected $deployer;\n\n    public static function setUpBeforeClass(): void\n    {\n        self::cleanUp();\n        mkdir(__TEMP_DIR__);\n    }\n\n    public static function tearDownAfterClass(): void\n    {\n        self::cleanUp();\n    }\n\n    protected static function cleanUp()\n    {\n        if (is_dir(__TEMP_DIR__)) {\n            exec('rm -rf ' . __TEMP_DIR__);\n        }\n    }\n\n    protected function init(string $recipe)\n    {\n        $console = new Application();\n        $console->setAutoExit(false);\n        $this->tester = new ApplicationTester($console);\n\n        $this->deployer = new Deployer($console);\n        $this->deployer->importer->import($recipe);\n        $this->deployer->init();\n        $this->deployer->config->set('deploy_path', __TEMP_DIR__ . '/{{hostname}}');\n    }\n\n    protected function dep(string $recipe, string $task)\n    {\n        $this->init($recipe);\n        $this->tester->run([\n            $task,\n            'selector' => 'all',\n            '-f' => $recipe,\n            '-l' => 1,\n        ], [\n            'verbosity' => Output::VERBOSITY_VERBOSE,\n            'interactive' => false,\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/legacy/CurrentPathTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nuse Symfony\\Component\\Console\\Output\\Output;\n\nclass CurrentPathTest extends AbstractTest\n{\n    public const RECIPE = __DIR__ . '/recipe/deploy.php';\n\n    public function testDeployWithDifferentCurrentPath()\n    {\n        $currentPath = __TEMP_DIR__ . '/prod/public_html';\n\n        $this->init(self::RECIPE);\n        $this->tester->run([\n            'deploy',\n            'selector' => 'prod',\n            '-f' => self::RECIPE,\n            '-o' => ['current_path=' . $currentPath],\n        ], [\n            'verbosity' => Output::VERBOSITY_VERBOSE,\n        ]);\n\n        $display = $this->tester->getDisplay();\n        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n        self::assertFileExists($currentPath . '/README.md');\n        self::assertFileExists($currentPath . '/config/test.yaml');\n    }\n}\n"
  },
  {
    "path": "tests/legacy/DeployTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nuse Symfony\\Component\\Console\\Output\\Output;\n\nclass DeployTest extends AbstractTest\n{\n    public const RECIPE = __DIR__ . '/recipe/deploy.php';\n\n    public function testDeploy()\n    {\n        $display = $this->dep(self::RECIPE, 'deploy');\n\n        $display = $this->tester->getDisplay();\n        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n\n        foreach ($this->deployer->hosts as $host) {\n            $deployPath = $host->get('deploy_path');\n\n            self::assertDirectoryExists($deployPath . '/.dep');\n            self::assertDirectoryExists($deployPath . '/releases');\n            self::assertDirectoryExists($deployPath . '/shared');\n            self::assertDirectoryExists($deployPath . '/current');\n            self::assertDirectoryExists($deployPath . '/current/');\n            self::assertFileExists($deployPath . '/current/README.md');\n            self::assertDirectoryExists($deployPath . '/current/storage/logs');\n            self::assertDirectoryExists($deployPath . '/current/storage/db');\n            self::assertDirectoryExists($deployPath . '/shared/storage/logs');\n            self::assertDirectoryExists($deployPath . '/shared/storage/db');\n            self::assertFileExists($deployPath . '/shared/uploads/poem.txt');\n            self::assertFileExists($deployPath . '/shared/.env');\n            self::assertFileExists($deployPath . '/current/config/test.yaml');\n            self::assertFileExists($deployPath . '/shared/config/test.yaml');\n            self::assertEquals(1, intval(exec(\"cd $deployPath && ls -1 releases | wc -l\")));\n        }\n    }\n\n    public function testDeploySelectHosts()\n    {\n        $this->init(self::RECIPE);\n        $this->tester->setInputs(['0,1']);\n        $this->tester->run(['deploy', '-f' => self::RECIPE, '-l' => 1], [\n            'verbosity' => Output::VERBOSITY_NORMAL,\n            'interactive' => true,\n        ]);\n        self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay());\n    }\n\n    public function testKeepReleases()\n    {\n        for ($i = 0; $i < 3; $i++) {\n            $this->dep(self::RECIPE, 'deploy');\n            self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay());\n        }\n\n        for ($i = 0; $i < 6; $i++) {\n            $this->dep(self::RECIPE, 'deploy:fail');\n            self::assertEquals(1, $this->tester->getStatusCode(), $this->tester->getDisplay());\n        }\n\n        for ($i = 0; $i < 3; $i++) {\n            $this->dep(self::RECIPE, 'deploy');\n            self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay());\n        }\n\n        foreach ($this->deployer->hosts as $host) {\n            $deployPath = $host->get('deploy_path');\n\n            self::assertEquals(3, intval(exec(\"cd $deployPath && ls -1 releases | wc -l\")));\n        }\n    }\n\n    /**\n     * @depends testKeepReleases\n     */\n    public function testRollback()\n    {\n        $this->dep(self::RECIPE, 'rollback');\n\n        self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay());\n\n        foreach ($this->deployer->hosts as $host) {\n            $deployPath = $host->get('deploy_path');\n\n            self::assertEquals(3, intval(exec(\"cd $deployPath && ls -1 releases | wc -l\")));\n        }\n    }\n\n    public function testFail()\n    {\n        $this->dep(self::RECIPE, 'deploy:fail');\n\n        $display = $this->tester->getDisplay();\n        self::assertEquals(1, $this->tester->getStatusCode(), $display);\n\n        foreach ($this->deployer->hosts as $host) {\n            $deployPath = $host->get('deploy_path');\n\n            self::assertEquals('ok', exec(\"cd $deployPath && [ -f .dep/deploy.lock ] || echo ok\"), 'fail hooks deploy:unlock did not run');\n        }\n    }\n\n    /**\n     * @depends testFail\n     */\n    public function testCleanup()\n    {\n        $this->dep(self::RECIPE, 'deploy:cleanup');\n\n        self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay());\n\n        foreach ($this->deployer->hosts as $host) {\n            $deployPath = $host->get('deploy_path');\n\n            self::assertFileDoesNotExist($deployPath . '/release');\n        }\n    }\n\n    public function testIsUnlockedExitsWithOneWhenDeployIsLocked()\n    {\n        $this->dep(self::RECIPE, 'deploy:lock');\n        $this->dep(self::RECIPE, 'deploy:is_locked');\n        $display = $this->tester->getDisplay();\n\n        self::assertStringContainsString('Deploy is locked by ', $display);\n        self::assertSame(1, $this->tester->getStatusCode());\n    }\n\n    public function testIsUnlockedExitsWithZeroWhenDeployIsNotLocked()\n    {\n        $this->dep(self::RECIPE, 'deploy:unlock');\n        $this->dep(self::RECIPE, 'deploy:is_locked');\n        $display = $this->tester->getDisplay();\n\n        self::assertStringContainsString('Deploy is unlocked.', $display);\n        self::assertSame(0, $this->tester->getStatusCode());\n    }\n}\n"
  },
  {
    "path": "tests/legacy/EnvTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nclass EnvTest extends AbstractTest\n{\n    public const RECIPE = __DIR__ . '/recipe/env.php';\n\n    public function testOnce()\n    {\n        $this->dep(self::RECIPE, 'test');\n\n        $display = $this->tester->getDisplay();\n        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n        self::assertStringContainsString('global=global', $display);\n        self::assertStringContainsString('local=local', $display);\n        self::assertStringContainsString('dotenv=Hello, world!', $display);\n        self::assertStringContainsString('dotenv=local', $display);\n    }\n}\n"
  },
  {
    "path": "tests/legacy/NamedArgumentsTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nuse Symfony\\Component\\Console\\Output\\Output;\n\n// TODO: Wait until Deployer 7.1 with only php8 supports.\n//class NamedArgumentsTest extends AbstractTest\n//{\n//    const RECIPE = __DIR__ . '/recipe/named_arguments.php';\n//\n//    public function testRunWithNamedArguments()\n//    {\n//        $this->init(self::RECIPE);\n//        $this->tester->run(['named_arguments', '-f' => self::RECIPE], ['verbosity' => Output::VERBOSITY_VERBOSE]);\n//\n//        $display = $this->tester->getDisplay();\n//        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n//        self::assertStringContainsString('Hello, world!', $display);\n//    }\n//\n//    public function testRunWithOptions()\n//    {\n//        $this->init(self::RECIPE);\n//        $this->tester->run(['options', '-f' => self::RECIPE], ['verbosity' => Output::VERBOSITY_VERBOSE]);\n//\n//        $display = $this->tester->getDisplay();\n//        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n//        self::assertStringContainsString('Hello, Anton!', $display);\n//    }\n//\n//    public function testRunWithOptionsWithNamedArguments()\n//    {\n//        $this->init(self::RECIPE);\n//        $this->tester->run(['options_with_named_arguments', '-f' => self::RECIPE], ['verbosity' => Output::VERBOSITY_VERBOSE]);\n//\n//        $display = $this->tester->getDisplay();\n//        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n//        self::assertStringContainsString('Hello, override!', $display);\n//    }\n//\n//    public function testRunLocallyWithNamedArguments()\n//    {\n//        $this->init(self::RECIPE);\n//        $this->tester->run(['run_locally_named_arguments', '-f' => self::RECIPE], ['verbosity' => Output::VERBOSITY_VERBOSE]);\n//\n//        $display = $this->tester->getDisplay();\n//        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n//        self::assertStringContainsString('Hello, world!', $display);\n//    }\n//}\n"
  },
  {
    "path": "tests/legacy/OncePerNodeTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nclass OncePerNodeTest extends AbstractTest\n{\n    public const RECIPE = __DIR__ . '/recipe/once_per_node.php';\n\n    public function testOnce()\n    {\n        $this->dep(self::RECIPE, 'test_once_per_node');\n\n        $display = $this->tester->getDisplay();\n        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n        self::assertStringContainsString('alias: group_a_1 hostname: localhost', $display);\n        self::assertStringNotContainsString('alias: group_a_2 hostname: localhost', $display);\n        self::assertStringContainsString('alias: group_b_1 hostname: group_b_1', $display);\n        self::assertStringNotContainsString('alias: group_b_2 hostname: group_b_2', $display);\n    }\n}\n"
  },
  {
    "path": "tests/legacy/OnceTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nclass OnceTest extends AbstractTest\n{\n    public const RECIPE = __DIR__ . '/recipe/once.php';\n\n    public function testOnce()\n    {\n        $this->dep(self::RECIPE, 'test_once');\n\n        $display = $this->tester->getDisplay();\n        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n        self::assertTrue(substr_count($display, 'SHOULD BE ONCE') == 1, $display);\n    }\n}\n"
  },
  {
    "path": "tests/legacy/ParallelTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nuse Symfony\\Component\\Console\\Output\\Output;\n\nclass ParallelTest extends AbstractTest\n{\n    public const RECIPE = __DIR__ . '/recipe/parallel.php';\n\n    public static function setUpBeforeClass(): void\n    {\n        parent::setUpBeforeClass();\n        putenv('DEPLOYER_LOCAL_WORKER=false'); // Allow to start workers. Don't forget to disable it later.\n    }\n\n    public static function tearDownAfterClass(): void\n    {\n        putenv('DEPLOYER_LOCAL_WORKER=true');\n        parent::tearDownAfterClass();\n    }\n\n    public function testWorker()\n    {\n        $this->init(self::RECIPE);\n        $this->tester->run([\n            'echo',\n            '-f' => self::RECIPE,\n            'selector' => 'all',\n        ], [\n            'verbosity' => Output::VERBOSITY_NORMAL,\n        ]);\n        self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay());\n    }\n\n    public function testServer()\n    {\n        $this->init(self::RECIPE);\n        $this->tester->setInputs(['prod', 'Black bear']);\n        $this->tester->run([\n            'ask',\n            '-f' => self::RECIPE,\n        ], [\n            'verbosity' => Output::VERBOSITY_NORMAL,\n            'interactive' => true,\n        ]);\n        $display = $this->tester->getDisplay();\n        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n        self::assertStringContainsString('[prod] Question: What kind of bear is best?', $display);\n        self::assertStringContainsString('[prod] Black bear', $display);\n    }\n\n    public function testOption()\n    {\n        $this->init(self::RECIPE);\n        $this->tester->run(\n            [\n                'echo',\n                'selector' => 'all',\n                '-o' => ['greet=Hello'],\n                '-f' => self::RECIPE,\n                //'-l' => 1,\n            ],\n            [\n                'verbosity' => Output::VERBOSITY_DEBUG,\n                'interactive' => false,\n            ],\n        );\n\n        $display = $this->tester->getDisplay();\n        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n        self::assertStringContainsString('[prod] Hello, prod!', $display);\n        self::assertStringContainsString('[beta] Hello, beta!', $display);\n    }\n\n    public function testCachedHostConfig()\n    {\n        $this->init(self::RECIPE);\n        $this->tester->run([\n            'cache_config_test',\n            '-f' => self::RECIPE,\n            'selector' => 'all',\n        ], [\n            'verbosity' => Output::VERBOSITY_NORMAL,\n        ]);\n\n        $display = $this->tester->getDisplay();\n        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n        self::assertTrue(substr_count($display, 'worker on prod') == 1, $display);\n        self::assertTrue(substr_count($display, 'worker on beta') == 1, $display);\n    }\n\n    public function testHostConfigFromCallback()\n    {\n        $this->init(self::RECIPE);\n        $this->tester->run([\n            'host_config_from_callback',\n            '-f' => self::RECIPE,\n            'selector' => 'all',\n        ], [\n            'verbosity' => Output::VERBOSITY_NORMAL,\n        ]);\n\n        $display = $this->tester->getDisplay();\n        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n        self::assertTrue(substr_count($display, '[prod] config value is from global') == 1, $display);\n        self::assertTrue(substr_count($display, '[beta] config value is from callback') == 1, $display);\n    }\n}\n"
  },
  {
    "path": "tests/legacy/SelectTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nuse Symfony\\Component\\Console\\Output\\Output;\n\nclass SelectTest extends AbstractTest\n{\n    public const RECIPE = __DIR__ . '/recipe/select.php';\n\n    public function testSelect()\n    {\n        $this->init(self::RECIPE);\n        $this->tester->run([\n            'test',\n            '-f' => self::RECIPE,\n            'selector' => 'prod',\n        ], [\n            'verbosity' => Output::VERBOSITY_DEBUG,\n        ]);\n\n        $display = $this->tester->getDisplay();\n        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n        self::assertStringNotContainsString('executing on prod', $display);\n        self::assertStringContainsString('executing on beta', $display);\n        self::assertStringContainsString('executing on dev', $display);\n    }\n}\n"
  },
  {
    "path": "tests/legacy/UpdateCodeTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nuse Symfony\\Component\\Console\\Output\\Output;\n\nclass UpdateCodeTest extends AbstractTest\n{\n    public const RECIPE = __DIR__ . '/recipe/update_code.php';\n\n    public function testDeployWithDifferentUpdateCodeTask()\n    {\n        $this->init(self::RECIPE);\n        $this->tester->run([\n            'deploy',\n            'selector' => 'prod',\n            '-f' => self::RECIPE,\n        ], [\n            'verbosity' => Output::VERBOSITY_VERBOSE,\n        ]);\n\n        $display = $this->tester->getDisplay();\n        $deployPath = $this->deployer->hosts->get('prod')->getDeployPath();\n\n        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n        self::assertFileExists($deployPath . '/current/uploaded.html');\n    }\n}\n"
  },
  {
    "path": "tests/legacy/YamlTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nuse Symfony\\Component\\Console\\Output\\Output;\n\nclass YamlTest extends AbstractTest\n{\n    public const RECIPE = __DIR__ . '/recipe/deploy.yaml';\n\n    public function testDeploy()\n    {\n        $this->init(self::RECIPE);\n        $this->deployer->config->set('repository', __REPOSITORY__);\n        $this->tester->run([\n            'deploy',\n            'selector' => 'all',\n            '-f' => self::RECIPE,\n        ], [\n            'verbosity' => Output::VERBOSITY_VERBOSE,\n            'interactive' => false,\n        ]);\n\n        $display = $this->tester->getDisplay();\n        self::assertEquals(0, $this->tester->getStatusCode(), $display);\n\n        foreach ($this->deployer->hosts as $host) {\n            $deployPath = $host->get('deploy_path');\n\n            self::assertDirectoryExists($deployPath . '/.dep');\n            self::assertDirectoryExists($deployPath . '/releases');\n            self::assertDirectoryExists($deployPath . '/shared');\n            self::assertDirectoryExists($deployPath . '/current');\n            self::assertDirectoryExists($deployPath . '/current/');\n            self::assertFileExists($deployPath . '/current/README.md');\n            self::assertDirectoryExists($deployPath . '/current/storage/logs');\n            self::assertDirectoryExists($deployPath . '/current/storage/db');\n            self::assertDirectoryExists($deployPath . '/shared/storage/logs');\n            self::assertDirectoryExists($deployPath . '/shared/storage/db');\n            self::assertFileExists($deployPath . '/shared/uploads/poem.txt');\n            self::assertFileExists($deployPath . '/shared/.env');\n            self::assertFileExists($deployPath . '/current/config/test.yaml');\n            self::assertFileExists($deployPath . '/shared/config/test.yaml');\n            self::assertEquals(1, intval(`cd $deployPath && ls -1 releases | wc -l`));\n        }\n    }\n}\n"
  },
  {
    "path": "tests/legacy/recipe/deploy.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire 'recipe/common.php';\n\nset('application', 'deployer');\nset('repository', __REPOSITORY__);\nset('shared_dirs', [\n    'uploads',\n    'storage/logs/',\n    'storage/db',\n]);\nset('shared_files', [\n    '.env',\n    'config/test.yaml',\n]);\nset('keep_releases', 3);\nset('http_user', false);\n\nlocalhost('prod');\n\ntask('deploy', [\n    'deploy:prepare',\n    'deploy:vendors',\n    'deploy:publish',\n]);\n\n// Mock vendors installation to speed up tests.\ntask('deploy:vendors', function () {\n    if (!commandExist('unzip')) {\n        warning('To speed up composer installation setup \"unzip\" command with PHP zip extension.');\n    }\n    run('cd {{release_path}} && echo {{bin/composer}} {{composer_options}} 2>&1');\n});\n\ntask('deploy:fail', [\n    'deploy:prepare',\n    'fail',\n    'deploy:publish',\n]);\n\ntask('fail', function () {\n    run('false');\n});\n\nfail('deploy:fail', 'deploy:unlock');\n"
  },
  {
    "path": "tests/legacy/recipe/deploy.yaml",
    "content": "import: recipe/common.php\n\nconfig:\n  application: deployer\n  shared_dirs:\n    - uploads\n    - storage/logs/\n    - storage/db\n  shared_files:\n    - .env\n    - config/test.yaml\n  keep_releases: 3\n  http_user: false\n\nhosts:\n  prod:\n    local: true\n\ntasks:\n  deploy:\n    - deploy:prepare\n    - deploy:vendors\n    - deploy:publish\n\n  deploy:vendors:\n    - cd: '{{release_path}}'\n    - run: echo {{bin/composer}} {{composer_options}} 2>&1\n"
  },
  {
    "path": "tests/legacy/recipe/env.php",
    "content": "<?php\n\nnamespace Deployer;\n\nlocalhost('prod');\n\nset('env', [\n    'VAR' => 'global',\n]);\n\ntask('test', function () {\n    info('global=' . run('echo $VAR'));\n    info('local=' . run('echo $VAR', env: ['VAR' => 'local']));\n    info('dotenv=' . run('echo $KEY'));\n    info('dotenv=' . run('echo $KEY', env: ['KEY' => 'local']));\n});\n\nbefore('test', function () {\n    run('mkdir -p {{deploy_path}}');\n    run('echo KEY=\"\\'Hello, world!\\'\" > {{deploy_path}}/.env');\n    set('dotenv', '{{deploy_path}}/.env');\n});\n"
  },
  {
    "path": "tests/legacy/recipe/once.php",
    "content": "<?php\n\nnamespace Deployer;\n\nlocalhost('prod');\nlocalhost('beta');\n\ntask('test_once', function () {\n    writeln('SHOULD BE ONCE');\n})->once();\n"
  },
  {
    "path": "tests/legacy/recipe/once_per_node.php",
    "content": "<?php\n\nnamespace Deployer;\n\nlocalhost('group_a_1')\n    ->setHostname('localhost');\nlocalhost('group_a_2')\n    ->setHostname('localhost');\nlocalhost('group_b_1')\n    ->setLabels(['node' => 'anna']);\nlocalhost('group_b_2')\n    ->setLabels(['node' => 'anna']);\n\ntask('test_once_per_node', function () {\n    writeln('alias: {{alias}} hostname: {{hostname}}');\n})->oncePerNode();\n"
  },
  {
    "path": "tests/legacy/recipe/parallel.php",
    "content": "<?php\n\nnamespace Deployer;\n\nlocalhost('prod');\nlocalhost('beta')\n    ->set('host_level_callback_config', function () {\n        return 'from callback';\n    });\n\n// testServer:\n\ntask('ask', function () {\n    $answer = ask('Question: What kind of bear is best?');\n    writeln($answer);\n});\n\n// testWorker, testOption:\n\nset('greet', '_');\n\ntask('echo', function () {\n    $alias = currentHost()->getAlias();\n    run(\"echo {{greet}}, $alias!\");\n});\n\n// testCachedHostConfig:\n\nset('upper_host', function () {\n    writeln('running ' . (Deployer::isWorker() ? 'worker' : 'master') . ' on ' . currentHost()->getAlias());\n    return strtoupper(currentHost()->getAlias());\n});\n\ntask('cache_config_test', function () {\n    writeln('echo 1: {{upper_host}}');\n});\n\nafter('cache_config_test', function () {\n    writeln('echo 2: {{upper_host}}');\n});\n\n// testHostConfigFromCallback:\n\nset('host_level_callback_config', 'from global');\n\ntask('host_config_from_callback', function () {\n    writeln('config value is {{host_level_callback_config}}');\n});\n"
  },
  {
    "path": "tests/legacy/recipe/select.php",
    "content": "<?php\n\nnamespace Deployer;\n\nlocalhost('prod')->setLabels(['env' => 'prod']);\nlocalhost('beta')->setLabels(['env' => 'dev']);\nlocalhost('dev')->setLabels(['env' => 'dev']);\n\ntask('test', function () {\n    on(select('env=dev'), function () {\n        info('executing on {{alias}}');\n    });\n});\n"
  },
  {
    "path": "tests/legacy/recipe/update_code.php",
    "content": "<?php\n\nnamespace Deployer;\n\nrequire __DIR__ . '/deploy.php';\n\ntask('deploy:update_code', function () {\n    upload(__FIXTURES__ . '/project/', '{{release_path}}');\n});\n"
  },
  {
    "path": "tests/phpstan-baseline.neon",
    "content": "parameters:\n\tignoreErrors:\n\t\t-\n\t\t\tmessage: \"#^Comparison operation \\\"\\\\>\\\" between 100|125|200|100000 and 0 is always true\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: ../src/Command/BlackjackCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Else branch is unreachable because previous condition is always true\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: ../src/Command/BlackjackCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^If condition is always false\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: ../src/Command/BlackjackCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Comparison operation \\\"\\\\>\\\" between 0 and 0 is always false\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: ../src/Command/BlackjackCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Unsafe usage of new static\\\\(\\\\)\\\\.$#\"\n\t\t\tcount: 2\n\t\t\tpath: ../src/Component/PharUpdate/Exception/Exception.php\n\n\t\t-\n\t\t\tmessage: \"#^If condition is always true\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: ../src/Host/Host.php\n\n\t\t-\n\t\t\tmessage: \"#^Unreachable statement \\\\- code above always terminates\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: ../src/Importer/Importer.php\n"
  },
  {
    "path": "tests/src/Collection/CollectionTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Collection;\n\nuse Deployer\\Host\\HostCollection;\nuse Deployer\\Task\\TaskCollection;\nuse PHPUnit\\Framework\\TestCase;\n\nclass CollectionTest extends TestCase\n{\n    public static function collections()\n    {\n        return [\n            [new Collection()],\n            [new TaskCollection()],\n            [new HostCollection()],\n        ];\n    }\n\n    /**\n     * @dataProvider collections\n     */\n    public function testCollection($collection)\n    {\n        $this->assertInstanceOf(Collection::class, $collection);\n\n        $object = new \\stdClass();\n        $collection->set('object', $object);\n\n        $this->assertTrue($collection->has('object'));\n        $this->assertEquals($object, $collection->get('object'));\n\n        $this->assertEquals(['object' => $object], $collection->select(function ($value, $key) use ($object) {\n            return $value === $object && $key === 'object';\n        }));\n    }\n\n    /**\n     * @dataProvider collections\n     * @depends      testCollection\n     */\n    public function testException($collection)\n    {\n        $this->expectException(\\InvalidArgumentException::class);\n        $collection->get('unexpected');\n    }\n}\n"
  },
  {
    "path": "tests/src/Command/BlackjackCommandTest.php",
    "content": "<?php\n\nuse Deployer\\Command\\BlackjackCommand;\nuse PHPUnit\\Framework\\TestCase;\n\nclass BlackjackCommandTest extends TestCase\n{\n    public function testHandValue()\n    {\n        self::assertEquals(3, BlackjackCommand::handValue([['3']]));\n        self::assertEquals(10, BlackjackCommand::handValue([['3'], ['7']]));\n        self::assertEquals(12, BlackjackCommand::handValue([['A'], ['A'], ['3'], ['7']]));\n        self::assertEquals(12, BlackjackCommand::handValue([['A'], ['A']]));\n        self::assertEquals(18, BlackjackCommand::handValue([['A'], ['7']]));\n        self::assertEquals(21, BlackjackCommand::handValue([['A'], ['3'], ['7']]));\n        self::assertEquals(21, BlackjackCommand::handValue([['A'], ['Q']]));\n        self::assertEquals(22, BlackjackCommand::handValue([['A'], ['Q'], ['A'], ['Q']]));\n    }\n}\n"
  },
  {
    "path": "tests/src/Component/Pimple/PimpleTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Component\\Pimple;\n\nuse Deployer\\Component\\Pimple\\Exception\\FrozenServiceException;\nuse Deployer\\Component\\Pimple\\Exception\\InvalidServiceIdentifierException;\nuse Deployer\\Component\\Pimple\\Exception\\UnknownIdentifierException;\nuse InvalidArgumentException;\nuse PHPUnit\\Framework\\TestCase;\nuse ReflectionProperty;\nuse RuntimeException;\n\nuse function extension_loaded;\n\nclass PimpleTest extends TestCase\n{\n    public function testWithString()\n    {\n        $pimple = new Container();\n        $pimple['param'] = 'value';\n\n        $this->assertEquals('value', $pimple['param']);\n    }\n\n    public function testWithClosure()\n    {\n        $pimple = new Container();\n        $pimple['service'] = function () {\n            return new Service();\n        };\n\n        $this->assertInstanceOf(Service::class, $pimple['service']);\n    }\n\n    public function testServicesShouldBeDifferent()\n    {\n        $pimple = new Container();\n        $pimple['service'] = $pimple->factory(function () {\n            return new Service();\n        });\n\n        $serviceOne = $pimple['service'];\n        $this->assertInstanceOf(Service::class, $serviceOne);\n\n        $serviceTwo = $pimple['service'];\n        $this->assertInstanceOf(Service::class, $serviceTwo);\n\n        $this->assertNotSame($serviceOne, $serviceTwo);\n    }\n\n    public function testShouldPassContainerAsParameter()\n    {\n        $pimple = new Container();\n        $pimple['service'] = function () {\n            return new Service();\n        };\n        $pimple['container'] = function ($container) {\n            return $container;\n        };\n\n        $this->assertNotSame($pimple, $pimple['service']);\n        $this->assertSame($pimple, $pimple['container']);\n    }\n\n    public function testIsset()\n    {\n        $pimple = new Container();\n        $pimple['param'] = 'value';\n        $pimple['service'] = function () {\n            return new Service();\n        };\n\n        $pimple['null'] = null;\n\n        $this->assertTrue(isset($pimple['param']));\n        $this->assertTrue(isset($pimple['service']));\n        $this->assertTrue(isset($pimple['null']));\n        $this->assertFalse(isset($pimple['non_existent']));\n    }\n\n    public function testConstructorInjection()\n    {\n        $params = ['param' => 'value'];\n        $pimple = new Container($params);\n\n        $this->assertSame($params['param'], $pimple['param']);\n    }\n\n    public function testOffsetGetValidatesKeyIsPresent()\n    {\n        $this->expectException(UnknownIdentifierException::class);\n        $this->expectExceptionMessage('Identifier \"foo\" is not defined.');\n\n        $pimple = new Container();\n        echo $pimple['foo'];\n    }\n\n    /**\n     * @group legacy\n     */\n    public function testLegacyOffsetGetValidatesKeyIsPresent()\n    {\n        $this->expectException(InvalidArgumentException::class);\n        $this->expectExceptionMessage('Identifier \"foo\" is not defined.');\n\n        $pimple = new Container();\n        echo $pimple['foo'];\n    }\n\n    public function testOffsetGetHonorsNullValues()\n    {\n        $pimple = new Container();\n        $pimple['foo'] = null;\n        $this->assertNull($pimple['foo']);\n    }\n\n    public function testUnset()\n    {\n        $pimple = new Container();\n        $pimple['param'] = 'value';\n        $pimple['service'] = function () {\n            return new Service();\n        };\n\n        unset($pimple['param'], $pimple['service']);\n        $this->assertFalse(isset($pimple['param']));\n        $this->assertFalse(isset($pimple['service']));\n    }\n\n    /**\n     * @dataProvider serviceDefinitionProvider\n     */\n    public function testShare($service)\n    {\n        $pimple = new Container();\n        $pimple['shared_service'] = $service;\n\n        $serviceOne = $pimple['shared_service'];\n        $this->assertInstanceOf(Service::class, $serviceOne);\n\n        $serviceTwo = $pimple['shared_service'];\n        $this->assertInstanceOf(Service::class, $serviceTwo);\n\n        $this->assertSame($serviceOne, $serviceTwo);\n    }\n\n    /**\n     * @dataProvider serviceDefinitionProvider\n     */\n    public function testProtect($service)\n    {\n        $pimple = new Container();\n        $pimple['protected'] = $pimple->protect($service);\n\n        $this->assertSame($service, $pimple['protected']);\n    }\n\n    public function testGlobalFunctionNameAsParameterValue()\n    {\n        $pimple = new Container();\n        $pimple['global_function'] = 'strlen';\n        $this->assertSame('strlen', $pimple['global_function']);\n    }\n\n    public function testRaw()\n    {\n        $pimple = new Container();\n        $pimple['service'] = $definition = $pimple->factory(function () {\n            return 'foo';\n        });\n        $this->assertSame($definition, $pimple->raw('service'));\n    }\n\n    public function testRawHonorsNullValues()\n    {\n        $pimple = new Container();\n        $pimple['foo'] = null;\n        $this->assertNull($pimple->raw('foo'));\n    }\n\n    public function testRawValidatesKeyIsPresent()\n    {\n        $this->expectException(UnknownIdentifierException::class);\n        $this->expectExceptionMessage('Identifier \"foo\" is not defined.');\n\n        $pimple = new Container();\n        $pimple->raw('foo');\n    }\n\n    /**\n     * @group legacy\n     */\n    public function testLegacyRawValidatesKeyIsPresent()\n    {\n        $this->expectException(InvalidArgumentException::class);\n        $this->expectExceptionMessage('Identifier \"foo\" is not defined.');\n\n        $pimple = new Container();\n        $pimple->raw('foo');\n    }\n\n    /**\n     * @dataProvider serviceDefinitionProvider\n     */\n    public function testExtend($service)\n    {\n        $pimple = new Container();\n        $pimple['shared_service'] = function () {\n            return new Service();\n        };\n        $pimple['factory_service'] = $pimple->factory(function () {\n            return new Service();\n        });\n\n        $pimple->extend('shared_service', $service);\n        $serviceOne = $pimple['shared_service'];\n        $this->assertInstanceOf(Service::class, $serviceOne);\n        $serviceTwo = $pimple['shared_service'];\n        $this->assertInstanceOf(Service::class, $serviceTwo);\n        $this->assertSame($serviceOne, $serviceTwo);\n        $this->assertSame($serviceOne->value, $serviceTwo->value);\n\n        $pimple->extend('factory_service', $service);\n        $serviceOne = $pimple['factory_service'];\n        $this->assertInstanceOf(Service::class, $serviceOne);\n        $serviceTwo = $pimple['factory_service'];\n        $this->assertInstanceOf(Service::class, $serviceTwo);\n        $this->assertNotSame($serviceOne, $serviceTwo);\n        $this->assertNotSame($serviceOne->value, $serviceTwo->value);\n    }\n\n    public function testExtendDoesNotLeakWithFactories()\n    {\n        if (extension_loaded('pimple')) {\n            $this->markTestSkipped('Pimple extension does not support this test');\n        }\n        $pimple = new Container();\n\n        $pimple['foo'] = $pimple->factory(function () {\n            return;\n        });\n        $pimple['foo'] = $pimple->extend('foo', function ($foo, $pimple) {\n            return;\n        });\n        unset($pimple['foo']);\n\n        $p = new ReflectionProperty($pimple, 'values');\n        $p->setAccessible(true);\n        $this->assertEmpty($p->getValue($pimple));\n\n        $p = new ReflectionProperty($pimple, 'factories');\n        $p->setAccessible(true);\n        $this->assertCount(0, $p->getValue($pimple));\n    }\n\n    public function testExtendValidatesKeyIsPresent()\n    {\n        $this->expectException(UnknownIdentifierException::class);\n        $this->expectExceptionMessage('Identifier \"foo\" is not defined.');\n\n        $pimple = new Container();\n        $pimple->extend('foo', function () {});\n    }\n\n    /**\n     * @group legacy\n     */\n    public function testLegacyExtendValidatesKeyIsPresent()\n    {\n        $this->expectException(InvalidArgumentException::class);\n        $this->expectExceptionMessage('Identifier \"foo\" is not defined.');\n\n        $pimple = new Container();\n        $pimple->extend('foo', function () {});\n    }\n\n    public function testKeys()\n    {\n        $pimple = new Container();\n        $pimple['foo'] = 123;\n        $pimple['bar'] = 123;\n\n        $this->assertEquals(['foo', 'bar'], $pimple->keys());\n    }\n\n    /** @test */\n    public function settingAnInvokableObjectShouldTreatItAsFactory()\n    {\n        $pimple = new Container();\n        $pimple['invokable'] = new Invokable();\n\n        $this->assertInstanceOf(Service::class, $pimple['invokable']);\n    }\n\n    /** @test */\n    public function settingNonInvokableObjectShouldTreatItAsParameter()\n    {\n        $pimple = new Container();\n        $pimple['non_invokable'] = new NonInvokable();\n\n        $this->assertInstanceOf(NonInvokable::class, $pimple['non_invokable']);\n    }\n\n    /**\n     * @dataProvider badServiceDefinitionProvider\n     */\n    public function testFactoryFailsForInvalidServiceDefinitions($service)\n    {\n        $this->expectException(\\TypeError::class);\n        $pimple = new Container();\n        $pimple->factory($service);\n    }\n\n    /**\n     * @group legacy\n     * @dataProvider badServiceDefinitionProvider\n     */\n    public function testLegacyFactoryFailsForInvalidServiceDefinitions($service)\n    {\n        $this->expectException(\\TypeError::class);\n        $pimple = new Container();\n        $pimple->factory($service);\n    }\n\n    /**\n     * @dataProvider badServiceDefinitionProvider\n     */\n    public function testProtectFailsForInvalidServiceDefinitions($service)\n    {\n        $this->expectException(\\TypeError::class);\n        $pimple = new Container();\n        $pimple->protect($service);\n    }\n\n    /**\n     * @group legacy\n     * @dataProvider badServiceDefinitionProvider\n     */\n    public function testLegacyProtectFailsForInvalidServiceDefinitions($service)\n    {\n        $this->expectException(\\TypeError::class);\n        $pimple = new Container();\n        $pimple->protect($service);\n    }\n\n    /**\n     * @dataProvider badServiceDefinitionProvider\n     */\n    public function testExtendFailsForKeysNotContainingServiceDefinitions($service)\n    {\n        $this->expectException(InvalidServiceIdentifierException::class);\n        $this->expectExceptionMessage('Identifier \"foo\" does not contain an object definition.');\n\n        $pimple = new Container();\n        $pimple['foo'] = $service;\n        $pimple->extend('foo', function () {});\n    }\n\n    /**\n     * @group legacy\n     * @dataProvider badServiceDefinitionProvider\n     */\n    public function testLegacyExtendFailsForKeysNotContainingServiceDefinitions($service)\n    {\n        $this->expectException(InvalidArgumentException::class);\n        $this->expectExceptionMessage('Identifier \"foo\" does not contain an object definition.');\n\n        $pimple = new Container();\n        $pimple['foo'] = $service;\n        $pimple->extend('foo', function () {});\n    }\n\n    /**\n     * @group legacy\n     * @expectedDeprecation How Pimple behaves when extending protected closures will be fixed in Pimple 4. Are you sure \"foo\" should be protected?\n     */\n    public function testExtendingProtectedClosureDeprecation()\n    {\n        $pimple = new Container();\n        $pimple['foo'] = $pimple->protect(function () {\n            return 'bar';\n        });\n\n        $pimple->extend('foo', function ($value) {\n            return $value . '-baz';\n        });\n\n        $this->assertSame('bar-baz', $pimple['foo']);\n    }\n\n    /**\n     * @dataProvider badServiceDefinitionProvider\n     */\n    public function testExtendFailsForInvalidServiceDefinitions($service)\n    {\n        $this->expectException(\\TypeError::class);\n        $pimple = new Container();\n        $pimple['foo'] = function () {};\n        $pimple->extend('foo', $service);\n    }\n\n    /**\n     * @group legacy\n     * @dataProvider badServiceDefinitionProvider\n     */\n    public function testLegacyExtendFailsForInvalidServiceDefinitions($service)\n    {\n        $this->expectException(\\TypeError::class);\n        $pimple = new Container();\n        $pimple['foo'] = function () {};\n        $pimple->extend('foo', $service);\n    }\n\n    public function testExtendFailsIfFrozenServiceIsNonInvokable()\n    {\n        $this->expectException(FrozenServiceException::class);\n        $this->expectExceptionMessage('Cannot override frozen service \"foo\".');\n\n        $pimple = new Container();\n        $pimple['foo'] = function () {\n            return new NonInvokable();\n        };\n        $foo = $pimple['foo'];\n\n        $pimple->extend('foo', function () {});\n    }\n\n    public function testExtendFailsIfFrozenServiceIsInvokable()\n    {\n        $this->expectException(FrozenServiceException::class);\n        $this->expectExceptionMessage('Cannot override frozen service \"foo\".');\n\n        $pimple = new Container();\n        $pimple['foo'] = function () {\n            return new Invokable();\n        };\n        $foo = $pimple['foo'];\n\n        $pimple->extend('foo', function () {});\n    }\n\n    /**\n     * Provider for invalid service definitions.\n     */\n    public static function badServiceDefinitionProvider()\n    {\n        return [\n            [123],\n            [new NonInvokable()],\n        ];\n    }\n\n    /**\n     * Provider for service definitions.\n     */\n    public static function serviceDefinitionProvider()\n    {\n        return [\n            [function ($value) {\n                $service = new Service();\n                $service->value = $value;\n\n                return $service;\n            }],\n            [new Invokable()],\n        ];\n    }\n\n    public function testDefiningNewServiceAfterFreeze()\n    {\n        $pimple = new Container();\n        $pimple['foo'] = function () {\n            return 'foo';\n        };\n        $foo = $pimple['foo'];\n\n        $pimple['bar'] = function () {\n            return 'bar';\n        };\n        $this->assertSame('bar', $pimple['bar']);\n    }\n\n    public function testOverridingServiceAfterFreeze()\n    {\n        $this->expectException(FrozenServiceException::class);\n        $this->expectExceptionMessage('Cannot override frozen service \"foo\".');\n\n        $pimple = new Container();\n        $pimple['foo'] = function () {\n            return 'foo';\n        };\n        $foo = $pimple['foo'];\n\n        $pimple['foo'] = function () {\n            return 'bar';\n        };\n    }\n\n    /**\n     * @group legacy\n     */\n    public function testLegacyOverridingServiceAfterFreeze()\n    {\n        $this->expectException(RuntimeException::class);\n        $this->expectExceptionMessage('Cannot override frozen service \"foo\".');\n\n        $pimple = new Container();\n        $pimple['foo'] = function () {\n            return 'foo';\n        };\n        $foo = $pimple['foo'];\n\n        $pimple['foo'] = function () {\n            return 'bar';\n        };\n    }\n\n    public function testRemovingServiceAfterFreeze()\n    {\n        $pimple = new Container();\n        $pimple['foo'] = function () {\n            return 'foo';\n        };\n        $foo = $pimple['foo'];\n\n        unset($pimple['foo']);\n        $pimple['foo'] = function () {\n            return 'bar';\n        };\n        $this->assertSame('bar', $pimple['foo']);\n    }\n\n    public function testExtendingService()\n    {\n        $pimple = new Container();\n        $pimple['foo'] = function () {\n            return 'foo';\n        };\n        $pimple['foo'] = $pimple->extend('foo', function ($foo, $app) {\n            return \"$foo.bar\";\n        });\n        $pimple['foo'] = $pimple->extend('foo', function ($foo, $app) {\n            return \"$foo.baz\";\n        });\n        $this->assertSame('foo.bar.baz', $pimple['foo']);\n    }\n\n    public function testExtendingServiceAfterOtherServiceFreeze()\n    {\n        $pimple = new Container();\n        $pimple['foo'] = function () {\n            return 'foo';\n        };\n        $pimple['bar'] = function () {\n            return 'bar';\n        };\n        $foo = $pimple['foo'];\n\n        $pimple['bar'] = $pimple->extend('bar', function ($bar, $app) {\n            return \"$bar.baz\";\n        });\n        $this->assertSame('bar.baz', $pimple['bar']);\n    }\n}\n\nclass Invokable\n{\n    public function __invoke($value = null)\n    {\n        $service = new Service();\n        $service->value = $value;\n\n        return $service;\n    }\n}\n\nclass NonInvokable\n{\n    public function __call($a, $b) {}\n}\n\nclass Service\n{\n    public $value;\n}\n"
  },
  {
    "path": "tests/src/Configuration/ConfigurationTest.php",
    "content": "<?php\n\nnamespace Deployer\\Configuration;\n\nuse Deployer\\Configuration;\nuse Deployer\\Exception\\ConfigurationException;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ConfigurationTest extends TestCase\n{\n    public function testParse()\n    {\n        $config = new Configuration();\n        $config->set('foo', 'a');\n        $config['bar'] = 'b';\n\n        self::assertEquals('a b', $config->parse('{{foo}} {{bar}}'));\n    }\n\n    public function testUnset()\n    {\n        $config = new Configuration();\n        $config->set('opt', true);\n        unset($config['opt']);\n        self::assertFalse(isset($config['opt']));\n    }\n\n    public function testGet()\n    {\n        $config = new Configuration();\n        $config->set('opt', true);\n        $config->set('fn', function () {\n            return 'func';\n        });\n\n        self::assertTrue(isset($config['opt']));\n        self::assertEquals(true, $config['opt']);\n        self::assertEquals('func', $config['fn']);\n    }\n\n    public function testGetDefault()\n    {\n        $config = new Configuration();\n        $config->set('name', 'alpha');\n\n        self::assertEquals('/alpha', $config->get('path', '/{{name}}'));\n    }\n\n    public function testGetException()\n    {\n        $this->expectException(ConfigurationException::class);\n\n        $config = new Configuration();\n        $config->set('name', 'alpha');\n\n        self::assertEquals('/alpha', $config->get('path'));\n    }\n\n    public function testGetParent()\n    {\n        $parent = new Configuration();\n        $config = new Configuration($parent);\n\n        $parent->set('opt', 'value');\n        self::assertEquals('value', $parent['opt']);\n        self::assertEquals('value', $config['opt']);\n\n        $parent->set('opt', 'newValue');\n        self::assertEquals('newValue', $parent['opt']);\n        self::assertEquals('value', $config['opt']);\n\n        $config->set('opt', 'hostValue');\n        self::assertEquals('newValue', $parent['opt']);\n        self::assertEquals('hostValue', $config['opt']);\n        self::assertEquals('okay', $config->get('miss', 'okay'));\n    }\n\n    public function testGetParentParent()\n    {\n        $global = new Configuration();\n        $parent = new Configuration($global);\n        $config = new Configuration($parent);\n\n        $global->set('global', 'value from {{path}}');\n        $parent->set('path', 'parent');\n\n        self::assertEquals('value from parent', $config->get('global'));\n    }\n\n    public function testGetParentWhatDependsOnChild()\n    {\n        $parent = new Configuration();\n        $alpha = new Configuration($parent);\n        $beta = new Configuration($parent);\n\n        $parent->set('deploy_path', 'path/{{name}}');\n        $alpha->set('name', 'alpha');\n        $beta->set('name', 'beta');\n\n        self::assertEquals('path/alpha', $alpha->get('deploy_path'));\n        self::assertEquals('path/beta', $beta->get('deploy_path'));\n    }\n\n    public function testGetFromCallback()\n    {\n        $config = new Configuration();\n        $config->set('func', function () {\n            return 'param';\n        });\n        self::assertEquals('param', $config['func']);\n    }\n\n    public function testAdd()\n    {\n        $config = new Configuration();\n        $config->set('opt', ['foo', 'bar']);\n        $config->add('opt', ['baz']);\n        self::assertEquals(['foo', 'bar', 'baz'], $config['opt']);\n    }\n\n    public function testAddEmpty()\n    {\n        $config = new Configuration();\n        $config->add('opt', ['baz']);\n        self::assertEquals(['baz'], $config['opt']);\n    }\n\n    public function testAddDefaultToNotArray()\n    {\n        $this->expectException(\\RuntimeException::class);\n        $this->expectExceptionMessage('Config option \"config\" isn\\'t array.');\n\n        $config = new Configuration();\n        $config->set('config', 'option');\n        $config->add('config', ['three']);\n    }\n\n    public function testAddToParent()\n    {\n        $parent = new Configuration();\n        $alpha = new Configuration($parent);\n\n        $parent->set('files', ['a', 'b']);\n        $alpha->add('files', ['c']);\n\n        self::assertEquals(['a', 'b', 'c'], $alpha->get('files'));\n    }\n\n    public function testAddToParentCallback()\n    {\n        $parent = new Configuration();\n        $alpha = new Configuration($parent);\n\n        $parent->set('files', function () {\n            return ['a', 'b'];\n        });\n        $alpha->add('files', ['c']);\n\n        self::assertEquals(['a', 'b', 'c'], $alpha->get('files'));\n    }\n\n    public function testPersist()\n    {\n        $parent = new Configuration();\n        $alpha = new Configuration($parent);\n\n        $parent->set('global', 'do not include');\n        $alpha->set('whoami', function () {\n            $this->fail('should not be called');\n        });\n        $alpha->set('name', 'alpha');\n\n        self::assertEquals(['name' => 'alpha'], $alpha->persist());\n    }\n}\n"
  },
  {
    "path": "tests/src/DeployerTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Application;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass DeployerTest extends TestCase\n{\n    private $deployer;\n\n    protected function setUp(): void\n    {\n        $console = new Application();\n        $input = $this->createMock(InputInterface::class);\n        $output = $this->createMock(OutputInterface::class);\n        $this->deployer = new Deployer($console, $input, $output);\n    }\n\n    protected function tearDown(): void\n    {\n        unset($this->deployer);\n    }\n\n    public function testInstance()\n    {\n        $this->assertEquals($this->deployer, Deployer::get());\n    }\n}\n"
  },
  {
    "path": "tests/src/FunctionsTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer;\n\nuse Deployer\\Configuration;\nuse Deployer\\Host\\Host;\nuse Deployer\\Host\\Localhost;\nuse Deployer\\Task\\Context;\nuse Deployer\\Task\\GroupTask;\nuse Deployer\\Task\\Task;\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Application;\nuse Symfony\\Component\\Console\\Input\\Input;\nuse Symfony\\Component\\Console\\Output\\Output;\n\nuse function Deployer\\localhost;\n\nclass FunctionsTest extends TestCase\n{\n    /**\n     * @var Deployer\n     */\n    private $deployer;\n\n    protected function setUp(): void\n    {\n        $console = new Application();\n\n        $input = $this->createMock(Input::class);\n        $output = $this->createMock(Output::class);\n        $host = new Localhost();\n\n        $this->deployer = new Deployer($console);\n        $this->deployer['input'] = $input;\n        $this->deployer['output'] = $output;\n        Context::push(new Context($host));\n    }\n\n    protected function tearDown(): void\n    {\n        Context::pop();\n        unset($this->deployer);\n        $this->deployer = null;\n    }\n\n    public function testHost()\n    {\n        host('domain.com');\n        self::assertInstanceOf(Host::class, $this->deployer->hosts->get('domain.com'));\n\n        host('a1.domain.com', 'a2.domain.com')->set('roles', 'app');\n        self::assertInstanceOf(Host::class, $this->deployer->hosts->get('a1.domain.com'));\n        self::assertInstanceOf(Host::class, $this->deployer->hosts->get('a2.domain.com'));\n\n        host('db[1:2].domain.com')->set('roles', 'db');\n        self::assertInstanceOf(Host::class, $this->deployer->hosts->get('db1.domain.com'));\n        self::assertInstanceOf(Host::class, $this->deployer->hosts->get('db2.domain.com'));\n    }\n\n    public function testLocalhost()\n    {\n        localhost('domain.com');\n        self::assertInstanceOf(Localhost::class, $this->deployer->hosts->get('domain.com'));\n    }\n\n    public function testTask()\n    {\n        task('task', function () {});\n\n        $task = $this->deployer->tasks->get('task');\n        self::assertInstanceOf(Task::class, $task);\n\n        $task = task('task');\n        self::assertInstanceOf(Task::class, $task);\n\n        task('group', ['task']);\n        $task = $this->deployer->tasks->get('group');\n        self::assertInstanceOf(GroupTask::class, $task);\n    }\n\n    public function testBefore()\n    {\n        task('main', function () {});\n        task('before', function () {});\n        before('main', 'before');\n        before('before', function () {});\n\n        $names = $this->taskToNames($this->deployer->scriptManager->getTasks('main'));\n        self::assertEquals(['before:before', 'before', 'main'], $names);\n    }\n\n    public function testAfter()\n    {\n        task('main', function () {});\n        task('after', function () {});\n        after('main', 'after');\n        after('after', function () {});\n\n        $names = $this->taskToNames($this->deployer->scriptManager->getTasks('main'));\n        self::assertEquals(['main', 'after', 'after:after'], $names);\n    }\n\n    public function testRunLocally()\n    {\n        $output = runLocally('echo \"hello\"');\n        self::assertEquals('hello', $output);\n    }\n\n    public function testWithinSetsWorkingPaths()\n    {\n        Context::get()->getConfig()->set('working_path', '/foo');\n\n        within('/bar', function () {\n            $withinWorkingPath = Context::get()->getConfig()->get('working_path');\n            self::assertEquals('/bar', $withinWorkingPath);\n        });\n\n        $originalWorkingPath = Context::get()->getConfig()->get('working_path');\n        self::assertEquals('/foo', $originalWorkingPath);\n    }\n\n    public function testWithinRestoresWorkingPathInCaseOfException()\n    {\n        Context::get()->getConfig()->set('working_path', '/foo');\n\n        try {\n            within('/bar', function () {\n                throw new \\Exception('Dummy exception');\n            });\n        } catch (\\Exception $exception) {\n            // noop\n        }\n\n        $originalWorkingPath = Context::get()->getConfig()->get('working_path');\n        self::assertEquals('/foo', $originalWorkingPath);\n    }\n\n    public function testWithinReturningValue()\n    {\n        $output = within('/foo', function () {\n            return 'bar';\n        });\n\n        self::assertEquals('bar', $output);\n    }\n\n    public function testWithinWithVoidFunction()\n    {\n        $output = within('/foo', function () {\n            // noop\n        });\n\n        self::assertNull($output);\n    }\n\n    private function taskToNames($tasks)\n    {\n        return array_map(function (Task $task) {\n            return $task->getName();\n        }, $tasks);\n    }\n}\n"
  },
  {
    "path": "tests/src/Host/ConfigurationTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Host;\n\nuse Deployer\\Configuration;\nuse Deployer\\Exception\\ConfigurationException;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ConfigurationTest extends TestCase\n{\n    public function testConfiguration()\n    {\n        $config = new Configuration();\n        $config->set('int', 42);\n        $config->set('string', 'value');\n        $config->set('array', [1, 'two']);\n        $config->set('hyphen-ated', 'hyphen');\n        $config->set('parse', 'is {{int}}');\n        $config->set('parse-hyphen', 'has {{hyphen-ated}}');\n        $config->set('callback', function () {\n            return 'callback';\n        });\n        $this->assertEquals(42, $config->get('int'));\n        $this->assertEquals('value', $config->get('string'));\n        $this->assertEquals([1, 'two'], $config->get('array'));\n        $this->assertEquals('default', $config->get('no', 'default'));\n        $this->assertEquals(null, $config->get('no', null));\n        $this->assertEquals('callback', $config->get('callback'));\n        $this->assertEquals('is 42', $config->get('parse'));\n        $this->assertEquals('has hyphen', $config->get('parse-hyphen'));\n\n        $config->set('int', 11);\n        $this->assertEquals('is 11', $config->get('parse'));\n\n        $this->expectException('RuntimeException');\n        $config->get('so');\n    }\n\n    public function testAddParams()\n    {\n        $config = new Configuration();\n        $config->set('config', [\n            'one',\n            'two' => 2,\n            'nested' => [],\n        ]);\n        $config->add('config', [\n            'two' => 20,\n            'nested' => [\n                'first',\n            ],\n        ]);\n        $config->add('config', [\n            'nested' => [\n                'second',\n            ],\n        ]);\n        $config->add('config', [\n            'extra',\n        ]);\n\n        $expected = [\n            'one',\n            'two' => 20,\n            'nested' => [\n                'first',\n                'second',\n            ],\n            'extra',\n        ];\n        $this->assertEquals($expected, $config->get('config'));\n    }\n\n    public function testAddParamsToNotArray()\n    {\n        $this->expectException(ConfigurationException::class);\n\n        $config = new Configuration();\n        $config->set('config', 'option');\n        $config->add('config', ['three']);\n    }\n}\n"
  },
  {
    "path": "tests/src/Host/HostTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Host;\n\nuse Deployer\\Configuration;\nuse PHPUnit\\Framework\\TestCase;\n\nclass HostTest extends TestCase\n{\n    public function testHost()\n    {\n        $host = new Host('host');\n        $host\n            ->setHostname('hostname')\n            ->setRemoteUser('remote_user')\n            ->setPort(22)\n            ->setConfigFile('~/.ssh/config')\n            ->setIdentityFile('~/.ssh/id_rsa')\n            ->setForwardAgent(true)\n            ->setSshMultiplexing(true);\n\n        self::assertEquals('host', $host->getAlias());\n        self::assertStringContainsString('host', $host->getTag());\n        self::assertEquals('hostname', $host->getHostname());\n        self::assertEquals('remote_user', $host->getRemoteUser());\n        self::assertEquals(22, $host->getPort());\n        self::assertEquals('~/.ssh/config', $host->getConfigFile());\n        self::assertEquals('~/.ssh/id_rsa', $host->getIdentityFile());\n        self::assertEquals(true, $host->getForwardAgent());\n        self::assertEquals(true, $host->getSshMultiplexing());\n    }\n\n    public function testConfigurationAccessor()\n    {\n        $host = new Host('host');\n        $host\n            ->set('roles', ['db', 'app'])\n            ->set('key', 'value')\n            ->set('array', [1])\n            ->add('array', [2]);\n\n        self::assertEquals(['db', 'app'], $host->get('roles'));\n        self::assertEquals('value', $host->get('key'));\n        self::assertEquals([1, 2], $host->get('array'));\n    }\n\n    public function testHostAlias()\n    {\n        $host = new Host('host/alias');\n        self::assertEquals('host/alias', $host->getAlias());\n        self::assertEquals('host', $host->getHostname());\n    }\n\n    public function testHostWithParams()\n    {\n        $host = new Host('host');\n        $value = 'new_value';\n        $host\n            ->set('env', $value)\n            ->set('identity_file', '{{env}}');\n\n        self::assertEquals($value, $host->getIdentityFile());\n    }\n\n    public function testHostWithUserFromConfig()\n    {\n        $parent = new Configuration();\n        $parent->set(\"deploy_user\", function () {\n            return \"test_user\";\n        });\n\n        $host = new Host('host');\n        $host->config()->bind($parent);\n        $host\n            ->setHostname('host')\n            ->setRemoteUser('{{deploy_user}}')\n            ->setPort(22);\n\n        self::assertEquals('test_user@host', $host->connectionString());\n    }\n}\n"
  },
  {
    "path": "tests/src/Host/RangeTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Host;\n\nuse PHPUnit\\Framework\\TestCase;\n\nclass RangeTest extends TestCase\n{\n    public function testExpand()\n    {\n        self::assertEquals(['h1', 'h2', 'h3'], Range::expand(['h[1:3]']));\n        self::assertEquals(['h1', 'h2', 'ha'], Range::expand(['h[1:2]', 'ha']));\n        self::assertEquals(['h0', 'h1'], Range::expand(['h[0:1]']));\n        self::assertEquals(['h1'], Range::expand(['h[1:1]']));\n        self::assertEquals(['ha', 'hb', 'hc', 'hd'], Range::expand(['h[a:d]']));\n\n        $hostnames = Range::expand(['h[01:20]']);\n        self::assertContains('h01', $hostnames);\n        self::assertContains('h10', $hostnames);\n        self::assertContains('h20', $hostnames);\n\n        self::assertCount(100, Range::expand(['h[1:100]']));\n        self::assertCount(26, Range::expand(['h[a:z]']));\n    }\n}\n"
  },
  {
    "path": "tests/src/Importer/ImporterTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Deployer\\Importer;\n\nuse Deployer\\Deployer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ImporterTest extends TestCase\n{\n    private $previousInput;\n    private $previousOutput;\n\n    public function setUp(): void\n    {\n        $deployer = Deployer::get();\n        $this->previousInput = $deployer->input;\n        $this->previousOutput = $deployer->output;\n    }\n\n    public function tearDown(): void\n    {\n        Deployer::get()->input = $this->previousInput;\n        Deployer::get()->output = $this->previousOutput;\n    }\n\n    public function testCanOneOverrideStaticMethod(): void\n    {\n        $extendedImporter = new class extends Importer {\n            public static $config = [];\n\n            protected static function config(array $config)\n            {\n                static::$config = $config;\n            }\n        };\n\n        $data = <<<EOL\n            config:\n                foo: bar\n            # test.yaml\n            EOL;\n\n        $extendedImporter::import(\"data:text/yaml,$data\");\n\n        static::assertSame(['foo' => 'bar'], $extendedImporter::$config);\n    }\n\n    public function testImporterIgnoresYamlHiddenKeys(): void\n    {\n        $data = <<<EOL\n            .base: &base\n              remote_user: foo\n              labels:\n                stage: production\n\n            hosts:\n              acceptance:\n                <<: *base\n                labels:\n                  stage: acceptance\n\n              production:\n                <<: *base\n                remote_user: bar\n              \n              production.beta:\n                <<: *base\n            # test.yaml\n            EOL;\n\n        Importer::import(\"data:text/yaml,$data\");\n        self::assertTrue(Deployer::get()->hosts->has('production'));\n        self::assertTrue(Deployer::get()->hosts->has('acceptance'));\n        self::assertTrue(Deployer::get()->hosts->has('production.beta'));\n        self::assertEquals('acceptance', Deployer::get()->hosts->get('acceptance')->getLabels()['stage']);\n        self::assertEquals('production', Deployer::get()->hosts->get('production')->getLabels()['stage']);\n        self::assertEquals('foo', Deployer::get()->hosts->get('acceptance')->getRemoteUser());\n        self::assertEquals('bar', Deployer::get()->hosts->get('production')->getRemoteUser());\n    }\n}\n"
  },
  {
    "path": "tests/src/Selector/SelectorTest.php",
    "content": "<?php\n\nnamespace Deployer\\Selector;\n\nuse Deployer\\Host\\Host;\nuse Deployer\\Host\\HostCollection;\nuse PHPUnit\\Framework\\TestCase;\n\nclass SelectorTest extends TestCase\n{\n    public function testSelectHosts()\n    {\n        $prod = (new Host('prod.domain.com'))->set('labels', ['stage' => 'prod']);\n        $front = (new Host('prod.domain.com/front'))->set('labels', ['stage' => 'prod', 'tier' => 'frontend']);\n        $beta = (new Host('beta.domain.com'))->set('labels', ['stage' => 'beta']);\n        $dev = (new Host('dev'))->set('labels', ['stage' => 'dev']);\n        $multi = (new Host('multi'))->set('labels', ['stage' => ['prod', 'beta']]);\n        $allHosts = [$prod, $front, $beta, $dev, $multi];\n\n        $hosts = new HostCollection();\n        foreach ($allHosts as $host) {\n            $hosts->set($host->getAlias(), $host);\n        }\n        $selector = new Selector($hosts);\n        self::assertEquals($allHosts, $selector->select('all'));\n        self::assertEquals([$prod, $front, $multi], $selector->select('stage=prod'));\n        self::assertEquals([$front], $selector->select('stage=prod & tier=frontend'));\n        self::assertEquals([$front, $beta, $multi], $selector->select('prod.domain.com/front, stage=beta'));\n        self::assertEquals([$prod, $beta, $dev, $multi], $selector->select('all & tier != frontend'));\n        self::assertEquals([$prod, $front, $dev], $selector->select('stage != beta'));\n    }\n}\n"
  },
  {
    "path": "tests/src/Ssh/IOArgumentsTest.php",
    "content": "<?php\n\nnamespace Deployer\\Ssh;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Input\\ArgvInput;\nuse Symfony\\Component\\Console\\Input\\InputDefinition;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\ConsoleOutput;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass IOArgumentsTest extends TestCase\n{\n    public function testCollect()\n    {\n        $definition = new InputDefinition([\n            new InputOption('option', 'o', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Set configuration option'),\n            new InputOption('limit', 'l', InputOption::VALUE_REQUIRED, 'How many tasks to run in parallel?'),\n            new InputOption('no-hooks', null, InputOption::VALUE_NONE, 'Run tasks without after/before hooks'),\n            new InputOption('plan', null, InputOption::VALUE_NONE, 'Show execution plan'),\n            new InputOption('start-from', null, InputOption::VALUE_REQUIRED, 'Start execution from this task'),\n            new InputOption('log', null, InputOption::VALUE_REQUIRED, 'Write log to a file'),\n            new InputOption('profile', null, InputOption::VALUE_REQUIRED, 'Write profile to a file', ),\n            new InputOption('ansi', null, InputOption::VALUE_OPTIONAL, 'Force ANSI output', ),\n        ]);\n\n        $args = IOArguments::collect(\n            new ArgvInput(['deploy', '-o', 'env=prod', '--ansi', '-l1'], $definition),\n            new ConsoleOutput(OutputInterface::VERBOSITY_DEBUG, false),\n        );\n\n        self::assertEquals(['--option','env=prod', '--limit', '1', '-vvv'], $args);\n    }\n}\n"
  },
  {
    "path": "tests/src/Support/HelpersTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Support;\n\nuse PHPUnit\\Framework\\TestCase;\n\nclass HelpersTest extends TestCase\n{\n    public function testArrayFlatten()\n    {\n        self::assertEquals(['a', 'b', 'c'], array_flatten(['a', ['b', 'key' => ['c']]]));\n    }\n\n    public function testArrayMergeAlternate()\n    {\n        $config = [\n            'one',\n            'two' => 2,\n            'nested' => [],\n        ];\n\n        $config = array_merge_alternate($config, [\n            'two' => 20,\n            'nested' => [\n                'first',\n            ],\n        ]);\n\n        $config = array_merge_alternate($config, [\n            'nested' => [\n                'second',\n            ],\n        ]);\n\n        $config = array_merge_alternate($config, [\n            'extra',\n        ]);\n\n        self::assertEquals([\n            'one',\n            'two' => 20,\n            'nested' => [\n                'first',\n                'second',\n            ],\n            'extra',\n        ], $config);\n    }\n\n    public function testParseHomeDir()\n    {\n        $this->assertStringStartsWith('/', parse_home_dir('~/path'));\n        $this->assertStringStartsWith('/', parse_home_dir('~'));\n        $this->assertStringStartsWith('~', parse_home_dir('~path'));\n        $this->assertStringEndsWith('~', parse_home_dir('path~'));\n    }\n\n    public function testEscapeShellArgument()\n    {\n        $this->assertEquals('\\'{\"foobar\":\"Lorem ipsum\\'\\\\\\'\\'s dolor\"}\\'', escape_shell_argument(json_encode(['foobar' => 'Lorem ipsum\\'s dolor'])));\n    }\n}\n"
  },
  {
    "path": "tests/src/Support/ObjectProxyTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Support;\n\nuse PHPUnit\\Framework\\TestCase;\n\nclass ObjectProxyTest extends TestCase\n{\n    public function testObjectProxy()\n    {\n        $mock = self::getMockBuilder('stdClass')\n            ->addMethods(['foo'])\n            ->getMock();\n        $mock\n            ->expects(self::once())\n            ->method('foo')\n            ->with('a', 'b');\n\n        $proxy = new ObjectProxy([$mock]);\n        $proxy->foo('a', 'b');\n    }\n}\n"
  },
  {
    "path": "tests/src/Task/ContextTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Task;\n\nuse Deployer\\Configuration;\nuse Deployer\\Host\\Host;\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass ContextTest extends TestCase\n{\n    public function testContext()\n    {\n        $host = $this->getMockBuilder(Host::class)->disableOriginalConstructor()->getMock();\n        $host\n            ->expects($this->once())\n            ->method('config')\n            ->willReturn($this->createMock(Configuration::class));\n\n        $context = new Context($host);\n\n        $this->assertInstanceOf(Host::class, $context->getHost());\n        $this->assertInstanceOf(Configuration::class, $context->getConfig());\n\n        Context::push($context);\n\n        $this->assertEquals($context, Context::get());\n        $this->assertEquals($context, Context::pop());\n    }\n}\n"
  },
  {
    "path": "tests/src/Task/ScriptManagerTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Task;\n\nuse PHPUnit\\Framework\\TestCase;\n\nclass ScriptManagerTest extends TestCase\n{\n    public function testGetTasks()\n    {\n        $notify = new Task('notify');\n        $info = new GroupTask('info', ['notify']);\n        $deploy = new GroupTask('deploy', ['deploy:setup', 'deploy:release']);\n        $deploy->addBefore($info);\n        $setup = new Task('deploy:setup');\n        $release = new Task('deploy:release');\n\n        $taskCollection = new TaskCollection();\n        $taskCollection->set($notify->getName(), $notify);\n        $taskCollection->set($info->getName(), $info);\n        $taskCollection->set($deploy->getName(), $deploy);\n        $taskCollection->set($setup->getName(), $setup);\n        $taskCollection->set($release->getName(), $release);\n\n        $scriptManager = new ScriptManager($taskCollection);\n        self::assertEquals([$notify, $setup, $release], $scriptManager->getTasks('deploy'));\n    }\n\n    public function testOnce()\n    {\n        $a = new Task('a');\n        $b = new Task('b');\n        $b->once();\n        $group = new GroupTask('group', ['a', 'b']);\n\n        $taskCollection = new TaskCollection();\n        $taskCollection->add($a);\n        $taskCollection->add($b);\n        $taskCollection->add($group);\n\n        $scriptManager = new ScriptManager($taskCollection);\n        self::assertEquals([$a, $b], $scriptManager->getTasks('group'));\n        self::assertFalse($a->isOnce());\n        self::assertTrue($b->isOnce());\n\n        $group->once();\n        self::assertEquals([$a, $b], $scriptManager->getTasks('group'));\n        self::assertTrue($a->isOnce());\n        self::assertTrue($b->isOnce());\n    }\n\n    public function testSelectsCombine()\n    {\n        $a = new Task('a');\n        $b = new Task('b');\n        $c = new Task('c');\n        $b->select('stage=beta');\n        $c->select('stage=alpha|beta & role=db');\n        $group = new GroupTask('group', ['a', 'b', 'c']);\n\n        $taskCollection = new TaskCollection();\n        $taskCollection->add($a);\n        $taskCollection->add($b);\n        $taskCollection->add($c);\n        $taskCollection->add($group);\n\n        $scriptManager = new ScriptManager($taskCollection);\n        self::assertEquals([$a, $b, $c], $scriptManager->getTasks('group'));\n        self::assertNull($a->getSelector());\n        self::assertEquals([[['=', 'stage', ['beta']]]], $b->getSelector());\n        self::assertEquals([[['=', 'stage', ['alpha', 'beta']],['=', 'role', ['db']]]], $c->getSelector());\n\n        $group->select('role=prod');\n        self::assertEquals([$a, $b, $c], $scriptManager->getTasks('group'));\n        self::assertEquals([[['=', 'role', ['prod']]]], $a->getSelector());\n        self::assertEquals([[['=', 'stage', ['beta']]],[['=', 'role', ['prod']]]], $b->getSelector());\n        self::assertEquals([[['=', 'stage', ['alpha', 'beta']],['=', 'role', ['db']]],[['=', 'role', ['prod']]]], $c->getSelector());\n    }\n\n    public function testThrowsExceptionIfTaskCollectionEmpty()\n    {\n        self::expectException(\\InvalidArgumentException::class);\n\n        $scriptManager = new ScriptManager(new TaskCollection());\n        $scriptManager->getTasks('');\n    }\n\n    public function testThrowsExceptionIfTaskDontExists()\n    {\n        self::expectException(\\InvalidArgumentException::class);\n\n        $taskCollection = new TaskCollection();\n        $taskCollection->set('testTask', new Task('testTask'));\n\n        $scriptManager = new ScriptManager($taskCollection);\n        $scriptManager->getTasks('testTask2');\n    }\n}\n"
  },
  {
    "path": "tests/src/Task/TaskTest.php",
    "content": "<?php\n/* (c) Anton Medvedev <anton@medv.io>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Deployer\\Task;\n\nuse Deployer\\Host\\Host;\nuse PHPUnit\\Framework\\TestCase;\n\nuse function Deployer\\invoke;\nuse function Deployer\\task;\n\nclass TaskTest extends TestCase\n{\n    protected function tearDown(): void\n    {\n        StubTask::$runned = 0;\n    }\n\n    public function testTask()\n    {\n        $mock = self::getMockBuilder('stdClass')\n            ->addMethods(['callback'])\n            ->getMock();\n        $mock\n            ->expects(self::exactly(1))\n            ->method('callback');\n\n        $task = new Task('task_name', function () use ($mock) {\n            $mock->callback();\n        });\n\n        $context = self::getMockBuilder(Context::class)\n            ->disableOriginalConstructor()\n            ->getMock();\n        $task->run($context);\n\n        self::assertEquals('task_name', $task->getName());\n\n        $task->desc('Task description.');\n        self::assertEquals('Task description.', $task->getDescription());\n\n        $task->hidden();\n        self::assertTrue($task->isHidden());\n\n        $task->once();\n        self::assertTrue($task->isOnce());\n\n        $task->oncePerNode();\n        self::assertTrue($task->isOncePerNode());\n    }\n\n    public function testInit()\n    {\n        $context = self::getMockBuilder(Context::class)->disableOriginalConstructor()->getMock();\n\n        // Test create task with [$object, 'method']\n        $mock1 = self::getMockBuilder('stdClass')\n            ->addMethods(['callback'])\n            ->getMock();\n        $mock1\n            ->expects(self::once())\n            ->method('callback');\n        $task1 = new Task('task1', [$mock1, 'callback']);\n        $task1->run($context);\n\n        // Test create task with anonymous functions\n        $mock2 = self::getMockBuilder('stdClass')\n            ->addMethods(['callback'])\n            ->getMock();\n        $mock2\n            ->expects(self::once())\n            ->method('callback');\n        $task2 = new Task('task2', function () use ($mock2) {\n            $mock2->callback();\n        });\n        $task2->run($context);\n\n        self::assertEquals(0, StubTask::$runned);\n        $task3 = new Task('task3', new StubTask());\n        $task3->run($context);\n        self::assertEquals(1, StubTask::$runned);\n    }\n\n    public function testGroupInvoke(): void\n    {\n        $spy = new StubTask();\n\n        task('foo', $spy);\n        task('bar', $spy);\n        task('group', ['foo', 'bar']);\n\n        (new Task('group:invoke', function () {\n            invoke('group');\n        }))->run(new Context(new Host('localhost')));\n\n        $this->assertSame(2, StubTask::$runned);\n    }\n}\n\n/**\n * Stub class for task callable by __invoke()\n */\nclass StubTask\n{\n    public static $runned = 0;\n\n    public function __invoke()\n    {\n        self::$runned++;\n    }\n}\n"
  }
]