├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── LICENSE.md ├── README.md ├── bin └── kirby ├── bootstrap.php ├── commands ├── backup.php ├── clean │ └── content.php ├── clear │ ├── cache.php │ ├── lock.php │ ├── logins.php │ ├── media.php │ └── sessions.php ├── download.php ├── help.php ├── install.php ├── install │ ├── kit.php │ └── repo.php ├── make │ ├── _templates │ │ ├── collection.php │ │ ├── command.php │ │ ├── config.php │ │ ├── controller.php │ │ ├── model.php │ │ ├── plugin.php │ │ ├── snippet.php │ │ └── template.php │ ├── blueprint.php │ ├── collection.php │ ├── command.php │ ├── config.php │ ├── controller.php │ ├── language.php │ ├── model.php │ ├── plugin.php │ ├── snippet.php │ ├── template.php │ └── user.php ├── plugin │ ├── install.php │ ├── remove.php │ └── upgrade.php ├── register.php ├── remove │ └── command.php ├── roots.php ├── security.php ├── unzip.php ├── upgrade.php ├── uuid │ ├── generate.php │ ├── populate.php │ └── remove.php └── version.php ├── composer.json ├── composer.lock ├── phpmd.xml.dist ├── phpunit.xml.dist ├── psalm.xml.dist ├── src └── CLI │ ├── CLI.php │ └── QuietWriter.php └── tests ├── CLI ├── BootstrapTest.php ├── CLITest.php ├── TestCase.php ├── bootstrap │ ├── a │ │ └── index.php │ ├── b │ │ └── www │ │ │ └── index.php │ ├── c │ │ └── public │ │ │ └── index.php │ └── d │ │ └── public_html │ │ └── index.php └── commands │ ├── invalid-action.php │ ├── invalid-format.php │ ├── nested │ ├── _templates │ │ └── template.php │ ├── command.php │ └── not-a-command.txt │ └── test.php └── bootstrap.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # PHP PSR-12 Coding Standards 5 | # https://www.php-fig.org/psr/psr-12/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | indent_style = tab 13 | indent_size = 2 14 | trim_trailing_whitespace = true 15 | 16 | [*.php] 17 | indent_size = 4 18 | insert_final_newline = true 19 | 20 | [*.yml] 21 | indent_style = space 22 | 23 | [*.md,*.txt] 24 | trim_trailing_whitespace = false 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://getkirby.com/buy'] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "composer" 9 | directory: "/" 10 | target-branch: "develop" 11 | schedule: 12 | interval: "daily" 13 | commit-message: 14 | prefix: "composer" 15 | labels: 16 | - "type: refactoring ♻️" 17 | open-pull-requests-limit: 30 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | tests: 6 | name: PHP ${{ matrix.php }} 7 | 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 5 10 | strategy: 11 | matrix: 12 | php: ["8.1", "8.2", "8.3", "8.4"] 13 | env: 14 | extensions: mbstring, pcov 15 | ini: pcov.directory=., "pcov.exclude=\"~(vendor|tests)~\"" 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@v3 20 | 21 | - name: Setup PHP cache environment 22 | id: ext-cache 23 | uses: shivammathur/cache-extensions@d622719c5f9eb1f119bee963028d0c0b984525c5 # pin@v1 24 | with: 25 | php-version: ${{ matrix.php }} 26 | extensions: ${{ env.extensions }} 27 | key: php-v1 28 | 29 | - name: Cache PHP extensions 30 | uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # pin@v3 31 | with: 32 | path: ${{ steps.ext-cache.outputs.dir }} 33 | key: ${{ steps.ext-cache.outputs.key }} 34 | restore-keys: ${{ steps.ext-cache.outputs.key }} 35 | 36 | - name: Setup PHP environment 37 | uses: shivammathur/setup-php@e6f75134d35752277f093989e72e140eaa222f35 # pin@v2 38 | with: 39 | php-version: ${{ matrix.php }} 40 | extensions: ${{ env.extensions }} 41 | ini-values: ${{ env.ini }} 42 | coverage: pcov 43 | tools: phpunit:9.5.13, psalm:4.11.2 44 | 45 | - name: Setup problem matchers 46 | run: | 47 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 48 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 49 | 50 | - name: Get Composer cache directory 51 | id: composerCache 52 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 53 | 54 | - name: Cache dependencies 55 | uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # pin@v3 56 | with: 57 | path: ${{ steps.composerCache.outputs.dir }} 58 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 59 | restore-keys: ${{ runner.os }}-composer- 60 | 61 | - name: Install dependencies 62 | run: composer install --prefer-dist 63 | 64 | - name: Cache analysis data 65 | id: finishPrepare 66 | uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # pin@v3 67 | with: 68 | path: ~/.cache/psalm 69 | key: backend-analysis-${{ matrix.php }}-v2 70 | 71 | - name: Run tests 72 | if: always() && steps.finishPrepare.outcome == 'success' 73 | run: phpunit --coverage-clover ${{ github.workspace }}/clover.xml 74 | # - name: Statically analyze using Psalm 75 | # if: always() && steps.finishPrepare.outcome == 'success' 76 | # run: psalm --output-format=github --php-version=${{ matrix.php }} 77 | 78 | # - name: Upload coverage results to Codecov 79 | # uses: codecov/codecov-action@66b3de25f6f91f65eb92c514d31d6b6f13d5ab18 # pin@v3 80 | # with: 81 | # file: ${{ github.workspace }}/clover.xml 82 | # flags: backend 83 | # env_vars: PHP 84 | # env: 85 | # PHP: ${{ matrix.php }} 86 | 87 | analysis: 88 | name: Analysis 89 | 90 | runs-on: ubuntu-latest 91 | timeout-minutes: 5 92 | env: 93 | php: "8.1" 94 | extensions: mbstring 95 | 96 | steps: 97 | - name: Checkout 98 | uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@v3 99 | 100 | - name: Setup PHP cache environment 101 | id: ext-cache 102 | uses: shivammathur/cache-extensions@d622719c5f9eb1f119bee963028d0c0b984525c5 # pin@v1 103 | with: 104 | php-version: ${{ env.php }} 105 | extensions: ${{ env.extensions }} 106 | key: php-v1 107 | 108 | - name: Cache PHP extensions 109 | uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # pin@v3 110 | with: 111 | path: ${{ steps.ext-cache.outputs.dir }} 112 | key: ${{ steps.ext-cache.outputs.key }} 113 | restore-keys: ${{ steps.ext-cache.outputs.key }} 114 | 115 | - name: Setup PHP environment 116 | id: finishPrepare 117 | uses: shivammathur/setup-php@e6f75134d35752277f093989e72e140eaa222f35 # pin@v2 118 | with: 119 | php-version: ${{ env.php }} 120 | extensions: ${{ env.extensions }} 121 | coverage: none 122 | tools: | 123 | composer:2.3.7, composer-normalize:2.28.0, 124 | composer-unused:0.7.12, phpcpd:6.0.3, phpmd:2.12.0 125 | 126 | - name: Validate composer.json/composer.lock 127 | if: always() && steps.finishPrepare.outcome == 'success' 128 | run: composer validate --strict --no-check-version --no-check-all 129 | 130 | - name: Ensure that composer.json is normalized 131 | if: always() && steps.finishPrepare.outcome == 'success' 132 | run: composer-normalize --dry-run 133 | 134 | - name: Get Composer cache directory 135 | id: composerCache1 136 | if: always() && steps.finishPrepare.outcome == 'success' 137 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 138 | 139 | - name: Cache dependencies 140 | id: composerCache2 141 | if: always() && steps.composerCache1.outcome == 'success' 142 | uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # pin@v3 143 | with: 144 | path: ${{ steps.composerCache1.outputs.dir }} 145 | key: ${{ runner.os }}-composer-locked-${{ hashFiles('**/composer.lock') }} 146 | restore-keys: ${{ runner.os }}-composer-locked- 147 | 148 | - name: Install dependencies 149 | id: composerInstall 150 | if: always() && steps.composerCache2.outcome == 'success' 151 | run: composer install --prefer-dist 152 | 153 | # - name: Check for unused Composer dependencies 154 | # if: always() && steps.composerInstall.outcome == 'success' 155 | # run: composer-unused --no-progress 156 | 157 | - name: Check for duplicated code 158 | if: always() && steps.composerInstall.outcome == 'success' 159 | run: phpcpd --fuzzy --exclude tests --exclude vendor . 160 | 161 | - name: Statically analyze using PHPMD 162 | if: always() && steps.composerInstall.outcome == 'success' 163 | run: phpmd . github phpmd.xml.dist --exclude 'tests/*,vendor/*,_templates/*' 164 | 165 | coding-style: 166 | name: Coding Style 167 | 168 | runs-on: ubuntu-latest 169 | timeout-minutes: 5 170 | 171 | steps: 172 | - name: Checkout 173 | uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@v3 174 | 175 | - name: Setup PHP environment 176 | uses: shivammathur/setup-php@e6f75134d35752277f093989e72e140eaa222f35 # pin@v2 177 | with: 178 | coverage: none 179 | tools: php-cs-fixer:3.49.0 180 | 181 | - name: Cache analysis data 182 | id: finishPrepare 183 | uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # pin@v3 184 | with: 185 | path: ~/.php-cs-fixer 186 | key: coding-style 187 | 188 | - name: Check for PHP coding style violations 189 | if: always() && steps.finishPrepare.outcome == 'success' 190 | env: 191 | PHP_CS_FIXER_IGNORE_ENV: 1 192 | # Use the --dry-run flag in push builds to get a failed CI status 193 | run: > 194 | php-cs-fixer fix --diff ${{ github.event_name != 'pull_request' && '--dry-run' || '' }} 195 | 196 | - name: Create code suggestions from the coding style changes (on PR only) 197 | if: > 198 | always() && steps.finishPrepare.outcome == 'success' && github.event_name == 'pull_request' 199 | uses: reviewdog/action-suggester@94877e550e6b522dc1d21231974b645ff2f084ce # pin@v1 200 | with: 201 | tool_name: PHP-CS-Fixer 202 | fail_on_error: "true" 203 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # os 2 | .DS_Store 3 | 4 | # vendor files 5 | /node_modules 6 | /vendor 7 | 8 | # cs fixer 9 | .php-cs-fixer.cache 10 | 11 | # tests 12 | .phpunit.cache 13 | /tests/coverage 14 | /tests/tmp 15 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | exclude('node_modules') 5 | ->exclude('_templates') 6 | ->in(__DIR__); 7 | 8 | $config = new PhpCsFixer\Config(); 9 | return $config 10 | ->setRules([ 11 | '@PSR12' => true, 12 | 'align_multiline_comment' => ['comment_type' => 'phpdocs_like'], 13 | 'array_indentation' => true, 14 | 'array_syntax' => ['syntax' => 'short'], 15 | 'cast_spaces' => ['space' => 'none'], 16 | // 'class_keyword_remove' => true, // replaces static::class with 'static' (won't work) 17 | 'combine_consecutive_issets' => true, 18 | 'combine_consecutive_unsets' => true, 19 | 'combine_nested_dirname' => true, 20 | 'concat_space' => ['spacing' => 'one'], 21 | 'declare_equal_normalize' => ['space' => 'single'], 22 | 'dir_constant' => true, 23 | 'function_typehint_space' => true, 24 | 'include' => true, 25 | 'logical_operators' => true, 26 | 'lowercase_cast' => true, 27 | 'lowercase_static_reference' => true, 28 | 'magic_constant_casing' => true, 29 | 'magic_method_casing' => true, 30 | 'method_chaining_indentation' => true, 31 | 'modernize_types_casting' => true, 32 | 'multiline_comment_opening_closing' => true, 33 | 'native_function_casing' => true, 34 | 'native_function_type_declaration_casing' => true, 35 | 'new_with_braces' => true, 36 | 'no_blank_lines_after_class_opening' => true, 37 | 'no_blank_lines_after_phpdoc' => true, 38 | 'no_empty_comment' => true, 39 | 'no_empty_phpdoc' => true, 40 | 'no_empty_statement' => true, 41 | 'no_leading_namespace_whitespace' => true, 42 | 'no_mixed_echo_print' => ['use' => 'echo'], 43 | 'no_unneeded_control_parentheses' => true, 44 | 'no_unused_imports' => true, 45 | 'no_useless_return' => true, 46 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 47 | // 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], // adds params in the wrong order 48 | 'phpdoc_align' => ['align' => 'left'], 49 | 'phpdoc_indent' => true, 50 | 'phpdoc_scalar' => true, 51 | 'phpdoc_trim' => true, 52 | 'short_scalar_cast' => true, 53 | 'single_line_comment_style' => true, 54 | 'single_quote' => true, 55 | 'ternary_to_null_coalescing' => true, 56 | 'whitespace_after_comma_in_array' => true 57 | ]) 58 | ->setRiskyAllowed(true) 59 | ->setIndent("\t") 60 | ->setFinder($finder); 61 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bastian Allgeier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kirby CLI 2 | 3 | The Kirby command line interface helps simplifying common tasks with your Kirby installations. 4 | 5 | ## Installation 6 | 7 | ### Via Composer 8 | 9 | ``` 10 | composer global require getkirby/cli 11 | ``` 12 | 13 | Make sure to add your composer bin directory to your `~/.bash_profile` (Mac OS users) or into your `~/.bashrc` (Linux users). 14 | 15 | Your global composer directory is normally either `~/.composer/vendor/bin` or `~/.config/composer/vendor/bin`. You can find the correct path by running … 16 | 17 | ``` 18 | composer -n config --global home 19 | ``` 20 | 21 | Afterwards, add the result to your bash profile … 22 | 23 | ``` 24 | export PATH=~/.composer/vendor/bin:$PATH 25 | ``` 26 | 27 | ### Did it work? 28 | 29 | Check if the installation worked by running the following in your terminal. 30 | 31 | ``` 32 | kirby 33 | ``` 34 | 35 | This should print the Kirby CLI version and a list of available commands 36 | 37 | ## Available core commands 38 | 39 | ``` 40 | - kirby backup 41 | - kirby clean:content 42 | - kirby clear:cache 43 | - kirby clear:lock 44 | - kirby clear:logins 45 | - kirby clear:media 46 | - kirby clear:sessions 47 | - kirby download 48 | - kirby help 49 | - kirby install 50 | - kirby install:kit 51 | - kirby install:repo 52 | - kirby make:blueprint 53 | - kirby make:collection 54 | - kirby make:command 55 | - kirby make:config 56 | - kirby make:controller 57 | - kirby make:language 58 | - kirby make:model 59 | - kirby make:plugin 60 | - kirby make:snippet 61 | - kirby make:template 62 | - kirby make:user 63 | - kirby plugin:install 64 | - kirby plugin:remove 65 | - kirby plugin:upgrade 66 | - kirby register 67 | - kirby remove:command 68 | - kirby roots 69 | - kirby security 70 | - kirby unzip 71 | - kirby upgrade 72 | - kirby uuid:generate 73 | - kirby uuid:populate 74 | - kirby uuid:remove 75 | - kirby version 76 | ``` 77 | 78 | ## Writing commands 79 | 80 | You can create a new command via the CLI: 81 | 82 | ```bash 83 | kirby make:command hello 84 | ``` 85 | 86 | This will create a new `site/commands` folder in your installation with a new `hello.php` file 87 | 88 | The CLI will already put the basic scaffolding into the file: 89 | 90 | ```php 91 | 'Nice command', 95 | 'args' => [], 96 | 'command' => static function ($cli): void { 97 | $cli->success('Nice command!'); 98 | } 99 | ]; 100 | ``` 101 | 102 | You can define your command logic in the command callback. The `$cli` object comes with a set of handy tools to create output, parse command arguments, create prompts and more. 103 | 104 | ## Global commands 105 | 106 | You might have some commands that you need for all your local Kirby installations. This is where global commands come in handy. You can create a new global command with the `--global` flag: 107 | 108 | ```bash 109 | kirby make:command hello --global 110 | ``` 111 | 112 | The command file will then be place in `~/.kirby/commands/hello.php` and is automatically available everywhere. 113 | 114 | ## Command environment 115 | 116 | To load a custom environment config for a particular host, you can set an env variable 117 | 118 | ``` 119 | env KIRBY_HOST=production.com kirby mycommand 120 | ``` 121 | 122 | ## Command plugins 123 | 124 | Your Kirby plugins can define their own set of commands: https://getkirby.com/docs/reference/plugins/extensions/commands 125 | 126 | ```php 127 | Kirby::plugin('your/plugin', [ 128 | 'commands' => [ 129 | 'your-plugin:test' => [ 130 | 'description' => 'Nice command', 131 | 'args' => [], 132 | 'command' => function ($cli) { 133 | $cli->success('My first plugin command'); 134 | } 135 | ] 136 | ] 137 | ]); 138 | ``` 139 | 140 | ## Check for installed commands 141 | 142 | You can always check back if your commands have been created properly by running `kirby` again 143 | 144 | ``` 145 | kirby 146 | ``` 147 | 148 | ## Removing commands 149 | 150 | Once you no longer need a command, you can remove it with … 151 | 152 | ```bash 153 | kirby remove:command hello 154 | ``` 155 | 156 | If you have a local and a global command, you can choose which one to delete. 157 | 158 | ## Debugging 159 | 160 | Use the `-d` or `--debug` argument to run the command in debug mode: 161 | 162 | ```bash 163 | kirby make:command hello --debug 164 | ``` 165 | 166 | ## Formatting Output 167 | 168 | Sending messages to the terminal is super easy. 169 | 170 | ### $cli->out() 171 | 172 | ```php 173 | $cli->out('This is some simple text'); 174 | ``` 175 | 176 | ### $cli->success() 177 | 178 | ```php 179 | $cli->success('This is text in a nice green box'); 180 | ``` 181 | 182 | ### $cli->error() 183 | 184 | ```php 185 | $cli->error('This is red text for errors'); 186 | ``` 187 | 188 | ### $cli->bold() 189 | 190 | ```php 191 | $cli->bold('This is some bold text'); 192 | ``` 193 | 194 | ### $cli->br() 195 | 196 | ```php 197 | // this will create a line break 198 | $cli->br(); 199 | ``` 200 | 201 | For more available colors and formats, check out the CLImate docs: https://climate.thephpleague.com/styling/colors/ 202 | 203 | ## Arguments 204 | 205 | Your commands can define a list of required and optional arguments that need to be provided by the user. 206 | 207 | ```php 208 | 'Hello world', 212 | 'args' => [ 213 | 'name' => [ 214 | 'description' => 'The name for the greeting', 215 | 'required' => true 216 | ] 217 | ], 218 | 'command' => static function ($cli): void { 219 | $cli->success('Hello ' . $cli->arg('name') . '!'); 220 | } 221 | ]; 222 | ``` 223 | 224 | The command can now be executed by providing the name … 225 | 226 | ``` 227 | kirby hello Joe 228 | ``` 229 | 230 | If no name is provided, an error will be shown. 231 | 232 | ### Argument docs 233 | 234 | Arguments can be required, can set a default value and more. Check out the CLImate docs for additional options: https://climate.thephpleague.com/arguments/ 235 | 236 | ## Prompts 237 | 238 | Instead of taking arguments from the command, you can also ask for them in a prompt: 239 | 240 | ```php 241 | 'Hello world', 245 | 'command' => static function ($cli): void { 246 | $name = $cli->prompt('Please enter a name:'); 247 | $cli->success('Hello ' . $name . '!'); 248 | } 249 | ]; 250 | ``` 251 | 252 | As a third alternative you can either take the argument or ask for it if it is not provided: 253 | 254 | ```php 255 | 'Hello world', 259 | 'args' => [ 260 | 'name' => [ 261 | 'description' => 'The name for the greeting', 262 | ] 263 | ], 264 | 'command' => static function ($cli): void { 265 | $name = $cli->argOrPrompt('name', 'Please enter a name:'); 266 | $cli->success('Hello ' . $name . '!'); 267 | } 268 | ]; 269 | ``` 270 | 271 | ## Checkboxes Radios and more 272 | 273 | The CLI also supports more complex ways to get input from users. Check out the CLImate docs how to work with user input: https://climate.thephpleague.com/terminal-objects/input/ 274 | 275 | ## Combining commands 276 | 277 | You can reuse all existing commands in your custom commands to create entire chains of actions. 278 | 279 | ```php 280 | 'Downloads the starterkit and the plainkit', 284 | 'command' => static function ($cli): void { 285 | 286 | $cli->command('install:kit', 'starterkit'); 287 | $cli->command('install:kit', 'plainkit'); 288 | 289 | $cli->success('Starterkit and plainkit have been installed'); 290 | } 291 | ]; 292 | ``` 293 | 294 | ## What's Kirby? 295 | 296 | - **[getkirby.com](https://getkirby.com)** – Get to know the CMS. 297 | - **[Try it](https://getkirby.com/try)** – Take a test ride with our online demo. Or download one of our kits to get started. 298 | - **[Documentation](https://getkirby.com/docs/guide)** – Read the official guide, reference and cookbook recipes. 299 | - **[Issues](https://github.com/getkirby/kirby/issues)** – Report bugs and other problems. 300 | - **[Feedback](https://feedback.getkirby.com)** – You have an idea for Kirby? Share it. 301 | - **[Forum](https://forum.getkirby.com)** – Whenever you get stuck, don't hesitate to reach out for questions and support. 302 | - **[Discord](https://chat.getkirby.com)** – Hang out and meet the community. 303 | - **[YouTube](https://youtube.com/kirbyCasts)** - Watch the latest video tutorials visually with Bastian. 304 | - **[Mastodon](https://mastodon.social/@getkirby)** – Spread the word. 305 | - **[Bluesky](https://bsky.app/profile/getkirby.com)** – Tell a friend. 306 | 307 | --- 308 | 309 | © 2009 Bastian Allgeier 310 | [getkirby.com](https://getkirby.com) · [License agreement](./LICENSE.md) 311 | -------------------------------------------------------------------------------- /bin/kirby: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 'Creates backup of application files', 10 | 'args' => [ 11 | 'root' => [ 12 | 'description' => 'Selects the kirby root, which should be backuped' 13 | ] 14 | ], 15 | 'command' => static function (CLI $cli): void { 16 | if (class_exists('ZipArchive') === false) { 17 | throw new Exception('ZipArchive library could not be found'); 18 | } 19 | 20 | $root = $cli->argOrPrompt( 21 | 'root', 22 | 'Which root should be backuped? (press to backup your entire site)', 23 | false 24 | ); 25 | 26 | $root = empty($root) === true ? 'index' : $root; 27 | $rootPath = $cli->kirby()->root($root); 28 | 29 | if ($rootPath === null) { 30 | throw new Exception('Invalid root entered: ' . $root); 31 | } 32 | 33 | if (is_dir($rootPath) === false) { 34 | throw new Exception('The root does not exist: ' . $root); 35 | } 36 | 37 | $kirbyPath = $cli->kirby()->root('index'); 38 | $backupPath = $kirbyPath . '/backup'; 39 | $backupFile = $backupPath . '/' . $root . '-' . date('Y-m-d-His') . '.zip'; 40 | 41 | if (is_file($backupFile) === true) { 42 | throw new Exception('The backup file exists'); 43 | } 44 | 45 | // create backup directory before the process 46 | Dir::make($backupPath); 47 | 48 | $zip = new ZipArchive(); 49 | if ($zip->open($backupFile, ZipArchive::CREATE) !== true) { 50 | throw new Exception('Failed to create backup file'); 51 | } 52 | 53 | $files = new RecursiveIteratorIterator( 54 | new RecursiveCallbackFilterIterator( 55 | new RecursiveDirectoryIterator( 56 | $rootPath, 57 | FilesystemIterator::SKIP_DOTS 58 | ), 59 | fn ($file) => $file->isFile() || in_array($file->getBaseName(), ['.git', 'backup']) === false 60 | ) 61 | ); 62 | 63 | foreach ($files as $file) { 64 | // skip directories, will be added automatically 65 | if ($file->isDir() === false) { 66 | // get real and relative path for current file 67 | $filePath = $file->getRealPath(); 68 | $relativePath = substr($filePath, strlen($rootPath) + 1); 69 | 70 | // add current file to archive 71 | $zip->addFile($filePath, $relativePath); 72 | } 73 | } 74 | 75 | if ($zip->close() === false) { 76 | throw new Exception('There was a problem writing the backup file'); 77 | } 78 | 79 | $cli->success('The backup has been created: ' . substr($backupFile, strlen($kirbyPath))); 80 | } 81 | ]; 82 | -------------------------------------------------------------------------------- /commands/clean/content.php: -------------------------------------------------------------------------------- 1 | content($lang)->fields(); 15 | 16 | // unset all fields in the `$ignore` array 17 | foreach ($ignore as $field) { 18 | if (array_key_exists($field, $contentFields) === true) { 19 | unset($contentFields[$field]); 20 | } 21 | } 22 | 23 | // get the keys 24 | $contentFields = array_keys($contentFields); 25 | 26 | // get all field keys from blueprint 27 | $blueprintFields = array_keys($item->blueprint()->fields()); 28 | 29 | // get all field keys that are in $contentFields but not in $blueprintFields 30 | $fieldsToBeDeleted = array_diff($contentFields, $blueprintFields); 31 | 32 | // update page only if there are any fields to be deleted 33 | if (count($fieldsToBeDeleted) > 0) { 34 | 35 | // flip keys and values and set new values to null 36 | $data = array_map(fn ($value) => null, array_flip($fieldsToBeDeleted)); 37 | 38 | // try to update the page with the data 39 | $item->update($data, $lang); 40 | } 41 | } 42 | }; 43 | 44 | return [ 45 | 'description' => 'Deletes all fields from page, file or user content files that are not defined in the blueprint, no matter if they contain content or not.', 46 | 'command' => static function (CLI $cli) use ($cleanContent): void { 47 | 48 | $cli->confirmToContinue('This will delete all fields from content files that are not defined in blueprints, no matter if they contain content or not. Are you sure?'); 49 | 50 | $kirby = $cli->kirby(); 51 | 52 | // Authenticate as almighty 53 | $kirby->impersonate('kirby'); 54 | 55 | // set the fields to be ignored 56 | $ignore = ['uuid', 'title', 'slug', 'template', 'sort', 'focus']; 57 | 58 | // call the script for all languages if multilang 59 | if ($kirby->multilang() === true) { 60 | $languages = $kirby->languages(); 61 | 62 | foreach ($languages as $language) { 63 | // should call kirby models for each loop 64 | // since generators cannot be cloned 65 | // otherwise run into an exception 66 | $collection = $kirby->models(); 67 | 68 | $cleanContent($collection, $ignore, $language->code()); 69 | } 70 | 71 | } else { 72 | $collection = $kirby->models(); 73 | $cleanContent($collection, $ignore); 74 | } 75 | 76 | $cli->success('The content files have been cleaned'); 77 | } 78 | ]; 79 | -------------------------------------------------------------------------------- /commands/clear/cache.php: -------------------------------------------------------------------------------- 1 | 'Clears the cache', 9 | 'args' => [ 10 | 'name' => [ 11 | 'description' => 'The name of the cache', 12 | ] 13 | ], 14 | 'command' => static function (CLI $cli): void { 15 | $kirby = $cli->kirby(); 16 | $name = $cli->argOrPrompt('name', 'Which cache should be emptied? (press to clear the pages cache)', false); 17 | $name = empty($name) === true ? 'pages' : $name; 18 | 19 | $kirby->cache($name)->flush(); 20 | 21 | $cli->success('The cache has been cleared'); 22 | } 23 | ]; 24 | -------------------------------------------------------------------------------- /commands/clear/lock.php: -------------------------------------------------------------------------------- 1 | 'Deletes the content `.lock` files', 10 | 'command' => static function (CLI $cli): void { 11 | $path = $cli->kirby()->root('content'); 12 | $directoryIterator = new RecursiveDirectoryIterator($path); 13 | $iterator = new RecursiveIteratorIterator($directoryIterator); 14 | $counter = 0; 15 | 16 | foreach ($iterator as $file) { 17 | if ($file->getFilename() === '.lock') { 18 | F::remove($file->getPathName()); 19 | $counter++; 20 | } 21 | } 22 | 23 | $cli->success($counter . ' lock file(s) have been deleted'); 24 | } 25 | ]; 26 | -------------------------------------------------------------------------------- /commands/clear/logins.php: -------------------------------------------------------------------------------- 1 | 'Deletes the users `.logins` file', 10 | 'command' => static function (CLI $cli): void { 11 | F::remove($cli->kirby()->root('accounts') . '/.logins'); 12 | 13 | $cli->success('The .logins file has been deleted'); 14 | } 15 | ]; 16 | -------------------------------------------------------------------------------- /commands/clear/media.php: -------------------------------------------------------------------------------- 1 | 'Deletes the media folder', 10 | 'command' => static function (CLI $cli): void { 11 | Dir::remove($cli->kirby()->root('media')); 12 | 13 | $cli->success('The media folder has been deleted'); 14 | } 15 | ]; 16 | -------------------------------------------------------------------------------- /commands/clear/sessions.php: -------------------------------------------------------------------------------- 1 | 'Destroys all sessions', 10 | 'command' => static function (CLI $cli): void { 11 | Dir::remove($cli->kirby()->root('sessions')); 12 | 13 | $cli->success('The sessions have been destroyed'); 14 | } 15 | ]; 16 | -------------------------------------------------------------------------------- /commands/download.php: -------------------------------------------------------------------------------- 1 | 'Downloads a file via URL', 10 | 'args' => [ 11 | 'url' => [ 12 | 'description' => 'The URL to the file', 13 | 'required' => true 14 | ], 15 | 'file' => [ 16 | 'description' => 'Where to save the download', 17 | 'required' => true 18 | ], 19 | ], 20 | 'command' => static function (CLI $cli): void { 21 | $client = new Client(); 22 | $progress = $cli->progress()->total(100); 23 | $file = $cli->arg('file'); 24 | 25 | try { 26 | $response = $client->get($cli->arg('url'), [ 27 | 'progress' => function ($total, $downloaded) use ($cli, $progress): void { 28 | try { 29 | if ($total > 0 && $downloaded > 0) { 30 | $progress->total($total); 31 | $progress->current($downloaded, ''); 32 | } 33 | } catch (Throwable $e) { 34 | $cli->out($e->getMessage()); 35 | } 36 | }, 37 | ]); 38 | 39 | file_put_contents($file, (string)$response->getBody()); 40 | } catch (Throwable $e) { 41 | throw new Exception('The file could not be downloaded. (Status: ' . $e->getResponse()->getStatusCode() . ')'); 42 | } 43 | } 44 | ]; 45 | -------------------------------------------------------------------------------- /commands/help.php: -------------------------------------------------------------------------------- 1 | 'Prints help for the Kirby CLI', 9 | 'command' => static function (CLI $cli): void { 10 | $commands = $cli->commands(); 11 | 12 | $cli->bold('Kirby CLI ' . $cli->version()); 13 | $cli->br(); 14 | $cli->out('Core commands:'); 15 | 16 | foreach ($commands['core'] as $command) { 17 | $cli->out('- kirby ' . $command); 18 | } 19 | 20 | if (count($commands['global']) > 0) { 21 | $cli->br(); 22 | $cli->out('Global commands:'); 23 | 24 | foreach ($commands['global'] as $command) { 25 | $cli->out('- kirby ' . $command); 26 | } 27 | } 28 | 29 | if (count($commands['custom']) > 0) { 30 | $cli->br(); 31 | $cli->out('Custom commands:'); 32 | 33 | foreach ($commands['custom'] as $command) { 34 | $cli->out('- kirby ' . $command); 35 | } 36 | } 37 | 38 | if (count($commands['plugins']) > 0) { 39 | $cli->br(); 40 | $cli->out('Plugin commands:'); 41 | 42 | foreach ($commands['plugins'] as $command) { 43 | $cli->out('- kirby ' . $command); 44 | } 45 | } 46 | 47 | $cli->br(); 48 | 49 | $cli->success('Have fun with the Kirby CLI!'); 50 | } 51 | ]; 52 | -------------------------------------------------------------------------------- /commands/install.php: -------------------------------------------------------------------------------- 1 | 'Installs the kirby folder', 9 | 'args' => [ 10 | 'version' => [ 11 | 'description' => 'The version corresponding with the tag name in the repo', 12 | 'defaultValue' => 'latest' 13 | ] 14 | ], 15 | 'command' => static function (CLI $cli): void { 16 | $cli->out('Installing Kirby (' . $cli->arg('version') . ') …'); 17 | $cli->run('install:repo', 'getkirby/kirby', 'kirby', '--version=' . $cli->arg('version')); 18 | $cli->success('Kirby has been installed'); 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /commands/install/kit.php: -------------------------------------------------------------------------------- 1 | 'Installs a Kirby Kit in a subfolder', 9 | 'args' => [ 10 | 'kit' => [ 11 | 'description' => 'The name of the kit (starterkit, demokit, plainkit)', 12 | ], 13 | 'folder' => [ 14 | 'description' => 'The name of folder the kit should be installed into', 15 | ] 16 | ], 17 | 'command' => static function (CLI $cli): void { 18 | $kit = $cli->arg('kit'); 19 | 20 | if (empty($kit) === true) { 21 | $input = $cli->radio('Which kit do you want to install?', [ 22 | 'starterkit', 23 | 'plainkit', 24 | 'demokit', 25 | ]); 26 | 27 | $kit = $input->prompt(); 28 | } 29 | 30 | $kit ??= 'starterkit'; 31 | $folder = $cli->argOrPrompt('folder', 'Enter a folder name (press to use "' . $kit . '")', false); 32 | $title = ucfirst($kit); 33 | 34 | if (empty($folder) === true) { 35 | $folder = $kit; 36 | } 37 | 38 | $cli->out('Installing Kirby ' . $title . ' …'); 39 | $cli->run('install:repo', 'getkirby/' . $kit, $folder); 40 | $cli->success('The Kirby ' . $title . ' has been installed'); 41 | } 42 | ]; 43 | -------------------------------------------------------------------------------- /commands/install/repo.php: -------------------------------------------------------------------------------- 1 | 'Downloads a repository from the getkirby org on Github', 9 | 'args' => [ 10 | 'repo' => [ 11 | 'description' => 'The Github repo path (i.e. getkirby/kirby)', 12 | 'required' => true 13 | ], 14 | 'folder' => [ 15 | 'description' => 'The name of folder the repo should be installed into', 16 | ], 17 | 'version' => [ 18 | 'prefix' => 'v', 19 | 'longPrefix' => 'version', 20 | 'description' => 'The version corresponding with the tag name in the repo', 21 | 'defaultValue' => 'latest' 22 | ] 23 | ], 24 | 'command' => static function (CLI $cli): void { 25 | $repo = $cli->arg('repo'); 26 | $folder = $cli->arg('folder'); 27 | 28 | if (empty($folder) === true) { 29 | $folder = basename($repo); 30 | } 31 | 32 | $archive = 'https://github.com/' . $repo . '/archive'; 33 | 34 | if ($cli->arg('version') === 'latest') { 35 | $url = $archive . '/main.zip'; 36 | } else { 37 | $url = $archive . '/refs/tags/' . $cli->arg('version') . '.zip'; 38 | } 39 | 40 | $zip = $cli->dir() . '/' . $folder . '-' . time() . '.zip'; 41 | $dir = $cli->dir() . '/' . $folder; 42 | 43 | $cli->confirmToDelete($zip, 'The zip file exists. Do you want to delete it?'); 44 | $cli->confirmToDelete($dir, 'The directory exists. Do you want to delete it?'); 45 | 46 | // download the zip file 47 | $cli->run('download', $url, $zip); 48 | 49 | // unzip the repo 50 | $cli->run('unzip', $zip, $dir); 51 | 52 | // remove the zip 53 | unlink($zip); 54 | } 55 | ]; 56 | -------------------------------------------------------------------------------- /commands/make/_templates/collection.php: -------------------------------------------------------------------------------- 1 | children(); 5 | }; 6 | -------------------------------------------------------------------------------- /commands/make/_templates/command.php: -------------------------------------------------------------------------------- 1 | 'Nice command', 5 | 'args' => [], 6 | 'command' => static function ($cli): void { 7 | $cli->success('Nice command!'); 8 | } 9 | ]; 10 | -------------------------------------------------------------------------------- /commands/make/_templates/config.php: -------------------------------------------------------------------------------- 1 | true 5 | ]; 6 | -------------------------------------------------------------------------------- /commands/make/_templates/controller.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /commands/make/_templates/template.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /commands/make/blueprint.php: -------------------------------------------------------------------------------- 1 | 'Creates a new blueprint file in site/blueprints', 9 | 'args' => [ 10 | 'name' => [ 11 | 'description' => 'The name of the blueprint', 12 | ] 13 | ], 14 | 'command' => static function (CLI $cli): void { 15 | $kirby = $cli->kirby(); 16 | $name = $cli->argOrPrompt('name', 'Enter a name for the blueprint:'); 17 | $file = $kirby->root('blueprints') . '/' . $name . '.yml'; 18 | 19 | $cli->make($file, 'title: {{ title }}', [ 20 | 'title' => ucfirst(basename($name)) 21 | ]); 22 | 23 | $cli->success('The blueprint has been created'); 24 | } 25 | ]; 26 | -------------------------------------------------------------------------------- /commands/make/collection.php: -------------------------------------------------------------------------------- 1 | 'Creates a new collection in site/collections', 9 | 'args' => [ 10 | 'name' => [ 11 | 'description' => 'The name of the collection', 12 | ] 13 | ], 14 | 'command' => static function (CLI $cli): void { 15 | $kirby = $cli->kirby(); 16 | $name = $cli->argOrPrompt('name', 'Enter a name for the collection:'); 17 | $file = $kirby->root('collections') . '/' . $name . '.php'; 18 | 19 | $cli->make($file, __DIR__ . '/_templates/collection.php'); 20 | 21 | $cli->success('The collection has been created'); 22 | } 23 | ]; 24 | -------------------------------------------------------------------------------- /commands/make/command.php: -------------------------------------------------------------------------------- 1 | 'Creates a new local command for the Kirby CLI', 9 | 'args' => [ 10 | 'name' => [ 11 | 'description' => 'The name of the command', 12 | ], 13 | 'global' => [ 14 | 'prefix' => 'g', 15 | 'longPrefix' => 'global', 16 | 'description' => 'Install the command globally', 17 | 'noValue' => true 18 | ] 19 | ], 20 | 'command' => static function (CLI $cli): void { 21 | $name = $cli->argOrPrompt('name', 'Enter a name for the command:'); 22 | $name = str_replace(':', '/', $name); 23 | 24 | $root = $cli->arg('global') === true ? 'commands.global' : 'commands.local'; 25 | $file = $cli->root($root) . '/' . $name . '.php'; 26 | 27 | $cli->make($file, __DIR__ . '/_templates/command.php'); 28 | $cli->success('The command has been created: ' . $file); 29 | } 30 | ]; 31 | -------------------------------------------------------------------------------- /commands/make/config.php: -------------------------------------------------------------------------------- 1 | 'Creates a new config file in site/config', 9 | 'args' => [ 10 | 'domain' => [ 11 | 'description' => 'An optional domain for a multi-environment config', 12 | ] 13 | ], 14 | 'command' => static function (CLI $cli): void { 15 | $kirby = $cli->kirby(); 16 | $domain = $cli->arg('domain'); 17 | $name = empty($domain) === false ? 'config.' . $domain : 'config'; 18 | $file = $kirby->root('config') . '/' . $name . '.php'; 19 | 20 | $cli->make($file, __DIR__ . '/_templates/config.php'); 21 | 22 | $cli->success('The config "' . basename($file) . '" has been created'); 23 | } 24 | ]; 25 | -------------------------------------------------------------------------------- /commands/make/controller.php: -------------------------------------------------------------------------------- 1 | 'Creates a new template controller in site/controllers', 9 | 'args' => [ 10 | 'name' => [ 11 | 'description' => 'The name of the controller', 12 | ] 13 | ], 14 | 'command' => static function (CLI $cli): void { 15 | $kirby = $cli->kirby(); 16 | $name = $cli->argOrPrompt('name', 'Enter a name for the controller:'); 17 | $file = $kirby->root('controllers') . '/' . $name . '.php'; 18 | 19 | $cli->make($file, __DIR__ . '/_templates/controller.php'); 20 | 21 | $cli->success('The controller has been created'); 22 | } 23 | ]; 24 | -------------------------------------------------------------------------------- /commands/make/language.php: -------------------------------------------------------------------------------- 1 | 'Creates a new language', 10 | 'args' => [ 11 | 'code' => [ 12 | 'description' => 'The code of the language' 13 | ], 14 | 'name' => [ 15 | 'description' => 'The name of the language' 16 | ], 17 | 'locale' => [ 18 | 'description' => 'The locale of the language' 19 | ], 20 | 'direction' => [ 21 | 'description' => 'The direction of the language' 22 | ] 23 | ], 24 | 'command' => static function (CLI $cli): void { 25 | $kirby = $cli->kirby(); 26 | $code = $cli->argOrPrompt('code', 'Enter a language code:'); 27 | $name = $cli->argOrPrompt('name', 'Enter a language name (optional):', false); 28 | $locale = $cli->argOrPrompt('locale', 'Enter a language locale (optional):', false); 29 | $direction = $cli->radio('Select language direction:', ['ltr', 'rtl'])->prompt(); 30 | 31 | // authenticate as almighty 32 | $kirby->impersonate('kirby'); 33 | 34 | Language::create([ 35 | 'code' => $code, 36 | 'name' => empty($name) === false ? $name : $code, 37 | 'locale' => $locale, 38 | 'direction' => $direction, 39 | 'default' => $kirby->languages()->count() === 0, 40 | ]); 41 | 42 | $cli->success('The language has been created'); 43 | } 44 | ]; 45 | -------------------------------------------------------------------------------- /commands/make/model.php: -------------------------------------------------------------------------------- 1 | 'Creates a new page model in site/models', 9 | 'args' => [ 10 | 'name' => [ 11 | 'description' => 'The name of the model', 12 | ] 13 | ], 14 | 'command' => static function (CLI $cli): void { 15 | $kirby = $cli->kirby(); 16 | $name = $cli->argOrPrompt('name', 'Enter a name for the model:'); 17 | $name = lcfirst($name); 18 | $file = $kirby->root('models') . '/' . $name . '.php'; 19 | 20 | $cli->make($file, __DIR__ . '/_templates/model.php', [ 21 | 'className' => ucfirst($name) 22 | ]); 23 | 24 | $cli->success('The model has been created'); 25 | } 26 | ]; 27 | -------------------------------------------------------------------------------- /commands/make/plugin.php: -------------------------------------------------------------------------------- 1 | 'Creates a new plugin in site/plugins', 9 | 'args' => [ 10 | 'name' => [ 11 | 'description' => 'The name of the plugin (`vendor/plugin`)', 12 | ] 13 | ], 14 | 'command' => static function (CLI $cli): void { 15 | $kirby = $cli->kirby(); 16 | $name = $cli->argOrPrompt('name', 'Enter a name (`vendor/plugin`) for the snippet:'); 17 | $name = lcfirst($name); 18 | $file = $kirby->root('plugins') . '/' . basename($name) . '/index.php'; 19 | 20 | $cli->make($file, __DIR__ . '/_templates/plugin.php', [ 21 | 'name' => $name 22 | ]); 23 | 24 | $cli->success('The plugin has been created'); 25 | } 26 | ]; 27 | -------------------------------------------------------------------------------- /commands/make/snippet.php: -------------------------------------------------------------------------------- 1 | 'Creates a new snippet in site/snippets', 9 | 'args' => [ 10 | 'name' => [ 11 | 'description' => 'The name of the snippet', 12 | ] 13 | ], 14 | 'command' => static function (CLI $cli): void { 15 | $kirby = $cli->kirby(); 16 | $name = $cli->argOrPrompt('name', 'Enter a name for the snippet:'); 17 | $name = lcfirst($name); 18 | $file = $kirby->root('snippets') . '/' . $name . '.php'; 19 | 20 | $cli->make($file, __DIR__ . '/_templates/snippet.php', [ 21 | 'name' => $name 22 | ]); 23 | 24 | $cli->success('The snippet has been created'); 25 | } 26 | ]; 27 | -------------------------------------------------------------------------------- /commands/make/template.php: -------------------------------------------------------------------------------- 1 | 'Creates a new template in site/templates', 9 | 'args' => [ 10 | 'name' => [ 11 | 'description' => 'The name of the template', 12 | ] 13 | ], 14 | 'command' => static function (CLI $cli): void { 15 | $kirby = $cli->kirby(); 16 | $name = $cli->argOrPrompt('name', 'Enter a name for the template:'); 17 | $name = lcfirst(basename($name)); 18 | $file = $kirby->root('templates') . '/' . $name . '.php'; 19 | 20 | $cli->make($file, __DIR__ . '/_templates/template.php'); 21 | 22 | $cli->success('The template has been created'); 23 | } 24 | ]; 25 | -------------------------------------------------------------------------------- /commands/make/user.php: -------------------------------------------------------------------------------- 1 | 'Creates a new user', 10 | 'args' => [ 11 | 'email' => [ 12 | 'description' => 'The email of the user' 13 | ], 14 | 'role' => [ 15 | 'description' => 'The role of the user' 16 | ], 17 | 'name' => [ 18 | 'description' => 'The name of the user' 19 | ], 20 | 'language' => [ 21 | 'description' => 'The language of the user', 22 | ], 23 | 'password' => [ 24 | 'description' => 'The password of the user' 25 | ] 26 | ], 27 | 'command' => static function (CLI $cli): void { 28 | $kirby = $cli->kirby(); 29 | $email = $cli->argOrPrompt('email', 'Enter an email:'); 30 | $password = $cli->argOrPrompt('password', 'Enter a password (Leave empty for passwordless login methods):', false); 31 | $role = $cli->radio('Select a user role:', $kirby->roles()->pluck('id'))->prompt(); 32 | $name = $cli->argOrPrompt('name', 'Enter a name (optional):', false); 33 | $language = $cli->argOrPrompt('language', 'Enter a language code (Leave empty to use default EN):', false); 34 | 35 | $data = [ 36 | 'email' => $email, 37 | 'name' => $name, 38 | 'role' => $role, 39 | 'language' => empty($language) === false ? strtolower($language) : 'en' 40 | ]; 41 | 42 | if (empty($password) === false) { 43 | $data['password'] = $password; 44 | } 45 | 46 | // authenticate as almighty 47 | $kirby->impersonate('kirby'); 48 | 49 | $user = User::create($data); 50 | 51 | $cli->success('The user has been created. The new user id: ' . $user->id()); 52 | } 53 | ]; 54 | -------------------------------------------------------------------------------- /commands/plugin/install.php: -------------------------------------------------------------------------------- 1 | 'Installs a Kirby plugin repository from Github', 10 | 'args' => [ 11 | 'repo' => [ 12 | 'description' => 'The Github repo path (i.e. getkirby/kql)', 13 | 'required' => true 14 | ], 15 | 'version' => [ 16 | 'description' => 'The version corresponding with the tag name in the repo', 17 | 'defaultValue' => 'latest' 18 | ] 19 | ], 20 | 'command' => static function (CLI $cli): void { 21 | $repo = $cli->arg('repo'); 22 | $version = $cli->arg('version'); 23 | $archiveUrl = 'https://github.com/' . $repo . '/archive'; 24 | $composerUrl = 'https://github.com/' . $repo . '/raw/HEAD/composer.json'; 25 | 26 | // make sure only `kirby-plugin` installable 27 | $composer = json_decode(file_get_contents($composerUrl)); 28 | if (($composer?->type ?? null) !== 'kirby-plugin') { 29 | throw new Exception('The GitHub repository should be a Kirby plugin'); 30 | } 31 | 32 | if ($version === 'latest') { 33 | $url = $archiveUrl . '/main.zip'; 34 | } else { 35 | $url = $archiveUrl . '/refs/tags/' . $cli->arg('version') . '.zip'; 36 | } 37 | 38 | list($vendor, $plugin) = explode('/', $repo); 39 | 40 | $zip = $cli->dir() . '/' . $vendor . '-' . $plugin . '-' . time() . '.zip'; 41 | $dir = $cli->kirby()->root('plugins') . '/' . $plugin; 42 | 43 | $cli->confirmToDelete($zip, 'The zip file exists. Do you want to delete it?'); 44 | $cli->confirmToDelete($dir, 'The directory exists. Do you want to delete it?'); 45 | 46 | $cli->out('Installing ' . $repo . ' plugin …'); 47 | 48 | // download the zip file 49 | $cli->run('download', $url, $zip); 50 | 51 | // unzip the repo 52 | $cli->run('unzip', $zip, $dir); 53 | 54 | // remove the zip 55 | F::unlink($zip); 56 | 57 | $cli->success('The ' . $repo . ' plugin has been installed'); 58 | } 59 | ]; 60 | -------------------------------------------------------------------------------- /commands/plugin/remove.php: -------------------------------------------------------------------------------- 1 | 'Removes a Kirby plugin', 10 | 'args' => [ 11 | 'repo' => [ 12 | 'description' => 'The Kirby plugin registry name (i.e. getkirby/kql)', 13 | 'required' => true 14 | ] 15 | ], 16 | 'command' => static function (CLI $cli): void { 17 | $repo = $cli->arg('repo'); 18 | 19 | if ($plugin = $cli->kirby()->plugin($repo)) { 20 | Dir::remove($plugin->root()); 21 | $cli->success('The ' . $repo . ' plugin has been removed'); 22 | } else { 23 | $cli->error('The ' . $repo . ' plugin could not be found'); 24 | } 25 | } 26 | ]; 27 | -------------------------------------------------------------------------------- /commands/plugin/upgrade.php: -------------------------------------------------------------------------------- 1 | 'Upgrades a Kirby plugin', 10 | 'args' => [ 11 | 'repo' => [ 12 | 'description' => 'The Kirby plugin registry name (i.e. getkirby/kql)', 13 | 'required' => true 14 | ], 15 | 'version' => [ 16 | 'description' => 'The version corresponding with the tag name in the repo', 17 | 'defaultValue' => 'latest' 18 | ] 19 | ], 20 | 'command' => static function (CLI $cli): void { 21 | $repo = $cli->arg('repo'); 22 | $version = $cli->arg('version'); 23 | 24 | if ($plugin = $cli->kirby()->plugin($repo)) { 25 | try { 26 | // move plugin directory to prevent overwrite 27 | Dir::move($plugin->root(), $plugin->root() . '.bak'); 28 | $cli->run('plugin:install', $repo, $version); 29 | Dir::remove($plugin->root() . '.bak'); 30 | $cli->success('The ' . $repo . ' plugin has been updated to ' . $version . ' version'); 31 | } catch (Throwable) { 32 | Dir::move($plugin->root() . '.bak', $plugin->root()); 33 | $cli->error('The ' . $repo . ' plugin could not updated'); 34 | } 35 | } else { 36 | $cli->error('The ' . $repo . ' plugin could not found'); 37 | } 38 | } 39 | ]; 40 | -------------------------------------------------------------------------------- /commands/register.php: -------------------------------------------------------------------------------- 1 | 'Registers the installation', 9 | 'args' => [ 10 | 'email' => [ 11 | 'prefix' => 'e', 12 | 'longPrefix' => 'email', 13 | 'description' => 'The email address you’ve used to purchase the license', 14 | ], 15 | 'license' => [ 16 | 'prefix' => 'l', 17 | 'longPrefix' => 'license', 18 | 'description' => 'Your Kirby 3 license key', 19 | ] 20 | ], 21 | 'command' => static function (CLI $cli): void { 22 | $kirby = $cli->kirby(); 23 | $license = $cli->argOrPrompt('license', 'Enter your license key:'); 24 | $email = $cli->argOrPrompt('email', 'Enter your email address:'); 25 | 26 | $kirby->system()->register($license, $email); 27 | 28 | $cli->success('Your installation has been registered'); 29 | } 30 | ]; 31 | -------------------------------------------------------------------------------- /commands/remove/command.php: -------------------------------------------------------------------------------- 1 | 'Removes a custom command', 9 | 'args' => [ 10 | 'name' => [ 11 | 'description' => 'The name of the command', 12 | ] 13 | ], 14 | 'command' => static function (CLI $cli): void { 15 | $name = $cli->argOrPrompt('name', 'Enter a name for the command:'); 16 | $name = str_replace(':', '/', $name); 17 | 18 | $global = $cli->root('commands.global') . '/' . $name . '.php'; 19 | $local = $cli->root('commands.local') . '/' . $name . '.php'; 20 | 21 | $files = []; 22 | 23 | if (is_file($global) === true) { 24 | $files[] = $global; 25 | } 26 | 27 | if (is_file($local) === true) { 28 | $files[] = $local; 29 | } 30 | 31 | if (count($files) > 1) { 32 | $input = $cli->checkboxes('Which commands do you want to delete?:', $files); 33 | $trash = $input->prompt(); 34 | } else { 35 | $trash = $files; 36 | } 37 | 38 | foreach ($trash as $file) { 39 | unlink($file); 40 | } 41 | 42 | if (count($trash) === 1) { 43 | $cli->success('The command has been deleted'); 44 | } else { 45 | $cli->success('The commands have been deleted'); 46 | } 47 | } 48 | ]; 49 | -------------------------------------------------------------------------------- /commands/roots.php: -------------------------------------------------------------------------------- 1 | 'Shows a list with all configured roots', 9 | 'command' => static function (CLI $cli): void { 10 | $cli->dump($cli->roots()); 11 | } 12 | ]; 13 | -------------------------------------------------------------------------------- /commands/security.php: -------------------------------------------------------------------------------- 1 | 'Performs security checks of the site', 12 | 'command' => static function (CLI $cli): void { 13 | $kirby = $cli->kirby(); 14 | $system = $kirby->system(); 15 | $updateStatus = $system->updateStatus(); 16 | $messages = [ 17 | ...array_column($updateStatus?->messages() ?? [], 'text'), 18 | ...$updateStatus->exceptionMessages() 19 | ]; 20 | 21 | if ($kirby->option('debug', false) === true) { 22 | $messages[] = I18n::translate('system.issues.debug'); 23 | } 24 | 25 | if ($kirby->environment()->https() !== true) { 26 | $messages[] = I18n::translate('system.issues.https'); 27 | } 28 | 29 | // checks exposable urls of the site 30 | // works only site url is absolute since can't get it in CLI mode 31 | // and CURL won't work for relative urls 32 | if (Url::isAbsolute($kirby->url())) { 33 | $urls = [ 34 | 'content' => $system->exposedFileUrl('content'), 35 | 'git' => $system->exposedFileUrl('git'), 36 | 'kirby' => $system->exposedFileUrl('kirby'), 37 | 'site' => $system->exposedFileUrl('site') 38 | ]; 39 | 40 | foreach ($urls as $key => $url) { 41 | if (empty($url) === false && Remote::get($url)->code() < 400) { 42 | $messages[] = I18n::translate('system.issues.' . $key); 43 | } 44 | } 45 | } else { 46 | $messages[] = 'Could not check for exposed folders as the site URL is not absolute'; 47 | } 48 | 49 | if (empty($messages) === false) { 50 | foreach ($messages as $message) { 51 | $cli->error('> ' . $message); 52 | } 53 | } else { 54 | $cli->success('Basic security checks were successful, please review https://getkirby.com/docs/guide/security for additional best practices.'); 55 | } 56 | } 57 | ]; 58 | -------------------------------------------------------------------------------- /commands/unzip.php: -------------------------------------------------------------------------------- 1 | 'Extracts a zip file', 9 | 'args' => [ 10 | 'file' => [ 11 | 'description' => 'The file to unzip', 12 | 'required' => true 13 | ], 14 | 'to' => [ 15 | 'description' => 'The place to extract the zip to', 16 | 'required' => true 17 | ] 18 | ], 19 | 'command' => static function (CLI $cli): void { 20 | if (class_exists('ZipArchive') === false) { 21 | throw new Exception('ZipArchive library could not be found'); 22 | } 23 | 24 | $file = $cli->arg('file'); 25 | $to = $cli->arg('to'); 26 | $temp = $to . '.temp'; 27 | 28 | if (is_file($file) === false) { 29 | throw new Exception('The ZIP file does not exist'); 30 | } 31 | 32 | if (is_dir($to) === true || is_dir($temp) === true) { 33 | throw new Exception('The target directory exists'); 34 | } 35 | 36 | // extract the zip file to the temp directory 37 | // to move temp directory to target directory 38 | // since there is not a php native function to move entire directory into parent 39 | $zipArchive = new ZipArchive(); 40 | if ($zipArchive->open($file) === true) { 41 | $zipArchive->extractTo($temp); 42 | $zipArchive->close(); 43 | } else { 44 | throw new Exception('The zip file could not read'); 45 | } 46 | 47 | $temp = realpath($temp); 48 | 49 | // target path doesn't exist yet and realpath won't work for it. So use temp path. 50 | $to = substr($temp, 0, strlen($temp) - strlen('.temp')); 51 | 52 | // find the archive folder in that tmp dir 53 | $archive = glob($temp . '/*', GLOB_ONLYDIR)[0] ?? null; 54 | 55 | if (is_dir($archive) === false) { 56 | throw new Exception('The archive directory could not be found'); 57 | } 58 | 59 | rename($archive, $to); 60 | $cli->rmdir($to . '.temp'); 61 | } 62 | ]; 63 | -------------------------------------------------------------------------------- /commands/upgrade.php: -------------------------------------------------------------------------------- 1 | 'Upgrades the Kirby core', 10 | 'args' => [ 11 | 'version' => [ 12 | 'description' => 'The version corresponding with the tag name in the kirby repo', 13 | 'defaultValue' => 'latest' 14 | ] 15 | ], 16 | 'command' => static function (CLI $cli): void { 17 | $version = $cli->arg('version'); 18 | $kirby = $cli->kirby(); 19 | $kirbyRoot = $kirby->root('kirby'); 20 | $folder = 'kirby.' . $version; 21 | 22 | // if entered version is `latest` 23 | // get exact version to compare current kirby version 24 | if ($version === 'latest') { 25 | $release = json_decode(file_get_contents('https://getkirby.com/security.json')); 26 | $version = $release->latest; 27 | } 28 | 29 | // checks if the current kirby version is the same as the new one 30 | if (version_compare($version, $kirby->version(), '==') === true) { 31 | $cli->success('Your Kirby installation is already up to date'); 32 | exit; 33 | } 34 | 35 | // checks current kirby version and prevents downgrade 36 | if (version_compare($kirby->version(), $version, '>') === true) { 37 | throw new Exception('Your current Kirby version is higher than the version you are trying to upgrade to'); 38 | } 39 | 40 | // confirms the process when major version upgrade available 41 | if ((int)$version > (int)$kirby->version()) { 42 | $confirm = $cli->confirm('Major version upgrade detected. Are you sure you want to proceed?'); 43 | 44 | if ($confirm->confirmed() === false) { 45 | throw new Exception('Major version upgrade has been canceled'); 46 | } 47 | } 48 | 49 | $cli->out('Upgrading Kirby from ' . $kirby->version() . ' to ' . $version . ' …'); 50 | $cli->run('install:repo', 'getkirby/kirby', $folder, '--version=' . $version); 51 | 52 | // move current kirby to temp directory as backup 53 | Dir::move($kirbyRoot, $kirbyRoot . '.old'); 54 | 55 | // move new kirby to current root 56 | Dir::move($cli->dir() . '/' . $folder, $kirbyRoot); 57 | 58 | // delete old kirby 59 | Dir::remove($kirbyRoot . '.old'); 60 | 61 | // delete temp panel directory 62 | Dir::remove($kirby->root('media') . '/panel'); 63 | 64 | $cli->success('Kirby has been upgraded to ' . $version); 65 | } 66 | ]; 67 | -------------------------------------------------------------------------------- /commands/uuid/generate.php: -------------------------------------------------------------------------------- 1 | 'Creates all missing UUIDs', 10 | 'command' => static function (CLI $cli): void { 11 | $kirby = $cli->kirby(); 12 | 13 | if (version_compare($kirby->version(), '3.7.9', '<=') === true) { 14 | $cli->error('UUIDs are not available in your Kirby version. Please upgrade to 3.8.0'); 15 | return; 16 | } 17 | 18 | Uuids::generate(); 19 | 20 | $cli->success('UUIDs have been created'); 21 | } 22 | ]; 23 | -------------------------------------------------------------------------------- /commands/uuid/populate.php: -------------------------------------------------------------------------------- 1 | 'Populats cache for all UUIDs', 10 | 'command' => static function (CLI $cli): void { 11 | $kirby = $cli->kirby(); 12 | 13 | if (version_compare($kirby->version(), '3.7.9', '<=') === true) { 14 | $cli->error('UUIDs are not available in your Kirby version. Please upgrade to 3.8.0'); 15 | return; 16 | } 17 | 18 | Uuids::populate(); 19 | 20 | $cli->success('UUID cache has been populated'); 21 | } 22 | ]; 23 | -------------------------------------------------------------------------------- /commands/uuid/remove.php: -------------------------------------------------------------------------------- 1 | 'Removes all UUIDs', 10 | 'command' => static function (CLI $cli): void { 11 | $cli->kirby(); 12 | 13 | Uuids::each( 14 | function ($model) { 15 | $model->save(['uuid' => null]); 16 | } 17 | ); 18 | 19 | Uuids::cache()->flush(); 20 | 21 | $cli->success('All UUIDs have been removed'); 22 | } 23 | ]; 24 | -------------------------------------------------------------------------------- /commands/version.php: -------------------------------------------------------------------------------- 1 | 'Prints the Kirby version', 9 | 'command' => static function (CLI $cli): void { 10 | $kirby = $cli->kirby(); 11 | $cli->success($kirby->version()); 12 | } 13 | ]; 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getkirby/cli", 3 | "description": "Kirby command line interface", 4 | "license": "MIT", 5 | "version": "1.6.0", 6 | "keywords": [ 7 | "kirby", 8 | "cms", 9 | "cli", 10 | "command" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Kirby Team", 15 | "email": "support@getkirby.com", 16 | "homepage": "https://getkirby.com" 17 | } 18 | ], 19 | "homepage": "https://getkirby.com", 20 | "support": { 21 | "email": "support@getkirby.com", 22 | "issues": "https://github.com/getkirby/cli/issues", 23 | "forum": "https://forum.getkirby.com", 24 | "source": "https://github.com/getkirby/cli" 25 | }, 26 | "require": { 27 | "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", 28 | "ext-zip": "*", 29 | "composer-runtime-api": "^2.2", 30 | "guzzlehttp/guzzle": "^7.9.2", 31 | "league/climate": "^3.10.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Kirby\\": [ 36 | "src/", 37 | "tests/" 38 | ] 39 | } 40 | }, 41 | "bin": [ 42 | "bin/kirby" 43 | ], 44 | "config": { 45 | "optimize-autoloader": true 46 | }, 47 | "scripts": { 48 | "analyze": [ 49 | "@analyze:composer", 50 | "@analyze:phpcpd", 51 | "@analyze:phpmd" 52 | ], 53 | "analyze:composer": "composer validate --strict --no-check-version --no-check-all", 54 | "analyze:phpcpd": "phpcpd --fuzzy --exclude tests --exclude vendor .", 55 | "analyze:phpmd": "phpmd . ansi phpmd.xml.dist --exclude 'tests/*,vendor/*,_templates/*'", 56 | "analyze:psalm": "psalm", 57 | "ci": [ 58 | "@fix", 59 | "@test" 60 | ], 61 | "fix": "php-cs-fixer fix", 62 | "test": "phpunit --stderr --coverage-html=tests/coverage" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "0f8fe4de12fd58407112d9344df28512", 8 | "packages": [ 9 | { 10 | "name": "guzzlehttp/guzzle", 11 | "version": "7.9.2", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/guzzle/guzzle.git", 15 | "reference": "d281ed313b989f213357e3be1a179f02196ac99b" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", 20 | "reference": "d281ed313b989f213357e3be1a179f02196ac99b", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "ext-json": "*", 25 | "guzzlehttp/promises": "^1.5.3 || ^2.0.3", 26 | "guzzlehttp/psr7": "^2.7.0", 27 | "php": "^7.2.5 || ^8.0", 28 | "psr/http-client": "^1.0", 29 | "symfony/deprecation-contracts": "^2.2 || ^3.0" 30 | }, 31 | "provide": { 32 | "psr/http-client-implementation": "1.0" 33 | }, 34 | "require-dev": { 35 | "bamarni/composer-bin-plugin": "^1.8.2", 36 | "ext-curl": "*", 37 | "guzzle/client-integration-tests": "3.0.2", 38 | "php-http/message-factory": "^1.1", 39 | "phpunit/phpunit": "^8.5.39 || ^9.6.20", 40 | "psr/log": "^1.1 || ^2.0 || ^3.0" 41 | }, 42 | "suggest": { 43 | "ext-curl": "Required for CURL handler support", 44 | "ext-intl": "Required for Internationalized Domain Name (IDN) support", 45 | "psr/log": "Required for using the Log middleware" 46 | }, 47 | "type": "library", 48 | "extra": { 49 | "bamarni-bin": { 50 | "bin-links": true, 51 | "forward-command": false 52 | } 53 | }, 54 | "autoload": { 55 | "files": [ 56 | "src/functions_include.php" 57 | ], 58 | "psr-4": { 59 | "GuzzleHttp\\": "src/" 60 | } 61 | }, 62 | "notification-url": "https://packagist.org/downloads/", 63 | "license": [ 64 | "MIT" 65 | ], 66 | "authors": [ 67 | { 68 | "name": "Graham Campbell", 69 | "email": "hello@gjcampbell.co.uk", 70 | "homepage": "https://github.com/GrahamCampbell" 71 | }, 72 | { 73 | "name": "Michael Dowling", 74 | "email": "mtdowling@gmail.com", 75 | "homepage": "https://github.com/mtdowling" 76 | }, 77 | { 78 | "name": "Jeremy Lindblom", 79 | "email": "jeremeamia@gmail.com", 80 | "homepage": "https://github.com/jeremeamia" 81 | }, 82 | { 83 | "name": "George Mponos", 84 | "email": "gmponos@gmail.com", 85 | "homepage": "https://github.com/gmponos" 86 | }, 87 | { 88 | "name": "Tobias Nyholm", 89 | "email": "tobias.nyholm@gmail.com", 90 | "homepage": "https://github.com/Nyholm" 91 | }, 92 | { 93 | "name": "Márk Sági-Kazár", 94 | "email": "mark.sagikazar@gmail.com", 95 | "homepage": "https://github.com/sagikazarmark" 96 | }, 97 | { 98 | "name": "Tobias Schultze", 99 | "email": "webmaster@tubo-world.de", 100 | "homepage": "https://github.com/Tobion" 101 | } 102 | ], 103 | "description": "Guzzle is a PHP HTTP client library", 104 | "keywords": [ 105 | "client", 106 | "curl", 107 | "framework", 108 | "http", 109 | "http client", 110 | "psr-18", 111 | "psr-7", 112 | "rest", 113 | "web service" 114 | ], 115 | "support": { 116 | "issues": "https://github.com/guzzle/guzzle/issues", 117 | "source": "https://github.com/guzzle/guzzle/tree/7.9.2" 118 | }, 119 | "funding": [ 120 | { 121 | "url": "https://github.com/GrahamCampbell", 122 | "type": "github" 123 | }, 124 | { 125 | "url": "https://github.com/Nyholm", 126 | "type": "github" 127 | }, 128 | { 129 | "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", 130 | "type": "tidelift" 131 | } 132 | ], 133 | "time": "2024-07-24T11:22:20+00:00" 134 | }, 135 | { 136 | "name": "guzzlehttp/promises", 137 | "version": "2.0.4", 138 | "source": { 139 | "type": "git", 140 | "url": "https://github.com/guzzle/promises.git", 141 | "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" 142 | }, 143 | "dist": { 144 | "type": "zip", 145 | "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", 146 | "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", 147 | "shasum": "" 148 | }, 149 | "require": { 150 | "php": "^7.2.5 || ^8.0" 151 | }, 152 | "require-dev": { 153 | "bamarni/composer-bin-plugin": "^1.8.2", 154 | "phpunit/phpunit": "^8.5.39 || ^9.6.20" 155 | }, 156 | "type": "library", 157 | "extra": { 158 | "bamarni-bin": { 159 | "bin-links": true, 160 | "forward-command": false 161 | } 162 | }, 163 | "autoload": { 164 | "psr-4": { 165 | "GuzzleHttp\\Promise\\": "src/" 166 | } 167 | }, 168 | "notification-url": "https://packagist.org/downloads/", 169 | "license": [ 170 | "MIT" 171 | ], 172 | "authors": [ 173 | { 174 | "name": "Graham Campbell", 175 | "email": "hello@gjcampbell.co.uk", 176 | "homepage": "https://github.com/GrahamCampbell" 177 | }, 178 | { 179 | "name": "Michael Dowling", 180 | "email": "mtdowling@gmail.com", 181 | "homepage": "https://github.com/mtdowling" 182 | }, 183 | { 184 | "name": "Tobias Nyholm", 185 | "email": "tobias.nyholm@gmail.com", 186 | "homepage": "https://github.com/Nyholm" 187 | }, 188 | { 189 | "name": "Tobias Schultze", 190 | "email": "webmaster@tubo-world.de", 191 | "homepage": "https://github.com/Tobion" 192 | } 193 | ], 194 | "description": "Guzzle promises library", 195 | "keywords": [ 196 | "promise" 197 | ], 198 | "support": { 199 | "issues": "https://github.com/guzzle/promises/issues", 200 | "source": "https://github.com/guzzle/promises/tree/2.0.4" 201 | }, 202 | "funding": [ 203 | { 204 | "url": "https://github.com/GrahamCampbell", 205 | "type": "github" 206 | }, 207 | { 208 | "url": "https://github.com/Nyholm", 209 | "type": "github" 210 | }, 211 | { 212 | "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", 213 | "type": "tidelift" 214 | } 215 | ], 216 | "time": "2024-10-17T10:06:22+00:00" 217 | }, 218 | { 219 | "name": "guzzlehttp/psr7", 220 | "version": "2.7.0", 221 | "source": { 222 | "type": "git", 223 | "url": "https://github.com/guzzle/psr7.git", 224 | "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" 225 | }, 226 | "dist": { 227 | "type": "zip", 228 | "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", 229 | "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", 230 | "shasum": "" 231 | }, 232 | "require": { 233 | "php": "^7.2.5 || ^8.0", 234 | "psr/http-factory": "^1.0", 235 | "psr/http-message": "^1.1 || ^2.0", 236 | "ralouphie/getallheaders": "^3.0" 237 | }, 238 | "provide": { 239 | "psr/http-factory-implementation": "1.0", 240 | "psr/http-message-implementation": "1.0" 241 | }, 242 | "require-dev": { 243 | "bamarni/composer-bin-plugin": "^1.8.2", 244 | "http-interop/http-factory-tests": "0.9.0", 245 | "phpunit/phpunit": "^8.5.39 || ^9.6.20" 246 | }, 247 | "suggest": { 248 | "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" 249 | }, 250 | "type": "library", 251 | "extra": { 252 | "bamarni-bin": { 253 | "bin-links": true, 254 | "forward-command": false 255 | } 256 | }, 257 | "autoload": { 258 | "psr-4": { 259 | "GuzzleHttp\\Psr7\\": "src/" 260 | } 261 | }, 262 | "notification-url": "https://packagist.org/downloads/", 263 | "license": [ 264 | "MIT" 265 | ], 266 | "authors": [ 267 | { 268 | "name": "Graham Campbell", 269 | "email": "hello@gjcampbell.co.uk", 270 | "homepage": "https://github.com/GrahamCampbell" 271 | }, 272 | { 273 | "name": "Michael Dowling", 274 | "email": "mtdowling@gmail.com", 275 | "homepage": "https://github.com/mtdowling" 276 | }, 277 | { 278 | "name": "George Mponos", 279 | "email": "gmponos@gmail.com", 280 | "homepage": "https://github.com/gmponos" 281 | }, 282 | { 283 | "name": "Tobias Nyholm", 284 | "email": "tobias.nyholm@gmail.com", 285 | "homepage": "https://github.com/Nyholm" 286 | }, 287 | { 288 | "name": "Márk Sági-Kazár", 289 | "email": "mark.sagikazar@gmail.com", 290 | "homepage": "https://github.com/sagikazarmark" 291 | }, 292 | { 293 | "name": "Tobias Schultze", 294 | "email": "webmaster@tubo-world.de", 295 | "homepage": "https://github.com/Tobion" 296 | }, 297 | { 298 | "name": "Márk Sági-Kazár", 299 | "email": "mark.sagikazar@gmail.com", 300 | "homepage": "https://sagikazarmark.hu" 301 | } 302 | ], 303 | "description": "PSR-7 message implementation that also provides common utility methods", 304 | "keywords": [ 305 | "http", 306 | "message", 307 | "psr-7", 308 | "request", 309 | "response", 310 | "stream", 311 | "uri", 312 | "url" 313 | ], 314 | "support": { 315 | "issues": "https://github.com/guzzle/psr7/issues", 316 | "source": "https://github.com/guzzle/psr7/tree/2.7.0" 317 | }, 318 | "funding": [ 319 | { 320 | "url": "https://github.com/GrahamCampbell", 321 | "type": "github" 322 | }, 323 | { 324 | "url": "https://github.com/Nyholm", 325 | "type": "github" 326 | }, 327 | { 328 | "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", 329 | "type": "tidelift" 330 | } 331 | ], 332 | "time": "2024-07-18T11:15:46+00:00" 333 | }, 334 | { 335 | "name": "league/climate", 336 | "version": "3.10.0", 337 | "source": { 338 | "type": "git", 339 | "url": "https://github.com/thephpleague/climate.git", 340 | "reference": "237f70e1032b16d32ff3f65dcda68706911e1c74" 341 | }, 342 | "dist": { 343 | "type": "zip", 344 | "url": "https://api.github.com/repos/thephpleague/climate/zipball/237f70e1032b16d32ff3f65dcda68706911e1c74", 345 | "reference": "237f70e1032b16d32ff3f65dcda68706911e1c74", 346 | "shasum": "" 347 | }, 348 | "require": { 349 | "php": "^7.3 || ^8.0", 350 | "psr/log": "^1.0 || ^2.0 || ^3.0", 351 | "seld/cli-prompt": "^1.0" 352 | }, 353 | "require-dev": { 354 | "mikey179/vfsstream": "^1.6.12", 355 | "mockery/mockery": "^1.6.12", 356 | "phpunit/phpunit": "^9.5.10", 357 | "squizlabs/php_codesniffer": "^3.10" 358 | }, 359 | "suggest": { 360 | "ext-mbstring": "If ext-mbstring is not available you MUST install symfony/polyfill-mbstring" 361 | }, 362 | "type": "library", 363 | "autoload": { 364 | "psr-4": { 365 | "League\\CLImate\\": "src/" 366 | } 367 | }, 368 | "notification-url": "https://packagist.org/downloads/", 369 | "license": [ 370 | "MIT" 371 | ], 372 | "authors": [ 373 | { 374 | "name": "Joe Tannenbaum", 375 | "email": "hey@joe.codes", 376 | "homepage": "http://joe.codes/", 377 | "role": "Developer" 378 | }, 379 | { 380 | "name": "Craig Duncan", 381 | "email": "git@duncanc.co.uk", 382 | "homepage": "https://github.com/duncan3dc", 383 | "role": "Developer" 384 | } 385 | ], 386 | "description": "PHP's best friend for the terminal. CLImate allows you to easily output colored text, special formats, and more.", 387 | "keywords": [ 388 | "cli", 389 | "colors", 390 | "command", 391 | "php", 392 | "terminal" 393 | ], 394 | "support": { 395 | "issues": "https://github.com/thephpleague/climate/issues", 396 | "source": "https://github.com/thephpleague/climate/tree/3.10.0" 397 | }, 398 | "time": "2024-11-18T09:09:55+00:00" 399 | }, 400 | { 401 | "name": "psr/http-client", 402 | "version": "1.0.3", 403 | "source": { 404 | "type": "git", 405 | "url": "https://github.com/php-fig/http-client.git", 406 | "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" 407 | }, 408 | "dist": { 409 | "type": "zip", 410 | "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", 411 | "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", 412 | "shasum": "" 413 | }, 414 | "require": { 415 | "php": "^7.0 || ^8.0", 416 | "psr/http-message": "^1.0 || ^2.0" 417 | }, 418 | "type": "library", 419 | "extra": { 420 | "branch-alias": { 421 | "dev-master": "1.0.x-dev" 422 | } 423 | }, 424 | "autoload": { 425 | "psr-4": { 426 | "Psr\\Http\\Client\\": "src/" 427 | } 428 | }, 429 | "notification-url": "https://packagist.org/downloads/", 430 | "license": [ 431 | "MIT" 432 | ], 433 | "authors": [ 434 | { 435 | "name": "PHP-FIG", 436 | "homepage": "https://www.php-fig.org/" 437 | } 438 | ], 439 | "description": "Common interface for HTTP clients", 440 | "homepage": "https://github.com/php-fig/http-client", 441 | "keywords": [ 442 | "http", 443 | "http-client", 444 | "psr", 445 | "psr-18" 446 | ], 447 | "support": { 448 | "source": "https://github.com/php-fig/http-client" 449 | }, 450 | "time": "2023-09-23T14:17:50+00:00" 451 | }, 452 | { 453 | "name": "psr/http-factory", 454 | "version": "1.1.0", 455 | "source": { 456 | "type": "git", 457 | "url": "https://github.com/php-fig/http-factory.git", 458 | "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" 459 | }, 460 | "dist": { 461 | "type": "zip", 462 | "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", 463 | "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", 464 | "shasum": "" 465 | }, 466 | "require": { 467 | "php": ">=7.1", 468 | "psr/http-message": "^1.0 || ^2.0" 469 | }, 470 | "type": "library", 471 | "extra": { 472 | "branch-alias": { 473 | "dev-master": "1.0.x-dev" 474 | } 475 | }, 476 | "autoload": { 477 | "psr-4": { 478 | "Psr\\Http\\Message\\": "src/" 479 | } 480 | }, 481 | "notification-url": "https://packagist.org/downloads/", 482 | "license": [ 483 | "MIT" 484 | ], 485 | "authors": [ 486 | { 487 | "name": "PHP-FIG", 488 | "homepage": "https://www.php-fig.org/" 489 | } 490 | ], 491 | "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", 492 | "keywords": [ 493 | "factory", 494 | "http", 495 | "message", 496 | "psr", 497 | "psr-17", 498 | "psr-7", 499 | "request", 500 | "response" 501 | ], 502 | "support": { 503 | "source": "https://github.com/php-fig/http-factory" 504 | }, 505 | "time": "2024-04-15T12:06:14+00:00" 506 | }, 507 | { 508 | "name": "psr/http-message", 509 | "version": "2.0", 510 | "source": { 511 | "type": "git", 512 | "url": "https://github.com/php-fig/http-message.git", 513 | "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" 514 | }, 515 | "dist": { 516 | "type": "zip", 517 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", 518 | "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", 519 | "shasum": "" 520 | }, 521 | "require": { 522 | "php": "^7.2 || ^8.0" 523 | }, 524 | "type": "library", 525 | "extra": { 526 | "branch-alias": { 527 | "dev-master": "2.0.x-dev" 528 | } 529 | }, 530 | "autoload": { 531 | "psr-4": { 532 | "Psr\\Http\\Message\\": "src/" 533 | } 534 | }, 535 | "notification-url": "https://packagist.org/downloads/", 536 | "license": [ 537 | "MIT" 538 | ], 539 | "authors": [ 540 | { 541 | "name": "PHP-FIG", 542 | "homepage": "https://www.php-fig.org/" 543 | } 544 | ], 545 | "description": "Common interface for HTTP messages", 546 | "homepage": "https://github.com/php-fig/http-message", 547 | "keywords": [ 548 | "http", 549 | "http-message", 550 | "psr", 551 | "psr-7", 552 | "request", 553 | "response" 554 | ], 555 | "support": { 556 | "source": "https://github.com/php-fig/http-message/tree/2.0" 557 | }, 558 | "time": "2023-04-04T09:54:51+00:00" 559 | }, 560 | { 561 | "name": "psr/log", 562 | "version": "3.0.2", 563 | "source": { 564 | "type": "git", 565 | "url": "https://github.com/php-fig/log.git", 566 | "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" 567 | }, 568 | "dist": { 569 | "type": "zip", 570 | "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", 571 | "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", 572 | "shasum": "" 573 | }, 574 | "require": { 575 | "php": ">=8.0.0" 576 | }, 577 | "type": "library", 578 | "extra": { 579 | "branch-alias": { 580 | "dev-master": "3.x-dev" 581 | } 582 | }, 583 | "autoload": { 584 | "psr-4": { 585 | "Psr\\Log\\": "src" 586 | } 587 | }, 588 | "notification-url": "https://packagist.org/downloads/", 589 | "license": [ 590 | "MIT" 591 | ], 592 | "authors": [ 593 | { 594 | "name": "PHP-FIG", 595 | "homepage": "https://www.php-fig.org/" 596 | } 597 | ], 598 | "description": "Common interface for logging libraries", 599 | "homepage": "https://github.com/php-fig/log", 600 | "keywords": [ 601 | "log", 602 | "psr", 603 | "psr-3" 604 | ], 605 | "support": { 606 | "source": "https://github.com/php-fig/log/tree/3.0.2" 607 | }, 608 | "time": "2024-09-11T13:17:53+00:00" 609 | }, 610 | { 611 | "name": "ralouphie/getallheaders", 612 | "version": "3.0.3", 613 | "source": { 614 | "type": "git", 615 | "url": "https://github.com/ralouphie/getallheaders.git", 616 | "reference": "120b605dfeb996808c31b6477290a714d356e822" 617 | }, 618 | "dist": { 619 | "type": "zip", 620 | "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", 621 | "reference": "120b605dfeb996808c31b6477290a714d356e822", 622 | "shasum": "" 623 | }, 624 | "require": { 625 | "php": ">=5.6" 626 | }, 627 | "require-dev": { 628 | "php-coveralls/php-coveralls": "^2.1", 629 | "phpunit/phpunit": "^5 || ^6.5" 630 | }, 631 | "type": "library", 632 | "autoload": { 633 | "files": [ 634 | "src/getallheaders.php" 635 | ] 636 | }, 637 | "notification-url": "https://packagist.org/downloads/", 638 | "license": [ 639 | "MIT" 640 | ], 641 | "authors": [ 642 | { 643 | "name": "Ralph Khattar", 644 | "email": "ralph.khattar@gmail.com" 645 | } 646 | ], 647 | "description": "A polyfill for getallheaders.", 648 | "support": { 649 | "issues": "https://github.com/ralouphie/getallheaders/issues", 650 | "source": "https://github.com/ralouphie/getallheaders/tree/develop" 651 | }, 652 | "time": "2019-03-08T08:55:37+00:00" 653 | }, 654 | { 655 | "name": "seld/cli-prompt", 656 | "version": "1.0.4", 657 | "source": { 658 | "type": "git", 659 | "url": "https://github.com/Seldaek/cli-prompt.git", 660 | "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5" 661 | }, 662 | "dist": { 663 | "type": "zip", 664 | "url": "https://api.github.com/repos/Seldaek/cli-prompt/zipball/b8dfcf02094b8c03b40322c229493bb2884423c5", 665 | "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5", 666 | "shasum": "" 667 | }, 668 | "require": { 669 | "php": ">=5.3" 670 | }, 671 | "require-dev": { 672 | "phpstan/phpstan": "^0.12.63" 673 | }, 674 | "type": "library", 675 | "extra": { 676 | "branch-alias": { 677 | "dev-master": "1.x-dev" 678 | } 679 | }, 680 | "autoload": { 681 | "psr-4": { 682 | "Seld\\CliPrompt\\": "src/" 683 | } 684 | }, 685 | "notification-url": "https://packagist.org/downloads/", 686 | "license": [ 687 | "MIT" 688 | ], 689 | "authors": [ 690 | { 691 | "name": "Jordi Boggiano", 692 | "email": "j.boggiano@seld.be" 693 | } 694 | ], 695 | "description": "Allows you to prompt for user input on the command line, and optionally hide the characters they type", 696 | "keywords": [ 697 | "cli", 698 | "console", 699 | "hidden", 700 | "input", 701 | "prompt" 702 | ], 703 | "support": { 704 | "issues": "https://github.com/Seldaek/cli-prompt/issues", 705 | "source": "https://github.com/Seldaek/cli-prompt/tree/1.0.4" 706 | }, 707 | "time": "2020-12-15T21:32:01+00:00" 708 | }, 709 | { 710 | "name": "symfony/deprecation-contracts", 711 | "version": "v3.5.1", 712 | "source": { 713 | "type": "git", 714 | "url": "https://github.com/symfony/deprecation-contracts.git", 715 | "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" 716 | }, 717 | "dist": { 718 | "type": "zip", 719 | "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", 720 | "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", 721 | "shasum": "" 722 | }, 723 | "require": { 724 | "php": ">=8.1" 725 | }, 726 | "type": "library", 727 | "extra": { 728 | "thanks": { 729 | "url": "https://github.com/symfony/contracts", 730 | "name": "symfony/contracts" 731 | }, 732 | "branch-alias": { 733 | "dev-main": "3.5-dev" 734 | } 735 | }, 736 | "autoload": { 737 | "files": [ 738 | "function.php" 739 | ] 740 | }, 741 | "notification-url": "https://packagist.org/downloads/", 742 | "license": [ 743 | "MIT" 744 | ], 745 | "authors": [ 746 | { 747 | "name": "Nicolas Grekas", 748 | "email": "p@tchwork.com" 749 | }, 750 | { 751 | "name": "Symfony Community", 752 | "homepage": "https://symfony.com/contributors" 753 | } 754 | ], 755 | "description": "A generic function and convention to trigger deprecation notices", 756 | "homepage": "https://symfony.com", 757 | "support": { 758 | "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" 759 | }, 760 | "funding": [ 761 | { 762 | "url": "https://symfony.com/sponsor", 763 | "type": "custom" 764 | }, 765 | { 766 | "url": "https://github.com/fabpot", 767 | "type": "github" 768 | }, 769 | { 770 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 771 | "type": "tidelift" 772 | } 773 | ], 774 | "time": "2024-09-25T14:20:29+00:00" 775 | } 776 | ], 777 | "packages-dev": [], 778 | "aliases": [], 779 | "minimum-stability": "stable", 780 | "stability-flags": [], 781 | "prefer-stable": false, 782 | "prefer-lowest": false, 783 | "platform": { 784 | "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", 785 | "ext-zip": "*", 786 | "composer-runtime-api": "^2.2" 787 | }, 788 | "platform-dev": [], 789 | "plugin-api-version": "2.6.0" 790 | } 791 | -------------------------------------------------------------------------------- /phpmd.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 24 | 25 | 26 | ./src 27 | 28 | 29 | 30 | 31 | 32 | ./tests 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /psalm.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/CLI/CLI.php: -------------------------------------------------------------------------------- 1 | 19 | * @link https://getkirby.com 20 | * @copyright Bastian Allgeier 21 | * @license https://opensource.org/licenses/MIT 22 | */ 23 | class CLI 24 | { 25 | protected CLImate $climate; 26 | protected App|null $kirby = null; 27 | protected array $options; 28 | protected array $roots; 29 | 30 | /** 31 | * Proxy for CLImate methods 32 | */ 33 | public function __call(string $method, array $arguments = []) 34 | { 35 | return $this->climate->$method(...$arguments); 36 | } 37 | 38 | /** 39 | * Creates a new CLI instance 40 | */ 41 | public function __construct() 42 | { 43 | $this->climate = new CLImate(); 44 | $this->roots = []; 45 | 46 | if (function_exists('kirby') === true && class_exists('Kirby\Cms\App') === true) { 47 | $this->kirby = App::instance(); 48 | $this->roots = $this->kirby->roots()->toArray(); 49 | } 50 | 51 | $this->createCommandRoots(); 52 | } 53 | 54 | /** 55 | * Returns the value for an argument if it can be found 56 | */ 57 | public function arg(string $name) 58 | { 59 | return $this->climate->arguments->get($name); 60 | } 61 | 62 | /** 63 | * Tries to get a value from one 64 | * of the arguments and otherwise 65 | * shows a prompt for it 66 | */ 67 | public function argOrPrompt(string $name, string $prompt, bool $required = true) 68 | { 69 | $value = $this->arg($name); 70 | 71 | if (empty($value) === true) { 72 | return $this->prompt($prompt, $required); 73 | } 74 | 75 | return $value; 76 | } 77 | 78 | /** 79 | * Returns all parsed arguments 80 | */ 81 | public function args() 82 | { 83 | return $this->climate->arguments; 84 | } 85 | 86 | /** 87 | * Returns the CLImate instance 88 | * if it has been initiated 89 | */ 90 | public function climate(): CLImate 91 | { 92 | return $this->climate; 93 | } 94 | 95 | /** 96 | * Runs a command with the given arguments 97 | */ 98 | public static function command(...$args): void 99 | { 100 | $cli = new static(); 101 | 102 | try { 103 | $cli->run(...$args); 104 | } catch (Throwable $e) { 105 | $cli->handleException($e); 106 | } 107 | } 108 | 109 | /** 110 | * Find the absolute path to the command file 111 | */ 112 | public function commandFile(string $name, bool $core = true): string 113 | { 114 | // load built-in command 115 | $file = $this->roots['commands.core'] . '/' . $name . '.php'; 116 | 117 | if ($core === true && is_file($file) === true) { 118 | return $file; 119 | } 120 | 121 | // global commands 122 | $file = $this->roots['commands.global'] . '/' . $name . '.php'; 123 | 124 | if (is_file($file) === true) { 125 | return $file; 126 | } 127 | 128 | // local commands 129 | $file = $this->roots['commands.local'] . '/' . $name . '.php'; 130 | 131 | if (is_file($file) === true) { 132 | return $file; 133 | } 134 | 135 | throw new Exception('The command does not exist'); 136 | } 137 | 138 | /** 139 | * Returns an array with all 140 | * global and custom commands 141 | */ 142 | public function commands(): array 143 | { 144 | $core = $this->commandsInDirectory($this->roots['commands.core']); 145 | $global = $this->commandsInDirectory($this->roots['commands.global']); 146 | $local = $this->commandsInDirectory($this->roots['commands.local']); 147 | $plugins = []; 148 | 149 | foreach ($local as $index => $command) { 150 | if (in_array($command, $core) === true) { 151 | unset($local[$index]); 152 | } 153 | } 154 | 155 | if ($this->kirby) { 156 | $extensions = $this->kirby->extensions('commands'); 157 | 158 | foreach ($extensions as $name => $command) { 159 | if (in_array($name, $core) === false) { 160 | $plugins[] = $name; 161 | } 162 | } 163 | } 164 | 165 | return [ 166 | 'core' => $core, 167 | 'global' => $global, 168 | 'custom' => $local, 169 | 'plugins' => $plugins 170 | ]; 171 | } 172 | 173 | /** 174 | * Scans a directory and finds all 175 | * valid commands inside. 176 | */ 177 | public function commandsInDirectory(string $directory): array 178 | { 179 | $directory = realpath($directory); 180 | 181 | if (!$directory || is_dir($directory) === false) { 182 | return []; 183 | } 184 | 185 | $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)); 186 | $commands = []; 187 | 188 | foreach ($iterator as $item) { 189 | if ($item->isFile() === false || $item->getExtension() !== 'php') { 190 | continue; 191 | } 192 | 193 | $path = preg_replace('!^' . preg_quote($directory) . '\/!', '', $item->getPathname()); 194 | $path = preg_replace('!.php$!', '', $path); 195 | 196 | if (str_contains($path, '_templates') === true) { 197 | continue; 198 | } 199 | 200 | $commands[] = str_replace('/', ':', $path); 201 | } 202 | 203 | asort($commands); 204 | 205 | return array_values($commands); 206 | } 207 | 208 | /** 209 | * Shows a prompt which has to be confirmed 210 | * in order to execute the callback 211 | */ 212 | public function confirmToContinue(string $message, ?callable $onExit = null): void 213 | { 214 | $input = $this->confirm($message); 215 | 216 | if ($input->confirmed() === false) { 217 | if (is_callable($onExit) === true) { 218 | $onExit(); 219 | } 220 | exit; 221 | } 222 | } 223 | 224 | /** 225 | * Shows a prompt before a file or directory 226 | * ($item) will be deleted 227 | */ 228 | public function confirmToDelete(string $item, string $message): bool 229 | { 230 | $item = realpath($item); 231 | 232 | if (!$item) { 233 | return true; 234 | } 235 | 236 | if (is_dir($item) === false && is_file($item) === false && is_link($item) === false) { 237 | return true; 238 | } 239 | 240 | $this->confirmToContinue($message); 241 | 242 | // we need to implement Dir::remove and F::remove here again, because 243 | // the Kirby installation might not be available when we need this 244 | if (is_dir($item) === true) { 245 | $this->rmdir($item); 246 | } else { 247 | unlink($item); 248 | } 249 | 250 | return true; 251 | } 252 | 253 | /** 254 | * Creates default values for command roots 255 | * if they are not set 256 | */ 257 | protected function createCommandRoots(): void 258 | { 259 | $local = $this->kirby?->root('commands') ?? getcwd() . '/commands'; 260 | 261 | $this->roots['commands.core'] ??= dirname(__DIR__, 2) . '/commands'; 262 | $this->roots['commands.global'] ??= $this->home() . '/commands'; 263 | $this->roots['commands.local'] ??= $local; 264 | } 265 | 266 | /** 267 | * Creates all custom roots for the CLI 268 | */ 269 | protected function createRoots(array $roots = []): array 270 | { 271 | foreach ($roots as $key => $value) { 272 | if (str_starts_with($value, '.') === true) { 273 | $root = realpath(getcwd() . '/' . $value); 274 | 275 | if ($root !== false) { 276 | $roots[$key] = $root; 277 | } 278 | } else { 279 | $roots[$key] = $value; 280 | } 281 | } 282 | 283 | return $roots; 284 | } 285 | 286 | /** 287 | * Get the current working directory 288 | */ 289 | public function dir(?string $folder = null): string 290 | { 291 | $current = getcwd(); 292 | 293 | if (empty($folder) === true) { 294 | return $current; 295 | } 296 | 297 | if (str_starts_with($folder, '.') === true) { 298 | return $current . '/' . $folder; 299 | } 300 | 301 | return $folder; 302 | } 303 | 304 | /** 305 | * Handles exception with throwing exception or out error message 306 | */ 307 | protected function handleException(Throwable $e): never 308 | { 309 | if ($this->isDefined('debug') === true) { 310 | throw $e; 311 | } 312 | 313 | $this->error($e->getMessage()); 314 | exit; 315 | } 316 | 317 | /** 318 | * Gets path for global commands (respecting 'XDG_CONFIG_HOME' if set) 319 | * 320 | * For more information on the 'XDG Base Directory Speicfications', 321 | * see https://specifications.freedesktop.org/basedir-spec/latest 322 | */ 323 | public function home(): string 324 | { 325 | if ($path = getenv('XDG_CONFIG_HOME')) { 326 | return $path . '/kirby'; 327 | } 328 | 329 | return getenv('HOME') . '/.kirby'; 330 | } 331 | 332 | /** 333 | * Checks if an argument is set 334 | */ 335 | public function isDefined(string $arg): bool 336 | { 337 | return $this->climate->arguments->defined($arg); 338 | } 339 | 340 | /** 341 | * Creates pretty json 342 | */ 343 | public function json(array $data = []): string 344 | { 345 | return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); 346 | } 347 | 348 | /** 349 | * Returns the parent Kirby instance 350 | * if an installation can be found 351 | */ 352 | public function kirby(bool $fail = true): ?App 353 | { 354 | if (is_a($this->kirby, 'Kirby\Cms\App') === false) { 355 | if ($fail === true) { 356 | throw new Exception('The Kirby installation could not be found'); 357 | } 358 | 359 | return null; 360 | } 361 | 362 | return $this->kirby; 363 | } 364 | 365 | /** 366 | * Loads a command either from the custom 367 | * site/commands directory of the current Kirby 368 | * installation or from the global commands 369 | * directory of the CLI. Global commands will always 370 | * overwrite local commands because they sometimes 371 | * depend on each other. 372 | */ 373 | public function load(string $name): callable|array 374 | { 375 | // convert the name to a path 376 | $path = str_replace(':', '/', $name); 377 | 378 | try { 379 | $command = require $this->commandFile($path); 380 | } catch (Throwable $e) { 381 | if (!$this->kirby) { 382 | throw $e; 383 | } 384 | 385 | // try to load a plugin command 386 | $command = $this->kirby->extension('commands', $name); 387 | 388 | if (empty($command) === true) { 389 | throw $e; 390 | } 391 | } 392 | 393 | // validate the command format 394 | if (is_array($command) === false) { 395 | throw new Exception('Invalid command format. The command must be defined as array'); 396 | } 397 | 398 | // validate that the command can be executed 399 | if (isset($command['command']) === false || is_callable($command['command']) === false) { 400 | throw new Exception('The command does not define a command action'); 401 | } 402 | 403 | return $command; 404 | } 405 | 406 | /** 407 | * Creates a file with the given content 408 | * and replaces all placeholders in the content 409 | * with values from the data array. $content 410 | * can also be a filename and the method will 411 | * automatically fetch content from the file 412 | */ 413 | public function make(string $file, string $content, array $data = []): bool 414 | { 415 | if (is_file($content) === true) { 416 | $content = file_get_contents($content); 417 | } 418 | 419 | $file = $this->template($file, $data); 420 | $content = $this->template($content, $data); 421 | $dir = dirname($file); 422 | 423 | $this->confirmToDelete($file, 'The file ' . basename($file) . ' exists. Do you want to replace it?'); 424 | 425 | if (is_dir($dir) === false) { 426 | mkdir($dir, 0755, true); 427 | } 428 | 429 | return file_put_contents($file, $content) !== false; 430 | } 431 | 432 | /** 433 | * Shows a prompt and returns the 434 | * entered value. 435 | */ 436 | public function prompt(string $prompt, bool $required = true) 437 | { 438 | while (empty($value) === true) { 439 | $input = $this->input($prompt); 440 | $value = $input->prompt(); 441 | 442 | if ($required === false) { 443 | return $value; 444 | } 445 | } 446 | 447 | return $value; 448 | } 449 | 450 | /** 451 | * Removes a folder including all containing files and folders 452 | */ 453 | public function rmdir($dir): bool 454 | { 455 | $dir = realpath($dir); 456 | 457 | if (is_dir($dir) === false) { 458 | return true; 459 | } 460 | 461 | if (is_link($dir) === true) { 462 | return unlink($dir); 463 | } 464 | 465 | foreach (scandir($dir) as $childName) { 466 | if (in_array($childName, ['.', '..']) === true) { 467 | continue; 468 | } 469 | 470 | $child = $dir . '/' . $childName; 471 | 472 | if (is_dir($child) === true && is_link($child) === false) { 473 | $this->rmdir($child); 474 | } else { 475 | unlink($child); 476 | } 477 | } 478 | 479 | return rmdir($dir); 480 | } 481 | 482 | /** 483 | * Returns a root either from the custom roots 484 | * array or from the Kirby instance 485 | */ 486 | public function root(string $key): ?string 487 | { 488 | return $this->roots[$key] ?? $this->kirby?->root($key); 489 | } 490 | 491 | /** 492 | * Returns all roots 493 | */ 494 | public function roots(): array 495 | { 496 | return $this->roots; 497 | } 498 | 499 | /** 500 | * Load and execute a command 501 | */ 502 | public function run(?string $name = null, ...$args): void 503 | { 504 | // create clean new climate instance 505 | $this->climate = new CLImate(); 506 | 507 | // custom commands 508 | $this->climate->style->addCommand('success', ['background_light_green', 'black']); 509 | 510 | // no command? show info about the cli 511 | if (empty($name) === true) { 512 | $this->run('help'); 513 | return; 514 | } 515 | 516 | // add the command as first argument 517 | $this->climate->arguments->add([ 518 | 'command' => [ 519 | 'description' => 'The name of the command', 520 | 'required' => true 521 | ] 522 | ]); 523 | 524 | $command = $this->load($name); 525 | 526 | $this->climate->arguments->add($command['args'] ?? []); 527 | $this->climate->description($command['description'] ?? 'kirby ' . $name); 528 | 529 | // add the quiet option 530 | $this->climate->arguments->add([ 531 | 'quiet' => [ 532 | 'description' => 'Surpresses any output', 533 | 'longPrefix' => 'quiet', 534 | 'noValue' => true 535 | ] 536 | ]); 537 | 538 | // add debug argument 539 | $this->climate->arguments->add([ 540 | 'debug' => [ 541 | 'description' => 'Enables debug mode', 542 | 'prefix' => 'd', 543 | 'longPrefix' => 'debug', 544 | 'noValue' => true 545 | ] 546 | ]); 547 | 548 | // add help argument 549 | $this->climate->arguments->add([ 550 | 'help' => [ 551 | 'description' => 'Prints a usage statement', 552 | 'prefix' => 'h', 553 | 'longPrefix' => 'help', 554 | 'noValue' => true 555 | ] 556 | ]); 557 | 558 | // build the args array 559 | $argv = [ 560 | 'kirby', 561 | $name, 562 | ...$args 563 | ]; 564 | 565 | try { 566 | $this->climate->arguments->parse($argv); 567 | } catch (Throwable $e) { 568 | $this->handleException($e); 569 | } 570 | 571 | // enable quiet mode 572 | if ($this->climate->arguments->get('quiet')) { 573 | $this->climate->output->add('quiet', new QuietWriter()); 574 | $this->climate->output->defaultTo('quiet'); 575 | } 576 | 577 | if ($this->climate->arguments->defined('help', $argv)) { 578 | $this->climate->usage($argv); 579 | return; 580 | } 581 | 582 | try { 583 | $command['command']($this); 584 | } catch (Throwable $e) { 585 | $this->handleException($e); 586 | } 587 | } 588 | 589 | /** 590 | * Replaces placeholders in string templates 591 | * Str::replace would be better but is only 592 | * available if Kirby is installed 593 | */ 594 | public function template(string $template, array $data = []): string 595 | { 596 | $keys = array_map(function ($key) { 597 | return '{{ ' . $key . ' }}'; 598 | }, array_keys($data)); 599 | 600 | return str_replace($keys, array_values($data), $template); 601 | } 602 | 603 | /** 604 | * Returns the CLI version from the composer.json 605 | */ 606 | public function version(): string 607 | { 608 | $composer = dirname(__DIR__, 2) . '/composer.json'; 609 | $contents = file_get_contents($composer); 610 | $json = json_decode($contents); 611 | 612 | return $json->version; 613 | } 614 | } 615 | -------------------------------------------------------------------------------- /src/CLI/QuietWriter.php: -------------------------------------------------------------------------------- 1 | assertSame($root . '/index.php', index()); 15 | $this->assertSame($root . '/index.php', bootstrap()); 16 | } 17 | 18 | /** 19 | * @covers bootstrap 20 | * @covers index 21 | */ 22 | public function testIndexInWww() 23 | { 24 | chdir($root = __DIR__ . '/bootstrap/b'); 25 | $this->assertSame($root . '/www/index.php', index()); 26 | $this->assertSame($root . '/www/index.php', bootstrap()); 27 | } 28 | 29 | /** 30 | * @covers bootstrap 31 | * @covers index 32 | */ 33 | public function testIndexInPublic() 34 | { 35 | chdir($root = __DIR__ . '/bootstrap/c'); 36 | $this->assertSame($root . '/public/index.php', index()); 37 | $this->assertSame($root . '/public/index.php', bootstrap()); 38 | } 39 | 40 | /** 41 | * @covers bootstrap 42 | * @covers index 43 | */ 44 | public function testIndexInPublicHtml() 45 | { 46 | chdir($root = __DIR__ . '/bootstrap/d'); 47 | $this->assertSame($root . '/public_html/index.php', index()); 48 | $this->assertSame($root . '/public_html/index.php', bootstrap()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/CLI/CLITest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(CLImate::class, $cli->climate()); 25 | } 26 | 27 | /** 28 | * @covers ::commandsInDirectory 29 | */ 30 | public function testCommandsInDirectory() 31 | { 32 | $cli = new CLI(); 33 | 34 | // missing command directory 35 | $commands = $cli->commandsInDirectory(__DIR__ . '/does-not-exist'); 36 | $this->assertSame([], $commands); 37 | 38 | // existing command directory 39 | $commands = $cli->commandsInDirectory(__DIR__ . '/commands'); 40 | $expected = [ 41 | 'invalid-action', 42 | 'invalid-format', 43 | 'nested:command', 44 | 'test' 45 | ]; 46 | 47 | $this->assertSame($expected, $commands); 48 | } 49 | 50 | /** 51 | * @covers ::dir 52 | */ 53 | public function testDir() 54 | { 55 | $cli = new CLI(); 56 | 57 | // current working directory 58 | $this->assertSame(__DIR__, $cli->dir()); 59 | 60 | // relative 61 | $this->assertSame(__DIR__ . '/./commands', $cli->dir('./commands')); 62 | $this->assertSame(__DIR__ . '/../commands', $cli->dir('../commands')); 63 | 64 | // absolute 65 | $this->assertSame('/test', $cli->dir('/test')); 66 | } 67 | 68 | /** 69 | * @covers ::home 70 | */ 71 | public function testHome() 72 | { 73 | $homeBefore = getenv('HOME'); 74 | $xdgHomeBefore = getenv('XDG_CONFIG_HOME'); 75 | 76 | // unset xdg config home to make sure home is used 77 | putenv('XDG_CONFIG_HOME'); 78 | putenv('HOME=/test'); 79 | 80 | $cli = new CLI(); 81 | 82 | $this->assertSame('/test/.kirby', $cli->home()); 83 | 84 | putenv('HOME=' . $homeBefore); 85 | putenv('XDG_CONFIG_HOME=' . $xdgHomeBefore); 86 | } 87 | 88 | /** 89 | * @covers ::home 90 | */ 91 | public function testHomeWithXdgConfig() 92 | { 93 | $before = getenv('XDG_CONFIG_HOME'); 94 | 95 | putenv('XDG_CONFIG_HOME=/test'); 96 | 97 | $cli = new CLI(); 98 | 99 | $this->assertSame('/test/kirby', $cli->home()); 100 | 101 | putenv('XDG_CONFIG_HOME=' . $before); 102 | } 103 | 104 | /** 105 | * @covers ::json 106 | */ 107 | public function testJson() 108 | { 109 | $cli = new CLI(); 110 | $json = $cli->json([ 111 | 'test' => 'value' 112 | ]); 113 | 114 | $expected = '{' . PHP_EOL; 115 | $expected .= ' "test": "value"' . PHP_EOL; 116 | $expected .= '}'; 117 | 118 | $this->assertSame($expected, $json); 119 | } 120 | 121 | /** 122 | * @covers ::kirby 123 | */ 124 | public function testKirby() 125 | { 126 | $cli = new CLI(); 127 | 128 | $this->expectException('Exception'); 129 | $this->expectExceptionMessage('The Kirby installation could not be found'); 130 | 131 | $cli->kirby(); 132 | } 133 | 134 | /** 135 | * @covers ::kirby 136 | */ 137 | public function testKirbyWithoutFailing() 138 | { 139 | $cli = new CLI(); 140 | 141 | $this->assertNull($cli->kirby(false)); 142 | } 143 | 144 | /** 145 | * @covers ::load 146 | */ 147 | public function testLoadFromCoreCommands() 148 | { 149 | $cli = new CLI(); 150 | 151 | $command = $cli->load('install'); 152 | $this->assertSame('Installs the kirby folder', $command['description']); 153 | } 154 | 155 | /** 156 | * @covers ::load 157 | */ 158 | public function testLoadFromLocalCommands() 159 | { 160 | $cli = new CLI(); 161 | 162 | $command = $cli->load('test'); 163 | $this->assertSame('Test', $command['description']); 164 | } 165 | 166 | /** 167 | * @covers ::load 168 | */ 169 | public function testLoadInvalidCommand() 170 | { 171 | $cli = new CLI(); 172 | 173 | $this->expectException(Exception::class); 174 | $this->expectExceptionMessage('The command does not exist'); 175 | 176 | $cli->load('foo'); 177 | } 178 | 179 | /** 180 | * @covers ::load 181 | */ 182 | public function testLoadInvalidCommandAction() 183 | { 184 | $cli = new CLI(); 185 | 186 | $this->expectException(Exception::class); 187 | $this->expectExceptionMessage('The command does not define a command action'); 188 | 189 | $cli->load('invalid-action'); 190 | } 191 | 192 | /** 193 | * @covers ::load 194 | */ 195 | public function testLoadInvalidCommandFormat() 196 | { 197 | $cli = new CLI(); 198 | 199 | $this->expectException(Exception::class); 200 | $this->expectExceptionMessage('Invalid command format. The command must be defined as array'); 201 | 202 | $cli->load('invalid-format'); 203 | } 204 | 205 | /** 206 | * @covers ::root 207 | */ 208 | public function testRoot() 209 | { 210 | $cli = new CLI(); 211 | 212 | $this->assertSame(dirname(__DIR__, 2) . '/commands', $cli->root('commands.core')); 213 | $this->assertSame($cli->home() . '/commands', $cli->root('commands.global')); 214 | $this->assertSame(__DIR__ . '/commands', $cli->root('commands.local')); 215 | } 216 | 217 | /** 218 | * @covers ::roots 219 | */ 220 | public function testRoots() 221 | { 222 | $cli = new CLI(); 223 | $roots = $cli->roots(); 224 | 225 | $this->assertArrayHasKey('commands.core', $roots); 226 | $this->assertArrayHasKey('commands.local', $roots); 227 | $this->assertArrayHasKey('commands.global', $roots); 228 | } 229 | 230 | /** 231 | * @covers ::template 232 | */ 233 | public function testTemplate() 234 | { 235 | $cli = new CLI(); 236 | 237 | $result = $cli->template('Hello {{ message }}', ['message' => 'world']); 238 | 239 | $this->assertSame('Hello world', $result); 240 | } 241 | 242 | /** 243 | * @covers ::version 244 | */ 245 | public function testVersion() 246 | { 247 | $cli = new CLI(); 248 | $this->assertMatchesRegularExpression('!^[0-9]+.[0-9]+.[0-9]+$!', $cli->version()); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /tests/CLI/TestCase.php: -------------------------------------------------------------------------------- 1 | 'Test', 5 | ]; 6 | -------------------------------------------------------------------------------- /tests/CLI/commands/invalid-format.php: -------------------------------------------------------------------------------- 1 | 'Nested command', 5 | 'command' => function () { 6 | return 'test'; 7 | } 8 | ]; 9 | -------------------------------------------------------------------------------- /tests/CLI/commands/nested/not-a-command.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getkirby/cli/6c7ccab551402fc1d6b735ffd9e70e27663c3eb5/tests/CLI/commands/nested/not-a-command.txt -------------------------------------------------------------------------------- /tests/CLI/commands/test.php: -------------------------------------------------------------------------------- 1 | 'Test', 5 | 'command' => function () { 6 | return 'test'; 7 | } 8 | ]; 9 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |