From 4b92bcfc06862bc3faff886774ca183d9e89cc51 Mon Sep 17 00:00:00 2001 From: Kowalski Date: Sun, 1 Nov 2026 09:10:08 +0100 Subject: [PATCH] Deployer --- .gitignore | 1 + app/Bootstrap.php | 5 + composer.json | 3 +- composer.lock | 997 ++++- config/local.neon.example | 18 + deploy.php | 51 + deploy/apache-vhost.conf.example | 33 + vendor/bin/dep | 119 + vendor/bin/dep.bat | 5 + vendor/bin/yaml-lint | 119 + vendor/bin/yaml-lint.bat | 5 + vendor/composer/autoload_classmap.php | 1 + vendor/composer/autoload_files.php | 8 + vendor/composer/autoload_psr4.php | 12 + vendor/composer/autoload_static.php | 69 + vendor/composer/installed.json | 1049 ++++- vendor/composer/installed.php | 127 +- vendor/deployer/deployer/.dockerignore | 4 + .../deployer/deployer/.php-cs-fixer.dist.php | 23 + vendor/deployer/deployer/Dockerfile | 22 + vendor/deployer/deployer/LICENSE | 21 + vendor/deployer/deployer/README.md | 52 + vendor/deployer/deployer/SECURITY.md | 22 + vendor/deployer/deployer/bin/build | 101 + vendor/deployer/deployer/bin/dep | 99 + vendor/deployer/deployer/bin/docgen | 73 + vendor/deployer/deployer/composer.json | 63 + vendor/deployer/deployer/contrib/bugsnag.php | 38 + .../deployer/deployer/contrib/cachetool.php | 116 + vendor/deployer/deployer/contrib/chatwork.php | 173 + .../deployer/deployer/contrib/cimonitor.php | 166 + .../deployer/deployer/contrib/cloudflare.php | 102 + vendor/deployer/deployer/contrib/cpanel.php | 269 ++ vendor/deployer/deployer/contrib/crontab.php | 154 + .../deployer/deployer/contrib/directadmin.php | 169 + vendor/deployer/deployer/contrib/discord.php | 119 + vendor/deployer/deployer/contrib/grafana.php | 66 + vendor/deployer/deployer/contrib/hangouts.php | 184 + vendor/deployer/deployer/contrib/hipchat.php | 46 + .../deployer/deployer/contrib/ispmanager.php | 780 ++++ .../deployer/deployer/contrib/mattermost.php | 154 + vendor/deployer/deployer/contrib/ms-teams.php | 164 + vendor/deployer/deployer/contrib/newrelic.php | 54 + vendor/deployer/deployer/contrib/npm.php | 35 + vendor/deployer/deployer/contrib/ntfy.php | 159 + vendor/deployer/deployer/contrib/phinx.php | 228 + vendor/deployer/deployer/contrib/php-fpm.php | 45 + vendor/deployer/deployer/contrib/rabbit.php | 110 + vendor/deployer/deployer/contrib/raygun.php | 42 + .../deployer/deployer/contrib/rocketchat.php | 173 + vendor/deployer/deployer/contrib/rollbar.php | 48 + vendor/deployer/deployer/contrib/rsync.php | 252 ++ vendor/deployer/deployer/contrib/sentry.php | 296 ++ vendor/deployer/deployer/contrib/slack.php | 178 + .../deployer/contrib/supervisord-monitor.php | 177 + vendor/deployer/deployer/contrib/telegram.php | 177 + .../deployer/contrib/webpack_encore.php | 44 + .../deployer/deployer/contrib/workplace.php | 122 + vendor/deployer/deployer/contrib/yammer.php | 122 + vendor/deployer/deployer/contrib/yarn.php | 29 + vendor/deployer/deployer/recipe/cakephp.php | 49 + .../deployer/deployer/recipe/codeigniter.php | 23 + .../deployer/deployer/recipe/codeigniter4.php | 190 + vendor/deployer/deployer/recipe/common.php | 199 + vendor/deployer/deployer/recipe/composer.php | 14 + vendor/deployer/deployer/recipe/contao.php | 114 + vendor/deployer/deployer/recipe/craftcms.php | 136 + .../deployer/recipe/deploy/check_remote.php | 50 + .../deployer/recipe/deploy/cleanup.php | 21 + .../deployer/recipe/deploy/clear_paths.php | 25 + .../deployer/recipe/deploy/copy_dirs.php | 32 + .../deployer/deployer/recipe/deploy/env.php | 13 + .../deployer/deployer/recipe/deploy/info.php | 38 + .../deployer/deployer/recipe/deploy/lock.php | 33 + .../deployer/deployer/recipe/deploy/push.php | 25 + .../deployer/recipe/deploy/release.php | 205 + .../deployer/recipe/deploy/rollback.php | 91 + .../deployer/deployer/recipe/deploy/setup.php | 22 + .../deployer/recipe/deploy/shared.php | 89 + .../deployer/recipe/deploy/symlink.php | 21 + .../deployer/recipe/deploy/update_code.php | 138 + .../deployer/recipe/deploy/vendors.php | 37 + .../deployer/recipe/deploy/writable.php | 164 + vendor/deployer/deployer/recipe/drupal7.php | 80 + vendor/deployer/deployer/recipe/drupal8.php | 32 + .../deployer/recipe/flow_framework.php | 48 + vendor/deployer/deployer/recipe/fuelphp.php | 22 + vendor/deployer/deployer/recipe/joomla.php | 17 + vendor/deployer/deployer/recipe/laravel.php | 297 ++ vendor/deployer/deployer/recipe/magento.php | 51 + vendor/deployer/deployer/recipe/magento2.php | 651 +++ vendor/deployer/deployer/recipe/pimcore.php | 31 + .../deployer/deployer/recipe/prestashop.php | 46 + vendor/deployer/deployer/recipe/provision.php | 218 + .../deployer/recipe/provision/404.html | 51 + .../deployer/recipe/provision/Caddyfile | 25 + .../deployer/recipe/provision/databases.php | 67 + .../deployer/recipe/provision/nodejs.php | 36 + .../deployer/recipe/provision/php.php | 85 + .../deployer/recipe/provision/user.php | 88 + .../deployer/recipe/provision/website.php | 76 + vendor/deployer/deployer/recipe/shopware.php | 202 + .../deployer/deployer/recipe/silverstripe.php | 95 + vendor/deployer/deployer/recipe/spiral.php | 154 + vendor/deployer/deployer/recipe/statamic.php | 127 + vendor/deployer/deployer/recipe/sulu.php | 33 + vendor/deployer/deployer/recipe/symfony.php | 79 + vendor/deployer/deployer/recipe/typo3.php | 222 + vendor/deployer/deployer/recipe/wordpress.php | 17 + vendor/deployer/deployer/recipe/yii.php | 29 + .../deployer/recipe/zend_framework.php | 17 + .../deployer/src/Collection/Collection.php | 81 + .../deployer/src/Command/BlackjackCommand.php | 399 ++ .../deployer/src/Command/CommandCommon.php | 42 + .../deployer/src/Command/ConfigCommand.php | 88 + .../deployer/src/Command/CustomOption.php | 52 + .../deployer/src/Command/InitCommand.php | 235 + .../deployer/src/Command/MainCommand.php | 207 + .../deployer/src/Command/RunCommand.php | 100 + .../deployer/src/Command/SelectCommand.php | 123 + .../deployer/src/Command/SshCommand.php | 118 + .../deployer/src/Command/TreeCommand.php | 180 + .../deployer/src/Command/WorkerCommand.php | 59 + .../src/Component/Pimple/Container.php | 289 ++ .../Exception/ExpectedInvokableException.php | 20 + .../Exception/FrozenServiceException.php | 29 + .../InvalidServiceIdentifierException.php | 29 + .../Exception/UnknownIdentifierException.php | 29 + .../deployer/deployer/src/Configuration.php | 222 + vendor/deployer/deployer/src/Deployer.php | 350 ++ .../deployer/src/Documentation/ApiGen.php | 129 + .../deployer/src/Documentation/DocConfig.php | 35 + .../deployer/src/Documentation/DocGen.php | 417 ++ .../deployer/src/Documentation/DocRecipe.php | 184 + .../deployer/src/Documentation/DocTask.php | 46 + .../src/Exception/ConfigurationException.php | 13 + .../deployer/src/Exception/Exception.php | 69 + .../Exception/GracefulShutdownException.php | 27 + .../src/Exception/HttpieException.php | 13 + .../deployer/src/Exception/RunException.php | 85 + .../src/Exception/SchemaException.php | 21 + .../src/Exception/TimeoutException.php | 22 + .../deployer/src/Exception/WillAskUser.php | 21 + .../deployer/deployer/src/Executor/Master.php | 320 ++ .../deployer/src/Executor/Planner.php | 70 + .../deployer/src/Executor/Response.php | 33 + .../deployer/deployer/src/Executor/Server.php | 334 ++ .../deployer/deployer/src/Executor/Worker.php | 54 + vendor/deployer/deployer/src/Host/Host.php | 301 ++ .../deployer/src/Host/HostCollection.php | 25 + .../deployer/deployer/src/Host/Localhost.php | 19 + vendor/deployer/deployer/src/Host/Range.php | 44 + .../deployer/deployer/src/Import/Import.php | 53 + .../deployer/src/Import/MamlRecipe.php | 485 +++ .../deployer/src/Import/YamlRecipe.php | 209 + .../src/Logger/Handler/FileHandler.php | 29 + .../src/Logger/Handler/HandlerInterface.php | 16 + .../src/Logger/Handler/NullHandler.php | 16 + .../deployer/deployer/src/Logger/Logger.php | 169 + .../src/ProcessRunner/ProcessRunner.php | 79 + .../deployer/src/Selector/Selector.php | 126 + .../deployer/deployer/src/Ssh/IOArguments.php | 75 + .../deployer/deployer/src/Ssh/RunParams.php | 33 + .../deployer/deployer/src/Ssh/SshClient.php | 119 + .../deployer/src/Support/ObjectProxy.php | 32 + .../deployer/src/Support/Reporter.php | 48 + .../deployer/deployer/src/Support/helpers.php | 295 ++ vendor/deployer/deployer/src/Task/Context.php | 80 + .../deployer/deployer/src/Task/GroupTask.php | 54 + .../deployer/src/Task/ScriptManager.php | 126 + vendor/deployer/deployer/src/Task/Task.php | 272 ++ .../deployer/src/Task/TaskCollection.php | 30 + .../deployer/src/Utility/HttpResponse.php | 50 + .../deployer/deployer/src/Utility/Httpie.php | 236 + .../deployer/deployer/src/Utility/Rsync.php | 154 + vendor/deployer/deployer/src/functions.php | 1259 ++++++ vendor/deployer/deployer/src/schema.json | 267 ++ vendor/maml/maml/.php-cs-fixer.php | 16 + vendor/maml/maml/LICENSE | 21 + vendor/maml/maml/README.md | 165 + vendor/maml/maml/composer.json | 39 + vendor/maml/maml/phpstan.neon | 5 + vendor/maml/maml/src/Annotated.php | 81 + vendor/maml/maml/src/Ast/ArrayNode.php | 19 + vendor/maml/maml/src/Ast/BooleanNode.php | 17 + vendor/maml/maml/src/Ast/CommentNode.php | 17 + vendor/maml/maml/src/Ast/Document.php | 21 + vendor/maml/maml/src/Ast/Element.php | 17 + vendor/maml/maml/src/Ast/FloatNode.php | 18 + vendor/maml/maml/src/Ast/IdentifierKey.php | 17 + vendor/maml/maml/src/Ast/IntegerNode.php | 18 + vendor/maml/maml/src/Ast/NullNode.php | 18 + vendor/maml/maml/src/Ast/ObjectNode.php | 19 + vendor/maml/maml/src/Ast/Position.php | 14 + vendor/maml/maml/src/Ast/Property.php | 19 + vendor/maml/maml/src/Ast/RawStringNode.php | 18 + vendor/maml/maml/src/Ast/Span.php | 13 + vendor/maml/maml/src/Ast/StringNode.php | 18 + vendor/maml/maml/src/AstParser.php | 828 ++++ vendor/maml/maml/src/AstPrinter.php | 179 + vendor/maml/maml/src/Maml.php | 189 + vendor/maml/maml/src/ParseException.php | 7 + vendor/maml/maml/src/Parser.php | 535 +++ .../maml/src/Schema/JsonSchemaGenerator.php | 207 + vendor/maml/maml/src/Schema/S.php | 109 + vendor/maml/maml/src/Schema/SchemaType.php | 10 + vendor/maml/maml/src/Schema/Type/AnyType.php | 15 + .../maml/maml/src/Schema/Type/ArrayOfType.php | 32 + .../maml/maml/src/Schema/Type/BooleanType.php | 15 + .../maml/maml/src/Schema/Type/FloatType.php | 30 + .../maml/maml/src/Schema/Type/IntegerType.php | 30 + .../maml/maml/src/Schema/Type/LiteralType.php | 28 + vendor/maml/maml/src/Schema/Type/MapType.php | 19 + vendor/maml/maml/src/Schema/Type/NullType.php | 15 + .../maml/maml/src/Schema/Type/NumberType.php | 30 + .../maml/maml/src/Schema/Type/ObjectType.php | 23 + .../maml/src/Schema/Type/OptionalType.php | 19 + .../src/Schema/Type/OrderedObjectType.php | 23 + .../maml/maml/src/Schema/Type/StringType.php | 22 + .../maml/maml/src/Schema/Type/TupleType.php | 21 + .../maml/maml/src/Schema/Type/UnionType.php | 21 + .../maml/maml/src/Schema/ValidationError.php | 16 + vendor/maml/maml/src/Schema/Validator.php | 405 ++ vendor/maml/maml/src/Stringifier.php | 212 + vendor/psr/container/.gitignore | 3 + vendor/psr/container/LICENSE | 21 + vendor/psr/container/README.md | 13 + vendor/psr/container/composer.json | 27 + .../src/ContainerExceptionInterface.php | 12 + .../psr/container/src/ContainerInterface.php | 36 + .../src/NotFoundExceptionInterface.php | 10 + vendor/symfony/console/Application.php | 1371 ++++++ vendor/symfony/console/Attribute/Argument.php | 151 + .../symfony/console/Attribute/AsCommand.php | 51 + vendor/symfony/console/Attribute/Ask.php | 152 + vendor/symfony/console/Attribute/Interact.php | 51 + .../InteractiveAttributeInterface.php | 20 + vendor/symfony/console/Attribute/MapInput.php | 188 + vendor/symfony/console/Attribute/Option.php | 198 + .../Attribute/Reflection/ReflectionMember.php | 109 + vendor/symfony/console/CHANGELOG.md | 314 ++ .../console/CI/GithubActionReporter.php | 97 + vendor/symfony/console/Color.php | 133 + vendor/symfony/console/Command/Command.php | 733 ++++ .../console/Command/CompleteCommand.php | 267 ++ .../console/Command/DumpCompletionCommand.php | 151 + .../symfony/console/Command/HelpCommand.php | 76 + .../console/Command/InvokableCommand.php | 272 ++ .../symfony/console/Command/LazyCommand.php | 206 + .../symfony/console/Command/ListCommand.php | 72 + .../symfony/console/Command/LockableTrait.php | 87 + .../Command/SignalableCommandInterface.php | 36 + .../console/Command/TraceableCommand.php | 368 ++ .../CommandLoader/CommandLoaderInterface.php | 38 + .../CommandLoader/ContainerCommandLoader.php | 52 + .../CommandLoader/FactoryCommandLoader.php | 52 + .../console/Completion/CompletionInput.php | 248 ++ .../Completion/CompletionSuggestions.php | 97 + .../Output/BashCompletionOutput.php | 33 + .../Output/CompletionOutputInterface.php | 25 + .../Output/FishCompletionOutput.php | 36 + .../Completion/Output/ZshCompletionOutput.php | 36 + .../symfony/console/Completion/Suggestion.php | 41 + vendor/symfony/console/ConsoleEvents.php | 72 + vendor/symfony/console/Cursor.php | 204 + .../DataCollector/CommandDataCollector.php | 239 ++ vendor/symfony/console/Debug/CliRequest.php | 68 + .../AddConsoleCommandPass.php | 169 + .../Descriptor/ApplicationDescription.php | 138 + .../symfony/console/Descriptor/Descriptor.php | 74 + .../Descriptor/DescriptorInterface.php | 24 + .../console/Descriptor/JsonDescriptor.php | 166 + .../console/Descriptor/MarkdownDescriptor.php | 173 + .../Descriptor/ReStructuredTextDescriptor.php | 273 ++ .../console/Descriptor/TextDescriptor.php | 317 ++ .../console/Descriptor/XmlDescriptor.php | 230 + .../console/Event/ConsoleAlarmEvent.php | 47 + .../console/Event/ConsoleCommandEvent.php | 54 + .../console/Event/ConsoleErrorEvent.php | 58 + vendor/symfony/console/Event/ConsoleEvent.php | 56 + .../console/Event/ConsoleSignalEvent.php | 56 + .../console/Event/ConsoleTerminateEvent.php | 50 + .../console/EventListener/ErrorListener.php | 89 + .../Exception/CommandNotFoundException.php | 43 + .../console/Exception/ExceptionInterface.php | 21 + .../Exception/InvalidArgumentException.php | 40 + .../Exception/InvalidOptionException.php | 42 + .../console/Exception/LogicException.php | 19 + .../Exception/MissingInputException.php | 21 + .../Exception/NamespaceNotFoundException.php | 21 + .../Exception/RunCommandFailedException.php | 29 + .../console/Exception/RuntimeException.php | 19 + .../console/Formatter/NullOutputFormatter.php | 51 + .../Formatter/NullOutputFormatterStyle.php | 48 + .../console/Formatter/OutputFormatter.php | 492 +++ .../Formatter/OutputFormatterInterface.php | 52 + .../Formatter/OutputFormatterStyle.php | 88 + .../OutputFormatterStyleInterface.php | 50 + .../Formatter/OutputFormatterStyleStack.php | 103 + .../WrappableOutputFormatterInterface.php | 25 + .../console/Helper/DebugFormatterHelper.php | 98 + .../console/Helper/DescriptorHelper.php | 91 + vendor/symfony/console/Helper/Dumper.php | 53 + .../console/Helper/FormatterHelper.php | 81 + vendor/symfony/console/Helper/Helper.php | 167 + .../console/Helper/HelperInterface.php | 35 + vendor/symfony/console/Helper/HelperSet.php | 74 + .../console/Helper/InputAwareHelper.php | 30 + .../symfony/console/Helper/OutputWrapper.php | 79 + .../symfony/console/Helper/ProcessHelper.php | 137 + vendor/symfony/console/Helper/ProgressBar.php | 647 +++ .../console/Helper/ProgressIndicator.php | 247 ++ .../symfony/console/Helper/QuestionHelper.php | 638 +++ .../console/Helper/SymfonyQuestionHelper.php | 103 + vendor/symfony/console/Helper/Table.php | 1155 +++++ vendor/symfony/console/Helper/TableCell.php | 71 + .../symfony/console/Helper/TableCellStyle.php | 84 + vendor/symfony/console/Helper/TableRows.php | 28 + .../symfony/console/Helper/TableSeparator.php | 25 + vendor/symfony/console/Helper/TableStyle.php | 375 ++ .../console/Helper/TerminalInputHelper.php | 166 + vendor/symfony/console/Helper/TreeHelper.php | 111 + vendor/symfony/console/Helper/TreeNode.php | 105 + vendor/symfony/console/Helper/TreeStyle.php | 78 + vendor/symfony/console/Input/ArgvInput.php | 398 ++ vendor/symfony/console/Input/ArrayInput.php | 191 + vendor/symfony/console/Input/Input.php | 174 + .../symfony/console/Input/InputArgument.php | 160 + .../console/Input/InputAwareInterface.php | 26 + .../symfony/console/Input/InputDefinition.php | 402 ++ .../symfony/console/Input/InputInterface.php | 138 + vendor/symfony/console/Input/InputOption.php | 262 ++ .../Input/StreamableInputInterface.php | 37 + vendor/symfony/console/Input/StringInput.php | 85 + .../console/Interaction/Interaction.php | 51 + vendor/symfony/console/LICENSE | 19 + .../symfony/console/Logger/ConsoleLogger.php | 120 + .../console/Messenger/RunCommandContext.php | 25 + .../console/Messenger/RunCommandMessage.php | 36 + .../Messenger/RunCommandMessageHandler.php | 56 + .../symfony/console/Output/AnsiColorMode.php | 106 + .../symfony/console/Output/BufferedOutput.php | 40 + .../symfony/console/Output/ConsoleOutput.php | 173 + .../console/Output/ConsoleOutputInterface.php | 30 + .../console/Output/ConsoleSectionOutput.php | 320 ++ vendor/symfony/console/Output/NullOutput.php | 94 + vendor/symfony/console/Output/Output.php | 144 + .../console/Output/OutputInterface.php | 103 + .../symfony/console/Output/StreamOutput.php | 127 + .../console/Output/TrimmedBufferOutput.php | 58 + .../console/Question/ChoiceQuestion.php | 178 + .../console/Question/ConfirmationQuestion.php | 57 + vendor/symfony/console/Question/Question.php | 319 ++ vendor/symfony/console/README.md | 27 + .../console/Resources/bin/hiddeninput.exe | Bin 0 -> 9216 bytes .../symfony/console/Resources/completion.bash | 122 + .../symfony/console/Resources/completion.fish | 36 + .../symfony/console/Resources/completion.zsh | 90 + .../console/SignalRegistry/SignalMap.php | 36 + .../console/SignalRegistry/SignalRegistry.php | 135 + .../console/SingleCommandApplication.php | 72 + vendor/symfony/console/Style/OutputStyle.php | 115 + .../symfony/console/Style/StyleInterface.php | 114 + vendor/symfony/console/Style/SymfonyStyle.php | 483 +++ vendor/symfony/console/Terminal.php | 227 + .../console/Tester/ApplicationTester.php | 85 + .../Tester/CommandCompletionTester.php | 54 + .../symfony/console/Tester/CommandTester.php | 77 + .../Tester/Constraint/CommandIsSuccessful.php | 43 + vendor/symfony/console/Tester/TesterTrait.php | 188 + vendor/symfony/console/composer.json | 55 + .../deprecation-contracts/CHANGELOG.md | 5 + vendor/symfony/deprecation-contracts/LICENSE | 19 + .../symfony/deprecation-contracts/README.md | 26 + .../deprecation-contracts/composer.json | 35 + .../deprecation-contracts/function.php | 27 + vendor/symfony/polyfill-ctype/Ctype.php | 232 + vendor/symfony/polyfill-ctype/LICENSE | 19 + vendor/symfony/polyfill-ctype/README.md | 12 + vendor/symfony/polyfill-ctype/bootstrap.php | 50 + vendor/symfony/polyfill-ctype/bootstrap80.php | 46 + vendor/symfony/polyfill-ctype/composer.json | 38 + .../polyfill-intl-grapheme/Grapheme.php | 518 +++ vendor/symfony/polyfill-intl-grapheme/LICENSE | 19 + .../symfony/polyfill-intl-grapheme/README.md | 32 + .../polyfill-intl-grapheme/bootstrap.php | 69 + .../polyfill-intl-grapheme/bootstrap80.php | 68 + .../polyfill-intl-grapheme/bootstrap85.php | 41 + .../polyfill-intl-grapheme/composer.json | 35 + .../symfony/polyfill-intl-normalizer/LICENSE | 19 + .../polyfill-intl-normalizer/Normalizer.php | 370 ++ .../polyfill-intl-normalizer/README.md | 14 + .../Resources/stubs/Normalizer.php | 17 + .../unidata/canonicalComposition.php | 945 ++++ .../unidata/canonicalDecomposition.php | 2065 +++++++++ .../Resources/unidata/combiningClass.php | 876 ++++ .../unidata/compatibilityDecomposition.php | 3695 ++++++++++++++++ .../unidata/rawCanonicalDecomposition.php | 2065 +++++++++ .../unidata/rawCompatibilityDecomposition.php | 3800 +++++++++++++++++ .../polyfill-intl-normalizer/bootstrap.php | 26 + .../polyfill-intl-normalizer/bootstrap80.php | 22 + .../polyfill-intl-normalizer/composer.json | 36 + vendor/symfony/polyfill-mbstring/LICENSE | 19 + vendor/symfony/polyfill-mbstring/Mbstring.php | 1662 +++++++ vendor/symfony/polyfill-mbstring/README.md | 13 + .../Resources/unidata/caseFolding.php | 119 + .../unidata/eastAsianWidthRegexp.php | 5 + .../Resources/unidata/lowerCase.php | 1397 ++++++ .../Resources/unidata/titleCase.php | 139 + .../Resources/unidata/titleCaseRegexp.php | 5 + .../Resources/unidata/upperCase.php | 1489 +++++++ .../symfony/polyfill-mbstring/bootstrap.php | 16 + .../symfony/polyfill-mbstring/bootstrap72.php | 173 + .../symfony/polyfill-mbstring/bootstrap80.php | 167 + .../symfony/polyfill-mbstring/composer.json | 39 + vendor/symfony/process/CHANGELOG.md | 134 + .../process/Exception/ExceptionInterface.php | 21 + .../Exception/InvalidArgumentException.php | 21 + .../process/Exception/LogicException.php | 21 + .../Exception/ProcessFailedException.php | 53 + .../Exception/ProcessSignaledException.php | 38 + .../Exception/ProcessStartFailedException.php | 43 + .../Exception/ProcessTimedOutException.php | 60 + .../Exception/RunProcessFailedException.php | 25 + .../process/Exception/RuntimeException.php | 21 + vendor/symfony/process/ExecutableFinder.php | 103 + vendor/symfony/process/InputStream.php | 91 + vendor/symfony/process/LICENSE | 19 + .../process/Messenger/RunProcessContext.php | 33 + .../process/Messenger/RunProcessMessage.php | 47 + .../Messenger/RunProcessMessageHandler.php | 36 + .../symfony/process/PhpExecutableFinder.php | 98 + vendor/symfony/process/PhpProcess.php | 74 + vendor/symfony/process/PhpSubprocess.php | 172 + .../symfony/process/Pipes/AbstractPipes.php | 218 + .../symfony/process/Pipes/PipesInterface.php | 61 + vendor/symfony/process/Pipes/UnixPipes.php | 144 + vendor/symfony/process/Pipes/WindowsPipes.php | 185 + vendor/symfony/process/Process.php | 1760 ++++++++ vendor/symfony/process/ProcessUtils.php | 64 + vendor/symfony/process/README.md | 13 + vendor/symfony/process/composer.json | 28 + .../service-contracts/Attribute/Required.php | 32 + .../Attribute/SubscribedService.php | 47 + vendor/symfony/service-contracts/CHANGELOG.md | 5 + .../ContainerAwareInterface.php | 19 + .../ContainerProviderInterface.php | 22 + vendor/symfony/service-contracts/LICENSE | 19 + vendor/symfony/service-contracts/README.md | 9 + .../service-contracts/ResetInterface.php | 33 + .../ServiceCollectionInterface.php | 26 + .../service-contracts/ServiceLocatorTrait.php | 114 + .../ServiceMethodsSubscriberTrait.php | 101 + .../ServiceProviderInterface.php | 45 + .../ServiceSubscriberInterface.php | 62 + .../ServiceSubscriberTrait.php | 84 + .../Test/ServiceLocatorTest.php | 19 + .../Test/ServiceLocatorTestCase.php | 97 + .../symfony/service-contracts/composer.json | 42 + vendor/symfony/string/AbstractString.php | 752 ++++ .../symfony/string/AbstractUnicodeString.php | 778 ++++ vendor/symfony/string/ByteString.php | 500 +++ vendor/symfony/string/CHANGELOG.md | 61 + vendor/symfony/string/CodePointString.php | 272 ++ .../string/Exception/ExceptionInterface.php | 16 + .../Exception/InvalidArgumentException.php | 16 + .../string/Exception/RuntimeException.php | 16 + .../string/Inflector/EnglishInflector.php | 626 +++ .../string/Inflector/FrenchInflector.php | 151 + .../string/Inflector/InflectorInterface.php | 33 + .../string/Inflector/SpanishInflector.php | 126 + vendor/symfony/string/LICENSE | 19 + vendor/symfony/string/LazyString.php | 174 + vendor/symfony/string/README.md | 14 + .../Resources/data/wcswidth_table_wide.php | 1203 ++++++ .../Resources/data/wcswidth_table_zero.php | 1503 +++++++ vendor/symfony/string/Resources/functions.php | 38 + .../symfony/string/Slugger/AsciiSlugger.php | 211 + .../string/Slugger/SluggerInterface.php | 27 + vendor/symfony/string/TruncateMode.php | 42 + vendor/symfony/string/UnicodeString.php | 525 +++ vendor/symfony/string/composer.json | 44 + vendor/symfony/yaml/CHANGELOG.md | 278 ++ vendor/symfony/yaml/Command/LintCommand.php | 277 ++ vendor/symfony/yaml/Dumper.php | 193 + vendor/symfony/yaml/Escaper.php | 97 + .../symfony/yaml/Exception/DumpException.php | 21 + .../yaml/Exception/ExceptionInterface.php | 21 + .../symfony/yaml/Exception/ParseException.php | 121 + .../yaml/Exception/RuntimeException.php | 21 + vendor/symfony/yaml/Inline.php | 1015 +++++ vendor/symfony/yaml/LICENSE | 19 + vendor/symfony/yaml/Parser.php | 1446 +++++++ vendor/symfony/yaml/ParserState.php | 69 + vendor/symfony/yaml/README.md | 13 + vendor/symfony/yaml/Resources/bin/yaml-lint | 54 + vendor/symfony/yaml/Tag/TaggedValue.php | 35 + vendor/symfony/yaml/Unescaper.php | 108 + vendor/symfony/yaml/Yaml.php | 105 + vendor/symfony/yaml/composer.json | 39 + 500 files changed, 81681 insertions(+), 5 deletions(-) create mode 100644 config/local.neon.example create mode 100644 deploy.php create mode 100644 deploy/apache-vhost.conf.example create mode 100644 vendor/bin/dep create mode 100644 vendor/bin/dep.bat create mode 100644 vendor/bin/yaml-lint create mode 100644 vendor/bin/yaml-lint.bat create mode 100644 vendor/deployer/deployer/.dockerignore create mode 100644 vendor/deployer/deployer/.php-cs-fixer.dist.php create mode 100644 vendor/deployer/deployer/Dockerfile create mode 100644 vendor/deployer/deployer/LICENSE create mode 100644 vendor/deployer/deployer/README.md create mode 100644 vendor/deployer/deployer/SECURITY.md create mode 100644 vendor/deployer/deployer/bin/build create mode 100644 vendor/deployer/deployer/bin/dep create mode 100644 vendor/deployer/deployer/bin/docgen create mode 100644 vendor/deployer/deployer/composer.json create mode 100644 vendor/deployer/deployer/contrib/bugsnag.php create mode 100644 vendor/deployer/deployer/contrib/cachetool.php create mode 100644 vendor/deployer/deployer/contrib/chatwork.php create mode 100644 vendor/deployer/deployer/contrib/cimonitor.php create mode 100644 vendor/deployer/deployer/contrib/cloudflare.php create mode 100644 vendor/deployer/deployer/contrib/cpanel.php create mode 100644 vendor/deployer/deployer/contrib/crontab.php create mode 100644 vendor/deployer/deployer/contrib/directadmin.php create mode 100644 vendor/deployer/deployer/contrib/discord.php create mode 100644 vendor/deployer/deployer/contrib/grafana.php create mode 100644 vendor/deployer/deployer/contrib/hangouts.php create mode 100644 vendor/deployer/deployer/contrib/hipchat.php create mode 100644 vendor/deployer/deployer/contrib/ispmanager.php create mode 100644 vendor/deployer/deployer/contrib/mattermost.php create mode 100644 vendor/deployer/deployer/contrib/ms-teams.php create mode 100644 vendor/deployer/deployer/contrib/newrelic.php create mode 100644 vendor/deployer/deployer/contrib/npm.php create mode 100644 vendor/deployer/deployer/contrib/ntfy.php create mode 100644 vendor/deployer/deployer/contrib/phinx.php create mode 100644 vendor/deployer/deployer/contrib/php-fpm.php create mode 100644 vendor/deployer/deployer/contrib/rabbit.php create mode 100644 vendor/deployer/deployer/contrib/raygun.php create mode 100644 vendor/deployer/deployer/contrib/rocketchat.php create mode 100644 vendor/deployer/deployer/contrib/rollbar.php create mode 100644 vendor/deployer/deployer/contrib/rsync.php create mode 100644 vendor/deployer/deployer/contrib/sentry.php create mode 100644 vendor/deployer/deployer/contrib/slack.php create mode 100644 vendor/deployer/deployer/contrib/supervisord-monitor.php create mode 100644 vendor/deployer/deployer/contrib/telegram.php create mode 100644 vendor/deployer/deployer/contrib/webpack_encore.php create mode 100644 vendor/deployer/deployer/contrib/workplace.php create mode 100644 vendor/deployer/deployer/contrib/yammer.php create mode 100644 vendor/deployer/deployer/contrib/yarn.php create mode 100644 vendor/deployer/deployer/recipe/cakephp.php create mode 100644 vendor/deployer/deployer/recipe/codeigniter.php create mode 100644 vendor/deployer/deployer/recipe/codeigniter4.php create mode 100644 vendor/deployer/deployer/recipe/common.php create mode 100644 vendor/deployer/deployer/recipe/composer.php create mode 100644 vendor/deployer/deployer/recipe/contao.php create mode 100644 vendor/deployer/deployer/recipe/craftcms.php create mode 100644 vendor/deployer/deployer/recipe/deploy/check_remote.php create mode 100644 vendor/deployer/deployer/recipe/deploy/cleanup.php create mode 100644 vendor/deployer/deployer/recipe/deploy/clear_paths.php create mode 100644 vendor/deployer/deployer/recipe/deploy/copy_dirs.php create mode 100644 vendor/deployer/deployer/recipe/deploy/env.php create mode 100644 vendor/deployer/deployer/recipe/deploy/info.php create mode 100644 vendor/deployer/deployer/recipe/deploy/lock.php create mode 100644 vendor/deployer/deployer/recipe/deploy/push.php create mode 100644 vendor/deployer/deployer/recipe/deploy/release.php create mode 100644 vendor/deployer/deployer/recipe/deploy/rollback.php create mode 100644 vendor/deployer/deployer/recipe/deploy/setup.php create mode 100644 vendor/deployer/deployer/recipe/deploy/shared.php create mode 100644 vendor/deployer/deployer/recipe/deploy/symlink.php create mode 100644 vendor/deployer/deployer/recipe/deploy/update_code.php create mode 100644 vendor/deployer/deployer/recipe/deploy/vendors.php create mode 100644 vendor/deployer/deployer/recipe/deploy/writable.php create mode 100644 vendor/deployer/deployer/recipe/drupal7.php create mode 100644 vendor/deployer/deployer/recipe/drupal8.php create mode 100644 vendor/deployer/deployer/recipe/flow_framework.php create mode 100644 vendor/deployer/deployer/recipe/fuelphp.php create mode 100644 vendor/deployer/deployer/recipe/joomla.php create mode 100644 vendor/deployer/deployer/recipe/laravel.php create mode 100644 vendor/deployer/deployer/recipe/magento.php create mode 100644 vendor/deployer/deployer/recipe/magento2.php create mode 100644 vendor/deployer/deployer/recipe/pimcore.php create mode 100644 vendor/deployer/deployer/recipe/prestashop.php create mode 100644 vendor/deployer/deployer/recipe/provision.php create mode 100644 vendor/deployer/deployer/recipe/provision/404.html create mode 100644 vendor/deployer/deployer/recipe/provision/Caddyfile create mode 100644 vendor/deployer/deployer/recipe/provision/databases.php create mode 100644 vendor/deployer/deployer/recipe/provision/nodejs.php create mode 100644 vendor/deployer/deployer/recipe/provision/php.php create mode 100644 vendor/deployer/deployer/recipe/provision/user.php create mode 100644 vendor/deployer/deployer/recipe/provision/website.php create mode 100644 vendor/deployer/deployer/recipe/shopware.php create mode 100644 vendor/deployer/deployer/recipe/silverstripe.php create mode 100644 vendor/deployer/deployer/recipe/spiral.php create mode 100644 vendor/deployer/deployer/recipe/statamic.php create mode 100644 vendor/deployer/deployer/recipe/sulu.php create mode 100644 vendor/deployer/deployer/recipe/symfony.php create mode 100644 vendor/deployer/deployer/recipe/typo3.php create mode 100644 vendor/deployer/deployer/recipe/wordpress.php create mode 100644 vendor/deployer/deployer/recipe/yii.php create mode 100644 vendor/deployer/deployer/recipe/zend_framework.php create mode 100644 vendor/deployer/deployer/src/Collection/Collection.php create mode 100644 vendor/deployer/deployer/src/Command/BlackjackCommand.php create mode 100644 vendor/deployer/deployer/src/Command/CommandCommon.php create mode 100644 vendor/deployer/deployer/src/Command/ConfigCommand.php create mode 100644 vendor/deployer/deployer/src/Command/CustomOption.php create mode 100644 vendor/deployer/deployer/src/Command/InitCommand.php create mode 100644 vendor/deployer/deployer/src/Command/MainCommand.php create mode 100644 vendor/deployer/deployer/src/Command/RunCommand.php create mode 100644 vendor/deployer/deployer/src/Command/SelectCommand.php create mode 100644 vendor/deployer/deployer/src/Command/SshCommand.php create mode 100644 vendor/deployer/deployer/src/Command/TreeCommand.php create mode 100644 vendor/deployer/deployer/src/Command/WorkerCommand.php create mode 100644 vendor/deployer/deployer/src/Component/Pimple/Container.php create mode 100644 vendor/deployer/deployer/src/Component/Pimple/Exception/ExpectedInvokableException.php create mode 100644 vendor/deployer/deployer/src/Component/Pimple/Exception/FrozenServiceException.php create mode 100644 vendor/deployer/deployer/src/Component/Pimple/Exception/InvalidServiceIdentifierException.php create mode 100644 vendor/deployer/deployer/src/Component/Pimple/Exception/UnknownIdentifierException.php create mode 100644 vendor/deployer/deployer/src/Configuration.php create mode 100644 vendor/deployer/deployer/src/Deployer.php create mode 100644 vendor/deployer/deployer/src/Documentation/ApiGen.php create mode 100644 vendor/deployer/deployer/src/Documentation/DocConfig.php create mode 100644 vendor/deployer/deployer/src/Documentation/DocGen.php create mode 100644 vendor/deployer/deployer/src/Documentation/DocRecipe.php create mode 100644 vendor/deployer/deployer/src/Documentation/DocTask.php create mode 100644 vendor/deployer/deployer/src/Exception/ConfigurationException.php create mode 100644 vendor/deployer/deployer/src/Exception/Exception.php create mode 100644 vendor/deployer/deployer/src/Exception/GracefulShutdownException.php create mode 100644 vendor/deployer/deployer/src/Exception/HttpieException.php create mode 100644 vendor/deployer/deployer/src/Exception/RunException.php create mode 100644 vendor/deployer/deployer/src/Exception/SchemaException.php create mode 100644 vendor/deployer/deployer/src/Exception/TimeoutException.php create mode 100644 vendor/deployer/deployer/src/Exception/WillAskUser.php create mode 100644 vendor/deployer/deployer/src/Executor/Master.php create mode 100644 vendor/deployer/deployer/src/Executor/Planner.php create mode 100644 vendor/deployer/deployer/src/Executor/Response.php create mode 100644 vendor/deployer/deployer/src/Executor/Server.php create mode 100644 vendor/deployer/deployer/src/Executor/Worker.php create mode 100644 vendor/deployer/deployer/src/Host/Host.php create mode 100644 vendor/deployer/deployer/src/Host/HostCollection.php create mode 100644 vendor/deployer/deployer/src/Host/Localhost.php create mode 100644 vendor/deployer/deployer/src/Host/Range.php create mode 100644 vendor/deployer/deployer/src/Import/Import.php create mode 100644 vendor/deployer/deployer/src/Import/MamlRecipe.php create mode 100644 vendor/deployer/deployer/src/Import/YamlRecipe.php create mode 100644 vendor/deployer/deployer/src/Logger/Handler/FileHandler.php create mode 100644 vendor/deployer/deployer/src/Logger/Handler/HandlerInterface.php create mode 100644 vendor/deployer/deployer/src/Logger/Handler/NullHandler.php create mode 100644 vendor/deployer/deployer/src/Logger/Logger.php create mode 100644 vendor/deployer/deployer/src/ProcessRunner/ProcessRunner.php create mode 100644 vendor/deployer/deployer/src/Selector/Selector.php create mode 100644 vendor/deployer/deployer/src/Ssh/IOArguments.php create mode 100644 vendor/deployer/deployer/src/Ssh/RunParams.php create mode 100644 vendor/deployer/deployer/src/Ssh/SshClient.php create mode 100644 vendor/deployer/deployer/src/Support/ObjectProxy.php create mode 100644 vendor/deployer/deployer/src/Support/Reporter.php create mode 100644 vendor/deployer/deployer/src/Support/helpers.php create mode 100644 vendor/deployer/deployer/src/Task/Context.php create mode 100644 vendor/deployer/deployer/src/Task/GroupTask.php create mode 100644 vendor/deployer/deployer/src/Task/ScriptManager.php create mode 100644 vendor/deployer/deployer/src/Task/Task.php create mode 100644 vendor/deployer/deployer/src/Task/TaskCollection.php create mode 100644 vendor/deployer/deployer/src/Utility/HttpResponse.php create mode 100644 vendor/deployer/deployer/src/Utility/Httpie.php create mode 100644 vendor/deployer/deployer/src/Utility/Rsync.php create mode 100644 vendor/deployer/deployer/src/functions.php create mode 100644 vendor/deployer/deployer/src/schema.json create mode 100644 vendor/maml/maml/.php-cs-fixer.php create mode 100644 vendor/maml/maml/LICENSE create mode 100644 vendor/maml/maml/README.md create mode 100644 vendor/maml/maml/composer.json create mode 100644 vendor/maml/maml/phpstan.neon create mode 100644 vendor/maml/maml/src/Annotated.php create mode 100644 vendor/maml/maml/src/Ast/ArrayNode.php create mode 100644 vendor/maml/maml/src/Ast/BooleanNode.php create mode 100644 vendor/maml/maml/src/Ast/CommentNode.php create mode 100644 vendor/maml/maml/src/Ast/Document.php create mode 100644 vendor/maml/maml/src/Ast/Element.php create mode 100644 vendor/maml/maml/src/Ast/FloatNode.php create mode 100644 vendor/maml/maml/src/Ast/IdentifierKey.php create mode 100644 vendor/maml/maml/src/Ast/IntegerNode.php create mode 100644 vendor/maml/maml/src/Ast/NullNode.php create mode 100644 vendor/maml/maml/src/Ast/ObjectNode.php create mode 100644 vendor/maml/maml/src/Ast/Position.php create mode 100644 vendor/maml/maml/src/Ast/Property.php create mode 100644 vendor/maml/maml/src/Ast/RawStringNode.php create mode 100644 vendor/maml/maml/src/Ast/Span.php create mode 100644 vendor/maml/maml/src/Ast/StringNode.php create mode 100644 vendor/maml/maml/src/AstParser.php create mode 100644 vendor/maml/maml/src/AstPrinter.php create mode 100644 vendor/maml/maml/src/Maml.php create mode 100644 vendor/maml/maml/src/ParseException.php create mode 100644 vendor/maml/maml/src/Parser.php create mode 100644 vendor/maml/maml/src/Schema/JsonSchemaGenerator.php create mode 100644 vendor/maml/maml/src/Schema/S.php create mode 100644 vendor/maml/maml/src/Schema/SchemaType.php create mode 100644 vendor/maml/maml/src/Schema/Type/AnyType.php create mode 100644 vendor/maml/maml/src/Schema/Type/ArrayOfType.php create mode 100644 vendor/maml/maml/src/Schema/Type/BooleanType.php create mode 100644 vendor/maml/maml/src/Schema/Type/FloatType.php create mode 100644 vendor/maml/maml/src/Schema/Type/IntegerType.php create mode 100644 vendor/maml/maml/src/Schema/Type/LiteralType.php create mode 100644 vendor/maml/maml/src/Schema/Type/MapType.php create mode 100644 vendor/maml/maml/src/Schema/Type/NullType.php create mode 100644 vendor/maml/maml/src/Schema/Type/NumberType.php create mode 100644 vendor/maml/maml/src/Schema/Type/ObjectType.php create mode 100644 vendor/maml/maml/src/Schema/Type/OptionalType.php create mode 100644 vendor/maml/maml/src/Schema/Type/OrderedObjectType.php create mode 100644 vendor/maml/maml/src/Schema/Type/StringType.php create mode 100644 vendor/maml/maml/src/Schema/Type/TupleType.php create mode 100644 vendor/maml/maml/src/Schema/Type/UnionType.php create mode 100644 vendor/maml/maml/src/Schema/ValidationError.php create mode 100644 vendor/maml/maml/src/Schema/Validator.php create mode 100644 vendor/maml/maml/src/Stringifier.php create mode 100644 vendor/psr/container/.gitignore create mode 100644 vendor/psr/container/LICENSE create mode 100644 vendor/psr/container/README.md create mode 100644 vendor/psr/container/composer.json create mode 100644 vendor/psr/container/src/ContainerExceptionInterface.php create mode 100644 vendor/psr/container/src/ContainerInterface.php create mode 100644 vendor/psr/container/src/NotFoundExceptionInterface.php create mode 100644 vendor/symfony/console/Application.php create mode 100644 vendor/symfony/console/Attribute/Argument.php create mode 100644 vendor/symfony/console/Attribute/AsCommand.php create mode 100644 vendor/symfony/console/Attribute/Ask.php create mode 100644 vendor/symfony/console/Attribute/Interact.php create mode 100644 vendor/symfony/console/Attribute/InteractiveAttributeInterface.php create mode 100644 vendor/symfony/console/Attribute/MapInput.php create mode 100644 vendor/symfony/console/Attribute/Option.php create mode 100644 vendor/symfony/console/Attribute/Reflection/ReflectionMember.php create mode 100644 vendor/symfony/console/CHANGELOG.md create mode 100644 vendor/symfony/console/CI/GithubActionReporter.php create mode 100644 vendor/symfony/console/Color.php create mode 100644 vendor/symfony/console/Command/Command.php create mode 100644 vendor/symfony/console/Command/CompleteCommand.php create mode 100644 vendor/symfony/console/Command/DumpCompletionCommand.php create mode 100644 vendor/symfony/console/Command/HelpCommand.php create mode 100644 vendor/symfony/console/Command/InvokableCommand.php create mode 100644 vendor/symfony/console/Command/LazyCommand.php create mode 100644 vendor/symfony/console/Command/ListCommand.php create mode 100644 vendor/symfony/console/Command/LockableTrait.php create mode 100644 vendor/symfony/console/Command/SignalableCommandInterface.php create mode 100644 vendor/symfony/console/Command/TraceableCommand.php create mode 100644 vendor/symfony/console/CommandLoader/CommandLoaderInterface.php create mode 100644 vendor/symfony/console/CommandLoader/ContainerCommandLoader.php create mode 100644 vendor/symfony/console/CommandLoader/FactoryCommandLoader.php create mode 100644 vendor/symfony/console/Completion/CompletionInput.php create mode 100644 vendor/symfony/console/Completion/CompletionSuggestions.php create mode 100644 vendor/symfony/console/Completion/Output/BashCompletionOutput.php create mode 100644 vendor/symfony/console/Completion/Output/CompletionOutputInterface.php create mode 100644 vendor/symfony/console/Completion/Output/FishCompletionOutput.php create mode 100644 vendor/symfony/console/Completion/Output/ZshCompletionOutput.php create mode 100644 vendor/symfony/console/Completion/Suggestion.php create mode 100644 vendor/symfony/console/ConsoleEvents.php create mode 100644 vendor/symfony/console/Cursor.php create mode 100644 vendor/symfony/console/DataCollector/CommandDataCollector.php create mode 100644 vendor/symfony/console/Debug/CliRequest.php create mode 100644 vendor/symfony/console/DependencyInjection/AddConsoleCommandPass.php create mode 100644 vendor/symfony/console/Descriptor/ApplicationDescription.php create mode 100644 vendor/symfony/console/Descriptor/Descriptor.php create mode 100644 vendor/symfony/console/Descriptor/DescriptorInterface.php create mode 100644 vendor/symfony/console/Descriptor/JsonDescriptor.php create mode 100644 vendor/symfony/console/Descriptor/MarkdownDescriptor.php create mode 100644 vendor/symfony/console/Descriptor/ReStructuredTextDescriptor.php create mode 100644 vendor/symfony/console/Descriptor/TextDescriptor.php create mode 100644 vendor/symfony/console/Descriptor/XmlDescriptor.php create mode 100644 vendor/symfony/console/Event/ConsoleAlarmEvent.php create mode 100644 vendor/symfony/console/Event/ConsoleCommandEvent.php create mode 100644 vendor/symfony/console/Event/ConsoleErrorEvent.php create mode 100644 vendor/symfony/console/Event/ConsoleEvent.php create mode 100644 vendor/symfony/console/Event/ConsoleSignalEvent.php create mode 100644 vendor/symfony/console/Event/ConsoleTerminateEvent.php create mode 100644 vendor/symfony/console/EventListener/ErrorListener.php create mode 100644 vendor/symfony/console/Exception/CommandNotFoundException.php create mode 100644 vendor/symfony/console/Exception/ExceptionInterface.php create mode 100644 vendor/symfony/console/Exception/InvalidArgumentException.php create mode 100644 vendor/symfony/console/Exception/InvalidOptionException.php create mode 100644 vendor/symfony/console/Exception/LogicException.php create mode 100644 vendor/symfony/console/Exception/MissingInputException.php create mode 100644 vendor/symfony/console/Exception/NamespaceNotFoundException.php create mode 100644 vendor/symfony/console/Exception/RunCommandFailedException.php create mode 100644 vendor/symfony/console/Exception/RuntimeException.php create mode 100644 vendor/symfony/console/Formatter/NullOutputFormatter.php create mode 100644 vendor/symfony/console/Formatter/NullOutputFormatterStyle.php create mode 100644 vendor/symfony/console/Formatter/OutputFormatter.php create mode 100644 vendor/symfony/console/Formatter/OutputFormatterInterface.php create mode 100644 vendor/symfony/console/Formatter/OutputFormatterStyle.php create mode 100644 vendor/symfony/console/Formatter/OutputFormatterStyleInterface.php create mode 100644 vendor/symfony/console/Formatter/OutputFormatterStyleStack.php create mode 100644 vendor/symfony/console/Formatter/WrappableOutputFormatterInterface.php create mode 100644 vendor/symfony/console/Helper/DebugFormatterHelper.php create mode 100644 vendor/symfony/console/Helper/DescriptorHelper.php create mode 100644 vendor/symfony/console/Helper/Dumper.php create mode 100644 vendor/symfony/console/Helper/FormatterHelper.php create mode 100644 vendor/symfony/console/Helper/Helper.php create mode 100644 vendor/symfony/console/Helper/HelperInterface.php create mode 100644 vendor/symfony/console/Helper/HelperSet.php create mode 100644 vendor/symfony/console/Helper/InputAwareHelper.php create mode 100644 vendor/symfony/console/Helper/OutputWrapper.php create mode 100644 vendor/symfony/console/Helper/ProcessHelper.php create mode 100644 vendor/symfony/console/Helper/ProgressBar.php create mode 100644 vendor/symfony/console/Helper/ProgressIndicator.php create mode 100644 vendor/symfony/console/Helper/QuestionHelper.php create mode 100644 vendor/symfony/console/Helper/SymfonyQuestionHelper.php create mode 100644 vendor/symfony/console/Helper/Table.php create mode 100644 vendor/symfony/console/Helper/TableCell.php create mode 100644 vendor/symfony/console/Helper/TableCellStyle.php create mode 100644 vendor/symfony/console/Helper/TableRows.php create mode 100644 vendor/symfony/console/Helper/TableSeparator.php create mode 100644 vendor/symfony/console/Helper/TableStyle.php create mode 100644 vendor/symfony/console/Helper/TerminalInputHelper.php create mode 100644 vendor/symfony/console/Helper/TreeHelper.php create mode 100644 vendor/symfony/console/Helper/TreeNode.php create mode 100644 vendor/symfony/console/Helper/TreeStyle.php create mode 100644 vendor/symfony/console/Input/ArgvInput.php create mode 100644 vendor/symfony/console/Input/ArrayInput.php create mode 100644 vendor/symfony/console/Input/Input.php create mode 100644 vendor/symfony/console/Input/InputArgument.php create mode 100644 vendor/symfony/console/Input/InputAwareInterface.php create mode 100644 vendor/symfony/console/Input/InputDefinition.php create mode 100644 vendor/symfony/console/Input/InputInterface.php create mode 100644 vendor/symfony/console/Input/InputOption.php create mode 100644 vendor/symfony/console/Input/StreamableInputInterface.php create mode 100644 vendor/symfony/console/Input/StringInput.php create mode 100644 vendor/symfony/console/Interaction/Interaction.php create mode 100644 vendor/symfony/console/LICENSE create mode 100644 vendor/symfony/console/Logger/ConsoleLogger.php create mode 100644 vendor/symfony/console/Messenger/RunCommandContext.php create mode 100644 vendor/symfony/console/Messenger/RunCommandMessage.php create mode 100644 vendor/symfony/console/Messenger/RunCommandMessageHandler.php create mode 100644 vendor/symfony/console/Output/AnsiColorMode.php create mode 100644 vendor/symfony/console/Output/BufferedOutput.php create mode 100644 vendor/symfony/console/Output/ConsoleOutput.php create mode 100644 vendor/symfony/console/Output/ConsoleOutputInterface.php create mode 100644 vendor/symfony/console/Output/ConsoleSectionOutput.php create mode 100644 vendor/symfony/console/Output/NullOutput.php create mode 100644 vendor/symfony/console/Output/Output.php create mode 100644 vendor/symfony/console/Output/OutputInterface.php create mode 100644 vendor/symfony/console/Output/StreamOutput.php create mode 100644 vendor/symfony/console/Output/TrimmedBufferOutput.php create mode 100644 vendor/symfony/console/Question/ChoiceQuestion.php create mode 100644 vendor/symfony/console/Question/ConfirmationQuestion.php create mode 100644 vendor/symfony/console/Question/Question.php create mode 100644 vendor/symfony/console/README.md create mode 100644 vendor/symfony/console/Resources/bin/hiddeninput.exe create mode 100644 vendor/symfony/console/Resources/completion.bash create mode 100644 vendor/symfony/console/Resources/completion.fish create mode 100644 vendor/symfony/console/Resources/completion.zsh create mode 100644 vendor/symfony/console/SignalRegistry/SignalMap.php create mode 100644 vendor/symfony/console/SignalRegistry/SignalRegistry.php create mode 100644 vendor/symfony/console/SingleCommandApplication.php create mode 100644 vendor/symfony/console/Style/OutputStyle.php create mode 100644 vendor/symfony/console/Style/StyleInterface.php create mode 100644 vendor/symfony/console/Style/SymfonyStyle.php create mode 100644 vendor/symfony/console/Terminal.php create mode 100644 vendor/symfony/console/Tester/ApplicationTester.php create mode 100644 vendor/symfony/console/Tester/CommandCompletionTester.php create mode 100644 vendor/symfony/console/Tester/CommandTester.php create mode 100644 vendor/symfony/console/Tester/Constraint/CommandIsSuccessful.php create mode 100644 vendor/symfony/console/Tester/TesterTrait.php create mode 100644 vendor/symfony/console/composer.json create mode 100644 vendor/symfony/deprecation-contracts/CHANGELOG.md create mode 100644 vendor/symfony/deprecation-contracts/LICENSE create mode 100644 vendor/symfony/deprecation-contracts/README.md create mode 100644 vendor/symfony/deprecation-contracts/composer.json create mode 100644 vendor/symfony/deprecation-contracts/function.php create mode 100644 vendor/symfony/polyfill-ctype/Ctype.php create mode 100644 vendor/symfony/polyfill-ctype/LICENSE create mode 100644 vendor/symfony/polyfill-ctype/README.md create mode 100644 vendor/symfony/polyfill-ctype/bootstrap.php create mode 100644 vendor/symfony/polyfill-ctype/bootstrap80.php create mode 100644 vendor/symfony/polyfill-ctype/composer.json create mode 100644 vendor/symfony/polyfill-intl-grapheme/Grapheme.php create mode 100644 vendor/symfony/polyfill-intl-grapheme/LICENSE create mode 100644 vendor/symfony/polyfill-intl-grapheme/README.md create mode 100644 vendor/symfony/polyfill-intl-grapheme/bootstrap.php create mode 100644 vendor/symfony/polyfill-intl-grapheme/bootstrap80.php create mode 100644 vendor/symfony/polyfill-intl-grapheme/bootstrap85.php create mode 100644 vendor/symfony/polyfill-intl-grapheme/composer.json create mode 100644 vendor/symfony/polyfill-intl-normalizer/LICENSE create mode 100644 vendor/symfony/polyfill-intl-normalizer/Normalizer.php create mode 100644 vendor/symfony/polyfill-intl-normalizer/README.md create mode 100644 vendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php create mode 100644 vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalComposition.php create mode 100644 vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php create mode 100644 vendor/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.php create mode 100644 vendor/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.php create mode 100644 vendor/symfony/polyfill-intl-normalizer/Resources/unidata/rawCanonicalDecomposition.php create mode 100644 vendor/symfony/polyfill-intl-normalizer/Resources/unidata/rawCompatibilityDecomposition.php create mode 100644 vendor/symfony/polyfill-intl-normalizer/bootstrap.php create mode 100644 vendor/symfony/polyfill-intl-normalizer/bootstrap80.php create mode 100644 vendor/symfony/polyfill-intl-normalizer/composer.json create mode 100644 vendor/symfony/polyfill-mbstring/LICENSE create mode 100644 vendor/symfony/polyfill-mbstring/Mbstring.php create mode 100644 vendor/symfony/polyfill-mbstring/README.md create mode 100644 vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php create mode 100644 vendor/symfony/polyfill-mbstring/Resources/unidata/eastAsianWidthRegexp.php create mode 100644 vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php create mode 100644 vendor/symfony/polyfill-mbstring/Resources/unidata/titleCase.php create mode 100644 vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php create mode 100644 vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.php create mode 100644 vendor/symfony/polyfill-mbstring/bootstrap.php create mode 100644 vendor/symfony/polyfill-mbstring/bootstrap72.php create mode 100644 vendor/symfony/polyfill-mbstring/bootstrap80.php create mode 100644 vendor/symfony/polyfill-mbstring/composer.json create mode 100644 vendor/symfony/process/CHANGELOG.md create mode 100644 vendor/symfony/process/Exception/ExceptionInterface.php create mode 100644 vendor/symfony/process/Exception/InvalidArgumentException.php create mode 100644 vendor/symfony/process/Exception/LogicException.php create mode 100644 vendor/symfony/process/Exception/ProcessFailedException.php create mode 100644 vendor/symfony/process/Exception/ProcessSignaledException.php create mode 100644 vendor/symfony/process/Exception/ProcessStartFailedException.php create mode 100644 vendor/symfony/process/Exception/ProcessTimedOutException.php create mode 100644 vendor/symfony/process/Exception/RunProcessFailedException.php create mode 100644 vendor/symfony/process/Exception/RuntimeException.php create mode 100644 vendor/symfony/process/ExecutableFinder.php create mode 100644 vendor/symfony/process/InputStream.php create mode 100644 vendor/symfony/process/LICENSE create mode 100644 vendor/symfony/process/Messenger/RunProcessContext.php create mode 100644 vendor/symfony/process/Messenger/RunProcessMessage.php create mode 100644 vendor/symfony/process/Messenger/RunProcessMessageHandler.php create mode 100644 vendor/symfony/process/PhpExecutableFinder.php create mode 100644 vendor/symfony/process/PhpProcess.php create mode 100644 vendor/symfony/process/PhpSubprocess.php create mode 100644 vendor/symfony/process/Pipes/AbstractPipes.php create mode 100644 vendor/symfony/process/Pipes/PipesInterface.php create mode 100644 vendor/symfony/process/Pipes/UnixPipes.php create mode 100644 vendor/symfony/process/Pipes/WindowsPipes.php create mode 100644 vendor/symfony/process/Process.php create mode 100644 vendor/symfony/process/ProcessUtils.php create mode 100644 vendor/symfony/process/README.md create mode 100644 vendor/symfony/process/composer.json create mode 100644 vendor/symfony/service-contracts/Attribute/Required.php create mode 100644 vendor/symfony/service-contracts/Attribute/SubscribedService.php create mode 100644 vendor/symfony/service-contracts/CHANGELOG.md create mode 100644 vendor/symfony/service-contracts/ContainerAwareInterface.php create mode 100644 vendor/symfony/service-contracts/ContainerProviderInterface.php create mode 100644 vendor/symfony/service-contracts/LICENSE create mode 100644 vendor/symfony/service-contracts/README.md create mode 100644 vendor/symfony/service-contracts/ResetInterface.php create mode 100644 vendor/symfony/service-contracts/ServiceCollectionInterface.php create mode 100644 vendor/symfony/service-contracts/ServiceLocatorTrait.php create mode 100644 vendor/symfony/service-contracts/ServiceMethodsSubscriberTrait.php create mode 100644 vendor/symfony/service-contracts/ServiceProviderInterface.php create mode 100644 vendor/symfony/service-contracts/ServiceSubscriberInterface.php create mode 100644 vendor/symfony/service-contracts/ServiceSubscriberTrait.php create mode 100644 vendor/symfony/service-contracts/Test/ServiceLocatorTest.php create mode 100644 vendor/symfony/service-contracts/Test/ServiceLocatorTestCase.php create mode 100644 vendor/symfony/service-contracts/composer.json create mode 100644 vendor/symfony/string/AbstractString.php create mode 100644 vendor/symfony/string/AbstractUnicodeString.php create mode 100644 vendor/symfony/string/ByteString.php create mode 100644 vendor/symfony/string/CHANGELOG.md create mode 100644 vendor/symfony/string/CodePointString.php create mode 100644 vendor/symfony/string/Exception/ExceptionInterface.php create mode 100644 vendor/symfony/string/Exception/InvalidArgumentException.php create mode 100644 vendor/symfony/string/Exception/RuntimeException.php create mode 100644 vendor/symfony/string/Inflector/EnglishInflector.php create mode 100644 vendor/symfony/string/Inflector/FrenchInflector.php create mode 100644 vendor/symfony/string/Inflector/InflectorInterface.php create mode 100644 vendor/symfony/string/Inflector/SpanishInflector.php create mode 100644 vendor/symfony/string/LICENSE create mode 100644 vendor/symfony/string/LazyString.php create mode 100644 vendor/symfony/string/README.md create mode 100644 vendor/symfony/string/Resources/data/wcswidth_table_wide.php create mode 100644 vendor/symfony/string/Resources/data/wcswidth_table_zero.php create mode 100644 vendor/symfony/string/Resources/functions.php create mode 100644 vendor/symfony/string/Slugger/AsciiSlugger.php create mode 100644 vendor/symfony/string/Slugger/SluggerInterface.php create mode 100644 vendor/symfony/string/TruncateMode.php create mode 100644 vendor/symfony/string/UnicodeString.php create mode 100644 vendor/symfony/string/composer.json create mode 100644 vendor/symfony/yaml/CHANGELOG.md create mode 100644 vendor/symfony/yaml/Command/LintCommand.php create mode 100644 vendor/symfony/yaml/Dumper.php create mode 100644 vendor/symfony/yaml/Escaper.php create mode 100644 vendor/symfony/yaml/Exception/DumpException.php create mode 100644 vendor/symfony/yaml/Exception/ExceptionInterface.php create mode 100644 vendor/symfony/yaml/Exception/ParseException.php create mode 100644 vendor/symfony/yaml/Exception/RuntimeException.php create mode 100644 vendor/symfony/yaml/Inline.php create mode 100644 vendor/symfony/yaml/LICENSE create mode 100644 vendor/symfony/yaml/Parser.php create mode 100644 vendor/symfony/yaml/ParserState.php create mode 100644 vendor/symfony/yaml/README.md create mode 100644 vendor/symfony/yaml/Resources/bin/yaml-lint create mode 100644 vendor/symfony/yaml/Tag/TaggedValue.php create mode 100644 vendor/symfony/yaml/Unescaper.php create mode 100644 vendor/symfony/yaml/Yaml.php create mode 100644 vendor/symfony/yaml/composer.json diff --git a/.gitignore b/.gitignore index 03db9fa..9012145 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ temp/ log/ data/sdf/* !data/sdf/.gitkeep +config/local.neon #ZAZA diff --git a/app/Bootstrap.php b/app/Bootstrap.php index fd322c4..0f9961b 100644 --- a/app/Bootstrap.php +++ b/app/Bootstrap.php @@ -42,6 +42,11 @@ class Bootstrap $configurator->addConfig($rootDir . '/config/common.neon'); $configurator->addConfig($rootDir . '/config/services.neon'); + $localConfig = $rootDir . '/config/local.neon'; + if (is_file($localConfig) && filesize($localConfig) > 0) { + $configurator->addConfig($localConfig); + } + return $configurator; } } diff --git a/composer.json b/composer.json index cec3db2..f3be317 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ }, "require-dev": { "nette/tester": "^2.5", - "symfony/thanks": "^1" + "symfony/thanks": "^1", + "deployer/deployer": "^8.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 3b92f2a..d942146 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3b179288bc4b8e0791654939f497738d", + "content-hash": "04d8602ccd11da64fa89499e47cd7d43", "packages": [ { "name": "latte/latte", @@ -1797,6 +1797,125 @@ } ], "packages-dev": [ + { + "name": "deployer/deployer", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/deployphp/deployer.git", + "reference": "199273500b4f14ff5eafd76bea524e5d44bcaff3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/deployphp/deployer/zipball/199273500b4f14ff5eafd76bea524e5d44bcaff3", + "reference": "199273500b4f14ff5eafd76bea524e5d44bcaff3", + "shasum": "" + }, + "require": { + "maml/maml": "^3.2", + "php": "^8.3", + "symfony/console": "^7.4.0 || ^8.0.0", + "symfony/process": "^7.4.0 || ^8.0.0", + "symfony/yaml": "^7.4.0 || ^8.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.68", + "phpstan/phpstan": "^2.1", + "phpunit/php-code-coverage": "^12.0", + "phpunit/phpunit": "^12.5" + }, + "bin": [ + "bin/dep" + ], + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Support/helpers.php" + ], + "psr-4": { + "Deployer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Medvedev", + "email": "anton@medv.io" + } + ], + "description": "Deployment Tool", + "homepage": "https://deployer.org", + "support": { + "docs": "https://deployer.org/docs", + "issues": "https://github.com/deployphp/deployer/issues", + "source": "https://github.com/deployphp/deployer" + }, + "funding": [ + { + "url": "https://github.com/sponsors/antonmedv", + "type": "github" + } + ], + "time": "2026-05-18T16:59:34+00:00" + }, + { + "name": "maml/maml", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/maml-dev/maml-php.git", + "reference": "f11cdff4325bf399f23c33fc1995450e40ac0854" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maml-dev/maml-php/zipball/f11cdff4325bf399f23c33fc1995450e40ac0854", + "reference": "f11cdff4325bf399f23c33fc1995450e40ac0854", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2" + }, + "require-dev": { + "php-cs-fixer/shim": "^3.94", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Maml\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Medvedev", + "email": "anton@medv.io" + } + ], + "description": "MAML Parser", + "homepage": "https://maml.dev", + "keywords": [ + "config", + "configuration", + "maml", + "parser", + "serializer" + ], + "support": { + "issues": "https://github.com/maml-dev/maml-php/issues", + "source": "https://github.com/maml-dev/maml-php/tree/v3.2.0" + }, + "time": "2026-04-12T16:50:36+00:00" + }, { "name": "nette/tester", "version": "v2.5.3", @@ -1872,6 +1991,806 @@ }, "time": "2024-06-18T18:44:12+00:00" }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "8012f842110b414fd1cc641adb0ed8423353b0bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/8012f842110b414fd1cc641adb0ed8423353b0bb", + "reference": "8012f842110b414fd1cc641adb0ed8423353b0bb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.22" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-10-28T11:08:27+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.43.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5a8f33c87aeecffce24e721b5064fab05f4becf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5a8f33c87aeecffce24e721b5064fab05f4becf8", + "reference": "5a8f33c87aeecffce24e721b5064fab05f4becf8", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-10-15T09:18:53+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-23T22:02:09+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.43.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "122889ed24dc000165bcbfd77ec412f15cbdfc6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/122889ed24dc000165bcbfd77ec412f15cbdfc6a", + "reference": "122889ed24dc000165bcbfd77ec412f15cbdfc6a", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-10-28T09:35:49+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "a5a6912edbbfc4c98cd080bc4bbe37c8e7eeb20a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/a5a6912edbbfc4c98cd080bc4bbe37c8e7eeb20a", + "reference": "a5a6912edbbfc4c98cd080bc4bbe37c8e7eeb20a", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.22" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-10-25T14:40:16+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-27T15:39:01+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "8dff7da3a56638f1d189c5368183294102de4ced" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/8dff7da3a56638f1d189c5368183294102de4ced", + "reference": "8dff7da3a56638f1d189c5368183294102de4ced", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.22" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-10-23T16:51:52+00:00" + }, { "name": "symfony/thanks", "version": "v1.4.0", @@ -1932,6 +2851,82 @@ } ], "time": "2024-10-01T09:47:21+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "1ed6afa13d48c579c51cbf32e38f50686313497b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/1ed6afa13d48c579c51cbf32e38f50686313497b", + "reference": "1ed6afa13d48c579c51cbf32e38f50686313497b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.22" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-10-18T14:39:27+00:00" } ], "aliases": [], diff --git a/config/local.neon.example b/config/local.neon.example new file mode 100644 index 0000000..ee34082 --- /dev/null +++ b/config/local.neon.example @@ -0,0 +1,18 @@ +# Zkopíruj na server do shared/config/local.neon (Deployer symlinkuje). +# Přepisuje hodnoty z common.neon — sem patří produkční hesla a DB. + +parameters: + clientDb: + user: kufrik + password: 'ZMEN_ME' + + tenantDb: + server: localhost + database: kufrik + + sdfPassword: 'ZMEN_ME' + +database: + dsn: 'sqlsrv:server=localhost;Database=kufrik_master;LoginTimeout=3;' + user: 'kufrik' + password: 'ZMEN_ME' diff --git a/deploy.php b/deploy.php new file mode 100644 index 0000000..70a1a0a --- /dev/null +++ b/deploy.php @@ -0,0 +1,51 @@ +setHostname('kmetix.cz') + ->setPort(52222) + ->setRemoteUser('deploy') + ->setSshMultiplexing(false) // Windows + ControlMaster = getsockname failed + ->set('deploy_path', '/var/www/html/kufrik') + ->set('branch', 'master'); + +desc('Vyčistí Nette temp cache v aktuální release'); +task('deploy:clear_cache', function () { + run('rm -rf {{release_path}}/temp/cache/*'); +}); + +after('deploy:symlink', 'deploy:clear_cache'); +after('deploy:failed', 'deploy:unlock'); diff --git a/deploy/apache-vhost.conf.example b/deploy/apache-vhost.conf.example new file mode 100644 index 0000000..2e4f77f --- /dev/null +++ b/deploy/apache-vhost.conf.example @@ -0,0 +1,33 @@ +# Ukázkový Apache vhost pro Ubuntu. +# DocumentRoot musí mířit na Deployer symlink current/www. +# +# 1) Zkopíruj do /etc/apache2/sites-available/kufrik.conf +# 2) Uprav ServerName a cesty +# 3) a2ensite kufrik && systemctl reload apache2 +# +# V php.ini (nebo poolu) nastav: +# opcache.revalidate_path=1 +# jinak Apache po symlinku current neuvidí novou release. + + + ServerName kufrik.example.cz + + DocumentRoot /var/www/kufrik/current/www + + + AllowOverride All + Require all granted + Options -Indexes +FollowSymLinks + + + # Zbytek projektu (app/, config/, vendor/, …) nesmí být webově dostupný. + + Require all denied + + + Require all granted + + + ErrorLog ${APACHE_LOG_DIR}/kufrik-error.log + CustomLog ${APACHE_LOG_DIR}/kufrik-access.log combined + diff --git a/vendor/bin/dep b/vendor/bin/dep new file mode 100644 index 0000000..96196e4 --- /dev/null +++ b/vendor/bin/dep @@ -0,0 +1,119 @@ +#!/usr/bin/env php +realpath = realpath($opened_path) ?: $opened_path; + $opened_path = $this->realpath; + $this->handle = fopen($this->realpath, $mode); + $this->position = 0; + + return (bool) $this->handle; + } + + public function stream_read($count) + { + $data = fread($this->handle, $count); + + if ($this->position === 0) { + $data = preg_replace('{^#!.*\r?\n}', '', $data); + } + + $this->position += strlen($data); + + return $data; + } + + public function stream_cast($castAs) + { + return $this->handle; + } + + public function stream_close() + { + fclose($this->handle); + } + + public function stream_lock($operation) + { + return $operation ? flock($this->handle, $operation) : true; + } + + public function stream_seek($offset, $whence) + { + if (0 === fseek($this->handle, $offset, $whence)) { + $this->position = ftell($this->handle); + return true; + } + + return false; + } + + public function stream_tell() + { + return $this->position; + } + + public function stream_eof() + { + return feof($this->handle); + } + + public function stream_stat() + { + return array(); + } + + public function stream_set_option($option, $arg1, $arg2) + { + return true; + } + + public function url_stat($path, $flags) + { + $path = substr($path, 17); + if (file_exists($path)) { + return stat($path); + } + + return false; + } + } + } + + if ( + (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) + || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) + ) { + return include("phpvfscomposer://" . __DIR__ . '/..'.'/deployer/deployer/bin/dep'); + } +} + +return include __DIR__ . '/..'.'/deployer/deployer/bin/dep'; diff --git a/vendor/bin/dep.bat b/vendor/bin/dep.bat new file mode 100644 index 0000000..11de56e --- /dev/null +++ b/vendor/bin/dep.bat @@ -0,0 +1,5 @@ +@ECHO OFF +setlocal DISABLEDELAYEDEXPANSION +SET BIN_TARGET=%~dp0/dep +SET COMPOSER_RUNTIME_BIN_DIR=%~dp0 +php "%BIN_TARGET%" %* diff --git a/vendor/bin/yaml-lint b/vendor/bin/yaml-lint new file mode 100644 index 0000000..388092f --- /dev/null +++ b/vendor/bin/yaml-lint @@ -0,0 +1,119 @@ +#!/usr/bin/env php +realpath = realpath($opened_path) ?: $opened_path; + $opened_path = $this->realpath; + $this->handle = fopen($this->realpath, $mode); + $this->position = 0; + + return (bool) $this->handle; + } + + public function stream_read($count) + { + $data = fread($this->handle, $count); + + if ($this->position === 0) { + $data = preg_replace('{^#!.*\r?\n}', '', $data); + } + + $this->position += strlen($data); + + return $data; + } + + public function stream_cast($castAs) + { + return $this->handle; + } + + public function stream_close() + { + fclose($this->handle); + } + + public function stream_lock($operation) + { + return $operation ? flock($this->handle, $operation) : true; + } + + public function stream_seek($offset, $whence) + { + if (0 === fseek($this->handle, $offset, $whence)) { + $this->position = ftell($this->handle); + return true; + } + + return false; + } + + public function stream_tell() + { + return $this->position; + } + + public function stream_eof() + { + return feof($this->handle); + } + + public function stream_stat() + { + return array(); + } + + public function stream_set_option($option, $arg1, $arg2) + { + return true; + } + + public function url_stat($path, $flags) + { + $path = substr($path, 17); + if (file_exists($path)) { + return stat($path); + } + + return false; + } + } + } + + if ( + (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) + || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) + ) { + return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint'); + } +} + +return include __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint'; diff --git a/vendor/bin/yaml-lint.bat b/vendor/bin/yaml-lint.bat new file mode 100644 index 0000000..fa76637 --- /dev/null +++ b/vendor/bin/yaml-lint.bat @@ -0,0 +1,5 @@ +@ECHO OFF +setlocal DISABLEDELAYEDEXPANSION +SET BIN_TARGET=%~dp0/yaml-lint +SET COMPOSER_RUNTIME_BIN_DIR=%~dp0 +php "%BIN_TARGET%" %* diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 169ec1a..c347222 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -608,6 +608,7 @@ return array( 'Nette\\Utils\\Type' => $vendorDir . '/nette/utils/src/Utils/Type.php', 'Nette\\Utils\\UnknownImageFileException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php', 'Nette\\Utils\\Validators' => $vendorDir . '/nette/utils/src/Utils/Validators.php', + 'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', 'Tester\\Assert' => $vendorDir . '/nette/tester/src/Framework/Assert.php', 'Tester\\AssertException' => $vendorDir . '/nette/tester/src/Framework/AssertException.php', 'Tester\\CodeCoverage\\Collector' => $vendorDir . '/nette/tester/src/CodeCoverage/Collector.php', diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php index 9f5fb28..a639f3b 100644 --- a/vendor/composer/autoload_files.php +++ b/vendor/composer/autoload_files.php @@ -6,7 +6,15 @@ $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( + '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php', + '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', + '8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php', + 'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php', + 'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php', '6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php', + '135133ad0ca20ef21cc262b30da9a000' => $vendorDir . '/deployer/deployer/src/functions.php', + 'eee1afd8f38c52a8d46f7c5bbb92afdd' => $vendorDir . '/deployer/deployer/src/Support/helpers.php', 'db356362850385d08a5381de2638b5fd' => $vendorDir . '/mpdf/mpdf/src/functions.php', 'd507e002f7fce7f0c6dbf1f22edcb902' => $vendorDir . '/tracy/tracy/src/Tracy/functions.php', ); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index b093038..37c9917 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -8,11 +8,23 @@ $baseDir = dirname($vendorDir); return array( 'setasign\\Fpdi\\' => array($vendorDir . '/setasign/fpdi/src'), 'Symfony\\Thanks\\' => array($vendorDir . '/symfony/thanks/src'), + 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'), + 'Symfony\\Polyfill\\Intl\\Grapheme\\' => array($vendorDir . '/symfony/polyfill-intl-grapheme'), + 'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'), + 'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'), + 'Symfony\\Component\\Yaml\\' => array($vendorDir . '/symfony/yaml'), + 'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'), + 'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'), + 'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'), 'Psr\\Log\\' => array($vendorDir . '/psr/log/src'), 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'), + 'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), 'Mpdf\\PsrLogAwareTrait\\' => array($vendorDir . '/mpdf/psr-log-aware-trait/src'), 'Mpdf\\PsrHttpMessageShim\\' => array($vendorDir . '/mpdf/psr-http-message-shim/src'), 'Mpdf\\' => array($vendorDir . '/mpdf/mpdf/src'), + 'Maml\\' => array($vendorDir . '/maml/maml/src'), + 'Deployer\\' => array($vendorDir . '/deployer/deployer/src'), 'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'), 'App\\' => array($baseDir . '/app'), ); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index a96aa18..cf61471 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -7,7 +7,15 @@ namespace Composer\Autoload; class ComposerStaticInit9c49a418f02ffb235919088891200a26 { public static $files = array ( + '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', + '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', + '8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php', + 'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php', + 'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php', '6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php', + '135133ad0ca20ef21cc262b30da9a000' => __DIR__ . '/..' . '/deployer/deployer/src/functions.php', + 'eee1afd8f38c52a8d46f7c5bbb92afdd' => __DIR__ . '/..' . '/deployer/deployer/src/Support/helpers.php', 'db356362850385d08a5381de2638b5fd' => __DIR__ . '/..' . '/mpdf/mpdf/src/functions.php', 'd507e002f7fce7f0c6dbf1f22edcb902' => __DIR__ . '/..' . '/tracy/tracy/src/Tracy/functions.php', ); @@ -20,20 +28,32 @@ class ComposerStaticInit9c49a418f02ffb235919088891200a26 'S' => array ( 'Symfony\\Thanks\\' => 15, + 'Symfony\\Polyfill\\Mbstring\\' => 26, + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33, + 'Symfony\\Polyfill\\Intl\\Grapheme\\' => 31, + 'Symfony\\Polyfill\\Ctype\\' => 23, + 'Symfony\\Contracts\\Service\\' => 26, + 'Symfony\\Component\\Yaml\\' => 23, + 'Symfony\\Component\\String\\' => 25, + 'Symfony\\Component\\Process\\' => 26, + 'Symfony\\Component\\Console\\' => 26, ), 'P' => array ( 'Psr\\Log\\' => 8, 'Psr\\Http\\Message\\' => 17, + 'Psr\\Container\\' => 14, ), 'M' => array ( 'Mpdf\\PsrLogAwareTrait\\' => 22, 'Mpdf\\PsrHttpMessageShim\\' => 24, 'Mpdf\\' => 5, + 'Maml\\' => 5, ), 'D' => array ( + 'Deployer\\' => 9, 'DeepCopy\\' => 9, ), 'A' => @@ -51,6 +71,42 @@ class ComposerStaticInit9c49a418f02ffb235919088891200a26 array ( 0 => __DIR__ . '/..' . '/symfony/thanks/src', ), + 'Symfony\\Polyfill\\Mbstring\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', + ), + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer', + ), + 'Symfony\\Polyfill\\Intl\\Grapheme\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme', + ), + 'Symfony\\Polyfill\\Ctype\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-ctype', + ), + 'Symfony\\Contracts\\Service\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/service-contracts', + ), + 'Symfony\\Component\\Yaml\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/yaml', + ), + 'Symfony\\Component\\String\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/string', + ), + 'Symfony\\Component\\Process\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/process', + ), + 'Symfony\\Component\\Console\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/console', + ), 'Psr\\Log\\' => array ( 0 => __DIR__ . '/..' . '/psr/log/src', @@ -59,6 +115,10 @@ class ComposerStaticInit9c49a418f02ffb235919088891200a26 array ( 0 => __DIR__ . '/..' . '/psr/http-message/src', ), + 'Psr\\Container\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/container/src', + ), 'Mpdf\\PsrLogAwareTrait\\' => array ( 0 => __DIR__ . '/..' . '/mpdf/psr-log-aware-trait/src', @@ -71,6 +131,14 @@ class ComposerStaticInit9c49a418f02ffb235919088891200a26 array ( 0 => __DIR__ . '/..' . '/mpdf/mpdf/src', ), + 'Maml\\' => + array ( + 0 => __DIR__ . '/..' . '/maml/maml/src', + ), + 'Deployer\\' => + array ( + 0 => __DIR__ . '/..' . '/deployer/deployer/src', + ), 'DeepCopy\\' => array ( 0 => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy', @@ -684,6 +752,7 @@ class ComposerStaticInit9c49a418f02ffb235919088891200a26 'Nette\\Utils\\Type' => __DIR__ . '/..' . '/nette/utils/src/Utils/Type.php', 'Nette\\Utils\\UnknownImageFileException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php', 'Nette\\Utils\\Validators' => __DIR__ . '/..' . '/nette/utils/src/Utils/Validators.php', + 'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', 'Tester\\Assert' => __DIR__ . '/..' . '/nette/tester/src/Framework/Assert.php', 'Tester\\AssertException' => __DIR__ . '/..' . '/nette/tester/src/Framework/AssertException.php', 'Tester\\CodeCoverage\\Collector' => __DIR__ . '/..' . '/nette/tester/src/CodeCoverage/Collector.php', diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index faa1575..f36b9e9 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -1,5 +1,73 @@ { "packages": [ + { + "name": "deployer/deployer", + "version": "v8.0.5", + "version_normalized": "8.0.5.0", + "source": { + "type": "git", + "url": "https://github.com/deployphp/deployer.git", + "reference": "199273500b4f14ff5eafd76bea524e5d44bcaff3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/deployphp/deployer/zipball/199273500b4f14ff5eafd76bea524e5d44bcaff3", + "reference": "199273500b4f14ff5eafd76bea524e5d44bcaff3", + "shasum": "" + }, + "require": { + "maml/maml": "^3.2", + "php": "^8.3", + "symfony/console": "^7.4.0 || ^8.0.0", + "symfony/process": "^7.4.0 || ^8.0.0", + "symfony/yaml": "^7.4.0 || ^8.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.68", + "phpstan/phpstan": "^2.1", + "phpunit/php-code-coverage": "^12.0", + "phpunit/phpunit": "^12.5" + }, + "time": "2026-05-18T16:59:34+00:00", + "bin": [ + "bin/dep" + ], + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "src/functions.php", + "src/Support/helpers.php" + ], + "psr-4": { + "Deployer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Medvedev", + "email": "anton@medv.io" + } + ], + "description": "Deployment Tool", + "homepage": "https://deployer.org", + "support": { + "docs": "https://deployer.org/docs", + "issues": "https://github.com/deployphp/deployer/issues", + "source": "https://github.com/deployphp/deployer" + }, + "funding": [ + { + "url": "https://github.com/sponsors/antonmedv", + "type": "github" + } + ], + "install-path": "../deployer/deployer" + }, { "name": "latte/latte", "version": "v3.0.20", @@ -89,6 +157,63 @@ }, "install-path": "../latte/latte" }, + { + "name": "maml/maml", + "version": "v3.2.0", + "version_normalized": "3.2.0.0", + "source": { + "type": "git", + "url": "https://github.com/maml-dev/maml-php.git", + "reference": "f11cdff4325bf399f23c33fc1995450e40ac0854" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maml-dev/maml-php/zipball/f11cdff4325bf399f23c33fc1995450e40ac0854", + "reference": "f11cdff4325bf399f23c33fc1995450e40ac0854", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2" + }, + "require-dev": { + "php-cs-fixer/shim": "^3.94", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0" + }, + "time": "2026-04-12T16:50:36+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Maml\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Medvedev", + "email": "anton@medv.io" + } + ], + "description": "MAML Parser", + "homepage": "https://maml.dev", + "keywords": [ + "config", + "configuration", + "maml", + "parser", + "serializer" + ], + "support": { + "issues": "https://github.com/maml-dev/maml-php/issues", + "source": "https://github.com/maml-dev/maml-php/tree/v3.2.0" + }, + "install-path": "../maml/maml" + }, { "name": "mpdf/mpdf", "version": "v8.3.1", @@ -1684,6 +1809,62 @@ }, "install-path": "../paragonie/random_compat" }, + { + "name": "psr/container", + "version": "2.0.2", + "version_normalized": "2.0.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "time": "2021-11-05T16:47:00+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "install-path": "../psr/container" + }, { "name": "psr/http-message", "version": "2.0", @@ -1867,6 +2048,780 @@ ], "install-path": "../setasign/fpdi" }, + { + "name": "symfony/console", + "version": "v7.4.22", + "version_normalized": "7.4.22.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "8012f842110b414fd1cc641adb0ed8423353b0bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/8012f842110b414fd1cc641adb0ed8423353b0bb", + "reference": "8012f842110b414fd1cc641adb0ed8423353b0bb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "time": "2026-10-28T11:08:27+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.22" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/console" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "version_normalized": "3.7.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "time": "2026-06-05T06:23:12+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/deprecation-contracts" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "version_normalized": "1.37.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "time": "2026-04-10T16:19:22+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-ctype" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.43.3", + "version_normalized": "1.43.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5a8f33c87aeecffce24e721b5064fab05f4becf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5a8f33c87aeecffce24e721b5064fab05f4becf8", + "reference": "5a8f33c87aeecffce24e721b5064fab05f4becf8", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "time": "2026-10-15T09:18:53+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-intl-grapheme" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.43.0", + "version_normalized": "1.43.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "time": "2026-09-23T22:02:09+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-intl-normalizer" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.43.3", + "version_normalized": "1.43.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "122889ed24dc000165bcbfd77ec412f15cbdfc6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/122889ed24dc000165bcbfd77ec412f15cbdfc6a", + "reference": "122889ed24dc000165bcbfd77ec412f15cbdfc6a", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "time": "2026-10-28T09:35:49+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-mbstring" + }, + { + "name": "symfony/process", + "version": "v7.4.22", + "version_normalized": "7.4.22.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "a5a6912edbbfc4c98cd080bc4bbe37c8e7eeb20a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/a5a6912edbbfc4c98cd080bc4bbe37c8e7eeb20a", + "reference": "a5a6912edbbfc4c98cd080bc4bbe37c8e7eeb20a", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "time": "2026-10-25T14:40:16+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.22" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/process" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.3", + "version_normalized": "3.7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "time": "2026-07-27T15:39:01+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/service-contracts" + }, + { + "name": "symfony/string", + "version": "v7.4.22", + "version_normalized": "7.4.22.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "8dff7da3a56638f1d189c5368183294102de4ced" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/8dff7da3a56638f1d189c5368183294102de4ced", + "reference": "8dff7da3a56638f1d189c5368183294102de4ced", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "time": "2026-10-23T16:51:52+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.22" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/string" + }, { "name": "symfony/thanks", "version": "v1.4.0", @@ -1931,6 +2886,85 @@ ], "install-path": "../symfony/thanks" }, + { + "name": "symfony/yaml", + "version": "v7.4.22", + "version_normalized": "7.4.22.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "1ed6afa13d48c579c51cbf32e38f50686313497b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/1ed6afa13d48c579c51cbf32e38f50686313497b", + "reference": "1ed6afa13d48c579c51cbf32e38f50686313497b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "time": "2026-10-18T14:39:27+00:00", + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.22" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/yaml" + }, { "name": "tracy/tracy", "version": "v2.10.8", @@ -2012,7 +3046,20 @@ ], "dev": true, "dev-package-names": [ + "deployer/deployer", + "maml/maml", "nette/tester", - "symfony/thanks" + "psr/container", + "symfony/console", + "symfony/deprecation-contracts", + "symfony/polyfill-ctype", + "symfony/polyfill-intl-grapheme", + "symfony/polyfill-intl-normalizer", + "symfony/polyfill-mbstring", + "symfony/process", + "symfony/service-contracts", + "symfony/string", + "symfony/thanks", + "symfony/yaml" ] } diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 83a7123..2517d43 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,13 +3,22 @@ 'name' => 'nette/web-project', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '4a4816dd0d78a148624c94f8c74ca7e114975e57', + 'reference' => '6192b1e012f08cb7c407c731b8218bc6346c4a41', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => true, ), 'versions' => array( + 'deployer/deployer' => array( + 'pretty_version' => 'v8.0.5', + 'version' => '8.0.5.0', + 'reference' => '199273500b4f14ff5eafd76bea524e5d44bcaff3', + 'type' => 'library', + 'install_path' => __DIR__ . '/../deployer/deployer', + 'aliases' => array(), + 'dev_requirement' => true, + ), 'latte/latte' => array( 'pretty_version' => 'v3.0.20', 'version' => '3.0.20.0', @@ -19,6 +28,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'maml/maml' => array( + 'pretty_version' => 'v3.2.0', + 'version' => '3.2.0.0', + 'reference' => 'f11cdff4325bf399f23c33fc1995450e40ac0854', + 'type' => 'library', + 'install_path' => __DIR__ . '/../maml/maml', + 'aliases' => array(), + 'dev_requirement' => true, + ), 'mpdf/mpdf' => array( 'pretty_version' => 'v8.3.1', 'version' => '8.3.1.0', @@ -211,7 +229,7 @@ 'nette/web-project' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '4a4816dd0d78a148624c94f8c74ca7e114975e57', + 'reference' => '6192b1e012f08cb7c407c731b8218bc6346c4a41', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -226,6 +244,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'psr/container' => array( + 'pretty_version' => '2.0.2', + 'version' => '2.0.2.0', + 'reference' => 'c71ecc56dfe541dbd90c5360474fbc405f8d5963', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/container', + 'aliases' => array(), + 'dev_requirement' => true, + ), 'psr/http-message' => array( 'pretty_version' => '2.0', 'version' => '2.0.0.0', @@ -244,6 +271,12 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'psr/log-implementation' => array( + 'dev_requirement' => true, + 'provided' => array( + 0 => '1.0|2.0|3.0', + ), + ), 'setasign/fpdi' => array( 'pretty_version' => 'v2.6.9', 'version' => '2.6.9.0', @@ -253,6 +286,87 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'symfony/console' => array( + 'pretty_version' => 'v7.4.22', + 'version' => '7.4.22.0', + 'reference' => '8012f842110b414fd1cc641adb0ed8423353b0bb', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/console', + 'aliases' => array(), + 'dev_requirement' => true, + ), + 'symfony/deprecation-contracts' => array( + 'pretty_version' => 'v3.7.1', + 'version' => '3.7.1.0', + 'reference' => 'f3202fa1b5097b0af062dc978b32ecf63404e31d', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/deprecation-contracts', + 'aliases' => array(), + 'dev_requirement' => true, + ), + 'symfony/polyfill-ctype' => array( + 'pretty_version' => 'v1.37.0', + 'version' => '1.37.0.0', + 'reference' => '141046a8f9477948ff284fa65be2095baafb94f2', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-ctype', + 'aliases' => array(), + 'dev_requirement' => true, + ), + 'symfony/polyfill-intl-grapheme' => array( + 'pretty_version' => 'v1.43.3', + 'version' => '1.43.3.0', + 'reference' => '5a8f33c87aeecffce24e721b5064fab05f4becf8', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-intl-grapheme', + 'aliases' => array(), + 'dev_requirement' => true, + ), + 'symfony/polyfill-intl-normalizer' => array( + 'pretty_version' => 'v1.43.0', + 'version' => '1.43.0.0', + 'reference' => 'ebd57ccc7edb93d632e184efbe7693d2517bab3b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-intl-normalizer', + 'aliases' => array(), + 'dev_requirement' => true, + ), + 'symfony/polyfill-mbstring' => array( + 'pretty_version' => 'v1.43.3', + 'version' => '1.43.3.0', + 'reference' => '122889ed24dc000165bcbfd77ec412f15cbdfc6a', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-mbstring', + 'aliases' => array(), + 'dev_requirement' => true, + ), + 'symfony/process' => array( + 'pretty_version' => 'v7.4.22', + 'version' => '7.4.22.0', + 'reference' => 'a5a6912edbbfc4c98cd080bc4bbe37c8e7eeb20a', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/process', + 'aliases' => array(), + 'dev_requirement' => true, + ), + 'symfony/service-contracts' => array( + 'pretty_version' => 'v3.7.3', + 'version' => '3.7.3.0', + 'reference' => '15e6a07ec2a2c75ceb1b21dd98105ee8456d2257', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/service-contracts', + 'aliases' => array(), + 'dev_requirement' => true, + ), + 'symfony/string' => array( + 'pretty_version' => 'v7.4.22', + 'version' => '7.4.22.0', + 'reference' => '8dff7da3a56638f1d189c5368183294102de4ced', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/string', + 'aliases' => array(), + 'dev_requirement' => true, + ), 'symfony/thanks' => array( 'pretty_version' => 'v1.4.0', 'version' => '1.4.0.0', @@ -262,6 +376,15 @@ 'aliases' => array(), 'dev_requirement' => true, ), + 'symfony/yaml' => array( + 'pretty_version' => 'v7.4.22', + 'version' => '7.4.22.0', + 'reference' => '1ed6afa13d48c579c51cbf32e38f50686313497b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/yaml', + 'aliases' => array(), + 'dev_requirement' => true, + ), 'tracy/tracy' => array( 'pretty_version' => 'v2.10.8', 'version' => '2.10.8.0', diff --git a/vendor/deployer/deployer/.dockerignore b/vendor/deployer/deployer/.dockerignore new file mode 100644 index 0000000..2602e3e --- /dev/null +++ b/vendor/deployer/deployer/.dockerignore @@ -0,0 +1,4 @@ +* +!.dockerignore +!Dockerfile +!deployer.phar diff --git a/vendor/deployer/deployer/.php-cs-fixer.dist.php b/vendor/deployer/deployer/.php-cs-fixer.dist.php new file mode 100644 index 0000000..c2b1201 --- /dev/null +++ b/vendor/deployer/deployer/.php-cs-fixer.dist.php @@ -0,0 +1,23 @@ +in(__DIR__ . '/bin') + ->in(__DIR__ . '/src') + ->in(__DIR__ . '/recipe') + ->in(__DIR__ . '/contrib') + ->in(__DIR__ . '/tests'); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PER-CS' => true, + + // Due to historical reasons we have to keep this. + // Docs parser expects comment right after php tag. + 'blank_line_after_opening_tag' => false, + + // For PHP 7.4 compatibility. + 'trailing_comma_in_multiline' => [ + 'elements' => ['arguments', 'array_destructuring', 'arrays'] + ], + ]) + ->setFinder($finder); diff --git a/vendor/deployer/deployer/Dockerfile b/vendor/deployer/deployer/Dockerfile new file mode 100644 index 0000000..c84729e --- /dev/null +++ b/vendor/deployer/deployer/Dockerfile @@ -0,0 +1,22 @@ +FROM php:8.5-cli-alpine + +RUN set -eux; \ + apk add --no-cache \ + bash \ + git \ + libzip \ + openssh-client \ + rsync \ + unzip \ + zip; \ + apk add --no-cache --virtual .build-deps \ + libzip-dev \ + linux-headers; \ + docker-php-ext-install -j"$(nproc)" pcntl sockets zip; \ + apk del .build-deps + +COPY --chmod=755 deployer.phar /bin/dep + +WORKDIR /app + +ENTRYPOINT ["/bin/dep"] diff --git a/vendor/deployer/deployer/LICENSE b/vendor/deployer/deployer/LICENSE new file mode 100644 index 0000000..a9e959d --- /dev/null +++ b/vendor/deployer/deployer/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2013 Anton Medvedev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/deployer/deployer/README.md b/vendor/deployer/deployer/README.md new file mode 100644 index 0000000..2d508b4 --- /dev/null +++ b/vendor/deployer/deployer/README.md @@ -0,0 +1,52 @@ +

+ + + + Deployer Logo + + + Deployer +

+

The PHP deployment tool with support for popular frameworks out of the box.

+ +



Deployer Screenshot


+ +--- + +

+ Browser testing via
+ + + + Testmu AI + + +

+ +--- + +Build Status +Latest Stable Version +License + +See [deployer.org](https://deployer.org) for more information and documentation. + +## Features + +- Automatic server **provisioning**. +- **Zero downtime** deployments. +- Ready to use recipes for **most frameworks**. + +## Additional resources + +* [GitHub Action for Deployer](https://github.com/deployphp/action) +* [Deployer Docker Image](https://hub.docker.com/r/deployphp/deployer) + +## License +[MIT](https://github.com/deployphp/deployer/blob/master/LICENSE) + +

+ + Join Crow Watch + +

diff --git a/vendor/deployer/deployer/SECURITY.md b/vendor/deployer/deployer/SECURITY.md new file mode 100644 index 0000000..62705b7 --- /dev/null +++ b/vendor/deployer/deployer/SECURITY.md @@ -0,0 +1,22 @@ +# Security Policy + +## Supported Versions + +Deployer is generally backwards compatible with very few exceptions, so we +recommend users to always use the latest version to experience stability, +performance and security. + +We generally backport security issues to a single previous major version, +unless this is not possible or feasible with a reasonable effort. + +| Version | Supported | +|---------|--------------------| +| 8 | :white_check_mark: | +| <= 7 | :x: | + +## Reporting a Vulnerability + +If you believe you've discovered a serious vulnerability, please contact the +Expr core team at anton+security@medv.io. We will evaluate your report and if +necessary issue a fix and an advisory. If the issue was previously undisclosed, +we'll also mention your name in the credits. diff --git a/vendor/deployer/deployer/bin/build b/vendor/deployer/deployer/bin/build new file mode 100644 index 0000000..ffc3f40 --- /dev/null +++ b/vendor/deployer/deployer/bin/build @@ -0,0 +1,101 @@ +#!/usr/bin/env php + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +if (ini_get('phar.readonly') === '1') { + fwrite(STDERR, "Writing to phar files is disabled. Change your `php.ini` or use `-d phar.readonly=false`.\n"); + exit(1); +} + +define('__ROOT__', realpath(__DIR__ . '/..')); +chdir(__ROOT__); + +$opt = getopt('v:', ['nozip']); + +$version = $opt['v'] ?? null; +if (empty($version)) { + fwrite(STDERR, "Please, specify version as \"-v8.0.0\".\n"); + exit(1); +} +if (!preg_match('/^\d+\.\d+\.\d+(-\w+(\.\d+)?)?$/', $version)) { + fwrite(STDERR, "Version must be \"7.0.0-beta.42\". Got \"$version\".\n"); + exit(1); +} + +passthru('composer install --no-dev --prefer-dist --optimize-autoloader'); + +$pharName = "deployer.phar"; +$pharFile = __ROOT__ . '/' . $pharName; +if (file_exists($pharFile)) { + unlink($pharFile); +} + +$ignore = [ + '.anton', + '.git', + 'Tests', + 'tests', + 'deploy.php', + '.php-cs-fixer.dist.php', +]; + +$compress = !isset($opt['nozip']); + +$phar = new Phar($pharFile, 0, $pharName); +$phar->setSignatureAlgorithm(Phar::SHA256); +$phar->startBuffering(); + +$iterator = new RecursiveDirectoryIterator(__ROOT__, FilesystemIterator::SKIP_DOTS); +$iterator = new RecursiveCallbackFilterIterator($iterator, + fn(SplFileInfo $fileInfo) => !in_array($fileInfo->getBasename(), $ignore, true), +); +$iterator = new RecursiveIteratorIterator($iterator); +$iterator = new CallbackFilterIterator($iterator, + // 'bash', 'fish', 'zsh' are completion templates + fn(SplFileInfo $fileInfo) => in_array($fileInfo->getExtension(), ['php', 'exe', 'bash', 'fish', 'zsh'], true), +); + +foreach ($iterator as $fileInfo) { + $file = str_replace(__ROOT__, '', $fileInfo->getRealPath()); + echo "+ $file\n"; + $phar->addFile($fileInfo->getRealPath(), $file); + + if ($compress) { + $phar[$file]->compress(Phar::GZ); + if (!$phar[$file]->isCompressed()) { + fwrite(STDERR, "Could not compress: $file\n"); + } + } +} + +// Add non-PHP files that the iterator skips. +foreach ([ + '/recipe/provision/Caddyfile', + '/recipe/provision/404.html', +] as $extra) { + echo "+ $extra\n"; + $phar->addFile(realpath(__ROOT__ . $extra), $extra); +} + +// Add bin/dep with version baked in. +echo "+ /bin/dep\n"; +$depContent = file_get_contents(__ROOT__ . '/bin/dep'); +$depContent = str_replace("#!/usr/bin/env php\n", '', $depContent); +$depContent = str_replace('__FILE__', 'str_replace("phar://", "", Phar::running())', $depContent); +$depContent = preg_replace("/run\('.+?'/", "run('$version'", $depContent); +$phar->addFromString('bin/dep', $depContent); + +$phar->setStub(<<stopBuffering(); +unset($phar); + +echo "$pharName was created successfully.\n"; diff --git a/vendor/deployer/deployer/bin/dep b/vendor/deployer/deployer/bin/dep new file mode 100644 index 0000000..a1830f5 --- /dev/null +++ b/vendor/deployer/deployer/bin/dep @@ -0,0 +1,99 @@ +#!/usr/bin/env php + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// Check PHP version +if (PHP_VERSION_ID < 80200) { + fwrite(STDERR, "PHP 8.2 or higher is required.\n"); + exit(1); +} + +// Detect deploy.php location +$deployFile = null; +foreach ($argv as $i => $arg) { + if (preg_match('/^(-f|--file)$/', $arg, $match) && $i + 1 < count($argv)) { + $deployFile = $argv[$i + 1]; + break; + } + if (preg_match('/^--file=(?.+)$/', $arg, $match)) { + $deployFile = $match['file']; + break; + } + if (preg_match('/^-f=?(?.+)$/', $arg, $match)) { + $deployFile = $match['file']; + break; + } +} +if (!empty($deployFile)) { + $deployFile = realpath($deployFile); +} +$lookUp = function (string $name): ?string { + $path = getcwd() . '/' . $name; + return is_readable($path) ? $path : null; +}; +if (empty($deployFile)) { + $deployFile = $lookUp('deploy.php'); +} +if (empty($deployFile)) { + $deployFile = $lookUp('deploy.maml'); +} +if (empty($deployFile)) { + $deployFile = $lookUp('deploy.yaml'); +} +if (empty($deployFile)) { + $deployFile = $lookUp('deploy.yml'); +} + +// Detect autoload location +$autoload = [ + __DIR__ . '/../vendor/autoload.php', // The dep located at "deployer.phar/bin" or in development. + __DIR__ . '/../../../autoload.php', // The dep located at "vendor/deployer/deployer/bin". + __DIR__ . '/../autoload.php', // The dep located at "vendor/bin". +]; +$includes = [ + __DIR__ . '/..', + __DIR__ . '/../../../deployer/deployer', + __DIR__ . '/../deployer/deployer', +]; +$includePath = false; +for ($i = 0; $i < count($autoload); $i++) { + if (file_exists($autoload[$i]) && is_dir($includes[$i])) { + require $autoload[$i]; + $includePath = $includes[$i]; + break; + } +} +if (empty($includePath)) { + fwrite(STDERR, "Error: The `autoload.php` file not found in:\n"); + for ($i = 0; $i < count($autoload); $i++) { + $a = file_exists($autoload[$i]) ? 'true' : 'false'; + $b = is_dir($includes[$i]) ? 'true' : 'false'; + fwrite(STDERR, " - file_exists($autoload[$i]) = $a\n"); + fwrite(STDERR, " is_dir($includes[$i]) = $b\n"); + } + exit(1); +} + +// Errors to exception +set_error_handler(function ($severity, $message, $filename, $lineno) { + if (error_reporting() == 0) { + return; + } + if (error_reporting() & $severity) { + throw new ErrorException($message, 0, $severity, $filename, $lineno); + } +}); + +// Enable recipe loading +set_include_path($includePath . PATH_SEPARATOR . get_include_path()); + +// Deployer constants +define('DEPLOYER', true); +define('DEPLOYER_BIN', __FILE__); +define('DEPLOYER_DEPLOY_FILE', $deployFile); + +Deployer\Deployer::run('master', $deployFile); diff --git a/vendor/deployer/deployer/bin/docgen b/vendor/deployer/deployer/bin/docgen new file mode 100644 index 0000000..fbd984e --- /dev/null +++ b/vendor/deployer/deployer/bin/docgen @@ -0,0 +1,73 @@ +#!/usr/bin/env php + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer; + +use Deployer\Documentation\ApiGen; +use Deployer\Documentation\DocGen; +use Deployer\Import\MamlRecipe; +use Maml\Maml; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Output\ConsoleOutput; + +require __DIR__ . '/../vendor/autoload.php'; + +chdir(realpath(__DIR__ . '/..')); + +$input = new ArgvInput(); +$output = new ConsoleOutput(); +$app = new Application('DocGen', '1.0.0'); +$app->setDefaultCommand('all'); + +$api = function () use ($output): int { + $parser = new ApiGen(); + $parser->parse(file_get_contents(__DIR__ . '/../src/functions.php')); + $md = $parser->markdown(); + file_put_contents(__DIR__ . '/../docs/api.md', $md); + $output->writeln('API Reference documentation updated.'); + return 0; +}; + +$recipes = function () use ($input, $output): int { + $docgen = new DocGen(__DIR__ . '/..'); + $docgen->parse(__DIR__ . '/../recipe'); + $docgen->parse(__DIR__ . '/../contrib'); + + if ($input->getOption('json')) { + echo json_encode($docgen->recipes, JSON_PRETTY_PRINT); + return 0; + } + + $docgen->gen(__DIR__ . '/../docs'); + $output->writeln('Recipes documentation updated.'); + return 0; +}; + +$schema = function () use ($output): int { + $newSchema = [ + '$id' => "https://deployer.org/schema.json#", + ...Maml::jsonSchema(MamlRecipe::schema()), + ]; + file_put_contents(__DIR__ . '/../src/schema.json', json_encode($newSchema, JSON_PRETTY_PRINT)); + $output->writeln('Schema updated.'); + return 0; +}; + +$app->register('api')->setCode($api); +$app->register('recipes')->setCode($recipes)->addOption('json'); +$app->register('schema')->setCode($schema); +$app->register('all')->setCode(function () use ($schema, $recipes, $api): int { + $api(); + $recipes(); + $schema(); + echo shell_exec('git status'); + return 0; +})->addOption('json'); + +$app->run($input, $output); diff --git a/vendor/deployer/deployer/composer.json b/vendor/deployer/deployer/composer.json new file mode 100644 index 0000000..de959b6 --- /dev/null +++ b/vendor/deployer/deployer/composer.json @@ -0,0 +1,63 @@ +{ + "name": "deployer/deployer", + "description": "Deployment Tool", + "license": "MIT", + "homepage": "https://deployer.org", + "support": { + "docs": "https://deployer.org/docs", + "source": "https://github.com/deployphp/deployer", + "issues": "https://github.com/deployphp/deployer/issues" + }, + "authors": [ + { + "name": "Anton Medvedev", + "email": "anton@medv.io" + } + ], + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/antonmedv" + } + ], + "autoload": { + "psr-4": { + "Deployer\\": "src/" + }, + "files": [ + "src/functions.php", + "src/Support/helpers.php" + ] + }, + "scripts": { + "test": "phpunit", + "check": "php-cs-fixer check", + "fix": "php-cs-fixer fix", + "docgen": "php bin/docgen", + "phpstan": "phpstan analyse -c phpstan.neon --memory-limit 1G", + "phpstan:baseline": "@phpstan --generate-baseline tests/phpstan-baseline.neon" + }, + "bin": [ + "bin/dep" + ], + "require": { + "php": "^8.3", + "maml/maml": "^3.2", + "symfony/console": "^7.4.0 || ^8.0.0", + "symfony/process": "^7.4.0 || ^8.0.0", + "symfony/yaml": "^7.4.0 || ^8.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.68", + "phpstan/phpstan": "^2.1", + "phpunit/php-code-coverage": "^12.0", + "phpunit/phpunit": "^12.5" + }, + "config": { + "sort-packages": true, + "process-timeout": 0, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} diff --git a/vendor/deployer/deployer/contrib/bugsnag.php b/vendor/deployer/deployer/contrib/bugsnag.php new file mode 100644 index 0000000..f87a697 --- /dev/null +++ b/vendor/deployer/deployer/contrib/bugsnag.php @@ -0,0 +1,38 @@ + get('bugsnag_api_key'), + 'releaseStage' => get('target'), + 'repository' => get('repository'), + 'provider' => get('bugsnag_provider', ''), + 'branch' => get('branch'), + 'revision' => runLocally('git log -n 1 --format="%h"'), + 'appVersion' => get('bugsnag_app_version', ''), + ]; + + Httpie::post('https://notify.bugsnag.com/deploy') + ->jsonBody($data) + ->send(); +}); diff --git a/vendor/deployer/deployer/contrib/cachetool.php b/vendor/deployer/deployer/contrib/cachetool.php new file mode 100644 index 0000000..ff6ba7e --- /dev/null +++ b/vendor/deployer/deployer/contrib/cachetool.php @@ -0,0 +1,116 @@ +set('cachetool', '127.0.0.1:9000'); + +host('production') + ->set('cachetool', '/var/run/php-fpm.sock'); +``` + +By default, if no `cachetool` parameter is provided, this recipe will fallback to the global setting. + +If your deployment user does not have permission to access the php-fpm.sock, you can alternatively use +the web adapter that creates a temporary php file and makes a web request to it with a configuration like +```php +set('cachetool_args', '--web --web-path=./public --web-url=https://{{hostname}}'); +``` + +## Usage + +Since APCu and OPcache deal with compiling and caching files, they should be executed right after the symlink is created for the new release: + +```php +after('deploy:symlink', 'cachetool:clear:opcache'); +// or +after('deploy:symlink', 'cachetool:clear:apcu'); +``` + +## Read more + +Read more information about cachetool on the website: +http://gordalina.github.io/cachetool/ + */ + +namespace Deployer; + +set('cachetool', ''); +/** + * URL to download cachetool from if it is not available + * + * CacheTool 9.x works with PHP >=8.1 + * CacheTool 8.x works with PHP >=8.0 + * CacheTool 7.x works with PHP >=7.3 + */ +set('cachetool_url', 'https://github.com/gordalina/cachetool/releases/download/9.1.0/cachetool.phar'); +set('cachetool_args', ''); +set('bin/cachetool', function () { + if (!test('[ -f {{release_or_current_path}}/cachetool.phar ]')) { + run("cd {{release_or_current_path}} && curl -sLO {{cachetool_url}}"); + } + return '{{release_or_current_path}}/cachetool.phar'; +}); +set('cachetool_options', function () { + $options = (array) get('cachetool'); + $fullOptions = (string) get('cachetool_args'); + $return = []; + + if ($fullOptions !== '') { + $return = [$fullOptions]; + } elseif (count($options) > 0) { + foreach ($options as $option) { + if (is_string($option) && $option !== '') { + $return[] = "--fcgi={$option}"; + } + } + } + + return $return ?: ['']; +}); + +/** + * Clear opcache cache + */ +desc('Clears OPcode cache'); +task('cachetool:clear:opcache', function () { + $options = get('cachetool_options'); + foreach ($options as $option) { + run("cd {{release_or_current_path}} && {{bin/php}} {{bin/cachetool}} opcache:reset $option"); + } +}); + +/** + * Clear APCu cache + */ +desc('Clears APCu system cache'); +task('cachetool:clear:apcu', function () { + $options = get('cachetool_options'); + foreach ($options as $option) { + run("cd {{release_or_current_path}} && {{bin/php}} {{bin/cachetool}} apcu:cache:clear $option"); + } +}); + +/** + * Clear file status cache, including the realpath cache + */ +desc('Clears file status and realpath caches'); +task('cachetool:clear:stat', function () { + $options = get('cachetool_options'); + foreach ($options as $option) { + run("cd {{release_or_current_path}} && {{bin/php}} {{bin/cachetool}} stat:clear $option"); + } +}); diff --git a/vendor/deployer/deployer/contrib/chatwork.php b/vendor/deployer/deployer/contrib/chatwork.php new file mode 100644 index 0000000..7d34b03 --- /dev/null +++ b/vendor/deployer/deployer/contrib/chatwork.php @@ -0,0 +1,173 @@ +formBody(['body' => get('chatwork_message')]) + ->header("X-ChatWorkToken", get('chatwork_token')) + ->send(); +}); + +// Tasks +desc('Tests messages'); +task('chatwork:test', function () { + set('chatwork_message', get('chatwork_notify_text')); + invoke('chatwork_send_message'); + set('chatwork_message', get('chatwork_success_text')); + invoke('chatwork_send_message'); + set('chatwork_message', get('chatwork_failure_text')); + invoke('chatwork_send_message'); +}) + ->once(); + +desc('Notifies Chatwork'); +task('chatwork:notify', function () { + if (!get('chatwork_token', false)) { + return; + } + + if (!get('chatwork_room_id', false)) { + return; + } + set('chatwork_message', get('chatwork_notify_text')); + invoke('chatwork_send_message'); +}) + ->once() + ->hidden(); + +desc('Notifies Chatwork about deploy finish'); +task('chatwork:notify:success', function () { + if (!get('chatwork_token', false)) { + return; + } + + if (!get('chatwork_room_id', false)) { + return; + } + + set('chatwork_message', get('chatwork_success_text')); + invoke('chatwork_send_message'); +}) + ->once() + ->hidden(); + +desc('Notifies Chatwork about deploy failure'); +task('chatwork:notify:failure', function () { + if (!get('chatwork_token', false)) { + return; + } + + if (!get('chatwork_room_id', false)) { + return; + } + + set('chatwork_message', get('chatwork_failure_text')); + invoke('chatwork_send_message'); +}) + ->once() + ->hidden(); diff --git a/vendor/deployer/deployer/contrib/cimonitor.php b/vendor/deployer/deployer/contrib/cimonitor.php new file mode 100644 index 0000000..6d37237 --- /dev/null +++ b/vendor/deployer/deployer/contrib/cimonitor.php @@ -0,0 +1,166 @@ + 'John Doe', + 'email' => 'john@enrise.com', + ]; + }); + ``` + +Various cimonitor statusses are set, in case you want to change these yourselves. See the [CIMonitor documentation](https://cimonitor.readthedocs.io/en/latest/) for the usages of different states. + +## Usage + +If you want to notify only about beginning of deployment add this line only: + +```php +before('deploy', 'cimonitor:notify'); +``` + +If you want to notify about successful end of deployment add this too: + +```php +after('deploy:success', 'cimonitor:notify:success'); +``` + +If you want to notify about failed deployment add this too: + +```php +after('deploy:failed', 'cimonitor:notify:failure'); +``` + */ + +namespace Deployer; + +use Deployer\Utility\Httpie; + +// Title of project based on git repo +set('cimonitor_title', function () { + $repo = get('repository'); + $pattern = '/\w+\/\w+/'; + return preg_match($pattern, $repo, $titles) ? $titles[0] : $repo; +}); +set('cimonitor_user', function () { + return [ + 'name' => runLocally('git config --get user.name'), + 'email' => runLocally('git config --get user.email'), + ]; +}); + +// CI monitor status states and job states +set('cimonitor_status_info', 'info'); +set('cimonitor_status_warning', 'warning'); +set('cimonitor_status_error', 'error'); +set('cimonitor_status_success', 'success'); +set('cimonitor_job_state_info', get('cimonitor_status_info')); +set('cimonitor_job_state_pending', 'pending'); +set('cimonitor_job_state_running', 'running'); +set('cimonitor_job_state_warning', get('cimonitor_status_warning')); +set('cimonitor_job_state_error', get('cimonitor_status_error')); +set('cimonitor_job_state_success', get('cimonitor_status_success')); + +desc('Notifies CIMonitor'); +task('cimonitor:notify', function () { + if (!get('cimonitor_webhook', false)) { + return; + } + + $body = [ + 'state' => get('cimonitor_status_warning'), + 'branch' => get('branch'), + 'title' => get('cimonitor_title'), + 'user' => get('cimonitor_user'), + 'stages' => [get('stage', '')], + 'jobs' => [ + [ + 'name' => 'Deploying...', + 'stage' => '', + 'state' => get('cimonitor_job_state_running'), + ], + ], + ]; + + Httpie::post(get('cimonitor_webhook'))->jsonBody($body)->send(); +}) + ->once() + ->hidden(); + +desc('Notifies CIMonitor about deploy finish'); +task('cimonitor:notify:success', function () { + if (!get('cimonitor_webhook', false)) { + return; + } + + $depstage = 'Deployed to ' . get('stage', ''); + + $body = [ + 'state' => get('cimonitor_status_success'), + 'branch' => get('branch'), + 'title' => get('cimonitor_title'), + 'user' => get('cimonitor_user'), + 'stages' => [$depstage], + 'jobs' => [ + [ + 'name' => 'Deploy', + 'stage' => $depstage, + 'state' => get('cimonitor_job_state_success'), + ], + ], + ]; + + Httpie::post(get('cimonitor_webhook'))->jsonBody($body)->send(); +}) + ->once() + ->hidden(); + +desc('Notifies CIMonitor about deploy failure'); +task('cimonitor:notify:failure', function () { + if (!get('cimonitor_webhook', false)) { + return; + } + + $body = [ + 'state' => get('cimonitor_status_error'), + 'branch' => get('branch'), + 'title' => get('cimonitor_title'), + 'user' => get('cimonitor_user'), + 'stages' => [get('stage', '')], + 'jobs' => [ + [ + 'name' => 'Deploy', + 'stage' => '', + 'state' => get('cimonitor_job_state_error'), + ], + ], + ]; + + Httpie::post(get('cimonitor_webhook'))->jsonBody($body)->send(); +}) + ->once() + ->hidden(); diff --git a/vendor/deployer/deployer/contrib/cloudflare.php b/vendor/deployer/deployer/contrib/cloudflare.php new file mode 100644 index 0000000..bb2db90 --- /dev/null +++ b/vendor/deployer/deployer/contrib/cloudflare.php @@ -0,0 +1,102 @@ + $config['service_key'], + ]; + } elseif (!empty($config['email']) && !empty($config['api_key'])) { + $headers = [ + 'X-Auth-Key' => $config['api_key'], + 'X-Auth-Email' => $config['email'], + ]; + } elseif (!empty($config['api_token'])) { + $headers = [ + 'Authorization' => 'Bearer ' . $config['api_token'], + ]; + } else { + throw new \RuntimeException("Set a service key or email / api key"); + } + + $headers['Content-Type'] = 'application/json'; + + $makeRequest = function ($url, $opts = []) use ($headers) { + $ch = curl_init("https://api.cloudflare.com/client/v4/$url"); + + $parsedHeaders = []; + foreach ($headers as $key => $value) { + $parsedHeaders[] = "$key: $value"; + } + + curl_setopt_array($ch, [ + CURLOPT_HTTPHEADER => $parsedHeaders, + CURLOPT_RETURNTRANSFER => true, + ]); + + curl_setopt_array($ch, $opts); + + $res = curl_exec($ch); + + if (curl_errno($ch)) { + throw new \RuntimeException("Error making curl request (result: $res)"); + } + + return $res; + }; + + $zoneId = $config['zone_id']; + if (empty($zoneId)) { + if (empty($config['domain'])) { + throw new \RuntimeException("Set a domain"); + } + + // get the mysterious zone id from Cloud Flare + $zones = json_decode($makeRequest( + "zones?name={$config['domain']}", + ), true); + + if (!empty($zones['errors'])) { + throw new \RuntimeException('Problem with zone data'); + } else { + $zoneId = current($zones['result'])['id']; + } + } + + // make purge request + $makeRequest( + "zones/$zoneId/purge_cache", + [ + CURLOPT_CUSTOMREQUEST => 'DELETE', + CURLOPT_POSTFIELDS => json_encode( + [ + 'purge_everything' => true, + ], + ), + ], + ); +}); diff --git a/vendor/deployer/deployer/contrib/cpanel.php b/vendor/deployer/deployer/contrib/cpanel.php new file mode 100644 index 0000000..3b86358 --- /dev/null +++ b/vendor/deployer/deployer/contrib/cpanel.php @@ -0,0 +1,269 @@ + getenv('CPANEL_HOST'), + 'port' => getenv('CPANEL_PORT'), + 'username' => getenv('CPANEL_USERNAME'), + 'auth_type' => getenv('CPANEL_AUTH_TYPE'), + 'token' => getenv('CPANEL_TOKEN'), + 'user' => getenv('CPANEL_USER'), + 'db_user' => getenv('CPANEL_DB_USER'), + 'db_user_privileges' => getenv('CPANEL_DB_PRIVILEGES'), + 'timeout' => 500, + + 'allowInStage' => ['staging', 'beta', 'alpha'], + + 'create_domain_format' => '%s-%s-%s', + 'create_domain_values' => ['staging', 'master', get('application')], + 'subdomain_prefix' => substr(md5(get('application')), 0,4) . '-', + 'subdomain_suffix' => getenv('SUDOMAIN_SUFFIX'), + + + 'create_db_format' => '%s_%s-%s-%s', + 'create_db_values' => ['apps', 'staging','master', get('application')], + +]); +``` + +- `cpanel` – array with configuration for cPanel + - `username` – WHM account + - `user` – cPanel account that you want in charge of the domain + - `token` – WHM API token + - `create_domain_format` – Format for name creation of domain + - `create_domain_values` – The actual value reference for naming + - `subdomain_prefix` – cPanel has a weird way of dealing with addons and subdomains, you cannot create 2 addons with the same subdomain, so you need to change it in some way, example uses first 4 chars of md5(app_name) + - `subdomain_suffix` – cPanel has a weird way of dealing with addons and subdomains, so the suffix needs to be your main domain for that account for deletion purposes + - `addondir` – addon dir is different from the deploy path because cPanel "injects" /home/user/ into the path, so tilde cannot be used + - `allowInStage` – Define the stages that cPanel recipe actions are allowed in + + +#### .env file example +``` +CPANEL_HOST=xxx.xxx.xxx.xxx +CPANEL_PORT=2087 +CPANEL_USERNAME=root +CPANEL_TOKEN=xxxx +CPANEL_USER=xxx +CPANEL_AUTH_TYPE=hash +CPANEL_DB_USER=db_user +CPANEL_DB_PRIVILEGES="ALL PRIVILEGES" +SUDOMAIN_SUFFIX=.mymaindomain.com + +``` + +### Tasks + +- `cpanel:createaddondomain` Creates an addon domain +- `cpanel:deleteaddondomain` Removes an addon domain +- `cpanel:createdb` Creates a new database + +### Usage + +A complete example with configs, staging and deployment + +``` +load(); // this is used just so an .env file can be used for credentials + +require 'cpanel.php'; + + +// Project name +set('application', 'myproject.com'); +// Project repository +set('repository', 'git@github.com:myorg/myproject.com'); + + + + + +set('cpanel', [ + 'host' => getenv('CPANEL_HOST'), + 'port' => getenv('CPANEL_PORT'), + 'username' => getenv('CPANEL_USERNAME'), + 'auth_type' => getenv('CPANEL_AUTH_TYPE'), + 'token' => getenv('CPANEL_TOKEN'), + 'user' => getenv('CPANEL_USER'), + 'db_user' => getenv('CPANEL_DB_USER'), + 'db_user_privileges' => getenv('CPANEL_DB_PRIVILEGES'), + 'timeout' => 500, + 'allowInStage' => ['staging', 'beta', 'alpha'], + + 'create_domain_format' => '%s-%s-%s', + 'create_domain_values' => ['staging', 'master', get('application')], + 'subdomain_prefix' => substr(md5(get('application')), 0,4) . '-', + 'subdomain_suffix' => getenv('SUDOMAIN_SUFFIX'), + + + 'create_db_format' => '%s_%s-%s-%s', + 'create_db_values' => ['apps', 'staging','master', get('application')], + +]); + +host('myproject.com') + ->stage('staging') + ->set('cpanel_createdb', vsprintf(get('cpanel')['create_db_format'], get('cpanel')['create_db_values'])) + ->set('branch', 'dev-branch') + ->set('deploy_path', '~/staging/' . vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values'])) + ->set('addondir', 'staging/' . vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values'])); +// Tasks +task('build', function () { + run('cd {{release_path}} && build'); +}); + +after('deploy:prepare', 'cpanel:createaddondomain'); +after('deploy:prepare', 'cpanel:createdb'); +``` + */ + +namespace Deployer; + +use Deployer\Task\Context; +use Gufy\CpanelPhp\Cpanel; + +/** + * @return Cpanel + * @throws Exception\Exception + */ +function getCpanel() +{ + $config = get('cpanel', []); + $allowInStage = $config['allowInStage']; + $stage = input()->getArgument('stage'); + + if (!class_exists('\Gufy\CpanelPhp\Cpanel')) { + throw new \RuntimeException("Please install php package gufy/cpanel-php to use CPanel API"); + } + + if (!in_array($stage, $allowInStage)) { + throw new \RuntimeException(sprintf("Since it creates addon domains and databases, CPanel recipe is available only in the %s environments", implode($allowInStage))); + } + + + if (!is_array($config) + || !isset($config['host']) + || !isset($config['port']) + || !isset($config['username']) + || !isset($config['token']) + || !isset($config['user'])) { + throw new \RuntimeException("Please configure CPanel config: set('cpanel', array('host' => 'xxx.xxx.xxx.xxx:', 'port' => 2087 , 'username' => 'root', 'token' => 'asdfasdf', 'cpaneluser' => 'guy'));"); + } + + $cpanel = new Cpanel([ + 'host' => 'https://' . $config['host'] . ':' . $config['port'], + 'username' => $config['username'], + 'auth_type' => $config['auth_type'], + 'password' => $config['token'], + ]); + + $cpanel->setTimeout($config['timeout']); + + return $cpanel; +} + +function getDomainInfo() +{ + $domain = vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values']); + $cleanDomain = str_replace(['.', ',', ' ', '/', '-'], '', $domain); + $subDomain = get('cpanel')['subdomain_prefix'] . $cleanDomain; + + return [ + 'domain' => $domain, + 'subDomain' => $subDomain, + 'subDomainWithSuffix' => $subDomain . get('cpanel')['subdomain_suffix'], + ]; +} + +desc('Creates database though CPanel API'); +task('cpanel:createdb', function () { + + $cpanel = getCPanel(); + $config = get('cpanel', []); + if (!askConfirmation(sprintf('This will try to create the database %s on the host though CPanel API, ok?', get('cpanel_createdb')), true)) { + return; + } + + $createDbDataResult = $cpanel->cpanel('MysqlFE', 'createdb', $config['user'], ['db' => get('cpanel_createdb')]); + $addPrivilegesDataResult = $cpanel->cpanel('MysqlFE', 'setdbuserprivileges', $config['user'], ['privileges' => $config['db_user_privileges'], 'db' => get('cpanel_createdb'), 'dbuser' => $config['db_user']]); + + $createDbData = json_decode($createDbDataResult, true); + $addPrivilegesData = json_decode($addPrivilegesDataResult, true); + + if (isset($createDbData['cpanelresult']['error'])) { + writeln($createDbData['cpanelresult']['error']); + } else { + writeln('Successfully created database!'); + } + + if (isset($addPrivilegesData['cpanelresult']['error'])) { + writeln($addPrivilegesData['cpanelresult']['error']); + } else { + writeln('Successfully added privileges to database!'); + } +}); + +desc('Creates addon domain though CPanel API'); +task('cpanel:createaddondomain', function () { + $cpanel = getCPanel(); + $config = get('cpanel', []); + $domain = getDomainInfo()['domain']; + $subDomain = getDomainInfo()['subDomain']; + if (!askConfirmation(sprintf('This will try to create the addon domain %s and point it to %s and subdomain %s, ok?', $domain, get('addondir'), $subDomain), true)) { + return; + } + + writeln(sprintf('Creates addon domain %s and pointing it to %s', $domain, get('addondir'))); + + $addAddonDomainResult = $cpanel->cpanel('AddonDomain', 'addaddondomain', $config['user'], ['dir' => get('addondir'), 'newdomain' => $domain, 'subdomain' => $subDomain]); + $addAddonDomainData = json_decode($addAddonDomainResult, true); + + if (isset($addAddonDomainResult['cpanelresult']['error'])) { + writeln($addAddonDomainData['cpanelresult']['error']); + } else { + writeln('Successfully created addon domain!'); + writeln($addAddonDomainData['cpanelresult']['data'][0]['reason']); + } +}); + +desc('Deletes addon domain though CPanel API'); +task('cpanel:deleteaddondomain', function () { + $cpanel = getCPanel(); + $config = get('cpanel', []); + $domain = getDomainInfo()['domain']; + $subDomain = getDomainInfo()['subDomain']; + $subDomainWithSuffix = getDomainInfo()['subDomainWithSuffix']; + + if (!askConfirmation(sprintf('This will delete the addon domain %s with corresponding subdomain %s, ok?', $domain, $subDomain), true)) { + return; + } + + writeln(sprintf('Deleting addon domain %s', $domain)); + + $delAddonDomainResult = $cpanel->cpanel('AddonDomain', 'deladdondomain', $config['user'], ['domain' => $domain, 'subdomain' => $subDomainWithSuffix]); + $delAddonDomainResult = json_decode($delAddonDomainResult, true); + + if (isset($delAddonDomainResult['cpanelresult']['error'])) { + writeln($delAddonDomainResult['cpanelresult']['error']); + } else { + writeln('Successfully deleted addon domain!'); + writeln($delAddonDomainResult['cpanelresult']['data'][0]['reason']); + } +}); diff --git a/vendor/deployer/deployer/contrib/crontab.php b/vendor/deployer/deployer/contrib/crontab.php new file mode 100644 index 0000000..2b916b8 --- /dev/null +++ b/vendor/deployer/deployer/contrib/crontab.php @@ -0,0 +1,154 @@ +> /dev/null 2>&1', +]); +``` + */ + +namespace Deployer; + +// Get path to bin +set('bin/crontab', function () { + return which('crontab'); +}); + +// Set the identifier used in the crontab, application name by default +set('crontab:identifier', function () { + return get('application', 'application'); +}); + +// Use sudo to run crontab. When running crontab with sudo, you can use the `-u` parameter to change a crontab for a different user. +set('crontab:use_sudo', false); + +desc('Sync crontab jobs'); +task('crontab:sync', function () { + $cronJobsLocal = array_map( + fn($job) => parse($job), + get('crontab:jobs', []), + ); + + if (count($cronJobsLocal) == 0) { + writeln("Nothing to sync - configure crontab:jobs"); + return; + } + + $cronJobs = getRemoteCrontab(); + $identifier = get('crontab:identifier'); + $sectionStart = "###< $identifier"; + $sectionEnd = "###> $identifier"; + + // find our cronjob section + $start = array_search($sectionStart, $cronJobs); + $end = array_search($sectionEnd, $cronJobs); + + if ($start === false || $end === false) { + // Move the duplicates over when first generating the section + foreach ($cronJobs as $index => $cronJob) { + if (in_array($cronJob, $cronJobsLocal)) { + unset($cronJobs[$index]); + writeln("Crontab: Found existing job in crontab, moving it to the section"); + } + } + + // Create the section + $cronJobs[] = $sectionStart; + $cronJobs = [...$cronJobs, ...$cronJobsLocal]; + $cronJobs[] = $sectionEnd; + writeln("Crontab: Found no section, created the section with configured jobs"); + } else { + // Replace the existing section + array_splice($cronJobs, $start + 1, $end - $start - 1, $cronJobsLocal); + writeln("Crontab: Found existing section, replaced with configured jobs"); + } + + setRemoteCrontab($cronJobs); +}); + +desc('Remove crontab jobs'); +task('crontab:remove', function () { + $cronJobsLocal = array_map( + fn($job) => parse($job), + get('crontab:jobs', []), + ); + + $cronJobs = getRemoteCrontab(); + $identifier = get('crontab:identifier'); + $sectionStart = "###< $identifier"; + $sectionEnd = "###> $identifier"; + + // Find our cronjob section + $start = array_search($sectionStart, $cronJobs); + $end = array_search($sectionEnd, $cronJobs); + + if ($start && $end) { + // Remove the existing section + array_splice($cronJobs, $start + 1, $end - $start - 1); + writeln("Crontab: Found existing section, removed jobs"); + } elseif (count($cronJobsLocal) > 0) { + $foundJobs = false; + // Remove individual jobs if no section is present + foreach ($cronJobs as $index => $cronJob) { + if (in_array($cronJob, $cronJobsLocal)) { + unset($cronJobs[$index]); + $foundJobs = true; + } + } + if ($foundJobs) { + writeln("Crontab: Found existing jobs in crontab, removed jobs"); + } else { + writeln("Crontab: No existing jobs in crontab, skipping"); + return; + } + } else { + writeln("Crontab: Found no section and crontab:jobs is not configured, skipping"); + return; + } + + setRemoteCrontab($cronJobs); +}); + +function setRemoteCrontab(array $lines): void +{ + $sudo = get('crontab:use_sudo') ? 'sudo' : ''; + + $tmpCrontabPath = sprintf('/tmp/%s', uniqid('crontab_save_')); + + if (test("[ -f '$tmpCrontabPath' ]")) { + run("unlink '$tmpCrontabPath'"); + } + + foreach ($lines as $line) { + run("echo " . quote($line) . " >> $tmpCrontabPath"); + } + + run("$sudo {{bin/crontab}} " . $tmpCrontabPath); + run('unlink ' . $tmpCrontabPath); +} + +function getRemoteCrontab(): array +{ + $sudo = get('crontab:use_sudo') ? 'sudo' : ''; + + if (!test("$sudo {{bin/crontab}} -l >> /dev/null 2>&1")) { + return []; + } + + return explode(PHP_EOL, run("$sudo {{bin/crontab}} -l")); +} diff --git a/vendor/deployer/deployer/contrib/directadmin.php b/vendor/deployer/deployer/contrib/directadmin.php new file mode 100644 index 0000000..1315e24 --- /dev/null +++ b/vendor/deployer/deployer/contrib/directadmin.php @@ -0,0 +1,169 @@ + '127.0.0.1', 'port' => 2222, 'username' => 'admin', 'password' => 'password']);"); + } + + return $config; +} + +/** + * DirectAdmin + * + * @param string $action + * @param array $data + * + * @return void + */ +function DirectAdmin(string $action, array $data = []) +{ + $config = getDirectAdminConfig(); + $scheme = $config['scheme'] ?? 'http'; + $port = $config['port'] ?? 2222; + + $result = Httpie::post(sprintf('%s://%s:%s/%s', $scheme, $config['host'], $port, $action)) + ->formBody($data) + ->setopt(CURLOPT_USERPWD, $config['username'] . ':' . $config['password']) + ->send(); + + parse_str($result, $resultData); + + if ($resultData['error'] === '1') { + $resultData['details'] = trim($resultData['details']); + $resultData['details'] = str_replace(['\\n', '\\r'], '', $resultData['details']); + $resultData['details'] = strip_tags($resultData['details']); + + writeln('DirectAdmin message: ' . $resultData['details'] . ''); + } +} + +desc('Creates a database on DirectAdmin'); +task('directadmin:createdb', function () { + $config = getDirectAdminConfig(); + + if (!isset($config['db_name']) + || !isset($config['db_user']) + || !isset($config['db_password'])) { + throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['db_name' => 'test', 'db_user' => 'test', 'db_password' => '123456']);"); + } + + DirectAdmin('CMD_API_DATABASES', [ + 'action' => 'create', + 'name' => $config['db_name'], + 'user' => $config['db_user'], + 'passwd' => $config['db_password'], + 'passwd2' => $config['db_password'], + ]); +}); + +desc('Deletes a database on DirectAdmin'); +task('directadmin:deletedb', function () { + $config = getDirectAdminConfig(); + + if (!isset($config['db_user'])) { + throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['db_user' => 'test_database']);"); + } + + DirectAdmin('CMD_API_DATABASES', [ + 'action' => 'delete', + 'select0' => $config['username'] . '_' . $config['db_user'], + ]); +}); + +desc('Creates a domain on DirectAdmin'); +task('directadmin:createdomain', function () { + $config = getDirectAdminConfig(); + + if (!isset($config['domain_name'])) { + throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['domain_name' => 'test.example.com']);"); + } + + DirectAdmin('CMD_API_DOMAIN', [ + 'action' => 'create', + 'domain' => $config['domain_name'], + 'ssl' => $config['domain_ssl'] ?? 'On', + 'cgi' => $config['domain_cgi'] ?? 'ON', + 'php' => $config['domain_php'] ?? 'ON', + ]); +}); + +desc('Deletes a domain on DirectAdmin'); +task('directadmin:deletedomain', function () { + $config = getDirectAdminConfig(); + + if (!isset($config['domain_name'])) { + throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['domain_name' => 'test.example.com']);"); + } + + DirectAdmin('CMD_API_DOMAIN', [ + 'delete' => 'anything', + 'confirmed' => 'anything', + 'select0' => $config['domain_name'], + ]); +}); + +desc('Symlink your private_html to public_html'); +task('directadmin:symlink-private-html', function () { + $config = getDirectAdminConfig(); + + if (!isset($config['domain_name'])) { + throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['domain_name' => 'test.example.com']);"); + } + + DirectAdmin('CMD_API_DOMAIN', [ + 'action' => 'private_html', + 'domain' => $config['domain_name'], + 'val' => 'symlink', + ]); +}); + +desc('Changes the PHP version from a domain'); +task('directadmin:php-version', function () { + $config = getDirectAdminConfig(); + + if (!isset($config['domain_name']) + || !isset($config['domain_php_version'])) { + throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['domain_name' => 'test.example.com', 'domain_php_version' => 1]);"); + } + + DirectAdmin('CMD_API_DOMAIN', [ + 'action' => 'php_selector', + 'domain' => $config['domain_name'], + 'php1_select' => $config['domain_php_version'], + ]); +}); diff --git a/vendor/deployer/deployer/contrib/discord.php b/vendor/deployer/deployer/contrib/discord.php new file mode 100644 index 0000000..d42b814 --- /dev/null +++ b/vendor/deployer/deployer/contrib/discord.php @@ -0,0 +1,119 @@ + parse(':information_source: **{{user}}** is deploying branch `{{what}}` to _{{where}}_'), + ]; +}); +set('discord_success_text', function () { + return [ + 'text' => parse(':white_check_mark: Branch `{{what}}` deployed to _{{where}}_ successfully'), + ]; +}); +set('discord_failure_text', function () { + return [ + 'text' => parse(':no_entry_sign: Branch `{{what}}` has failed to deploy to _{{where}}_'), + ]; +}); + +// The message +set('discord_message', 'discord_notify_text'); + +// Helpers +task('discord_send_message', function () { + $message = get(get('discord_message')); + + Httpie::post(get('discord_webhook'))->jsonBody($message)->send(); +}); + +// Tasks +desc('Tests messages'); +task('discord:test', function () { + set('discord_message', 'discord_notify_text'); + invoke('discord_send_message'); + set('discord_message', 'discord_success_text'); + invoke('discord_send_message'); + set('discord_message', 'discord_failure_text'); + invoke('discord_send_message'); +}) + ->once(); + +desc('Notifies Discord'); +task('discord:notify', function () { + set('discord_message', 'discord_notify_text'); + invoke('discord_send_message'); +}) + ->once() + ->isHidden(); + +desc('Notifies Discord about deploy finish'); +task('discord:notify:success', function () { + set('discord_message', 'discord_success_text'); + invoke('discord_send_message'); +}) + ->once() + ->isHidden(); + +desc('Notifies Discord about deploy failure'); +task('discord:notify:failure', function () { + set('discord_message', 'discord_failure_text'); + invoke('discord_send_message'); +}) + ->once() + ->isHidden(); diff --git a/vendor/deployer/deployer/contrib/grafana.php b/vendor/deployer/deployer/contrib/grafana.php new file mode 100644 index 0000000..80316ab --- /dev/null +++ b/vendor/deployer/deployer/contrib/grafana.php @@ -0,0 +1,66 @@ + 'eyJrIj...', + 'url' => 'http://grafana/api/annotations', + 'tags' => ['deploy', 'production'], +]); + +``` + +## Usage + +If you want to create annotation about successful end of deployment. + +```php +after('deploy:success', 'grafana:annotation'); +``` + +*/ + +namespace Deployer; + +use Deployer\Utility\Httpie; + +desc('Creates Grafana annotation of deployment'); +task('grafana:annotation', function () { + $defaultConfig = [ + 'url' => null, + 'token' => null, + 'time' => round(microtime(true) * 1000), + 'tags' => [], + 'text' => null, + ]; + + $config = array_merge($defaultConfig, (array) get('grafana')); + if (!isset($config['url']) || !isset($config['token'])) { + throw new \RuntimeException("Please configure Grafana: set('grafana', ['url' => 'https://localhost/api/annotations', token' => 'eyJrIjo...']);"); + } + + $params = [ + 'time' => $config['time'], + 'isRegion' => false, + 'tags' => $config['tags'], + 'text' => $config['text'], + ]; + if (!isset($params['text'])) { + $params['text'] = 'Deployed ' . trim(runLocally('git log -n 1 --format="%h"')); + } + + Httpie::post($config['url']) + ->header('Authorization', 'Bearer ' . $config['token']) + ->jsonBody($params) + ->send(); +}); diff --git a/vendor/deployer/deployer/contrib/hangouts.php b/vendor/deployer/deployer/contrib/hangouts.php new file mode 100644 index 0000000..1eca01f --- /dev/null +++ b/vendor/deployer/deployer/contrib/hangouts.php @@ -0,0 +1,184 @@ + [ + 'title' => get('chat_title'), + 'subtitle' => get('chat_subtitle'), + 'imageUrl' => get('favicon'), + 'imageStyle' => 'IMAGE', + ], + 'sections' => [ + 'widgets' => [ + 'keyValue' => [ + 'topLabel' => get('chat_line1'), + 'content' => get('chat_line2'), + 'contentMultiline' => false, + 'bottomLabel' => 'started', + // Use 'iconUrl' to set a custom icon URL (png) + 'icon' => 'CLOCK', + 'button' => [ + 'textButton' => [ + 'text' => 'Visit site', + 'onClick' => [ + 'openLink' => [ + 'url' => get('hostname'), + ], + ], + ], + ], + ], + ], + ], + ]; + + Httpie::post(get('chat_webhook'))->jsonBody(['cards' => $card])->send(); +}) + ->once() + ->hidden(); + +desc('Notifies Google Hangouts Chat about deploy finish'); +task('chat:notify:success', function () { + if (!get('chat_webhook', false)) { + return; + } + + $card = [ + 'header' => [ + 'title' => get('chat_title'), + 'subtitle' => get('chat_subtitle'), + 'imageUrl' => get('favicon'), + 'imageStyle' => 'IMAGE', + ], + 'sections' => [ + 'widgets' => [ + 'keyValue' => [ + 'topLabel' => get('chat_line1'), + 'content' => get('chat_line2'), + 'contentMultiline' => false, + 'bottomLabel' => 'succeeded', + // Use 'iconUrl' to set a custom icon URL (png) + 'icon' => 'STAR', + 'button' => [ + 'textButton' => [ + 'text' => 'Visit site', + 'onClick' => [ + 'openLink' => [ + 'url' => get('hostname'), + ], + ], + ], + ], + ], + ], + ], + ]; + + Httpie::post(get('chat_webhook'))->jsonBody(['cards' => $card])->send(); +}) + ->once() + ->hidden(); + +desc('Notifies Google Hangouts Chat about deploy failure'); +task('chat:notify:failure', function () { + if (!get('chat_webhook', false)) { + return; + } + + $card = [ + 'header' => [ + 'title' => get('chat_title'), + 'subtitle' => get('chat_subtitle'), + 'imageUrl' => get('favicon'), + 'imageStyle' => 'IMAGE', + ], + 'sections' => [ + 'widgets' => [ + 'keyValue' => [ + 'topLabel' => get('chat_line1'), + 'content' => get('chat_line2'), + 'contentMultiline' => false, + 'bottomLabel' => 'failed', + // Use 'iconUrl' to set a custom icon URL (png) + // or use 'icon' and pick from this list: + // https://developers.google.com/hangouts/chat/reference/message-formats/cards#customicons + 'button' => [ + 'textButton' => [ + 'text' => 'Visit site', + 'onClick' => [ + 'openLink' => [ + 'url' => get('hostname'), + ], + ], + ], + ], + ], + ], + ], + ]; + + Httpie::post(get('chat_webhook'))->jsonBody(['cards' => $card])->send(); +}) + ->once() + ->hidden(); diff --git a/vendor/deployer/deployer/contrib/hipchat.php b/vendor/deployer/deployer/contrib/hipchat.php new file mode 100644 index 0000000..192b69e --- /dev/null +++ b/vendor/deployer/deployer/contrib/hipchat.php @@ -0,0 +1,46 @@ + get('hipchat_room_id'), + 'from' => get('target'), + 'message' => get('hipchat_message'), + 'color' => get('hipchat_color'), + 'auth_token' => get('hipchat_token'), + 'notify' => 0, + 'format' => 'json', + ]; + + Httpie::get(get('hipchat_url')) + ->query($params) + ->send(); +}); diff --git a/vendor/deployer/deployer/contrib/ispmanager.php b/vendor/deployer/deployer/contrib/ispmanager.php new file mode 100644 index 0000000..2387122 --- /dev/null +++ b/vendor/deployer/deployer/contrib/ispmanager.php @@ -0,0 +1,780 @@ + [ + 'dsn' => 'https://root:password@localhost:1500/ispmgr', + 'secure' => true, + ], + 'createDomain' => null, + 'updateDomain' => null, + 'deleteDomain' => null, + 'createDatabase' => null, + 'deleteDatabase' => null, + 'phpSelect' => null, + 'createAlias' => null, + 'deleteAlias' => null, +]); + +// Vhost default configuration +set('vhost', [ + 'name' => '{{domain}}', + 'php_enable' => 'on', + 'aliases' => 'www.{{domain}}', + 'home' => 'www/{{domain}}', + 'owner' => get('ispmanager_owner'), + 'email' => 'webmaster@{{domain}}', + 'charset' => 'off', + 'dirindex' => 'index.php uploaded.html', + 'ssi' => 'on', + 'php' => 'on', + 'php_mode' => 'php_mode_mod', + 'basedir' => 'on', + 'php_apache_version' => 'native', + 'cgi' => 'off', + 'log_access' => 'on', + 'log_error' => 'on', +]); + +// Storage +set('ispmanager_session', ''); +set('ispmanager_databases', [ + 'servers' => [], + 'hosts' => [], + 'dblist' => [], +]); + +set('ispmanager_domains', []); +set('ispmanager_phplist', []); +set('ispmanager_aliaslist', []); + +desc('Installs ispmanager'); +task('ispmanager:init', function () { + $config = get('ispmanager'); + + if (!is_null($config['createDatabase']) || !is_null($config['deleteDatabase'])) { + invoke('ispmanager:db-server-list'); + invoke('ispmanager:db-list'); + } + + if (!is_null($config['createDomain']) || !is_null($config['deleteDomain'])) { + invoke('ispmanager:domain-list'); + } + + if (!is_null($config['phpSelect'])) { + invoke('ispmanager:domain-list'); + invoke('ispmanager:get-php-list'); + } + + if (!is_null($config['createAlias']) || !is_null($config['deleteAlias'])) { + invoke('ispmanager:domain-list'); + } +}); + +desc('Takes database servers list'); +task('ispmanager:db-server-list', function () { + $response = ispmanagerRequest('get', [ + 'func' => 'db.server', + ]); + + $hostList = []; + $serverList = []; + + if (isset($response['doc']['elem']) && count($response['doc']['elem']) > 0) { + foreach ($response['doc']['elem'] as $dbServer) { + $serverList[$dbServer['name']['$']] = [ + 'host' => $dbServer['host']['$'], + 'name' => $dbServer['name']['$'], + 'version' => $dbServer['savedver']['$'], + ]; + + if (!strpos($dbServer['host']['$'], ':')) { + $dbHost = $dbServer['host']['$'] . ':3306'; + } else { + $dbHost = $dbServer['host']['$']; + } + + $hostList[$dbHost] = [ + 'host' => $dbHost, + 'name' => $dbServer['name']['$'], + 'version' => $dbServer['savedver']['$'], + ]; + } + } + + add('ispmanager_databases', [ + 'servers' => $serverList, + 'hosts' => $hostList, + ]); +}); + +desc('Takes databases list'); +task('ispmanager:db-list', function () { + $response = ispmanagerRequest('get', [ + 'func' => 'db', + ]); + + $dbList = []; + if (isset($response['doc']['elem']) && count($response['doc']['elem']) > 0) { + foreach ($response['doc']['elem'] as $db) { + $dbList[$db['pair']['$']] = [ + 'name' => $db['name']['$'], + 'server' => $db['server']['$'], + 'location' => $db['pair']['$'], + ]; + } + } + + add('ispmanager_databases', [ + 'dblist' => $dbList, + ]); +}); + +desc('Takes domain list'); +task('ispmanager:domain-list', function () { + $response = ispmanagerRequest('get', [ + 'func' => 'webdomain', + ]); + + $domainList = []; + if (isset($response['doc']['elem']) && count($response['doc']['elem']) > 0) { + foreach ($response['doc']['elem'] as $domain) { + $domainList[] = $domain['name']['$']; + } + } + + add('ispmanager_domains', $domainList); +}); + +desc('Creates new database'); +task('ispmanager:db-create', function () { + $config = get('ispmanager'); + + if (is_null($config['createDatabase'])) { + warning('Action for database create is not active'); + return; + } + + $dsnData = parse_url($config['createDatabase']['dsn']); + + $dbInfo = get('ispmanager_databases'); + + $hostInfo = null; + foreach ($dbInfo['hosts'] as $hostData) { + if ($hostData['host'] == $dsnData['host'] . ':' . $dsnData['port']) { + $hostInfo = $hostData; + break; + } + } + + if (is_null($hostInfo)) { + throw new Exception('Incorrect DB host'); + } + + $dbName = substr($dsnData['path'], 1); + + $dbLocation = $dbName . '->' . $hostInfo['name']; + + if (isset($dbInfo['dblist'][$dbLocation])) { + if (!isset($config['createDatabase']['skipIfExist']) || !$config['createDatabase']['skipIfExist']) { + throw new Exception('Database already exists!'); + } else { + warning('Database already exists - skipping'); + return; + } + } + + $dbCreateRequest = [ + 'func' => 'db.edit', + 'name' => $dbName, + 'owner' => get('ispmanager_owner'), + 'server' => $hostInfo['name'], + 'charset' => $config['createDatabase']['charset'], + 'sok' => 'ok', + ]; + + if ($dsnData['user'] == '*') { + $dbCreateRequest['user'] = '*'; + $dbCreateRequest['username'] = $dbName; + + if ($dsnData['pass'] == '*') { + $dbCreateRequest['password'] = generatePassword(8); + } else { + $dbCreateRequest['password'] = $dsnData['pass']; + } + } else { + $dbCreateRequest['user'] = $dsnData['user']; + } + + + $response = ispmanagerRequest('post', $dbCreateRequest); + + if (isset($response['doc']['error']['msg']['$'])) { + throw new Exception($response['doc']['error']['msg']['$']); + } else { + info('Database successfully created'); + } +}); + +desc('Deletes database'); +task('ispmanager:db-delete', function () { + $config = get('ispmanager'); + + if (is_null($config['deleteDatabase'])) { + warning('Action for database delete is not active'); + return; + } + + $dbInfo = get('ispmanager_databases'); + $dsnData = parse_url($config['deleteDatabase']['dsn']); + + $hostInfo = null; + foreach ($dbInfo['hosts'] as $hostData) { + if ($hostData['host'] == $dsnData['host'] . ':' . $dsnData['port']) { + $hostInfo = $hostData; + break; + } + } + + if (is_null($hostInfo)) { + throw new Exception('Incorrect DB host'); + } + + $dbName = substr($dsnData['path'], 1); + + $dbLocation = $dbName . '->' . $hostInfo['name']; + + if (!isset($dbInfo['dblist'][$dbLocation])) { + if (!isset($config['deleteDatabase']['skipIfNotExist']) || !$config['deleteDatabase']['skipIfNotExist']) { + throw new Exception('Database not exist!'); + } else { + warning('Database not exist - skipping'); + return; + } + } + + $dbDeleteRequest = [ + 'func' => 'db.delete', + 'elid' => $dbLocation, + ]; + + $response = ispmanagerRequest('post', $dbDeleteRequest); + + if (isset($response['doc']['error']['msg']['$'])) { + throw new Exception($response['doc']['error']['msg']['$']); + } else { + info('Database successfully deleted'); + } +}); + +desc('Creates new domain'); +task('ispmanager:domain-create', function () { + $config = get('ispmanager'); + + if (is_null($config['createDomain'])) { + warning('Action for domain create is not active'); + return; + } + + if (!isset($config['createDomain']['name']) || $config['createDomain']['name'] == '') { + throw new Exception('Invalid domain name!'); + } + + // Check domain exists + $existDomains = get('ispmanager_domains'); + + if (in_array($config['createDomain']['name'], $existDomains)) { + if (!isset($config['createDomain']['skipIfExist']) || !$config['createDomain']['skipIfExist']) { + throw new Exception('Domain already exists!'); + } else { + warning('Domain already exists - skipping'); + return; + } + } + + // Build vhost create request + $vhostTemplate = get('vhost'); + + $domainCreateRequest = [ + 'func' => 'webdomain.edit', + 'sok' => 'ok', + ]; + + foreach ($vhostTemplate as $key => $value) { + $domainCreateRequest[$key] = str_replace('{{domain}}', $config['createDomain']['name'], $vhostTemplate[$key]); + } + + $response = ispmanagerRequest('post', $domainCreateRequest); + + if (isset($response['doc']['error']['msg']['$'])) { + throw new Exception($response['doc']['error']['msg']['$']); + } else { + info('Domain successfully created'); + } +}); + +desc('Gets allowed PHP modes and versions'); +task('ispmanager:get-php-list', function () { + // Get www-root settings for fpm version + $response = ispmanagerRequest('get', [ + 'func' => 'user.edit', + 'elid' => get('ispmanager_owner'), + 'elname' => get('ispmanager_owner'), + ]); + + $userFPMVersion = $response['doc']['limit_php_fpm_version']['$'] ?? null; + + $response = ispmanagerRequest('get', [ + 'func' => 'phpversions', + ]); + + $versions = []; + foreach ($response['doc']['elem'] as $phpVersion) { + $versions[$phpVersion['key']['$']] = [ + 'name' => $phpVersion['name']['$'], + 'php_mode_mod' => false, + 'php_mode_cgi' => false, + 'php_mode_fcgi_apache' => false, + 'php_mode_fcgi_nginxfpm' => false, + ]; + + if (isset($phpVersion['default_apache']) && $phpVersion['default_apache']['$'] == 'on') { + $versions[$phpVersion['key']['$']]['php_mode_mod'] = true; + } + + if (isset($phpVersion['cgi']) && $phpVersion['cgi']['$'] == 'on') { + $versions[$phpVersion['key']['$']]['php_mode_cgi'] = true; + } + + if (isset($phpVersion['apache']) && $phpVersion['apache']['$'] == 'on') { + $versions[$phpVersion['key']['$']]['php_mode_fcgi_apache'] = true; + } + + if (isset($phpVersion['fpm']) && $phpVersion['fpm']['$'] == 'on' && $phpVersion['key']['$'] == $userFPMVersion) { + $versions[$phpVersion['key']['$']]['php_mode_fcgi_nginxfpm'] = true; + } + + } + + add('ispmanager_phplist', $versions); +}); + +desc('Prints allowed PHP modes and versions'); +task('ispmanager:print-php-list', function () { + invoke('ispmanager:get-php-list'); + + $versions = get('ispmanager_phplist'); + writeln("PHP versions: "); + writeln(str_repeat('*', 32)); + foreach ($versions as $versionKey => $versionData) { + writeln('PHP ' . $versionData['name'] . ' (ID: ' . $versionKey . ')'); + writeln(str_repeat('*', 32)); + if (!$versionData['php_mode_mod']) { + writeln('Apache module support (php_mode_mod) - NO'); + } else { + writeln('Apache module support (php_mode_mod) - YES'); + } + + if (!$versionData['php_mode_cgi']) { + writeln('CGI support (php_mode_cgi) - NO'); + } else { + writeln('CGI support (php_mode_cgi) - YES'); + } + + if (!$versionData['php_mode_fcgi_apache']) { + writeln('Apache fast-cgi support (php_mode_fcgi_apache) - NO'); + } else { + writeln('Apache fast-cgi support (php_mode_fcgi_apache) - YES'); + } + + if (!$versionData['php_mode_fcgi_nginxfpm']) { + writeln('nginx fast-cgi support (php_mode_fcgi_nginxfpm) - NO'); + } else { + writeln('nginx fast-cgi support (php_mode_fcgi_nginxfpm) - YES'); + } + + writeln(str_repeat('*', 32)); + } +}); + +desc('Switches PHP version for domain'); +task('ispmanager:domain-php-select', function () { + $config = get('ispmanager'); + + if (is_null($config['phpSelect'])) { + warning('Action for domain update is not active'); + return; + } + + if (!isset($config['phpSelect']['name']) || $config['phpSelect']['name'] == '') { + throw new Exception('Invalid domain name!'); + } + + $existDomains = get('ispmanager_domains'); + + if (!in_array($config['phpSelect']['name'], $existDomains)) { + throw new Exception('Domain not exist!'); + } + + if (!isset($config['phpSelect']['mode']) || !isset($config['phpSelect']['version'])) { + throw new Exception('Incorrect settings for select php version'); + } + + $phpVersions = get('ispmanager_phplist'); + + $newVersion = $config['phpSelect']['version']; + $newMode = $config['phpSelect']['mode']; + + if (!isset($phpVersions[$newVersion])) { + throw new Exception('Incorrect php version'); + } + + $versionData = $phpVersions[$newVersion]; + + if (!isset($versionData[$newMode]) || !$versionData[$newMode]) { + throw new Exception('Incorrect php mode'); + } + + $domainUpdateRequest = [ + 'func' => 'webdomain.edit', + 'elid' => $config['phpSelect']['name'], + 'name' => $config['phpSelect']['name'], + 'php_mode' => $newMode, + 'sok' => 'ok', + ]; + + if ($newMode == 'php_mode_mod') { + $domainUpdateRequest['php_apache_version'] = $newVersion; + } elseif ($newMode == 'php_mode_cgi') { + $domainUpdateRequest['php_cgi_version'] = $newVersion; + } elseif ($newMode == 'php_mode_fcgi_apache') { + $domainUpdateRequest['php_cgi_version'] = $newVersion; + $domainUpdateRequest['php_apache_version'] = $newVersion; + } elseif ($newMode == 'php_mode_fcgi_nginxfpm') { + $domainUpdateRequest['php_cgi_version'] = $newVersion; + $domainUpdateRequest['php_fpm_version'] = $newVersion; + } else { + throw new Exception('Unknown PHP mode!'); + } + + $response = ispmanagerRequest('post', $domainUpdateRequest); + + if (isset($response['doc']['error']['msg']['$'])) { + throw new Exception($response['doc']['error']['msg']['$']); + } else { + info('PHP successfully selected'); + } +}); + +desc('Creates new domain alias'); +task('ispmanager:domain-alias-create', function () { + $config = get('ispmanager'); + + if (is_null($config['createAlias'])) { + warning('Action for alias create is not active'); + return; + } + + if (!isset($config['createAlias']['name']) || $config['createAlias']['name'] == '') { + throw new Exception('Invalid domain name!'); + } + + $existDomains = get('ispmanager_domains'); + + if (!in_array($config['createAlias']['name'], $existDomains)) { + throw new Exception('Domain not exist!'); + } + + if (!isset($config['createAlias']['alias']) || $config['createAlias']['alias'] == '') { + throw new Exception('Invalid alias name!'); + } + + // Get current domain data + $response = ispmanagerRequest('get', [ + 'func' => 'webdomain.edit', + 'elid' => $config['createAlias']['name'], + 'elname' => $config['createAlias']['name'], + ]); + + $existAliases = []; + if (isset($response['doc']['aliases']['$'])) { + $existAliases = explode(' ', $response['doc']['aliases']['$']); + } + + $newAliasList = []; + $createAliasList = explode(' ', $config['createAlias']['alias']); + foreach ($createAliasList as $createAlias) { + if (in_array($createAlias, $existAliases)) { + if (!isset($config['createAlias']['skipIfExist']) || !$config['createAlias']['skipIfExist']) { + throw new Exception('Alias already exists!'); + } else { + warning('Alias ' . $createAlias . ' already exists - skipping'); + continue; + } + } + + $newAliasList[] = $createAlias; + } + + $saveAliases = array_merge($existAliases, $newAliasList); + + $domainUpdateRequest = [ + 'func' => 'webdomain.edit', + 'elid' => $config['createAlias']['name'], + 'name' => $config['createAlias']['name'], + 'aliases' => implode(' ', $saveAliases), + 'sok' => 'ok', + ]; + + $response = ispmanagerRequest('post', $domainUpdateRequest); + + if (isset($response['doc']['error']['msg']['$'])) { + throw new Exception($response['doc']['error']['msg']['$']); + } else { + info('Alias successfully created'); + } +}); + +desc('Deletes domain alias'); +task('ispmanager:domain-alias-delete', function () { + $config = get('ispmanager'); + + if (is_null($config['deleteAlias'])) { + warning('Action for alias create is not active'); + return; + } + + if (!isset($config['deleteAlias']['name']) || $config['deleteAlias']['name'] == '') { + throw new Exception('Invalid domain name!'); + } + + $existDomains = get('ispmanager_domains'); + + if (!in_array($config['deleteAlias']['name'], $existDomains)) { + throw new Exception('Domain not exist!'); + } + + if (!isset($config['deleteAlias']['alias']) || $config['deleteAlias']['alias'] == '') { + throw new Exception('Invalid alias name!'); + } + + // Get current domain data + $response = ispmanagerRequest('get', [ + 'func' => 'webdomain.edit', + 'elid' => $config['createAlias']['name'], + 'elname' => $config['createAlias']['name'], + ]); + + $existAliases = []; + if (isset($response['doc']['aliases']['$'])) { + $existAliases = explode(' ', $response['doc']['aliases']['$']); + } + + $deleteAliasList = explode(' ', $config['deleteAlias']['alias']); + foreach ($deleteAliasList as $deleteAlias) { + if (!in_array($deleteAlias, $existAliases)) { + if (!isset($config['deleteAlias']['skipIfNotExist']) || !$config['deleteAlias']['skipIfNotExist']) { + throw new Exception('Alias not exist!'); + } else { + warning('Alias ' . $deleteAlias . ' not exist - skipping'); + continue; + } + } + + if (($index = array_search($deleteAlias, $existAliases)) !== false) { + unset($existAliases[$index]); + } + } + + $domainUpdateRequest = [ + 'func' => 'webdomain.edit', + 'elid' => $config['deleteAlias']['name'], + 'name' => $config['deleteAlias']['name'], + 'aliases' => implode(' ', $existAliases), + 'sok' => 'ok', + ]; + + $response = ispmanagerRequest('post', $domainUpdateRequest); + + if (isset($response['doc']['error']['msg']['$'])) { + throw new Exception($response['doc']['error']['msg']['$']); + } else { + info('Alias successfully deleted'); + } +}); + +desc('Deletes domain'); +task('ispmanager:domain-delete', function () { + $config = get('ispmanager'); + + if (is_null($config['deleteDomain'])) { + warning('Action for domain delete is not active'); + return; + } + + if (!isset($config['deleteDomain']['name']) || $config['deleteDomain']['name'] == '') { + throw new Exception('Invalid domain name!'); + } + + // Check domain exists + $existDomains = get('ispmanager_domains'); + + if (!in_array($config['deleteDomain']['name'], $existDomains)) { + if (!isset($config['deleteDomain']['skipIfNotExist']) || !$config['deleteDomain']['skipIfNotExist']) { + throw new Exception('Domain not exist!'); + } else { + warning('Domain not exist - skipping'); + return; + } + } + + // Build request + $domainDeleteRequest = [ + 'func' => 'webdomain.delete.confirm', + 'elid' => $config['deleteDomain']['name'], + 'sok' => 'ok', + ]; + + if (!isset($config['deleteDomain']['removeDir']) || !$config['deleteDomain']['removeDir']) { + $domainDeleteRequest['remove_directory'] = 'off'; + } else { + $domainDeleteRequest['remove_directory'] = 'on'; + } + + $response = ispmanagerRequest('post', $domainDeleteRequest); + + if (isset($response['doc']['error']['msg']['$'])) { + throw new Exception($response['doc']['error']['msg']['$']); + } else { + info('Domain successfully deleted'); + } +}); + +desc('Auto task processing'); +task('ispmanager:process', function () { + $config = get('ispmanager'); + + if (!is_null($config['createDatabase'])) { + invoke('ispmanager:db-create'); + } + + if (!is_null($config['deleteDatabase'])) { + invoke('ispmanager:db-delete'); + } + + if (!is_null($config['createDomain'])) { + invoke('ispmanager:domain-create'); + } + + if (!is_null($config['deleteDomain'])) { + invoke('ispmanager:domain-delete'); + } + + if (!is_null($config['phpSelect'])) { + invoke('ispmanager:domain-php-select'); + } + + if (!is_null($config['createAlias'])) { + invoke('ispmanager:domain-alias-create'); + } + + if (!is_null($config['deleteAlias'])) { + invoke('ispmanager:domain-alias-delete'); + } +}); + +function ispmanagerRequest($method, $requestData) +{ + $config = get('ispmanager'); + $dsnData = parse_url($config['api']['dsn']); + + $requestUrl = $dsnData['scheme'] . '://' . $dsnData['host'] . ':' . $dsnData['port'] . $dsnData['path']; + + if ($config['api']['secure'] && get('ispmanager_session') == '') { + ispmanagerAuthRequest($requestUrl, $dsnData['user'], $dsnData['pass']); + } + + if ($method == 'post') { + return Httpie::post($requestUrl) + ->formBody(prepareRequest($requestData)) + ->setopt(CURLOPT_SSL_VERIFYPEER, false) + ->setopt(CURLOPT_SSL_VERIFYHOST, false) + ->getJson(); + } elseif ($method == 'get') { + return Httpie::get($requestUrl) + ->query(prepareRequest($requestData)) + ->setopt(CURLOPT_SSL_VERIFYPEER, false) + ->setopt(CURLOPT_SSL_VERIFYHOST, false) + ->getJson(); + } else { + throw new Exception('Unknown request method'); + } +} + +function ispmanagerAuthRequest($url, $login, $pass) +{ + $authRequestData = [ + 'func' => 'auth', + 'username' => $login, + 'password' => $pass, + ]; + + $responseData = Httpie::post($url) + ->setopt(CURLOPT_SSL_VERIFYPEER, false) + ->setopt(CURLOPT_SSL_VERIFYHOST, false) + ->formBody(prepareRequest($authRequestData)) + ->getJson(); + + if (isset($responseData['doc']['auth']['$id'])) { + set('ispmanager_session', $responseData['doc']['auth']['$id']); + } else { + throw new Exception('Error while create auth session'); + } +} + +function prepareRequest($requestData) +{ + $config = get('ispmanager'); + $dsnData = parse_url($config['api']['dsn']); + + if (!isset($requestData['out'])) { + $requestData['out'] = 'json'; + } + + if (!$config['api']['secure']) { + $requestData['authinfo'] = $dsnData['user'] . ':' . $dsnData['pass']; + } else { + if (get('ispmanager_session') != '') { + $requestData['auth'] = get('ispmanager_session'); + } + } + + return $requestData; +} + +function generatePassword($lenght) +{ + return substr(md5(uniqid()), 0, $lenght); +} + +// Callbacks before actions under domains +before('ispmanager:domain-alias-create', 'ispmanager:init'); +before('ispmanager:domain-alias-delete', 'ispmanager:init'); +before('ispmanager:domain-create', 'ispmanager:init'); +before('ispmanager:domain-delete', 'ispmanager:init'); +before('ispmanager:domain-php-select', 'ispmanager:init'); + +// Callbacks before actions under databases +before('ispmanager:db-create', 'ispmanager:init'); +before('ispmanager:db-delete', 'ispmanager:init'); diff --git a/vendor/deployer/deployer/contrib/mattermost.php b/vendor/deployer/deployer/contrib/mattermost.php new file mode 100644 index 0000000..29edd71 --- /dev/null +++ b/vendor/deployer/deployer/contrib/mattermost.php @@ -0,0 +1,154 @@ + get('mattermost_text'), + 'username' => get('mattermost_username'), + ]; + + if (get('mattermost_channel')) { + $body['channel'] = get('mattermost_channel'); + } + if (get('mattermost_icon_url')) { + $body['icon_url'] = get('mattermost_icon_url'); + } + + Httpie::post(get('mattermost_webhook'))->jsonBody($body)->send(); +}); + +desc('Notifies mattermost about deploy finish'); +task('mattermost:notify:success', function () { + if (null === get('mattermost_webhook')) { + return; + } + + $body = [ + 'text' => get('mattermost_success_text'), + 'username' => get('mattermost_username'), + ]; + + if (get('mattermost_channel')) { + $body['channel'] = get('mattermost_channel'); + } + if (get('mattermost_icon_url')) { + $body['icon_url'] = get('mattermost_icon_url'); + } + + Httpie::post(get('mattermost_webhook'))->jsonBody($body)->send(); +}); + +desc('Notifies mattermost about deploy failure'); +task('mattermost:notify:failure', function () { + if (null === get('mattermost_webhook')) { + return; + } + + $body = [ + 'text' => get('mattermost_failure_text'), + 'username' => get('mattermost_username'), + ]; + + if (get('mattermost_channel')) { + $body['channel'] = get('mattermost_channel'); + } + if (get('mattermost_icon_url')) { + $body['icon_url'] = get('mattermost_icon_url'); + } + + Httpie::post(get('mattermost_webhook'))->jsonBody($body)->send(); +}); diff --git a/vendor/deployer/deployer/contrib/ms-teams.php b/vendor/deployer/deployer/contrib/ms-teams.php new file mode 100644 index 0000000..3c4fbf9 --- /dev/null +++ b/vendor/deployer/deployer/contrib/ms-teams.php @@ -0,0 +1,164 @@ +jsonBody([ + "themeColor" => get('teams_color'), + 'text' => get('teams_text'), + ])->send(); + } catch (\Exception $e) { + if (get('teams_failure_continue', false)) { + warning('Error sending Teams Notification: ' . $e->getMessage()); + } else { + throw $e; + } + } + +}) + ->once() + ->hidden(); + +desc('Notifies Teams about deploy finish'); +task('teams:notify:success', function () { + if (!get('teams_webhook', false)) { + warning('No MS Teams webhook configured'); + return; + } + + try { + Httpie::post(get('teams_webhook'))->jsonBody([ + "themeColor" => get('teams_success_color'), + 'text' => get('teams_success_text'), + ])->send(); + } catch (\Exception $e) { + if (get('teams_failure_continue', false)) { + warning('Error sending Teams Notification: ' . $e->getMessage()); + } else { + throw $e; + } + } +}) + ->once() + ->hidden(); + +desc('Notifies Teams about deploy failure'); +task('teams:notify:failure', function () { + if (!get('teams_webhook', false)) { + warning('No MS Teams webhook configured'); + return; + } + + try { + Httpie::post(get('teams_webhook'))->jsonBody([ + "themeColor" => get('teams_failure_color'), + 'text' => get('teams_failure_text'), + ])->send(); + } catch (\Exception $e) { + if (get('teams_failure_continue', false)) { + warning('Error sending Teams Notification: ' . $e->getMessage()); + } else { + throw $e; + } + } +}) + ->once() + ->hidden(); diff --git a/vendor/deployer/deployer/contrib/newrelic.php b/vendor/deployer/deployer/contrib/newrelic.php new file mode 100644 index 0000000..b2c39b1 --- /dev/null +++ b/vendor/deployer/deployer/contrib/newrelic.php @@ -0,0 +1,54 @@ + get('user'), + 'revision' => get('newrelic_revision'), + 'description' => get('newrelic_description'), + ]; + + Httpie::post("https://$endpoint/v2/applications/$appId/deployments.json") + ->header("X-Api-Key", $apiKey) + ->query(['deployment' => $data]) + ->send(); + } +}) + ->once() + ->hidden(); diff --git a/vendor/deployer/deployer/contrib/npm.php b/vendor/deployer/deployer/contrib/npm.php new file mode 100644 index 0000000..3d82993 --- /dev/null +++ b/vendor/deployer/deployer/contrib/npm.php @@ -0,0 +1,35 @@ +jsonBody([ + "topic" => get('ntfy_topic'), + "title" => get('ntfy_title'), + "message" => get('ntfy_text'), + "tags" => explode(",", get('ntfy_tags')), + "priority" => get('ntfy_priority'), + ])->send(); +}) + ->once() + ->hidden(); + +desc('Notifies ntfy server about deploy finish'); +task('ntfy:notify:success', function () { + if (!get('ntfy_topic', false)) { + warning('No ntfy topic configured'); + return; + } + + Httpie::post(get('ntfy_server'))->jsonBody([ + "topic" => get('ntfy_topic'), + "title" => get('ntfy_title'), + "message" => get('ntfy_success_text'), + "tags" => explode(",", get('ntfy_success_tags')), + "priority" => get('ntfy_success_priority'), + ])->send(); +}) + ->once() + ->hidden(); + +desc('Notifies ntfy server about deploy failure'); +task('ntfy:notify:failure', function () { + if (!get('ntfy_topic', false)) { + warning('No ntfy topic configured'); + return; + } + + Httpie::post(get('ntfy_server'))->jsonBody([ + "topic" => get('ntfy_topic'), + "title" => get('ntfy_title'), + "message" => get('ntfy_failure_text'), + "tags" => explode(",", get('ntfy_failure_tags')), + "priority" => get('ntfy_failure_priority'), + ])->send(); +}) + ->once() + ->hidden(); diff --git a/vendor/deployer/deployer/contrib/phinx.php b/vendor/deployer/deployer/contrib/phinx.php new file mode 100644 index 0000000..2e37c43 --- /dev/null +++ b/vendor/deployer/deployer/contrib/phinx.php @@ -0,0 +1,228 @@ + 'development', + 'configuration' => './migration/.phinx.yml', + 'target' => '20120103083322', + 'remove-all' => '', +]; + +set('phinx_path', '/usr/local/phinx/bin/phinx'); +set('phinx', $phinx_env_vars); + +after('cleanup', 'phinx:migrate'); + +// or set it for a specific server +host('dev') + ->user('user') + ->set('deploy_path', '/var/www') + ->set('phinx', $phinx_env_vars) + ->set('phinx_path', ''); +``` + +## Suggested Usage + +You can run all tasks before or after any +tasks (but you need to specify external configs for phinx). +If you use internal configs (which are in your project) you need +to run it after the `deploy:update_code` task is completed. + +## Read more + +For further reading see [phinx.org](https://phinx.org). Complete descriptions of all possible options can be found on the [commands page](http://docs.phinx.org/en/latest/commands.html). + + */ + +namespace Deployer; + +use Deployer\Exception\RunException; + +/* + * Phinx recipe for Deployer + * + * @author Alexey Boyko + * @contributor Security-Database + * @copyright 2016 Alexey Boyko + * @license MIT https://github.com/deployphp/recipes/blob/master/LICENSE + * + * @link https://github.com/deployphp/recipes + * + * @see http://deployer.org + * @see https://phinx.org + */ + +/** + * Path to Phinx + */ +set('bin/phinx', function () { + try { + $phinxPath = which('phinx'); + } catch (RunException $e) { + $phinxPath = null; + } + + if ($phinxPath !== null) { + return "phinx"; + } elseif (test('[ -f {{release_path}}/vendor/bin/phinx ]')) { + return "{{release_path}}/vendor/bin/phinx"; + } elseif (test('[ -f ~/.composer/vendor/bin/phinx ]')) { + return '~/.composer/vendor/bin/phinx'; + } else { + throw new \RuntimeException('Cannot find phinx. Please specify path to phinx manually'); + } +}); + +/** + * Make Phinx command + * + * @param string $cmdName Name of command + * @param array $conf Command options(config) + * + * @return string Phinx command to execute + */ +function phinx_get_cmd($cmdName, $conf) +{ + $phinx = get('phinx_path') ?: get('bin/phinx'); + + $phinxCmd = "$phinx $cmdName"; + + $options = ''; + + foreach ($conf as $name => $value) { + $options .= " --$name $value"; + } + + $phinxCmd .= $options; + + return $phinxCmd; +} + +/** + * Returns options array that allowed for command + * + * @param array $allowedOptions List of allowed options + * + * @return array Array of options + */ +function phinx_get_allowed_config($allowedOptions) +{ + $opts = []; + + try { + foreach (get('phinx') as $key => $val) { + if (in_array($key, $allowedOptions)) { + $opts[$key] = $val; + } + } + } catch (\RuntimeException $e) { + } + + return $opts; +} + + +desc('Migrats database with phinx'); +task('phinx:migrate', function () { + $ALLOWED_OPTIONS = [ + 'configuration', + 'date', + 'environment', + 'target', + 'parser', + ]; + + $conf = phinx_get_allowed_config($ALLOWED_OPTIONS); + + cd('{{release_path}}'); + + $phinxCmd = phinx_get_cmd('migrate', $conf); + + run($phinxCmd); + + cd('{{deploy_path}}'); +}); + +desc('Rollbacks database migrations with phinx'); +task('phinx:rollback', function () { + $ALLOWED_OPTIONS = [ + 'configuration', + 'date', + 'environment', + 'target', + 'parser', + ]; + + $conf = phinx_get_allowed_config($ALLOWED_OPTIONS); + + cd('{{release_path}}'); + + $phinxCmd = phinx_get_cmd('rollback', $conf); + + run($phinxCmd); + + cd('{{deploy_path}}'); +}); + +desc('Seeds database with phinx'); +task('phinx:seed', function () { + $ALLOWED_OPTIONS = [ + 'configuration', + 'environment', + 'parser', + 'seed', + ]; + + $conf = phinx_get_allowed_config($ALLOWED_OPTIONS); + + cd('{{release_path}}'); + + $phinxCmd = phinx_get_cmd('seed:run', $conf); + + run($phinxCmd); + + cd('{{deploy_path}}'); +}); + +desc('Sets a migrations breakpoint with phinx'); +task('phinx:breakpoint', function () { + $ALLOWED_OPTIONS = [ + 'configuration', + 'environment', + 'remove-all', + 'target', + ]; + + $conf = phinx_get_allowed_config($ALLOWED_OPTIONS); + + cd('{{release_path}}'); + + $phinxCmd = phinx_get_cmd('breakpoint', $conf); + + run($phinxCmd); + + cd('{{deploy_path}}'); +}); diff --git a/vendor/deployer/deployer/contrib/php-fpm.php b/vendor/deployer/deployer/contrib/php-fpm.php new file mode 100644 index 0000000..cd4fd7d --- /dev/null +++ b/vendor/deployer/deployer/contrib/php-fpm.php @@ -0,0 +1,45 @@ + 'localhost', + 'port' => '5672', + 'username' => 'guest', + 'password' => 'guest', + 'channel' => 'notify-channel', + 'vhost' => '/my-app' +]); +``` + +### Suggested Usage + +Since you should only notify RabbitMQ channel of a successful deployment, the `deploy:rabbit` task should be executed right at the end. + +```php +// deploy.php + +before('deploy:end', 'deploy:rabbit'); +``` + */ + +namespace Deployer; + +use Deployer\Task\Context; +use PhpAmqpLib\Connection\AMQPConnection; +use PhpAmqpLib\Message\AMQPMessage; + +desc('Notifies RabbitMQ channel about deployment'); +task('deploy:rabbit', function () { + + if (!class_exists('PhpAmqpLib\Connection\AMQPConnection')) { + throw new \RuntimeException("Please install php package videlalvaro/php-amqplib to use rabbitmq"); + } + + $config = get('rabbit', []); + + if (!isset($config['message'])) { + $releasePath = get('release_path'); + $host = Context::get()->getHost(); + + $stage = get('stage', false); + $stageInfo = ($stage) ? sprintf(' on *%s*', $stage) : ''; + + $message = "Deployment to '%s'%s was successful\n(%s)"; + $config['message'] = sprintf( + $message, + $host->getHostname(), + $stageInfo, + $releasePath, + ); + } + + $defaultConfig = [ + 'host' => 'localhost', + 'port' => 5672, + 'username' => 'guest', + 'password' => 'guest', + 'vhost' => '/', + ]; + + $config = array_merge($defaultConfig, $config); + + if (!isset($config['channel']) + || !isset($config['host']) + || !isset($config['port']) + || !isset($config['username']) + || !isset($config['password']) + || !isset($config['vhost'])) { + throw new \RuntimeException("Please configure rabbit config: set('rabbit', array('channel' => 'channel', 'host' => 'host', 'port' => 'port', 'username' => 'username', 'password' => 'password'));"); + } + + $connection = new AMQPConnection($config['host'], $config['port'], $config['username'], $config['password'], $config['vhost']); + $channel = $connection->channel(); + + $msg = new AMQPMessage($config['message']); + $channel->basic_publish($msg, $config['channel'], $config['channel']); + + $channel->close(); + $connection->close(); + +}); diff --git a/vendor/deployer/deployer/contrib/raygun.php b/vendor/deployer/deployer/contrib/raygun.php new file mode 100644 index 0000000..f4088dd --- /dev/null +++ b/vendor/deployer/deployer/contrib/raygun.php @@ -0,0 +1,42 @@ + get('raygun_api_key'), + 'version' => get('raygun_version'), + 'ownerName' => get('raygun_owner_name'), + 'emailAddress' => get('raygun_email'), + 'comment' => get('raygun_comment'), + 'scmIdentifier' => get('raygun_scm_identifier'), + 'scmType' => get('raygun_scm_type'), + ]; + + Httpie::post('https://app.raygun.io/deployments') + ->jsonBody($data) + ->send(); +}); diff --git a/vendor/deployer/deployer/contrib/rocketchat.php b/vendor/deployer/deployer/contrib/rocketchat.php new file mode 100644 index 0000000..2ad9bfe --- /dev/null +++ b/vendor/deployer/deployer/contrib/rocketchat.php @@ -0,0 +1,173 @@ + get('rockchat_title'), + 'username' => get('rocketchat_username'), + 'attachments' => [[ + 'text' => get('rocketchat_text'), + 'color' => get('rocketchat_color'), + ]], + ]; + + if (get('rocketchat_channel')) { + $body['channel'] = get('rocketchat_channel'); + } + if (get('rocketchat_room_id')) { + $body['roomId'] = get('rocketchat_room_id'); + } + if (get('rocketchat_icon_url')) { + $body['avatar'] = get('rocketchat_icon_url'); + } elseif (get('rocketchat_icon_emoji')) { + $body['emoji'] = get('rocketchat_icon_emoji'); + } + + Httpie::post(get('rocketchat_webhook'))->jsonBody($body)->send(); +}); + +desc('Notifies RocketChat about deploy finish'); +task('rocketchat:notify:success', function () { + if (null === get('rocketchat_webhook')) { + return; + } + + $body = [ + 'text' => get('rockchat_title'), + 'username' => get('rocketchat_username'), + 'attachments' => [[ + 'text' => get('rocketchat_success_text'), + 'color' => get('rocketchat_success_color'), + ]], + ]; + + if (get('rocketchat_channel')) { + $body['channel'] = get('rocketchat_channel'); + } + if (get('rocketchat_room_id')) { + $body['roomId'] = get('rocketchat_room_id'); + } + if (get('rocketchat_icon_url')) { + $body['avatar'] = get('rocketchat_icon_url'); + } elseif (get('rocketchat_icon_emoji')) { + $body['emoji'] = get('rocketchat_icon_emoji'); + } + + Httpie::post(get('rocketchat_webhook'))->jsonBody($body)->send(); +}); + +desc('Notifies RocketChat about deploy failure'); +task('rocketchat:notify:failure', function () { + if (null === get('rocketchat_webhook')) { + return; + } + + $body = [ + 'text' => get('rockchat_title'), + 'username' => get('rocketchat_username'), + 'attachments' => [[ + 'color' => get('rocketchat_failure_color'), + 'text' => get('rocketchat_failure_text'), + ]], + ]; + + if (get('rocketchat_channel')) { + $body['channel'] = get('rocketchat_channel'); + } + if (get('rocketchat_room_id')) { + $body['roomId'] = get('rocketchat_room_id'); + } + if (get('rocketchat_icon_url')) { + $body['avatar'] = get('rocketchat_icon_url'); + } elseif (get('rocketchat_icon_emoji')) { + $body['emoji'] = get('rocketchat_icon_emoji'); + } + + Httpie::post(get('rocketchat_webhook'))->jsonBody($body)->send(); +}); diff --git a/vendor/deployer/deployer/contrib/rollbar.php b/vendor/deployer/deployer/contrib/rollbar.php new file mode 100644 index 0000000..a268fb9 --- /dev/null +++ b/vendor/deployer/deployer/contrib/rollbar.php @@ -0,0 +1,48 @@ + get('rollbar_token'), + 'environment' => get('where'), + 'revision' => runLocally('git log -n 1 --format="%h"'), + 'local_username' => get('user'), + 'rollbar_username' => get('rollbar_username'), + 'comment' => get('rollbar_comment'), + ]; + + Httpie::post('https://api.rollbar.com/api/1/deploy/') + ->formBody($params) + ->send(); +}) + ->once(); diff --git a/vendor/deployer/deployer/contrib/rsync.php b/vendor/deployer/deployer/contrib/rsync.php new file mode 100644 index 0000000..b648f74 --- /dev/null +++ b/vendor/deployer/deployer/contrib/rsync.php @@ -0,0 +1,252 @@ + [ + '.git', + 'deploy.php', + ], + 'exclude-file' => false, + 'include' => [], + 'include-file' => false, + 'filter' => [], + 'filter-file' => false, + 'filter-perdir'=> false, + 'flags' => 'rz', // Recursive, with compress + 'options' => ['delete'], + 'timeout' => 60, +]); +``` + +If You have multiple excludes, You can put them in file and reference that instead. If You use `deploy:rsync_warmup` You could set additional options that could speed-up and/or affect way things are working. For example: + +```php +// deploy.php + +set('rsync',[ + 'exclude' => ['excludes_file'], + 'exclude-file' => '/tmp/localdeploys/excludes_file', //Use absolute path to avoid possible rsync problems + 'include' => [], + 'include-file' => false, + 'filter' => [], + 'filter-file' => false, + 'filter-perdir' => false, + 'flags' => 'rzcE', // Recursive, with compress, check based on checksum rather than time/size, preserve Executable flag + 'options' => ['delete', 'delete-after', 'force'], //Delete after successful transfer, delete even if deleted dir is not empty + 'timeout' => 3600, //for those huge repos or crappy connection +]); +``` + + +### Parameter + +- **rsync_src**: per-host rsync source. This can be server, stage or whatever-dependent. By default it's set to current directory +- **rsync_dest**: per-host rsync destination. This can be server, stage or whatever-dependent. by default it's equivalent to release deploy destination. + +### Sample configurations: + +This is default configuration: + +```php +set('rsync_src', __DIR__); +set('rsync_dest','{{release_path}}'); +``` + +If You use local deploy recipe You can set src to local release: + +```php +host('hostname') + ->hostname('10.10.10.10') + ->port(22) + ->set('deploy_path','/your/remote/path/app') + ->set('rsync_src', '/your/local/path/app') + ->set('rsync_dest','{{release_path}}'); +``` + +## Usage + +- `rsync` task + + Set `rsync_src` to locally cloned repository and rsync to `rsync_dest`. Then set this task instead of `deploy:update_code` in Your `deploy` task if Your hosting provider does not allow git. + +- `rsync:warmup` task + + If Your deploy task looks like: + + ```php + task('deploy', [ + 'deploy:prepare', + 'deploy:release', + 'rsync', + 'deploy:vendors', + 'deploy:symlink', + ])->desc('Deploy your project'); + ``` + + And Your `rsync_dest` is set to `{{release_path}}` then You could add this task to run before `rsync` task or after `deploy:release`, whatever is more convenient. + + */ + +namespace Deployer; + +use Deployer\Host\Localhost; +use Deployer\Task\Context; + +use function Deployer\Support\rsync_rsh; + +set('rsync', [ + 'exclude' => [ + '.git', + 'deploy.php', + ], + 'exclude-file' => false, + 'include' => [], + 'include-file' => false, + 'filter' => [], + 'filter-file' => false, + 'filter-perdir' => false, + 'flags' => 'rz', + 'options' => ['delete'], + 'timeout' => 300, +]); + +set('rsync_src', __DIR__); +set('rsync_dest', '{{release_path}}'); + +set('rsync_excludes', function () { + $config = get('rsync'); + $excludes = $config['exclude']; + $excludeFile = $config['exclude-file']; + $excludesRsync = ''; + foreach ($excludes as $exclude) { + $excludesRsync .= ' --exclude=' . quote($exclude); + } + if (!empty($excludeFile) && file_exists($excludeFile) && is_file($excludeFile) && is_readable($excludeFile)) { + $excludesRsync .= ' --exclude-from=' . quote($excludeFile); + } + + return $excludesRsync; +}); + +set('rsync_includes', function () { + $config = get('rsync'); + $includes = $config['include']; + $includeFile = $config['include-file']; + $includesRsync = ''; + foreach ($includes as $include) { + $includesRsync .= ' --include=' . quote($include); + } + if (!empty($includeFile) && file_exists($includeFile) && is_file($includeFile) && is_readable($includeFile)) { + $includesRsync .= ' --include-from=' . quote($includeFile); + } + + return $includesRsync; +}); + +set('rsync_filter', function () { + $config = get('rsync'); + $filters = $config['filter']; + $filterFile = $config['filter-file']; + $filterPerDir = $config['filter-perdir']; + $filtersRsync = ''; + foreach ($filters as $filter) { + $filtersRsync .= " --filter='$filter'"; + } + if (!empty($filterFile)) { + $filtersRsync .= " --filter='merge $filterFile'"; + } + if (!empty($filterPerDir)) { + $filtersRsync .= " --filter='dir-merge $filterPerDir'"; + } + return $filtersRsync; +}); + +set('rsync_options', function () { + $config = get('rsync'); + $options = $config['options']; + $optionsRsync = []; + foreach ($options as $option) { + $optionsRsync[] = "--$option"; + } + return implode(' ', $optionsRsync); +}); + + +desc('Warmups remote Rsync target'); +task('rsync:warmup', function () { + $config = get('rsync'); + + $source = "{{current_path}}"; + $destination = "{{deploy_path}}/release"; + + if (test("[ -d $(echo $source) ]")) { + run("rsync -{$config['flags']} {{rsync_options}}{{rsync_excludes}}{{rsync_includes}}{{rsync_filter}} $source/ $destination/"); + } else { + writeln("No way to warmup rsync."); + } +}); + + +desc('Rsync local->remote'); +task('rsync', function () { + $config = get('rsync'); + + $src = get('rsync_src'); + while (is_callable($src)) { + $src = $src(); + } + + if (!trim($src)) { + // if $src is not set here rsync is going to do a directory listing + // exiting with code 0, since only doing a directory listing clearly + // is not what we want to achieve we need to throw an exception + throw new \RuntimeException('You need to specify a source path.'); + } + + $dst = get('rsync_dest'); + while (is_callable($dst)) { + $dst = $dst(); + } + + if (!trim($dst)) { + // if $dst is not set here we are going to sync to root + // and even worse - depending on rsync flags and permission - + // might end up deleting everything we have write permission to + throw new \RuntimeException('You need to specify a destination path.'); + } + + $rsyncFlags = (is_string($config['flags']) && trim($config['flags']) !== '') ? "-{$config['flags']}" : ''; + + $host = Context::get()->getHost(); + if ($host instanceof Localhost) { + runLocally("rsync {$rsyncFlags} {{rsync_options}}{{rsync_includes}}{{rsync_excludes}}{{rsync_filter}} '$src/' '$dst/'", timeout: $config['timeout']); + return; + } + + $rsh = quote(rsync_rsh($host->connectionOptions())); + runLocally("rsync {$rsyncFlags} -e $rsh {{rsync_options}}{{rsync_includes}}{{rsync_excludes}}{{rsync_filter}} '$src/' '{$host->connectionString()}:$dst/'", timeout: $config['timeout']); +}); diff --git a/vendor/deployer/deployer/contrib/sentry.php b/vendor/deployer/deployer/contrib/sentry.php new file mode 100644 index 0000000..4acb7bb --- /dev/null +++ b/vendor/deployer/deployer/contrib/sentry.php @@ -0,0 +1,296 @@ + 'exampleorg', + 'projects' => [ + 'exampleproj' + ], + 'token' => 'd47828...', + 'version' => '0.0.1', + +]); +``` + +### Suggested Usage + +Since you should only notify Sentry of a successful deployment, the deploy:sentry task should be executed right at the end. + +```php +// deploy.php + +after('deploy', 'deploy:sentry'); +``` + + */ + +namespace Deployer; + +use Closure; +use DateTime; +use Deployer\Exception\ConfigurationException; +use Deployer\Utility\Httpie; + +desc('Notifies Sentry of deployment'); +task( + 'deploy:sentry', + static function () { + $now = date('c'); + + $defaultConfig = [ + 'version' => getReleaseGitRef(), + 'version_prefix' => null, + 'refs' => [], + 'ref' => null, + 'commits' => getGitCommitsRefs(), + 'url' => null, + 'date_released' => $now, + 'date_deploy_started' => $now, + 'date_deploy_finished' => $now, + 'sentry_server' => 'https://sentry.io', + 'previous_commit' => null, + 'environment' => get('symfony_env', 'prod'), + 'deploy_name' => null, + ]; + + $config = array_merge($defaultConfig, (array) get('sentry')); + array_walk( + $config, + static function (&$value) use ($config) { + if (is_callable($value)) { + $value = $value($config); + } + }, + ); + + if ( + !isset($config['organization'], $config['token'], $config['version']) + || (empty($config['projects']) || !is_array($config['projects'])) + ) { + throw new \RuntimeException( + << 'exampleorg', + 'projects' => [ + 'exampleproj', + 'exampleproje2' + ], + 'token' => 'd47828...', + ] + );" + EXAMPLE, + ); + } + + $releaseData = array_filter( + [ + 'version' => ($config['version_prefix'] ?? '') . $config['version'], + 'refs' => $config['refs'], + 'ref' => $config['ref'], + 'url' => $config['url'], + 'commits' => array_slice($config['commits'] ?? [], 0), // reset keys to serialize as array in json + 'dateReleased' => $config['date_released'], + 'projects' => $config['projects'], + 'previousCommit' => $config['previous_commit'], + ], + ); + + $releasesApiUrl = $config['sentry_server'] . '/api/0/organizations/' . $config['organization'] . '/releases/'; + $response = Httpie::post( + $releasesApiUrl, + ) + ->setopt(CURLOPT_TIMEOUT, 10) + ->header('Authorization', sprintf('Bearer %s', $config['token'])) + ->jsonBody($releaseData) + ->getJson(); + + if (!isset($response['version'], $response['projects'])) { + throw new \RuntimeException(sprintf('Unable to create a release: %s', print_r($response, true))); + } + + writeln( + sprintf( + 'Sentry: Release of version %s ' + . 'for projects: %s created successfully.', + $response['version'], + implode(', ', array_column($response['projects'], 'slug')), + ), + ); + + $deployData = array_filter( + [ + 'environment' => $config['environment'], + 'name' => $config['deploy_name'], + 'url' => $config['url'], + 'dateStarted' => $config['date_deploy_started'], + 'dateFinished' => $config['date_deploy_finished'], + ], + ); + + $response = Httpie::post( + $releasesApiUrl . $response['version'] . '/deploys/', + ) + ->setopt(CURLOPT_TIMEOUT, 10) + ->header('Authorization', sprintf('Bearer %s', $config['token'])) + ->jsonBody($deployData) + ->getJson(); + + if (!isset($response['id'], $response['environment'])) { + throw new \RuntimeException(sprintf('Unable to create a deployment: %s', print_r($response, true))); + } + + writeln( + sprintf( + 'Sentry: Deployment %s ' + . 'for environment %s created successfully', + $response['id'], + $response['environment'], + ), + ); + }, +); + +function getPreviousReleaseRevision() +{ + switch (get('update_code_strategy')) { + case 'local_archive': + case 'archive': + if (has('previous_release')) { + return run('cat {{previous_release}}/REVISION'); + } + + return null; + case 'clone': + if (has('previous_release')) { + cd('{{previous_release}}'); + return trim(run('git rev-parse HEAD')); + } + + return null; + default: + throw new ConfigurationException(parse("Unknown `update_code_strategy` option: {{update_code_strategy}}.")); + } +} + +function getCurrentReleaseRevision() +{ + switch (get('update_code_strategy')) { + case 'local_archive': + case 'archive': + return run('cat {{release_path}}/REVISION'); + + case 'clone': + cd('{{release_path}}'); + return trim(run('git rev-parse HEAD')); + + default: + throw new ConfigurationException(parse("Unknown `update_code_strategy` option: {{update_code_strategy}}.")); + } +} + +function getReleaseGitRef(): Closure +{ + return static function ($config = []): string { + $strategy = get('update_code_strategy'); + + if ($strategy === 'archive') { + cd('{{deploy_path}}/.dep/repo'); + } else { + cd('{{release_path}}'); + } + + if (isset($config['git_version_command'])) { + return trim(run($config['git_version_command'])); + } + + if ($strategy !== 'clone') { + return run('cat {{current_path}}/REVISION'); + } + + return trim(run('git log -n 1 --format="%h"')); + }; +} + +function getGitCommitsRefs(): Closure +{ + return static function ($config = []): array { + $previousReleaseRevision = getPreviousReleaseRevision(); + $currentReleaseRevision = getCurrentReleaseRevision() ?: 'HEAD'; + + if ($previousReleaseRevision === null) { + $commitRange = $currentReleaseRevision; + } else { + $commitRange = $previousReleaseRevision . '..' . $currentReleaseRevision; + } + + try { + if (get('update_code_strategy') === 'archive') { + cd('{{deploy_path}}/.dep/repo'); + } else { + cd('{{release_path}}'); + } + + $result = run(sprintf('git rev-list --pretty="%s" %s', 'format:%H#%an#%ae#%at#%s', $commitRange)); + $lines = array_filter( + // limit number of commits for first release with many commits + array_map('trim', array_slice(explode("\n", $result), 0, 200)), + static function (string $line): bool { + return !empty($line) && strpos($line, 'commit') !== 0; + }, + ); + + return array_map( + static function (string $line): array { + [$ref, $authorName, $authorEmail, $timestamp, $message] = explode('#', $line, 5); + + return [ + 'id' => $ref, + 'author_name' => $authorName, + 'author_email' => $authorEmail, + 'message' => $message, + 'timestamp' => date(\DateTime::ATOM, (int) $timestamp), + ]; + }, + $lines, + ); + + } catch (\Deployer\Exception\RunException $e) { + writeln($e->getMessage()); + return []; + } + }; +} diff --git a/vendor/deployer/deployer/contrib/slack.php b/vendor/deployer/deployer/contrib/slack.php new file mode 100644 index 0000000..b78dfc4 --- /dev/null +++ b/vendor/deployer/deployer/contrib/slack.php @@ -0,0 +1,178 @@ +Add to Slack + + +Add hook on deploy: + +```php +before('deploy', 'slack:notify'); +``` + +## Configuration + +- `slack_webhook` – slack incoming webhook url, **required** + ``` + set('slack_webhook', 'https://hooks.slack.com/...'); + ``` +- `slack_channel` - channel to send notification to. The default is the channel configured in the webhook +- `slack_title` – the title of application, default `{{application}}` +- `slack_text` – notification message template, markdown supported + ``` + set('slack_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*'); + ``` +- `slack_success_text` – success template, default: + ``` + set('slack_success_text', 'Deploy to *{{where}}* successful'); + ``` +- `slack_failure_text` – failure template, default: + ``` + set('slack_failure_text', 'Deploy to *{{where}}* failed'); + ``` + +- `slack_color` – color's attachment +- `slack_success_color` – success color's attachment +- `slack_failure_color` – failure color's attachment +- `slack_fields` - set attachments fields for pretty output in Slack, default: + ``` + set('slack_fields', []); + ``` + +## Usage + +If you want to notify only about beginning of deployment add this line only: + +```php +before('deploy', 'slack:notify'); +``` + +If you want to notify about successful end of deployment add this too: + +```php +after('deploy:success', 'slack:notify:success'); +``` + +If you want to notify about failed deployment add this too: + +```php +after('deploy:failed', 'slack:notify:failure'); +``` + + */ + +namespace Deployer; + +use Deployer\Utility\Httpie; + +// Channel to publish to, when false the default channel the webhook will be used +set('slack_channel', false); + +// Title of project +set('slack_title', function () { + return get('application', 'Project'); +}); + +// Deploy message +set('slack_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*'); +set('slack_success_text', 'Deploy to *{{where}}* successful'); +set('slack_failure_text', 'Deploy to *{{where}}* failed'); +set('slack_rollback_text', '_{{user}}_ rolled back changes on *{{where}}*'); +set('slack_fields', []); + +// Color of attachment +set('slack_color', '#4d91f7'); +set('slack_success_color', '#00c100'); +set('slack_failure_color', '#ff0909'); +set('slack_rollback_color', '#eba211'); + +function checkSlackAnswer($result) +{ + if ('invalid_token' === $result) { + warning('Invalid Slack token'); + return false; + } + return true; +} + +desc('Notifies Slack'); +task('slack:notify', function () { + if (!get('slack_webhook', false)) { + warning('No Slack webhook configured'); + return; + } + + $attachment = [ + 'title' => get('slack_title'), + 'text' => get('slack_text'), + 'color' => get('slack_color'), + 'mrkdwn_in' => ['text'], + ]; + + $result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send(); + checkSlackAnswer($result); +}) + ->once() + ->hidden(); + +desc('Notifies Slack about deploy finish'); +task('slack:notify:success', function () { + if (!get('slack_webhook', false)) { + warning('No Slack webhook configured'); + return; + } + + $attachment = [ + 'title' => get('slack_title'), + 'text' => get('slack_success_text'), + 'color' => get('slack_success_color'), + 'fields' => get('slack_fields'), + 'mrkdwn_in' => ['text'], + ]; + + $result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send(); + checkSlackAnswer($result); +}) + ->once() + ->hidden(); + +desc('Notifies Slack about deploy failure'); +task('slack:notify:failure', function () { + if (!get('slack_webhook', false)) { + warning('No Slack webhook configured'); + return; + } + + $attachment = [ + 'title' => get('slack_title'), + 'text' => get('slack_failure_text'), + 'color' => get('slack_failure_color'), + 'mrkdwn_in' => ['text'], + ]; + + $result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send(); + checkSlackAnswer($result); +}) + ->once() + ->hidden(); + +desc('Notifies Slack about rollback'); +task('slack:notify:rollback', function () { + if (!get('slack_webhook', false)) { + warning('No Slack webhook configured'); + return; + } + + $attachment = [ + 'title' => get('slack_title'), + 'text' => get('slack_rollback_text'), + 'color' => get('slack_rollback_color'), + 'mrkdwn_in' => ['text'], + ]; + + $result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send(); + checkSlackAnswer($result); +}) + ->once() + ->hidden(); diff --git a/vendor/deployer/deployer/contrib/supervisord-monitor.php b/vendor/deployer/deployer/contrib/supervisord-monitor.php new file mode 100644 index 0000000..0bf1993 --- /dev/null +++ b/vendor/deployer/deployer/contrib/supervisord-monitor.php @@ -0,0 +1,177 @@ + 'https://youruri.xyz/supervisor', + 'basic_auth_user' => 'username', + 'basic_auth_password' => 'password', + 'process_name' => 'process01', +]); +``` + +or + +``` +set('supervisord_uri', 'https://youruri.xyz/supervisor'); +set('supervisord_basic_auth_user', 'username'); +set('supervisord_basic_auth_password', 'password'); +set('supervisord_process_name', 'process01'); +``` + +- `supervisord` – array with configuration for Supervisord + - `uri` – URI to the Supervisord monitor page + - `basic_auth_user` – Basic auth username to access the URI + - `basic_auth_password` – Basic auth password to access the URI + - `process_name` – the process name, as visible in the Supervisord monitor page. Multiple processes can be listed here, comma separated + +### Task + +- `supervisord-monitor:restart` Restarts given processes +- `supervisord-monitor:stop` Stops given processes +- `supervisord-monitor:start` Starts given processes + +### Usage + +A complete example with configs, staging and deployment + +``` + 'https://youruri.xyz/supervisor', + 'basic_auth_user' => 'username', + 'basic_auth_password' => 'password', + 'process_name' => 'process01', +]); + +host('staging.myproject.com') + ->set('branch', 'develop') + ->set('labels', ['stage' => 'staging']); + +host('myproject.com') + ->set('branch', 'main') + ->set('labels', ['stage' => 'production']); + +// Tasks +task('build', function () { + run('cd {{release_path}} && build'); +}); + +task('deploy', [ + 'build', + 'supervisord', +]); + +task('supervisord', ['supervisord-monitor:restart']) + ->select('stage=production'); +``` +*/ + +namespace Deployer; + +use Deployer\Utility\Httpie; + +function supervisordCheckConfig() +{ + $config = get('supervisord', []); + foreach ($config as $key => $value) { + if ($value) { + set('supervisord_' . $key, $value); + } + } + + if (!get('supervisord_uri') + || !get('supervisord_basic_auth_user') + || !get('supervisord_basic_auth_password') + || !get('supervisord_process_name')) { + throw new \RuntimeException("Please configure Supervisord config: set('supervisord', array('uri' => 'yourdomain.xyz/supervisor', 'basic_auth_user' => 'abc' , 'basic_auth_password' => 'xyz', 'process_name' => 'process01,process02')); or set('supervisord_uri', 'yourdomain.xyz/supervisor'); set('supervisord_basic_auth_user', 'abc'); etc"); + } +} + +function supervisordGetBasicAuthToken() +{ + return 'Basic ' . base64_encode(get('supervisord_basic_auth_user') . ':' . get('supervisord_basic_auth_password')); +} + +function supervisordIsAuthenticated() +{ + supervisordCheckConfig(); + + $authResponseInfo = []; + Httpie::post(get('supervisord_uri'))->header('Authorization', supervisordGetBasicAuthToken())->send($authResponseInfo); + + return $authResponseInfo['http_code'] === 200; +} + +function supervisordControlAction($name, $action = 'stop') +{ + $stopResponseInfo = []; + Httpie::post(get('supervisord_uri') . '/control/' . $action . '/localhost/' . $name)->header('Authorization', supervisordGetBasicAuthToken())->send($stopResponseInfo); + + return $stopResponseInfo['http_code'] === 200; +} + +task('supervisord-monitor:restart', function () { + if (supervisordIsAuthenticated()) { + $names = explode(',', get('supervisord_process_name')); + foreach ($names as $name) { + $name = trim($name); + if (supervisordControlAction($name, 'stop')) { + writeln('Daemon [' . $name . '] stopped'); + if (supervisordControlAction($name, 'start')) { + writeln('Daemon [' . $name . '] started'); + } + } + } + } else { + writeln('Authentication failed'); + } +}); + +task('supervisord-monitor:stop', function () { + if (supervisordIsAuthenticated()) { + $names = explode(',', get('supervisord_process_name')); + foreach ($names as $name) { + $name = trim($name); + if (supervisordControlAction($name, 'stop')) { + writeln('Daemon [' . $name . '] stopped'); + } + } + } else { + writeln('Authentication failed'); + } +}); + +task('supervisord-monitor:start', function () { + if (supervisordIsAuthenticated()) { + $names = explode(',', get('supervisord_process_name')); + foreach ($names as $name) { + $name = trim($name); + if (supervisordControlAction($name, 'start')) { + writeln('Daemon [' . $name . '] started'); + } + } + } else { + writeln('Authentication failed'); + } +}); diff --git a/vendor/deployer/deployer/contrib/telegram.php b/vendor/deployer/deployer/contrib/telegram.php new file mode 100644 index 0000000..0015fea --- /dev/null +++ b/vendor/deployer/deployer/contrib/telegram.php @@ -0,0 +1,177 @@ + get('telegram_chat_id'), + 'text' => get('telegram_text'), + 'parse_mode' => 'Markdown', + ], + ); + + $httpie = Httpie::get($telegramUrl); + + if (get('telegram_proxy', '') !== '') { + $httpie = $httpie->setopt(CURLOPT_PROXY, get('telegram_proxy')); + } + + $httpie->send(); +}) + ->once() + ->hidden(); + +desc('Notifies Telegram about deploy finish'); +task('telegram:notify:success', function () { + if (!get('telegram_token', false)) { + warning('No Telegram token configured'); + return; + } + + if (!get('telegram_chat_id', false)) { + warning('No Telegram chat id configured'); + return; + } + + $telegramUrl = get('telegram_url') . '?' . http_build_query( + [ + 'chat_id' => get('telegram_chat_id'), + 'text' => get('telegram_success_text'), + 'parse_mode' => 'Markdown', + ], + ); + + $httpie = Httpie::get($telegramUrl); + + if (get('telegram_proxy', '') !== '') { + $httpie = $httpie->setopt(CURLOPT_PROXY, get('telegram_proxy')); + } + + $httpie->send(); +}) + ->once() + ->hidden(); + +desc('Notifies Telegram about deploy failure'); +task('telegram:notify:failure', function () { + if (!get('telegram_token', false)) { + warning('No Telegram token configured'); + return; + } + + if (!get('telegram_chat_id', false)) { + warning('No Telegram chat id configured'); + return; + } + + $telegramUrl = get('telegram_url') . '?' . http_build_query( + [ + 'chat_id' => get('telegram_chat_id'), + 'text' => get('telegram_failure_text'), + 'parse_mode' => 'Markdown', + ], + ); + + $httpie = Httpie::get($telegramUrl); + + if (get('telegram_proxy', '') !== '') { + $httpie = $httpie->setopt(CURLOPT_PROXY, get('telegram_proxy')); + } + + $httpie->send(); +}) + ->once() + ->hidden(); diff --git a/vendor/deployer/deployer/contrib/webpack_encore.php b/vendor/deployer/deployer/contrib/webpack_encore.php new file mode 100644 index 0000000..df71168 --- /dev/null +++ b/vendor/deployer/deployer/contrib/webpack_encore.php @@ -0,0 +1,44 @@ +/feed?access_token='); + + // With publishing bot + set('workplace_webhook', 'https://graph.facebook.com/v3.0/group/feed?access_token='); + + // Use markdown on message + set('workplace_webhook', 'https://graph.facebook.com//feed?access_token=&formatting=MARKDOWN'); + ``` + + - `workplace_text` - notification message + ``` + set('workplace_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*'); + ``` + + - `workplace_success_text` – success template, default: + ``` + set('workplace_success_text', 'Deploy to *{{where}}* successful'); + ``` + - `workplace_failure_text` – failure template, default: + ``` + set('workplace_failure_text', 'Deploy to *{{where}}* failed'); + ``` + - `workplace_edit_post` – whether to create a new post for deploy result, or edit the first one created, default creates a new post: + ``` + set('workplace_edit_post', false); + ``` + +## Usage + +If you want to notify only about beginning of deployment add this line only: + +```php +before('deploy', 'workplace:notify'); +``` + +If you want to notify about successful end of deployment add this too: + +```php +after('deploy:success', 'workplace:notify:success'); +``` + +If you want to notify about failed deployment add this too: + +```php +after('deploy:failed', 'workplace:notify:failure'); +``` + + */ + +namespace Deployer; + +use Deployer\Utility\Httpie; + +// Deploy message +set('workplace_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*'); +set('workplace_success_text', 'Deploy to *{{where}}* successful'); +set('workplace_failure_text', 'Deploy to *{{where}}* failed'); + +// By default, create a new post for every message +set('workplace_edit_post', false); + +desc('Notifies Workplace'); +task('workplace:notify', function () { + if (!get('workplace_webhook', false)) { + return; + } + $url = get('workplace_webhook') . '&message=' . urlencode(get('workplace_text')); + $response = Httpie::post($url)->getJson(); + + if (get('workplace_edit_post', false)) { + // Endpoint will be something like: https//graph.facebook.com/? + $url = sprintf( + '%s://%s/%s?%s', + parse_url(get('workplace_webhook'), PHP_URL_SCHEME), + parse_url(get('workplace_webhook'), PHP_URL_HOST), + $response['id'], + parse_url(get('workplace_webhook'), PHP_URL_QUERY), + ); + // Replace the webhook with a url that points to the created post + set('workplace_webhook', $url); + } +}) + ->once() + ->hidden(); + +desc('Notifies Workplace about deploy finish'); +task('workplace:notify:success', function () { + if (!get('workplace_webhook', false)) { + return; + } + $url = get('workplace_webhook') . '&message=' . urlencode(get('workplace_success_text')); + Httpie::post($url)->send(); +}) + ->once() + ->hidden(); + +desc('Notifies Workplace about deploy failure'); +task('workplace:notify:failure', function () { + if (!get('workplace_webhook', false)) { + return; + } + $url = get('workplace_webhook') . '&message=' . urlencode(get('workplace_failure_text')); + Httpie::post($url)->send(); +}) + ->once() + ->hidden(); diff --git a/vendor/deployer/deployer/contrib/yammer.php b/vendor/deployer/deployer/contrib/yammer.php new file mode 100644 index 0000000..2fe102b --- /dev/null +++ b/vendor/deployer/deployer/contrib/yammer.php @@ -0,0 +1,122 @@ +{{user}} deploying {{what}} to {{where}} + ``` +- `yammer_success_body` – success template, default: + ``` + Deploy to {{where}} successful + ``` +- `yammer_failure_body` – failure template, default: + ``` + Deploy to {{where}} failed + ``` + +## Usage + +If you want to notify only about beginning of deployment add this line only: + +```php +before('deploy', 'yammer:notify'); +``` + +If you want to notify about successful end of deployment add this too: + +```php +after('deploy:success', 'yammer:notify:success'); +``` + +If you want to notify about failed deployment add this too: + +```php +after('deploy:failed', 'yammer:notify:failure'); +``` + + */ + +namespace Deployer; + +use Deployer\Utility\Httpie; + +set('yammer_url', 'https://www.yammer.com/api/v1/messages.json'); + +// Title of project +set('yammer_title', function () { + return get('application', 'Project'); +}); + +// Deploy message +set('yammer_body', '{{user}} deploying {{what}} to {{where}}'); +set('yammer_success_body', 'Deploy to {{where}} successful'); +set('yammer_failure_body', 'Deploy to {{where}} failed'); + +desc('Notifies Yammer'); +task('yammer:notify', function () { + $params = [ + 'is_rich_text' => 'true', + 'message_type' => 'announcement', + 'group_id' => get('yammer_group_id'), + 'title' => get('yammer_title'), + 'body' => get('yammer_body'), + ]; + + Httpie::post(get('yammer_url')) + ->header('Authorization', 'Bearer ' . get('yammer_token')) + ->header('Content-type', 'application/json') + ->jsonBody($params) + ->send(); +}) + ->once() + ->hidden(); + +desc('Notifies Yammer about deploy finish'); +task('yammer:notify:success', function () { + $params = [ + 'is_rich_text' => 'true', + 'message_type' => 'announcement', + 'group_id' => get('yammer_group_id'), + 'title' => get('yammer_title'), + 'body' => get('yammer_success_body'), + ]; + + Httpie::post(get('yammer_url')) + ->header('Authorization', 'Bearer ' . get('yammer_token')) + ->header('Content-type', 'application/json') + ->jsonBody($params) + ->send(); +}) + ->once() + ->hidden(); + +desc('Notifies Yammer about deploy failure'); +task('yammer:notify:failure', function () { + $params = [ + 'is_rich_text' => 'true', + 'message_type' => 'announcement', + 'group_id' => get('yammer_group_id'), + 'title' => get('yammer_title'), + 'body' => get('yammer_failure_body'), + ]; + + Httpie::post(get('yammer_url')) + ->header('Authorization', 'Bearer ' . get('yammer_token')) + ->header('Content-type', 'application/json') + ->jsonBody($params) + ->send(); +}) + ->once() + ->hidden(); diff --git a/vendor/deployer/deployer/contrib/yarn.php b/vendor/deployer/deployer/contrib/yarn.php new file mode 100644 index 0000000..3f1e963 --- /dev/null +++ b/vendor/deployer/deployer/contrib/yarn.php @@ -0,0 +1,29 @@ +desc('Initialization'); + +/** + * Run migrations + */ +task('deploy:run_migrations', function () { + run('{{bin/php}} {{release_or_current_path}}/bin/cake.php migrations migrate --no-lock'); + run('{{bin/php}} {{release_or_current_path}}/bin/cake.php schema_cache build'); +})->desc('Run migrations'); + +/** + * Main task + */ +task('deploy', [ + 'deploy:prepare', + 'deploy:vendors', + 'deploy:init', + 'deploy:run_migrations', + 'deploy:publish', +])->desc('Deploy your project'); diff --git a/vendor/deployer/deployer/recipe/codeigniter.php b/vendor/deployer/deployer/recipe/codeigniter.php new file mode 100644 index 0000000..46bf1f2 --- /dev/null +++ b/vendor/deployer/deployer/recipe/codeigniter.php @@ -0,0 +1,23 @@ + #.#: The minimum Codeigniter4 version required (included). + * - 'max' => #.#: The maximum Codeigniter4 version required (included). + * - 'skipIfNoEnv': Skip and warn the user if `.env` file is inexistent or empty. + * - 'failIfNoEnv': Fail the command if `.env` file is inexistent or empty. + * - 'showOutput': Show the output of the command if given. + * + * @param string $command The spark command (with cli options if any). + * @param array $options The options that define the behavior of the command. + * @return callable A function that can be used as a task. + */ +function spark($command, $options = []) +{ + return function () use ($command, $options) { + + // Ensure the spark command is available on the current version. + $versionTooEarly = array_key_exists('min', $options) + && codeigniter4_version_compare($options['min'], '<'); + + $versionTooLate = array_key_exists('max', $options) + && codeigniter4_version_compare($options['max'], '>'); + + if ($versionTooEarly || $versionTooLate) { + return; + } + + // Ensure we warn or fail when a command relies on the ".env" file. + if (in_array('failIfNoEnv', $options) && !test('[ -s {{release_or_current_path}}/.env ]')) { + throw new \Exception('Your .env file is empty! Cannot proceed.'); + } + + if (in_array('skipIfNoEnv', $options) && !test('[ -s {{release_or_current_path}}/.env ]')) { + warning("Your .env file is empty! Skipping..."); + return; + } + + $spark = '{{release_or_current_path}}/spark'; + + // Run the spark command. + $output = run("{{bin/php}} $spark $command"); + + // Output the results when appropriate. + if (in_array('showOutput', $options)) { + writeln("$output"); + } + }; +} + +function codeigniter4_version_compare($version, $comparator) +{ + return version_compare(get('codeigniter4_version'), $version, $comparator); +} + + +/** + * Discover & Checks + */ + +desc('Shows file cache information in the current system.'); +task('spark:cache:info', spark('cache:info', ['showOutput'])); + +desc('Check your Config values.'); +task('spark:config:check', spark('config:check', ['skipIfNoEnv', 'showOutput', 'min' => '4.5.0'])); + +desc('Retrieves the current environment, or set a new one.'); +task('spark:env', spark('env', ['skipIfNoEnv', 'showOutput'])); + +desc('Check filters for a route.'); +task('spark:filter:check', spark('filter:check', ['showOutput', 'min' => '4.3.0'])); + +desc('Find and save available phrases to translate.'); +task('spark:lang:find', spark('lang:find', ['showOutput', 'min' => '4.5.0'])); + +desc('Verifies your namespaces are setup correctly.'); +task('spark:namespaces', spark('namespaces', ['showOutput'])); + +desc('Check your php.ini values.'); +task('spark:phpini:check', spark('phpini:check', ['showOutput', 'min' => '4.5.0'])); + +desc('Displays all routes.'); +task('spark:routes', spark('routes', ['showOutput', 'min' => '4.3.0'])); + + +/** + * Actions + */ + +desc('Generates a new encryption key and writes it in an `.env` file.'); +task('spark:key:generate', spark('key:generate', ['skipIfNoEnv'])); + +desc('Optimize for production.'); +task('spark:optimize', spark('optimize', ['min' => '4.5.0'])); + +desc('Discovers and executes all predefined Publisher classes.'); +task('spark:publish', spark('publish', ['skipIfNoEnv', 'showOutput'])); + + +/* + * Database and migrations. + */ + +desc('Create a new database schema.'); +task('spark:db:create', spark('db:create', ['showOutput'])); + +desc('Runs the specified seeder to populate known data into the database.'); +task('spark:db:seed', spark('db:seed', ['skipIfNoEnv'])); + +desc('Retrieves information on the selected table.'); +task('spark:db:table', spark('db:table', ['skipIfNoEnv', 'showOutput', 'min' => '4.5.0'])); + +desc('Locates and runs all new migrations against the database.'); +task('spark:migrate', spark('migrate --all', ['skipIfNoEnv'])); + +desc('Does a rollback followed by a latest to refresh the current state of the database.'); +task('spark:migrate:refresh', spark('migrate:refresh -f --all', ['skipIfNoEnv'])); + +desc('Runs the "down" method for all migrations in the last batch.'); +task('spark:migrate:rollback', spark('migrate:rollback -f', ['skipIfNoEnv', 'showOutput'])); + +desc('Displays a list of all migrations and whether they\'ve been run or not.'); +task('spark:migrate:status', spark('migrate:status', ['skipIfNoEnv', 'showOutput'])); + + +/** + * Housekeeping + */ + +desc('Clears the current system caches.'); +task('spark:cache:clear', spark('cache:clear')); + +desc('Clears all debugbar JSON files.'); +task('spark:debugbar:clear', spark('debugbar:clear')); + +desc('Clears all log files.'); +task('spark:logs:clear', spark('logs:clear')); + + +/** + * Custom Spark Command for shield or setting packages + */ +desc('Run a custom spark command.'); +task('spark:custom', spark('', ['showOutput'])); + + + +/** + * Main deploy task. + */ +desc('Deploys your project'); +task('deploy', [ + 'deploy:prepare', + 'deploy:vendors', + 'spark:optimize', + 'spark:migrate', + 'deploy:publish', +]); diff --git a/vendor/deployer/deployer/recipe/common.php b/vendor/deployer/deployer/recipe/common.php new file mode 100644 index 0000000..be12d04 --- /dev/null +++ b/vendor/deployer/deployer/recipe/common.php @@ -0,0 +1,199 @@ + 'something', + * ]); + * ``` + * + * It is possible to override it per `run()` call. + * + * ```php + * run('echo $KEY', env: ['KEY' => 'over']); + * ``` + */ +set('env', []); + +/** + * Path to `.env` file which will be used as environment variables for each command per `run()`. + * + * ```php + * set('dotenv', '{{release_or_current_path}}/.env'); + * ``` + */ +set('dotenv', false); + +/** + * The deploy path. + * + * For example can be set for a bunch of host once as: + * ```php + * set('deploy_path', '~/{{alias}}'); + * ``` + */ +set('deploy_path', function () { + throw new ConfigurationException('Please, specify `deploy_path`.'); +}); + +/** + * Return current release path. Default to {{deploy_path}}/`current`. + * ```php + * set('current_path', '/var/public_html'); + * ``` + */ +set('current_path', '{{deploy_path}}/current'); + +// Path to the `php` bin. +set('bin/php', function () { + if (currentHost()->hasOwn('php_version')) { + return '/usr/bin/php{{php_version}}'; + } + return which('php'); +}); + +// Path to the `git` bin. +set('bin/git', function () { + return which('git'); +}); + +// Should {{bin/symlink}} use `--relative` option or not. Will detect +// automatically. +set('use_relative_symlink', function () { + return commandSupportsOption('ln', '--relative'); +}); + +// Path to the `ln` bin. With predefined options `-nfs`. +set('bin/symlink', function () { + return get('use_relative_symlink') ? 'ln -nfs --relative' : 'ln -nfs'; +}); + +// Path to a file which will store temp script with sudo password. +// Defaults to `.dep/sudo_pass`. This script is only temporary and will be deleted after +// sudo command executed. +set('sudo_askpass', function () { + if (test('[ -d {{deploy_path}}/.dep ]')) { + return '{{deploy_path}}/.dep/sudo_pass'; + } else { + return '/tmp/dep_sudo_pass'; + } +}); + +desc('Prepares a new release'); +task('deploy:prepare', [ + 'deploy:info', + 'deploy:setup', + 'deploy:lock', + 'deploy:release', + 'deploy:update_code', + 'deploy:env', + 'deploy:shared', + 'deploy:writable', +]); + +desc('Publishes the release'); +task('deploy:publish', [ + 'deploy:symlink', + 'deploy:unlock', + 'deploy:cleanup', + 'deploy:success', +]); + +desc('Deploys your project'); +task('deploy', [ + 'deploy:prepare', + 'deploy:publish', +]); + + +/** + * Prints success message + */ +task('deploy:success', function () { + info('successfully deployed!'); +}) + ->hidden(); + + +/** + * Hook on deploy failure. + */ +task('deploy:failed', function () {}) + ->hidden(); + +fail('deploy', 'deploy:failed'); + +/** + * Follows latest application logs. + */ +desc('Shows application logs'); +task('logs:app', function () { + if (!has('log_files')) { + warning("Please, specify \"log_files\" option."); + return; + } + cd('{{current_path}}'); + run('tail -f {{log_files}}'); +})->verbose(); diff --git a/vendor/deployer/deployer/recipe/composer.php b/vendor/deployer/deployer/recipe/composer.php new file mode 100644 index 0000000..5ad96e3 --- /dev/null +++ b/vendor/deployer/deployer/recipe/composer.php @@ -0,0 +1,14 @@ + contao-manager/login.lock'); +}); + +desc('Enable maintenance mode'); +task('contao:maintenance:enable', function () { + // Enable maintenance mode in both the current and release build, so that the maintenance mode will be enabled + // for the current installation before the symlink changes and the new installation after the symlink changed. + foreach (array_unique([parse('{{current_path}}'), parse('{{release_or_current_path}}')]) as $path) { + // The current path might not be present during first deploy. + if (!test("[ -d $path ]")) { + continue; + } + + cd($path); + run('{{bin/console}} contao:maintenance-mode enable {{console_options}}'); + } +}); + +desc('Disable maintenance mode'); +task('contao:maintenance:disable', function () { + foreach (array_unique([parse('{{current_path}}'), parse('{{release_or_current_path}}')]) as $path) { + if (!test("[ -d $path ]")) { + continue; + } + + cd($path); + run('{{bin/console}} contao:maintenance-mode disable {{console_options}}'); + } +}); + +desc('Deploy the project'); +task('deploy', [ + 'deploy:prepare', + 'deploy:vendors', + 'contao:maintenance:enable', + 'contao:migrate', + 'contao:maintenance:disable', + 'deploy:publish', +]); diff --git a/vendor/deployer/deployer/recipe/craftcms.php b/vendor/deployer/deployer/recipe/craftcms.php new file mode 100644 index 0000000..03fbbd0 --- /dev/null +++ b/vendor/deployer/deployer/recipe/craftcms.php @@ -0,0 +1,136 @@ +$output"); + } + }; +} + +/* + * Migrations + */ + +desc('Runs all pending Craft, plugin, and content migrations'); +task('craft:migrate/all', craft('migrate/all')); + +desc('Upgrades Craft by applying new migrations'); +task('craft:migrate/up', craft('migrate/up')); + +/* + * Generate keys + */ + +desc('Generates a new application ID and saves it in the `.env` file'); +task('craft:setup/app-id', craft('setup/app-id')); + +desc('Generates a new security key and saves it in the `.env` file'); +task('craft:setup/security-key', craft('setup/security-key')); + +/* + * Project config + */ + +desc('Applies project config file changes.'); +task('craft:project-config/apply', craft('project-config/apply')); + +/* + * Caches + */ + +desc('Flushes all caches registered in the system'); +task('craft:cache/flush-all', craft('cache/flush-all')); + +desc('Clear all caches'); +task('craft:clear-caches/all', craft('clear-caches/all')); + +desc('Clear all Asset caches'); +task('craft:clear-caches/asset', craft('clear-caches/asset')); + +desc('Clear all Asset indexing data'); +task('craft:clear-caches/asset-indexing-data', craft('clear-caches/asset-indexing-data')); + +desc('Clear all compiled classes'); +task('craft:clear-caches/compiled-classes', craft('clear-caches/compiled-classes')); + +desc('Clear all compiled templates'); +task('craft:clear-caches/compiled-templates', craft('clear-caches/compiled-templates')); + +desc('Clear all control panel resources'); +task('craft:clear-caches/cp-resources', craft('clear-caches/cp-resources')); + +desc('Clear all data caches'); +task('craft:clear-caches/data', craft('clear-caches/data')); + +desc('Clear all temp files'); +task('craft:clear-caches/temp-files', craft('clear-caches/temp-files')); + +/* + * Garbage collection + */ + +desc('Runs garbage collection'); +task('craft:gc', craft('gc --delete-all-trashed=1 --silent-exit-on-exception=1', ['showOutput'])); + +/* + * Main deploy + */ + +desc('Deploys Craft CMS'); +task('deploy', [ + 'deploy:prepare', + 'deploy:vendors', + 'craft:clear-caches/compiled-classes', + 'craft:migrate/all', + 'craft:project-config/apply', + 'craft:gc', + 'craft:clear-caches/all', + 'deploy:publish', +]); diff --git a/vendor/deployer/deployer/recipe/deploy/check_remote.php b/vendor/deployer/deployer/recipe/deploy/check_remote.php new file mode 100644 index 0000000..8364b6c --- /dev/null +++ b/vendor/deployer/deployer/recipe/deploy/check_remote.php @@ -0,0 +1,50 @@ +getOption('revision'); + + if (!$targetRevision) { + $ref = 'HEAD'; + $opt = ''; + if ($tag = input()->getOption('tag')) { + $ref = $tag; + $opt = '--tags'; + } elseif ($branch = get('branch')) { + $ref = $branch; + $opt = '--heads'; + } + $remoteLs = runLocally("git ls-remote $opt $repository $ref"); + if (strstr($remoteLs, "\n")) { + throw new Exception("Could not determine target revision. '$ref' matched multiple commits."); + } + if (!$remoteLs) { + throw new Exception("Could not resolve a revision from '$ref'."); + } + $targetRevision = substr($remoteLs, 0, strpos($remoteLs, "\t")); + } + + // Compare commit hashes. We use strpos to support short versions. + $targetRevision = trim($targetRevision); + $lastDeployedRevision = run('cat {{current_path}}/REVISION'); + if ($targetRevision && strpos($lastDeployedRevision, $targetRevision) === 0) { + throw new GracefulShutdownException("Already up-to-date."); + } + + info("deployed different version"); +}); diff --git a/vendor/deployer/deployer/recipe/deploy/cleanup.php b/vendor/deployer/deployer/recipe/deploy/cleanup.php new file mode 100644 index 0000000..f0f700b --- /dev/null +++ b/vendor/deployer/deployer/recipe/deploy/cleanup.php @@ -0,0 +1,21 @@ + 0) { + foreach (array_slice($releases, $keep) as $release) { + run("$sudo rm -rf {{deploy_path}}/releases/$release"); + } + } +}); diff --git a/vendor/deployer/deployer/recipe/deploy/clear_paths.php b/vendor/deployer/deployer/recipe/deploy/clear_paths.php new file mode 100644 index 0000000..9e1850b --- /dev/null +++ b/vendor/deployer/deployer/recipe/deploy/clear_paths.php @@ -0,0 +1,25 @@ +getAlias(); +}); + +desc('Displays info about deployment'); +task('deploy:info', function () { + $releaseName = test('[ -d {{deploy_path}}/.dep ]') ? get('release_name') : 1; + + info("deploying {{what}} to {{where}} (release {$releaseName})"); +}); diff --git a/vendor/deployer/deployer/recipe/deploy/lock.php b/vendor/deployer/deployer/recipe/deploy/lock.php new file mode 100644 index 0000000..bce59b1 --- /dev/null +++ b/vendor/deployer/deployer/recipe/deploy/lock.php @@ -0,0 +1,33 @@ + {{deploy_path}}/.dep/deploy.lock"); + if ($locked === '+locked') { + $lockedUser = run("cat {{deploy_path}}/.dep/deploy.lock"); + throw new GracefulShutdownException( + "Deploy locked by $lockedUser.\n" + . "Execute \"deploy:unlock\" task to unlock.", + ); + } +}); + +desc('Unlocks deploy'); +task('deploy:unlock', function () { + run("rm -f {{deploy_path}}/.dep/deploy.lock");//always success +}); + +desc('Checks if deploy is locked'); +task('deploy:is_locked', function () { + $locked = test("[ -f {{deploy_path}}/.dep/deploy.lock ]"); + if ($locked) { + $lockedUser = run("cat {{deploy_path}}/.dep/deploy.lock"); + throw new GracefulShutdownException("Deploy is locked by $lockedUser."); + } + info('Deploy is unlocked.'); +}); diff --git a/vendor/deployer/deployer/recipe/deploy/push.php b/vendor/deployer/deployer/recipe/deploy/push.php new file mode 100644 index 0000000..7c4aced --- /dev/null +++ b/vendor/deployer/deployer/recipe/deploy/push.php @@ -0,0 +1,25 @@ + false, 'options' => ['--relative']], + ); + + // Mark this release as dirty. + run("echo '{{user}}' > {{current_path}}/DIRTY_RELEASE"); +}); diff --git a/vendor/deployer/deployer/recipe/deploy/release.php b/vendor/deployer/deployer/recipe/deploy/release.php new file mode 100644 index 0000000..25c8b81 --- /dev/null +++ b/vendor/deployer/deployer/recipe/deploy/release.php @@ -0,0 +1,205 @@ + .dep/latest_release"); + } + + // Metainfo. + $timestamp = timestamp(); + $metainfo = [ + 'created_at' => $timestamp, + 'release_name' => $releaseName, + 'user' => get('user'), + 'target' => get('target'), + ]; + + // Save metainfo about release. + $json = quote(json_encode($metainfo)); + run("echo $json >> .dep/releases_log"); + + // Make new release. + run("mkdir -p $releasePath"); + run("{{bin/symlink}} $releasePath {{deploy_path}}/release"); + + // Add to releases list. + array_unshift($releasesList, $releaseName); + set('releases_list', $releasesList); + + // Set previous_release. + if (isset($releasesList[1])) { + set('previous_release', "{{deploy_path}}/releases/{$releasesList[1]}"); + } +}); + +desc('Shows releases list'); +/* + * Example output: + * ``` + * +---------------------+------example.org ------------+--------+-----------+ + * | Date (UTC) | Release | Author | Target | Commit | + * +---------------------+-------------+----------------+--------+-----------+ + * | 2021-11-06 20:51:45 | 1 | Anton Medvedev | HEAD | 34d24192e | + * | 2021-11-06 21:00:50 | 2 (bad) | Anton Medvedev | HEAD | 392948a40 | + * | 2021-11-06 23:19:20 | 3 | Anton Medvedev | HEAD | a4057a36c | + * | 2021-11-06 23:24:30 | 4 (current) | Anton Medvedev | HEAD | s3wa45ca6 | + * +---------------------+-------------+----------------+--------+-----------+ + * ``` + */ +task('releases', function () { + cd('{{deploy_path}}'); + + $releasesLog = get('releases_log'); + $currentRelease = basename(run('readlink {{current_path}}')); + $releasesList = get('releases_list'); + + $table = []; + $tz = !empty(getenv('TIMEZONE')) ? getenv('TIMEZONE') : date_default_timezone_get(); + + foreach ($releasesLog as &$metainfo) { + $date = \DateTime::createFromFormat(\DateTimeInterface::ISO8601, $metainfo['created_at']); + $date->setTimezone(new \DateTimeZone($tz)); + $status = $release = $metainfo['release_name']; + if (in_array($release, $releasesList, true)) { + if (test("[ -f releases/$release/BAD_RELEASE ]")) { + $status = "$release (bad)"; + } elseif (test("[ -f releases/$release/DIRTY_RELEASE ]")) { + $status = "$release (dirty)"; + } else { + $status = "$release"; + } + try { + $revision = run("cat releases/$release/REVISION"); + } catch (\Throwable $e) { + $revision = 'unknown'; + } + } else { + $revision = 'unknown'; + } + if ($release === $currentRelease) { + $status .= ' (current)'; + } + $table[] = [ + $date->format("Y-m-d H:i:s"), + $status, + $metainfo['user'], + $metainfo['target'], + $revision, + ]; + } + + (new Table(output())) + ->setHeaderTitle(currentHost()->getAlias()) + ->setHeaders(["Date ($tz)", 'Release', 'Author', 'Target', 'Commit']) + ->setRows($table) + ->render(); +}); diff --git a/vendor/deployer/deployer/recipe/deploy/rollback.php b/vendor/deployer/deployer/recipe/deploy/rollback.php new file mode 100644 index 0000000..7afd34d --- /dev/null +++ b/vendor/deployer/deployer/recipe/deploy/rollback.php @@ -0,0 +1,91 @@ +$currentRelease."); + + if (!test("[ -d releases/$candidate ]")) { + throw new \RuntimeException(parse("Release \"$candidate\" not found in \"{{deploy_path}}/releases\".")); + } + if (test("[ -f releases/$candidate/BAD_RELEASE ]")) { + writeln("Candidate $candidate marked as bad release."); + if (!askConfirmation("Continue rollback to $candidate?")) { + writeln('Rollback aborted.'); + return; + } + } + writeln("Rolling back to $candidate release."); + + // Symlink to old release. + run("{{bin/symlink}} releases/$candidate {{current_path}}"); + + // Mark release as bad. + $timestamp = timestamp(); + run("echo '$timestamp,{{user}}' > releases/$currentRelease/BAD_RELEASE"); + + writeln("rollback to release $candidate was successful"); +}); diff --git a/vendor/deployer/deployer/recipe/deploy/setup.php b/vendor/deployer/deployer/recipe/deploy/setup.php new file mode 100644 index 0000000..5a29dd4 --- /dev/null +++ b/vendor/deployer/deployer/recipe/deploy/setup.php @@ -0,0 +1,22 @@ +getVerbosity() === OutputInterface::VERBOSITY_DEBUG ? 'v' : ''; + + foreach (get('shared_dirs') as $dir) { + // Make sure all path without tailing slash. + $dir = trim($dir, '/'); + + // Check if shared dir does not exist. + if (!test("[ -d $sharedPath/$dir ]")) { + // Create shared dir if it does not exist. + run("mkdir -p $sharedPath/$dir"); + // If release contains shared dir, copy that dir from release to shared. + if (test("[ -d $(echo {{release_path}}/$dir) ]")) { + run("cp -r$copyVerbosity {{release_path}}/$dir $sharedPath/" . dirname($dir)); + } + } + + // Remove from source. + run("rm -rf {{release_path}}/$dir"); + + // Create path to shared dir in release dir if it does not exist. + // Symlink will not create the path and will fail otherwise. + run("mkdir -p `dirname {{release_path}}/$dir`"); + + // Symlink shared dir to release dir + run("{{bin/symlink}} $sharedPath/$dir {{release_path}}/$dir"); + } + + foreach (get('shared_files') as $file) { + $dirname = dirname(parse($file)); + + // Create dir of shared file if not existing + if (!test("[ -d $sharedPath/$dirname ]")) { + run("mkdir -p $sharedPath/$dirname"); + } + + // Check if shared file does not exist in shared. + // and file exist in release + if (!test("[ -f $sharedPath/$file ]") && test("[ -f {{release_path}}/$file ]")) { + // Copy file in shared dir if not present + run("cp -r$copyVerbosity {{release_path}}/$file $sharedPath/$file"); + } + + // Remove from source. + run("if [ -f $(echo {{release_path}}/$file) ]; then rm -rf {{release_path}}/$file; fi"); + + // Ensure dir is available in release + run("if [ ! -d $(echo {{release_path}}/$dirname) ]; then mkdir -p {{release_path}}/$dirname;fi"); + + // Touch shared + run("[ -f $sharedPath/$file ] || touch $sharedPath/$file"); + + // Symlink shared dir to release dir + run("{{bin/symlink}} $sharedPath/$file {{release_path}}/$file"); + } +}); diff --git a/vendor/deployer/deployer/recipe/deploy/symlink.php b/vendor/deployer/deployer/recipe/deploy/symlink.php new file mode 100644 index 0000000..8b9c2c0 --- /dev/null +++ b/vendor/deployer/deployer/recipe/deploy/symlink.php @@ -0,0 +1,21 @@ +hasOption('branch') && !empty(input()->getOption('branch'))) { + $target = input()->getOption('branch'); + } + if (input()->hasOption('tag') && !empty(input()->getOption('tag'))) { + $target = input()->getOption('tag'); + } + if (input()->hasOption('revision') && !empty(input()->getOption('revision'))) { + $target = input()->getOption('revision'); + } + + if (empty($target)) { + $target = "HEAD"; + } + return $target; +}); + +// Sets deploy:update_code strategy. +// Can be one of: +// - local_archive (copies the repository from local machine) +// - archive (default, fetches the code from the remote repository) +// - clone (if you need the origin repository `.git` dir in your {{release_path}}, clones from remote repository) +set('update_code_strategy', 'archive'); + +// Sets environment variable _GIT_SSH_COMMAND_ for `git clone` command. +// If `StrictHostKeyChecking` flag is set to `accept-new` then ssh will +// automatically add new host keys to the user known hosts files, but +// will not permit connections to hosts with changed host keys. +set('git_ssh_command', 'ssh -o StrictHostKeyChecking=accept-new'); + +/** + * Specifies a sub directory within the repository to deploy. + * Works only when [`update_code_strategy`](#update_code_strategy) is set to `archive` (default) or `local_archive`. + * + * Example: + * - set value to `src` if you want to deploy the folder that lives at `/src`. + * - set value to `src/api` if you want to deploy the folder that lives at `/src/api`. + * + * Note: do not use a leading `/`! + */ +set('sub_directory', false); + +/** + * Update code at {{release_path}} on host. + */ +desc('Updates code'); +task('deploy:update_code', function () { + $strategy = get('update_code_strategy'); + $target = get('target'); + + $targetWithDir = $target; + if (!empty(get('sub_directory'))) { + $targetWithDir .= ':{{sub_directory}}'; + } + + if ($strategy === 'local_archive') { + $gitRoot = runLocally("git rev-parse --show-toplevel"); + runLocally("git -C " . quote($gitRoot) . " archive $targetWithDir -o archive.tar"); + upload("$gitRoot/archive.tar", '{{release_path}}/archive.tar'); + run("tar -xf {{release_path}}/archive.tar -C {{release_path}}"); + run("rm {{release_path}}/archive.tar"); + unlink("$gitRoot/archive.tar"); + + $rev = quote(runLocally("git rev-list $target -1")); + } else { + $git = get('bin/git'); + $repository = get('repository'); + + if (empty($repository)) { + throw new ConfigurationException("Missing 'repository' configuration."); + } + + $bare = parse('{{deploy_path}}/.dep/repo'); + $env = [ + 'GIT_TERMINAL_PROMPT' => '0', + 'GIT_SSH_COMMAND' => get('git_ssh_command'), + ]; + + start: + // Clone the repository to a bare repo. + run("[ -d $bare ] || mkdir -p $bare"); + run("[ -f $bare/HEAD ] || $git clone --mirror $repository $bare 2>&1", env: $env); + + cd($bare); + + // If remote url changed, drop `.dep/repo` and reinstall. + if (run("$git config --get remote.origin.url") !== $repository) { + cd('{{deploy_path}}'); + run("rm -rf $bare"); + goto start; + } + + run("$git remote update 2>&1", env: $env); + + // Copy to release_path. + if ($strategy === 'archive') { + run("$git archive $targetWithDir | tar -x -f - -C {{release_path}} 2>&1"); + } elseif ($strategy === 'clone') { + cd('{{release_path}}'); + run("$git clone -l $bare ."); + run("$git remote set-url origin $repository", env: $env); + run("$git checkout --force $target"); + } else { + throw new ConfigurationException(parse("Unknown `update_code_strategy` option: {{update_code_strategy}}.")); + } + + $rev = quote(run("$git rev-list $target -1")); + } + + // Save git revision in REVISION file. + run("echo $rev > {{release_path}}/REVISION"); +}); diff --git a/vendor/deployer/deployer/recipe/deploy/vendors.php b/vendor/deployer/deployer/recipe/deploy/vendors.php new file mode 100644 index 0000000..e42cb81 --- /dev/null +++ b/vendor/deployer/deployer/recipe/deploy/vendors.php @@ -0,0 +1,37 @@ +&1'); +}); diff --git a/vendor/deployer/deployer/recipe/deploy/writable.php b/vendor/deployer/deployer/recipe/deploy/writable.php new file mode 100644 index 0000000..63c110d --- /dev/null +++ b/vendor/deployer/deployer/recipe/deploy/writable.php @@ -0,0 +1,164 @@ + $group) { + if ($index > 0) { + $setFaclGroups .= " "; + } + $setFaclGroups .= "-m g:\"$group\":rwX"; + } + // Check if remote user exists, before adding it to setfacl + $remoteUserExists = test("id -u $remoteUser &>/dev/null 2>&1 || exit 0"); + if ($remoteUserExists === true) { + $setFaclUsers .= " -m u:$remoteUser:rwX"; + } + if (empty($sudo)) { + // When running without sudo, exception may be thrown + // if executing setfacl on files created by http user (in directory that has been setfacl before). + // These directories/files should be skipped unless forcing ACL reset. + // Now, we will check each directory for ACL and only setfacl for which has not been set before, + // unless writable_acl_force is enabled. + $writeableDirs = get('writable_dirs'); + $forceAcl = get('writable_acl_force'); + foreach ($writeableDirs as $dir) { + // Check if ACL has been set or not + $hasfacl = run("getfacl -p $dir | grep \"^user:$httpUser:.*w\" | wc -l"); + // Set ACL for directory if it has not been set before or if forcing ACL reset + if ($forceAcl || !$hasfacl) { + run("setfacl -L $recursive $setFaclUsers $setFaclGroups $dir"); + run("setfacl -dL $recursive $setFaclUsers $setFaclGroups $dir"); + } + } + } else { + run("$sudo setfacl -L $recursive $setFaclUsers $setFaclGroups $dirs"); + run("$sudo setfacl -dL $recursive $setFaclUsers $setFaclGroups $dirs"); + } + } else { + $alias = currentHost()->getAlias(); + throw new \RuntimeException("Can't set writable dirs with ACL.\nInstall ACL with next command:\ndep run 'sudo apt-get install acl' -- $alias"); + } + } elseif ($mode === 'sticky') { + // Changes the group of the files, sets sticky bit to the directories + // and add the writable bit for all files + run("for dir in $dirs;" + . 'do ' + . 'chgrp -L -R {{http_group}} ${dir}; ' + . 'find ${dir} -type d -exec chmod g+rwxs \{\} \;;' + . 'find ${dir} -type f -exec chmod g+rw \{\} \;;' + . 'done'); + } elseif ($mode === 'skip') { + // Does nothing, saves time if no changes are required for some environments + return; + } else { + throw new \RuntimeException("Unknown writable_mode `$mode`."); + } +}); diff --git a/vendor/deployer/deployer/recipe/drupal7.php b/vendor/deployer/deployer/recipe/drupal7.php new file mode 100644 index 0000000..59be350 --- /dev/null +++ b/vendor/deployer/deployer/recipe/drupal7.php @@ -0,0 +1,80 @@ + $value) { + $keys = []; + for ($i = $iterator->getDepth(); $i > 0; $i--) { + $keys[] = $iterator->getSubIterator($i - 1)->key(); + } + $keys[] = $key; + + $replacements['{{' . implode('.', $keys) . '}}'] = $value; + } + + //Create settings from template + $settings = file_get_contents($template); + + $settings = strtr($settings, $replacements); + + writeln('settings.php created successfully'); + + $tmpFilename = tempnam(sys_get_temp_dir(), 'tmp_settings_'); + file_put_contents($tmpFilename, $settings); + + upload($tmpFilename, '{{deploy_path}}/shared/sites/{{drupal_site}}/settings.php'); + + unlink($tmpFilename); + } +}); + +//Upload Drupal 7 files folder +task('drupal:upload_files', function () { + if (askConfirmation('Are you sure?')) { + upload('sites/{{drupal_site}}/files', '{{deploy_path}}/shared/sites/{{drupal_site}}/files'); + } +}); diff --git a/vendor/deployer/deployer/recipe/drupal8.php b/vendor/deployer/deployer/recipe/drupal8.php new file mode 100644 index 0000000..151fd6c --- /dev/null +++ b/vendor/deployer/deployer/recipe/drupal8.php @@ -0,0 +1,32 @@ + #.#: The minimum Laravel version required (included). + * - 'max' => #.#: The maximum Laravel version required (included). + * - 'skipIfNoEnv': Skip and warn the user if `.env` file is inexistant or empty. + * - 'failIfNoEnv': Fail the command if `.env` file is inexistant or empty. + * - 'showOutput': Show the output of the command if given. + * + * @param string $command The artisan command (with cli options if any). + * @param array $options The options that define the behaviour of the command. + * @return callable A function that can be used as a task. + */ +function artisan($command, $options = []) +{ + return function () use ($command, $options) { + + // Ensure the artisan command is available on the current version. + $versionTooEarly = array_key_exists('min', $options) + && laravel_version_compare($options['min'], '<'); + + $versionTooLate = array_key_exists('max', $options) + && laravel_version_compare($options['max'], '>'); + + if ($versionTooEarly || $versionTooLate) { + return; + } + + // Get the dotenv path or use default. + $dotenv = get('dotenv', '{{release_or_current_path}}/.env'); + + // Ensure we warn or fail when a command relies on the ".env" file. + if (in_array('failIfNoEnv', $options) && !test("[ -s $dotenv ]")) { + throw new \Exception('Your .env file is empty! Cannot proceed.'); + } + + if (in_array('skipIfNoEnv', $options) && !test("[ -s $dotenv ]")) { + warning("Your .env file is empty! Skipping..."); + return; + } + + // Run the artisan command. + $output = run("{{bin/php}} {{bin/artisan}} $command"); + + // Output the results when appropriate. + if (in_array('showOutput', $options)) { + writeln("$output"); + } + }; +} + +function laravel_version_compare($version, $comparator) +{ + return version_compare(get('laravel_version'), $version, $comparator); +} + +/* + * Maintenance mode. + */ + +desc('Puts the application into maintenance / demo mode'); +task('artisan:down', artisan('down', ['showOutput'])); + +desc('Brings the application out of maintenance mode'); +task('artisan:up', artisan('up', ['showOutput'])); + +/* + * Generate keys. + */ + +desc('Sets the application key'); +task('artisan:key:generate', artisan('key:generate')); + +desc('Creates the encryption keys for API authentication'); +task('artisan:passport:keys', artisan('passport:keys')); + +/* + * Database and migrations. + */ + +desc('Seeds the database with records'); +task('artisan:db:seed', artisan('db:seed --force', ['skipIfNoEnv', 'showOutput'])); + +desc('Runs the database migrations'); +task('artisan:migrate', artisan('migrate --force', ['skipIfNoEnv'])); + +desc('Drops all tables and re-run all migrations'); +task('artisan:migrate:fresh', artisan('migrate:fresh --force', ['skipIfNoEnv'])); + +desc('Rollbacks the last database migration'); +task('artisan:migrate:rollback', artisan('migrate:rollback --force', ['skipIfNoEnv', 'showOutput'])); + +desc('Shows the status of each migration'); +task('artisan:migrate:status', artisan('migrate:status', ['skipIfNoEnv', 'showOutput'])); + +/* + * Cache and optimizations. + */ + +desc('Flushes the application cache'); +task('artisan:cache:clear', artisan('cache:clear')); + +desc('Creates a cache file for faster configuration loading'); +task('artisan:config:cache', artisan('config:cache')); + +desc('Removes the configuration cache file'); +task('artisan:config:clear', artisan('config:clear')); + +desc('Discovers and cache the application\'s events and listeners'); +task('artisan:event:cache', artisan('event:cache', ['min' => '5.8.9'])); + +desc('Clears all cached events and listeners'); +task('artisan:event:clear', artisan('event:clear', ['min' => '5.8.9'])); + +desc('Lists the application\'s events and listeners'); +task('artisan:event:list', artisan('event:list', ['showOutput', 'min' => '5.8.9'])); + +desc('Cache the framework bootstrap files'); +task('artisan:optimize', artisan('optimize')); + +desc('Removes the cached bootstrap files'); +task('artisan:optimize:clear', artisan('optimize:clear')); + +desc('Reload running services'); +task('artisan:reload', artisan('reload', ['min' => '12.41'])); + +desc('Creates a route cache file for faster route registration'); +task('artisan:route:cache', artisan('route:cache')); + +desc('Removes the route cache file'); +task('artisan:route:clear', artisan('route:clear')); + +desc('Lists all registered routes'); +task('artisan:route:list', artisan('route:list', ['showOutput'])); + +desc('Creates the symbolic links configured for the application'); +task('artisan:storage:link', artisan('storage:link', ['min' => 5.3])); + +desc('Compiles all of the application\'s Blade templates'); +task('artisan:view:cache', artisan('view:cache', ['min' => 5.6])); + +desc('Clears all compiled view files'); +task('artisan:view:clear', artisan('view:clear')); + +/** + * Queue and Horizon. + */ + +desc('Lists all of the failed queue jobs'); +task('artisan:queue:failed', artisan('queue:failed', ['showOutput'])); + +desc('Flushes all of the failed queue jobs'); +task('artisan:queue:flush', artisan('queue:flush')); + +desc('Restarts queue worker daemons after their current job'); +task('artisan:queue:restart', artisan('queue:restart')); + +desc('Starts a master supervisor in the foreground'); +task('artisan:horizon', artisan('horizon')); + +desc('Deletes all of the jobs from the specified queue'); +task('artisan:horizon:clear', artisan('horizon:clear --force')); + +desc('Instructs the master supervisor to continue processing jobs'); +task('artisan:horizon:continue', artisan('horizon:continue')); + +desc('Lists all of the deployed machines'); +task('artisan:horizon:list', artisan('horizon:list', ['showOutput'])); + +desc('Pauses the master supervisor'); +task('artisan:horizon:pause', artisan('horizon:pause')); + +desc('Terminates any rogue Horizon processes'); +task('artisan:horizon:purge', artisan('horizon:purge')); + +desc('Gets the current status of Horizon'); +task('artisan:horizon:status', artisan('horizon:status', ['showOutput'])); + +desc('Terminates the master supervisor so it can be restarted'); +task('artisan:horizon:terminate', artisan('horizon:terminate')); + +desc('Publish all of the Horizon resources'); +task('artisan:horizon:publish', artisan('horizon:publish')); + +desc('Lists all of the supervisors'); +task('artisan:horizon:supervisors', artisan('horizon:supervisors', ['showOutput'])); + +desc('Deletes metrics for all jobs and queues'); +task('artisan:horizon:clear-metrics', artisan('horizon:clear-metrics')); + +desc('Stores a snapshot of the queue metrics'); +task('artisan:horizon:snapshot', artisan('horizon:snapshot')); + +/* + * Scheduler. + */ + +desc('Interrupt in-progress schedule:run invocations'); +task('artisan:schedule:interrupt', artisan('schedule:interrupt')); + +/* + * Telescope. + */ + +desc('Clears all entries from Telescope'); +task('artisan:telescope:clear', artisan('telescope:clear')); + +desc('Prunes stale entries from the Telescope database'); +task('artisan:telescope:prune', artisan('telescope:prune')); + +/* + * Octane. + */ + +desc('Starts the octane server'); +task('artisan:octane', artisan('octane:start')); + +desc('Reloads the octane server'); +task('artisan:octane:reload', artisan('octane:reload')); + +desc('Stops the octane server'); +task('artisan:octane:stop', artisan('octane:stop')); + +desc('Check the status of the octane server'); +task('artisan:octane:status', artisan('octane:status')); + +/* + * Nova. + */ + +desc('Publish all of the Laravel Nova resources'); +task('artisan:nova:publish', artisan('nova:publish')); + +/* + * Reverb. + */ + +desc('Starts the Reverb server'); +task('artisan:reverb:start', artisan('reverb:start')); + +desc('Restarts the Reverb server'); +task('artisan:reverb:restart', artisan('reverb:restart')); + +/* + * Pulse. + */ + +desc('Starts the Pulse server'); +task('artisan:pulse:check', artisan('pulse:check')); + +desc('Restarts the Pulse server'); +task('artisan:pulse:restart', artisan('pulse:restart')); + +desc('Purges all Pulse data from storage'); +task('artisan:pulse:purge', artisan('pulse:purge')); + +desc('Process incoming Pulse data from the ingest stream'); +task('artisan:pulse:work', artisan('pulse:work')); + +/** + * Main deploy task. + */ +desc('Deploys your project'); +task('deploy', [ + 'deploy:prepare', + 'deploy:vendors', + 'artisan:storage:link', + 'artisan:optimize', + 'artisan:migrate', + 'deploy:publish', + 'artisan:reload', +]); diff --git a/vendor/deployer/deployer/recipe/magento.php b/vendor/deployer/deployer/recipe/magento.php new file mode 100644 index 0000000..09d8be0 --- /dev/null +++ b/vendor/deployer/deployer/recipe/magento.php @@ -0,0 +1,51 @@ +cleanCache();\""); +}); + +/** + * Remove files that can be used to compromise Magento + */ +task('deploy:clear_version', function () { + run("rm -f {{release_or_current_path}}/LICENSE.html"); + run("rm -f {{release_or_current_path}}/LICENSE.txt"); + run("rm -f {{release_or_current_path}}/LICENSE_AFL.txt"); + run("rm -f {{release_or_current_path}}/RELEASE_NOTES.txt"); +})->hidden(); + +after('deploy:update_code', 'deploy:clear_version'); + + +/** + * Main task + */ +desc('Deploys your project'); +task('deploy', [ + 'deploy:prepare', + 'deploy:cache:clear', + 'deploy:publish', +]); diff --git a/vendor/deployer/deployer/recipe/magento2.php b/vendor/deployer/deployer/recipe/magento2.php new file mode 100644 index 0000000..895ccca --- /dev/null +++ b/vendor/deployer/deployer/recipe/magento2.php @@ -0,0 +1,651 @@ + null, - Will compile all languages from {{static_content_locales}} for Magento/luma +// 'Custom/theme' => 'en_US fr_FR' - Will compile only en_US and fr_FR for Custom/theme +// 'Custom/another' => '{{static_content_locales}} it_IT' - Will compile all languages from {{static_content_locales}} + it_IT for Custom/another +// ]); - Will compile this theme with every language +set('magento_themes', [ + +]); + +// Static content deployment options, e.g. '--no-parent' +set('static_deploy_options', ''); + +// Deploy frontend and adminhtml together as default +set('split_static_deployment', false); + +// Use the default languages for the backend as default +set('static_content_locales_backend', '{{static_content_locales}}'); + +// backend themes to deploy. Only used if split_static_deployment=true +// This setting supports the same options/structure as {{magento_themes}} +set('magento_themes_backend', ['Magento/backend' => null]); + +// Configuration + +// Also set the number of concurrent jobs to run. The default is 1 +// Update using: `set('static_content_jobs', '1');` +set('static_content_jobs', '1'); + +set('content_version', function () { + return time(); +}); + +// Magento directory relative to repository root. Use "." (default) if it is not located in a subdirectory +set('magento_dir', '.'); + + +set('shared_files', [ + '{{magento_dir}}/app/etc/env.php', + '{{magento_dir}}/var/.maintenance.ip', +]); +set('shared_dirs', [ + '{{magento_dir}}/var/composer_home', + '{{magento_dir}}/var/log', + '{{magento_dir}}/var/export', + '{{magento_dir}}/var/report', + '{{magento_dir}}/var/import', + '{{magento_dir}}/var/import_history', + '{{magento_dir}}/var/session', + '{{magento_dir}}/var/importexport', + '{{magento_dir}}/var/backups', + '{{magento_dir}}/var/tmp', + '{{magento_dir}}/pub/sitemap', + '{{magento_dir}}/pub/media', + '{{magento_dir}}/pub/static/_cache', +]); +set('writable_dirs', [ + '{{magento_dir}}/var', + '{{magento_dir}}/pub/static', + '{{magento_dir}}/pub/media', + '{{magento_dir}}/generated', + '{{magento_dir}}/var/page_cache', +]); +set('clear_paths', [ + '{{magento_dir}}/generated/*', + '{{magento_dir}}/pub/static/_cache/*', + '{{magento_dir}}/var/generation/*', + '{{magento_dir}}/var/cache/*', + '{{magento_dir}}/var/page_cache/*', + '{{magento_dir}}/var/view_preprocessed/*', +]); + +// WARNING: Do not use {{bin/magento}} in deploy:failed handlers - release_or_current_path points to the failed +// release during failure, not the live one. Use {{current_path}} explicitly instead. +// See: config_import_needed_on_current, magento:config:import:on-current, magento:maintenance:enable/disable. +set('bin/magento', '{{release_or_current_path}}/{{magento_dir}}/bin/magento'); + +set('magento_version', function () { + // detect version + $versionOutput = run('{{bin/php}} {{bin/magento}} --version'); + preg_match('/(\d+\.?)+(-p\d+)?$/', $versionOutput, $matches); + return $matches[0] ?? '2.0'; +}); + +set('config_import_needed', function () { + // detect if app:config:import is needed + try { + run('{{bin/php}} {{bin/magento}} app:config:status'); + } catch (RunException $e) { + if ($e->getExitCode() == CONFIG_IMPORT_NEEDED_EXIT_CODE) { + return true; + } + + throw $e; + } + return false; +}); + +set('config_import_needed_on_current', function () { + if (!test('[ -d {{current_path}}/{{magento_dir}} ] && [ -f {{current_path}}/{{magento_dir}}/bin/magento ]')) { + writeln('Current Magento installation is unavailable => import skipped'); + return false; + } + + try { + // detect if app:config:import is needed on the current (live) release + // do not use {{bin/magento}} as it resolves via release_or_current_path which is unreliable in failure scenarios + run('{{bin/php}} {{current_path}}/{{magento_dir}}/bin/magento app:config:status'); + } catch (RunException|TimeoutException $e) { + if ($e instanceof RunException && $e->getExitCode() == CONFIG_IMPORT_NEEDED_EXIT_CODE) { + return true; + } + + // In failure scenarios, non-status errors should not break deploy:failed handlers. + writeln('Unable to determine app config status on current release => import skipped'); + return false; + } + return false; +}); + +set('database_upgrade_needed', function () { + // detect if db upgrade is needed + try { + run('{{bin/php}} {{bin/magento}} setup:db:status'); + } catch (RunException $e) { + if ($e->getExitCode() == DB_UPDATE_NEEDED_EXIT_CODE) { + return true; + } + + throw $e; + } + + return false; +}); + +set('full_upgrade_needed', function () { + //Some conditions, such as new RabittMQ services require a full upgrade and are not detecet by setup:db:status + //TODO: Add checks, once implemented, for detecting necessary full upgrade process. See future RabbitMQ Check: https://github.com/magento/magento2/pull/39698 + return false; +}); + +set('upgrade_needed', function () { + // Detect necessary upgrade, partial db or full upgrade + try { + return get('database_upgrade_needed') || get('full_upgrade_needed'); + } catch (RunException $e) { + throw $e; + } +}); + +// Deploy without setting maintenance mode if possible +set('enable_zerodowntime', true); + +// Tasks + +// To work correctly with artifact deployment, it is necessary to set the MAGE_MODE correctly in `app/etc/config.php` +// e.g. +// ```php +// 'MAGE_MODE' => 'production' +// ``` +desc('Compiles magento di'); +task('magento:compile', function () { + run("{{bin/php}} {{bin/magento}} setup:di:compile"); + run('cd {{release_or_current_path}}/{{magento_dir}} && {{bin/composer}} dump-autoload -o'); +}); + +// To work correctly with artifact deployment it is necessary to set `system/dev/js` , `system/dev/css` and `system/dev/template` +// in `app/etc/config.php`, e.g.: +// ```php +// 'system' => [ +// 'default' => [ +// 'dev' => [ +// 'js' => [ +// 'merge_files' => '1', +// 'minify_files' => '1' +// ], +// 'css' => [ +// 'merge_files' => '1', +// 'minify_files' => '1' +// ], +// 'template' => [ +// 'minify_html' => '1' +// ] +// ] +// ] +// ``` +desc('Deploys assets'); +task('magento:deploy:assets', function () { + $themesToCompile = ''; + if (get('split_static_deployment')) { + invoke('magento:deploy:assets:adminhtml'); + invoke('magento:deploy:assets:frontend'); + } else { + if (count(get('magento_themes')) > 0) { + $themes = array_is_list(get('magento_themes')) ? get('magento_themes') : array_keys(get('magento_themes')); + foreach ($themes as $theme) { + $themesToCompile .= ' -t ' . $theme; + } + } + run("{{bin/php}} {{bin/magento}} setup:static-content:deploy -f --content-version={{content_version}} {{static_deploy_options}} {{static_content_locales}} $themesToCompile -j {{static_content_jobs}}"); + } +}); + +desc('Deploys assets for backend only'); +task('magento:deploy:assets:adminhtml', function () { + magentoDeployAssetsSplit('backend'); +}); + +desc('Deploys assets for frontend only'); +task('magento:deploy:assets:frontend', function () { + magentoDeployAssetsSplit('frontend'); +}); + +/** + * @phpstan-param 'frontend'|'backend' $area + * + * @throws ConfigurationException + */ +function magentoDeployAssetsSplit(string $area) +{ + if (!in_array($area, ['frontend', 'backend'], true)) { + throw new ConfigurationException("\$area must be either 'frontend' or 'backend', '$area' given"); + } + + $maxProcesses = get('static_content_jobs'); + + $isFrontend = $area === 'frontend'; + $suffix = $isFrontend + ? '' + : '_backend'; + + $themesConfig = get("magento_themes$suffix"); + $defaultLanguages = get("static_content_locales$suffix"); + $useDefaultLanguages = array_is_list($themesConfig); + + /** @var list $themes */ + $themes = $useDefaultLanguages + ? $themesConfig + : array_keys($themesConfig); + + $staticContentArea = $isFrontend + ? 'frontend' + : 'adminhtml'; + + // group themes by their languages to minimize number of static content deploy commands, and allow parallel jobs + $themeGroups = []; + if ($useDefaultLanguages) { + $themeGroups[$defaultLanguages] = $themes; + } else { + foreach ($themes as $theme) { + $locales = array_unique(array_filter(explode(' ', parse($themesConfig[$theme] ?? $defaultLanguages)))); + sort($locales); // sort locales to ensure consistent grouping + $localesSorted = implode(' ', $locales); + $themeGroups[$localesSorted][] = $theme; + } + } + + foreach ($themeGroups as $locales => $themes) { + $localeCount = substr_count($locales, ' ') + 1; + // how many themes can we process in parallel? + $maxConcurrentThemes = max(1, floor($maxProcesses / $localeCount)); + // WARNING: when static_content_jobs>1, and it's doing more than 1 theme-locale per process, it can get stuck - so we do 1batch at a time + do { + $chunk = array_splice($themes, 0, $maxConcurrentThemes); + $themeArgs = '-t ' . implode(' -t ', $chunk); + run("{{bin/php}} {{bin/magento}} setup:static-content:deploy -f --area=$staticContentArea --content-version={{content_version}} {{static_deploy_options}} $locales $themeArgs -j {{static_content_jobs}}"); + } while (!empty($themes)); + } + +} + +desc('Syncs content version'); +task('magento:sync:content_version', function () { + $timestamp = time(); + on(select('all'), function (Host $host) use ($timestamp) { + $host->set('content_version', $timestamp); + }); +})->once(); + +before('magento:deploy:assets', 'magento:sync:content_version'); + +desc('Enables maintenance mode'); +task('magento:maintenance:enable', function () { + // do not use {{bin/magento}} because it would be in "release" but the maintenance mode must be set in "current" + run("if [ -d $(echo {{current_path}}) ]; then {{bin/php}} {{current_path}}/{{magento_dir}}/bin/magento maintenance:enable; fi"); +}); + +desc('Disables maintenance mode'); +task('magento:maintenance:disable', function () { + // do not use {{bin/magento}} because it would be in "release" but the maintenance mode must be set in "current" + run("if [ -d $(echo {{current_path}}) ]; then {{bin/php}} {{current_path}}/{{magento_dir}}/bin/magento maintenance:disable; fi"); +}); + +desc('Set maintenance mode if needed'); +task('magento:maintenance:enable-if-needed', function () { + ! get('enable_zerodowntime') || get('upgrade_needed') || get('config_import_needed') + ? invoke('magento:maintenance:enable') + : writeln('Config and database up to date => no maintenance mode'); +}); + +desc('Config Import'); +task('magento:config:import', function () { + if (get('config_import_needed')) { + run('{{bin/php}} {{bin/magento}} app:config:import --no-interaction'); + } else { + writeln('App config is up to date => import skipped'); + } +}); + +desc('Config Import on current release'); +task('magento:config:import:on-current', function () { + try { + if (get('config_import_needed_on_current')) { + // do not use {{bin/magento}} as it must run on the current (last successful) release in failure scenarios + run('{{bin/php}} {{current_path}}/{{magento_dir}}/bin/magento app:config:import --no-interaction'); + } else { + writeln('App config import skipped'); + } + } catch (RunException|TimeoutException $e) { + writeln('Unable to import app config on current release => import skipped'); + } +}); + +desc('Upgrades magento database'); +task('magento:upgrade:db', function () { + if (get('database_upgrade_needed')) { + // clear config cache, so there is no error when a new MQ topic is introduced + run("{{bin/php}} {{bin/magento}} cache:clean config"); + run("{{bin/php}} {{bin/magento}} setup:db-schema:upgrade --no-interaction"); + run("{{bin/php}} {{bin/magento}} setup:db-data:upgrade --no-interaction"); + } else { + writeln('Database schema is up to date => upgrade skipped'); + } +}); + +desc('Run upgrades if needed'); +task('magento:upgrade', function () { + if (get('full_upgrade_needed')) { + run("{{bin/php}} {{bin/magento}} setup:upgrade --no-interaction --keep-generated"); + } elseif (get('database_upgrade_needed')) { + invoke('magento:upgrade:db'); + } +})->once(); + +desc('Flushes Magento Cache'); +task('magento:cache:flush', function () { + run("{{bin/php}} {{bin/magento}} cache:flush"); +}); + +desc('Magento2 deployment operations'); +task('deploy:magento', [ + 'magento:build', + 'magento:maintenance:enable-if-needed', + 'magento:config:import', + 'magento:upgrade', + 'magento:maintenance:disable', +]); + +desc('Magento2 build operations'); +task('magento:build', [ + 'magento:compile', + 'magento:deploy:assets', +]); + +desc('Deploys your project'); +task('deploy', [ + 'deploy:prepare', + 'deploy:vendors', + 'deploy:clear_paths', + 'deploy:magento', + 'deploy:publish', +]); + +after('deploy:symlink', 'magento:cache:flush'); + +after('deploy:failed', 'deploy:magento:failed'); + +desc('Run Magento post-deployment failure tasks'); +task('deploy:magento:failed', [ + 'magento:config:import:on-current', + 'magento:maintenance:disable', +]); + +// Artifact deployment section + +// The file the artifact is saved to +set('artifact_file', 'artifact.tar.gz'); + +// The directory the artifact is saved in +set('artifact_dir', 'artifacts'); + +// Points to a file with a list of files to exclude from packaging. +// The format is as with the `tar --exclude-from=[file]` option +set('artifact_excludes_file', 'artifacts/excludes'); + +// If set to true, the artifact is built from a clean copy of the project repository instead of the current working directory +set('build_from_repo', false); + +// Set this value if "build_from_repo" is set to true. The target to deploy must also be set with "--branch", "--tag" or "--revision" +set('repository', null); + +// The relative path to the artifact file. If the directory does not exist, it will be created +set('artifact_path', function () { + if (!testLocally('[ -d {{artifact_dir}} ]')) { + runLocally('mkdir -p {{artifact_dir}}'); + } + return get('artifact_dir') . '/' . get('artifact_file'); +}); + +// The location of the tar command. On MacOS you should have installed gtar, as it supports the required settings +set('bin/tar', function () { + if (commandExist('gtar')) { + return which('gtar'); + } else { + return which('tar'); + } +}); + +// tasks section + +desc('Packages all relevant files in an artifact.'); +task('artifact:package', function () { + if (!test('[ -f {{artifact_excludes_file}} ]')) { + throw new GracefulShutdownException( + "No artifact excludes file provided, provide one at artifacts/excludes or change location", + ); + } + run('{{bin/tar}} --exclude-from={{artifact_excludes_file}} -czf {{artifact_path}} -C {{release_or_current_path}} .'); +}); + +desc('Uploads artifact in release folder for extraction.'); +task('artifact:upload', function () { + upload(get('artifact_path'), '{{release_path}}'); +}); + +desc('Extracts artifact in release path.'); +task('artifact:extract', function () { + run('{{bin/tar}} -xzpf {{release_path}}/{{artifact_file}} -C {{release_path}}'); + run('rm -rf {{release_path}}/{{artifact_file}}'); +}); + +desc('Clears generated files prior to building.'); +task('build:remove-generated', function () { + run('rm -rf generated/*'); +}); + +desc('Prepare local artifact build'); +task('build:prepare', function () { + if (!currentHost()->get('local')) { + throw new GracefulShutdownException('Artifact can only be built locally, you provided a non local host'); + } + + $buildDir = get('build_from_repo') ? get('artifact_dir') . '/repo' : '.'; + set('deploy_path', $buildDir); + set('release_path', $buildDir); + set('current_path', $buildDir); + + if (!get('build_from_repo')) { + return; + } + + $repository = (string) get('repository'); + if ($repository === '') { + throw new GracefulShutdownException('You must specify the "repository" option.'); + } + + run('rm -rf {{release_or_current_path}}'); + run('git clone {{repository}} {{release_or_current_path}}'); + run('git -C {{release_or_current_path}} checkout --force {{target}}'); +}); + +desc('Builds an artifact.'); +task('artifact:build', [ + 'build:prepare', + 'build:remove-generated', + 'deploy:vendors', + 'magento:compile', + 'magento:deploy:assets', + 'artifact:package', +]); + +// Array of shared files that will be added to the default shared_files without overriding +set('additional_shared_files', []); +// Array of shared directories that will be added to the default shared_dirs without overriding +set('additional_shared_dirs', []); + + +desc('Adds additional files and dirs to the list of shared files and dirs'); +task('deploy:additional-shared', function () { + add('shared_files', get('additional_shared_files')); + add('shared_dirs', get('additional_shared_dirs')); +}); + +/** + * Update cache id_prefix on deploy so that you are compiling against a fresh cache + * Reference Issue: https://github.com/davidalger/capistrano-magento2/issues/151 + * To use this feature, add the following to your deployer scripts: + * ```php + * after('deploy:shared', 'magento:set_cache_prefix'); + * after('deploy:magento', 'magento:cleanup_cache_prefix'); + * ``` + **/ +desc('Update cache id_prefix'); +task('magento:set_cache_prefix', function () { + //download current env config + $tmpConfigFile = tempnam(sys_get_temp_dir(), 'deployer_config'); + download('{{deploy_path}}/shared/' . ENV_CONFIG_FILE_PATH, $tmpConfigFile); + $envConfigArray = include($tmpConfigFile); + //set prefix to `alias_releasename_` + $prefixUpdate = get('alias') . '_' . get('release_name') . '_'; + + //check for preload keys and update + if (isset($envConfigArray['cache']['frontend']['default']['backend_options']['preload_keys'])) { + $oldPrefix = $envConfigArray['cache']['frontend']['default']['id_prefix']; + $preloadKeys = $envConfigArray['cache']['frontend']['default']['backend_options']['preload_keys']; + $newPreloadKeys = []; + foreach ($preloadKeys as $preloadKey) { + $newPreloadKeys[] = preg_replace('/^' . $oldPrefix . '/', $prefixUpdate, $preloadKey); + } + $envConfigArray['cache']['frontend']['default']['backend_options']['preload_keys'] = $newPreloadKeys; + } + + //update id_prefix to include release name + $envConfigArray['cache']['frontend']['default']['id_prefix'] = $prefixUpdate; + $envConfigArray['cache']['frontend']['page_cache']['id_prefix'] = $prefixUpdate; + + //Generate configuration array as string + $envConfigStr = ' $name, 'VERSION_ID' => $version] = parse_ini_string($release); + if ($name !== 'Ubuntu') { + warning('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); + warning('!! !!'); + warning('!! Only Ubuntu is supported! !!'); + warning('!! !!'); + warning('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); + if (!askConfirmation(' Do you want to continue? (Not recommended)', false)) { + throw new \RuntimeException('Provision aborted due to incompatible OS.'); + } + } + // Also only version 20 and older are supported. + if (version_compare($version, '20', '<')) { + warning("Ubuntu $version is not supported. Use Ubuntu 20 or newer."); + if (!askConfirmation(' Do you want to continue? (Not recommended)', false)) { + throw new \RuntimeException('Provision aborted due to incompatible OS.'); + } + } +})->oncePerNode(); + +desc('Collects required params'); +task('provision:configure', function () { + set('remote_user', get('provision_user')); + + $params = [ + 'sudo_password', + 'domain', + 'public_path', + 'php_version', + 'db_type', + ]; + $dbparams = [ + 'db_user', + 'db_name', + 'db_password', + ]; + + $showCode = false; + + foreach ($params as $name) { + if (!Context::get()->getConfig()->hasOwn($name)) { + $showCode = true; + } + get($name); + } + + if (get('db_type') !== 'none') { + foreach ($dbparams as $name) { + if (!Context::get()->getConfig()->hasOwn($name)) { + $showCode = true; + } + get($name); + } + } + + if ($showCode) { + $code = "\n\n====== Configuration Start ======"; + $code .= "\nhost('{{alias}}')"; + $codeParams = $params; + if (get('db_type') !== 'none') { + $codeParams = array_merge($codeParams, $dbparams); + } + foreach ($codeParams as $name) { + $code .= "\n ->set('$name', '" . get($name) . "')"; + } + $code .= ";\n"; + $code .= "====== Configuration End ======\n\n"; + writeln($code); + } +}); + + +desc('Adds repositories and update'); +task('provision:update', function () { + set('remote_user', get('provision_user')); + + // Update before installing anything + run('apt-get update', env: ['DEBIAN_FRONTEND' => 'noninteractive']); + + // Pre-requisites + run('apt install -y curl gpg software-properties-common', env: ['DEBIAN_FRONTEND' => 'noninteractive']); + + // PHP + run('apt-add-repository ppa:ondrej/php -y', env: [ + 'DEBIAN_FRONTEND' => 'noninteractive', + 'LC_ALL' => 'C.UTF-8', + ]); + + // Caddy + run("curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor --yes -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg"); + run("curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' > /etc/apt/sources.list.d/caddy-stable.list"); + + // Update + run('apt-get update', env: ['DEBIAN_FRONTEND' => 'noninteractive']); +}) + ->oncePerNode() + ->verbose(); + +desc('Upgrades all packages'); +task('provision:upgrade', function () { + set('remote_user', get('provision_user')); + run('apt-get upgrade -y', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900); +}) + ->oncePerNode() + ->verbose(); + +desc('Installs packages'); +task('provision:install', function () { + set('remote_user', get('provision_user')); + $packages = [ + 'acl', + 'apt-transport-https', + 'build-essential', + 'caddy', + 'curl', + 'debian-archive-keyring', + 'debian-keyring', + 'fail2ban', + 'gcc', + 'git', + 'libmcrypt4', + 'libpcre3-dev', + 'libsqlite3-dev', + 'make', + 'ncdu', + 'nodejs', + 'pkg-config', + 'python-is-python3', + 'redis', + 'sendmail', + 'sqlite3', + 'ufw', + 'unzip', + 'uuid-runtime', + 'whois', + ]; + run('apt-get install -y ' . implode(' ', $packages), env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900); +}) + ->verbose() + ->oncePerNode(); + +desc('Configures the ssh'); +task('provision:ssh', function () { + set('remote_user', get('provision_user')); + run("sed -i 's/PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config"); + run('ssh-keygen -A'); + run('service ssh restart'); + if (test('[ ! -d /root/.ssh ]')) { + run('mkdir -p /root/.ssh'); + run('touch /root/.ssh/authorized_keys'); + } +})->oncePerNode(); + +desc('Setups a firewall'); +task('provision:firewall', function () { + set('remote_user', get('provision_user')); + run('ufw allow 22'); + run('ufw allow 80'); + run('ufw allow 443'); + run('ufw --force enable'); +})->oncePerNode(); + +desc('Verifies what provision was successful'); +task('provision:verify', function () { + fetch('{{domain}}', 'get', [], null, $info, true); + if ($info['http_code'] === 404) { + info("provisioned successfully!"); + } +}); diff --git a/vendor/deployer/deployer/recipe/provision/404.html b/vendor/deployer/deployer/recipe/provision/404.html new file mode 100644 index 0000000..5866577 --- /dev/null +++ b/vendor/deployer/deployer/recipe/provision/404.html @@ -0,0 +1,51 @@ + + + + + + 404 Not Found + + + +
+ + + +

Not Found

+

The requested URL was not found on this server.

+
+ + diff --git a/vendor/deployer/deployer/recipe/provision/Caddyfile b/vendor/deployer/deployer/recipe/provision/Caddyfile new file mode 100644 index 0000000..d734ffb --- /dev/null +++ b/vendor/deployer/deployer/recipe/provision/Caddyfile @@ -0,0 +1,25 @@ +{{domain}} { + root * {{deploy_path}}/current/{{public_path}} + encode zstd gzip + file_server + php_fastcgi * unix//run/php/php{{php_version}}-fpm.sock { + resolve_root_symlink + } + + log { + output file {{deploy_path}}/log/access.log { + mode 0644 + } + } + + handle_errors { + @404 { + expression {http.error.status_code} == 404 + } + rewrite @404 /404.html + encode zstd gzip + file_server { + root /var/deployer + } + } +} diff --git a/vendor/deployer/deployer/recipe/provision/databases.php b/vendor/deployer/deployer/recipe/provision/databases.php new file mode 100644 index 0000000..6371bdc --- /dev/null +++ b/vendor/deployer/deployer/recipe/provision/databases.php @@ -0,0 +1,67 @@ +limit(1); + +desc('Provision MySQL'); +task('provision:mysql', function () { + run('apt-get install -y mysql-server', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900); + run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'0.0.0.0' IDENTIFIED BY '%db_password%';\"", secrets: ['db_password' => get('db_password')]); + run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'%' IDENTIFIED BY '%db_password%';\"", secrets: ['db_password' => get('db_password')]); + run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'0.0.0.0' WITH GRANT OPTION;\""); + run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'%' WITH GRANT OPTION;\""); + run("mysql --user=\"root\" -e \"FLUSH PRIVILEGES;\""); + run("mysql --user=\"root\" -e \"CREATE DATABASE IF NOT EXISTS {{db_name}} character set UTF8mb4 collate utf8mb4_bin;\""); +}); + +desc('Provision MariaDB'); +task('provision:mariadb', function () { + run('apt-get install -y mariadb-server', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900); + run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'0.0.0.0' IDENTIFIED BY '%db_password%';\"", secrets: ['db_password' => get('db_password')]); + run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'%' IDENTIFIED BY '%db_password%';\"", secrets: ['db_password' => get('db_password')]); + run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'0.0.0.0' WITH GRANT OPTION;\""); + run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'%' WITH GRANT OPTION;\""); + run("mysql --user=\"root\" -e \"FLUSH PRIVILEGES;\""); + run("mysql --user=\"root\" -e \"CREATE DATABASE IF NOT EXISTS {{db_name}} character set UTF8mb4 collate utf8mb4_bin;\""); +}); + +desc('Provision PostgreSQL'); +task('provision:postgresql', function () { + run('apt-get install -y postgresql postgresql-contrib', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900); + run("sudo -u postgres psql <<< $'CREATE DATABASE {{db_name}};'"); + run("sudo -u postgres psql <<< $'CREATE USER {{db_user}} WITH ENCRYPTED PASSWORD \'%db_password%\';'", secrets: ['db_password' => get('db_password')]); + run("sudo -u postgres psql <<< $'GRANT ALL PRIVILEGES ON DATABASE {{db_name}} TO {{db_user}};'"); +}); diff --git a/vendor/deployer/deployer/recipe/provision/nodejs.php b/vendor/deployer/deployer/recipe/provision/nodejs.php new file mode 100644 index 0000000..45eba13 --- /dev/null +++ b/vendor/deployer/deployer/recipe/provision/nodejs.php @@ -0,0 +1,36 @@ +> /etc/profile.d/fnm.sh"); +}) + ->oncePerNode(); diff --git a/vendor/deployer/deployer/recipe/provision/php.php b/vendor/deployer/deployer/recipe/provision/php.php new file mode 100644 index 0000000..5dc19d8 --- /dev/null +++ b/vendor/deployer/deployer/recipe/provision/php.php @@ -0,0 +1,85 @@ + 2) { + $defaultPhpVersion = "$parts[0].$parts[1]"; + } + + return ask(' What PHP version to install? ', $defaultPhpVersion, ['5.6', '7.4', '8.2', '8.3', '8.4', '8.5']); +}); + +desc('Installs PHP packages'); +task('provision:php', function () { + set('remote_user', get('provision_user')); + + $version = get('php_version'); + info("Installing PHP $version"); + $packages = [ + "php$version-bcmath", + "php$version-cli", + "php$version-curl", + "php$version-dev", + "php$version-fpm", + "php$version-gd", + "php$version-imap", + "php$version-intl", + "php$version-mbstring", + "php$version-mysql", + "php$version-pgsql", + "php$version-readline", + "php$version-soap", + "php$version-sqlite3", + "php$version-xml", + "php$version-zip", + ]; + run('apt-get install -y ' . implode(' ', $packages), env: ['DEBIAN_FRONTEND' => 'noninteractive']); + + // Configure PHP-CLI + run("sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/cli/php.ini"); + run("sed -i 's/display_errors = .*/display_errors = On/' /etc/php/$version/cli/php.ini"); + run("sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/$version/cli/php.ini"); + run("sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/$version/cli/php.ini"); + run("sed -i 's/;date.timezone.*/date.timezone = UTC/' /etc/php/$version/cli/php.ini"); + + // Configure PHP-FPM + run("sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/fpm/php.ini"); + run("sed -i 's/display_errors = .*/display_errors = On/' /etc/php/$version/fpm/php.ini"); + run("sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/$version/fpm/php.ini"); + run("sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/$version/fpm/php.ini"); + run("sed -i 's/;date.timezone.*/date.timezone = UTC/' /etc/php/$version/fpm/php.ini"); + run("sed -i 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/' /etc/php/$version/fpm/php.ini"); + + // Configure FPM Pool + run("sed -i 's/;request_terminate_timeout = .*/request_terminate_timeout = 60/' /etc/php/$version/fpm/pool.d/www.conf"); + run("sed -i 's/;catch_workers_output = .*/catch_workers_output = yes/' /etc/php/$version/fpm/pool.d/www.conf"); + run("sed -i 's/;php_flag\[display_errors\] = .*/php_flag[display_errors] = yes/' /etc/php/$version/fpm/pool.d/www.conf"); + run("sed -i 's/;php_admin_value\[error_log\] = .*/php_admin_value[error_log] = \/var\/log\/fpm-php.www.log/' /etc/php/$version/fpm/pool.d/www.conf"); + run("sed -i 's/;php_admin_flag\[log_errors\] = .*/php_admin_flag[log_errors] = on/' /etc/php/$version/fpm/pool.d/www.conf"); + + // Configure PHP sessions directory + run('chmod 733 /var/lib/php/sessions'); + run('chmod +t /var/lib/php/sessions'); +}) + ->verbose() + ->limit(1); + +desc('Shows php-fpm logs'); +task('logs:php-fpm', function () { + $fpmLogs = run("ls -1 /var/log | grep fpm"); + if (empty($fpmLogs)) { + throw new \RuntimeException('No PHP-FPM logs found.'); + } + run("sudo tail -f /var/log/$fpmLogs"); +})->verbose(); + +desc('Installs Composer'); +task('provision:composer', function () { + run('curl -sS https://getcomposer.org/installer | php'); + run('mv composer.phar /usr/local/bin/composer'); +})->oncePerNode(); diff --git a/vendor/deployer/deployer/recipe/provision/user.php b/vendor/deployer/deployer/recipe/provision/user.php new file mode 100644 index 0000000..9940daa --- /dev/null +++ b/vendor/deployer/deployer/recipe/provision/user.php @@ -0,0 +1,88 @@ +/dev/null 2>&1')) { + // TODO: Check what created deployer user configured correctly. + // TODO: Update sudo_password of deployer user. + // TODO: Copy ssh_copy_id to deployer ssh dir. + info('deployer user already exist'); + } else { + run('useradd deployer'); + run('mkdir -p /home/deployer/.ssh'); + run('mkdir -p /home/deployer/.deployer'); + run('adduser deployer sudo'); + + run('chsh -s /bin/bash deployer'); + run('cp /root/.profile /home/deployer/.profile'); + run('cp /root/.bashrc /home/deployer/.bashrc'); + run('touch /home/deployer/.sudo_as_admin_successful'); + + // Make color prompt. + run("sed -i 's/#force_color_prompt=yes/force_color_prompt=yes/' /home/deployer/.bashrc"); + + $password = run("mkpasswd -m sha-512 '%password%'", secrets: ['password' => get('sudo_password')]); + run("usermod --password '%password%' deployer", secrets: ['password' => $password]); + + // Copy root public key to deployer user so user can login without password. + run('cp /root/.ssh/authorized_keys /home/deployer/.ssh/authorized_keys'); + + // Create ssh key if not already exists. + run('ssh-keygen -f /home/deployer/.ssh/id_ed25519 -t ed25519 -N ""'); + + try { + run('chown -R deployer:deployer /home/deployer'); + run('chmod -R 755 /home/deployer'); + run('chmod 700 /home/deployer/.ssh'); + run('chmod 600 /home/deployer/.ssh/id_ed25519'); + run('chmod 600 /home/deployer/.ssh/authorized_keys'); + } catch (\Throwable $e) { + warning($e->getMessage()); + } + + run('usermod -a -G www-data deployer'); + run('usermod -a -G caddy deployer'); + } +})->oncePerNode(); + + +desc('Copy public key to remote server'); +task('provision:ssh_copy_id', function () { + $defaultKeys = [ + '~/.ssh/id_rsa.pub', + '~/.ssh/id_ed25519.pub', + '~/.ssh/id_ecdsa.pub', + '~/.ssh/id_dsa.pub', + ]; + + $publicKeyContent = false; + foreach ($defaultKeys as $key) { + $file = parse_home_dir($key); + if (file_exists($file)) { + $publicKeyContent = file_get_contents($file); + break; + } + } + + if (!$publicKeyContent) { + $publicKeyContent = ask(' Public key: ', ''); + } + + if (empty($publicKeyContent)) { + info('Skipping public key copy as no public key was found or provided.'); + return; + } + + run('echo "$PUBLIC_KEY" >> /home/deployer/.ssh/authorized_keys', env: ['PUBLIC_KEY' => $publicKeyContent]); +}); diff --git a/vendor/deployer/deployer/recipe/provision/website.php b/vendor/deployer/deployer/recipe/provision/website.php new file mode 100644 index 0000000..b7403d2 --- /dev/null +++ b/vendor/deployer/deployer/recipe/provision/website.php @@ -0,0 +1,76 @@ + /var/deployer/404.html"); +})->oncePerNode(); + +desc('Provision website'); +task('provision:website', function () { + $restoreBecome = become('deployer'); + + run("[ -d {{deploy_path}} ] || mkdir -p {{deploy_path}}"); + run("chown -R deployer:deployer {{deploy_path}}"); + + set('deploy_path', run("realpath {{deploy_path}}")); + cd('{{deploy_path}}'); + + run("[ -d log ] || mkdir log"); + run("chgrp caddy log"); + + $caddyfile = parse(file_get_contents(__DIR__ . '/Caddyfile')); + + if (test('[ -f Caddyfile ]')) { + run("echo $'$caddyfile' > Caddyfile.new"); + $diff = run('diff -U5 --color=always Caddyfile Caddyfile.new', nothrow: true); + if (empty($diff)) { + run('rm Caddyfile.new'); + } else { + info('Found Caddyfile changes'); + writeln("\n" . $diff); + $answer = askChoice(' Which Caddyfile to save? ', ['old', 'new'], 0); + if ($answer === 'old') { + run('rm Caddyfile.new'); + } else { + run('mv Caddyfile.new Caddyfile'); + } + } + } else { + run("echo $'$caddyfile' > Caddyfile"); + } + + $restoreBecome(); + + if (!test("grep -q 'import {{deploy_path}}/Caddyfile' /etc/caddy/Caddyfile")) { + run("echo 'import {{deploy_path}}/Caddyfile' >> /etc/caddy/Caddyfile"); + } + run('service caddy reload'); + + info("Website {{domain}} configured!"); +})->limit(1); + +desc('Shows access logs'); +task('logs:access', function () { + run('tail -f {{deploy_path}}/log/access.log'); +})->verbose(); + +desc('Shows caddy syslog'); +task('logs:caddy', function () { + run('sudo journalctl -u caddy -f'); +})->verbose(); diff --git a/vendor/deployer/deployer/recipe/shopware.php b/vendor/deployer/deployer/recipe/shopware.php new file mode 100644 index 0000000..0b7e35e --- /dev/null +++ b/vendor/deployer/deployer/recipe/shopware.php @@ -0,0 +1,202 @@ +set('remote_user', 'SSH-USER') + * ->set('deploy_path', '/var/www/shopware') // This is the path where deployer will create its directory structure + * ->set('http_user', 'www-data') // Not needed, if the `user` is the same, the web server is running with + * ->set('http_group', 'www-data') + * ->set('writable_mode', 'chmod') + * ->set('writable_recursive', true) + * ->set('become', 'www-data'); // You might want to change user to execute remote tasks because of access rights of created cache files + * ``` + * + * :::note + * Please remember that the installation must be modified so that it can be + * [build without database](https://developer.shopware.com/docs/guides/hosting/installation-updates/deployments/build-w-o-db#compiling-the-storefront-without-database). + * ::: + */ + +namespace Deployer; + +require_once __DIR__ . '/common.php'; + +add('recipes', ['shopware']); + +set('bin/console', '{{bin/php}} {{release_or_current_path}}/bin/console'); + +set('default_timeout', 3600); // Increase when tasks take longer than that. + +// These files are shared among all releases. +set('shared_files', [ + '.env.local', + 'install.lock', + 'public/.htaccess', + 'public/.user.ini', +]); + +// These directories are shared among all releases. +set('shared_dirs', [ + 'config/jwt', + 'files', + 'var/log', + 'public/media', + 'public/plugins', + 'public/thumbnail', + 'public/sitemap', +]); + +// These directories are made writable (the definition of "writable" requires attention). +// Please note that the files in `config/jwt/*` receive special attention in the `sw:writable:jwt` task. +set('writable_dirs', [ + 'config/jwt', + 'custom/plugins', + 'files', + 'public/bundles', + 'public/css', + 'public/fonts', + 'public/js', + 'public/media', + 'public/plugins', + 'public/sitemap', + 'public/theme', + 'public/thumbnail', + 'var', +]); + +// This sets the Shopware version to the version of the Shopware console command. +set('shopware_version', function () { + $versionOutput = run('cd {{release_path}} && {{bin/console}} -V'); + preg_match('/(\d+\.\d+\.\d+\.\d+)/', $versionOutput, $matches); + return $matches[0] ?? '6.6.0'; +}); + +// This task remotely executes the `cache:clear` console command on the target server. +task('sw:cache:clear', static function () { + run('cd {{release_path}} && {{bin/console}} cache:clear --no-warmup'); +}); + +// This task remotely executes the cache warmup console commands on the target server, so that the first user, who +// visits the website, doesn't have to wait for the cache to be built up. +task('sw:cache:warmup', static function () { + run('cd {{release_path}} && {{bin/console}} cache:warmup'); + + // Shopware 6.6+ dropped support for the http:cache:warmup command, so only execute it if the version is less than 6.6 + if (version_compare(get('shopware_version'), '6.6.0') < 0) { + run('cd {{release_path}} && {{bin/console}} http:cache:warm:up'); + } +}); + +// This task remotely executes the `database:migrate` console command on the target server. +task('sw:database:migrate', static function () { + run('cd {{release_path}} && {{bin/console}} database:migrate --all'); +}); + +task('sw:plugin:refresh', function () { + run('cd {{release_path}} && {{bin/console}} plugin:refresh'); +}); + +task('sw:scheduled-task:register', function () { + run('cd {{release_path}} && {{bin/console}} scheduled-task:register'); +}); + +task('sw:theme:refresh', function () { + run('cd {{release_path}} && {{bin/console}} theme:refresh'); +}); + +// This task is not used by default, but can be used, e.g. in combination with `SHOPWARE_SKIP_THEME_COMPILE=1`, +// to build the theme remotely instead of locally. +task('sw:theme:compile', function () { + run('cd {{release_path}} && {{bin/console}} theme:compile'); +}); + +function getPlugins(): array +{ + $output = run('cd {{release_path}} && {{bin/console}} plugin:list --json'); + $plugins = json_decode($output); + + return $plugins; +} + +task('sw:plugin:update:all', static function () { + $plugins = getPlugins(); + foreach ($plugins as $plugin) { + if ($plugin->installedAt && $plugin->upgradeVersion) { + writeln("Running plugin update for " . $plugin->name . "\n"); + run("cd {{release_path}} && {{bin/console}} plugin:update " . $plugin->name); + } + } +}); + +task('sw:writable:jwt', static function () { + if (!test('[ -d {{deploy_path}}/config/jwt/ ]')) { + return; + } + run('cd {{release_path}} && find config/jwt/ -type f -exec chmod -R 660 {} +'); +}); + +/** + * Grouped SW deploy tasks. + */ +task('sw:deploy', [ + 'sw:database:migrate', + 'sw:plugin:refresh', + 'sw:theme:refresh', + 'sw:scheduled-task:register', + 'sw:cache:clear', + 'sw:plugin:update:all', + 'sw:cache:clear', +]); + +desc('Deploys your project'); +task('deploy', [ + 'deploy:prepare', + 'sw:writable:jwt', + 'sw:deploy', + 'deploy:clear_paths', + 'sw:cache:warmup', + 'deploy:publish', +]); + +task('deploy:update_code')->setCallback(static function () { + upload('.', '{{release_path}}', [ + 'options' => [ + '--exclude=.git', + '--exclude=deploy.php', + '--exclude=node_modules', + ], + ]); +}); + +task('sw-build-without-db:get-remote-config', static function () { + if (!test('[ -d {{current_path}} ]')) { + return; + } + within('{{current_path}}', function () { + run('{{bin/php}} ./bin/console bundle:dump'); + download('{{current_path}}/var/plugins.json', './var/'); + + run('{{bin/php}} ./bin/console theme:dump -n'); + download('{{current_path}}/files/theme-config', './files/'); + }); +}); + +task('sw-build-without-db:build', static function () { + runLocally('CI=1 SHOPWARE_SKIP_BUNDLE_DUMP=1 ./bin/build-js.sh'); +}); + +task('sw-build-without-db', [ + 'sw-build-without-db:get-remote-config', + 'sw-build-without-db:build', +]); + +before('deploy:update_code', 'sw-build-without-db'); diff --git a/vendor/deployer/deployer/recipe/silverstripe.php b/vendor/deployer/deployer/recipe/silverstripe.php new file mode 100644 index 0000000..810f8fe --- /dev/null +++ b/vendor/deployer/deployer/recipe/silverstripe.php @@ -0,0 +1,95 @@ += 6) + run('{{release_or_current_path}}/{{silverstripe_sake}} db:build'); + } +}); + +desc('Rebuild database and cache'); +task('silverstripe:buildflush', function () { + if (get('silverstripe_cli_script')) { + // Old behavior (Silverstripe < 6) + run('{{bin/php}} {{release_or_current_path}}/{{silverstripe_cli_script}} /dev/build flush=all'); + } elseif (get('silverstripe_sake')) { + // New behavior (Silverstripe >= 6) + run('{{release_or_current_path}}/{{silverstripe_sake}} -f db:build'); + } +}); + +/** + * Main task + */ +desc('Deploys your project'); +task('deploy', [ + 'deploy:prepare', + 'deploy:vendors', + 'silverstripe:buildflush', + 'deploy:publish', +]); diff --git a/vendor/deployer/deployer/recipe/spiral.php b/vendor/deployer/deployer/recipe/spiral.php new file mode 100644 index 0000000..41b7beb --- /dev/null +++ b/vendor/deployer/deployer/recipe/spiral.php @@ -0,0 +1,154 @@ +$output"); + } + }; +} + +/** + * Run a RoadRunner command. + * + * Supported options: + * - 'showOutput': Show the output of the command if given. + */ +function rr(string $command, array $options = []): \Closure +{ + return function () use ($command, $options): void { + $output = run("cd {{roadrunner_path}} && ./rr $command"); + + if (\in_array('showOutput', $options, true)) { + writeln("$output"); + } + }; +} + +/** + * Spiral Framework console commands + */ +desc('Configure project'); +task('spiral:configure', command('configure', ['showOutput'])); + +desc('Update (init) cycle schema from database and annotated classes'); +task('spiral:cycle', command('cycle', ['showOutput'])); + +desc('Perform all outstanding migrations'); +task('spiral:migrate', command('migrate', ['showOutput'])); + +desc('Update project state'); +task('spiral:update', command('update', ['showOutput'])); + +desc('Clean application runtime cache'); +task('spiral:cache:clean', command('cache:clean', ['showOutput'])); + +desc('Reset translation cache'); +task('spiral:i18n:reset', command('i18n:reset', ['showOutput'])); + +desc('Generate new encryption key, if it doesn\'t exist'); +task('spiral:encrypt-key', command('encrypt:key -m .env -p', ['showOutput'])); + +desc('Warm-up view cache'); +task('spiral:views:compile', command('views:compile', ['showOutput'])); + +desc('Clear view cache'); +task('spiral:views:reset', command('views:reset', ['showOutput'])); + +/** + * Cycle ORM and migrations console commands + */ +desc('Generate ORM schema migrations'); +task('cycle:migrate', command('cycle:migrate', ['showOutput'])); + +desc('Render available CycleORM schemas'); +task('cycle:render', command('cycle:render', ['showOutput'])); + +desc('Sync Cycle ORM schema with database without intermediate migration (risk operation)'); +task('cycle:sync', command('cycle:sync', ['showOutput'])); + +desc('Init migrations component (create migrations table)'); +task('migrate:init', command('migrate:init', ['showOutput'])); + +desc('Replay (down, up) one or multiple migrations'); +task('migrate:replay', command('migrate:replay', ['showOutput'])); + +desc('Rollback one (default) or multiple migrations'); +task('migrate:rollback', command('migrate:rollback', ['showOutput'])); + +desc('Get list of all available migrations and their statuses'); +task('migrate:status', command('migrate:status', ['showOutput'])); + +/** + * RoadRunner console commands + */ +desc('Start RoadRunner server'); +task('roadrunner:serve', function (): void { + exec(parse('cd {{roadrunner_path}} && ./rr serve -p > /dev/null 2>&1 &')); +}); + +desc('Stop RoadRunner server'); +task('roadrunner:stop', rr('stop', ['showOutput'])); + +desc('Reset workers of all services'); +task('roadrunner:reset', rr('reset', ['showOutput'])); + +/** + * Download and restart RoadRunner + */ +desc('Download RoadRunner'); +task('deploy:download-rr', function (): void { + $output = run("cd {{release_or_current_path}} && {{bin/php}} ./vendor/bin/rr get-binary -l {{roadrunner_path}}"); + writeln("$output"); +}); + +desc('Restart RoadRunner'); +task('deploy:restart-rr', function (): void { + try { + invoke('roadrunner:reset'); + writeln("Roadrunner successfully restarted."); + } catch (\Throwable $e) { + invoke('roadrunner:serve'); + writeln("Roadrunner successfully started."); + } +}); + +/** + * Main task + */ +desc('Deploys your project'); +task('deploy', [ + 'deploy:prepare', + 'deploy:vendors', + 'spiral:encrypt-key', + 'spiral:configure', + 'deploy:download-rr', + 'deploy:publish', + 'deploy:restart-rr', +]); diff --git a/vendor/deployer/deployer/recipe/statamic.php b/vendor/deployer/deployer/recipe/statamic.php new file mode 100644 index 0000000..e736ed1 --- /dev/null +++ b/vendor/deployer/deployer/recipe/statamic.php @@ -0,0 +1,127 @@ + array_merge(get('shared_dirs'), get('shared_files'), $exclude), + 'exclude-file' => false, + 'include' => ['vendor'], + 'include-file' => false, + 'filter' => ['dir-merge,-n /.gitignore'], + 'filter-file' => false, + 'filter-perdir' => false, + 'flags' => 'avz', + 'options' => ['delete', 'keep-dirlinks', 'links'], + 'timeout' => 600, +]); + +/** + * TYPO3 Commands + * All run via {{bin/php}} {{release_path}}/{{bin/typo3}} + */ + +desc('TYPO3 - Clear all caches'); +task('typo3:cache:flush', function () { + run('{{bin/php}} {{release_path}}/{{bin/typo3}} cache:flush'); +}); + +desc('TYPO3 - Cache warmup for system caches'); +task('typo3:cache:warmup', function () { + run('{{bin/php}} {{release_path}}/{{bin/typo3}} cache:warmup --group system'); +}); + +desc('TYPO3 - Update the language files of all activated extensions'); +task('typo3:language:update', function () { + run('{{bin/php}} {{release_path}}/{{bin/typo3}} language:update'); +}); + +desc('TYPO3 - Set up all extensions'); +task('typo3:extension:setup', function () { + run('{{bin/php}} {{release_path}}/{{bin/typo3}} extension:setup'); +}); + +desc('TYPO3 - Fix folder structure'); +task('typo3:install:fixfolderstructure', function () { + run('{{bin/php}} {{release_path}}/{{bin/typo3}} install:fixfolderstructure'); +}); + +/** + * Main deploy task for TYPO3. + * + * 1. Lock deploy to avoid concurrent runs + * 2. Create release directory + * 3. Update code (Git or rsync) + * 4. Symlink shared dirs/files + * 5. Fix TYPO3 folder structure + * 6. Ensure writable dirs + * 7. Run extension setup & perform schema updates + * 8. Update language files + * 9. Install composer vendors + * 10. Flush caches + * 11. Warm up TYPO3 caches + * 12. Unlock and clean up + */ +desc('Deploys a TYPO3 project'); +task('deploy', [ + 'deploy:info', + 'deploy:setup', + 'deploy:lock', + 'deploy:release', + 'typo3:update_code', + 'deploy:shared', + 'deploy:writable', + 'deploy:vendors', + 'typo3:install:fixfolderstructure', + 'typo3:extension:setup', + 'typo3:language:update', + 'typo3:cache:flush', + 'typo3:cache:warmup', + 'deploy:publish', +]); + +after('deploy:failed', 'deploy:unlock'); diff --git a/vendor/deployer/deployer/recipe/wordpress.php b/vendor/deployer/deployer/recipe/wordpress.php new file mode 100644 index 0000000..de46f9d --- /dev/null +++ b/vendor/deployer/deployer/recipe/wordpress.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Collection; + +use Countable; +use IteratorAggregate; + +class Collection implements Countable, IteratorAggregate +{ + protected array $values = []; + + public function all(): array + { + return $this->values; + } + + public function get(string $name): mixed + { + if ($this->has($name)) { + return $this->values[$name]; + } + throw $this->notFound($name); + } + + public function has(string $name): bool + { + return array_key_exists($name, $this->values); + } + + public function set(string $name, mixed $object) + { + $this->values[$name] = $object; + } + + public function remove(string $name): void + { + if ($this->has($name)) { + unset($this->values[$name]); + } + } + + public function count(): int + { + return count($this->values); + } + + public function select(callable $callback): array + { + $values = []; + + foreach ($this->values as $key => $value) { + if ($callback($value, $key)) { + $values[$key] = $value; + } + } + + return $values; + } + + /** + * @return \ArrayIterator|\Traversable + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + return new \ArrayIterator($this->values); + } + + protected function notFound(string $name): \InvalidArgumentException + { + return new \InvalidArgumentException("Element \"$name\" not found in collection."); + } +} diff --git a/vendor/deployer/deployer/src/Command/BlackjackCommand.php b/vendor/deployer/deployer/src/Command/BlackjackCommand.php new file mode 100644 index 0000000..2b19fb2 --- /dev/null +++ b/vendor/deployer/deployer/src/Command/BlackjackCommand.php @@ -0,0 +1,399 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface as Input; +use Symfony\Component\Console\Output\OutputInterface as Output; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; + +use function Deployer\Support\array_flatten; + +class BlackjackCommand extends Command +{ + use CommandCommon; + + /** + * @var Input + */ + private $input; + + /** + * @var Output + */ + private $output; + + public function __construct() + { + parent::__construct('blackjack'); + $this->setDescription('Play blackjack'); + } + + protected function execute(Input $input, Output $output): int + { + $this->input = $input; + $this->output = $output; + $this->telemetry(); + $io = new SymfonyStyle($this->input, $this->output); + + if (getenv('COLORTERM') === 'truecolor') { + $this->print("\x1b[38;2;255;95;109m╭\x1b[39m\x1b[38;2;255;95;107m─\x1b[39m\x1b[38;2;255;96;106m─\x1b[39m\x1b[38;2;255;96;104m─\x1b[39m\x1b[38;2;255;96;103m─\x1b[39m\x1b[38;2;255;97;101m─\x1b[39m\x1b[38;2;255;97;100m─\x1b[39m\x1b[38;2;255;97;99m─\x1b[39m\x1b[38;2;255;98;97m─\x1b[39m\x1b[38;2;255;100;98m─\x1b[39m\x1b[38;2;255;102;98m─\x1b[39m\x1b[38;2;255;104;98m─\x1b[39m\x1b[38;2;255;106;99m─\x1b[39m\x1b[38;2;255;108;99m─\x1b[39m\x1b[38;2;255;110;99m─\x1b[39m\x1b[38;2;255;112;100m─\x1b[39m\x1b[38;2;255;114;100m─\x1b[39m\x1b[38;2;255;116;100m─\x1b[39m\x1b[38;2;255;118;100m─\x1b[39m\x1b[38;2;255;120;101m─\x1b[39m\x1b[38;2;255;122;101m─\x1b[39m\x1b[38;2;255;124;101m─\x1b[39m\x1b[38;2;255;126;102m╮\x1b[39m"); + $this->print("\x1b[38;2;255;128;102m│\x1b[39m \x1b[38;2;255;130;102m│\x1b[39m"); + $this->print("\x1b[38;2;255;132;103m│\x1b[39m \x1b[38;2;255;134;103mW\x1b[39m\x1b[38;2;255;136;103me\x1b[39m\x1b[38;2;255;138;104ml\x1b[39m\x1b[38;2;255;140;104mc\x1b[39m\x1b[38;2;255;142;104mo\x1b[39m\x1b[38;2;255;144;104mm\x1b[39m\x1b[38;2;255;146;105me\x1b[39m\x1b[38;2;255;148;105m!\x1b[39m \x1b[38;2;255;150;105m│\x1b[39m"); + $this->print("\x1b[38;2;255;152;106m│\x1b[39m \x1b[38;2;255;153;106m│\x1b[39m"); + $this->print("\x1b[38;2;255;155;106m╰\x1b[39m\x1b[38;2;255;157;107m─\x1b[39m\x1b[38;2;255;159;107m─\x1b[39m\x1b[38;2;255;161;107m─\x1b[39m\x1b[38;2;255;163;108m─\x1b[39m\x1b[38;2;255;165;108m─\x1b[39m\x1b[38;2;255;166;108m─\x1b[39m\x1b[38;2;255;168;108m─\x1b[39m\x1b[38;2;255;170;109m─\x1b[39m\x1b[38;2;255;172;109m─\x1b[39m\x1b[38;2;255;174;109m─\x1b[39m\x1b[38;2;255;176;110m─\x1b[39m\x1b[38;2;255;177;110m─\x1b[39m\x1b[38;2;255;179;110m─\x1b[39m\x1b[38;2;255;181;111m─\x1b[39m\x1b[38;2;255;183;111m─\x1b[39m\x1b[38;2;255;185;111m─\x1b[39m\x1b[38;2;255;186;111m─\x1b[39m\x1b[38;2;255;188;112m─\x1b[39m\x1b[38;2;255;190;112m─\x1b[39m\x1b[38;2;255;192;112m─\x1b[39m\x1b[38;2;255;193;113m─\x1b[39m\x1b[38;2;255;195;113m╯\x1b[0m"); + } else { + $this->print("╭─────────────────────╮"); + $this->print("│ │"); + $this->print("│ Welcome! │"); + $this->print("│ │"); + $this->print("╰─────────────────────╯"); + } + + $money = 100; + + if (md5(strval(getenv('MONEY'))) === '5a7c2f336d0cc43b68951e75cdffe333') { + $money += 25; + $this->print('You got an extra $25.'); + } elseif (md5(strval(getenv('MONEY'))) === '530029252abcbda4a2a2069036ccc7fc') { + $money += 100; + $this->print('You got an extra $100.'); + } elseif (md5(strval(getenv('MONEY'))) === '1aa827a06ecbfa5d6fa7c62ad245f3a3') { + $money = 100000; + } + + $hasWatch = true; + $orderWhiskey = false; + $whiskeyLevel = 0; + + $deck = $this->newDeck(); + $graveyard = []; + $dealersHand = []; + $playersHand = []; + shuffle($deck); + $deal = function () use (&$deck, &$graveyard) { + if (count($deck) == 0) { + shuffle($graveyard); + $deck = $graveyard; + $graveyard = []; + } + return array_pop($deck); + }; + + start: + $this->print("You have $$money."); + if ($money > 0) { + $bet = (int) $io->ask('Your bet', '5'); + if ($bet <= 0) { + goto start; + } + if ($bet > $money) { + goto start; + } + } elseif ($hasWatch) { // @phpstan-ignore-line + $answer = $io->askQuestion(new ChoiceQuestion('?', ['leave', '- Here, take my watch! [$25]'], 0)); + if ($answer == 'leave') { + goto leave; + } else { + $hasWatch = false; + $money = 25; + $bet = 25; + } + } else { + goto leave; + } + + $graveyard = array_merge($graveyard, $dealersHand); + $dealersHand = []; + $dealersHand[] = $deal(); + $this->print("Dealers hand:"); + $this->printHand($dealersHand); + + $graveyard = array_merge($graveyard, $playersHand); + $playersHand = []; + $playersHand[] = $deal(); + $playersHand[] = $deal(); + $this->print("Your hand:"); + $this->printHand($playersHand, 2); + + while (true) { + $question = new ChoiceQuestion('Your turn', ['hit', 'stand'], 0); + $answer = $io->askQuestion($question); + + if ($answer === 'hit') { + $playersHand[] = $deal(); + usleep(200000); + } + + if ($answer === 'stand') { + break; + } + + $this->printHand($playersHand); + $handValue = self::handValue($playersHand); + + if ($handValue > 21) { + $this->print("You got $handValue."); + $this->print("Bust!"); + $this->print("-$$bet"); + $money -= $bet; + goto nextRound; + } + } + + $this->printHand($dealersHand); + $this->print("Dealer: " . self::handValue($dealersHand)); + sleep(1); + + while (self::handValue($dealersHand) <= 17) { + $dealersHand[] = $deal(); + $this->printHand($dealersHand); + $this->print("Dealer: " . self::handValue($dealersHand)); + sleep(1); + } + + $d = self::handValue($dealersHand); + $p = self::handValue($playersHand); + $this->print("You got $p and dealer $d."); + + if ($d > 21 || $p > $d) { + $this->print("You won!"); + $this->print("+$$bet"); + $money += $bet; + } elseif ($p < $d) { + $this->print("You lose!"); + $this->print("-$$bet"); + $money -= $bet; + } else { + $this->print("Push!"); + } + + nextRound: + $choices = ['continue', 'leave']; + if ($orderWhiskey) { + $orderWhiskey = false; + $whiskeyLevel = 4; + $this->print(); + $this->print('The waitress brought whiskey and says:'); + $this->print(' - Your whiskey, sir.'); + if ($money >= 5) { + array_push($choices, 'tip the waitress [$5]'); + } + } elseif ($money >= 5) { + array_push($choices, 'order whiskey [$5]'); + } + + if ($whiskeyLevel > 0) { + $this->printWhiskey($whiskeyLevel); + $whiskeyLevel--; + } + $answer = $io->askQuestion(new ChoiceQuestion('?', $choices, 0)); + + if ($answer == 'leave') { + goto leave; + } elseif ($money >= 5 && $answer == 'order whiskey [$5]') { + $orderWhiskey = true; + $this->print('You say:'); + $this->print(' - Whiskey, please.'); + $money -= 5; + } elseif ($money >= 5 && $answer == 'tip the waitress [$5]') { + $this->print('The waitress says:'); + $this->print(' - Thank you, sir!'); + $money -= 5; + } + $this->print(); + $this->print("=====> Next round <====="); + goto start; + + leave: + if ($money >= 5) { + $answer = $io->ask('Leave a $5 tip to the dealer?', 'yes'); + if ($answer === 'yes') { + $this->print("You can leave a tip here:"); + $this->print(); + $this->print("- https://github.com/sponsors/antonmedv"); + $this->print("- https://paypal.me/antonmedv"); + $this->print(); + } + } + $this->print('Thanks for playing, Come again!'); + return 0; + } + + private function newDeck(): array + { + $deck = []; + foreach (['♠', '♣', '♥', '♦'] as $suit) { + for ($i = 2; $i <= 10; $i++) { + $deck[] = [strval($i), $suit]; + } + $deck[] = ['J', $suit]; + $deck[] = ['Q', $suit]; + $deck[] = ['K', $suit]; + $deck[] = ['A', $suit]; + } + return $deck; + } + + public static function handValue(array $hand): int + { + $aces = 0; + $value = 0; + foreach ($hand as [$rank]) { + switch ($rank) { + case '2': + $value += 2; + break; + case '3': + $value += 3; + break; + case '4': + $value += 4; + break; + case '5': + $value += 5; + break; + case '6': + $value += 6; + break; + case '7': + $value += 7; + break; + case '8': + $value += 8; + break; + case '9': + $value += 9; + break; + case '10': + case 'J': + case 'Q': + case 'K': + $value += 10; + break; + case 'A': + $aces++; + break; + } + } + $variants = [$value]; + while ($aces-- > 0) { + $variants = array_flatten(array_map(function ($v) { + return [$v + 1, $v + 11]; + }, $variants)); + } + $sum = $variants[0]; + for ($i = 1; $i < count($variants); $i++) { + if ($variants[$i] <= 21) { + $sum = $variants[$i]; + } else { + break; + } + } + return $sum; + } + + private function print(string $text = "") + { + $this->output->writeln(" $text"); + } + + private function printHand(array $hand, int $offset = 1) + { + $cards = []; + for ($i = 0; $i < count($hand) - $offset; $i++) { + [$rank] = $hand[$i]; + $cards[] = [ + "┌───", + "│" . str_pad($rank, 3), + "│ ", + "│ ", + "│ ", + "│ ", + "└───", + ]; + } + + for (; $i < count($hand); $i++) { + [$rank, $suit] = $hand[$i]; + $cards[] = [ + "┌───────┐", + "│" . str_pad($rank, 7) . "│", + "│ │", + "│ " . $suit . " │", + "│ │", + "│" . str_pad($rank, 7, " ", STR_PAD_LEFT) . "│", + "└───────┘", + ]; + } + + for ($i = 0; $i < 7; $i++) { + $this->output->write(" "); + foreach ($cards as $lines) { + $this->output->write($lines[$i]); + } + $this->output->write("\n"); + } + } + + private function printWhiskey(int $whiskeyLevel) + { + if ($whiskeyLevel == 4) { + echo << + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Command; + +use Deployer\Deployer; +use Deployer\Support\Reporter; + +use function Deployer\Support\ci_name; + +trait CommandCommon +{ + /** + * @codeCoverageIgnore + */ + protected function telemetry(array $data = []): void + { + if (getenv('DO_NOT_TRACK') === 'true') { + return; + } + try { + Reporter::report(array_merge([ + 'command_name' => $this->getName(), + 'deployer_version' => DEPLOYER_VERSION, + 'deployer_phar' => Deployer::isPharArchive(), + 'php_version' => phpversion(), + 'os' => defined('PHP_OS_FAMILY') ? PHP_OS_FAMILY : (stristr(PHP_OS, 'DAR') ? 'OSX' : (stristr(PHP_OS, 'WIN') ? 'WIN' : (stristr(PHP_OS, 'LINUX') ? 'LINUX' : PHP_OS))), + 'ci' => ci_name(), + ], $data)); + } catch (\Throwable $e) { + return; + } + } + +} diff --git a/vendor/deployer/deployer/src/Command/ConfigCommand.php b/vendor/deployer/deployer/src/Command/ConfigCommand.php new file mode 100644 index 0000000..4526948 --- /dev/null +++ b/vendor/deployer/deployer/src/Command/ConfigCommand.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Command; + +use Deployer\Deployer; +use Deployer\Exception\WillAskUser; +use Deployer\Task\Context; +use Maml\Maml; +use Symfony\Component\Console\Input\InputInterface as Input; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Output\OutputInterface as Output; +use Symfony\Component\Yaml\Yaml; + +class ConfigCommand extends SelectCommand +{ + public function __construct(Deployer $deployer) + { + parent::__construct('config', $deployer); + $this->setDescription('Get all configuration options for hosts'); + } + + protected function configure(): void + { + parent::configure(); + $this->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (json, maml, yaml)', 'maml'); + $this->getDefinition()->getArgument('selector')->setDefault(['all']); + } + + protected function execute(Input $input, Output $output): int + { + $this->deployer->input = $input; + $this->deployer->output = new NullOutput(); + $hosts = $this->selectHosts($input, $output); + $data = []; + $keys = $this->deployer->config->keys(); + WillAskUser::$noAsk = true; + try { + foreach ($hosts as $host) { + Context::push(new Context($host)); + $values = []; + foreach ($keys as $key) { + try { + $values[$key] = $host->get($key); + } catch (WillAskUser $exception) { + $values[$key] = ['ask' => $exception->getMessage()]; + } catch (\Throwable $exception) { + $values[$key] = ['error' => $exception->getMessage()]; + } + } + foreach ($host->config()->persist() as $k => $v) { + $values[$k] = $v; + } + ksort($values); + $data[$host->getAlias()] = $values; + Context::pop(); + } + } finally { + WillAskUser::$noAsk = false; + } + $format = $input->getOption('format'); + switch ($format) { + case 'json': + $output->writeln(json_encode($data, JSON_PRETTY_PRINT)); + break; + + case 'maml': + $output->writeln(Maml::stringify($data)); + break; + + case 'yaml': + $output->write(Yaml::dump($data)); + break; + + default: + throw new \Exception("Unknown format: $format."); + } + return 0; + } +} diff --git a/vendor/deployer/deployer/src/Command/CustomOption.php b/vendor/deployer/deployer/src/Command/CustomOption.php new file mode 100644 index 0000000..1d043e3 --- /dev/null +++ b/vendor/deployer/deployer/src/Command/CustomOption.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Command; + +use Deployer\Host\Host; + +trait CustomOption +{ + /** + * @param Host[] $hosts + * @param string[] $options + */ + protected function applyOverrides(array $hosts, array $options) + { + $override = []; + foreach ($options as $option) { + [$name, $value] = explode('=', $option); + $value = $this->castValueToPhpType(trim($value)); + $override[trim($name)] = $value; + } + + foreach ($hosts as $host) { + foreach ($override as $key => $value) { + $host->set($key, $value); + } + } + } + + /** + * @param mixed $value + * @return bool|mixed + */ + protected function castValueToPhpType($value) + { + switch ($value) { + case 'true': + return true; + case 'false': + return false; + default: + return $value; + } + } +} diff --git a/vendor/deployer/deployer/src/Command/InitCommand.php b/vendor/deployer/deployer/src/Command/InitCommand.php new file mode 100644 index 0000000..31c71d4 --- /dev/null +++ b/vendor/deployer/deployer/src/Command/InitCommand.php @@ -0,0 +1,235 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Command; + +use Deployer\Deployer; +use Maml\Annotated; +use Maml\Maml; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\PhpProcess; +use Symfony\Component\Process\Process; + +class InitCommand extends Command +{ + use CommandCommon; + + protected function configure(): void + { + $this + ->setName('init') + ->setDescription('Initialize deployer in your project') + ->addOption('path', 'p', InputOption::VALUE_REQUIRED, 'Recipe path'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $io->block('Deployer ' . DEPLOYER_VERSION); + + $recipePath = $input->getOption('path'); + $language = $io->choice('Select recipe language', ['php', 'maml'], 'php'); + if (empty($recipePath)) { + $recipePath = "deploy.$language"; + } + + // Avoid accidentally override of existing file. + if (file_exists($recipePath)) { + $io->warning("$recipePath already exists"); + if (!$io->confirm("Do you want to override the existing file?", false)) { + $io->block('👍🏻'); + exit(1); + } + } + + // Template + $template = $io->choice('Select project template', $this->recipes(), 'common'); + + // Repo + $default = ''; + try { + $process = Process::fromShellCommandline('git remote get-url origin'); + $default = $process->mustRun()->getOutput(); + $default = trim($default); + } catch (RuntimeException $e) { + } + $repository = $io->ask('Repository', $default); + + // Guess host + if (preg_match('/github.com:(?[A-Za-z0-9_.\-]+)\//', $repository, $m)) { + $org = $m['org']; + $tempHostFile = tempnam(sys_get_temp_dir(), 'temp-host-file'); + $php = new PhpProcess( + <<blog, PHP_URL_HOST); + file_put_contents('$tempHostFile', \$host); + EOF, + ); + $php->start(); + } + + // Project + $default = ''; + try { + $process = Process::fromShellCommandline('basename "$PWD"'); + $default = $process->mustRun()->getOutput(); + $default = trim($default); + } catch (RuntimeException $e) { + } + $project = $io->ask('Project name', $default); + + // Hosts + $host = null; + if (isset($tempHostFile)) { + $host = file_get_contents($tempHostFile); + } + $hostsString = $io->ask('Hosts (comma separated)', $host); + if ($hostsString !== null) { + $hosts = explode(',', $hostsString); + } else { + $hosts = []; + } + + + $code = match ($language) { + 'php' => $this->php($template, $project, $repository, $hosts), + 'maml' => $this->maml($template, $project, $repository, $hosts), + default => $default, + }; + + file_put_contents($recipePath, $code); + + $this->telemetry(); + $output->writeln(sprintf( + 'Successfully created %s', + $recipePath, + )); + return 0; + } + + private function php(string $template, string $project, string $repository, array $hosts): string + { + $h = ""; + foreach ($hosts as $host) { + $h .= "host('{$host}')\n" + . " ->set('remote_user', 'deployer')\n" + . " ->set('deploy_path', '~/{$project}');\n"; + } + + return << Annotated::with([ + "recipe/$template.php", + ])->comment( + ' You can import other php or maml recipes.', + ' Import recipes directly from recipe/ folder.', + ), + "config" => Annotated::with([ + "repository" => "$repository", + ])->emptyLineBefore(), + "hosts" => [], + "tasks" => Annotated::with([ + "example" => [ + [ + "run" => "date", + "cwd" => "~", + ], + ], + ])->emptyLineBefore()->comment( + ' Define tasks as list of steps to run,', + ' or use array to strings to define group task.', + ), + ]; + + foreach ($hosts as $host) { + $recipe['hosts'][$host] = [ + "remote_user" => "deployer", + "deploy_path" => "~/$project", + ]; + } + + $recipe['hosts'] = Annotated::with($recipe['hosts']) + ->emptyLineBefore() + ->comment( + ' Hosts', + ); + + return Maml::stringify($recipe); + } + + private function recipes(): array + { + $recipes = []; + $dir = new \DirectoryIterator(__DIR__ . '/../../recipe'); + foreach ($dir as $fileinfo) { + if ($fileinfo->isDot()) { + continue; + } + if ($fileinfo->isDir()) { + continue; + } + + $recipe = pathinfo($fileinfo->getFilename(), PATHINFO_FILENAME); + + if ($recipe === 'README') { + continue; + } + + $recipes[] = $recipe; + } + + sort($recipes); + return $recipes; + } +} diff --git a/vendor/deployer/deployer/src/Command/MainCommand.php b/vendor/deployer/deployer/src/Command/MainCommand.php new file mode 100644 index 0000000..35a6d33 --- /dev/null +++ b/vendor/deployer/deployer/src/Command/MainCommand.php @@ -0,0 +1,207 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Command; + +use Deployer\Deployer; +use Deployer\Exception\Exception; +use Deployer\Exception\GracefulShutdownException; +use Deployer\Executor\Planner; +use Deployer\Utility\Httpie; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Input\InputInterface as Input; +use Symfony\Component\Console\Input\InputOption as Option; +use Symfony\Component\Console\Output\OutputInterface as Output; + +class MainCommand extends SelectCommand +{ + use CustomOption; + use CommandCommon; + + public function __construct(string $name, ?string $description, Deployer $deployer) + { + parent::__construct($name, $deployer); + if ($description) { + $this->setDescription($description); + } + } + + protected function configure(): void + { + parent::configure(); + + // Add global options defined with `option()` func. + $this->getDefinition()->addOptions($this->deployer->inputDefinition->getOptions()); + + $this->addOption( + 'option', + 'o', + Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, + 'Set configuration option', + ); + $this->addOption( + 'limit', + 'l', + Option::VALUE_REQUIRED, + 'How many tasks to run in parallel?', + ); + $this->addOption( + 'no-hooks', + null, + Option::VALUE_NONE, + 'Run tasks without after/before hooks', + ); + $this->addOption( + 'plan', + null, + Option::VALUE_NONE, + 'Show execution plan', + ); + $this->addOption( + 'start-from', + null, + Option::VALUE_REQUIRED, + 'Start execution from this task', + ); + $this->addOption( + 'log', + null, + Option::VALUE_REQUIRED, + 'Write log to a file', + ); + $this->addOption( + 'profile', + null, + Option::VALUE_REQUIRED, + 'Write profile to a file', + ); + } + + protected function execute(Input $input, Output $output): int + { + $this->deployer->input = $input; + $this->deployer->output = $output; + $this->deployer['log'] = $input->getOption('log'); + $hash = null; + if (!empty($this->deployer->config['repository'])) { + $hash = sha1($this->deployer->config['repository']); + } else { + $hosts = array_values($this->deployer->hosts->all()); + if (count($hosts) > 0) { + $hash = sha1($hosts[0]->getHostname()); + } + } + $this->telemetry([ + 'project_hash' => $hash, + 'hosts_count' => $this->deployer->hosts->count(), + 'recipes' => $this->deployer->config->get('recipes', []), + 'recipe_type' => $this->deployer->config->get('recipe_type', ''), + ]); + + $hosts = $this->selectHosts($input, $output); + $this->applyOverrides($hosts, $input->getOption('option')); + + // Save selected_hosts for selectedHosts() func. + $hostsAliases = []; + foreach ($hosts as $host) { + $hostsAliases[] = $host->getAlias(); + } + // Save selected_hosts per each host, and not globally. Otherwise it will + // not be accessible for workers. + foreach ($hosts as $host) { + $host->set('selected_hosts', $hostsAliases); + } + + $plan = $input->getOption('plan') ? new Planner($output, $hosts) : null; + + $this->deployer->scriptManager->setHooksEnabled(!$input->getOption('no-hooks')); + $startFrom = $input->getOption('start-from'); + if ($startFrom && !$this->deployer->tasks->has($startFrom)) { + throw new Exception("Task $startFrom does not exist."); + } + $skippedTasks = []; + $tasks = $this->deployer->scriptManager->getTasks($this->getName(), $startFrom, $skippedTasks); + + if (empty($tasks)) { + throw new Exception('No task will be executed, because the selected hosts do not meet the conditions of the tasks'); + } + + if (!$plan) { + $this->checkUpdates(); + if (!empty($skippedTasks)) { + foreach ($skippedTasks as $taskName) { + $output->writeln("skip $taskName"); + } + } + } + $exitCode = $this->deployer->master->run($tasks, $hosts, $plan); + + if ($plan) { + $plan->render(); + return 0; + } + + if ($exitCode === 0) { + $this->showBanner(); + return 0; + } + if ($exitCode === GracefulShutdownException::EXIT_CODE) { + return 1; + } + + // Check if we have tasks to execute on failure. + if ($this->deployer['fail']->has($this->getName())) { + $taskName = $this->deployer['fail']->get($this->getName()); + $tasks = $this->deployer->scriptManager->getTasks($taskName); + $this->deployer->master->run($tasks, $hosts); + } + + return $exitCode; + } + + private function checkUpdates() + { + try { + fwrite(STDERR, Httpie::get('https://deployer.org/check-updates/' . DEPLOYER_VERSION)->send()->body()); + } catch (\Throwable $e) { + // Meh + } + } + + private function showBanner() + { + if (getenv('DO_NOT_SHOW_BANNER') === 'true') { + return; + } + + try { + $withColors = ''; + if (function_exists('posix_isatty') && posix_isatty(STDOUT)) { + $withColors = '_with_colors'; + } + fwrite(STDERR, Httpie::get("https://deployer.medv.io/banners/" . $this->getName() . $withColors)->send()->body()); + } catch (\Throwable $e) { + // Meh + } + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + parent::complete($input, $suggestions); + if ($input->mustSuggestOptionValuesFor('start-from')) { + $taskNames = []; + foreach ($this->deployer->scriptManager->getTasks($this->getName()) as $task) { + $taskNames[] = $task->getName(); + } + $suggestions->suggestValues($taskNames); + } + } +} diff --git a/vendor/deployer/deployer/src/Command/RunCommand.php b/vendor/deployer/deployer/src/Command/RunCommand.php new file mode 100644 index 0000000..715c714 --- /dev/null +++ b/vendor/deployer/deployer/src/Command/RunCommand.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Command; + +use Deployer\Deployer; +use Deployer\Task\Context; +use Deployer\Task\Task; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface as Input; +use Symfony\Component\Console\Input\InputOption as Option; +use Symfony\Component\Console\Output\OutputInterface as Output; + +use function Deployer\cd; +use function Deployer\get; +use function Deployer\has; +use function Deployer\run; +use function Deployer\test; + +class RunCommand extends SelectCommand +{ + use CommandCommon; + use CustomOption; + + public function __construct(Deployer $deployer) + { + parent::__construct('run', $deployer); + $this->setDescription('Run any arbitrary command on hosts'); + } + + protected function configure(): void + { + $this->addArgument( + 'command-to-run', + InputArgument::REQUIRED, + 'Command to run on a remote host', + ); + parent::configure(); + $this->addOption( + 'option', + 'o', + Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, + 'Set configuration option', + ); + $this->addOption( + 'timeout', + 't', + Option::VALUE_REQUIRED, + 'Command timeout in seconds', + ); + $this->addOption( + 'raw', + 'r', + Option::VALUE_NONE, + 'No formatting, raw command output', + ); + } + + protected function execute(Input $input, Output $output): int + { + $this->deployer->input = $input; + $this->deployer->output = $output; + $this->telemetry(); + + $command = $input->getArgument('command-to-run') ?? ''; + $hosts = $this->selectHosts($input, $output); + $this->applyOverrides($hosts, $input->getOption('option')); + + $task = new Task($command, function () use ($input, $command) { + if (has('current_path')) { + $path = get('current_path'); + if (test("[ -d $path ]")) { + cd($path); + } + } + run( + $command, + timeout: intval($input->getOption('timeout')), + forceOutput: true, + ); + }); + + foreach ($hosts as $host) { + try { + $task->run(new Context($host)); + } catch (\Throwable $exception) { + $this->deployer->logger->renderException($exception, $host); + } + } + + return 0; + } +} diff --git a/vendor/deployer/deployer/src/Command/SelectCommand.php b/vendor/deployer/deployer/src/Command/SelectCommand.php new file mode 100644 index 0000000..0bada23 --- /dev/null +++ b/vendor/deployer/deployer/src/Command/SelectCommand.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Command; + +use Deployer\Deployer; +use Deployer\Exception\ConfigurationException; +use Deployer\Exception\Exception; +use Deployer\Host\Host; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface as Input; +use Symfony\Component\Console\Output\OutputInterface as Output; +use Symfony\Component\Console\Question\ChoiceQuestion; + +abstract class SelectCommand extends Command +{ + /** + * @var Deployer + */ + protected $deployer; + + public function __construct(string $name, Deployer $deployer) + { + $this->deployer = $deployer; + parent::__construct($name); + } + + protected function configure(): void + { + $this->addArgument('selector', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Host selector'); + } + + /** + * @return Host[] + */ + protected function selectHosts(Input $input, Output $output): array + { + $output->getFormatter()->setStyle('success', new OutputFormatterStyle('green')); + if (!$output->isDecorated() && !defined('NO_ANSI')) { + define('NO_ANSI', 'true'); + } + $selector = $input->getArgument('selector'); + $selector = empty($selector) ? Deployer::get()->config->get('default_selector', '') : $selector; + $selectExpression = is_array($selector) ? implode(',', $selector) : $selector; + + if (empty($selectExpression)) { + if (count($this->deployer->hosts) === 0) { + throw new ConfigurationException("No host configured.\nSpecify at least one host: `localhost();`."); + } elseif (count($this->deployer->hosts) === 1) { + $hosts = $this->deployer->hosts->all(); + } elseif ($input->isInteractive()) { + $hostsAliases = []; + foreach ($this->deployer->hosts as $host) { + $hostsAliases[] = $host->getAlias(); + } + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new ChoiceQuestion( + 'Select hosts: (comma separated)', + $hostsAliases, + ); + $question->setMultiselect(true); + $question->setErrorMessage('There is no "%s" host.'); + $answer = $helper->ask($input, $output, $question); + $answer = array_unique($answer); + $hosts = $this->deployer->hosts->select(function (Host $host) use ($answer) { + return in_array($host->getAlias(), $answer, true); + }); + } + } else { + $hosts = $this->deployer->selector->select($selectExpression); + } + + if (empty($hosts)) { + $message = 'No host selected.'; + if (!empty($selectExpression)) { + $message .= " Please, check your selector:\n\n $selectExpression"; + } + throw new Exception($message); + } + + return $hosts; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + parent::complete($input, $suggestions); + if ($input->mustSuggestArgumentValuesFor('selector')) { + $selectors = ['all']; + $configs = []; + foreach ($this->deployer->hosts as $host) { + $configs[$host->getAlias()] = $host->config()->persist(); + } + foreach ($configs as $alias => $c) { + $selectors[] = $alias; + foreach ($c['labels'] ?? [] as $label => $value) { + $selectors[] = "$label=$value"; + } + } + $selectors = array_unique($selectors); + $suggestions->suggestValues($selectors); + } + if ($input->mustSuggestOptionValuesFor('option')) { + $values = []; + foreach ($this->deployer->config->keys() as $key) { + $values[] = $key . '='; + } + $suggestions->suggestValues($values); + } + } +} diff --git a/vendor/deployer/deployer/src/Command/SshCommand.php b/vendor/deployer/deployer/src/Command/SshCommand.php new file mode 100644 index 0000000..b0c8416 --- /dev/null +++ b/vendor/deployer/deployer/src/Command/SshCommand.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Command; + +use Deployer\Deployer; +use Deployer\Host\Localhost; +use Deployer\Task\Context; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; + +use function Deployer\quote; + +/** + * @codeCoverageIgnore + */ +class SshCommand extends Command +{ + use CommandCommon; + + /** + * @var Deployer + */ + private $deployer; + + public function __construct(Deployer $deployer) + { + parent::__construct('ssh'); + $this->setDescription('Connect to host through ssh'); + $this->deployer = $deployer; + } + + protected function configure(): void + { + $this->addArgument( + 'hostname', + InputArgument::OPTIONAL, + 'Hostname', + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->telemetry(); + $hostname = $input->getArgument('hostname'); + if (!empty($hostname)) { + $host = $this->deployer->hosts->get($hostname); + } else { + $hostsAliases = []; + foreach ($this->deployer->hosts as $host) { + if ($host instanceof Localhost) { + continue; + } + $hostsAliases[] = $host->getAlias(); + } + + if (count($hostsAliases) === 0) { + $output->writeln('No remote hosts.'); + return 2; // Because there are no hosts. + } + + if (count($hostsAliases) === 1) { + $host = $this->deployer->hosts->get($hostsAliases[0]); + } else { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new ChoiceQuestion( + 'Select host:', + $hostsAliases, + ); + $question->setErrorMessage('There is no "%s" host.'); + + $hostname = $helper->ask($input, $output, $question); + $host = $this->deployer->hosts->get($hostname); + } + } + + $shell_path = 'exec $SHELL -l'; + if ($host->has('shell_path')) { + $shell_path = 'exec ' . $host->get('shell_path') . ' -l'; + } + + Context::push(new Context($host)); + $host->setSshMultiplexing(false); + $connOptions = implode(' ', array_map(function ($option) { + return quote($option); + }, $host->connectionOptions())); + $deployPath = $host->get('deploy_path', '~'); + + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + passthru("ssh -t $connOptions {$host->connectionString()} \"cd $deployPath/current 2>/dev/null || cd $deployPath; $shell_path\""); + } else { + passthru("ssh -t $connOptions {$host->connectionString()} 'cd $deployPath/current 2>/dev/null || cd $deployPath; $shell_path'"); + } + return 0; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + parent::complete($input, $suggestions); + if ($input->mustSuggestArgumentValuesFor('hostname')) { + $suggestions->suggestValues(array_keys($this->deployer->hosts->all())); + } + } +} diff --git a/vendor/deployer/deployer/src/Command/TreeCommand.php b/vendor/deployer/deployer/src/Command/TreeCommand.php new file mode 100644 index 0000000..075721b --- /dev/null +++ b/vendor/deployer/deployer/src/Command/TreeCommand.php @@ -0,0 +1,180 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Command; + +use Deployer\Deployer; +use Deployer\Task\GroupTask; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface as Input; +use Symfony\Component\Console\Output\OutputInterface as Output; + +class TreeCommand extends Command +{ + /** + * @var Output + */ + protected $output; + /** + * @var Deployer + */ + private $deployer; + /** + * @var array + */ + private $tree; + /** + * @var int + */ + private $depth = 0; + /** + * @var array + */ + private $openGroupDepths = []; + + public function __construct(Deployer $deployer) + { + parent::__construct('tree'); + $this->setDescription('Display the task-tree for a given task'); + $this->deployer = $deployer; + $this->tree = []; + } + + protected function configure(): void + { + $this->addArgument( + 'task', + InputArgument::REQUIRED, + 'Task to display the tree for', + ); + } + + protected function execute(Input $input, Output $output): int + { + $this->output = $output; + + $rootTaskName = $input->getArgument('task'); + + $this->buildTree($rootTaskName); + $this->outputTree($rootTaskName); + return 0; + } + + private function buildTree(string $taskName) + { + $this->createTreeFromTaskName($taskName, '', true); + } + + private function createTreeFromTaskName(string $taskName, string $postfix = '', bool $isLast = false) + { + $task = $this->deployer->tasks->get($taskName); + + if (!$task->isEnabled()) { + if (empty($postfix)) { + $postfix = ' // disabled'; + } else { + $postfix .= '; disabled'; + } + } + + if ($task->getBefore()) { + $beforePostfix = sprintf(" // before %s", $task->getName()); + + foreach ($task->getBefore() as $beforeTask) { + $this->createTreeFromTaskName($beforeTask, $beforePostfix); + } + } + + if ($task instanceof GroupTask) { + $isLast = $isLast && empty($task->getAfter()); + + $this->addTaskToTree($task->getName() . $postfix, $isLast); + + if (!$isLast) { + $this->openGroupDepths[] = $this->depth; + } + + $this->depth++; + + $taskGroup = $task->getGroup(); + foreach ($taskGroup as $subtask) { + $isLastSubtask = $subtask === end($taskGroup); + $this->createTreeFromTaskName($subtask, '', $isLastSubtask); + } + + if (!$isLast) { + array_pop($this->openGroupDepths); + } + + $this->depth--; + } else { + $this->addTaskToTree($task->getName() . $postfix, $isLast); + } + + if ($task->getAfter()) { + $afterPostfix = sprintf(" // after %s", $task->getName()); + + foreach ($task->getAfter() as $afterTask) { + $this->createTreeFromTaskName($afterTask, $afterPostfix); + } + } + } + + private function addTaskToTree(string $taskName, bool $isLast = false) + { + $this->tree[] = [ + 'taskName' => $taskName, + 'depth' => $this->depth, + 'isLast' => $isLast, + 'openDepths' => $this->openGroupDepths, + ]; + } + + private function outputTree(string $taskName) + { + $this->output->writeln("The task-tree for $taskName:"); + + /** + * @var int number of spaces for each depth increase + */ + $REPEAT_COUNT = 4; + + foreach ($this->tree as $treeItem) { + $depth = $treeItem['depth']; + + $startSymbol = $treeItem['isLast'] || $treeItem === end($this->tree) ? '└' : '├'; + + $prefix = ''; + + for ($i = 0; $i < $depth; $i++) { + if (in_array($i, $treeItem['openDepths'])) { + $prefix .= '│' . str_repeat(' ', $REPEAT_COUNT - 1); + } else { + $prefix .= str_repeat(' ', $REPEAT_COUNT); + } + } + + $prefix .= $startSymbol . '──'; + + $this->output->writeln(sprintf('%s %s', $prefix, $treeItem['taskName'])); + } + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + parent::complete($input, $suggestions); + if ($input->mustSuggestArgumentValuesFor('task')) { + $suggestions->suggestValues(array_keys($this->deployer->tasks->all())); + } + } +} diff --git a/vendor/deployer/deployer/src/Command/WorkerCommand.php b/vendor/deployer/deployer/src/Command/WorkerCommand.php new file mode 100644 index 0000000..f6a90fb --- /dev/null +++ b/vendor/deployer/deployer/src/Command/WorkerCommand.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Command; + +use Deployer\Deployer; +use Deployer\Executor\Worker; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption as Option; +use Symfony\Component\Console\Output\OutputInterface; + +class WorkerCommand extends MainCommand +{ + public function __construct(Deployer $deployer) + { + parent::__construct('worker', null, $deployer); + $this->setHidden(true); + } + + protected function configure(): void + { + parent::configure(); + $this->addOption('task', null, Option::VALUE_REQUIRED); + $this->addOption('host', null, Option::VALUE_REQUIRED); + $this->addOption('port', null, Option::VALUE_REQUIRED); + $this->addOption('decorated', null, Option::VALUE_NONE); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->deployer->input = $input; + $this->deployer->output = $output; + $this->deployer['log'] = $input->getOption('log'); + $output->setDecorated($input->getOption('decorated')); + if (!$output->isDecorated() && !defined('NO_ANSI')) { + define('NO_ANSI', 'true'); + } + + define('MASTER_ENDPOINT', 'http://localhost:' . $input->getOption('port')); + define('MASTER_TOKEN', getenv('DEPLOYER_MASTER_TOKEN') ?: ''); + + $task = $this->deployer->tasks->get($input->getOption('task')); + $host = $this->deployer->hosts->get($input->getOption('host')); + $host->config()->load(); + + $worker = new Worker($this->deployer); + $exitCode = $worker->execute($task, $host); + + $host->config()->save(); + return $exitCode; + } +} diff --git a/vendor/deployer/deployer/src/Component/Pimple/Container.php b/vendor/deployer/deployer/src/Component/Pimple/Container.php new file mode 100644 index 0000000..30430b2 --- /dev/null +++ b/vendor/deployer/deployer/src/Component/Pimple/Container.php @@ -0,0 +1,289 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Component\Pimple; + +use Deployer\Component\Pimple\Exception\ExpectedInvokableException; +use Deployer\Component\Pimple\Exception\FrozenServiceException; +use Deployer\Component\Pimple\Exception\InvalidServiceIdentifierException; +use Deployer\Component\Pimple\Exception\UnknownIdentifierException; + +/** + * Container main class. + * + * @author Fabien Potencier + */ +class Container implements \ArrayAccess +{ + /** + * @var array + */ + private $values = []; + /** + * @var \SplObjectStorage + */ + private $factories; + /** + * @var \SplObjectStorage + */ + private $protected; + /** + * @var array + */ + private $frozen = []; + /** + * @var array + */ + private $raw = []; + /** + * @var array + */ + private $keys = []; + + /** + * Instantiates the container. + * + * Objects and parameters can be passed as argument to the constructor. + * + * @param array $values The parameters or objects + */ + public function __construct(array $values = []) + { + $this->factories = new \SplObjectStorage(); + $this->protected = new \SplObjectStorage(); + + foreach ($values as $key => $value) { + $this->offsetSet($key, $value); + } + } + + /** + * Sets a parameter or an object. + * + * Objects must be defined as Closures. + * + * Allowing any PHP callable leads to difficult to debug problems + * as function names (strings) are callable (creating a function with + * the same name as an existing parameter would break your container). + * + * @param string $id The unique identifier for the parameter or object + * @param mixed $value The value of the parameter or a closure to define an object + * + * @throws FrozenServiceException Prevent override of a frozen service + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint + */ + #[\ReturnTypeWillChange] + public function offsetSet($id, $value) + { + if (isset($this->frozen[$id])) { + throw new FrozenServiceException($id); + } + + $this->values[$id] = $value; + $this->keys[$id] = true; + } + + /** + * Gets a parameter or an object. + * + * @param string $id The unique identifier for the parameter or object + * + * @return mixed The value of the parameter or an object + * + * @throws UnknownIdentifierException If the identifier is not defined + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint + */ + #[\ReturnTypeWillChange] + public function offsetGet($id) + { + if (!isset($this->keys[$id])) { + throw new UnknownIdentifierException($id); + } + + if ( + isset($this->raw[$id]) + || !\is_object($this->values[$id]) + || isset($this->protected[$this->values[$id]]) + || !\method_exists($this->values[$id], '__invoke') + ) { + return $this->values[$id]; + } + + if (isset($this->factories[$this->values[$id]])) { + return $this->values[$id]($this); + } + + $raw = $this->values[$id]; + $val = $this->values[$id] = $raw($this); + $this->raw[$id] = $raw; + + $this->frozen[$id] = true; + + return $val; + } + + /** + * Checks if a parameter or an object is set. + * + * @param string $id The unique identifier for the parameter or object + * + * @return bool + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint + */ + #[\ReturnTypeWillChange] + public function offsetExists($id) + { + return isset($this->keys[$id]); + } + + /** + * Unsets a parameter or an object. + * + * @param string $id The unique identifier for the parameter or object + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint + */ + #[\ReturnTypeWillChange] + public function offsetUnset($id) + { + if (isset($this->keys[$id])) { + if (\is_object($this->values[$id])) { + unset($this->factories[$this->values[$id]], $this->protected[$this->values[$id]]); + } + + unset($this->values[$id], $this->frozen[$id], $this->raw[$id], $this->keys[$id]); + } + } + + /** + * Marks a callable as being a factory service. + * + * @param callable $callable A service definition to be used as a factory + * + * @return callable The passed callable + * + * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object + */ + public function factory(callable $callable) + { + if (!\method_exists($callable, '__invoke')) { + throw new ExpectedInvokableException('Service definition is not a Closure or invokable object.'); + } + + $this->factories->offsetSet($callable, null); + + return $callable; + } + + /** + * Protects a callable from being interpreted as a service. + * + * This is useful when you want to store a callable as a parameter. + * + * @param callable $callable A callable to protect from being evaluated + * + * @return callable The passed callable + * + * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object + */ + public function protect(callable $callable) + { + if (!\method_exists($callable, '__invoke')) { + throw new ExpectedInvokableException('Callable is not a Closure or invokable object.'); + } + + $this->protected->offsetSet($callable, null); + + return $callable; + } + + /** + * Gets a parameter or the closure defining an object. + * + * @param string $id The unique identifier for the parameter or object + * + * @return mixed The value of the parameter or the closure defining an object + * + * @throws UnknownIdentifierException If the identifier is not defined + */ + public function raw(string $id) + { + if (!isset($this->keys[$id])) { + throw new UnknownIdentifierException($id); + } + + if (isset($this->raw[$id])) { + return $this->raw[$id]; + } + + return $this->values[$id]; + } + + /** + * Extends an object definition. + * + * Useful when you want to extend an existing object definition, + * without necessarily loading that object. + * + * @param string $id The unique identifier for the object + * @param callable $callable A service definition to extend the original + * + * @return callable The wrapped callable + * + * @throws UnknownIdentifierException If the identifier is not defined + * @throws FrozenServiceException If the service is frozen + * @throws InvalidServiceIdentifierException If the identifier belongs to a parameter + * @throws ExpectedInvokableException If the extension callable is not a closure or an invokable object + */ + public function extend(string $id, callable $callable) + { + if (!isset($this->keys[$id])) { + throw new UnknownIdentifierException($id); + } + + if (isset($this->frozen[$id])) { + throw new FrozenServiceException($id); + } + + if (!\is_object($this->values[$id]) || !\method_exists($this->values[$id], '__invoke')) { + throw new InvalidServiceIdentifierException($id); + } + + if (isset($this->protected[$this->values[$id]])) { + @\trigger_error(\sprintf('How Pimple behaves when extending protected closures will be fixed in Pimple 4. Are you sure "%s" should be protected?', $id), E_USER_DEPRECATED); + } + + if (!\is_object($callable) || !\method_exists($callable, '__invoke')) { + throw new ExpectedInvokableException('Extension service definition is not a Closure or invokable object.'); + } + + $factory = $this->values[$id]; + + $extended = function ($c) use ($callable, $factory) { + return $callable($factory($c), $c); + }; + + if (isset($this->factories[$factory])) { + $this->factories->offsetUnset($factory); + $this->factories->offsetSet($extended, null); + } + + return $this[$id] = $extended; + } + + /** + * Returns all defined value names. + * + * @return array An array of value names + */ + public function keys() + { + return \array_keys($this->values); + } +} diff --git a/vendor/deployer/deployer/src/Component/Pimple/Exception/ExpectedInvokableException.php b/vendor/deployer/deployer/src/Component/Pimple/Exception/ExpectedInvokableException.php new file mode 100644 index 0000000..f244a0a --- /dev/null +++ b/vendor/deployer/deployer/src/Component/Pimple/Exception/ExpectedInvokableException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Component\Pimple\Exception; + +use Psr\Container\ContainerExceptionInterface; + +/** + * A closure or invokable object was expected. + * + * @author Pascal Luna + */ +class ExpectedInvokableException extends \InvalidArgumentException implements ContainerExceptionInterface {} diff --git a/vendor/deployer/deployer/src/Component/Pimple/Exception/FrozenServiceException.php b/vendor/deployer/deployer/src/Component/Pimple/Exception/FrozenServiceException.php new file mode 100644 index 0000000..f332c53 --- /dev/null +++ b/vendor/deployer/deployer/src/Component/Pimple/Exception/FrozenServiceException.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Component\Pimple\Exception; + +use Psr\Container\ContainerExceptionInterface; + +/** + * An attempt to modify a frozen service was made. + * + * @author Pascal Luna + */ +class FrozenServiceException extends \RuntimeException implements ContainerExceptionInterface +{ + /** + * @param string $id Identifier of the frozen service + */ + public function __construct(string $id) + { + parent::__construct(\sprintf('Cannot override frozen service "%s".', $id)); + } +} diff --git a/vendor/deployer/deployer/src/Component/Pimple/Exception/InvalidServiceIdentifierException.php b/vendor/deployer/deployer/src/Component/Pimple/Exception/InvalidServiceIdentifierException.php new file mode 100644 index 0000000..99358ec --- /dev/null +++ b/vendor/deployer/deployer/src/Component/Pimple/Exception/InvalidServiceIdentifierException.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Component\Pimple\Exception; + +use Psr\Container\NotFoundExceptionInterface; + +/** + * An attempt to perform an operation that requires a service identifier was made. + * + * @author Pascal Luna + */ +class InvalidServiceIdentifierException extends \InvalidArgumentException implements NotFoundExceptionInterface +{ + /** + * @param string $id The invalid identifier + */ + public function __construct(string $id) + { + parent::__construct(\sprintf('Identifier "%s" does not contain an object definition.', $id)); + } +} diff --git a/vendor/deployer/deployer/src/Component/Pimple/Exception/UnknownIdentifierException.php b/vendor/deployer/deployer/src/Component/Pimple/Exception/UnknownIdentifierException.php new file mode 100644 index 0000000..d2aa39a --- /dev/null +++ b/vendor/deployer/deployer/src/Component/Pimple/Exception/UnknownIdentifierException.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Component\Pimple\Exception; + +use Psr\Container\NotFoundExceptionInterface; + +/** + * The identifier of a valid service or parameter was expected. + * + * @author Pascal Luna + */ +class UnknownIdentifierException extends \InvalidArgumentException implements NotFoundExceptionInterface +{ + /** + * @param string $id The unknown identifier + */ + public function __construct(string $id) + { + parent::__construct(\sprintf('Identifier "%s" is not defined.', $id)); + } +} diff --git a/vendor/deployer/deployer/src/Configuration.php b/vendor/deployer/deployer/src/Configuration.php new file mode 100644 index 0000000..4f0a85f --- /dev/null +++ b/vendor/deployer/deployer/src/Configuration.php @@ -0,0 +1,222 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer; + +use Deployer\Exception\ConfigurationException; +use Deployer\Utility\Httpie; + +use function Deployer\Support\array_merge_alternate; +use function Deployer\Support\is_closure; +use function Deployer\Support\normalize_line_endings; + +class Configuration implements \ArrayAccess +{ + private ?Configuration $parent; + private array $values = []; + + public function __construct(?Configuration $parent = null) + { + $this->parent = $parent; + } + + public function update(array $values): void + { + $this->values = array_merge($this->values, $values); + } + + public function bind(Configuration $parent): void + { + $this->parent = $parent; + } + + public function set(string $name, mixed $value): void + { + $this->values[$name] = $value; + } + + public function has(string $name): bool + { + $ok = array_key_exists($name, $this->values); + if ($ok) { + return true; + } + if ($this->parent) { + return $this->parent->has($name); + } + return false; + } + + public function hasOwn(string $name): bool + { + return array_key_exists($name, $this->values); + } + + public function add(string $name, array $array): void + { + if ($this->has($name)) { + $config = $this->get($name); + if (!is_array($config)) { + throw new ConfigurationException("Config option \"$name\" isn't array."); + } + $this->set($name, array_merge_alternate($config, $array)); + } else { + $this->set($name, $array); + } + } + + public function get(string $name, mixed $default = null): mixed + { + if (array_key_exists($name, $this->values)) { + if (is_closure($this->values[$name])) { + return $this->values[$name] = $this->parse(call_user_func($this->values[$name])); + } else { + return $this->parse($this->values[$name]); + } + } + + if ($this->parent) { + $rawValue = $this->parent->fetch($name); + if ($rawValue !== null) { + if (is_closure($rawValue)) { + return $this->values[$name] = $this->parse(call_user_func($rawValue)); + } else { + return $this->values[$name] = $this->parse($rawValue); + } + } + } + + if (func_num_args() >= 2) { + return $this->parse($default); + } + + throw new ConfigurationException("Config option \"$name\" does not exist."); + } + + protected function fetch(string $name): mixed + { + if (array_key_exists($name, $this->values)) { + return $this->values[$name]; + } + if ($this->parent) { + return $this->parent->fetch($name); + } + return null; + } + + public function parse(mixed $value): mixed + { + if (is_string($value)) { + $normalizedValue = normalize_line_endings($value); + $normalizedValue = str_replace('\{{', "\x00\x00", $normalizedValue); + $result = preg_replace_callback('/\{\{\s*([\w\.\/-]+)\s*(?:\|\s*(\w+)\s*)?\}\}/', function (array $matches) { + $value = $this->get($matches[1]); + if (isset($matches[2])) { + $value = match ($matches[2]) { + 'quote' => quote((string) $value), + default => throw new \InvalidArgumentException("Unknown filter: {$matches[2]}"), + }; + } + return $value; + }, $normalizedValue); + return str_replace("\x00\x00", '{{', $result); + } + + return $value; + } + + public function keys(): array + { + return array_keys($this->values); + } + + /** + * @param string $offset + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + return $this->has($offset); + } + + /** + * @param string $offset + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return $this->get($offset); + } + + /** + * @param string $offset + * @param mixed $value + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value): void + { + $this->set($offset, $value); + } + + /** + * @param mixed $offset + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset): void + { + unset($this->values[$offset]); + } + + public function load(): void + { + if (!Deployer::isWorker()) { + return; + } + + $values = Httpie::post(MASTER_ENDPOINT . '/load') + ->noTimeout() + ->bearerToken(MASTER_TOKEN) + ->jsonBody([ + 'host' => $this->get('alias'), + ]) + ->sendJson(); + $this->update($values); + } + + public function save(): void + { + if (!Deployer::isWorker()) { + return; + } + + Httpie::post(MASTER_ENDPOINT . '/save') + ->noTimeout() + ->bearerToken(MASTER_TOKEN) + ->jsonBody([ + 'host' => $this->get('alias'), + 'config' => $this->persist(), + ]) + ->sendJson(); + } + + public function persist(): array + { + $values = []; + foreach ($this->values as $key => $value) { + if (is_closure($value)) { + continue; + } + $values[$key] = $value; + } + return $values; + } +} diff --git a/vendor/deployer/deployer/src/Deployer.php b/vendor/deployer/deployer/src/Deployer.php new file mode 100644 index 0000000..3b75ddf --- /dev/null +++ b/vendor/deployer/deployer/src/Deployer.php @@ -0,0 +1,350 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer; + +use Deployer\Collection\Collection; +use Deployer\Command\BlackjackCommand; +use Deployer\Command\ConfigCommand; +use Deployer\Command\InitCommand; +use Deployer\Command\MainCommand; +use Deployer\Command\RunCommand; +use Deployer\Command\SshCommand; +use Deployer\Command\TreeCommand; +use Deployer\Command\WorkerCommand; +use Deployer\Component\Pimple\Container; +use Deployer\Exception\SchemaException; +use Deployer\Executor\Master; +use Deployer\Host\Host; +use Deployer\Host\HostCollection; +use Deployer\Host\Localhost; +use Deployer\Import\Import; +use Deployer\Logger\Handler\FileHandler; +use Deployer\Logger\Handler\NullHandler; +use Deployer\Logger\Logger; +use Deployer\ProcessRunner\ProcessRunner; +use Deployer\Selector\Selector; +use Deployer\Ssh\SshClient; +use Deployer\Task\ScriptManager; +use Deployer\Task\TaskCollection; +use Deployer\Utility\Httpie; +use Deployer\Utility\Rsync; +use Symfony\Component\Console; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Throwable; + +/** + * @property Application $console + * @property InputInterface $input + * @property OutputInterface $output + * @property Task\TaskCollection|Task\Task[] $tasks + * @property HostCollection|Host[] $hosts + * @property Configuration $config + * @property Rsync $rsync + * @property SshClient $sshClient + * @property ProcessRunner $processRunner + * @property Task\ScriptManager $scriptManager + * @property Selector $selector + * @property Master $master + * @property Logger $logger + * @property Collection $fail + * @property InputDefinition $inputDefinition + * @property Import $importer + */ +class Deployer extends Container +{ + private static ?self $instance = null; + + public function __construct(Application $console) + { + parent::__construct(); + + /****************************** + * Console * + ******************************/ + + $console->getDefinition()->addOption( + new InputOption('file', 'f', InputOption::VALUE_REQUIRED, 'Recipe file path'), + ); + + $this['console'] = function () use ($console) { + return $console; + }; + $this['input'] = function () { + throw new \RuntimeException('Uninitialized "input" in Deployer container.'); + }; + $this['output'] = function () { + throw new \RuntimeException('Uninitialized "output" in Deployer container.'); + }; + $this['inputDefinition'] = function () { + return new InputDefinition(); + }; + $this['questionHelper'] = function () { + return $this->getHelper('question'); + }; + + /****************************** + * Config * + ******************************/ + + $this['config'] = function () { + return new Configuration(); + }; + // -l act as if it had been invoked as a login shell (i.e. source ~/.profile file) + // -s commands are read from the standard input (no arguments should remain after this option) + $this->config['shell'] = function () { + if (currentHost() instanceof Localhost) { + return 'bash -s'; // Non-login shell for localhost. + } + return 'bash -ls'; + }; + $this->config['forward_agent'] = true; + $this->config['ssh_multiplexing'] = true; + + /****************************** + * Core * + ******************************/ + + $this['logHandler'] = function () { + return !empty($this['log']) + ? new FileHandler($this['log']) + : new NullHandler(); + }; + $this['logger'] = function ($c) { + return new Logger($c['output'], $this['logHandler']); + }; + $this['sshClient'] = function ($c) { + return new SshClient($c['output'], $c['logger']); + }; + $this['rsync'] = function ($c) { + return new Rsync($c['output'], $c['logger']); + }; + $this['processRunner'] = function ($c) { + return new ProcessRunner($c['logger']); + }; + $this['tasks'] = function () { + return new TaskCollection(); + }; + $this['hosts'] = function () { + return new HostCollection(); + }; + $this['scriptManager'] = function ($c) { + return new ScriptManager($c['tasks']); + }; + $this['selector'] = function ($c) { + return new Selector($c['hosts']); + }; + $this['fail'] = function () { + return new Collection(); + }; + $this['master'] = function ($c) { + return new Master( + $c['hosts'], + $c['input'], + $c['output'], + $c['logger'], + ); + }; + $this['importer'] = function () { + return new Import(); + }; + + self::$instance = $this; + } + + public static function get(): self + { + if (self::$instance === null) { + throw new \RuntimeException('Deployer is not initialized.'); + } + + return self::$instance; + } + + /** + * @internal For tests that need a clean Deployer singleton between cases. + */ + public static function resetInstance(): void + { + self::$instance = null; + } + + public function init(): void + { + $this->addTaskCommands(); + $this->getConsole()->addCommand(new BlackjackCommand()); + $this->getConsole()->addCommand(new ConfigCommand($this)); + $this->getConsole()->addCommand(new WorkerCommand($this)); + $this->getConsole()->addCommand(new InitCommand()); + $this->getConsole()->addCommand(new TreeCommand($this)); + $this->getConsole()->addCommand(new SshCommand($this)); + $this->getConsole()->addCommand(new RunCommand($this)); + } + + /** + * Transform tasks to console commands. + */ + public function addTaskCommands(): void + { + foreach ($this->tasks as $name => $task) { + $command = new MainCommand($name, $task->getDescription(), $this); + $command->setHidden($task->isHidden()); + + $this->getConsole()->addCommand($command); + } + } + + public function __get(string $name): mixed + { + if (isset($this[$name])) { + return $this[$name]; + } else { + throw new \InvalidArgumentException("Property \"$name\" does not exist."); + } + } + + public function __set(string $name, mixed $value): void + { + $this[$name] = $value; + } + + public function getConsole(): Application + { + return $this['console']; + } + + public function getHelper(string $name): Console\Helper\HelperInterface + { + return $this->getConsole()->getHelperSet()->get($name); + } + + public static function run(string $version, ?string $deployFile): void + { + if (str_contains($version, 'master')) { + // Get version from composer.lock + $lockFile = __DIR__ . '/../../../../composer.lock'; + if (file_exists($lockFile)) { + $content = file_get_contents($lockFile); + $json = json_decode($content); + foreach ($json->packages as $package) { + if ($package->name === 'deployer/deployer') { + $version = $package->version; + } + } + } + } + + // Version must be without "v" prefix. + // Incorrect: v7.0.0 + // Correct: 7.0.0 + // But deployphp/deployer uses tags with "v", and it gets passed to + // the composer.json file. Let's manually remove it from the version. + if (preg_match("/^v/", $version)) { + $version = substr($version, 1); + } + + if (!defined('DEPLOYER_VERSION')) { + define('DEPLOYER_VERSION', $version); + } + + $input = new ArgvInput(); + $output = new ConsoleOutput(); + + try { + $console = new Application('Deployer', $version); + $deployer = new self($console); + + // Import recipe file + if (is_readable($deployFile ?? '')) { + $deployer->config->set('recipe_type', match (pathinfo($deployFile, PATHINFO_EXTENSION)) { + 'php' => 'php', + 'maml' => 'maml', + 'yaml', 'yml' => 'yaml', + default => 'unknown', + }); + $deployer->importer->import($deployFile); + } + + $deployer->init(); + $console->run($input, $output); + + } catch (Throwable $exception) { + if (str_contains("$input", "-vvv")) { + $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); + } + self::printException($output, $exception); + + exit(1); + } + } + + public static function printException(OutputInterface $output, Throwable $exception): void + { + if ($exception instanceof SchemaException) { + $output->writeln([ + " Schema error {$exception->getMessage()}", + ]); + } else { + $class = get_class($exception); + $file = basename($exception->getFile()); + $output->writeln([ + " {$class} in {$file} on line {$exception->getLine()}:", + "", + implode("\n", array_map(function ($line) { + return " " . $line; + }, explode("\n", $exception->getMessage()))), + "", + ]); + if ($output->isDebug()) { + $output->writeln($exception->getTraceAsString()); + } + } + + if ($exception->getPrevious()) { + self::printException($output, $exception->getPrevious()); + } + } + + public static function isWorker(): bool + { + return defined('MASTER_ENDPOINT'); + } + + /** + * @return array|bool|string + */ + public static function masterCall(Host $host, string $func, mixed ...$arguments): mixed + { + // As request to master will stop master permanently, wait a little bit + // in order for ticker gather worker outputs and print it to user. + usleep(100_000); // Sleep 100ms. + + return Httpie::post(MASTER_ENDPOINT . '/proxy') + ->noTimeout() + ->bearerToken(MASTER_TOKEN) + ->jsonBody([ + 'host' => $host->getAlias(), + 'func' => $func, + 'arguments' => $arguments, + ]) + ->sendJson(); + } + + public static function isPharArchive(): bool + { + return str_starts_with(__FILE__, 'phar:'); + } +} diff --git a/vendor/deployer/deployer/src/Documentation/ApiGen.php b/vendor/deployer/deployer/src/Documentation/ApiGen.php new file mode 100644 index 0000000..2cd64b0 --- /dev/null +++ b/vendor/deployer/deployer/src/Documentation/ApiGen.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Documentation; + +class ApiGen +{ + /** + * @var array + */ + private $fns = []; + + public function parse(string $source): void + { + $comment = ''; + $params = ''; + $signature = ''; + + $source = str_replace("\r\n", "\n", $source); + + $state = 'root'; + foreach (explode("\n", $source) as $lineNumber => $line) { + switch ($state) { + case 'root': + if (str_starts_with($line, '/**')) { + $state = 'comment'; + break; + } + if (str_starts_with($line, 'function')) { + $signature = preg_replace('/^function\s+/', '', $line); + $funcName = preg_replace('/\(.*$/', '', $signature); + $this->fns[] = [ + 'comment' => $comment, + 'params' => $params, + 'funcName' => $funcName, + 'signature' => $signature, + ]; + $comment = ''; + $params = ''; + + if (str_ends_with($signature, '(')) { + $state = 'params'; + } else { + $signature = ''; + } + } + break; + + case 'comment': + if (str_ends_with($line, '*/')) { + $state = 'root'; + break; + } + if (preg_match('/^\s\*\s@param\s(?.+?)\$(?.+?)\s(?.+)$/', $line, $matches)) { + if (empty($params)) { + $params = "| Argument | Type | Comment |\n|---|---|---|\n"; + } + $type = implode(' or ', array_map(function ($t) { + $t = trim($t, ' '); + return "`$t`"; + }, explode('|', $matches['type']))); + $params .= "| `\${$matches['name']}` | $type | {$matches['comment']} |\n"; + break; + } + if (str_starts_with($line, ' * @')) { + break; + } + $comment .= preg_replace('/^\s\*\s?/', '', $line) . "\n"; + break; + + case 'params': + if (preg_match('/^\).+\{$/', $line, $matches)) { + $signature .= "\n" . preg_replace('/\{$/', '', $line); + $this->fns[count($this->fns) - 1]['signature'] = $signature; + $state = 'root'; + } else { + $signature .= "\n" . $line; + } + break; + } + } + } + + public function markdown(): string + { + $output = << + + + + # API Reference + + + MD; + + foreach ($this->fns as $fn) { + [ + 'comment' => $comment, + 'params' => $params, + 'funcName' => $funcName, + 'signature' => $signature, + ] = $fn; + + if (!empty($params)) { + $params = "\n$params"; + } + + $output .= << + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Documentation; + +class DocConfig +{ + /** + * @var string + */ + public $name; + /** + * @var string + */ + public $defaultValue; + /** + * @var string + */ + public $comment; + /** + * @var string + */ + public $recipePath; + /** + * @var int + */ + public $lineNumber; +} diff --git a/vendor/deployer/deployer/src/Documentation/DocGen.php b/vendor/deployer/deployer/src/Documentation/DocGen.php new file mode 100644 index 0000000..fe7b711 --- /dev/null +++ b/vendor/deployer/deployer/src/Documentation/DocGen.php @@ -0,0 +1,417 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Documentation; + +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use RecursiveRegexIterator; +use RegexIterator; + +class DocGen +{ + /** + * @var string + */ + public $root; + /** + * @var DocRecipe[] + */ + public $recipes = []; + + public function __construct(string $root) + { + $this->root = str_replace(DIRECTORY_SEPARATOR, '/', realpath($root)); + } + + public function parse(string $source): void + { + $directory = new RecursiveDirectoryIterator($source); + $iterator = new RegexIterator(new RecursiveIteratorIterator($directory), '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH); + foreach ($iterator as [$path]) { + $realPath = str_replace(DIRECTORY_SEPARATOR, '/', realpath($path)); + $recipePath = str_replace($this->root . '/', '', $realPath); + $recipeName = preg_replace('/\.php$/i', '', basename($recipePath)); + $recipe = new DocRecipe($recipeName, $recipePath); + $recipe->parse(file_get_contents($path)); + $this->recipes[$recipePath] = $recipe; + } + } + + public function gen(string $destination): ?string + { + foreach ($this->recipes as $recipe) { + // $find will try to return DocConfig for a given config $name. + $findConfig = function (string $name) use ($recipe): ?DocConfig { + if (array_key_exists($name, $recipe->config)) { + return $recipe->config[$name]; + } + foreach ($recipe->require as $r) { + if (array_key_exists($r, $this->recipes)) { + if (array_key_exists($name, $this->recipes[$r]->config)) { + return $this->recipes[$r]->config[$name]; + } + } + } + foreach ($this->recipes as $r) { + if (array_key_exists($name, $r->config)) { + return $r->config[$name]; + } + } + return null; + }; + $findConfigOverride = function (DocRecipe $recipe, string $name) use (&$findConfigOverride): ?DocConfig { + foreach ($recipe->require as $r) { + if (array_key_exists($r, $this->recipes)) { + if (array_key_exists($name, $this->recipes[$r]->config)) { + return $this->recipes[$r]->config[$name]; + } + } + } + foreach ($recipe->require as $r) { + if (array_key_exists($r, $this->recipes)) { + return $findConfigOverride($this->recipes[$r], $name); + } + } + return null; + }; + // Replace all {{name}} with link to correct config declaration. + $replaceLinks = function (string $comment) use ($findConfig): string { + $output = ''; + $code = false; + foreach (explode("\n", $comment) as $i => $line) { + if (str_starts_with($line, '```') || str_starts_with($line, '~~~')) { + $code = !$code; + } + if ($code) { + $output .= $line; + $output .= "\n"; + continue; + } + $output .= preg_replace_callback('#(\{\{(?[\w_:\-/]+)\}\})#', function ($m) use ($findConfig) { + $name = $m['name']; + $config = $findConfig($name); + if ($config !== null) { + $md = php_to_md($config->recipePath); + $anchor = anchor($name); + return "[$name](/docs/$md#$anchor)"; + } + return "{{" . $name . "}}"; + }, $line); + $output .= "\n"; + } + return $output; + }; + $findTask = function (string $name, bool $searchOtherRecipes = true) use ($recipe): ?DocTask { + if (array_key_exists($name, $recipe->tasks)) { + return $recipe->tasks[$name]; + } + foreach ($recipe->require as $r) { + if (array_key_exists($r, $this->recipes)) { + if (array_key_exists($name, $this->recipes[$r]->tasks)) { + return $this->recipes[$r]->tasks[$name]; + } + } + } + if ($searchOtherRecipes) { + foreach ($this->recipes as $r) { + if (array_key_exists($name, $r->tasks)) { + return $r->tasks[$name]; + } + } + } + return null; + }; + + $title = join(' ', array_map('ucfirst', explode('_', $recipe->recipeName))) . ' Recipe'; + $config = ''; + $tasks = ''; + $intro = <<recipePath'; + ``` + + [Source](/$recipe->recipePath) + + + MD; + if (is_framework_recipe($recipe)) { + $brandName = framework_brand_name($recipe->recipeName); + $typeOfProject = preg_match('/^symfony/i', $recipe->recipeName) ? 'Application' : 'Project'; + $title = "How to Deploy a $brandName $typeOfProject"; + + $intro .= <<group as $taskName) { + $t = $findTask($taskName); + if ($t !== null) { + $intro .= "$ident* {$t->mdLink()} – $t->desc\n"; + if ($t->group !== null) { + $map($t, $ident . ' '); + } + } + } + }; + $deployTask = $findTask('deploy'); + if ($deployTask !== null) { + $intro .= "The [deploy](#deploy) task of **$brandName** consists of:\n"; + $map($deployTask); + } + + $intro .= "\n\n"; + + $artifactBuildTask = $findTask('artifact:build', false); + $artifactDeployTask = $findTask('artifact:deploy', false); + if ($artifactDeployTask !== null && $artifactBuildTask !== null) { + $intro .= "In addition the **$brandName** recipe contains an artifact deployment.\n"; + $intro .= <<set('local', true); + ``` + to your deploy.php or + ```yaml + hosts: + localhost: + local: true + ``` + to your deploy yaml. + + The [artifact:build](#artifact:build) command of **$brandName** consists of: + MD; + $map($artifactBuildTask); + + $intro .= "\n\n The [artifact:deploy](#artifact:deploy) command of **$brandName** consists of:\n"; + + $map($artifactDeployTask); + + $intro .= "\n\n"; + } + } + if (count($recipe->require) > 0) { + if (is_framework_recipe($recipe)) { + $link = recipe_to_md_link($recipe->require[0]); + $intro .= "The $recipe->recipeName recipe is based on the $link recipe.\n"; + } else { + $intro .= "* Requires\n"; + foreach ($recipe->require as $r) { + $link = recipe_to_md_link($r); + $intro .= " * {$link}\n"; + } + } + } + if (!empty($recipe->comment)) { + $intro .= "\n$recipe->comment\n"; + } + if (count($recipe->config) > 0) { + $config .= "## Configuration\n"; + foreach ($recipe->config as $c) { + $config .= "### {$c->name}\n"; + $config .= "[Source](https://github.com/deployphp/deployer/blob/master/{$c->recipePath}#L{$c->lineNumber})\n\n"; + $o = $findConfigOverride($recipe, $c->name); + if ($o !== null) { + $md = php_to_md($o->recipePath); + $anchor = anchor($c->name); + $config .= "Overrides [{$c->name}](/docs/$md#$anchor) from `$o->recipePath`.\n\n"; + } + $config .= $replaceLinks($c->comment); + $config .= "\n"; + if ( + !empty($c->defaultValue) + && $c->defaultValue !== "''" + && $c->defaultValue !== '[]' + ) { + $config .= "```php title=\"Default value\"\n"; + $config .= $c->defaultValue; + $config .= "\n"; + $config .= "```\n"; + } + $config .= "\n\n"; + } + } + if (count($recipe->tasks) > 0) { + $tasks .= "## Tasks\n\n"; + foreach ($recipe->tasks as $t) { + $anchorTag = '{#' . anchor($t->name) . '}'; + $name = title($t->name); + $tasks .= "### $name $anchorTag\n"; + $tasks .= "[Source](https://github.com/deployphp/deployer/blob/master/{$t->recipePath}#L{$t->lineNumber})\n\n"; + $tasks .= add_tailing_dot($t->desc) . "\n\n"; + $tasks .= $replaceLinks($t->comment); + if (is_array($t->group)) { + $tasks .= "\n\n"; + $tasks .= "This task is group task which contains next tasks:\n"; + foreach ($t->group as $taskName) { + $t = $findTask($taskName); + if ($t !== null) { + $tasks .= "* {$t->mdLink()}\n"; + } else { + $tasks .= "* `$taskName`\n"; + } + } + } + $tasks .= "\n\n"; + } + } + + $output = << + + + + # $title + + $intro + $config + $tasks + MD; + + $filePath = "$destination/" . php_to_md($recipe->recipePath); + if (!file_exists(dirname($filePath))) { + mkdir(dirname($filePath), 0o755, true); + } + $output = remove_text_emoji($output); + file_put_contents($filePath, $output); + } + $this->generateRecipesIndex($destination); + $this->generateContribIndex($destination); + return null; + } + + public function generateRecipesIndex(string $destination) + { + $index = "# All Recipes\n\n"; + $list = []; + foreach ($this->recipes as $recipe) { + if (preg_match('/^recipe\/[^\/]+\.php$/', $recipe->recipePath)) { + $name = framework_brand_name($recipe->recipeName); + $list[] = "* [$name Recipe](/docs/recipe/{$recipe->recipeName}.md)"; + } + } + sort($list); + $index .= implode("\n", $list); + file_put_contents("$destination/recipe/README.md", $index); + } + + public function generateContribIndex(string $destination) + { + $index = "# All Contrib Recipes\n\n"; + $list = []; + foreach ($this->recipes as $recipe) { + if (preg_match('/^contrib\/[^\/]+\.php$/', $recipe->recipePath)) { + $name = ucfirst($recipe->recipeName); + $list[] = "* [$name Recipe](/docs/contrib/$recipe->recipeName.md)"; + } + } + sort($list); + $index .= implode("\n", $list); + file_put_contents("$destination/contrib/README.md", $index); + } +} + +function trim_comment(string $line): string +{ + return preg_replace('#^(/\*\*?\s?|\s\*\s?|//\s?)#', '', $line); +} + +function indent(string $text): string +{ + return implode("\n", array_map(function ($line) { + return " " . $line; + }, explode("\n", $text))); +} + +function php_to_md(string $file): string +{ + return preg_replace('#\.php$#', '.md', $file); +} + +function title(string $s): string +{ + return str_replace(':', '\\:', $s); +} + +function anchor(string $s): string +{ + return strtolower(str_replace(':', '-', $s)); +} + +function remove_text_emoji(string $text): string +{ + return preg_replace('/:(bowtie|smile|laughing|blush|smiley|relaxed|smirk|heart_eyes|kissing_heart|kissing_closed_eyes|flushed|relieved|satisfied|grin|wink|stuck_out_tongue_winking_eye|stuck_out_tongue_closed_eyes|grinning|kissing|kissing_smiling_eyes|stuck_out_tongue|sleeping|worried|frowning|anguished|open_mouth|grimacing|confused|hushed|expressionless|unamused|sweat_smile|sweat|disappointed_relieved|weary|pensive|disappointed|confounded|fearful|cold_sweat|persevere|cry|sob|joy|astonished|scream|neckbeard|tired_face|angry|rage|triumph|sleepy|yum|mask|sunglasses|dizzy_face|imp|smiling_imp|neutral_face|no_mouth|innocent|alien|yellow_heart|blue_heart|purple_heart|heart|green_heart|broken_heart|heartbeat|heartpulse|two_hearts|revolving_hearts|cupid|sparkling_heart|sparkles|star|star2|dizzy|boom|collision|anger|exclamation|question|grey_exclamation|grey_question|zzz|dash|sweat_drops|notes|musical_note|fire|hankey|poop|shit|\+1|thumbsup|\-1|thumbsdown|ok_hand|punch|facepunch|fist|v|wave|hand|raised_hand|open_hands|point_up|point_down|point_left|point_right|raised_hands|pray|point_up_2|clap|muscle|metal|fu|walking|runner|running|couple|family|two_men_holding_hands|two_women_holding_hands|dancer|dancers|ok_woman|no_good|information_desk_person|raising_hand|bride_with_veil|person_with_pouting_face|person_frowning|bow|couplekiss|couple_with_heart|massage|haircut|nail_care|boy|girl|woman|man|baby|older_woman|older_man|person_with_blond_hair|man_with_gua_pi_mao|man_with_turban|construction_worker|cop|angel|princess|smiley_cat|smile_cat|heart_eyes_cat|kissing_cat|smirk_cat|scream_cat|crying_cat_face|joy_cat|pouting_cat|japanese_ogre|japanese_goblin|see_no_evil|hear_no_evil|speak_no_evil|guardsman|skull|feet|lips|kiss|droplet|ear|eyes|nose|tongue|love_letter|bust_in_silhouette|busts_in_silhouette|speech_balloon|thought_balloon|feelsgood|finnadie|goberserk|godmode|hurtrealbad|rage1|rage2|rage3|rage4|suspect|trollface|sunny|umbrella|cloud|snowflake|snowman|zap|cyclone|foggy|ocean|cat|dog|mouse|hamster|rabbit|wolf|frog|tiger|koala|bear|pig|pig_nose|cow|boar|monkey_face|monkey|horse|racehorse|camel|sheep|elephant|panda_face|snake|bird|baby_chick|hatched_chick|hatching_chick|chicken|penguin|turtle|bug|honeybee|ant|beetle|snail|octopus|tropical_fish|fish|whale|whale2|dolphin|cow2|ram|rat|water_buffalo|tiger2|rabbit2|dragon|goat|rooster|dog2|pig2|mouse2|ox|dragon_face|blowfish|crocodile|dromedary_camel|leopard|cat2|poodle|paw_prints|bouquet|cherry_blossom|tulip|four_leaf_clover|rose|sunflower|hibiscus|maple_leaf|leaves|fallen_leaf|herb|mushroom|cactus|palm_tree|evergreen_tree|deciduous_tree|chestnut|seedling|blossom|ear_of_rice|shell|globe_with_meridians|sun_with_face|full_moon_with_face|new_moon_with_face|new_moon|waxing_crescent_moon|first_quarter_moon|waxing_gibbous_moon|full_moon|waning_gibbous_moon|last_quarter_moon|waning_crescent_moon|last_quarter_moon_with_face|first_quarter_moon_with_face|moon|earth_africa|earth_americas|earth_asia|volcano|milky_way|partly_sunny|octocat|squirrel|bamboo|gift_heart|dolls|school_satchel|mortar_board|flags|fireworks|sparkler|wind_chime|rice_scene|jack_o_lantern|ghost|santa|christmas_tree|gift|bell|no_bell|tanabata_tree|tada|confetti_ball|balloon|crystal_ball|cd|dvd|floppy_disk|camera|video_camera|movie_camera|computer|tv|iphone|phone|telephone|telephone_receiver|pager|fax|minidisc|vhs|sound|speaker|mute|loudspeaker|mega|hourglass|hourglass_flowing_sand|alarm_clock|watch|radio|satellite|loop|mag|mag_right|unlock|lock|lock_with_ink_pen|closed_lock_with_key|key|bulb|flashlight|high_brightness|low_brightness|electric_plug|battery|calling|email|mailbox|postbox|bath|bathtub|shower|toilet|wrench|nut_and_bolt|hammer|seat|moneybag|yen|dollar|pound|euro|credit_card|money_with_wings|e-mail|inbox_tray|outbox_tray|envelope|incoming_envelope|postal_horn|mailbox_closed|mailbox_with_mail|mailbox_with_no_mail|door|smoking|bomb|gun|hocho|pill|syringe|page_facing_up|page_with_curl|bookmark_tabs|bar_chart|chart_with_upwards_trend|chart_with_downwards_trend|scroll|clipboard|calendar|date|card_index|file_folder|open_file_folder|scissors|pushpin|paperclip|black_nib|pencil2|straight_ruler|triangular_ruler|closed_book|green_book|blue_book|orange_book|notebook|notebook_with_decorative_cover|ledger|books|bookmark|name_badge|microscope|telescope|newspaper|football|basketball|soccer|baseball|tennis|8ball|rugby_football|bowling|golf|mountain_bicyclist|bicyclist|horse_racing|snowboarder|swimmer|surfer|ski|spades|hearts|clubs|diamonds|gem|ring|trophy|musical_score|musical_keyboard|violin|space_invader|video_game|black_joker|flower_playing_cards|game_die|dart|mahjong|clapper|memo|pencil|book|art|microphone|headphones|trumpet|saxophone|guitar|shoe|sandal|high_heel|lipstick|boot|shirt|tshirt|necktie|womans_clothes|dress|running_shirt_with_sash|jeans|kimono|bikini|ribbon|tophat|crown|womans_hat|mans_shoe|closed_umbrella|briefcase|handbag|pouch|purse|eyeglasses|fishing_pole_and_fish|coffee|tea|sake|baby_bottle|beer|beers|cocktail|tropical_drink|wine_glass|fork_and_knife|pizza|hamburger|fries|poultry_leg|meat_on_bone|spaghetti|curry|fried_shrimp|bento|sushi|fish_cake|rice_ball|rice_cracker|rice|ramen|stew|oden|dango|egg|bread|doughnut|custard|icecream|ice_cream|shaved_ice|birthday|cake|cookie|chocolate_bar|candy|lollipop|honey_pot|apple|green_apple|tangerine|lemon|cherries|grapes|watermelon|strawberry|peach|melon|banana|pear|pineapple|sweet_potato|eggplant|tomato|corn|house|house_with_garden|school|office|post_office|hospital|bank|convenience_store|love_hotel|hotel|wedding|church|department_store|european_post_office|city_sunrise|city_sunset|japanese_castle|european_castle|tent|factory|tokyo_tower|japan|mount_fuji|sunrise_over_mountains|sunrise|stars|statue_of_liberty|bridge_at_night|carousel_horse|rainbow|ferris_wheel|fountain|roller_coaster|ship|speedboat|boat|sailboat|rowboat|anchor|rocket|airplane|helicopter|steam_locomotive|tram|mountain_railway|bike|aerial_tramway|suspension_railway|mountain_cableway|tractor|blue_car|oncoming_automobile|car|red_car|taxi|oncoming_taxi|articulated_lorry|bus|oncoming_bus|rotating_light|police_car|oncoming_police_car|fire_engine|ambulance|minibus|truck|train|station|train2|bullettrain_front|bullettrain_side|light_rail|monorail|railway_car|trolleybus|ticket|fuelpump|vertical_traffic_light|traffic_light|warning|construction|beginner|atm|slot_machine|busstop|barber|hotsprings|checkered_flag|crossed_flags|izakaya_lantern|moyai|circus_tent|performing_arts|round_pushpin|triangular_flag_on_post|jp|kr|cn|us|fr|es|it|ru|gb|uk|de|one|two|three|four|five|six|seven|eight|nine|keycap_ten|1234|zero|hash|symbols|arrow_backward|arrow_down|arrow_forward|arrow_left|capital_abcd|abcd|abc|arrow_lower_left|arrow_lower_right|arrow_right|arrow_up|arrow_upper_left|arrow_upper_right|arrow_double_down|arrow_double_up|arrow_down_small|arrow_heading_down|arrow_heading_up|leftwards_arrow_with_hook|arrow_right_hook|left_right_arrow|arrow_up_down|arrow_up_small|arrows_clockwise|arrows_counterclockwise|rewind|fast_forward|information_source|ok|twisted_rightwards_arrows|repeat|repeat_one|new|top|up|cool|free|ng|cinema|koko|signal_strength|u5272|u5408|u55b6|u6307|u6708|u6709|u6e80|u7121|u7533|u7a7a|u7981|sa|restroom|mens|womens|baby_symbol|no_smoking|parking|wheelchair|metro|baggage_claim|accept|wc|potable_water|put_litter_in_its_place|secret|congratulations|m|passport_control|left_luggage|customs|ideograph_advantage|cl|sos|id|no_entry_sign|underage|no_mobile_phones|do_not_litter|non-potable_water|no_bicycles|no_pedestrians|children_crossing|no_entry|eight_spoked_asterisk|eight_pointed_black_star|heart_decoration|vs|vibration_mode|mobile_phone_off|chart|currency_exchange|aries|taurus|gemini|cancer|leo|virgo|libra|scorpius|sagittarius|capricorn|aquarius|pisces|ophiuchus|six_pointed_star|negative_squared_cross_mark|a|b|ab|o2|diamond_shape_with_a_dot_inside|recycle|end|on|soon|clock1|clock130|clock10|clock1030|clock11|clock1130|clock12|clock1230|clock2|clock230|clock3|clock330|clock4|clock430|clock5|clock530|clock6|clock630|clock7|clock730|clock8|clock830|clock9|clock930|heavy_dollar_sign|copyright|registered|tm|x|heavy_exclamation_mark|bangbang|interrobang|o|heavy_multiplication_x|heavy_plus_sign|heavy_minus_sign|heavy_division_sign|white_flower|100|heavy_check_mark|ballot_box_with_check|radio_button|link|curly_loop|wavy_dash|part_alternation_mark|trident|black_square|white_square|white_check_mark|black_square_button|white_square_button|black_circle|white_circle|red_circle|large_blue_circle|large_blue_diamond|large_orange_diamond|small_blue_diamond|small_orange_diamond|small_red_triangle|small_red_triangle_down|shipit):/i', ':​\1:', $text); +} + +function add_tailing_dot(string $sentence): string +{ + if (empty($sentence)) { + return $sentence; + } + if (str_ends_with($sentence, '.')) { + return $sentence; + } + return $sentence . '.'; +} + +function recipe_to_md_link(string $recipe): string +{ + $md = php_to_md($recipe); + $basename = basename($recipe, '.php'); + return "[$basename](/docs/$md)"; +} + +function is_framework_recipe(DocRecipe $recipe): bool +{ + return preg_match('/recipe\/[\w_\d]+\.php$/', $recipe->recipePath) + && !in_array($recipe->recipeName, ['common', 'composer', 'provision'], true); +} + +function framework_brand_name(string $brandName): string +{ + $brandName = preg_replace('/(\w+)(\d)/', '$1 $2', $brandName); + $brandName = preg_replace('/typo 3/', 'TYPO3', $brandName); + $brandName = preg_replace('/yii/', 'Yii2', $brandName); + $brandName = preg_replace('/wordpress/', 'WordPress', $brandName); + $brandName = preg_replace('/_/', ' ', $brandName); + $brandName = preg_replace('/framework/', 'Framework', $brandName); + return ucfirst($brandName); +} diff --git a/vendor/deployer/deployer/src/Documentation/DocRecipe.php b/vendor/deployer/deployer/src/Documentation/DocRecipe.php new file mode 100644 index 0000000..5f24bb6 --- /dev/null +++ b/vendor/deployer/deployer/src/Documentation/DocRecipe.php @@ -0,0 +1,184 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Documentation; + +class DocRecipe +{ + /** + * @var string + */ + public $recipeName; + /** + * @var string + */ + public $recipePath; + /** + * @var string + */ + public $comment; + /** + * @var string[] + */ + public $require = []; + /** + * @var DocConfig[] + */ + public $config = []; + /** + * @var DocTask[] + */ + public $tasks = []; + + public function __construct(string $recipeName, string $recipePath) + { + $this->recipeName = $recipeName; + $this->recipePath = $recipePath; + } + + /** + * @return bool|int + */ + public function parse(string $content) + { + $comment = ''; + $desc = ''; + $currentTask = null; + + $content = str_replace("\r\n", "\n", $content); + + $state = 'root'; + $lines = explode("\n", $content); + + for ($i = 0; $i < count($lines); $i++) { + $line = $lines[$i]; + + if (empty($line)) { + continue; // Skip empty lines + } + + $m = []; + $match = function ($regexp) use ($line, &$m) { + return preg_match("#$regexp#", $line, $m); + }; + switch ($state) { + case 'root': + if ($match('^/\*\*?')) { + $state = 'comment'; + $comment .= trim_comment($line) . "\n"; + break; + } + if ($match('^//')) { + $comment .= trim_comment($line) . "\n"; + break; + } + if ($match('^require.+?[\'"](?.+?)[\'"]')) { + $this->require[] = dirname($this->recipePath) . $m['recipe']; + break; + } + if ($match('^set\([\'"](?[\w_:\-/]+?)[\'"]')) { + $set = new DocConfig(); + $set->name = $m['config_name']; + $set->comment = trim($comment); + $comment = ''; + $set->recipePath = $this->recipePath; + $set->lineNumber = $i + 1; + if (preg_match('#^set\(.+?,\s(?.+?)\);$#', $line, $m)) { + $set->defaultValue = $m['value']; + } + if (preg_match('#^set\(.+?,\s\[$#', $line, $m)) { + $multiLineArray = "[\n"; + $line = $lines[++$i]; + while (!preg_match('/^]/', $line)) { + $multiLineArray .= $line . "\n"; + $line = $lines[++$i]; + } + $multiLineArray .= "]"; + $set->defaultValue = $multiLineArray; + } + if (preg_match('/^set\(.+?, function/', $line, $m)) { + $body = []; + $line = $lines[++$i]; + while (!preg_match('/^}\);$/', $line)) { + $body[] = trim($line); + $line = $lines[++$i]; + } + if (count($body) === 1 && preg_match('/throw new/', $body[0])) { + $set->comment .= "\n:::info Required\nThrows exception if not set.\n:::\n"; + } elseif (count($body) <= 4) { + $set->defaultValue = implode("\n", $body); + } else { + $set->comment .= "\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n"; + } + } + $this->config[$set->name] = $set; + break; + } + if ($match('^desc\([\'"](?.+?)[\'"]\);$')) { + $desc = $m['desc']; + break; + } + if ($match('^task\([\'"](?[\w_:-]+?)[\'"],\s\[$')) { + $task = new DocTask(); + $task->name = $m['task_name']; + $task->desc = $desc; + $task->comment = trim($comment); + $comment = ''; + $task->group = []; + $task->recipePath = $this->recipePath; + $task->lineNumber = $i + 1; + $this->tasks[$task->name] = $task; + $state = 'group_task'; + $currentTask = $task; + break; + } + if ($match('^task\([\'"](?[\w_:-]+?)[\'"],')) { + $task = new DocTask(); + $task->name = $m['task_name']; + $task->desc = $desc; + $task->comment = trim($comment); + $comment = ''; + $task->recipePath = $this->recipePath; + $task->lineNumber = $i + 1; + $this->tasks[$task->name] = $task; + break; + } + if ($match('^<\?php')) { + break; + } + if ($match('^namespace Deployer;$')) { + $this->comment = $comment; + break; + } + + $desc = ''; + $comment = ''; + break; + + case 'comment': + if ($match('\*/\s*$')) { + $state = 'root'; + break; + } + $comment .= trim_comment($line) . "\n"; + break; + + case 'group_task': + if ($match('^\s+\'(?[\w_:-]+?)\',$')) { + $currentTask->group[] = $m['task_name']; + break; + } + $state = 'root'; + break; + } + } + return false; + } +} diff --git a/vendor/deployer/deployer/src/Documentation/DocTask.php b/vendor/deployer/deployer/src/Documentation/DocTask.php new file mode 100644 index 0000000..6da1e1e --- /dev/null +++ b/vendor/deployer/deployer/src/Documentation/DocTask.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Documentation; + +class DocTask +{ + /** + * @var string + */ + public $name; + /** + * @var string + */ + public $desc; + /** + * @var string + */ + public $comment; + /** + * @var array|null + */ + public $group; + /** + * @var string + */ + public $recipePath; + /** + * @var int + */ + public $lineNumber; + + public function mdLink(): string + { + $md = php_to_md($this->recipePath); + $anchor = anchor($this->name); + return "[$this->name](/docs/$md#$anchor)"; + } +} diff --git a/vendor/deployer/deployer/src/Exception/ConfigurationException.php b/vendor/deployer/deployer/src/Exception/ConfigurationException.php new file mode 100644 index 0000000..91b313d --- /dev/null +++ b/vendor/deployer/deployer/src/Exception/ConfigurationException.php @@ -0,0 +1,13 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Exception; + +class ConfigurationException extends \RuntimeException {} diff --git a/vendor/deployer/deployer/src/Exception/Exception.php b/vendor/deployer/deployer/src/Exception/Exception.php new file mode 100644 index 0000000..1aa73be --- /dev/null +++ b/vendor/deployer/deployer/src/Exception/Exception.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Exception; + +use Throwable; + +class Exception extends \Exception +{ + /** + * @var string + */ + private static $taskSourceLocation = ''; + /** + * @var string + */ + private $taskFilename = ''; + /** + * @var int|mixed + */ + private $taskLineNumber = 0; + + public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null) + { + if (function_exists('debug_backtrace')) { + $trace = debug_backtrace(); + foreach ($trace as $t) { + if (!empty($t['file']) && $t['file'] === self::$taskSourceLocation) { + $this->taskFilename = basename($t['file']); + $this->taskLineNumber = $t['line']; + break; + } + } + } + parent::__construct($message, $code, $previous); + } + + public static function setTaskSourceLocation(string $filepath): void + { + self::$taskSourceLocation = $filepath; + } + + public function getTaskFilename(): string + { + return $this->taskFilename; + } + + public function getTaskLineNumber(): int + { + return $this->taskLineNumber; + } + + public function setTaskFilename(string $taskFilename): void + { + $this->taskFilename = $taskFilename; + } + + public function setTaskLineNumber(int $taskLineNumber): void + { + $this->taskLineNumber = $taskLineNumber; + } +} diff --git a/vendor/deployer/deployer/src/Exception/GracefulShutdownException.php b/vendor/deployer/deployer/src/Exception/GracefulShutdownException.php new file mode 100644 index 0000000..a5b0bb3 --- /dev/null +++ b/vendor/deployer/deployer/src/Exception/GracefulShutdownException.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Exception; + +/** + * Then this exception thrown, it will not trigger "fail" callback. + * + * fail('deploy', 'deploy:failed'); + * + * task('deploy', function () { + * throw new GracefulShutdownException(...); + * }); + * + * In example above task `deploy:failed` will not be called. + */ +class GracefulShutdownException extends Exception +{ + public const EXIT_CODE = 42; +} diff --git a/vendor/deployer/deployer/src/Exception/HttpieException.php b/vendor/deployer/deployer/src/Exception/HttpieException.php new file mode 100644 index 0000000..c5a15b1 --- /dev/null +++ b/vendor/deployer/deployer/src/Exception/HttpieException.php @@ -0,0 +1,13 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Exception; + +class HttpieException extends \RuntimeException {} diff --git a/vendor/deployer/deployer/src/Exception/RunException.php b/vendor/deployer/deployer/src/Exception/RunException.php new file mode 100644 index 0000000..3d638df --- /dev/null +++ b/vendor/deployer/deployer/src/Exception/RunException.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Exception; + +use Deployer\Host\Host; +use Symfony\Component\Process\Process; + +class RunException extends Exception +{ + /** + * @var Host + */ + private $host; + /** + * @var string + */ + private $command; + /** + * @var int + */ + private $exitCode; + /** + * @var string + */ + private $output; + /** + * @var string + */ + private $errorOutput; + + public function __construct( + Host $host, + string $command, + int $exitCode, + string $output, + string $errorOutput, + ) { + $this->host = $host; + $this->command = $command; + $this->exitCode = $exitCode; + $this->output = $output; + $this->errorOutput = $errorOutput; + + $message = sprintf('The command "%s" failed.', $command); + parent::__construct($message, $exitCode); + } + + public function getHost(): Host + { + return $this->host; + } + + public function getCommand(): string + { + return $this->command; + } + + public function getExitCode(): int + { + return $this->exitCode; + } + + public function getExitCodeText(): string + { + return Process::$exitCodes[$this->exitCode] ?? 'Unknown error'; + } + + public function getOutput(): string + { + return $this->output; + } + + public function getErrorOutput(): string + { + return $this->errorOutput; + } +} diff --git a/vendor/deployer/deployer/src/Exception/SchemaException.php b/vendor/deployer/deployer/src/Exception/SchemaException.php new file mode 100644 index 0000000..33e0338 --- /dev/null +++ b/vendor/deployer/deployer/src/Exception/SchemaException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Exception; + +use Throwable; + +class SchemaException extends \RuntimeException +{ + public function __construct(string $message = "", ?Throwable $previous = null) + { + parent::__construct($message, 0, $previous); + } +} diff --git a/vendor/deployer/deployer/src/Exception/TimeoutException.php b/vendor/deployer/deployer/src/Exception/TimeoutException.php new file mode 100644 index 0000000..06a2c6d --- /dev/null +++ b/vendor/deployer/deployer/src/Exception/TimeoutException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Exception; + +class TimeoutException extends Exception +{ + public function __construct( + string $command, + ?float $timeout, + ) { + $message = sprintf('The command "%s" exceeded the timeout of %s seconds.', $command, $timeout); + parent::__construct($message, 1); + } +} diff --git a/vendor/deployer/deployer/src/Exception/WillAskUser.php b/vendor/deployer/deployer/src/Exception/WillAskUser.php new file mode 100644 index 0000000..e170dbf --- /dev/null +++ b/vendor/deployer/deployer/src/Exception/WillAskUser.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Exception; + +class WillAskUser extends Exception +{ + public static bool $noAsk = false; + + public function __construct(string $message) + { + parent::__construct($message); + } +} diff --git a/vendor/deployer/deployer/src/Executor/Master.php b/vendor/deployer/deployer/src/Executor/Master.php new file mode 100644 index 0000000..2c60d32 --- /dev/null +++ b/vendor/deployer/deployer/src/Executor/Master.php @@ -0,0 +1,320 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Executor; + +use Deployer\Deployer; +use Deployer\Host\Host; +use Deployer\Host\HostCollection; +use Deployer\Logger\Logger; +use Deployer\Selector\Selector; +use Deployer\Ssh\IOArguments; +use Deployer\Task\Context; +use Deployer\Task\Task; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\Process; + +const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; + +function spinner(string $message = ''): string +{ + $frame = FRAMES[(int) ((int) (new \DateTime())->format('u') / 1e5) % count(FRAMES)]; + return " $frame $message\r"; +} + +class Master +{ + private HostCollection $hosts; + private InputInterface $input; + private OutputInterface $output; + private Logger $logger; + private string|false $phpBin; + + public function __construct( + HostCollection $hosts, + InputInterface $input, + OutputInterface $output, + Logger $logger, + ) { + $this->hosts = $hosts; + $this->input = $input; + $this->output = $output; + $this->logger = $logger; + $this->phpBin = (new PhpExecutableFinder())->find(); + } + + /** + * @param Task[] $tasks + * @param Host[] $hosts + */ + public function run(array $tasks, array $hosts, ?Planner $plan = null): int + { + $globalLimit = (int) $this->input->getOption('limit') ?: count($hosts); + + foreach ($tasks as $task) { + if (!$plan) { + $this->logger->startTask($task); + } + + $plannedHosts = $hosts; + + $limit = min($globalLimit, $task->getLimit() ?? $globalLimit); + + if ($task->isOnce()) { + $plannedHosts = []; + foreach ($hosts as $currentHost) { + if (Selector::apply($task->getSelector(), $currentHost)) { + $plannedHosts[] = $currentHost; + break; + } + } + } elseif ($task->isOncePerNode()) { + $plannedHosts = []; + foreach ($hosts as $currentHost) { + if (Selector::apply($task->getSelector(), $currentHost)) { + $nodeLabel = $currentHost->getHostname(); + $labels = $currentHost->config()->get('labels', []); + if (is_array($labels) && array_key_exists('node', $labels)) { + $nodeLabel = $labels['node']; + } + if (array_key_exists($nodeLabel, $plannedHosts)) { + continue; + } + $plannedHosts[$nodeLabel] = $currentHost; + } + } + } + + if ($limit === 1 || count($plannedHosts) === 1) { + foreach ($plannedHosts as $currentHost) { + if (!Selector::apply($task->getSelector(), $currentHost)) { + if ($plan) { + $plan->commit([], $task); + } + continue; + } + + if ($plan) { + $plan->commit([$currentHost], $task); + continue; + } + + $exitCode = $this->runTask($task, [$currentHost]); + if ($exitCode !== 0) { + return $exitCode; + } + } + } else { + foreach (array_chunk($plannedHosts, $limit) as $chunk) { + $selectedHosts = []; + foreach ($chunk as $currentHost) { + if (Selector::apply($task->getSelector(), $currentHost)) { + $selectedHosts[] = $currentHost; + } + } + + if ($plan) { + $plan->commit($selectedHosts, $task); + continue; + } + + $exitCode = $this->runTask($task, $selectedHosts); + if ($exitCode !== 0) { + return $exitCode; + } + } + } + + if (!$plan) { + $this->logger->endTask($task); + } + } + + return 0; + } + + /** + * @param Host[] $hosts + */ + private function runTask(Task $task, array $hosts): int + { + if (getenv('DEPLOYER_LOCAL_WORKER') === 'true') { + // This allows to code coverage all recipe, + // as well as speedup tests by not spawning + // lots of processes. Also there is a few tests + // what runs with workers for tests subprocess + // communications. + foreach ($hosts as $host) { + $worker = new Worker(Deployer::get()); + $exitCode = $worker->execute($task, $host); + if ($exitCode !== 0) { + $this->logger->endTask($task); + return $exitCode; + } + } + return 0; + } + + $server = new Server('127.0.0.1', 0, $this->output); + $authToken = bin2hex(random_bytes(16)); + $server->setAuthToken($authToken); + + /** @var Process[] $processes */ + $processes = []; + + $server->afterRun(function (int $port) use (&$processes, $hosts, $task, $authToken) { + foreach ($hosts as $host) { + $processes[] = $this->createProcess($host, $task, $port, $authToken); + } + + foreach ($processes as $process) { + $process->start(); + } + }); + + $echoCallback = function (string $output) { + $output = preg_replace('/\n$/', '', $output); + if (strlen($output) !== 0) { + $this->output->writeln($output); + } + }; + + $server->ticker(function () use (&$processes, $server, $echoCallback) { + $this->gatherOutput($processes, $echoCallback); + if ($this->output->isDecorated() && !getenv('CI')) { + $this->output->write(spinner()); + } + if ($this->allFinished($processes)) { + $server->stop(); + } + }); + + $server->router(function (string $path, array $payload) { + switch ($path) { + case '/load': + ['host' => $host] = $payload; + + $host = $this->hosts->get($host); + $config = $host->config()->persist(); + + return new Response(200, $config); + + case '/save': + ['host' => $host, 'config' => $config] = $payload; + + $host = $this->hosts->get($host); + $host->config()->update($config); + + return new Response(200, true); + + case '/proxy': + ['host' => $host, 'func' => $func, 'arguments' => $arguments] = $payload; + + $allowedFunctions = [ + 'Deployer\ask', + 'Deployer\askChoice', + 'Deployer\askConfirmation', + 'Deployer\askHiddenResponse', + ]; + if (!in_array($func, $allowedFunctions, true)) { + return new Response(403, ['error' => "Function not allowed: $func"]); + } + + Context::push(new Context($this->hosts->get($host))); + $answer = call_user_func($func, ...$arguments); + Context::pop(); + + return new Response(200, $answer); + + default: + return new Response(404, null); + } + }); + + $server->run(); + + if ($this->output->isDecorated() && !getenv('CI')) { + $this->output->write(" \r"); // clear spinner + } + $this->gatherOutput($processes, $echoCallback); + + if ($this->cumulativeExitCode($processes) !== 0) { + $this->logger->endTask($task); + } + + return $this->cumulativeExitCode($processes); + } + + protected function createProcess(Host $host, Task $task, int $port, string $authToken): Process + { + $command = [ + $this->phpBin, DEPLOYER_BIN, + 'worker', '--port', $port, + '--task', $task, + '--host', $host->getAlias(), + ]; + $command = array_merge($command, IOArguments::collect($this->input, $this->output)); + if ($task->isVerbose() && $this->output->getVerbosity() === OutputInterface::VERBOSITY_NORMAL) { + $command[] = '-v'; + } + if ($this->output->isDebug()) { + $this->output->writeln("[$host] " . join(' ', $command)); + } + $process = new Process($command); + $process->setEnv(['DEPLOYER_MASTER_TOKEN' => $authToken]); + return $process; + } + + /** + * @param Process[] $processes + */ + protected function allFinished(array $processes): bool + { + foreach ($processes as $process) { + if (!$process->isTerminated()) { + return false; + } + } + return true; + } + + /** + * @param Process[] $processes + */ + protected function gatherOutput(array $processes, callable $callback): void + { + foreach ($processes as $process) { + $output = $process->getIncrementalOutput(); + if (strlen($output) !== 0) { + $callback($output); + } + + $errorOutput = $process->getIncrementalErrorOutput(); + if (strlen($errorOutput) !== 0) { + $callback($errorOutput); + } + } + } + + /** + * @param Process[] $processes + */ + protected function cumulativeExitCode(array $processes): int + { + foreach ($processes as $process) { + if ($process->getExitCode() > 0) { + return $process->getExitCode(); + } + } + return 0; + } +} diff --git a/vendor/deployer/deployer/src/Executor/Planner.php b/vendor/deployer/deployer/src/Executor/Planner.php new file mode 100644 index 0000000..801b65e --- /dev/null +++ b/vendor/deployer/deployer/src/Executor/Planner.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Executor; + +use Deployer\Host\Host; +use Deployer\Task\Task; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Output\OutputInterface; + +class Planner +{ + /** + * @var Table + */ + private $table; + /** + * @var array + */ + private $template; + + /** + * Planner constructor. + * + * @param Host[] $hosts + */ + public function __construct(OutputInterface $output, array $hosts) + { + $headers = []; + $this->template = []; + foreach ($hosts as $host) { + $headers[] = $host->getTag(); + $this->template[] = $host->getAlias(); + } + $this->table = new Table($output); + $this->table->setHeaders($headers); + $this->table->setStyle('box'); + } + + /** + * @param Host[] $hosts + */ + public function commit(array $hosts, Task $task): void + { + $row = []; + foreach ($this->template as $alias) { + $on = "-"; + foreach ($hosts as $host) { + if ($alias === $host->getAlias()) { + $on = $task->getName(); + break; + } + } + $row[] = $on; + } + $this->table->addRow($row); + } + + public function render() + { + $this->table->render(); + } +} diff --git a/vendor/deployer/deployer/src/Executor/Response.php b/vendor/deployer/deployer/src/Executor/Response.php new file mode 100644 index 0000000..22865c5 --- /dev/null +++ b/vendor/deployer/deployer/src/Executor/Response.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Executor; + +class Response +{ + private int $status; + private mixed $body; + + public function __construct(int $status, mixed $body) + { + $this->status = $status; + $this->body = $body; + } + + public function getStatus(): int + { + return $this->status; + } + + public function getBody(): mixed + { + return $this->body; + } +} diff --git a/vendor/deployer/deployer/src/Executor/Server.php b/vendor/deployer/deployer/src/Executor/Server.php new file mode 100644 index 0000000..811b963 --- /dev/null +++ b/vendor/deployer/deployer/src/Executor/Server.php @@ -0,0 +1,334 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Executor; + +use Closure; +use Deployer\Exception\Exception; +use Symfony\Component\Console\Output\OutputInterface; + +class Server +{ + private string $host; + private int $port; + private OutputInterface $output; + private bool $stop = false; + + /** + * @var ?resource + */ + private $socket; + + /** + * Each client is tracked with its socket, read buffer, and connection timestamp. + * @var array + */ + private array $clients = []; + + private int $nextClientId = 0; + + private Closure $afterCallback; + private Closure $tickerCallback; + private Closure $routerCallback; + + private ?string $authToken = null; + + /** + * Timeout in seconds for idle client connections. + */ + private float $clientTimeout = 30.0; + + private const REASON_PHRASES = [ + 200 => 'OK', + 403 => 'Forbidden', + 404 => 'Not Found', + 500 => 'Internal Server Error', + ]; + + public function __construct(string $host, int $port, OutputInterface $output) + { + $this->host = $host; + $this->port = $port; + $this->output = $output; + } + + public function run(): void + { + try { + $this->socket = $this->createServerSocket(); + $this->updatePort(); + if ($this->output->isDebug()) { + $this->output->writeln("[master] Starting server at http://{$this->host}:{$this->port}"); + } + + ($this->afterCallback)($this->port); + + while (true) { + $this->acceptNewConnections(); + $this->handleClientRequests(); + + // Prevent CPU exhaustion and 60fps ticker. + usleep(16_000); // 16ms + + ($this->tickerCallback)(); + + if ($this->stop) { + break; + } + } + + if ($this->output->isDebug()) { + $this->output->writeln("[master] Stopping server at http://{$this->host}:{$this->port}"); + } + } finally { + foreach ($this->clients as $id => $client) { + $this->closeClient($id); + } + if (isset($this->socket)) { + fclose($this->socket); + } + } + } + + /** + * @return resource + * @throws Exception + */ + private function createServerSocket() + { + $server = stream_socket_server("tcp://{$this->host}:{$this->port}", $errno, $errstr); + if (!$server) { + throw new Exception("Socket creation failed: $errstr ($errno)"); + } + + if (!stream_set_blocking($server, false)) { + throw new Exception("Failed to set server socket to non-blocking mode"); + } + + return $server; + } + + private function updatePort(): void + { + $name = stream_socket_get_name($this->socket, false); + if ($name) { + [, $port] = explode(':', $name); + $this->port = (int) $port; + } else { + throw new Exception("Failed to get the assigned port"); + } + } + + private function acceptNewConnections(): void + { + while ($newClientSocket = @stream_socket_accept($this->socket, 0)) { + if (!stream_set_blocking($newClientSocket, false)) { + fclose($newClientSocket); + continue; + } + $id = $this->nextClientId++; + $this->clients[$id] = [ + 'socket' => $newClientSocket, + 'buffer' => '', + 'connectedAt' => microtime(true), + ]; + } + } + + private function handleClientRequests(): void + { + foreach ($this->clients as $id => $client) { + $socket = $client['socket']; + + if (feof($socket)) { + $this->closeClient($id); + continue; + } + + // Read available data into the buffer. + $data = @fread($socket, 65536); + if ($data !== false && $data !== '') { + $this->clients[$id]['buffer'] .= $data; + } + + // Check if we have a complete request. + $buffer = $this->clients[$id]['buffer']; + if (!self::isCompleteRequest($buffer)) { + // Check for idle timeout. + if (microtime(true) - $client['connectedAt'] > $this->clientTimeout) { + $this->closeClient($id); + } + continue; + } + + // Process the complete request. + try { + [$path, $payload, $headers] = self::parseRequest($buffer); + + if ($this->authToken !== null) { + $provided = $headers['authorization'] ?? ''; + if ($provided !== "Bearer {$this->authToken}") { + $this->sendResponse($socket, new Response(403, ['error' => 'Forbidden'])); + continue; + } + } + + $response = ($this->routerCallback)($path, $payload); + $this->sendResponse($socket, $response); + } catch (\Throwable $e) { + $errorResponse = new Response(500, ['error' => $e->getMessage()]); + try { + $this->sendResponse($socket, $errorResponse); + } catch (\Throwable) { + // Best effort — socket may be broken. + } + } finally { + $this->closeClient($id); + } + } + } + + /** + * Check if the buffer contains a complete HTTP request + * (headers terminated by \r\n\r\n, and body length matches Content-Length). + */ + public static function isCompleteRequest(string $buffer): bool + { + $headerEnd = strpos($buffer, "\r\n\r\n"); + if ($headerEnd === false) { + return false; + } + + $headers = substr($buffer, 0, $headerEnd); + $bodyStart = $headerEnd + 4; + + // Extract Content-Length from headers. + if (preg_match('/^Content-Length:\s*(\d+)/mi', $headers, $matches)) { + $contentLength = (int) $matches[1]; + return strlen($buffer) - $bodyStart >= $contentLength; + } + + // No Content-Length header — request is complete after headers. + return true; + } + + /** + * Parse a complete HTTP request into path, payload, and headers. + * + * @return array{0: string, 1: mixed, 2: array} + */ + public static function parseRequest(string $request): array + { + $headerEnd = strpos($request, "\r\n\r\n"); + if ($headerEnd === false) { + throw new Exception("Malformed request: no header terminator found"); + } + + $headerSection = substr($request, 0, $headerEnd); + $body = substr($request, $headerEnd + 4); + + $lines = explode("\r\n", $headerSection); + $requestLine = $lines[0]; + $parts = explode(' ', $requestLine); + if (count($parts) !== 3) { + throw new Exception("Malformed request line: $requestLine"); + } + $path = $parts[1]; + + $headers = []; + for ($i = 1; $i < count($lines); $i++) { + $line = $lines[$i]; + if (str_contains($line, ':')) { + [$key, $value] = explode(':', $line, 2); + $headers[strtolower(trim($key))] = trim($value); + } + } + + if (empty($headers['content-type']) || $headers['content-type'] !== 'application/json') { + throw new Exception("Malformed request: invalid Content-Type"); + } + + // Trim body to Content-Length if present (ignore trailing data). + if (isset($headers['content-length'])) { + $body = substr($body, 0, (int) $headers['content-length']); + } + + $payload = json_decode($body, true, flags: JSON_THROW_ON_ERROR); + return [$path, $payload, $headers]; + } + + /** + * @param resource $socket + */ + private function sendResponse($socket, Response $response): void + { + $code = $response->getStatus(); + $reason = self::REASON_PHRASES[$code] ?? 'Unknown'; + $content = json_encode($response->getBody(), flags: JSON_PRETTY_PRINT); + $data = "HTTP/1.1 $code $reason\r\n" + . "Content-Type: application/json\r\n" + . "Content-Length: " . strlen($content) . "\r\n" + . "Connection: close\r\n\r\n" + . $content; + + self::writeAll($socket, $data); + } + + /** + * Write all data to socket, handling partial writes. + * + * @param resource $socket + */ + public static function writeAll($socket, string $data): void + { + $written = 0; + $len = strlen($data); + while ($written < $len) { + $bytes = @fwrite($socket, substr($data, $written)); + if ($bytes === false || $bytes === 0) { + throw new Exception('Socket write failed'); + } + $written += $bytes; + } + } + + private function closeClient(int $id): void + { + if (isset($this->clients[$id])) { + fclose($this->clients[$id]['socket']); + unset($this->clients[$id]); + } + } + + public function afterRun(Closure $callback): void + { + $this->afterCallback = $callback; + } + + public function ticker(Closure $callback): void + { + $this->tickerCallback = $callback; + } + + public function router(Closure $callback): void + { + $this->routerCallback = $callback; + } + + public function setAuthToken(string $token): void + { + $this->authToken = $token; + } + + public function stop(): void + { + $this->stop = true; + } +} diff --git a/vendor/deployer/deployer/src/Executor/Worker.php b/vendor/deployer/deployer/src/Executor/Worker.php new file mode 100644 index 0000000..75cae14 --- /dev/null +++ b/vendor/deployer/deployer/src/Executor/Worker.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Executor; + +use Deployer\Deployer; +use Deployer\Exception\Exception; +use Deployer\Exception\GracefulShutdownException; +use Deployer\Exception\RunException; +use Deployer\Host\Host; +use Deployer\Task\Context; +use Deployer\Task\Task; +use Throwable; + +class Worker +{ + private Deployer $deployer; + + public function __construct(Deployer $deployer) + { + $this->deployer = $deployer; + } + + public function execute(Task $task, Host $host): int + { + try { + Exception::setTaskSourceLocation($task->getSourceLocation()); + + $context = new Context($host); + $task->run($context); + + if ($task->getName() !== 'connect') { + $this->deployer->logger->endOnHost($host); + } + return 0; + } catch (Throwable $e) { + $this->deployer->logger->renderException($e, $host); + if ($e instanceof GracefulShutdownException) { + return GracefulShutdownException::EXIT_CODE; + } + if ($e instanceof RunException) { + return $e->getExitCode(); + } + return 255; + } + } +} diff --git a/vendor/deployer/deployer/src/Host/Host.php b/vendor/deployer/deployer/src/Host/Host.php new file mode 100644 index 0000000..f4a476e --- /dev/null +++ b/vendor/deployer/deployer/src/Host/Host.php @@ -0,0 +1,301 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Host; + +use Deployer\Configuration; +use Deployer\Deployer; +use Deployer\Exception\ConfigurationException; +use Deployer\Exception\Exception; +use Deployer\Task\Context; + +use function Deployer\Support\colorize_host; +use function Deployer\Support\parse_home_dir; + +class Host +{ + private Configuration $config; + + public function __construct(string $hostname) + { + $parent = Deployer::get()->config; + $this->config = new Configuration($parent); + $this->set('#alias', $hostname); + $this->set('hostname', preg_replace('/\/.+$/', '', $hostname)); + } + + public function __toString(): string + { + return $this->getAlias(); + } + + public function config(): Configuration + { + return $this->config; + } + + public function set(string $name, mixed $value): self + { + if ($name === 'alias') { + throw new ConfigurationException("Can not update alias of the host.\nThis will change only host own alias,\nbut not the key it is stored in HostCollection."); + } + if ($name === '#alias') { + $name = 'alias'; + } + $this->config->set($name, $value); + return $this; + } + + public function add(string $name, array $value): self + { + $this->config->add($name, $value); + return $this; + } + + public function has(string $name): bool + { + return $this->config->has($name); + } + + public function hasOwn(string $name): bool + { + return $this->config->hasOwn($name); + } + + public function get(string $name, mixed $default = null): mixed + { + return $this->config->get($name, $default); + } + + public function getAlias(): ?string + { + return $this->config->get('alias', null); + } + + public function setTag(string $tag): self + { + $this->config->set('tag', $tag); + return $this; + } + + public function getTag(): ?string + { + return $this->config->get('tag', colorize_host($this->getAlias())); + } + + public function setHostname(string $hostname): self + { + $this->config->set('hostname', $hostname); + return $this; + } + + public function getHostname(): ?string + { + return $this->config->get('hostname', null); + } + + public function setRemoteUser(string $user): self + { + $this->config->set('remote_user', $user); + return $this; + } + + public function getRemoteUser(): ?string + { + return $this->config->get('remote_user', null); + } + + public function setPort(int|string|null $port): self + { + $this->config->set('port', $port); + return $this; + } + + public function getPort(): int|string|null + { + return $this->config->get('port', null); + } + + public function setConfigFile(string $file): self + { + $this->config->set('config_file', $file); + return $this; + } + + public function getConfigFile(): ?string + { + return $this->config->get('config_file', null); + } + + public function setIdentityFile(string $file): self + { + $this->config->set('identity_file', $file); + return $this; + } + + public function getIdentityFile(): ?string + { + return $this->config->get('identity_file', null); + } + + public function setForwardAgent(bool $on): self + { + $this->config->set('forward_agent', $on); + return $this; + } + + public function getForwardAgent(): ?bool + { + return $this->config->get('forward_agent', null); + } + + public function setSshMultiplexing(bool $on): self + { + $this->config->set('ssh_multiplexing', $on); + return $this; + } + + public function getSshMultiplexing(): ?bool + { + return $this->config->get('ssh_multiplexing', null); + } + + public function setShell(string $command): self + { + $this->config->set('shell', $command); + return $this; + } + + public function getShell(): ?string + { + return $this->config->get('shell', null); + } + + public function setShellPath(string $path): self + { + $this->config->set('shell_path', $path); + return $this; + } + + public function getShellPath(): ?string + { + return $this->config->get('shell_path', null); + } + + public function setDeployPath(string $path): self + { + $this->config->set('deploy_path', $path); + return $this; + } + + public function getDeployPath(): ?string + { + return $this->config->get('deploy_path', null); + } + + public function setLabels(array $labels): self + { + $this->config->set('labels', $labels); + return $this; + } + + public function addLabels(array $labels): self + { + $existingLabels = $this->getLabels() ?? []; + $this->setLabels(array_replace_recursive($existingLabels, $labels)); + return $this; + } + + public function getLabels(): ?array + { + return $this->config->get('labels', null); + } + + public function setSshArguments(array $args): self + { + $this->config->set('ssh_arguments', $args); + return $this; + } + + public function getSshArguments(): ?array + { + return $this->config->get('ssh_arguments', null); + } + + public function setSshControlPath(string $path): self + { + $this->config->set('ssh_control_path', $path); + return $this; + } + + public function getSshControlPath(): string + { + return $this->config->get('ssh_control_path', $this->generateControlPath()); + } + + private function generateControlPath(): string + { + $C = $this->getHostname(); + if ($this->has('remote_user')) { + $C = $this->getRemoteUser() . '@' . $C; + } + if ($this->has('port')) { + $C .= ':' . $this->getPort(); + } + + // In case of CI environment, lets use shared memory. + if (getenv('CI') && is_writable('/dev/shm')) { + return "/dev/shm/$C"; + } + + return "~/.ssh/$C"; + } + + public function connectionString(): string + { + if ($this->get('remote_user', '') !== '') { + return $this->get('remote_user') . '@' . $this->get('hostname'); + } + return $this->get('hostname'); + } + + /** + * @return string[] + */ + public function connectionOptions(): array + { + $options = []; + if ($this->has('ssh_arguments')) { + foreach ($this->getSshArguments() as $arg) { + $options = array_merge($options, explode(' ', $arg)); + } + } + if ($this->has('port')) { + $options = array_merge($options, ['-p', $this->getPort()]); + } + if ($this->has('config_file')) { + $options = array_merge($options, ['-F', parse_home_dir($this->getConfigFile())]); + } + if ($this->has('identity_file')) { + $options = array_merge($options, ['-i', parse_home_dir($this->getIdentityFile())]); + } + if ($this->has('forward_agent') && $this->getForwardAgent()) { + $options = array_merge($options, ['-A']); + } + if ($this->has('ssh_multiplexing') && $this->getSshMultiplexing()) { + $options = array_merge($options, [ + '-o', 'ControlMaster=auto', + '-o', 'ControlPersist=60', + '-o', 'ControlPath=' . $this->getSshControlPath(), + ]); + } + return $options; + } +} diff --git a/vendor/deployer/deployer/src/Host/HostCollection.php b/vendor/deployer/deployer/src/Host/HostCollection.php new file mode 100644 index 0000000..e131a0c --- /dev/null +++ b/vendor/deployer/deployer/src/Host/HostCollection.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Host; + +use Deployer\Collection\Collection; + +/** + * @method Host get($name) + * @method Host[] getIterator() + */ +class HostCollection extends Collection +{ + protected function notFound(string $name): \InvalidArgumentException + { + return new \InvalidArgumentException("Host \"$name\" not found."); + } +} diff --git a/vendor/deployer/deployer/src/Host/Localhost.php b/vendor/deployer/deployer/src/Host/Localhost.php new file mode 100644 index 0000000..c758d7e --- /dev/null +++ b/vendor/deployer/deployer/src/Host/Localhost.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Host; + +class Localhost extends Host +{ + public function __construct(string $hostname = 'localhost') + { + parent::__construct($hostname); + } +} diff --git a/vendor/deployer/deployer/src/Host/Range.php b/vendor/deployer/deployer/src/Host/Range.php new file mode 100644 index 0000000..db50210 --- /dev/null +++ b/vendor/deployer/deployer/src/Host/Range.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Host; + +class Range +{ + public const PATTERN = '/\[(.+?)\]/'; + + public static function expand(array $hostnames): array + { + $expanded = []; + foreach ($hostnames as $hostname) { + if (preg_match(self::PATTERN, $hostname, $matches)) { + [$start, $end] = explode(':', $matches[1]); + $zeroBased = (bool) preg_match('/^0[1-9]/', $start); + + foreach (range($start, $end) as $i) { + $expanded[] = preg_replace(self::PATTERN, self::format((string) $i, $zeroBased), $hostname); + } + } else { + $expanded[] = $hostname; + } + } + + return $expanded; + } + + private static function format(string $i, bool $zeroBased): string + { + if ($zeroBased) { + return strlen($i) === 1 ? "0$i" : $i; + } else { + return $i; + } + } +} diff --git a/vendor/deployer/deployer/src/Import/Import.php b/vendor/deployer/deployer/src/Import/Import.php new file mode 100644 index 0000000..318404e --- /dev/null +++ b/vendor/deployer/deployer/src/Import/Import.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Import; + +use Deployer\Exception\Exception; + +class Import +{ + /** + * @param string|string[] $paths + * @throws Exception + */ + public static function import(mixed $paths): void + { + if (!is_array($paths)) { + $paths = [$paths]; + } + foreach ($paths as $path) { + if (preg_match('/\.php$/i', $path)) { + // Prevent variable leak into deploy.php file + call_user_func(function () use ($path) { + // Reorder autoload stack + $originStack = spl_autoload_functions(); + + require $path; + + $newStack = spl_autoload_functions(); + if ($originStack[0] !== $newStack[0]) { + foreach (array_reverse($originStack) as $loader) { + spl_autoload_unregister($loader); + spl_autoload_register($loader, true, true); + } + } + }); + } elseif (preg_match('/\.maml$/i', $path)) { + $recipe = new MamlRecipe($path); + $recipe->run(); + } elseif (preg_match('/\.ya?ml$/i', $path)) { + YamlRecipe::exec($path); + } else { + throw new Exception("Unknown file format: $path"); + } + } + } +} diff --git a/vendor/deployer/deployer/src/Import/MamlRecipe.php b/vendor/deployer/deployer/src/Import/MamlRecipe.php new file mode 100644 index 0000000..c46efaf --- /dev/null +++ b/vendor/deployer/deployer/src/Import/MamlRecipe.php @@ -0,0 +1,485 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Import; + +use Deployer\Exception\Exception; +use Deployer\Exception\SchemaException; +use Maml\Ast\ArrayNode; +use Maml\Ast\BooleanNode; +use Maml\Ast\ObjectNode; +use Maml\Ast\Property; +use Maml\Ast\RawStringNode; +use Maml\Ast\Span; +use Maml\Ast\StringNode; +use Maml\Maml; +use Maml\Schema\S; +use Maml\Schema\SchemaType; + +use function Deployer\after; +use function Deployer\before; +use function Deployer\cd; +use function Deployer\download; +use function Deployer\fail; +use function Deployer\host; +use function Deployer\localhost; +use function Deployer\run; +use function Deployer\runLocally; +use function Deployer\set; +use function Deployer\task; +use function Deployer\upload; + +class MamlRecipe +{ + private string $filename; + private string $content; + + public function __construct(string $path) + { + $this->filename = basename($path); + $this->content = file_get_contents($path, true); + } + + public static function schema(): SchemaType + { + $cd = S::object([ + 'cd' => S::string(), + ]); + + $run = S::object([ + 'run' => S::string(), + 'cd' => S::optional(S::string()), + 'cwd' => S::optional(S::string()), + 'env' => S::optional(S::map(S::string())), + 'secrets' => S::optional(S::map(S::string())), + 'nothrow' => S::optional(S::boolean()), + 'forceOutput' => S::optional(S::boolean()), + 'timeout' => S::optional(S::number()), + 'idleTimeout' => S::optional(S::number()), + ]); + + $runLocally = S::object([ + 'runLocally' => S::string(), + 'cwd' => S::optional(S::string()), + 'timeout' => S::optional(S::number()), + 'idleTimeout' => S::optional(S::number()), + 'secrets' => S::optional(S::map(S::string())), + 'env' => S::optional(S::map(S::string())), + 'nothrow' => S::optional(S::boolean()), + 'forceOutput' => S::optional(S::boolean()), + 'shell' => S::optional(S::string()), + ]); + + $upload = S::object([ + 'upload' => S::object([ + 'src' => S::union(S::string(), S::arrayOf(S::string())), + 'dest' => S::string(), + ]), + ]); + + $download = S::object([ + 'download' => S::object([ + 'src' => S::string(), + 'dest' => S::string(), + ]), + ]); + + $taskConfig = S::object([ + 'desc' => S::optional(S::string()), + 'once' => S::optional(S::boolean()), + 'hidden' => S::optional(S::boolean()), + 'limit' => S::optional(S::number()), + 'select' => S::optional(S::string()), + ]); + + $step = S::union( + $cd, + $run, + $runLocally, + $upload, + $download, + $taskConfig, + ); + + return S::object([ + 'import' => S::optional( + S::union( + S::string(), + S::arrayOf(S::string()), + ), + ), + 'config' => S::optional( + S::map(S::any()), + ), + 'hosts' => S::optional( + S::map( + S::map(S::any()), + ), + ), + 'tasks' => S::optional( + S::map( + S::union( + S::arrayOf($step), + S::arrayOf(S::string()), + ), + ), + ), + 'before' => S::optional( + S::map( + S::union( + S::string(), + S::arrayOf(S::string()), + ), + ), + ), + 'after' => S::optional( + S::map( + S::union( + S::string(), + S::arrayOf(S::string()), + ), + ), + ), + 'fail' => S::optional( + S::map(S::string()), + ), + ]); + } + + public function run(): void + { + $recipe = Maml::parseAst($this->content); + + $validationErrors = Maml::validate($recipe, self::schema()); + + $exception = null; + foreach ($validationErrors as $error) { + $exception = new SchemaException( + Maml::errorSnippet( + $this->content, + $error->span, + $error->message, + context: 3, + gutter: true, + ), + $exception, + ); + } + if ($exception) { + throw $exception; + } + + foreach ($recipe->value->properties as $property) { + $key = $property->key->value; + switch ($key) { + case 'import': + $this->import($property); + break; + + case 'config': + $this->config($property); + break; + + case 'hosts': + $this->hosts($property); + break; + + case 'tasks': + $this->tasks($property); + break; + + case 'before': + $this->before($property); + break; + + case 'after': + $this->after($property); + break; + + case 'fail': + $this->fail($property); + break; + + default: + $this->throwException("Unknown property $key", $property->key->span); + } + } + } + + private function import(Property $property): void + { + if ($property->value instanceof StringNode || $property->value instanceof RawStringNode) { + Import::import($property->value->value); + } elseif ($property->value instanceof ArrayNode) { + foreach ($property->value->elements as $element) { + $import = $element->value; + if ($import instanceof StringNode || $import instanceof RawStringNode) { + Import::import($import->value); + } else { + $this->throwException('Invalid import format', $import->span); + } + } + } else { + $this->throwException('Invalid import format', $property->value->span); + } + } + + protected function config(Property $property): void + { + $object = $property->value; + if (!$object instanceof ObjectNode) { + $this->throwException('Invalid config format', $property->value->span); + } + foreach ($object->properties as $property) { + $key = $property->key->value; + $value = $property->value; + set($key, Maml::toValue($value)); + } + } + + protected function hosts(Property $property): void + { + $object = $property->value; + if (!$object instanceof ObjectNode) { + $this->throwException('Invalid hosts format', $property->value->span); + } + foreach ($object->properties as $property) { + $alias = $property->key->value; + $object = $property->value; + if (!$object instanceof ObjectNode) { + $this->throwException('Invalid host format', $property->value->span); + } + $isLocalhost = false; + foreach ($object->properties as $config) { + $key = $config->key->value; + $value = $config->value; + if ($key === 'local' && $value instanceof BooleanNode && $value->value === true) { + $isLocalhost = true; + } + } + if ($isLocalhost) { + $host = localhost($alias); + } else { + $host = host($alias); + } + foreach ($object->properties as $config) { + $host->set($config->key->value, Maml::toValue($config->value)); + } + } + } + + protected function tasks(Property $property): void + { + $tasks = $property->value; + if (!$tasks instanceof ObjectNode) { + $this->throwException('Invalid tasks format', $property->value->span); + } + + foreach ($tasks->properties as $task) { + $name = $task->key->value; + $value = $task->value; + $desc = trim(implode("\n", array_map(fn($comment) => $comment->value, $task->leadingComments))); + if (!$value instanceof ArrayNode) { + $this->throwException('Task must be an array', $value->span); + } + $this->createTask($name, $value, $desc); + } + } + + private function createTask(string $name, ArrayNode $array, string $desc) + { + $isGroupTask = true; + $groupTasks = []; + foreach ($array->elements as $element) { + if ($element->value instanceof StringNode || $element->value instanceof RawStringNode) { + $groupTasks[] = $element->value->value; + } else { + $isGroupTask = false; + } + } + + if ($isGroupTask) { + task($name, $groupTasks)->desc($desc); + return; + } + + $body = function () { + // Empty task body. + }; + + $task = task($name, $body)->desc($desc); + + foreach ($array->elements as $element) { + $object = $element->value; + if (!$object instanceof ObjectNode) { + $this->throwException('Task step must be an object', $object->span); + } + + $step = Maml::toValue($element->value); + + foreach ($object->properties as $property) { + $key = $property->key->value; + + if (in_array($key, ['desc', 'once', 'hidden', 'limit', 'select'])) { + $task->$key($step[$key]); + continue; + } + + $prev = $body; + + $body = match ($key) { + 'cd' => function () use ($step, $prev, $property) { + $prev(); + try { + cd($step['cd']); + } catch (\Throwable $e) { + $this->wrapException($e, $property->span); + } + }, + 'run' => function () use ($step, $prev, $property) { + $prev(); + try { + run( + $step['run'], + cwd: $step['cwd'] ?? null, + env: $step['env'] ?? null, + secrets: $step['secrets'] ?? null, + nothrow: $step['nothrow'] ?? false, + forceOutput: $step['forceOutput'] ?? false, + timeout: $step['timeout'] ?? null, + idleTimeout: $step['idleTimeout'] ?? null, + ); + } catch (\Throwable $e) { + $this->wrapException($e, $property->span); + } + }, + 'runLocally' => function () use ($step, $prev, $property) { + $prev(); + try { + runLocally( + $step['runLocally'], + cwd: $step['cwd'] ?? null, + timeout: $step['timeout'] ?? null, + idleTimeout: $step['idleTimeout'] ?? null, + secrets: $step['secrets'] ?? null, + env: $step['env'] ?? null, + forceOutput: $step['forceOutput'] ?? false, + nothrow: $step['nothrow'] ?? false, + shell: $step['shell'] ?? null, + ); + } catch (\Throwable $e) { + $this->wrapException($e, $property->span); + } + }, + 'upload' => function () use ($step, $prev, $property) { + $prev(); + try { + upload( + $step['upload']['src'], + $step['upload']['dest'], + ); + } catch (\Throwable $e) { + $this->wrapException($e, $property->span); + } + }, + 'download' => function () use ($step, $prev, $property) { + $prev(); + try { + download( + $step['download']['src'], + $step['download']['dest'], + ); + } catch (\Throwable $e) { + $this->wrapException($e, $property->span); + } + }, + default => $body, + }; + } + } + + $task->setCallback($body); + + return $task; + } + + protected function before(Property $property): void + { + $object = $property->value; + if (!$object instanceof ObjectNode) { + $this->throwException('Invalid before format', $object->span); + } + foreach ($object->properties as $property) { + $key = $property->key->value; + $value = Maml::toValue($property->value); + + if (is_array($value)) { + foreach (array_reverse($value) as $v) { + before($key, $v); + } + } else { + before($key, $value); + } + } + } + + protected function after(Property $property): void + { + $object = $property->value; + if (!$object instanceof ObjectNode) { + $this->throwException('Invalid after format', $object->span); + } + foreach ($object->properties as $property) { + $key = $property->key->value; + $value = Maml::toValue($property->value); + + if (is_array($value)) { + foreach (array_reverse($value) as $v) { + after($key, $v); + } + } else { + after($key, $value); + } + } + } + + protected function fail(Property $property): void + { + $object = $property->value; + if (!$object instanceof ObjectNode) { + $this->throwException('Invalid fail format', $object->span); + } + foreach ($object->properties as $property) { + $key = $property->key->value; + $value = Maml::toValue($property->value); + fail($key, $value); + } + } + + private function throwException(string $string, Span $span): never + { + throw new Exception(Maml::errorSnippet( + $this->content, + $span, + $string, + context: 3, + gutter: true, + )); + } + + private function wrapException(\Throwable $e, Span $span): never + { + if ($e instanceof Exception) { + $e->setTaskFilename($this->filename); + $e->setTaskLineNumber($span->start->line); + } + throw $e; + } +} diff --git a/vendor/deployer/deployer/src/Import/YamlRecipe.php b/vendor/deployer/deployer/src/Import/YamlRecipe.php new file mode 100644 index 0000000..e97016e --- /dev/null +++ b/vendor/deployer/deployer/src/Import/YamlRecipe.php @@ -0,0 +1,209 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Import; + +use Deployer\Exception\ConfigurationException; +use Deployer\Exception\Exception; +use Symfony\Component\Yaml\Yaml; + +use function array_filter; +use function array_keys; +use function Deployer\after; +use function Deployer\before; +use function Deployer\cd; +use function Deployer\download; +use function Deployer\host; +use function Deployer\localhost; +use function Deployer\run; +use function Deployer\runLocally; +use function Deployer\set; +use function Deployer\task; +use function Deployer\upload; + +use const ARRAY_FILTER_USE_KEY; + +class YamlRecipe +{ + private static string $filename; + + public static function exec(string $path): void + { + self::$filename = basename($path); + $content = file_get_contents($path, true); + + $root = array_filter(Yaml::parse($content), static function (string $key) { + return !str_starts_with($key, '.'); + }, ARRAY_FILTER_USE_KEY); + + foreach (array_keys($root) as $key) { + static::$key($root[$key]); + } + } + + protected static function import(mixed $paths): void + { + Import::import($paths); + } + + protected static function hosts(array $hosts) + { + foreach ($hosts as $alias => $config) { + if ($config['local'] ?? false) { + $host = localhost($alias); + } else { + $host = host($alias); + } + if (is_array($config)) { + foreach ($config as $key => $value) { + $host->set($key, $value); + } + } + } + } + + protected static function config(array $config) + { + foreach ($config as $key => $value) { + set($key, $value); + } + } + + protected static function tasks(array $tasks) + { + $buildTask = function ($name, $steps) { + $body = function () {}; + $task = task($name, $body); + + foreach ($steps as $step) { + $buildStep = function (array $step) use (&$body, $task) { + $has = null; + + if (isset($step['cd'])) { + $cd = $step['cd']; + $prev = $body; + $body = function () use ($cd, $prev) { + $prev(); + cd($cd); + }; + } + + if (isset($step['run'])) { + $has = 'run'; + $run = $step['run']; + $prev = $body; + $body = function () use ($run, $prev) { + $prev(); + try { + run($run); + } catch (Exception $e) { + $e->setTaskFilename(self::$filename); + throw $e; + } + }; + } + + if (isset($step['run_locally'])) { + if ($has !== null) { + throw new ConfigurationException("Task step can not have both $has and run_locally."); + } + $has = 'run_locally'; + $run_locally = $step['run_locally']; + $prev = $body; + $body = function () use ($run_locally, $prev) { + $prev(); + try { + runLocally($run_locally); + } catch (Exception $e) { + $e->setTaskFilename(self::$filename); + throw $e; + } + }; + } + + if (isset($step['upload'])) { + if ($has !== null) { + throw new ConfigurationException("Task step can not have both $has and upload."); + } + $has = 'upload'; + $upload = $step['upload']; + $prev = $body; + $body = function () use ($upload, $prev) { + $prev(); + upload($upload['src'], $upload['dest']); + }; + } + + if (isset($step['download'])) { + if ($has !== null) { + throw new ConfigurationException("Task step can not have both $has and download."); + } + $prev = $body; + $download = $step['download']; + $body = function () use ($download, $prev) { + $prev(); + download($download['src'], $download['dest']); + }; + } + + foreach (['desc', 'once', 'hidden', 'limit', 'select'] as $method) { + if (isset($step[$method])) { + $task->$method($step[$method]); + } + } + }; + + $buildStep($step); + $task->setCallback($body); + } + }; + + foreach ($tasks as $name => $config) { + foreach ($config as $key => $value) { + if (!is_int($key) || !is_string($value)) { + goto not_a_group_task; + } + } + + // Create a group task. + task($name, $config); + continue; + + not_a_group_task: + $buildTask($name, $config); + } + } + + protected static function after(array $after) + { + foreach ($after as $key => $value) { + if (is_array($value)) { + foreach (array_reverse($value) as $v) { + after($key, $v); + } + } else { + after($key, $value); + } + } + } + + protected static function before(array $before) + { + foreach ($before as $key => $value) { + if (is_array($value)) { + foreach (array_reverse($value) as $v) { + before($key, $v); + } + } else { + before($key, $value); + } + } + } +} diff --git a/vendor/deployer/deployer/src/Logger/Handler/FileHandler.php b/vendor/deployer/deployer/src/Logger/Handler/FileHandler.php new file mode 100644 index 0000000..6ef8e26 --- /dev/null +++ b/vendor/deployer/deployer/src/Logger/Handler/FileHandler.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Logger\Handler; + +class FileHandler implements HandlerInterface +{ + /** + * @var string + */ + private $filePath; + + public function __construct(string $filePath) + { + $this->filePath = $filePath; + } + + public function writeln(string $message): void + { + file_put_contents($this->filePath, $message . "\n", FILE_APPEND); + } +} diff --git a/vendor/deployer/deployer/src/Logger/Handler/HandlerInterface.php b/vendor/deployer/deployer/src/Logger/Handler/HandlerInterface.php new file mode 100644 index 0000000..83fbdf0 --- /dev/null +++ b/vendor/deployer/deployer/src/Logger/Handler/HandlerInterface.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Logger\Handler; + +interface HandlerInterface +{ + public function writeln(string $message): void; +} diff --git a/vendor/deployer/deployer/src/Logger/Handler/NullHandler.php b/vendor/deployer/deployer/src/Logger/Handler/NullHandler.php new file mode 100644 index 0000000..e880304 --- /dev/null +++ b/vendor/deployer/deployer/src/Logger/Handler/NullHandler.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Logger\Handler; + +class NullHandler implements HandlerInterface +{ + public function writeln(string $message): void {} +} diff --git a/vendor/deployer/deployer/src/Logger/Logger.php b/vendor/deployer/deployer/src/Logger/Logger.php new file mode 100644 index 0000000..3025825 --- /dev/null +++ b/vendor/deployer/deployer/src/Logger/Logger.php @@ -0,0 +1,169 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Logger; + +use Deployer\Exception\Exception; +use Deployer\Exception\RunException; +use Deployer\Host\Host; +use Deployer\Logger\Handler\HandlerInterface; +use Deployer\Task\Task; +use Symfony\Component\Console\Output\OutputInterface; +use Throwable; + +use function Deployer\Support\ci_name; +use function Deployer\Support\human_duration; + +class Logger +{ + private OutputInterface $output; + private HandlerInterface $fileLog; + + private ?float $startTime = null; + + public function __construct(OutputInterface $output, HandlerInterface $log) + { + $this->output = $output; + $this->fileLog = $log; + } + + public function command(Host $host, string $type, string $command): void + { + if ($this->output->isVerbose()) { + $this->output->writeln("[{$host->getTag()}] $type $command"); + } + $this->fileLog->writeln("[{$host->getAlias()}] $type: $command"); + } + + public function print(Host $host, string $buffer, bool $force = false): void + { + if ($this->output->isVerbose() || $force) { + foreach (explode("\n", rtrim($buffer)) as $line) { + if (empty($line)) { + return; + } + $this->output->writeln("[{$host->getTag()}] $line"); + } + } + foreach (explode("\n", rtrim($buffer)) as $line) { + if (empty($line)) { + return; + } + $this->fileLog->writeln("[{$host->getAlias()}] $line"); + } + } + + public function startTask(Task $task): void + { + $this->startTime = round(microtime(true) * 1000); + $ci = ci_name(); + if ($ci === 'github') { + $this->output->writeln("::group::task {$task->getName()}"); + } elseif ($ci === 'gitlab') { + $sectionId = md5($task->getName()); + $start = round($this->startTime / 1000); + $this->output->writeln("\e[0Ksection_start:{$start}:{$sectionId}\r\e[0K{$task->getName()}"); + } else { + $this->output->writeln("task {$task->getName()}"); + } + $this->fileLog->writeln("# task {$task->getName()}"); + } + + public function endTask(Task $task): void + { + if (empty($this->startTime)) { + $this->startTime = round(microtime(true) * 1000); + } + + $endTime = round(microtime(true) * 1000); + $taskTime = human_duration((int) ($endTime - $this->startTime)); + + $ci = ci_name(); + if ($ci === 'github') { + $this->output->writeln("done {$task->getName()} $taskTime"); + $this->output->writeln("::endgroup::"); + } elseif ($ci === 'gitlab') { + $this->output->writeln("done {$task->getName()} $taskTime"); + $sectionId = md5($task->getName()); + $endSec = (int) round($endTime / 1000); + $this->output->writeln("\e[0Ksection_end:{$endSec}:{$sectionId}\r\e[0K"); + } elseif ($this->output->isVeryVerbose()) { + $this->output->writeln("done {$task->getName()} $taskTime"); + } + + $this->fileLog->writeln("# done {$task->getName()} $taskTime"); + } + + public function endOnHost(Host $host): void + { + if ($this->output->isVeryVerbose()) { + $this->output->writeln("done on {$host->getTag()}"); + } + + $this->fileLog->writeln("# done on {$host->getAlias()}"); + } + + public function renderException(Throwable $exception, Host $host): void + { + if ($exception instanceof RunException) { + $message = "[{$host->getTag()}] error in {$exception->getTaskFilename()} on line {$exception->getTaskLineNumber()}:\n"; + if ($this->output->getVerbosity() === OutputInterface::VERBOSITY_NORMAL) { + $message .= "[{$host->getTag()}] run {$exception->getCommand()}\n"; + foreach (explode("\n", $exception->getErrorOutput()) as $line) { + $line = trim($line); + if ($line !== "") { + $message .= "[{$host->getTag()}] err $line\n"; + } + } + foreach (explode("\n", $exception->getOutput()) as $line) { + $line = trim($line); + if ($line !== "") { + $message .= "[{$host->getTag()}] $line\n"; + } + } + } + $message .= "[{$host->getTag()}] exit code {$exception->getExitCode()} ({$exception->getExitCodeText()})\n"; + $this->output->write($message); + } else { + $message = ""; + $class = get_class($exception); + $file = basename($exception->getFile()); + $line = $exception->getLine(); + if ($exception instanceof Exception) { + $file = $exception->getTaskFilename(); + $line = $exception->getTaskLineNumber(); + } + $message .= "[{$host->getTag()}] $class in $file on line $line:\n"; + $message .= "[{$host->getTag()}]\n"; + foreach (explode("\n", $exception->getMessage()) as $line) { + $line = trim($line); + if ($line !== "") { + $message .= "[{$host->getTag()}] $line\n"; + } + } + $message .= "[{$host->getTag()}]\n"; + if ($this->output->isDebug()) { + foreach (explode("\n", $exception->getTraceAsString()) as $line) { + $line = trim($line); + if ($line !== "") { + $message .= "[{$host->getTag()}] $line\n"; + } + } + } + $this->output->write($message); + } + + $this->fileLog->writeln("[{$host->getAlias()}] $exception"); + + if ($exception->getPrevious()) { + $this->renderException($exception->getPrevious(), $host); + } + } +} diff --git a/vendor/deployer/deployer/src/ProcessRunner/ProcessRunner.php b/vendor/deployer/deployer/src/ProcessRunner/ProcessRunner.php new file mode 100644 index 0000000..5debffd --- /dev/null +++ b/vendor/deployer/deployer/src/ProcessRunner/ProcessRunner.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\ProcessRunner; + +use Deployer\Exception\RunException; +use Deployer\Exception\TimeoutException; +use Deployer\Host\Host; +use Deployer\Logger\Logger; +use Deployer\Ssh\RunParams; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Symfony\Component\Process\Process; + +use function Deployer\Support\deployer_root; +use function Deployer\Support\env_stringify; +use function Deployer\Support\replace_secrets; + +class ProcessRunner +{ + private Logger $logger; + + public function __construct(Logger $logger) + { + $this->logger = $logger; + } + + public function run(Host $host, string $command, RunParams $params): string + { + $this->logger->command($host, 'run', $command); + + if (!empty($params->env)) { + $env = env_stringify($params->env); + $command = "export $env; $command"; + } + + if (!empty($params->dotenv)) { + $command = "source $params->dotenv; $command"; + } + + $process = Process::fromShellCommandline($params->shell) + ->setInput(replace_secrets($command, $params->secrets)) + ->setTimeout($params->timeout) + ->setIdleTimeout($params->idleTimeout) + ->setWorkingDirectory($params->cwd ?? deployer_root()); + + $callback = function ($type, $buffer) use ($params, $host) { + $this->logger->print($host, $buffer, $params->forceOutput); + }; + + try { + $process->mustRun($callback); + return $process->getOutput(); + } catch (ProcessFailedException) { + if ($params->nothrow) { + return ''; + } + throw new RunException( + $host, + $command, + $process->getExitCode(), + $process->getOutput(), + $process->getErrorOutput(), + ); + } catch (ProcessTimedOutException $exception) { + throw new TimeoutException( + $command, + $exception->getExceededTimeout(), + ); + } + } +} diff --git a/vendor/deployer/deployer/src/Selector/Selector.php b/vendor/deployer/deployer/src/Selector/Selector.php new file mode 100644 index 0000000..275f9e3 --- /dev/null +++ b/vendor/deployer/deployer/src/Selector/Selector.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Selector; + +use Deployer\Host\Host; +use Deployer\Host\HostCollection; + +use function Deployer\Support\array_all; + +class Selector +{ + /** + * @var HostCollection + */ + private $hosts; + + public function __construct(HostCollection $hosts) + { + $this->hosts = $hosts; + } + + /** + * @return Host[] + */ + public function select(string $selectExpression) + { + $conditions = self::parse($selectExpression); + + $hosts = []; + foreach ($this->hosts as $host) { + if (self::apply($conditions, $host)) { + $hosts[] = $host; + } + } + + return $hosts; + } + + public static function apply(?array $conditions, Host $host): bool + { + if (empty($conditions)) { + return true; + } + + $labels = $host->get('labels', []); + $labels['alias'] = $host->getAlias(); + $labels['true'] = 'true'; + $isTrue = function ($value) { + return $value; + }; + + foreach ($conditions as $hmm) { + $ok = []; + foreach ($hmm as [$op, $var, $value]) { + if (is_array($value)) { + $orOk = []; + foreach ($value as $val) { + $orOk[] = self::compare($op, $labels[$var] ?? null, $val); + } + $ok[] = count(array_filter($orOk, $isTrue)) > 0; + } else { + $ok[] = self::compare($op, $labels[$var] ?? null, $value); + } + } + if (count($ok) > 0 && array_all($ok, $isTrue)) { + return true; + } + } + return false; + } + + /** + * @param string|string[] $a + */ + private static function compare(string $op, $a, ?string $b): bool + { + $matchFunction = function ($a, ?string $b) { + foreach ((array) $a as $item) { + if ($item === $b) { + return true; + } + } + + return false; + }; + + if ($op === '=') { + return $matchFunction($a, $b); + } + if ($op === '!=') { + return !$matchFunction($a, $b); + } + return false; + } + + public static function parse(string $expression): array + { + $all = []; + foreach (explode(',', $expression) as $sub) { + $conditions = []; + foreach (explode('&', $sub) as $part) { + $part = trim($part); + if ($part === 'all') { + $conditions[] = ['=', 'true', 'true']; + continue; + } + if (preg_match('/(?.+?)(?!?=)(?.+)/', $part, $match)) { + $values = array_map('trim', explode('|', trim($match['value']))); + $conditions[] = [$match['op'], trim($match['var']), $values]; + } else { + $conditions[] = ['=', 'alias', trim($part)]; + } + } + $all[] = $conditions; + } + return $all; + } +} diff --git a/vendor/deployer/deployer/src/Ssh/IOArguments.php b/vendor/deployer/deployer/src/Ssh/IOArguments.php new file mode 100644 index 0000000..252d53b --- /dev/null +++ b/vendor/deployer/deployer/src/Ssh/IOArguments.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Ssh; + +use Deployer\Exception\Exception; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class IOArguments +{ + public static function collect(InputInterface $input, OutputInterface $output): array + { + $arguments = []; + foreach ($input->getOptions() as $name => $value) { + if (!$input->getOption($name)) { + continue; + } + if ($name === 'file') { + $arguments[] = "--file"; + $arguments[] = ltrim($value, '='); + continue; + } + if (in_array($name, ['verbose'], true)) { + continue; + } + if (!is_array($value)) { + $value = [$value]; + } + foreach ($value as $v) { + if (is_bool($v)) { + $arguments[] = "--$name"; + continue; + } + + $arguments[] = "--$name"; + $arguments[] = $v; + } + } + + if ($output->isDecorated()) { + $arguments[] = '--decorated'; + } + $verbosity = self::verbosity($output->getVerbosity()); + if (!empty($verbosity)) { + $arguments[] = $verbosity; + } + return $arguments; + } + + private static function verbosity(int $verbosity): string + { + switch ($verbosity) { + case OutputInterface::VERBOSITY_QUIET: + return '-q'; + case OutputInterface::VERBOSITY_NORMAL: + return ''; + case OutputInterface::VERBOSITY_VERBOSE: + return '-v'; + case OutputInterface::VERBOSITY_VERY_VERBOSE: + return '-vv'; + case OutputInterface::VERBOSITY_DEBUG: + return '-vvv'; + default: + throw new Exception('Unknown verbosity level: ' . $verbosity); + } + } +} diff --git a/vendor/deployer/deployer/src/Ssh/RunParams.php b/vendor/deployer/deployer/src/Ssh/RunParams.php new file mode 100644 index 0000000..a3d2af3 --- /dev/null +++ b/vendor/deployer/deployer/src/Ssh/RunParams.php @@ -0,0 +1,33 @@ +secrets = array_merge($params->secrets ?? [], $secrets ?? []); + $params->timeout = $timeout ?? $params->timeout; + $params->killOnTimeout = $killOnTimeout ?? $params->killOnTimeout; + return $params; + } +} diff --git a/vendor/deployer/deployer/src/Ssh/SshClient.php b/vendor/deployer/deployer/src/Ssh/SshClient.php new file mode 100644 index 0000000..d31eee2 --- /dev/null +++ b/vendor/deployer/deployer/src/Ssh/SshClient.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Ssh; + +use Deployer\Exception\RunException; +use Deployer\Exception\TimeoutException; +use Deployer\Host\Host; +use Deployer\Logger\Logger; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Symfony\Component\Process\Process; + +use function Deployer\Support\env_stringify; +use function Deployer\Support\replace_secrets; + +class SshClient +{ + private OutputInterface $output; + private Logger $logger; + + public function __construct(OutputInterface $output, Logger $logger) + { + $this->output = $output; + $this->logger = $logger; + } + + public function run(Host $host, string $command, RunParams $params): string + { + $shellId = 'id$' . bin2hex(random_bytes(10)); + $shellCommand = $host->getShell(); + if ($host->has('become') && !empty($host->get('become'))) { + $shellCommand = "sudo -H -u {$host->get('become')} " . $shellCommand; + } + + $ssh = array_merge(['ssh'], $host->connectionOptions(), [$host->connectionString(), ": $shellId; $shellCommand"]); + + // -vvv for ssh command + if ($this->output->isDebug()) { + $sshString = $ssh[0]; + for ($i = 1; $i < count($ssh); $i++) { + $sshString .= ' ' . \Deployer\quote((string) $ssh[$i]); + } + $this->output->writeln("[$host] $sshString"); + } + + if (!empty($params->cwd)) { + $command = "cd $params->cwd && ($command)"; + } + + if (!empty($params->env)) { + $env = env_stringify($params->env); + $command = "export $env; $command"; + } + + $this->logger->command($host, 'run', $command); + + $process = new Process($ssh); + $process + ->setInput(replace_secrets($command, $params->secrets)) + ->setTimeout($params->timeout) + ->setIdleTimeout($params->idleTimeout); + + $callback = function ($type, $buffer) use ($params, $host) { + $this->logger->print($host, $buffer, $params->forceOutput); + }; + + try { + $process->run($callback); + } catch (ProcessTimedOutException $exception) { + // Let's try to kill all processes started by this command. + // Cleanup runs pass killOnTimeout: false so a timeout here can't + // recurse, and cleanup failures must not mask the timeout, so + // swallow them. + if ($params->killOnTimeout) { + try { + $pid = trim($this->run( + $host, + "ps x | grep $shellId | grep -v grep | awk '{print \$1}'", + $params->with(timeout: 10, killOnTimeout: false), + )); + if ($pid !== '') { + // Minus before pid means all processes in this group. + $this->run($host, "kill -9 -$pid", $params->with(timeout: 20, killOnTimeout: false)); + } + } catch (\Throwable) { + // The shell may have already exited, or `ps`/`kill` may be + // restricted. Either way, the timeout is the real error. + } + } + throw new TimeoutException( + $command, + $exception->getExceededTimeout(), + ); + } + + $output = $process->getOutput(); + $exitCode = $process->getExitCode(); + + if ($exitCode !== 0 && !$params->nothrow) { + throw new RunException( + $host, + $command, + $exitCode, + $output, + $process->getErrorOutput(), + ); + } + + return $output; + } +} diff --git a/vendor/deployer/deployer/src/Support/ObjectProxy.php b/vendor/deployer/deployer/src/Support/ObjectProxy.php new file mode 100644 index 0000000..856a5e2 --- /dev/null +++ b/vendor/deployer/deployer/src/Support/ObjectProxy.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Support; + +class ObjectProxy +{ + /** + * @var array + */ + private $objects; + + public function __construct(array $objects) + { + $this->objects = $objects; + } + + public function __call(string $name, array $arguments): self + { + foreach ($this->objects as $object) { + $object->$name(...$arguments); + } + return $this; + } +} diff --git a/vendor/deployer/deployer/src/Support/Reporter.php b/vendor/deployer/deployer/src/Support/Reporter.php new file mode 100644 index 0000000..1c85658 --- /dev/null +++ b/vendor/deployer/deployer/src/Support/Reporter.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Support; + +use Symfony\Component\Process\PhpProcess; + +/** + * @codeCoverageIgnore + */ +class Reporter +{ + public static function report(array $stats): void + { + $version = DEPLOYER_VERSION; + $body = json_encode($stats); + $length = strlen($body); + $php = new PhpProcess(<<start(); + } +} diff --git a/vendor/deployer/deployer/src/Support/helpers.php b/vendor/deployer/deployer/src/Support/helpers.php new file mode 100644 index 0000000..52adf88 --- /dev/null +++ b/vendor/deployer/deployer/src/Support/helpers.php @@ -0,0 +1,295 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Support; + +use function Deployer\quote; + +function array_flatten(array $array): array +{ + $flatten = []; + array_walk_recursive($array, function ($value) use (&$flatten) { + $flatten[] = $value; + }); + return $flatten; +} + +/** + * Recursively merge two config arrays with a specific behavior: + * + * 1. scalar values are overridden + * 2. array values are extended uniquely if all keys are numeric + * 3. all other array values are merged + */ +function array_merge_alternate(array $original, array $override): array +{ + foreach ($override as $key => $value) { + if (isset($original[$key])) { + if (!is_array($original[$key])) { + if (is_numeric($key)) { + // Append scalar value + $original[] = $value; + } else { + // Override scalar value + $original[$key] = $value; + } + } elseif (array_keys($original[$key]) === range(0, count($original[$key]) - 1)) { + // Uniquely append to array with numeric keys + $original[$key] = array_unique(array_merge($original[$key], $value)); + } else { + // Merge all other arrays + $original[$key] = array_merge_alternate($original[$key], $value); + } + } else { + // Simply add new key/value + $original[$key] = $value; + } + } + + return $original; +} + +function env_stringify(array $array): string +{ + return implode(' ', array_map( + function ($key, $value) { + return sprintf("%s=%s", $key, quote((string) $value)); + }, + array_keys($array), + $array, + )); +} + +function replace_secrets(string $command, ?array $secrets): string +{ + if (!empty($secrets)) { + foreach ($secrets as $key => $value) { + $command = str_replace('%' . $key . '%', strval($value), $command); + } + } + return $command; +} + +function is_closure(mixed $var): bool +{ + return is_object($var) && ($var instanceof \Closure); +} + +/** + * Check if all elements satisfy predicate. + */ +function array_all(array $array, callable $predicate): bool +{ + foreach ($array as $key => $value) { + if (!$predicate($value, $key)) { + return false; + } + } + return true; +} + +/** + * Cleanup CRLF new line endings. + */ +function normalize_line_endings(string $string): string +{ + return str_replace(["\r\n", "\r"], "\n", $string); +} + +/** + * Expand leading tilde (~) symbol in given path. + */ +function parse_home_dir(string $path): string +{ + if ('~' === $path || str_starts_with($path, '~/')) { + if (isset($_SERVER['HOME'])) { + $home = $_SERVER['HOME']; + } elseif (isset($_SERVER['HOMEDRIVE'], $_SERVER['HOMEPATH'])) { + $home = $_SERVER['HOMEDRIVE'] . $_SERVER['HOMEPATH']; + } else { + return $path; + } + + return $home . substr($path, 1); + } + + return $path; +} + +function colorize_host(string $alias): string +{ + if (defined('NO_ANSI')) { + return $alias; + } + + if (in_array($alias, ['localhost', 'local'], true)) { + return $alias; + } + + if (getenv('COLORTERM') === 'truecolor') { + $hsv = function ($h, $s, $v) { + $r = $g = $b = $i = $f = $p = $q = $t = 0; + $i = floor($h * 6); + $f = $h * 6 - $i; + $p = $v * (1 - $s); + $q = $v * (1 - $f * $s); + $t = $v * (1 - (1 - $f) * $s); + switch ($i % 6) { + case 0: + $r = $v; + $g = $t; + $b = $p; + break; + case 1: + $r = $q; + $g = $v; + $b = $p; + break; + case 2: + $r = $p; + $g = $v; + $b = $t; + break; + case 3: + $r = $p; + $g = $q; + $b = $v; + break; + case 4: + $r = $t; + $g = $p; + $b = $v; + break; + case 5: + $r = $v; + $g = $p; + $b = $q; + break; + } + $r = round($r * 255); + $g = round($g * 255); + $b = round($b * 255); + return "\x1b[38;2;{$r};{$g};{$b}m"; + }; + $total = 100; + $colors = []; + for ($i = 0; $i < $total; $i++) { + $colors[] = $hsv($i / $total, .5, .9); + } + if ($alias === 'prod' || $alias === 'production') { + return "$colors[99]$alias\x1b[0m"; + } + if ($alias === 'beta') { + return "$colors[14]$alias\x1b[0m"; + } + $tag = $colors[abs(crc32($alias)) % count($colors)]; + return "$tag$alias\x1b[0m"; + } + + $colors = [ + 'fg=cyan;options=bold', + 'fg=green;options=bold', + 'fg=yellow;options=bold', + 'fg=cyan', + 'fg=blue', + 'fg=yellow', + 'fg=magenta', + 'fg=blue;options=bold', + 'fg=green', + 'fg=magenta;options=bold', + 'fg=red;options=bold', + ]; + $tag = $colors[abs(crc32($alias)) % count($colors)]; + return "<$tag>$alias"; +} + +function human_duration(int $millis): string +{ + if ($millis < 0) { + $millis = 0; + } + if ($millis < 1000) { + return "{$millis}ms"; + } + $seconds = intdiv($millis, 1000); + $ms = $millis - $seconds * 1000; + if ($seconds < 60) { + return "{$seconds}s {$ms}ms"; + } + $minutes = intdiv($seconds, 60); + $seconds = $seconds - $minutes * 60; + if ($minutes < 60) { + return "{$minutes}m {$seconds}s"; + } + $hours = intdiv($minutes, 60); + $minutes = $minutes - $hours * 60; + return "{$hours}h {$minutes}m"; +} + +function ci_name(): ?string +{ + if (getenv('GITHUB_WORKFLOW')) { + return 'github'; + } + if (getenv('GITLAB_CI')) { + return 'gitlab'; + } + return null; +} + +function deployer_root(): string +{ + if (getenv('DEPLOYER_ROOT') !== false) { + return getenv('DEPLOYER_ROOT'); + } else { + if (defined('DEPLOYER_DEPLOY_FILE')) { + return dirname(DEPLOYER_DEPLOY_FILE); + } else { + return getcwd(); + } + } +} + +/** + * Constructs an rsync-compatible remote shell command string using SSH and the provided options. + * + * @see https://github.com/RsyncProject/rsync/blob/4f6e4ea64ac3e2ac50f48103a22601f4a40ee8be/rsync.1.md?plain=1#L2152-L2162 + * + * Command-line arguments are permitted in COMMAND provided that COMMAND is + * presented to rsync as a single argument. You must use spaces (not tabs or + * other whitespace) to separate the command and args from each other, and you + * can use single- and/or double-quotes to preserve spaces in an argument (but + * not backslashes). Note that doubling a single-quote inside a single-quoted + * string gives you a single-quote; likewise for double-quotes (though you + * need to pay attention to which quotes your shell is parsing and which + * quotes rsync is parsing). Some examples: + * + * -e 'ssh -p 2234' + * -e 'ssh -o "ProxyCommand nohup ssh firewall nc -w1 %h %p"' + * + */ +function rsync_rsh(array $args): string +{ + $parts = ['ssh']; + foreach ($args as $option) { + if (is_integer($option)) { + $option = (string) $option; + } + if (str_contains($option, "\0")) { + throw new \InvalidArgumentException('rsync_rsh: NUL byte not allowed in ssh option'); + } + if (preg_match('/^[a-zA-Z0-9_\-=]+$/', $option)) { + $parts[] = $option; + } else { + $parts[] = "'" . str_replace("'", "''", $option) . "'"; + } + } + return implode(' ', $parts); +} diff --git a/vendor/deployer/deployer/src/Task/Context.php b/vendor/deployer/deployer/src/Task/Context.php new file mode 100644 index 0000000..510ddf6 --- /dev/null +++ b/vendor/deployer/deployer/src/Task/Context.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Task; + +use Deployer\Configuration; +use Deployer\Exception\Exception; +use Deployer\Host\Host; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Context +{ + private Host $host; + + /** + * @var Context[] + */ + private static array $contexts = []; + + public function __construct(Host $host) + { + $this->host = $host; + } + + public static function push(Context $context): void + { + self::$contexts[] = $context; + } + + public static function has(): bool + { + return !empty(self::$contexts); + } + + public static function get(): Context + { + if (empty(self::$contexts)) { + throw new Exception("Context was requested but was not available."); + } + return end(self::$contexts); + } + + public static function pop(): ?Context + { + return array_pop(self::$contexts); + } + + /** + * Throws a Exception when not called within a task-context and therefore no Context is available. + * + * This method provides a useful error to the end-user to make him/her aware + * to use a function in the required task-context. + * + * @throws Exception + */ + public static function required(string $callerName): void + { + if (empty(self::$contexts)) { + throw new Exception("'$callerName' can only be used within a task."); + } + } + + public function getConfig(): Configuration + { + return $this->host->config(); + } + + public function getHost(): Host + { + return $this->host; + } +} diff --git a/vendor/deployer/deployer/src/Task/GroupTask.php b/vendor/deployer/deployer/src/Task/GroupTask.php new file mode 100644 index 0000000..d98a9f7 --- /dev/null +++ b/vendor/deployer/deployer/src/Task/GroupTask.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Task; + +use function Deployer\invoke; + +class GroupTask extends Task +{ + /** + * List of tasks. + * + * @var string[] + */ + private $group; + + /** + * @param string[] $group + */ + public function __construct(string $name, array $group) + { + parent::__construct($name); + $this->group = $group; + } + + public function run(Context $context): void + { + foreach ($this->group as $item) { + invoke($item); + } + } + + /** + * List of dependent tasks names + * + * @return string[] + */ + public function getGroup(): array + { + return $this->group; + } + + public function setGroup(array $group): void + { + $this->group = $group; + } +} diff --git a/vendor/deployer/deployer/src/Task/ScriptManager.php b/vendor/deployer/deployer/src/Task/ScriptManager.php new file mode 100644 index 0000000..29e6ef4 --- /dev/null +++ b/vendor/deployer/deployer/src/Task/ScriptManager.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Task; + +use Deployer\Exception\Exception; + +use function Deployer\Support\array_flatten; + +class ScriptManager +{ + /** + * @var TaskCollection + */ + private $tasks; + /** + * @var bool + */ + private $hooksEnabled = true; + /** + * @var array + */ + private $visitedTasks = []; + + public function __construct(TaskCollection $tasks) + { + $this->tasks = $tasks; + } + + /** + * Return tasks to run. + * + * @return Task[] + */ + public function getTasks(string $name, ?string $startFrom = null, array &$skipped = []): array + { + $tasks = []; + $this->visitedTasks = []; + $allTasks = $this->doGetTasks($name); + + if ($startFrom === null) { + $tasks = $allTasks; + } else { + $skip = true; + foreach ($allTasks as $task) { + if ($skip) { + if ($task->getName() === $startFrom) { + $skip = false; + } else { + $skipped[] = $task->getName(); + continue; + } + } + $tasks[] = $task; + } + if (count($tasks) === 0) { + throw new Exception('All tasks skipped via --start-from option. Nothing to run.'); + } + } + + $enabledTasks = []; + foreach ($tasks as $task) { + if ($task->isEnabled()) { + $enabledTasks[] = $task; + } + } + + return $enabledTasks; + } + + /** + * @return Task[] + */ + public function doGetTasks(string $name): array + { + if (array_key_exists($name, $this->visitedTasks)) { + if ($this->visitedTasks[$name] >= 100) { + throw new Exception("Looks like a circular dependency with \"$name\" task."); + } + $this->visitedTasks[$name]++; + } else { + $this->visitedTasks[$name] = 1; + } + + $tasks = []; + $task = $this->tasks->get($name); + if ($this->hooksEnabled) { + $tasks = array_merge(array_map([$this, 'doGetTasks'], $task->getBefore()), $tasks); + } + if ($task instanceof GroupTask) { + foreach ($task->getGroup() as $taskName) { + $subTasks = $this->doGetTasks($taskName); + foreach ($subTasks as $subTask) { + $subTask->addSelector($task->getSelector()); + if ($task->isOnce()) { + $subTask->once(); + } + $tasks[] = $subTask; + } + } + } else { + $tasks[] = $task; + } + if ($this->hooksEnabled) { + $tasks = array_merge($tasks, array_map([$this, 'doGetTasks'], $task->getAfter())); + } + return array_flatten($tasks); + } + + public function getHooksEnabled(): bool + { + return $this->hooksEnabled; + } + + public function setHooksEnabled(bool $hooksEnabled): void + { + $this->hooksEnabled = $hooksEnabled; + } +} diff --git a/vendor/deployer/deployer/src/Task/Task.php b/vendor/deployer/deployer/src/Task/Task.php new file mode 100644 index 0000000..f3907ee --- /dev/null +++ b/vendor/deployer/deployer/src/Task/Task.php @@ -0,0 +1,272 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Task; + +use Deployer\Selector\Selector; + +class Task +{ + /** + * @var string + */ + private $name; + /** + * @var callable|null + */ + private $callback; + /** + * @var string + */ + private $description; + /** + * @var string + */ + private $sourceLocation = ''; + /** + * @var array + */ + private $before = []; + /** + * @var array + */ + private $after = []; + /** + * @var bool + */ + private $hidden = false; + /** + * @var bool + */ + private $once = false; + /** + * @var bool + */ + private $oncePerNode = false; + /** + * @var int|null + */ + private $limit = null; + /** + * @var array|null + */ + private $selector = null; + /** + * @var bool + */ + private $verbose = false; + /** + * @var bool + */ + private $enabled = true; + + /** + * @param callable():void $callback + */ + public function __construct(string $name, ?callable $callback = null) + { + $this->name = $name; + $this->callback = $callback; + } + + /** + * @param callable():void $callback + */ + public function setCallback(callable $callback): void + { + $this->callback = $callback; + } + + public function run(Context $context): void + { + Context::push($context); + + try { + call_user_func($this->callback); // call task + } finally { + $context->getConfig()->set('working_path', null); + Context::pop(); + } + } + + public function getName(): string + { + return $this->name; + } + + public function __toString(): string + { + return $this->getName(); + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function desc(string $description): self + { + $this->description = $description; + return $this; + } + + public function getSourceLocation(): string + { + return $this->sourceLocation; + } + + public function setSourceLocation(string $path): void + { + $this->sourceLocation = $path; + } + + public function saveSourceLocation(): void + { + if (function_exists('debug_backtrace')) { + $trace = debug_backtrace(); + $this->sourceLocation = $trace[1]['file']; + } + } + + /** + * Mark this task to run only once on one of hosts. + */ + public function once(bool $once = true): self + { + $this->once = $once; + return $this; + } + + public function isOnce(): bool + { + return $this->once; + } + + /** + * Mark task to only run once per node. + * Node is a group of hosts with same hostname or with same node label. + */ + public function oncePerNode(bool $once = true): self + { + $this->oncePerNode = $once; + return $this; + } + + public function isOncePerNode(): bool + { + return $this->oncePerNode; + } + + /** + * Mark task as hidden and not accessible from CLI. + */ + public function hidden(bool $hidden = true): self + { + $this->hidden = $hidden; + return $this; + } + + public function isHidden(): bool + { + return $this->hidden; + } + + /** + * Make $task being run before this task. + */ + public function addBefore(string $task): self + { + array_unshift($this->before, $task); + return $this; + } + + /** + * Make $task being run after this task + */ + public function addAfter(string $task): self + { + array_push($this->after, $task); + return $this; + } + + public function getBefore(): array + { + return $this->before; + } + + public function getAfter(): array + { + return $this->after; + } + + public function getLimit(): ?int + { + return $this->limit; + } + + public function limit(?int $limit): self + { + $this->limit = $limit; + return $this; + } + + public function select(string $selector): self + { + $this->selector = Selector::parse($selector); + return $this; + } + + /** + * @return array + */ + public function getSelector(): ?array + { + return $this->selector; + } + + public function addSelector(?array $newSelector): void + { + if ($newSelector !== null) { + if ($this->selector === null) { + $this->selector = $newSelector; + } else { + $this->selector = array_merge($this->selector, $newSelector); + } + } + } + + public function isVerbose(): bool + { + return $this->verbose; + } + + public function verbose(bool $verbose = true): self + { + $this->verbose = $verbose; + return $this; + } + + public function isEnabled(): bool + { + return $this->enabled; + } + + public function disable(): self + { + $this->enabled = false; + return $this; + } + + public function enable(): self + { + $this->enabled = true; + return $this; + } +} diff --git a/vendor/deployer/deployer/src/Task/TaskCollection.php b/vendor/deployer/deployer/src/Task/TaskCollection.php new file mode 100644 index 0000000..87b15ad --- /dev/null +++ b/vendor/deployer/deployer/src/Task/TaskCollection.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Task; + +use Deployer\Collection\Collection; + +/** + * @method Task get($name) + * @method Task[] getIterator() + */ +class TaskCollection extends Collection +{ + protected function notFound(string $name): \InvalidArgumentException + { + return new \InvalidArgumentException("Task `$name` not found."); + } + + public function add(Task $task): void + { + $this->set($task->getName(), $task); + } +} diff --git a/vendor/deployer/deployer/src/Utility/HttpResponse.php b/vendor/deployer/deployer/src/Utility/HttpResponse.php new file mode 100644 index 0000000..2faa084 --- /dev/null +++ b/vendor/deployer/deployer/src/Utility/HttpResponse.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Utility; + +use Deployer\Exception\HttpieException; + +class HttpResponse +{ + private string $body; + private array $info; + + public function __construct(string $body, array $info) + { + $this->body = $body; + $this->info = $info; + } + + public function body(): string + { + return $this->body; + } + + public function status(): int + { + return (int) ($this->info['http_code'] ?? 0); + } + + public function json(): mixed + { + return json_decode($this->body, true, flags: JSON_THROW_ON_ERROR); + } + + public function info(): array + { + return $this->info; + } + + public function __toString(): string + { + return $this->body; + } +} diff --git a/vendor/deployer/deployer/src/Utility/Httpie.php b/vendor/deployer/deployer/src/Utility/Httpie.php new file mode 100644 index 0000000..94b3b02 --- /dev/null +++ b/vendor/deployer/deployer/src/Utility/Httpie.php @@ -0,0 +1,236 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Utility; + +use Deployer\Exception\HttpieException; + +class Httpie +{ + private static bool $extensionChecked = false; + + private string $method = 'GET'; + private string $url = ''; + private array $headers = []; + private string $body = ''; + private array $curlopts = []; + private bool $nothrow = false; + + public function __construct() + { + if (!self::$extensionChecked) { + if (!extension_loaded('curl')) { + throw new \Exception( + "Please, install curl extension.\n" + . "https://php.net/curl.installation", + ); + } + self::$extensionChecked = true; + } + } + + public static function get(string $url): self + { + $http = new self(); + $http->method = 'GET'; + $http->url = $url; + return $http; + } + + public static function post(string $url): self + { + $http = new self(); + $http->method = 'POST'; + $http->url = $url; + return $http; + } + + public static function patch(string $url): self + { + $http = new self(); + $http->method = 'PATCH'; + $http->url = $url; + return $http; + } + + public static function put(string $url): self + { + $http = new self(); + $http->method = 'PUT'; + $http->url = $url; + return $http; + } + + public static function delete(string $url): self + { + $http = new self(); + $http->method = 'DELETE'; + $http->url = $url; + return $http; + } + + public function query(array $params): self + { + $separator = str_contains($this->url, '?') ? '&' : '?'; + $this->url .= $separator . http_build_query($params); + return $this; + } + + public function header(string $header, string $value): self + { + $this->headers[$header] = $value; + return $this; + } + + public function bearerToken(string $token): self + { + $this->headers['Authorization'] = 'Bearer ' . $token; + return $this; + } + + public function basicAuth(string $user, string $pass): self + { + $this->curlopts[CURLOPT_USERPWD] = "$user:$pass"; + return $this; + } + + public function timeout(int $seconds): self + { + $this->curlopts[CURLOPT_TIMEOUT] = $seconds; + $this->curlopts[CURLOPT_CONNECTTIMEOUT] = $seconds; + return $this; + } + + public function noTimeout(): self + { + $this->curlopts[CURLOPT_TIMEOUT] = 0; + $this->curlopts[CURLOPT_CONNECTTIMEOUT] = 0; + return $this; + } + + public function body(string $body): self + { + $this->body = $body; + $this->headers['Content-Length'] = (string) strlen($this->body); + return $this; + } + + public function jsonBody(array $data): self + { + $this->body = json_encode($data, JSON_PRETTY_PRINT); + $this->headers['Content-Type'] = 'application/json'; + $this->headers['Content-Length'] = (string) strlen($this->body); + return $this; + } + + public function formBody(array $data): self + { + $this->body = http_build_query($data); + $this->headers['Content-type'] = 'application/x-www-form-urlencoded'; + $this->headers['Content-Length'] = (string) strlen($this->body); + return $this; + } + + /** + * @param mixed $value + */ + public function setopt(int $key, $value): self + { + $this->curlopts[$key] = $value; + return $this; + } + + public function nothrow(bool $on = true): self + { + $this->nothrow = $on; + return $this; + } + + /** + * Send the request and return a response object. + */ + public function send(?array &$info = null): HttpResponse + { + if ($this->url === '') { + throw new \RuntimeException('URL must not be empty to Httpie::send()'); + } + $ch = curl_init($this->url); + curl_setopt($ch, CURLOPT_USERAGENT, 'Deployer ' . DEPLOYER_VERSION); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method); + $headers = []; + foreach ($this->headers as $key => $value) { + $headers[] = "$key: $value"; + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_POSTFIELDS, $this->body); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_MAXREDIRS, 10); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + foreach ($this->curlopts as $key => $value) { + curl_setopt($ch, $key, $value); + } + $result = curl_exec($ch); + $info = curl_getinfo($ch) ?: []; + if ($result === false) { + if ($this->nothrow) { + return new HttpResponse('', $info); + } + $error = curl_error($ch); + $errno = curl_errno($ch); + throw new HttpieException($error, $errno); + } + return new HttpResponse($result, $info); + } + + /** + * Send the request and return the decoded JSON response. + */ + public function sendJson(): mixed + { + return $this->send()->json(); + } + + /** + * @deprecated Use sendJson() instead. + */ + public function getJson(): mixed + { + return $this->sendJson(); + } + + // Getters for testing. + + public function getMethod(): string + { + return $this->method; + } + + public function getUrl(): string + { + return $this->url; + } + + public function getHeaders(): array + { + return $this->headers; + } + + public function getBody(): string + { + return $this->body; + } + + public function getCurlopts(): array + { + return $this->curlopts; + } +} diff --git a/vendor/deployer/deployer/src/Utility/Rsync.php b/vendor/deployer/deployer/src/Utility/Rsync.php new file mode 100644 index 0000000..dbacf3c --- /dev/null +++ b/vendor/deployer/deployer/src/Utility/Rsync.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer\Utility; + +use Deployer\Exception\RunException; +use Deployer\Host\Host; +use Deployer\Logger\Logger; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Process; + +use function Deployer\quote; +use function Deployer\Support\rsync_rsh; +use function Deployer\writeln; + +class Rsync +{ + private OutputInterface $output; + private Logger $logger; + + public function __construct(OutputInterface $output, Logger $logger) + { + $this->output = $output; + $this->logger = $logger; + } + + /** + * Start rsync process. + * + * @param string|string[] $source + * @phpstan-param array{flags?: string, options?: array, timeout?: int|null, progress_bar?: bool, display_stats?: bool} $config + * @throws RunException + */ + public function call(Host $host, $source, string $destination, array $config = []): void + { + $defaults = [ + 'timeout' => null, + 'options' => [], + 'flags' => '-azP', + 'progress_bar' => true, + 'display_stats' => false, + ]; + $config = array_merge($defaults, $config); + + $options = $config['options']; + $flags = $config['flags']; + $displayStats = $config['display_stats'] || in_array('--stats', $options, true); + + if ($displayStats && !in_array('--stats', $options, true)) { + $options[] = '--stats'; + } + + $connectionOptions = $host->connectionOptions(); + if (!empty($connectionOptions)) { + $rsh = rsync_rsh($connectionOptions); + $options = array_merge($options, ['-e', $rsh]); + } + if ($host->has('become') && !empty($host->get('become'))) { + $options = array_merge($options, ['--rsync-path', "sudo -H -u {$host->get('become')} rsync"]); + } + if (!is_array($source)) { + $source = [$source]; + } + $command = array_values(array_filter( + array_merge(['rsync', $flags], $options, $source, [$destination]), + function (string $value) { + return $value !== ''; + }, + )); + + $printCommandForDebug = $command[0]; + for ($i = 1; $i < count($command); $i++) { + $printCommandForDebug .= ' ' . quote($command[$i]); + } + if ($this->output->isVerbose()) { + $this->output->writeln("[$host] $printCommandForDebug"); + } + + $progressBar = null; + if ($this->output->getVerbosity() === OutputInterface::VERBOSITY_NORMAL && $config['progress_bar']) { + $progressBar = new ProgressBar($this->output); + $progressBar->setBarCharacter('≡'); + $progressBar->setProgressCharacter('>'); + $progressBar->setEmptyBarCharacter('-'); + } + + $fullOutput = ''; + + $callback = function ($type, $buffer) use ($host, $progressBar, &$fullOutput) { + $fullOutput .= $buffer; + if ($progressBar) { + foreach (explode("\n", $buffer) as $line) { + if (preg_match('/(to-chk|to-check)=(\d+?)\/(\d+)/', $line, $match)) { + $max = intval($match[3]); + $step = $max - intval($match[2]); + $progressBar->setMaxSteps($max); + $progressBar->setFormat("[$host] %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%"); + $progressBar->setProgress($step); + } + } + return; + } + $this->logger->print($host, $buffer); + }; + + $process = new Process($command); + $process->setTimeout($config['timeout']); + try { + $process->mustRun($callback); + + if ($displayStats) { + $stats = []; + + $statsStarted = false; + foreach (explode("\n", $fullOutput) as $line) { + if (strpos($line, 'Number of files') === 0) { + $statsStarted = true; + } + + if ($statsStarted) { + if (empty($line)) { + break; + } + $stats[] = $line; + } + } + + writeln("Rsync operation stats\n" . '' . implode("\n", $stats) . ''); + } + + } catch (ProcessFailedException $exception) { + throw new RunException( + $host, + $printCommandForDebug, + $process->getExitCode(), + $process->getOutput(), + $process->getErrorOutput(), + ); + } finally { + if ($progressBar) { + $progressBar->clear(); + } + } + } +} diff --git a/vendor/deployer/deployer/src/functions.php b/vendor/deployer/deployer/src/functions.php new file mode 100644 index 0000000..a47cb3a --- /dev/null +++ b/vendor/deployer/deployer/src/functions.php @@ -0,0 +1,1259 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer; + +use Deployer\Exception\Exception; +use Deployer\Exception\GracefulShutdownException; +use Deployer\Exception\RunException; +use Deployer\Exception\TimeoutException; +use Deployer\Exception\WillAskUser; +use Deployer\Host\Host; +use Deployer\Host\Localhost; +use Deployer\Host\Range; +use Deployer\Import\Import; +use Deployer\Ssh\RunParams; +use Deployer\Support\ObjectProxy; +use Deployer\Task\Context; +use Deployer\Task\GroupTask; +use Deployer\Task\Task; +use Deployer\Utility\Httpie; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Question\Question; + +use function Deployer\Support\array_merge_alternate; +use function Deployer\Support\is_closure; + +/** + * Define one or more hosts for deployment. + * + * ```php + * host('example.org'); + * host('prod.example.org', 'staging.example.org'); + * ``` + * + * Inside a task, returns the `Host` instance for the given alias: + * ```php + * task('test', function () { + * $port = host('example.org')->get('port'); + * }); + * ``` + */ +function host(string ...$hostname): Host|ObjectProxy +{ + $deployer = Deployer::get(); + if (count($hostname) === 1 && $deployer->hosts->has($hostname[0])) { + return $deployer->hosts->get($hostname[0]); + } + $aliases = Range::expand($hostname); + + foreach ($aliases as $alias) { + if ($deployer->hosts->has($alias)) { + $host = $deployer->hosts->get($alias); + throw new \InvalidArgumentException("Host \"$host\" already exists."); + } + } + + if (count($aliases) === 1) { + $host = new Host($aliases[0]); + $deployer->hosts->set($aliases[0], $host); + return $host; + } else { + $hosts = array_map(function ($hostname) use ($deployer): Host { + $host = new Host($hostname); + $deployer->hosts->set($hostname, $host); + return $host; + }, $aliases); + return new ObjectProxy($hosts); + } +} + +/** + * Define a local host. Commands run on the local machine instead of over SSH. + * + * ```php + * localhost('ci'); // Alias and hostname will be "ci". + * ``` + */ +function localhost(string ...$hostnames): Localhost|ObjectProxy +{ + $deployer = Deployer::get(); + $hostnames = Range::expand($hostnames); + + if (count($hostnames) <= 1) { + $host = count($hostnames) === 1 ? new Localhost($hostnames[0]) : new Localhost(); + $deployer->hosts->set($host->getAlias(), $host); + return $host; + } else { + $hosts = array_map(function ($hostname) use ($deployer): Localhost { + $host = new Localhost($hostname); + $deployer->hosts->set($host->getAlias(), $host); + return $host; + }, $hostnames); + return new ObjectProxy($hosts); + } +} + +/** + * Return the host the current task is running on. + * + * ```php + * task('whoami', function () { + * writeln(currentHost()->getAlias()); + * }); + * ``` + */ +function currentHost(): Host +{ + return Context::get()->getHost(); +} + +/** + * Return hosts matching a selector expression. + * + * ```php + * on(select('stage=prod, role=db'), function (Host $host) { + * // Runs on hosts tagged stage=prod AND role=db. + * }); + * ``` + * + * @return Host[] + */ +function select(string $selector): array +{ + return Deployer::get()->selector->select($selector); +} + +/** + * Return the hosts the user picked on the command line. + * + * @return Host[] + */ +function selectedHosts(): array +{ + $hosts = []; + foreach (get('selected_hosts', []) as $alias) { + $hosts[] = Deployer::get()->hosts->get($alias); + } + return $hosts; +} + +/** + * Import another recipe (PHP or MAML). + * + * Built-in `recipe/*` and `contrib/*` paths are already on PHP's include path, + * so they can be imported by relative path. Use `__DIR__` for files inside + * your own project. + * + * ```php + * import('recipe/common.php'); // built-in recipe + * import('contrib/rsync.php'); // contrib recipe + * import(__DIR__ . '/config/hosts.maml'); // local file + * ``` + */ +function import(string $file): void +{ + Import::import($file); +} + +/** + * Set the description for the next task defined with `task()`. + * + * ```php + * desc('Restart php-fpm'); + * task('restart', function () { + * run('sudo systemctl restart php-fpm'); + * }); + * ``` + * + * Calling `desc()` with no argument returns the pending description. + */ +function desc(?string $title = null): ?string +{ + static $store = null; + + if ($title === null) { + return $store; + } else { + return $store = $title; + } +} + +/** + * Define a task, or return an already defined one. + * + * Pass a callback to define a single task: + * ```php + * task('deploy:run', function () { + * run('echo deploying'); + * }); + * ``` + * + * Pass an array of task names to define a group task: + * ```php + * task('deploy', ['deploy:update_code', 'deploy:vendors', 'deploy:symlink']); + * ``` + * + * Pass only the name to fetch an existing task: + * ```php + * task('deploy')->desc('Run full deploy'); + * ``` + * + * @param string $name Task name. + * @param callable|array|null $body Callback, list of task names, or null to fetch an existing task. + * @return Task + */ +function task(string $name, callable|array|null $body = null): Task +{ + $deployer = Deployer::get(); + + if (empty($body)) { + return $deployer->tasks->get($name); + } + + if (is_callable($body)) { + $task = new Task($name, $body); + } elseif (is_array($body)) { + $task = new GroupTask($name, $body); + } else { + throw new \InvalidArgumentException('Task body should be a function or an array.'); + } + + if ($deployer->tasks->has($name)) { + // If task already exists, try to replace. + $existingTask = $deployer->tasks->get($name); + if (get_class($existingTask) !== get_class($task)) { + // There is no "up" or "down"casting in PHP. + throw new \Exception('Tried to replace Task \'' . $name . '\' with a GroupTask or vice-versa. This is not supported. If you are sure you want to do that, remove the old task `Deployer::get()->tasks->remove()` and then re-add the task.'); + } + if ($existingTask instanceof GroupTask) { + $existingTask->setGroup($body); + } elseif ($existingTask instanceof Task) { + $existingTask->setCallback($body); + } + $task = $existingTask; + } else { + // If task does not exist, add it to the Collection. + $deployer->tasks->set($name, $task); + } + + $task->saveSourceLocation(); + + if (!empty(desc())) { + $task->desc(desc()); + desc(''); // Clear title. + } + + return $task; +} + +/** + * Run a task (or callback) before another task. + * + * ```php + * before('deploy:symlink', 'deploy:cache:warmup'); + * + * before('deploy:symlink', function () { + * run('echo about to symlink'); + * }); + * ``` + * + * @param string $task Name of the task to attach the hook to. + * @param string|callable $do Task name or callback to run. + * + * @return ?Task + */ +function before(string $task, string|callable $do): ?Task +{ + if (is_closure($do)) { + $newTask = task("before:$task", $do); + before($task, "before:$task"); + return $newTask; + } + task($task)->addBefore($do); + + return null; +} + +/** + * Run a task (or callback) after another task. + * + * ```php + * after('deploy:symlink', 'deploy:cleanup'); + * + * after('deploy:failed', function () { + * run('echo something went wrong'); + * }); + * ``` + * + * @param string $task Name of the task to attach the hook to. + * @param string|callable $do Task name or callback to run. + * + * @return ?Task + */ +function after(string $task, string|callable $do): ?Task +{ + if (is_closure($do)) { + $newTask = task("after:$task", $do); + after($task, "after:$task"); + return $newTask; + } + task($task)->addAfter($do); + + return null; +} + +/** + * Run a task (or callback) when another task fails. + * + * Calling `fail()` again for the same task replaces the previous handler. + * + * ```php + * fail('deploy', 'deploy:unlock'); + * + * fail('deploy', function () { + * run('echo rollback triggered'); + * }); + * ``` + * + * @param string $task Name of the task whose failure triggers `$do`. + * @param string|callable $do Task name or callback to run on failure. + * + * @return ?Task + */ +function fail(string $task, string|callable $do): ?Task +{ + if (is_callable($do)) { + $newTask = task("fail:$task", $do); + fail($task, "fail:$task"); + return $newTask; + } + $deployer = Deployer::get(); + $deployer->fail->set($task, $do); + + return null; +} + +/** + * Add a CLI option to the `dep` binary. + * + * ```php + * use Symfony\Component\Console\Input\InputOption; + * + * option('tag', null, InputOption::VALUE_REQUIRED, 'Release tag'); + * + * task('deploy', function () { + * $tag = input()->getOption('tag'); + * }); + * ``` + * + * @param string $name Option name (long form, no leading dashes). + * @param string|array|null $shortcut Single-letter shortcut, `|`-separated list, or array of shortcuts. + * @param int|null $mode One of the `InputOption::VALUE_*` constants. + * @param string $description Help text shown in `dep --help`. + * @param string|string[]|int|bool|null $default Default value (must be null for `VALUE_NONE`). + */ +function option(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null): void +{ + Deployer::get()->inputDefinition->addOption( + new InputOption($name, $shortcut, $mode, $description, $default), + ); +} + +/** + * Change the current working directory. + * + * Both `cd()` and the `cwd:` argument of `run()` change the working directory + * for executed commands. The difference: `cd()` changes it for the rest of the + * current task, while `cwd:` overrides it for a single `run()` call only. + * + * ```php + * set('deploy_path', '~/deployer.org'); + * + * task('task1', function () { + * cd('{{deploy_path}}'); + * + * run('pwd'); + * // output: /home/deployer/deployer.org + * + * run('pwd', cwd: '/usr'); // Override working dir for this run only. + * // output: /usr + * + * run('pwd'); + * // output: /home/deployer/deployer.org + * }); + * ``` + * + * Note that `cd()` only changes the working directory within a single task. + * The next task starts fresh. + * + * ```php + * task('task2', function () { + * run('pwd'); // cd from previous task is not used. + * // output: /home/deployer + * }); + * + * task('all', [ + * 'task1', + * 'task2', + * ]); + * ``` + */ +function cd(string $path): void +{ + set('working_path', parse($path)); +} + +/** + * Switch the user that `run()` uses for subsequent commands. + * + * Returns a closure that restores the previous user when called. + * + * ```php + * $restore = become('deployer'); + * run('whoami'); // deployer + * $restore(); + * ``` + * + * @return \Closure Restores the previous user when invoked. + */ +function become(string $user): \Closure +{ + $currentBecome = get('become'); + set('become', $user); + return function () use ($currentBecome) { + set('become', $currentBecome); + }; +} + +/** + * Run a callback inside a working directory, then restore the previous one. + * + * Use this when you need a scoped `cd()` that does not leak to the rest of the task. + * + * ```php + * within('{{release_path}}', function () { + * run('composer install'); + * }); + * ``` + * + * @return mixed Whatever `$callback` returns, or null. + * @throws Exception + */ +function within(string $path, callable $callback): mixed +{ + $lastWorkingPath = get('working_path', ''); + try { + set('working_path', parse($path)); + return $callback(); + } finally { + set('working_path', $lastWorkingPath); + } +} + +/** + * Run a command on the current remote host and return its trimmed stdout. + * + * ```php + * run('echo hello world'); + * run('cd {{deploy_path}} && git status'); + * run('curl medv.io', timeout: 5); + * + * $path = run('readlink {{deploy_path}}/current'); + * run("echo $path"); + * ``` + * + * Pass secrets via `%name%` placeholders so they are redacted in logs: + * ```php + * run('curl -u admin:%token% https://api.example', secrets: ['token' => getenv('TOKEN')]); + * ``` + * + * Use the `| quote` filter to escape config values as shell arguments: + * ```php + * run('echo {{ message | quote }}'); + * run('grep -r {{ pattern | quote }} {{release_path}}'); + * ``` + * + * To emit a literal `{{`, escape it with a backslash: + * ```php + * run('echo \{{not_replaced}}'); // outputs: {{not_replaced}} + * ``` + * + * @param string $command Command to run on the remote host. + * @param string|null $cwd Working directory for this run. Defaults to `{{working_path}}` (set by `cd()`). + * @param int|null $timeout Max runtime in seconds (default: `{{default_timeout}}`, 300; `null` disables). + * @param int|null $idleTimeout Max seconds without output before aborting. + * @param array|null $secrets Map of `%name%` placeholders to redacted values. + * @param array|null $env Environment variables: `run('echo $KEY', env: ['KEY' => 'value']);` + * @param bool|null $forceOutput Print command output in real time. + * @param bool|null $nothrow Return output instead of throwing on non-zero exit. + * @throws RunException + * @throws TimeoutException + * @throws WillAskUser + */ +function run( + string $command, + ?string $cwd = null, + ?array $env = null, + #[\SensitiveParameter] + ?array $secrets = null, + ?bool $nothrow = false, + ?bool $forceOutput = false, + ?int $timeout = null, + ?int $idleTimeout = null, +): string { + $runParams = new RunParams( + shell: currentHost()->getShell(), + cwd: $cwd ?? (has('working_path') ? get('working_path') : null), + env: array_merge_alternate(get('env', []), $env ?? []), + nothrow: $nothrow, + timeout: $timeout ?? get('default_timeout', 300), + idleTimeout: $idleTimeout, + forceOutput: $forceOutput, + secrets: $secrets, + ); + + $dotenv = get('dotenv', false); + if (!empty($dotenv)) { + $runParams->dotenv = $dotenv; + } + + $run = function (string $command, ?RunParams $params = null) use ($runParams): string { + $params = $params ?? $runParams; + $host = currentHost(); + $command = parse($command); + if ($host instanceof Localhost) { + $process = Deployer::get()->processRunner; + $output = $process->run($host, $command, $params); + } else { + $client = Deployer::get()->sshClient; + $output = $client->run($host, $command, $params); + } + return rtrim($output); + }; + + if (preg_match('/^sudo\b/', $command)) { + try { + return $run($command); + } catch (RunException) { + $askpass = get('sudo_askpass', '/tmp/dep_sudo_pass'); + $password = get('sudo_pass', false); + if ($password === false) { + writeln("run $command"); + $password = askHiddenResponse(" [sudo] password for {{remote_user}}: "); + } + $run("echo -e '#!/bin/sh\necho \"\$PASSWORD\"' > $askpass"); + $run("chmod a+x $askpass"); + $command = preg_replace('/^sudo\b/', 'sudo -A', $command); + $output = $run(" SUDO_ASKPASS=$askpass PASSWORD=%sudo_pass% $command", $runParams->with( + secrets: ['sudo_pass' => quote($password)], + )); + $run("rm $askpass"); + return $output; + } + } else { + return $run($command); + } +} + + +/** + * Run a command on the local machine and return its trimmed stdout. + * + * ```php + * $branch = runLocally('git rev-parse --abbrev-ref HEAD'); + * runLocally('npm run build', timeout: 600); + * ``` + * + * @param string $command Command to run locally. + * @param string|null $cwd Working directory for this run. Defaults to `{{working_path}}`. + * @param int|null $timeout Max runtime in seconds (default 300, `null` disables). + * @param int|null $idleTimeout Max seconds without output before aborting. + * @param array|null $secrets Map of `%name%` placeholders to redacted values. + * @param array|null $env Environment variables: `runLocally('echo $KEY', env: ['KEY' => 'value']);` + * @param bool|null $forceOutput Print command output in real time. + * @param bool|null $nothrow Return output instead of throwing on non-zero exit. + * @param string|null $shell Shell to run in. Default `bash -s`. + * + * @throws RunException + * @throws TimeoutException + */ +function runLocally( + string $command, + ?string $cwd = null, + ?int $timeout = null, + ?int $idleTimeout = null, + #[\SensitiveParameter] + ?array $secrets = null, + ?array $env = null, + ?bool $forceOutput = false, + ?bool $nothrow = false, + ?string $shell = null, +): string { + $runParams = new RunParams( + shell: $shell ?? 'bash -s', + cwd: $cwd, + env: $env, + nothrow: $nothrow, + timeout: $timeout, + idleTimeout: $idleTimeout, + forceOutput: $forceOutput, + secrets: $secrets, + ); + + $process = Deployer::get()->processRunner; + $command = parse($command); + + $output = $process->run(new Localhost(), $command, $runParams); + return rtrim($output); +} + +/** + * Return whether a shell test command succeeds on the remote host. + * + * ```php + * if (test('[ -d {{release_path}} ]')) { + * run('rm -rf {{release_path}}'); + * } + * ``` + */ +function test(string $command): bool +{ + $true = '+' . array_rand(array_flip(['accurate', 'appropriate', 'correct', 'legitimate', 'precise', 'right', 'true', 'yes', 'indeed'])); + return trim(run("if $command; then echo $true; fi")) === $true; +} + +/** + * Return whether a shell test command succeeds on the local machine. + * + * ```php + * if (testLocally('[ -f .env ]')) { + * upload('.env', '{{release_path}}/.env'); + * } + * ``` + */ +function testLocally(string $command): bool +{ + return runLocally("if $command; then echo +true; fi") === '+true'; +} + +/** + * Run a callback on each given host. + * + * ```php + * on(select('stage=prod, role=db'), function (Host $host) { + * run('mysqldump app > /tmp/backup.sql'); + * }); + * + * on(host('example.org'), function (Host $host) { + * run('uptime'); + * }); + * + * on(Deployer::get()->hosts, function (Host $host) { + * run('uname -a'); + * }); + * ``` + * + * @param Host|Host[] $hosts + */ +function on($hosts, callable $callback): void +{ + if (!is_array($hosts) && !($hosts instanceof \Traversable)) { + $hosts = [$hosts]; + } + + foreach ($hosts as $host) { + if ($host instanceof Host) { + $host->config()->load(); + Context::push(new Context($host)); + try { + $callback($host); + $host->config()->save(); + } catch (GracefulShutdownException $e) { + Deployer::get()->logger->renderException($e, $host); + } finally { + Context::pop(); + } + } else { + throw new \InvalidArgumentException("Function on can iterate only on Host instances."); + } + } +} + +/** + * Run another task by name from inside the current task. + * + * ```php + * task('deploy', function () { + * invoke('deploy:update_code'); + * invoke('deploy:symlink'); + * }); + * ``` + * + * @throws Exception + */ +function invoke(string $taskName): void +{ + $task = Deployer::get()->tasks->get($taskName); + Deployer::get()->logger->startTask($task); + $task->run(Context::get()); + Deployer::get()->logger->endTask($task); +} + +/** + * Upload files or directories to the current host via rsync. + * + * To copy the *contents* of a directory, end the source with `/`: + * ```php + * upload('build/', '{{release_path}}/public'); // copies contents of build/ + * upload('build', '{{release_path}}/public'); // copies the build/ dir itself + * ``` + * + * `$config` keys: + * - `flags`: replaces the default `-azP` flags + * - `options`: extra flags appended to the rsync command + * - `timeout`: process timeout in seconds (`null` = no limit) + * - `progress_bar`: show transfer progress + * - `display_stats`: show rsync statistics + * + * Note: PHP shell-escaping breaks rsync's `--exclude={'a','b'}` brace list. + * Pass each exclude as its own `--exclude=...` option, or use `--exclude-from=`. + * + * @param string|string[] $source + * @param array $config + * @phpstan-param array{flags?: string, options?: array, timeout?: int|null, progress_bar?: bool, display_stats?: bool} $config + * + * @throws RunException + */ +function upload($source, string $destination, array $config = []): void +{ + $rsync = Deployer::get()->rsync; + $host = currentHost(); + $source = is_array($source) ? array_map('Deployer\parse', $source) : parse($source); + $destination = parse($destination); + + if ($host instanceof Localhost) { + $rsync->call($host, $source, $destination, $config); + } else { + $rsync->call($host, $source, "{$host->connectionString()}:$destination", $config); + } +} + +/** + * Download a file or directory from the current host via rsync. + * + * ```php + * download('{{deploy_path}}/.dep/database.sql', 'backup/database.sql'); + * ``` + * + * `$config` accepts the same keys as `upload()`. + * + * @param array $config + * + * @throws RunException + */ +function download(string $source, string $destination, array $config = []): void +{ + $rsync = Deployer::get()->rsync; + $host = currentHost(); + $source = parse($source); + $destination = parse($destination); + + if ($host instanceof Localhost) { + $rsync->call($host, $source, $destination, $config); + } else { + $rsync->call($host, "{$host->connectionString()}:$source", $destination, $config); + } +} + +/** + * Print an info message, prefixed with `info` and the host alias. + * + * ```php + * info('Deployed release {{release_name}}'); + * ``` + */ +function info(string $message): void +{ + writeln("info " . parse($message)); +} + +/** + * Print a warning message. + */ +function warning(string $message): void +{ + $message = "warning $message"; + + if (Context::has()) { + writeln($message); + } else { + Deployer::get()->output->writeln($message); + } +} + +/** + * Write a line to the output, prefixed with the current host alias. + * + * The message is parsed for `{{config}}` placeholders before printing. + */ +function writeln(string $message, int $options = 0): void +{ + $host = currentHost(); + output()->writeln("[$host] " . parse($message), $options); +} + +/** + * Replace `{{name}}` placeholders in a string with values from the current config. + * + * ```php + * $current = parse('{{deploy_path}}/current'); + * ``` + */ +function parse(string $value): string +{ + return Context::get()->getConfig()->parse($value); +} + +/** + * Set a [global config](https://deployer.org/docs/8.x/basics#global-configurations) value. + * + * ```php + * set('keep_releases', 5); + * set('shared_files', ['.env']); + * ``` + * + * @param mixed $value + * @throws Exception + */ +function set(string $name, $value): void +{ + if (!Context::has()) { + Deployer::get()->config->set($name, $value); + } else { + Context::get()->getConfig()->set($name, $value); + } +} + +/** + * Append values to an array config option. + * + * ```php + * add('shared_files', ['.env']); + * add('shared_dirs', ['storage/logs']); + * ``` + */ +function add(string $name, array $array): void +{ + if (!Context::has()) { + Deployer::get()->config->add($name, $array); + } else { + Context::get()->getConfig()->add($name, $array); + } +} + +/** + * Return a config value, or `$default` if it isn't set. + * + * Callable values are resolved on first access and cached. + * + * ```php + * $branch = get('branch', 'main'); + * ``` + * + * @param mixed|null $default + * + * @return mixed + */ +function get(string $name, $default = null) +{ + if (!Context::has()) { + return Deployer::get()->config->get($name, $default); + } else { + return Context::get()->getConfig()->get($name, $default); + } +} + +/** + * Return whether a config option is set. + */ +function has(string $name): bool +{ + if (!Context::has()) { + return Deployer::get()->config->has($name); + } else { + return Context::get()->getConfig()->has($name); + } +} + +/** + * Prompt the user for a string, on the current host. + * + * Returns `$default` in quiet mode (`-q`). + * + * ```php + * $branch = ask('Branch to deploy?', 'main'); + * $tag = ask('Tag?', null, ['v1.0', 'v1.1', 'v1.2']); + * ``` + */ +function ask(string $message, ?string $default = null, ?array $autocomplete = null): ?string +{ + if (WillAskUser::$noAsk) { + throw new WillAskUser($message); + } + Context::required(__FUNCTION__); + + if (output()->isQuiet()) { + return $default; + } + + if (Deployer::isWorker()) { + return Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args()); + } + + /** @var QuestionHelper */ + $helper = Deployer::get()->getHelper('question'); + + $tag = currentHost()->getTag(); + $message = parse($message); + $message = "[$tag] $message " . (($default === null) ? "" : "(default: $default) "); + + $question = new Question($message, $default); + if (!empty($autocomplete)) { + $question->setAutocompleterValues($autocomplete); + } + + return $helper->ask(input(), output(), $question); +} + +/** + * Prompt the user to pick one or more values from a list. + * + * ```php + * $color = askChoice('Pick a color', ['red', 'green', 'blue'], 0); + * ``` + * + * @param mixed $default Key into `$availableChoices`, or null for no default. + * @return mixed Selected value (or array of values when `$multiselect` is true). + * @throws Exception + */ +function askChoice(string $message, array $availableChoices, $default = null, bool $multiselect = false) +{ + if (WillAskUser::$noAsk) { + throw new WillAskUser($message); + } + Context::required(__FUNCTION__); + + if (empty($availableChoices)) { + throw new \InvalidArgumentException('Available choices should not be empty'); + } + + if ($default !== null && !array_key_exists($default, $availableChoices)) { + throw new \InvalidArgumentException('Default choice is not available'); + } + + if (output()->isQuiet()) { + if ($default === null) { + $default = key($availableChoices); + } + return [$default => $availableChoices[$default]]; + } + + if (Deployer::isWorker()) { + return Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args()); + } + + /** @var QuestionHelper */ + $helper = Deployer::get()->getHelper('question'); + + $tag = currentHost()->getTag(); + $message = parse($message); + $message = "[$tag] $message " . (($default === null) ? "" : "(default: $default) "); + + $question = new ChoiceQuestion($message, $availableChoices, $default); + $question->setMultiselect($multiselect); + + return $helper->ask(input(), output(), $question); +} + +/** + * Prompt the user with a yes/no question. Returns `$default` in quiet mode. + * + * ```php + * if (askConfirmation('Drop the database?')) { + * run('mysql -e "DROP DATABASE app"'); + * } + * ``` + */ +function askConfirmation(string $message, bool $default = false): bool +{ + if (WillAskUser::$noAsk) { + throw new WillAskUser($message); + } + Context::required(__FUNCTION__); + + if (output()->isQuiet()) { + return $default; + } + + if (Deployer::isWorker()) { + return Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args()); + } + + /** @var QuestionHelper */ + $helper = Deployer::get()->getHelper('question'); + + $yesOrNo = $default ? 'Y/n' : 'y/N'; + $tag = currentHost()->getTag(); + $message = parse($message); + $message = "[$tag] $message [$yesOrNo] "; + + $question = new ConfirmationQuestion($message, $default); + + return $helper->ask(input(), output(), $question); +} + +/** + * Prompt the user for input without echoing it. Use for passwords and tokens. + */ +function askHiddenResponse(string $message): string +{ + if (WillAskUser::$noAsk) { + throw new WillAskUser($message); + } + Context::required(__FUNCTION__); + + if (output()->isQuiet()) { + return ''; + } + + if (Deployer::isWorker()) { + return (string) Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args()); + } + + /** @var QuestionHelper */ + $helper = Deployer::get()->getHelper('question'); + + $tag = currentHost()->getTag(); + $message = parse($message); + $message = "[$tag] $message "; + + $question = new Question($message); + $question->setHidden(true); + $question->setHiddenFallback(false); + + return (string) $helper->ask(input(), output(), $question); +} + +/** + * Return the Symfony Console input, e.g. to read CLI options. + * + * ```php + * $tag = input()->getOption('tag'); + * ``` + */ +function input(): InputInterface +{ + return Deployer::get()->input; +} + +/** + * Return the Symfony Console output, e.g. to check verbosity. + */ +function output(): OutputInterface +{ + return Deployer::get()->output; +} + +/** + * Return whether a command is available on the current host's PATH. + * + * ```php + * if (!commandExist('git')) { + * throw error('git is required to deploy'); + * } + * ``` + * + * @throws RunException + */ +function commandExist(string $command): bool +{ + return test("hash $command 2>/dev/null"); +} + +/** + * Return whether a command's man page or `--help` output mentions the given option. + * + * Useful for picking newer flags only when the installed version supports them. + * + * ```php + * $progress = commandSupportsOption('rsync', '--info=progress2') ? '--info=progress2' : '--progress'; + * ``` + * + * @throws RunException + */ +function commandSupportsOption(string $command, string $option): bool +{ + $man = run("(man $command 2>&1 || $command -h 2>&1 || $command --help 2>&1) | grep -- $option || true"); + if (empty($man)) { + return false; + } + return str_contains($man, $option); +} + +/** + * Return the absolute path of a command on the current host. + * + * Tries `command -v`, then `which`, then `type -p`. + * + * ```php + * $php = which('php'); + * run("$php -v"); + * ``` + * + * @throws RunException + * @throws \RuntimeException If the command is not found. + */ +function which(string $name): string +{ + $nameQuoted = quote($name); + + // Try `command`, should cover all Bourne-like shells + // Try `which`, should cover most other cases + // Fallback to `type` command, if the rest fails + $path = run("command -v $nameQuoted || which $nameQuoted || type -p $nameQuoted"); + if (empty($path)) { + throw new \RuntimeException("Can't locate [$nameQuoted] - neither of [command|which|type] commands are available"); + } + + // Deal with issue when `type -p` outputs something like `type -ap` in some implementations + return trim(str_replace("$name is", "", $path)); + +} + +/** + * Return the remote host's environment as an associative array. + * + * ```php + * $remotePath = remoteEnv()['PATH']; + * run('echo $PATH', env: ['PATH' => "/home/user/bin:$remotePath"]); + * ``` + */ +function remoteEnv(): array +{ + $vars = []; + $data = run('env'); + foreach (explode("\n", $data) as $line) { + [$name, $value] = explode('=', $line, 2); + $vars[$name] = $value; + } + return $vars; +} + +/** + * Build a Deployer `Exception` with `{{config}}` placeholders parsed in the message. + * + * ```php + * if (!commandExist('git')) { + * throw error('git is required on {{alias}}'); + * } + * ``` + */ +function error(string $message): Exception +{ + return new Exception(parse($message)); +} + +/** + * Return the current UTC timestamp in ISO 8601 format. + */ +function timestamp(): string +{ + return (new \DateTime('now', new \DateTimeZone('UTC')))->format(\DateTime::ISO8601); +} + +/** + * Quote a string for safe use as a shell argument (ANSI-C `$'...'` syntax). + * + * Strings made of safe characters (alphanumeric, `/.-+@:=,%`) are returned unquoted. + * Throws on null bytes. + * + * ```php + * run('git log --format=' . quote($format)); + * run('echo ' . quote("it's a test")); // echo $'it\'s a test' + * ``` + */ +function quote(string|int $arg): string +{ + $arg = (string) $arg; + if ($arg === '') { + return "\$''"; + } + if (str_contains($arg, "\0")) { + throw new \InvalidArgumentException('quote(): null byte is not allowed in shell arguments'); + } + if (preg_match('/^[\w\/.\-+@:=,%]+$/', $arg)) { + return $arg; + } + return "\$'" . strtr($arg, [ + '\\' => '\\\\', + "'" => "\\'", + "\f" => '\\f', + "\n" => '\\n', + "\r" => '\\r', + "\t" => '\\t', + "\v" => '\\v', + ]) . "'"; +} + +/** + * Make an HTTP request and return the response body. + * + * Pass `$info` by reference to capture status code, headers, and timing + * (same shape as PHP's `curl_getinfo`). Use `nothrow: true` to receive the + * body even on non-2xx responses. + * + * ```php + * $body = fetch('{{domain}}/health', info: $info); + * if ($info['http_code'] !== 200) { + * throw error("health check failed: {$info['http_code']}"); + * } + * + * fetch('https://api.example.com/notify', 'post', + * ['Authorization' => 'Bearer {{token}}'], + * json_encode(['release' => '{{release_name}}']), + * ); + * ``` + */ +function fetch(string $url, string $method = 'get', array $headers = [], ?string $body = null, ?array &$info = null, bool $nothrow = false): string +{ + $url = parse($url); + $http = match (strtolower($method)) { + 'get' => Httpie::get($url), + 'post' => Httpie::post($url), + 'put' => Httpie::put($url), + 'patch' => Httpie::patch($url), + 'delete' => Httpie::delete($url), + default => throw new \InvalidArgumentException("Unknown method \"$method\"."), + }; + $http = $http->nothrow($nothrow); + foreach ($headers as $key => $value) { + $http = $http->header($key, $value); + } + if ($body !== null) { + $http = $http->body($body); + } + return $http->send($info)->body(); +} diff --git a/vendor/deployer/deployer/src/schema.json b/vendor/deployer/deployer/src/schema.json new file mode 100644 index 0000000..7612e08 --- /dev/null +++ b/vendor/deployer/deployer/src/schema.json @@ -0,0 +1,267 @@ +{ + "$id": "https:\/\/deployer.org\/schema.json#", + "$schema": "http:\/\/json-schema.org\/draft-07\/schema#", + "type": "object", + "properties": { + "import": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "config": { + "type": "object", + "additionalProperties": true + }, + "hosts": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": true + } + }, + "tasks": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "cd": { + "type": "string" + } + }, + "required": [ + "cd" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "run": { + "type": "string" + }, + "cd": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "secrets": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "nothrow": { + "type": "boolean" + }, + "forceOutput": { + "type": "boolean" + }, + "timeout": { + "type": "number" + }, + "idleTimeout": { + "type": "number" + } + }, + "required": [ + "run" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "runLocally": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "timeout": { + "type": "number" + }, + "idleTimeout": { + "type": "number" + }, + "secrets": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "nothrow": { + "type": "boolean" + }, + "forceOutput": { + "type": "boolean" + }, + "shell": { + "type": "string" + } + }, + "required": [ + "runLocally" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "upload": { + "type": "object", + "properties": { + "src": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "dest": { + "type": "string" + } + }, + "required": [ + "src", + "dest" + ], + "additionalProperties": false + } + }, + "required": [ + "upload" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "download": { + "type": "object", + "properties": { + "src": { + "type": "string" + }, + "dest": { + "type": "string" + } + }, + "required": [ + "src", + "dest" + ], + "additionalProperties": false + } + }, + "required": [ + "download" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "desc": { + "type": "string" + }, + "once": { + "type": "boolean" + }, + "hidden": { + "type": "boolean" + }, + "limit": { + "type": "number" + }, + "select": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "before": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "after": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "fail": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/vendor/maml/maml/.php-cs-fixer.php b/vendor/maml/maml/.php-cs-fixer.php new file mode 100644 index 0000000..80eee96 --- /dev/null +++ b/vendor/maml/maml/.php-cs-fixer.php @@ -0,0 +1,16 @@ +in([__DIR__ . '/src', __DIR__ . '/tests']); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PER-CS' => true, + 'declare_strict_types' => true, + 'no_unused_imports' => true, + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'single_quote' => true, + 'trailing_comma_in_multiline' => true, + ]) + ->setFinder($finder) + ->setRiskyAllowed(true); diff --git a/vendor/maml/maml/LICENSE b/vendor/maml/maml/LICENSE new file mode 100644 index 0000000..5d90ac8 --- /dev/null +++ b/vendor/maml/maml/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Anton Medvedev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/maml/maml/README.md b/vendor/maml/maml/README.md new file mode 100644 index 0000000..5f80f60 --- /dev/null +++ b/vendor/maml/maml/README.md @@ -0,0 +1,165 @@ +# maml-php + +[MAML](https://maml.dev) parser for PHP. Includes a full AST with source positions, comment preservation, and pretty printing. + +- Spec-accurate parser and serializer +- Full AST with source positions (offset, line, column) on every node +- Comments preserved and attached to nearest nodes +- Schema validation with detailed error reporting +- `printAst()` reconstructs source from AST, including comments +- `errorSnippet()` for user-friendly error messages pointing at source locations +- Zero dependencies +- 100% test coverage + +## Installation + +``` +composer require maml/maml +``` + +Requires PHP 8.2+ with `mbstring`. + +## Quick Start + +```php +use Maml\Maml; + +// Parse to plain PHP values +$data = Maml::parse('{name: "MAML", version: 1}'); +$data['name']; // "MAML" + +// Serialize back to MAML +Maml::stringify(['name' => 'MAML', 'version' => 1]); +// { +// name: "MAML" +// version: 1 +// } +``` + +## AST + +```php +$source = '{ + # Database config + host: "localhost" + port: 5432 +}'; + +$doc = Maml::parseAst($source); +``` + +Every node has a `type` string and a `span` with start/end positions: + +```php +$doc->value->type; // "Object" +$doc->value->span->start->line; // 1 +$doc->value->properties[0]->key->value; // "host" +``` + +### Printing + +`printAst()` reconstructs MAML source from an AST, preserving comments and blank lines: + +```php +Maml::printAst($doc); +// { +// # Database config +// host: "localhost" +// port: 5432 +// } +``` + +### Converting to plain values + +`toValue()` strips AST metadata and returns plain PHP values: + +```php +Maml::toValue($doc); // ["host" => "localhost", "port" => 5432] +``` + +### Error snippets + +Point at any AST node in source for user-friendly error messages. Accepts `Position` (single `^`) or `Span` (underline `^^^^`): + +```php +$node = $doc->value->properties[1]->value; +Maml::errorSnippet($source, $node->span, 'Port out of range'); +// Port out of range on line 4. +// +// port: 5432 +// ^^^^ +``` + +Show context lines and Rust-style gutter with line numbers: + +```php +Maml::errorSnippet($source, $node->span, 'Port out of range', context: 2, gutter: true); +// Port out of range on line 4. +// +// 2 | host: "localhost" +// 3 | port: 5432 +// 4 | timeout: -1 +// | ^^ +``` + +Options: `context` (lines above), `indent` (default `' '`), `gutter` (line numbers). + +## Schema Validation + +Define expected shapes with the `S` builder, validate against a parsed AST: + +```php +use Maml\Schema\S; + +$schema = S::object([ + 'host' => S::string(), + 'port' => S::integer(min: 1, max: 65535), + 'tags' => S::arrayOf(S::string(), minItems: 1), + 'ssl' => S::optional(S::boolean()), + 'mode' => S::enum('fast', 'safe', 'auto'), + 'version' => S::optional(S::string(pattern: '/^\d+\.\d+\.\d+$/')), +]); + +$doc = Maml::parseAst($source); +$errors = Maml::validate($doc, $schema); + +foreach ($errors as $error) { + // $error->message "Expected integer, got string" + // $error->path "$.port" + // $error->span Span(start: Position(line: 3, ...), end: ...) + echo Maml::errorSnippet($source, $error->span, $error->message); +} +``` + +### Available schema types + +| Builder | Matches | +|------------------------------------|----------------------------------------------------| +| `S::string()` | String or raw string | +| `S::string(pattern: '/.../')` | String matching regex | +| `S::integer()` | Integer | +| `S::integer(min: 0, max: 100)` | Integer within range | +| `S::float()` | Float | +| `S::float(min: 0.0, max: 1.0)` | Float within range | +| `S::number()` | Integer or float | +| `S::number(min: 0)` | Number with minimum | +| `S::boolean()` | Boolean | +| `S::null()` | Null | +| `S::any()` | Anything | +| `S::literal('x')` | Exact value | +| `S::enum('a', 'b')` | One of the listed values | +| `S::object([...])` | Object with typed properties, rejects unknown keys | +| `S::object([...], S::any())` | Same, but allows extra keys | +| `S::object([...], S::string())` | Same, extra keys must match schema | +| `S::orderedObject([...])` | Object with properties in order | +| `S::map(schema)` | Object with any keys, typed values | +| `S::optional(schema)` | Property may be absent | +| `S::arrayOf(schema)` | Array of uniform type | +| `S::arrayOf(schema, minItems: 1)` | Array with minimum length | +| `S::arrayOf(schema, maxItems: 10)` | Array with maximum length | +| `S::tuple([s1, s2])` | Fixed-length array | +| `S::union(s1, s2)` | One of several schemas | + +## License + +[MIT](LICENSE) diff --git a/vendor/maml/maml/composer.json b/vendor/maml/maml/composer.json new file mode 100644 index 0000000..3b7a36b --- /dev/null +++ b/vendor/maml/maml/composer.json @@ -0,0 +1,39 @@ +{ + "name": "maml/maml", + "description": "MAML Parser", + "type": "library", + "license": "MIT", + "keywords": [ + "maml", + "parser", + "serializer", + "config", + "configuration" + ], + "homepage": "https://maml.dev", + "autoload": { + "psr-4": { + "Maml\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Maml\\Tests\\": "tests/" + } + }, + "authors": [ + { + "name": "Anton Medvedev", + "email": "anton@medv.io" + } + ], + "require": { + "php": ">=8.2", + "ext-mbstring": "*" + }, + "require-dev": { + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "phpstan/phpstan": "^2.1", + "php-cs-fixer/shim": "^3.94" + } +} diff --git a/vendor/maml/maml/phpstan.neon b/vendor/maml/maml/phpstan.neon new file mode 100644 index 0000000..13ec8a3 --- /dev/null +++ b/vendor/maml/maml/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: max + paths: + - src + - tests diff --git a/vendor/maml/maml/src/Annotated.php b/vendor/maml/maml/src/Annotated.php new file mode 100644 index 0000000..533a000 --- /dev/null +++ b/vendor/maml/maml/src/Annotated.php @@ -0,0 +1,81 @@ +leadingComments = $leadingComments; + $this->trailingComment = $trailingComment; + $this->emptyLineBefore = $emptyLineBefore; + $this->danglingComments = $danglingComments; + } + + public static function with(mixed $value): self + { + return new self($value); + } + + public function comment(string ...$comments): self + { + return new self( + $this->value, + [...$this->leadingComments, ...$comments], + $this->trailingComment, + $this->emptyLineBefore, + $this->danglingComments, + ); + } + + public function trailingComment(string $comment): self + { + return new self( + $this->value, + $this->leadingComments, + $comment, + $this->emptyLineBefore, + $this->danglingComments, + ); + } + + public function emptyLineBefore(): self + { + return new self( + $this->value, + $this->leadingComments, + $this->trailingComment, + true, + $this->danglingComments, + ); + } + + public function danglingComment(string ...$comments): self + { + return new self( + $this->value, + $this->leadingComments, + $this->trailingComment, + $this->emptyLineBefore, + [...$this->danglingComments, ...$comments], + ); + } +} diff --git a/vendor/maml/maml/src/Ast/ArrayNode.php b/vendor/maml/maml/src/Ast/ArrayNode.php new file mode 100644 index 0000000..927640e --- /dev/null +++ b/vendor/maml/maml/src/Ast/ArrayNode.php @@ -0,0 +1,19 @@ +type = 'Boolean'; + } +} diff --git a/vendor/maml/maml/src/Ast/CommentNode.php b/vendor/maml/maml/src/Ast/CommentNode.php new file mode 100644 index 0000000..4b84cad --- /dev/null +++ b/vendor/maml/maml/src/Ast/CommentNode.php @@ -0,0 +1,17 @@ +type = 'Comment'; + } +} diff --git a/vendor/maml/maml/src/Ast/Document.php b/vendor/maml/maml/src/Ast/Document.php new file mode 100644 index 0000000..1f6b8ed --- /dev/null +++ b/vendor/maml/maml/src/Ast/Document.php @@ -0,0 +1,21 @@ +type = 'Float'; + } +} diff --git a/vendor/maml/maml/src/Ast/IdentifierKey.php b/vendor/maml/maml/src/Ast/IdentifierKey.php new file mode 100644 index 0000000..5828e37 --- /dev/null +++ b/vendor/maml/maml/src/Ast/IdentifierKey.php @@ -0,0 +1,17 @@ +type = 'Identifier'; + } +} diff --git a/vendor/maml/maml/src/Ast/IntegerNode.php b/vendor/maml/maml/src/Ast/IntegerNode.php new file mode 100644 index 0000000..2bc1b67 --- /dev/null +++ b/vendor/maml/maml/src/Ast/IntegerNode.php @@ -0,0 +1,18 @@ +type = 'Integer'; + } +} diff --git a/vendor/maml/maml/src/Ast/NullNode.php b/vendor/maml/maml/src/Ast/NullNode.php new file mode 100644 index 0000000..2b3ac20 --- /dev/null +++ b/vendor/maml/maml/src/Ast/NullNode.php @@ -0,0 +1,18 @@ +type = 'Null'; + $this->value = null; + } +} diff --git a/vendor/maml/maml/src/Ast/ObjectNode.php b/vendor/maml/maml/src/Ast/ObjectNode.php new file mode 100644 index 0000000..71fa560 --- /dev/null +++ b/vendor/maml/maml/src/Ast/ObjectNode.php @@ -0,0 +1,19 @@ +type = 'RawString'; + } +} diff --git a/vendor/maml/maml/src/Ast/Span.php b/vendor/maml/maml/src/Ast/Span.php new file mode 100644 index 0000000..b4ef17c --- /dev/null +++ b/vendor/maml/maml/src/Ast/Span.php @@ -0,0 +1,13 @@ +type = 'String'; + } +} diff --git a/vendor/maml/maml/src/AstParser.php b/vendor/maml/maml/src/AstParser.php new file mode 100644 index 0000000..72de514 --- /dev/null +++ b/vendor/maml/maml/src/AstParser.php @@ -0,0 +1,828 @@ + '"', + '\\' => '\\', + 'n' => "\n", + 'r' => "\r", + 't' => "\t", + ]; + + private string $source; + private int $pos = 0; + private int $lineNumber = 1; + private int $columnNumber = 0; + private string $ch = ''; + private bool $done = false; + + /** @var CommentNode[] */ + private array $comments = []; + + private function __construct(string $source) + { + $this->source = $source; + $this->next(); + } + + public static function parse(string $source): Document + { + $parser = new self($source); + $docStart = new Position(0, 1, 1); + + $value = $parser->parseValue(); + $parser->skipWhitespace(); + + if (!$parser->done) { + throw new ParseException($parser->errorSnippet()); + } + + $parser->expectValue($value); + $docEnd = $parser->here(); + + $doc = new Document($value, new Span($docStart, $docEnd)); + self::attachComments($doc, $parser->comments, $source); + self::attachBlankLines($doc->value, $source); + return $doc; + } + + private function next(): void + { + if ($this->pos < \strlen($this->source)) { + $this->ch = $this->source[$this->pos]; + $this->pos++; + if ($this->ch === "\n") { + $this->lineNumber++; + $this->columnNumber = 0; + } else { + $this->columnNumber++; + } + } else { + $this->ch = ''; + $this->done = true; + } + } + + private function here(): Position + { + if ($this->done) { + return new Position( + \strlen($this->source), + $this->lineNumber, + $this->columnNumber + 1, + ); + } + return new Position( + $this->pos - 1, + $this->lineNumber, + $this->columnNumber, + ); + } + + private function lookahead(int $n): string + { + return \substr($this->source, $this->pos, $n); + } + + private function parseValue(): StringNode|RawStringNode|IntegerNode|FloatNode|BooleanNode|NullNode|ObjectNode|ArrayNode|null + { + $this->skipWhitespace(); + return $this->parseRawString() + ?? $this->parseString() + ?? $this->parseNumber() + ?? $this->parseObject() + ?? $this->parseArray() + ?? $this->parseKeyword('true') + ?? $this->parseKeyword('false') + ?? $this->parseKeyword('null'); + } + + private function parseString(): ?StringNode + { + if ($this->ch !== '"') { + return null; + } + $start = $this->here(); + $str = ''; + $escaped = false; + while (true) { + $this->next(); + if ($this->done) { + throw new ParseException($this->errorSnippet()); + } + if ($escaped) { + if ($this->ch === 'u') { + $this->next(); + if ($this->ch !== '{') { + throw new ParseException( + $this->errorSnippet( + 'Invalid escape sequence ' . $this->formatChar($this->ch) . ' (expected "{")', + ), + ); + } + $hex = ''; + while (true) { + $this->next(); + if ($this->ch === '}') { + break; + } + if (!$this->isHexDigit($this->ch)) { + throw new ParseException( + $this->errorSnippet( + 'Invalid escape sequence ' . $this->formatChar($this->ch), + ), + ); + } + $hex .= $this->ch; + if (\strlen($hex) > 6) { + throw new ParseException( + $this->errorSnippet('Invalid escape sequence (too many hex digits)'), + ); + } + } + if ($hex === '') { + throw new ParseException( + $this->errorSnippet('Invalid escape sequence'), + ); + } + $codePoint = (int) \hexdec($hex); + if ($codePoint > 0x10FFFF || ($codePoint >= 0xD800 && $codePoint <= 0xDFFF)) { + throw new ParseException( + $this->errorSnippet('Invalid escape sequence (out of range)'), + ); + } + $str .= self::codePointToUtf8($codePoint); + } else { + $escapedChar = self::ESCAPE_MAP[$this->ch] ?? null; + if ($escapedChar === null) { + throw new ParseException( + $this->errorSnippet( + 'Invalid escape sequence ' . $this->formatChar($this->ch), + ), + ); + } + $str .= $escapedChar; + } + $escaped = false; + } elseif ($this->ch === '\\') { + $escaped = true; + } elseif ($this->ch === '"') { + break; + } elseif ($this->ch === "\n") { + throw new ParseException($this->errorSnippet()); + } elseif (($this->ch < "\x20" && $this->ch !== "\t") || $this->ch === "\x7F") { + throw new ParseException($this->errorSnippet()); + } else { + $str .= $this->ch; + } + } + $this->next(); + $end = $this->here(); + $raw = \substr($this->source, $start->offset, $end->offset - $start->offset); + return new StringNode($str, $raw, new Span($start, $end)); + } + + private function parseRawString(): ?RawStringNode + { + if ($this->ch !== '"' || $this->lookahead(2) !== '""') { + return null; + } + $start = $this->here(); + $this->next(); + $this->next(); + $this->next(); + $hasLeadingNewline = false; + if ($this->ch === "\r" && $this->lookahead(1) === "\n") { + $this->next(); + } + if ($this->ch === "\n") { + $hasLeadingNewline = true; + $this->next(); + } + + $str = ''; + while (!$this->done) { + if ($this->ch === '"' && $this->lookahead(2) === '""') { + $this->next(); + $this->next(); + $this->next(); + if ($str === '' && !$hasLeadingNewline) { + throw new ParseException($this->errorSnippet('Raw strings cannot be empty')); + } + $end = $this->here(); + $raw = \substr($this->source, $start->offset, $end->offset - $start->offset); + return new RawStringNode($str, $raw, new Span($start, $end)); + } + $str .= $this->ch; + $this->next(); + } + throw new ParseException($this->errorSnippet()); + } + + private function parseNumber(): IntegerNode|FloatNode|null + { + if (!$this->isDigit($this->ch) && $this->ch !== '-') { + return null; + } + $start = $this->here(); + $numStr = ''; + $float = false; + if ($this->ch === '-') { + $numStr .= $this->ch; + $this->next(); + if (!$this->isDigit($this->ch)) { + throw new ParseException($this->errorSnippet()); + } + } + if ($this->ch === '0') { + $numStr .= $this->ch; + $this->next(); + } else { + while ($this->isDigit($this->ch)) { + $numStr .= $this->ch; + $this->next(); + } + } + if ($this->ch === '.') { + $float = true; + $numStr .= $this->ch; + $this->next(); + if (!$this->isDigit($this->ch)) { + throw new ParseException($this->errorSnippet()); + } + while ($this->isDigit($this->ch)) { + $numStr .= $this->ch; + $this->next(); + } + } + if ($this->ch === 'e' || $this->ch === 'E') { + $float = true; + $numStr .= $this->ch; + $this->next(); + if ($this->ch === '+' || $this->ch === '-') { + $numStr .= $this->ch; + $this->next(); + } + if (!$this->isDigit($this->ch)) { + throw new ParseException($this->errorSnippet()); + } + while ($this->isDigit($this->ch)) { + $numStr .= $this->ch; + $this->next(); + } + } + $end = $this->here(); + $span = new Span($start, $end); + if ($float) { + return new FloatNode((float) $numStr, $numStr, $span); + } + $num = $this->toSafeNumber($numStr); + if (\is_float($num)) { + return new FloatNode($num, $numStr, $span); + } + return new IntegerNode($num, $numStr, $span); + } + + private function parseObject(): ?ObjectNode + { + if ($this->ch !== '{') { + return null; + } + $start = $this->here(); + $this->next(); + $this->skipWhitespace(); + $properties = []; + $seen = []; + if ($this->ch === '}') { + $this->next(); + $end = $this->here(); + return new ObjectNode($properties, new Span($start, $end)); + } + while (true) { + $keyStart = $this->here(); + if ($this->ch === '"') { + $key = $this->parseString() ?? throw new ParseException($this->errorSnippet()); + } else { + $key = $this->parseKey(); + } + if (isset($seen[$key->value])) { + $this->pos = $keyStart->offset + 1; + throw new ParseException( + $this->errorSnippet('Duplicate key ' . \json_encode($key->value)), + ); + } + $seen[$key->value] = true; + $this->skipWhitespace(); + if ($this->ch !== ':') { + throw new ParseException($this->errorSnippet()); + } + $this->next(); + $value = $this->parseValue(); + $this->expectValue($value); + $propSpan = new Span($keyStart, $value->span->end); + $properties[] = new Property($key, $value, $propSpan); + $newlineAfterValue = $this->skipWhitespace(); + if ($this->ch === '}') { + $this->next(); + $end = $this->here(); + return new ObjectNode($properties, new Span($start, $end)); + } elseif ($this->ch === ',') { + $this->next(); + $this->skipWhitespace(); + if ($this->ch === '}') { + $this->next(); + $end = $this->here(); + return new ObjectNode($properties, new Span($start, $end)); + } + } elseif ($newlineAfterValue) { + continue; + } else { + throw new ParseException( + $this->errorSnippet('Expected comma or newline between key-value pairs'), + ); + } + } + } + + private function parseKey(): IdentifierKey + { + $start = $this->here(); + $identifier = ''; + while ($this->isKeyChar($this->ch)) { + $identifier .= $this->ch; + $this->next(); + } + if ($identifier === '') { + throw new ParseException($this->errorSnippet()); + } + $end = $this->here(); + return new IdentifierKey($identifier, new Span($start, $end)); + } + + private function parseArray(): ?ArrayNode + { + if ($this->ch !== '[') { + return null; + } + $start = $this->here(); + $this->next(); + $this->skipWhitespace(); + $elements = []; + if ($this->ch === ']') { + $this->next(); + $end = $this->here(); + return new ArrayNode($elements, new Span($start, $end)); + } + while (true) { + $value = $this->parseValue(); + $this->expectValue($value); + $elements[] = new Element($value); + $newLineAfterValue = $this->skipWhitespace(); + if ($this->ch === ']') { + $this->next(); + $end = $this->here(); + return new ArrayNode($elements, new Span($start, $end)); + } elseif ($this->ch === ',') { + $this->next(); + $this->skipWhitespace(); + if ($this->ch === ']') { + $this->next(); + $end = $this->here(); + return new ArrayNode($elements, new Span($start, $end)); + } + } elseif ($newLineAfterValue) { + continue; + } else { + throw new ParseException( + $this->errorSnippet('Expected comma or newline between values'), + ); + } + } + } + + private function parseKeyword(string $name): BooleanNode|NullNode|null + { + if ($this->ch !== $name[0]) { + return null; + } + $start = $this->here(); + for ($i = 1; $i < \strlen($name); $i++) { + $this->next(); + if ($this->ch !== $name[$i]) { + throw new ParseException($this->errorSnippet()); + } + } + $this->next(); + if ( + $this->isWhitespace($this->ch) + || $this->ch === ',' + || $this->ch === '}' + || $this->ch === ']' + || $this->done + ) { + $end = $this->here(); + $span = new Span($start, $end); + if ($name === 'null') { + return new NullNode($span); + } + return new BooleanNode($name === 'true', $span); + } + throw new ParseException($this->errorSnippet()); + } + + private function skipWhitespace(): bool + { + $hasNewline = false; + while ($this->isWhitespace($this->ch)) { + $hasNewline = $hasNewline || $this->ch === "\n"; + $this->next(); + } + $hasNewlineAfterComment = $this->skipComment(); + return $hasNewline || $hasNewlineAfterComment; + } + + private function skipComment(): bool + { + if ($this->ch === '#') { + $start = $this->here(); + $text = ''; + $this->next(); // skip '#' + while (!$this->done && $this->ch !== "\n") { + $text .= $this->ch; + $this->next(); + } + $end = $this->here(); + $this->comments[] = new CommentNode($text, new Span($start, $end)); + return $this->skipWhitespace(); + } + return false; + } + + private function isWhitespace(string $ch): bool + { + return $ch === ' ' || $ch === "\n" || $ch === "\t" || $ch === "\r"; + } + + private function isHexDigit(string $ch): bool + { + return ($ch >= '0' && $ch <= '9') || ($ch >= 'A' && $ch <= 'F'); + } + + private function isDigit(string $ch): bool + { + return $ch >= '0' && $ch <= '9'; + } + + private function isKeyChar(string $ch): bool + { + return ($ch >= 'A' && $ch <= 'Z') + || ($ch >= 'a' && $ch <= 'z') + || ($ch >= '0' && $ch <= '9') + || $ch === '_' + || $ch === '-'; + } + + private function toSafeNumber(string $str): int|float + { + if ($str === '-0') { + return -0.0; + } + $int = (int) $str; + if ((string) $int === $str) { + return $int; + } + throw new ParseException($this->errorSnippet('Integer overflow')); + } + + /** + * @phpstan-assert !null $value + */ + private function expectValue(mixed $value): void + { + if ($value === null) { + throw new ParseException($this->errorSnippet()); + } + } + + private static function codePointToUtf8(int $cp): string + { + return \mb_chr($cp, 'UTF-8'); + } + + private function formatChar(string $ch): string + { + $ord = \ord($ch); + if ($ch === '' || $ord < 0x80) { + return (string) \json_encode($ch, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE); + } + $bytePos = $this->pos - 1; + $byte = \ord($this->source[$bytePos]); + $len = match (true) { + $byte < 0xC0 => 1, + $byte < 0xE0 => 2, + $byte < 0xF0 => 3, + default => 4, + }; + $fullChar = \substr($this->source, $bytePos, $len); + return (string) \json_encode($fullChar, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE); + } + + private function errorSnippet(string $message = ''): string + { + $customMessage = $message !== ''; + if (!$customMessage) { + if ($this->ch === '' || $this->done) { + $message = 'Unexpected end of input'; + } else { + $message = 'Unexpected character ' . $this->formatChar($this->ch); + } + } + if (!$customMessage && $this->ch === '' && $this->done) { + $message = 'Unexpected end of input'; + } + + $start = \max(0, $this->pos - 40); + $pre = \substr($this->source, $start, $this->pos - $start); + $lines = \explode("\n", $pre); + $lastLine = \end($lines) ?: ''; + $postParts = \explode("\n", \substr($this->source, $this->pos, 40), 2); + $postfix = $postParts[0]; + + if ($lastLine === '') { + $count = \count($lines); + $lastLine = ($count >= 2) ? $lines[$count - 2] : ''; + $lastLine .= ' '; + $this->lineNumber--; + $postfix = ''; + } + + $snippet = " {$lastLine}{$postfix}\n"; + $pointer = ' ' . \str_repeat('.', \max(0, \strlen($lastLine) - 1)) . "^\n"; + return "{$message} on line {$this->lineNumber}.\n\n{$snippet}{$pointer}"; + } + + // ---- Comment attachment ---- + + /** + * @param CommentNode[] $comments + */ + private static function attachComments(Document $doc, array $comments, string $source): void + { + if (\count($comments) === 0) { + return; + } + + $valueStart = $doc->value->span->start->offset; + $valueEnd = $doc->value->span->end->offset; + + $inside = []; + foreach ($comments as $c) { + if ($c->span->start->offset < $valueStart) { + $doc->leadingComments[] = $c; + } elseif ($c->span->start->offset >= $valueEnd) { + $doc->danglingComments[] = $c; + } else { + $inside[] = $c; + } + } + + if (\count($inside) > 0) { + self::distributeComments($doc->value, $inside, $source); + } + } + + /** + * @param CommentNode[] $comments + */ + private static function distributeComments( + StringNode|RawStringNode|IntegerNode|FloatNode|BooleanNode|NullNode|ObjectNode|ArrayNode $node, + array $comments, + string $source, + ): void { + if ($node instanceof ObjectNode) { + self::distributeToObject($node, $comments, $source); + } elseif ($node instanceof ArrayNode) { + self::distributeToArray($node, $comments, $source); + } + } + + /** + * @param CommentNode[] $comments + */ + private static function distributeToObject(ObjectNode $node, array $comments, string $source): void + { + $props = $node->properties; + + if (\count($props) === 0) { + $node->danglingComments = $comments; + return; + } + + foreach ($comments as $c) { + // Check if comment is inside a nested value + $nested = false; + foreach ($props as $prop) { + if ( + $c->span->start->offset >= $prop->value->span->start->offset + && $c->span->start->offset < $prop->value->span->end->offset + ) { + self::distributeComments($prop->value, [$c], $source); + $nested = true; + break; + } + } + if ($nested) { + continue; + } + + // Try to attach as trailing comment (on same line as a property's value) + $attached = false; + foreach ($props as $prop) { + if ( + $c->span->start->offset > $prop->value->span->end->offset + && !self::hasNewlineBetween( + $source, + $prop->value->span->start->offset, + $c->span->start->offset, + ) + ) { + $prop->trailingComment = $c; + $attached = true; + break; + } + } + if ($attached) { + continue; + } + + // Try to attach as leading comment (before a property's key) + foreach ($props as $prop) { + if ($c->span->start->offset < $prop->key->span->start->offset) { + $prop->leadingComments[] = $c; + $attached = true; + break; + } + } + if ($attached) { + continue; + } + + // Dangling comment (after last property, before closing brace) + $node->danglingComments[] = $c; + } + } + + /** + * @param CommentNode[] $comments + */ + private static function distributeToArray(ArrayNode $node, array $comments, string $source): void + { + $elements = $node->elements; + + if (\count($elements) === 0) { + $node->danglingComments = $comments; + return; + } + + foreach ($comments as $c) { + // Check if comment is inside a nested value + $nested = false; + foreach ($elements as $el) { + if ( + $c->span->start->offset >= $el->value->span->start->offset + && $c->span->start->offset < $el->value->span->end->offset + ) { + self::distributeComments($el->value, [$c], $source); + $nested = true; + break; + } + } + if ($nested) { + continue; + } + + // Try to attach as trailing comment (on same line as element's value) + $attached = false; + foreach ($elements as $el) { + if ( + $c->span->start->offset > $el->value->span->end->offset + && !self::hasNewlineBetween( + $source, + $el->value->span->start->offset, + $c->span->start->offset, + ) + ) { + $el->trailingComment = $c; + $attached = true; + break; + } + } + if ($attached) { + continue; + } + + // Try to attach as leading comment (before next element) + foreach ($elements as $el) { + if ($c->span->start->offset < $el->value->span->start->offset) { + $el->leadingComments[] = $c; + $attached = true; + break; + } + } + if ($attached) { + continue; + } + + // Dangling comment (after last element, before closing bracket) + $node->danglingComments[] = $c; + } + } + + // ---- Blank line detection ---- + + private static function attachBlankLines( + StringNode|RawStringNode|IntegerNode|FloatNode|BooleanNode|NullNode|ObjectNode|ArrayNode $node, + string $source, + ): void { + if ($node instanceof ObjectNode) { + $props = $node->properties; + for ($i = 0; $i < \count($props); $i++) { + $regionStart = $i === 0 + ? $node->span->start->offset + 1 + : ($props[$i - 1]->trailingComment !== null + ? $props[$i - 1]->trailingComment->span->end->offset + : $props[$i - 1]->span->end->offset); + + $regionEnd = \count($props[$i]->leadingComments) > 0 + ? $props[$i]->leadingComments[0]->span->start->offset + : $props[$i]->key->span->start->offset; + + $props[$i]->emptyLineBefore = self::hasBlankLine($source, $regionStart, $regionEnd); + self::attachBlankLines($props[$i]->value, $source); + } + } elseif ($node instanceof ArrayNode) { + $elements = $node->elements; + for ($i = 0; $i < \count($elements); $i++) { + $regionStart = $i === 0 + ? $node->span->start->offset + 1 + : ($elements[$i - 1]->trailingComment !== null + ? $elements[$i - 1]->trailingComment->span->end->offset + : $elements[$i - 1]->value->span->end->offset); + + $regionEnd = \count($elements[$i]->leadingComments) > 0 + ? $elements[$i]->leadingComments[0]->span->start->offset + : $elements[$i]->value->span->start->offset; + + $elements[$i]->emptyLineBefore = self::hasBlankLine($source, $regionStart, $regionEnd); + self::attachBlankLines($elements[$i]->value, $source); + } + } + } + + private static function hasNewlineBetween(string $source, int $from, int $to): bool + { + for ($i = $from; $i < $to; $i++) { + if ($source[$i] === "\n") { + return true; + } + } + return false; + } + + private static function hasBlankLine(string $source, int $from, int $to): bool + { + $afterNewline = false; + for ($i = $from; $i < $to; $i++) { + if ($source[$i] === "\n") { + if ($afterNewline) { + return true; + } + $afterNewline = true; + } elseif ($source[$i] !== ' ' && $source[$i] !== "\t" && $source[$i] !== "\r") { + $afterNewline = false; + } + } + return false; + } +} diff --git a/vendor/maml/maml/src/AstPrinter.php b/vendor/maml/maml/src/AstPrinter.php new file mode 100644 index 0000000..7a7c634 --- /dev/null +++ b/vendor/maml/maml/src/AstPrinter.php @@ -0,0 +1,179 @@ +leadingComments as $c) { + $out .= '#' . $c->value . "\n"; + } + $out .= self::doPrint($node->value, 0); + foreach ($node->danglingComments as $c) { + $out .= "\n" . '#' . $c->value; + } + return $out; + } + return self::doPrint($node, 0); + } + + /** + * @param CommentNode[] $comments + */ + private static function printComments(array $comments, string $indent): string + { + $out = ''; + foreach ($comments as $c) { + $out .= $indent . '#' . $c->value . "\n"; + } + return $out; + } + + private static function doPrint( + StringNode|RawStringNode|IntegerNode|FloatNode|BooleanNode|NullNode|ObjectNode|ArrayNode $node, + int $level, + ): string { + if ($node instanceof StringNode) { + return self::quoteString($node->value); + } + if ($node instanceof RawStringNode) { + return $node->raw; + } + if ($node instanceof IntegerNode || $node instanceof FloatNode) { + return $node->raw; + } + if ($node instanceof BooleanNode) { + return $node->value ? 'true' : 'false'; + } + if ($node instanceof NullNode) { + return 'null'; + } + if ($node instanceof ArrayNode) { + $len = \count($node->elements); + $hasComments = \count($node->danglingComments) > 0; + if ($len === 0 && !$hasComments) { + return '[]'; + } + + $childIndent = self::getIndent($level + 1); + $parentIndent = self::getIndent($level); + $out = "[\n"; + + for ($i = 0; $i < $len; $i++) { + $el = $node->elements[$i]; + + if ($i > 0) { + $out .= "\n"; + if ($el->emptyLineBefore) { + $out .= "\n"; + } + } + $out .= self::printComments($el->leadingComments, $childIndent); + $out .= $childIndent . self::doPrint($el->value, $level + 1); + + if ($el->trailingComment !== null) { + $out .= ' #' . $el->trailingComment->value; + } + } + + if (\count($node->danglingComments) > 0) { + if ($len > 0) { + $out .= "\n"; + } + $out .= self::printComments($node->danglingComments, $childIndent); + $out = \rtrim($out, "\n"); + } + + return $out . "\n" . $parentIndent . ']'; + } + // $node is ObjectNode at this point (all other types handled above) + $len = \count($node->properties); + $hasComments = \count($node->danglingComments) > 0; + if ($len === 0 && !$hasComments) { + return '{}'; + } + + $childIndent = self::getIndent($level + 1); + $parentIndent = self::getIndent($level); + $out = "{\n"; + + for ($i = 0; $i < $len; $i++) { + $prop = $node->properties[$i]; + + if ($i > 0) { + $out .= "\n"; + if ($prop->emptyLineBefore) { + $out .= "\n"; + } + } + $out .= self::printComments($prop->leadingComments, $childIndent); + + $keyStr = $prop->key->type === 'Identifier' + ? $prop->key->value + : self::quoteString($prop->key->value); + $out .= $childIndent . $keyStr . ': ' . self::doPrint($prop->value, $level + 1); + + if ($prop->trailingComment !== null) { + $out .= ' #' . $prop->trailingComment->value; + } + } + + if (\count($node->danglingComments) > 0) { + if ($len > 0) { + $out .= "\n"; + } + $out .= self::printComments($node->danglingComments, $childIndent); + $out = \rtrim($out, "\n"); + } + + return $out . "\n" . $parentIndent . '}'; + } + + private static function quoteString(string $s): string + { + $out = '"'; + $len = \mb_strlen($s, 'UTF-8'); + for ($i = 0; $i < $len; $i++) { + $c = \mb_substr($s, $i, 1, 'UTF-8'); + $code = \mb_ord($c, 'UTF-8'); + if ($c === '"') { + $out .= '\\"'; + } elseif ($c === '\\') { + $out .= '\\\\'; + } elseif ($c === "\n") { + $out .= '\\n'; + } elseif ($c === "\r") { + $out .= '\\r'; + } elseif ($c === "\t") { + $out .= '\\t'; + } elseif ($code < 0x20 || $code === 0x7F) { + $out .= '\\u{' . \strtoupper(\dechex($code)) . '}'; + } else { + $out .= $c; + } + } + return $out . '"'; + } + + private static function getIndent(int $level): string + { + return \str_repeat(' ', $level); + } +} diff --git a/vendor/maml/maml/src/Maml.php b/vendor/maml/maml/src/Maml.php new file mode 100644 index 0000000..ac10d34 --- /dev/null +++ b/vendor/maml/maml/src/Maml.php @@ -0,0 +1,189 @@ + + */ + public static function jsonSchema(SchemaType $schema): array + { + return JsonSchemaGenerator::generate($schema); + } + + public static function printAst( + Document|StringNode|RawStringNode|IntegerNode|FloatNode|BooleanNode|NullNode|ObjectNode|ArrayNode $node, + ): string { + return AstPrinter::print($node); + } + + public static function toValue( + Document|StringNode|RawStringNode|IntegerNode|FloatNode|BooleanNode|NullNode|ObjectNode|ArrayNode $node, + ): mixed { + if ($node instanceof Document) { + return self::toValue($node->value); + } + if ($node instanceof StringNode || $node instanceof RawStringNode) { + return $node->value; + } + if ($node instanceof IntegerNode || $node instanceof FloatNode) { + return $node->value; + } + if ($node instanceof BooleanNode) { + return $node->value; + } + if ($node instanceof NullNode) { + return null; + } + if ($node instanceof ArrayNode) { + return \array_map( + fn(Element $el) => self::toValue($el->value), + $node->elements, + ); + } + // $node is ObjectNode at this point (all other types handled above) + $result = []; + foreach ($node->properties as $prop) { + $result[$prop->key->value] = self::toValue($prop->value); + } + return $result; + } + + public static function errorSnippet( + string $source, + Position|Span $location, + string $message, + int $context = 0, + string $indent = ' ', + bool $gutter = false, + ): string { + $start = $location instanceof Span ? $location->start : $location; + $lineNum = $start->line; + + $sourceLines = \explode("\n", $source); + $lineIndex = $lineNum - 1; + $errorLine = $sourceLines[$lineIndex] ?? ''; + + // Convert byte column to character column (0-based) + $byteCol = $start->column - 1; + $charCol = \mb_strlen(\substr($errorLine, 0, $byteCol), 'UTF-8'); + + // Calculate underline width in characters + if ($location instanceof Span) { + if ($location->end->line === $lineNum) { + $endByteCol = $location->end->column - 1; + $width = \mb_strlen(\substr($errorLine, $byteCol, $endByteCol - $byteCol), 'UTF-8'); + } else { + $width = \mb_strlen($errorLine, 'UTF-8') - $charCol; + } + } else { + $width = 1; + } + $width = \max(1, $width); + + // Collect lines to display (context + error line) + $firstIndex = \max(0, $lineIndex - $context); + $lines = []; + $lineNums = []; + for ($i = $firstIndex; $i <= $lineIndex; $i++) { + $lines[] = $sourceLines[$i] ?? ''; + $lineNums[] = $i + 1; + } + + // Truncation (work in character space) + $maxWidth = 70; + $lineCharLen = \mb_strlen($errorLine, 'UTF-8'); + $adjustedCol = $charCol; + + if ($lineCharLen > $maxWidth) { + // Calculate window centered on error + $windowStart = $charCol - \intdiv($maxWidth, 3); + $windowStart = \max(0, $windowStart); + if ($windowStart + $maxWidth < $charCol + $width) { + $windowStart = \max(0, $charCol + $width - $maxWidth); + } + if ($windowStart + $maxWidth > $lineCharLen) { + $windowStart = \max(0, $lineCharLen - $maxWidth); + } + + $leftTrim = $windowStart > 0; + $rightTrim = ($windowStart + $maxWidth) < $lineCharLen; + + for ($i = 0; $i < \count($lines); $i++) { + $visible = \mb_substr($lines[$i], $windowStart, $maxWidth, 'UTF-8'); + if ($leftTrim && \mb_strlen($visible, 'UTF-8') > 0) { + $visible = '…' . \mb_substr($visible, 1, null, 'UTF-8'); + } + if ($rightTrim && \mb_strlen($visible, 'UTF-8') > 0) { + $visible = \mb_substr($visible, 0, -1, 'UTF-8') . '…'; + } + $lines[$i] = $visible; + } + + $adjustedCol = $charCol - $windowStart; + } + + // Build output + $out = "{$message} on line {$lineNum}.\n\n"; + + if ($gutter) { + $gutterWidth = \strlen((string) $lineNum); + $pad = \str_repeat(' ', $gutterWidth); + foreach ($lines as $i => $line) { + $num = \str_pad((string) $lineNums[$i], $gutterWidth, ' ', \STR_PAD_LEFT); + $out .= $indent . $num . ' | ' . $line . "\n"; + } + $out .= $indent . $pad . ' | ' . \str_repeat(' ', $adjustedCol) . \str_repeat('^', $width) . "\n"; + } else { + foreach ($lines as $line) { + $out .= $indent . $line . "\n"; + } + $out .= $indent . \str_repeat(' ', $adjustedCol) . \str_repeat('^', $width) . "\n"; + } + + return $out; + } +} diff --git a/vendor/maml/maml/src/ParseException.php b/vendor/maml/maml/src/ParseException.php new file mode 100644 index 0000000..8d8660a --- /dev/null +++ b/vendor/maml/maml/src/ParseException.php @@ -0,0 +1,7 @@ + '"', + '\\' => '\\', + 'n' => "\n", + 'r' => "\r", + 't' => "\t", + ]; + + private string $source; + private int $pos = 0; + private int $lineNumber = 1; + private string $ch = ''; + private bool $done = false; + + private function __construct(string $source) + { + $this->source = $source; + $this->next(); + } + + public static function parse(string $source): mixed + { + $parser = new self($source); + $value = $parser->parseValue(); + $parser->skipWhitespace(); + + if (!$parser->done) { + throw new ParseException($parser->errorSnippet()); + } + + $parser->expectValue($value); + return $value; + } + + private function next(): void + { + if ($this->pos < \strlen($this->source)) { + $this->ch = $this->source[$this->pos]; + $this->pos++; + } else { + $this->ch = ''; + $this->done = true; + } + if ($this->ch === "\n") { + $this->lineNumber++; + } + } + + private function lookahead(int $n): string + { + return \substr($this->source, $this->pos, $n); + } + + private function parseValue(): mixed + { + $this->skipWhitespace(); + + $v = $this->parseRawString(); + if ($v !== self::NOT_MATCHED) { + return $v; + } + + $v = $this->parseString(); + if ($v !== self::NOT_MATCHED) { + return $v; + } + + $v = $this->parseNumber(); + if ($v !== self::NOT_MATCHED) { + return $v; + } + + $v = $this->parseObject(); + if ($v !== self::NOT_MATCHED) { + return $v; + } + + $v = $this->parseArray(); + if ($v !== self::NOT_MATCHED) { + return $v; + } + + $v = $this->parseKeyword('true', true); + if ($v !== self::NOT_MATCHED) { + return $v; + } + + $v = $this->parseKeyword('false', false); + if ($v !== self::NOT_MATCHED) { + return $v; + } + + $v = $this->parseKeyword('null', null); + if ($v !== self::NOT_MATCHED) { + return $v; + } + + return self::NOT_MATCHED; + } + + private function parseString(): mixed + { + if ($this->ch !== '"') { + return self::NOT_MATCHED; + } + $str = ''; + $escaped = false; + while (true) { + $this->next(); + if ($this->done) { + throw new ParseException($this->errorSnippet()); + } + if ($escaped) { + if ($this->ch === 'u') { + $this->next(); + if ($this->ch !== '{') { + throw new ParseException( + $this->errorSnippet( + 'Invalid escape sequence ' . $this->formatChar($this->ch) . ' (expected "{")', + ), + ); + } + $hex = ''; + while (true) { + $this->next(); + if ($this->ch === '}') { + break; + } + if (!$this->isHexDigit($this->ch)) { + throw new ParseException( + $this->errorSnippet( + 'Invalid escape sequence ' . $this->formatChar($this->ch), + ), + ); + } + $hex .= $this->ch; + if (\strlen($hex) > 6) { + throw new ParseException( + $this->errorSnippet('Invalid escape sequence (too many hex digits)'), + ); + } + } + if ($hex === '') { + throw new ParseException( + $this->errorSnippet('Invalid escape sequence'), + ); + } + $codePoint = (int) \hexdec($hex); + if ($codePoint > 0x10FFFF || ($codePoint >= 0xD800 && $codePoint <= 0xDFFF)) { + throw new ParseException( + $this->errorSnippet('Invalid escape sequence (out of range)'), + ); + } + $str .= self::codePointToUtf8($codePoint); + } else { + $escapedChar = self::ESCAPE_MAP[$this->ch] ?? null; + if ($escapedChar === null) { + throw new ParseException( + $this->errorSnippet( + 'Invalid escape sequence ' . $this->formatChar($this->ch), + ), + ); + } + $str .= $escapedChar; + } + $escaped = false; + } elseif ($this->ch === '\\') { + $escaped = true; + } elseif ($this->ch === '"') { + break; + } elseif ($this->ch === "\n") { + throw new ParseException($this->errorSnippet()); + } elseif (($this->ch < "\x20" && $this->ch !== "\t") || $this->ch === "\x7F") { + throw new ParseException($this->errorSnippet()); + } else { + $str .= $this->ch; + } + } + $this->next(); + return $str; + } + + private function parseRawString(): mixed + { + if ($this->ch !== '"' || $this->lookahead(2) !== '""') { + return self::NOT_MATCHED; + } + $this->next(); + $this->next(); + $this->next(); + $hasLeadingNewline = false; + if ($this->ch === "\r" && $this->lookahead(1) === "\n") { + $this->next(); + } + if ($this->ch === "\n") { + $hasLeadingNewline = true; + $this->next(); + } + + $str = ''; + while (!$this->done) { + if ($this->ch === '"' && $this->lookahead(2) === '""') { + $this->next(); + $this->next(); + $this->next(); + if ($str === '' && !$hasLeadingNewline) { + throw new ParseException($this->errorSnippet('Raw strings cannot be empty')); + } + return $str; + } + $str .= $this->ch; + $this->next(); + } + throw new ParseException($this->errorSnippet()); + } + + private function parseNumber(): mixed + { + if (!$this->isDigit($this->ch) && $this->ch !== '-') { + return self::NOT_MATCHED; + } + $numStr = ''; + $float = false; + if ($this->ch === '-') { + $numStr .= $this->ch; + $this->next(); + if (!$this->isDigit($this->ch)) { + throw new ParseException($this->errorSnippet()); + } + } + if ($this->ch === '0') { + $numStr .= $this->ch; + $this->next(); + } else { + while ($this->isDigit($this->ch)) { + $numStr .= $this->ch; + $this->next(); + } + } + if ($this->ch === '.') { + $float = true; + $numStr .= $this->ch; + $this->next(); + if (!$this->isDigit($this->ch)) { + throw new ParseException($this->errorSnippet()); + } + while ($this->isDigit($this->ch)) { + $numStr .= $this->ch; + $this->next(); + } + } + if ($this->ch === 'e' || $this->ch === 'E') { + $float = true; + $numStr .= $this->ch; + $this->next(); + if ($this->ch === '+' || $this->ch === '-') { + $numStr .= $this->ch; + $this->next(); + } + if (!$this->isDigit($this->ch)) { + throw new ParseException($this->errorSnippet()); + } + while ($this->isDigit($this->ch)) { + $numStr .= $this->ch; + $this->next(); + } + } + return $float ? (float) $numStr : $this->toSafeNumber($numStr); + } + + private function parseObject(): mixed + { + if ($this->ch !== '{') { + return self::NOT_MATCHED; + } + $this->next(); + $this->skipWhitespace(); + $obj = []; + if ($this->ch === '}') { + $this->next(); + return $obj; + } + while (true) { + $keyPos = $this->pos; + if ($this->ch === '"') { + /** @var string $key */ + $key = $this->parseString(); + } else { + $key = $this->parseKey(); + } + if (\array_key_exists($key, $obj)) { + $this->pos = $keyPos; + throw new ParseException( + $this->errorSnippet('Duplicate key ' . \json_encode($key)), + ); + } + $this->skipWhitespace(); + if ($this->ch !== ':') { + throw new ParseException($this->errorSnippet()); + } + $this->next(); + $value = $this->parseValue(); + $this->expectValue($value); + $obj[$key] = $value; + $newlineAfterValue = $this->skipWhitespace(); + if ($this->ch === '}') { + $this->next(); + return $obj; + } elseif ($this->ch === ',') { + $this->next(); + $this->skipWhitespace(); + if ($this->ch === '}') { + $this->next(); + return $obj; + } + } elseif ($newlineAfterValue) { + continue; + } else { + throw new ParseException( + $this->errorSnippet('Expected comma or newline between key-value pairs'), + ); + } + } + } + + private function parseKey(): string + { + $identifier = ''; + while ($this->isKeyChar($this->ch)) { + $identifier .= $this->ch; + $this->next(); + } + if ($identifier === '') { + throw new ParseException($this->errorSnippet()); + } + return $identifier; + } + + private function parseArray(): mixed + { + if ($this->ch !== '[') { + return self::NOT_MATCHED; + } + $this->next(); + $this->skipWhitespace(); + $array = []; + if ($this->ch === ']') { + $this->next(); + return $array; + } + while (true) { + $value = $this->parseValue(); + $this->expectValue($value); + $array[] = $value; + $newLineAfterValue = $this->skipWhitespace(); + if ($this->ch === ']') { + $this->next(); + return $array; + } elseif ($this->ch === ',') { + $this->next(); + $this->skipWhitespace(); + if ($this->ch === ']') { + $this->next(); + return $array; + } + } elseif ($newLineAfterValue) { + continue; + } else { + throw new ParseException( + $this->errorSnippet('Expected comma or newline between values'), + ); + } + } + } + + private function parseKeyword(string $name, mixed $value): mixed + { + if ($this->ch !== $name[0]) { + return self::NOT_MATCHED; + } + for ($i = 1; $i < \strlen($name); $i++) { + $this->next(); + if ($this->ch !== $name[$i]) { + throw new ParseException($this->errorSnippet()); + } + } + $this->next(); + if ( + $this->isWhitespace($this->ch) + || $this->ch === ',' + || $this->ch === '}' + || $this->ch === ']' + || $this->done + ) { + return $value; + } + throw new ParseException($this->errorSnippet()); + } + + private function skipWhitespace(): bool + { + $hasNewline = false; + while ($this->isWhitespace($this->ch)) { + $hasNewline = $hasNewline || $this->ch === "\n"; + $this->next(); + } + $hasNewlineAfterComment = $this->skipComment(); + return $hasNewline || $hasNewlineAfterComment; + } + + private function skipComment(): bool + { + if ($this->ch === '#') { + while (!$this->done && $this->ch !== "\n") { + $this->next(); + } + return $this->skipWhitespace(); + } + return false; + } + + private function isWhitespace(string $ch): bool + { + return $ch === ' ' || $ch === "\n" || $ch === "\t" || $ch === "\r"; + } + + private function isHexDigit(string $ch): bool + { + return ($ch >= '0' && $ch <= '9') || ($ch >= 'A' && $ch <= 'F'); + } + + private function isDigit(string $ch): bool + { + return $ch >= '0' && $ch <= '9'; + } + + private function isKeyChar(string $ch): bool + { + return ($ch >= 'A' && $ch <= 'Z') + || ($ch >= 'a' && $ch <= 'z') + || ($ch >= '0' && $ch <= '9') + || $ch === '_' + || $ch === '-'; + } + + private function toSafeNumber(string $str): int|float + { + if ($str === '-0') { + return -0.0; + } + $int = (int) $str; + if ((string) $int === $str) { + return $int; + } + throw new ParseException($this->errorSnippet('Integer overflow')); + } + + private function expectValue(mixed $value): void + { + if ($value === self::NOT_MATCHED) { + throw new ParseException($this->errorSnippet()); + } + } + + private static function codePointToUtf8(int $cp): string + { + // Code point is guaranteed to be a valid Unicode scalar value + // (validated before this method is called). + return \mb_chr($cp, 'UTF-8'); + } + + private function formatChar(string $ch): string + { + $ord = \ord($ch); + if ($ch === '' || $ord < 0x80) { + return (string) \json_encode($ch, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE); + } + // Multi-byte UTF-8: extract full character from source at current position + $bytePos = $this->pos - 1; + $byte = \ord($this->source[$bytePos]); + $len = match (true) { + $byte < 0xC0 => 1, + $byte < 0xE0 => 2, + $byte < 0xF0 => 3, + default => 4, + }; + $fullChar = \substr($this->source, $bytePos, $len); + return (string) \json_encode($fullChar, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE); + } + + private function errorSnippet(string $message = ''): string + { + $customMessage = $message !== ''; + if (!$customMessage) { + if ($this->ch === '' || $this->done) { + $message = 'Unexpected end of input'; + } else { + $message = 'Unexpected character ' . $this->formatChar($this->ch); + } + } + if (!$customMessage && $this->ch === '' && $this->done) { + $message = 'Unexpected end of input'; + } + + $start = \max(0, $this->pos - 40); + $pre = \substr($this->source, $start, $this->pos - $start); + $lines = \explode("\n", $pre); + $lastLine = \end($lines) ?: ''; + $postParts = \explode("\n", \substr($this->source, $this->pos, 40), 2); + $postfix = $postParts[0]; + + if ($lastLine === '') { + // error at "\n" + $count = \count($lines); + $lastLine = ($count >= 2) ? $lines[$count - 2] : ''; + $lastLine .= ' '; + $this->lineNumber--; + $postfix = ''; + } + + $snippet = " {$lastLine}{$postfix}\n"; + $pointer = ' ' . \str_repeat('.', \max(0, \strlen($lastLine) - 1)) . "^\n"; + return "{$message} on line {$this->lineNumber}.\n\n{$snippet}{$pointer}"; + } +} diff --git a/vendor/maml/maml/src/Schema/JsonSchemaGenerator.php b/vendor/maml/maml/src/Schema/JsonSchemaGenerator.php new file mode 100644 index 0000000..4cc3809 --- /dev/null +++ b/vendor/maml/maml/src/Schema/JsonSchemaGenerator.php @@ -0,0 +1,207 @@ + + */ + public static function generate(SchemaType $schema): array + { + return [ + '$schema' => 'http://json-schema.org/draft-07/schema#', + ...self::convert($schema), + ]; + } + + /** + * @return array + */ + private static function convert(SchemaType $schema): array + { + if ($schema instanceof OptionalType) { + return self::convert($schema->inner); + } + + if ($schema instanceof AnyType) { + return []; + } + + if ($schema instanceof StringType) { + $result = ['type' => 'string']; + if ($schema->pattern !== null) { + // Strip PHP delimiters (e.g. "/^foo$/" → "^foo$") + $result['pattern'] = self::stripDelimiters($schema->pattern); + } + return $result; + } + + if ($schema instanceof IntegerType) { + $result = ['type' => 'integer']; + if ($schema->min !== null) { + $result['minimum'] = $schema->min; + } + if ($schema->max !== null) { + $result['maximum'] = $schema->max; + } + return $result; + } + + if ($schema instanceof FloatType) { + $result = ['type' => 'number']; + if ($schema->min !== null) { + $result['minimum'] = $schema->min; + } + if ($schema->max !== null) { + $result['maximum'] = $schema->max; + } + return $result; + } + + if ($schema instanceof NumberType) { + $result = ['type' => 'number']; + if ($schema->min !== null) { + $result['minimum'] = $schema->min; + } + if ($schema->max !== null) { + $result['maximum'] = $schema->max; + } + return $result; + } + + if ($schema instanceof BooleanType) { + return ['type' => 'boolean']; + } + + if ($schema instanceof NullType) { + return ['type' => 'null']; + } + + if ($schema instanceof LiteralType) { + return ['const' => $schema->value]; + } + + if ($schema instanceof ObjectType || $schema instanceof OrderedObjectType) { + return self::convertObject($schema->properties, $schema->additionalProperties); + } + + if ($schema instanceof MapType) { + $result = ['type' => 'object']; + if ($schema->values instanceof AnyType) { + $result['additionalProperties'] = true; + } else { + $result['additionalProperties'] = self::convert($schema->values); + } + return $result; + } + + if ($schema instanceof ArrayOfType) { + $result = [ + 'type' => 'array', + 'items' => self::convert($schema->items), + ]; + if ($schema->minItems !== null) { + $result['minItems'] = $schema->minItems; + } + if ($schema->maxItems !== null) { + $result['maxItems'] = $schema->maxItems; + } + return $result; + } + + if ($schema instanceof TupleType) { + return [ + 'type' => 'array', + 'items' => \array_map(fn(SchemaType $s) => self::convert($s), $schema->elements), + 'additionalItems' => false, + 'minItems' => \count($schema->elements), + 'maxItems' => \count($schema->elements), + ]; + } + + // $schema is UnionType at this point + /** @var UnionType $schema */ + // Detect all-literal unions and emit compact "enum" keyword + $allLiterals = true; + foreach ($schema->branches as $branch) { + if (!($branch instanceof LiteralType)) { + $allLiterals = false; + break; + } + } + if ($allLiterals) { + return [ + 'enum' => \array_map(fn(SchemaType $s) => ($s instanceof LiteralType) ? $s->value : null, $schema->branches), + ]; + } + return [ + 'oneOf' => \array_map(fn(SchemaType $s) => self::convert($s), $schema->branches), + ]; + } + + /** + * @param array $properties + * @return array + */ + private static function convertObject(array $properties, ?SchemaType $additionalProperties): array + { + $result = ['type' => 'object']; + + $props = []; + $required = []; + foreach ($properties as $key => $propSchema) { + if ($propSchema instanceof OptionalType) { + $props[$key] = self::convert($propSchema->inner); + } else { + $props[$key] = self::convert($propSchema); + $required[] = $key; + } + } + + if ($props !== []) { + $result['properties'] = $props; + } + if ($required !== []) { + $result['required'] = $required; + } + + if ($additionalProperties === null) { + $result['additionalProperties'] = false; + } elseif (!($additionalProperties instanceof AnyType)) { + $result['additionalProperties'] = self::convert($additionalProperties); + } + + return $result; + } + + private static function stripDelimiters(string $pattern): string + { + if (\strlen($pattern) >= 2) { + $delimiter = $pattern[0]; + $lastPos = \strrpos($pattern, $delimiter, 1); + if ($lastPos !== false) { + return \substr($pattern, 1, $lastPos - 1); + } + } + return $pattern; + } +} diff --git a/vendor/maml/maml/src/Schema/S.php b/vendor/maml/maml/src/Schema/S.php new file mode 100644 index 0000000..851f612 --- /dev/null +++ b/vendor/maml/maml/src/Schema/S.php @@ -0,0 +1,109 @@ + $properties */ + public static function object(array $properties, ?SchemaType $additionalProperties = null): ObjectType + { + return new ObjectType($properties, $additionalProperties); + } + + /** @param array $properties */ + public static function orderedObject(array $properties, ?SchemaType $additionalProperties = null): OrderedObjectType + { + return new OrderedObjectType($properties, $additionalProperties); + } + + public static function optional(SchemaType $schema): OptionalType + { + return new OptionalType($schema); + } + + public static function arrayOf(SchemaType $items, ?int $minItems = null, ?int $maxItems = null): ArrayOfType + { + return new ArrayOfType($items, $minItems, $maxItems); + } + + /** @param SchemaType[] $elements */ + public static function tuple(array $elements): TupleType + { + return new TupleType($elements); + } + + public static function map(SchemaType $values): MapType + { + return new MapType($values); + } + + public static function union(SchemaType ...$branches): UnionType + { + return new UnionType($branches); + } + + public static function any(): AnyType + { + return new AnyType(); + } + + public static function enum(string|int|float|bool|null ...$values): UnionType + { + return new UnionType( + \array_map(fn($v) => new LiteralType($v), $values), + ); + } +} diff --git a/vendor/maml/maml/src/Schema/SchemaType.php b/vendor/maml/maml/src/Schema/SchemaType.php new file mode 100644 index 0000000..235e66b --- /dev/null +++ b/vendor/maml/maml/src/Schema/SchemaType.php @@ -0,0 +1,10 @@ +items->describe() . '[]'; + $constraints = []; + if ($this->minItems !== null) { + $constraints[] = 'minItems: ' . $this->minItems; + } + if ($this->maxItems !== null) { + $constraints[] = 'maxItems: ' . $this->maxItems; + } + if ($constraints === []) { + return $base; + } + return $base . '(' . \implode(', ', $constraints) . ')'; + } +} diff --git a/vendor/maml/maml/src/Schema/Type/BooleanType.php b/vendor/maml/maml/src/Schema/Type/BooleanType.php new file mode 100644 index 0000000..bb4ab11 --- /dev/null +++ b/vendor/maml/maml/src/Schema/Type/BooleanType.php @@ -0,0 +1,15 @@ +min !== null) { + $constraints[] = 'min: ' . $this->min; + } + if ($this->max !== null) { + $constraints[] = 'max: ' . $this->max; + } + if ($constraints === []) { + return 'float'; + } + return 'float(' . \implode(', ', $constraints) . ')'; + } +} diff --git a/vendor/maml/maml/src/Schema/Type/IntegerType.php b/vendor/maml/maml/src/Schema/Type/IntegerType.php new file mode 100644 index 0000000..bdb0a3d --- /dev/null +++ b/vendor/maml/maml/src/Schema/Type/IntegerType.php @@ -0,0 +1,30 @@ +min !== null) { + $constraints[] = 'min: ' . $this->min; + } + if ($this->max !== null) { + $constraints[] = 'max: ' . $this->max; + } + if ($constraints === []) { + return 'integer'; + } + return 'integer(' . \implode(', ', $constraints) . ')'; + } +} diff --git a/vendor/maml/maml/src/Schema/Type/LiteralType.php b/vendor/maml/maml/src/Schema/Type/LiteralType.php new file mode 100644 index 0000000..542a2fd --- /dev/null +++ b/vendor/maml/maml/src/Schema/Type/LiteralType.php @@ -0,0 +1,28 @@ +value === null) { + return 'null'; + } + if (\is_bool($this->value)) { + return $this->value ? 'true' : 'false'; + } + if (\is_string($this->value)) { + return '"' . $this->value . '"'; + } + return (string) $this->value; + } +} diff --git a/vendor/maml/maml/src/Schema/Type/MapType.php b/vendor/maml/maml/src/Schema/Type/MapType.php new file mode 100644 index 0000000..fc9b313 --- /dev/null +++ b/vendor/maml/maml/src/Schema/Type/MapType.php @@ -0,0 +1,19 @@ +values->describe() . '>'; + } +} diff --git a/vendor/maml/maml/src/Schema/Type/NullType.php b/vendor/maml/maml/src/Schema/Type/NullType.php new file mode 100644 index 0000000..8c099e6 --- /dev/null +++ b/vendor/maml/maml/src/Schema/Type/NullType.php @@ -0,0 +1,15 @@ +min !== null) { + $constraints[] = 'min: ' . $this->min; + } + if ($this->max !== null) { + $constraints[] = 'max: ' . $this->max; + } + if ($constraints === []) { + return 'number'; + } + return 'number(' . \implode(', ', $constraints) . ')'; + } +} diff --git a/vendor/maml/maml/src/Schema/Type/ObjectType.php b/vendor/maml/maml/src/Schema/Type/ObjectType.php new file mode 100644 index 0000000..e9a9fe2 --- /dev/null +++ b/vendor/maml/maml/src/Schema/Type/ObjectType.php @@ -0,0 +1,23 @@ + $properties + */ + public function __construct( + public array $properties, + public ?SchemaType $additionalProperties = null, + ) {} + + public function describe(): string + { + return 'object{' . \implode(', ', \array_keys($this->properties)) . '}'; + } +} diff --git a/vendor/maml/maml/src/Schema/Type/OptionalType.php b/vendor/maml/maml/src/Schema/Type/OptionalType.php new file mode 100644 index 0000000..8e0b9b4 --- /dev/null +++ b/vendor/maml/maml/src/Schema/Type/OptionalType.php @@ -0,0 +1,19 @@ +inner->describe() . '?'; + } +} diff --git a/vendor/maml/maml/src/Schema/Type/OrderedObjectType.php b/vendor/maml/maml/src/Schema/Type/OrderedObjectType.php new file mode 100644 index 0000000..ca8cfa8 --- /dev/null +++ b/vendor/maml/maml/src/Schema/Type/OrderedObjectType.php @@ -0,0 +1,23 @@ + $properties + */ + public function __construct( + public array $properties, + public ?SchemaType $additionalProperties = null, + ) {} + + public function describe(): string + { + return 'ordered object{' . \implode(', ', \array_keys($this->properties)) . '}'; + } +} diff --git a/vendor/maml/maml/src/Schema/Type/StringType.php b/vendor/maml/maml/src/Schema/Type/StringType.php new file mode 100644 index 0000000..3d3aea0 --- /dev/null +++ b/vendor/maml/maml/src/Schema/Type/StringType.php @@ -0,0 +1,22 @@ +pattern !== null) { + return 'string(pattern: ' . $this->pattern . ')'; + } + return 'string'; + } +} diff --git a/vendor/maml/maml/src/Schema/Type/TupleType.php b/vendor/maml/maml/src/Schema/Type/TupleType.php new file mode 100644 index 0000000..65edf35 --- /dev/null +++ b/vendor/maml/maml/src/Schema/Type/TupleType.php @@ -0,0 +1,21 @@ + $s->describe(), $this->elements); + return '[' . \implode(', ', $parts) . ']'; + } +} diff --git a/vendor/maml/maml/src/Schema/Type/UnionType.php b/vendor/maml/maml/src/Schema/Type/UnionType.php new file mode 100644 index 0000000..e202bde --- /dev/null +++ b/vendor/maml/maml/src/Schema/Type/UnionType.php @@ -0,0 +1,21 @@ + $s->describe(), $this->branches); + return \implode(' | ', $parts); + } +} diff --git a/vendor/maml/maml/src/Schema/ValidationError.php b/vendor/maml/maml/src/Schema/ValidationError.php new file mode 100644 index 0000000..cb070fb --- /dev/null +++ b/vendor/maml/maml/src/Schema/ValidationError.php @@ -0,0 +1,16 @@ +validateNode($doc->value, $schema, '$'); + return $validator->errors; + } + + private function validateNode( + StringNode|RawStringNode|IntegerNode|FloatNode|BooleanNode|NullNode|ObjectNode|ArrayNode $node, + SchemaType $schema, + string $path, + ): void { + // Unwrap OptionalType outside of object context + if ($schema instanceof OptionalType) { + $this->validateNode($node, $schema->inner, $path); + return; + } + + if ($schema instanceof AnyType) { + return; + } + + if ($schema instanceof StringType) { + if (!($node instanceof StringNode) && !($node instanceof RawStringNode)) { + $this->addError('Expected string, got ' . self::describeNode($node), $path, $node->span); + return; + } + if ($schema->pattern !== null && !\preg_match($schema->pattern, $node->value)) { + $this->addError( + 'String does not match pattern ' . $schema->pattern, + $path, + $node->span, + ); + } + return; + } + + if ($schema instanceof IntegerType) { + if (!($node instanceof IntegerNode)) { + $this->addError('Expected integer, got ' . self::describeNode($node), $path, $node->span); + return; + } + $this->checkRange($node->value, $schema->min, $schema->max, $path, $node->span); + return; + } + + if ($schema instanceof FloatType) { + if (!($node instanceof FloatNode)) { + $this->addError('Expected float, got ' . self::describeNode($node), $path, $node->span); + return; + } + $this->checkRange($node->value, $schema->min, $schema->max, $path, $node->span); + return; + } + + if ($schema instanceof NumberType) { + if (!($node instanceof IntegerNode) && !($node instanceof FloatNode)) { + $this->addError('Expected number, got ' . self::describeNode($node), $path, $node->span); + return; + } + $this->checkRange($node->value, $schema->min, $schema->max, $path, $node->span); + return; + } + + if ($schema instanceof BooleanType) { + if (!($node instanceof BooleanNode)) { + $this->addError('Expected boolean, got ' . self::describeNode($node), $path, $node->span); + } + return; + } + + if ($schema instanceof NullType) { + if (!($node instanceof NullNode)) { + $this->addError('Expected null, got ' . self::describeNode($node), $path, $node->span); + } + return; + } + + if ($schema instanceof LiteralType) { + $this->validateLiteral($node, $schema, $path); + return; + } + + if ($schema instanceof ObjectType) { + $this->validateObject($node, $schema->properties, $schema->additionalProperties, $path); + return; + } + + if ($schema instanceof OrderedObjectType) { + $this->validateObject($node, $schema->properties, $schema->additionalProperties, $path); + if ($node instanceof ObjectNode) { + $this->validateOrder($node, $schema->properties, $path); + } + return; + } + + if ($schema instanceof MapType) { + if (!($node instanceof ObjectNode)) { + $this->addError('Expected ' . $schema->describe() . ', got ' . self::describeNode($node), $path, $node->span); + return; + } + foreach ($node->properties as $prop) { + $this->validateNode($prop->value, $schema->values, $path . '.' . $prop->key->value); + } + return; + } + + if ($schema instanceof ArrayOfType) { + if (!($node instanceof ArrayNode)) { + $this->addError('Expected ' . $schema->describe() . ', got ' . self::describeNode($node), $path, $node->span); + return; + } + $count = \count($node->elements); + if ($schema->minItems !== null && $count < $schema->minItems) { + $this->addError( + 'Expected at least ' . $schema->minItems . ' items, got ' . $count, + $path, + $node->span, + ); + } + if ($schema->maxItems !== null && $count > $schema->maxItems) { + $this->addError( + 'Expected at most ' . $schema->maxItems . ' items, got ' . $count, + $path, + $node->span, + ); + } + foreach ($node->elements as $i => $element) { + $this->validateNode($element->value, $schema->items, $path . '[' . $i . ']'); + } + return; + } + + if ($schema instanceof TupleType) { + if (!($node instanceof ArrayNode)) { + $this->addError('Expected ' . $schema->describe() . ', got ' . self::describeNode($node), $path, $node->span); + return; + } + $expected = \count($schema->elements); + $actual = \count($node->elements); + if ($actual !== $expected) { + $this->addError( + 'Expected array of length ' . $expected . ', got ' . $actual, + $path, + $node->span, + ); + } + $len = \min($expected, $actual); + for ($i = 0; $i < $len; $i++) { + $this->validateNode($node->elements[$i]->value, $schema->elements[$i], $path . '[' . $i . ']'); + } + return; + } + + if ($schema instanceof UnionType) { + $this->validateUnion($node, $schema, $path); + return; + } + } + + private function validateLiteral( + StringNode|RawStringNode|IntegerNode|FloatNode|BooleanNode|NullNode|ObjectNode|ArrayNode $node, + LiteralType $schema, + string $path, + ): void { + $expected = $schema->value; + + if ($expected === null) { + if (!($node instanceof NullNode)) { + $this->addError('Expected null, got ' . self::describeNode($node), $path, $node->span); + } + return; + } + + if (\is_bool($expected)) { + if (!($node instanceof BooleanNode) || $node->value !== $expected) { + $this->addError('Expected ' . $schema->describe() . ', got ' . self::describeNodeValue($node), $path, $node->span); + } + return; + } + + if (\is_string($expected)) { + if (($node instanceof StringNode || $node instanceof RawStringNode) && $node->value === $expected) { + return; + } + $this->addError('Expected ' . $schema->describe() . ', got ' . self::describeNodeValue($node), $path, $node->span); + return; + } + + if (\is_int($expected)) { + if ($node instanceof IntegerNode && $node->value === $expected) { + return; + } + $this->addError('Expected ' . $schema->describe() . ', got ' . self::describeNodeValue($node), $path, $node->span); + return; + } + + // float + if ($node instanceof FloatNode && $node->value === $expected) { + return; + } + $this->addError('Expected ' . $schema->describe() . ', got ' . self::describeNodeValue($node), $path, $node->span); + } + + /** + * @param array $properties + */ + private function validateObject( + StringNode|RawStringNode|IntegerNode|FloatNode|BooleanNode|NullNode|ObjectNode|ArrayNode $node, + array $properties, + ?SchemaType $additionalProperties, + string $path, + ): void { + if (!($node instanceof ObjectNode)) { + $desc = 'object{' . \implode(', ', \array_keys($properties)) . '}'; + $this->addError('Expected ' . $desc . ', got ' . self::describeNode($node), $path, $node->span); + return; + } + + // Build map of actual keys + $actual = []; + foreach ($node->properties as $prop) { + $actual[$prop->key->value] = $prop; + } + + // Unknown keys: reject or validate against additionalProperties schema + foreach ($actual as $key => $prop) { + if (!isset($properties[$key])) { + if ($additionalProperties === null) { + $this->addError('Unknown property "' . $key . '"', $path . '.' . $key, $prop->key->span); + } else { + $this->validateNode($prop->value, $additionalProperties, $path . '.' . $key); + } + } + } + + // Missing required keys + recurse into present values + foreach ($properties as $key => $propSchema) { + $isOptional = $propSchema instanceof OptionalType; + $innerSchema = $isOptional ? $propSchema->inner : $propSchema; + + if (isset($actual[$key])) { + $this->validateNode($actual[$key]->value, $innerSchema, $path . '.' . $key); + } elseif (!$isOptional) { + $this->addError('Missing required property "' . $key . '"', $path . '.' . $key, $node->span); + } + } + } + + /** + * @param array $properties + */ + private function validateOrder(ObjectNode $node, array $properties, string $path): void + { + $schemaKeys = \array_keys($properties); + + // Collect actual keys in AST order, filtered to schema-defined keys only + $actualOrder = []; + foreach ($node->properties as $prop) { + $key = $prop->key->value; + if (isset($properties[$key])) { + $actualOrder[] = $key; + } + } + + // Expected order: schema keys filtered to only those present + $presentSet = \array_flip($actualOrder); + $expectedOrder = \array_filter($schemaKeys, fn(string $k) => isset($presentSet[$k])); + $expectedOrder = \array_values($expectedOrder); + + if ($actualOrder !== $expectedOrder) { + $this->addError( + 'Properties are not in the expected order. Expected: ' . \implode(', ', $expectedOrder), + $path, + $node->span, + ); + } + } + + private function validateUnion( + StringNode|RawStringNode|IntegerNode|FloatNode|BooleanNode|NullNode|ObjectNode|ArrayNode $node, + UnionType $schema, + string $path, + ): void { + $bestErrors = null; + $bestDepth = -1; + $bestCount = \PHP_INT_MAX; + + foreach ($schema->branches as $branch) { + $sub = new self(); + $sub->validateNode($node, $branch, $path); + if ($sub->errors === []) { + return; + } + + $maxDepth = 0; + foreach ($sub->errors as $e) { + $depth = \substr_count($e->path, '.') + \substr_count($e->path, '['); + $maxDepth = \max($maxDepth, $depth); + } + $count = \count($sub->errors); + + if ($maxDepth > $bestDepth || ($maxDepth === $bestDepth && $count < $bestCount)) { + $bestErrors = $sub->errors; + $bestDepth = $maxDepth; + $bestCount = $count; + } + } + + // If best branch matched deeper than the union's own path, report its specific errors + $unionDepth = \substr_count($path, '.') + \substr_count($path, '['); + if ($bestErrors !== null && $bestDepth > $unionDepth) { + \array_push($this->errors, ...$bestErrors); + } else { + $this->addError( + 'Expected ' . $schema->describe() . ', got ' . self::describeNode($node), + $path, + $node->span, + ); + } + } + + private function checkRange(int|float $value, int|float|null $min, int|float|null $max, string $path, Span $span): void + { + if ($min !== null && $value < $min) { + $this->addError('Value ' . $value . ' is less than minimum ' . $min, $path, $span); + } + if ($max !== null && $value > $max) { + $this->addError('Value ' . $value . ' is greater than maximum ' . $max, $path, $span); + } + } + + private function addError(string $message, string $path, ?Span $span): void + { + $this->errors[] = new ValidationError($message, $path, $span); + } + + private static function describeNode( + StringNode|RawStringNode|IntegerNode|FloatNode|BooleanNode|NullNode|ObjectNode|ArrayNode $node, + ): string { + return match (true) { + $node instanceof StringNode, $node instanceof RawStringNode => 'string', + $node instanceof IntegerNode => 'integer', + $node instanceof FloatNode => 'float', + $node instanceof BooleanNode => 'boolean', + $node instanceof NullNode => 'null', + $node instanceof ObjectNode => 'object', + $node instanceof ArrayNode => 'array', + }; + } + + private static function describeNodeValue( + StringNode|RawStringNode|IntegerNode|FloatNode|BooleanNode|NullNode|ObjectNode|ArrayNode $node, + ): string { + return match (true) { + $node instanceof StringNode, $node instanceof RawStringNode => 'string "' . $node->value . '"', + $node instanceof IntegerNode => 'integer ' . $node->value, + $node instanceof FloatNode => 'float ' . $node->value, + $node instanceof BooleanNode => $node->value ? 'true' : 'false', + $node instanceof NullNode => 'null', + $node instanceof ObjectNode => 'object', + $node instanceof ArrayNode => 'array', + }; + } +} diff --git a/vendor/maml/maml/src/Stringifier.php b/vendor/maml/maml/src/Stringifier.php new file mode 100644 index 0000000..0629457 --- /dev/null +++ b/vendor/maml/maml/src/Stringifier.php @@ -0,0 +1,212 @@ +leadingComments as $c) { + $leading .= '#' . $c . "\n"; + } + $value = $value->value; + } + if (is_array($value)) { + if (array_is_list($value)) { + return $leading . self::stringifyArray($value, 0, $annotated); + } + return $leading . self::stringifyObject($value, 0, $annotated); + } + if ($value instanceof \stdClass) { + return $leading . self::stringifyObject((array) $value, 0, $annotated); + } + if ($annotated !== null) { + foreach ($annotated->danglingComments as $c) { + $dangling .= "\n" . '#' . $c; + } + } + return $leading . self::doStringify($value, 0) . $dangling; + } + + private static function doStringify(mixed $value, int $level): string + { + $annotated = null; + if ($value instanceof Annotated) { + $annotated = $value; + $value = $value->value; + } + if ($value === null) { + return 'null'; + } + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + if (is_int($value)) { + return (string) $value; + } + if (is_float($value)) { + $str = (string) $value; + if ($str === '-0') { + return '-0'; + } + return $str; + } + if (is_string($value)) { + return self::quoteString($value); + } + if (is_array($value)) { + if (array_is_list($value)) { + return self::stringifyArray($value, $level, $annotated); + } + return self::stringifyObject($value, $level, $annotated); + } + if ($value instanceof \stdClass) { + return self::stringifyObject((array) $value, $level, $annotated); + } + throw new \InvalidArgumentException('Unsupported value type: ' . get_debug_type($value)); + } + + /** + * @param list $arr + */ + private static function stringifyArray(array $arr, int $level, ?Annotated $annotated = null): string + { + $hasDangling = $annotated !== null && count($annotated->danglingComments) > 0; + if (count($arr) === 0 && !$hasDangling) { + return '[]'; + } + $childIndent = self::getIndent($level + 1); + $parentIndent = self::getIndent($level); + $out = "[\n"; + for ($i = 0; $i < count($arr); $i++) { + $elAnnotated = null; + $innerValue = $arr[$i]; + if ($innerValue instanceof Annotated) { + $elAnnotated = $innerValue; + $innerValue = $elAnnotated->value; + } + if ($i > 0) { + $out .= "\n"; + if ($elAnnotated !== null && $elAnnotated->emptyLineBefore) { + $out .= "\n"; + } + } + if ($elAnnotated !== null) { + foreach ($elAnnotated->leadingComments as $c) { + $out .= $childIndent . '#' . $c . "\n"; + } + } + $out .= $childIndent . self::doStringify($arr[$i], $level + 1); + if ($elAnnotated !== null && $elAnnotated->trailingComment !== null && !is_array($innerValue) && !$innerValue instanceof \stdClass) { + $out .= ' #' . $elAnnotated->trailingComment; + } + } + if ($hasDangling) { + if (count($arr) > 0) { + $out .= "\n"; + } + foreach ($annotated->danglingComments as $c) { + $out .= $childIndent . '#' . $c . "\n"; + } + $out = rtrim($out, "\n"); + } + return $out . "\n" . $parentIndent . ']'; + } + + /** + * @param array $obj + */ + private static function stringifyObject(array $obj, int $level, ?Annotated $annotated = null): string + { + $keys = array_keys($obj); + $hasDangling = $annotated !== null && count($annotated->danglingComments) > 0; + if (count($keys) === 0 && !$hasDangling) { + return '{}'; + } + $childIndent = self::getIndent($level + 1); + $parentIndent = self::getIndent($level); + $out = "{\n"; + for ($i = 0; $i < count($keys); $i++) { + $key = (string) $keys[$i]; + $elAnnotated = null; + $innerValue = $obj[$key]; + if ($innerValue instanceof Annotated) { + $elAnnotated = $innerValue; + $innerValue = $elAnnotated->value; + } + if ($i > 0) { + $out .= "\n"; + if ($elAnnotated !== null && $elAnnotated->emptyLineBefore) { + $out .= "\n"; + } + } + if ($elAnnotated !== null) { + foreach ($elAnnotated->leadingComments as $c) { + $out .= $childIndent . '#' . $c . "\n"; + } + } + $out .= $childIndent . self::stringifyKey($key) . ': ' . self::doStringify($obj[$key], $level + 1); + if ($elAnnotated !== null && $elAnnotated->trailingComment !== null && !is_array($innerValue) && !$innerValue instanceof \stdClass) { + $out .= ' #' . $elAnnotated->trailingComment; + } + } + if ($hasDangling) { + if (count($keys) > 0) { + $out .= "\n"; + } + foreach ($annotated->danglingComments as $c) { + $out .= $childIndent . '#' . $c . "\n"; + } + $out = rtrim($out, "\n"); + } + return $out . "\n" . $parentIndent . '}'; + } + + private static function stringifyKey(string $key): string + { + if (preg_match(self::KEY_PATTERN, $key)) { + return $key; + } + return self::quoteString($key); + } + + private static function quoteString(string $s): string + { + $out = '"'; + $len = mb_strlen($s, 'UTF-8'); + for ($i = 0; $i < $len; $i++) { + $c = mb_substr($s, $i, 1, 'UTF-8'); + $code = mb_ord($c, 'UTF-8'); + if ($c === '"') { + $out .= '\\"'; + } elseif ($c === '\\') { + $out .= '\\\\'; + } elseif ($c === "\n") { + $out .= '\\n'; + } elseif ($c === "\r") { + $out .= '\\r'; + } elseif ($c === "\t") { + $out .= '\\t'; + } elseif ($code < 0x20 || $code === 0x7F) { + $out .= '\\u{' . strtoupper(dechex($code)) . '}'; + } else { + $out .= $c; + } + } + return $out . '"'; + } + + private static function getIndent(int $level): string + { + return str_repeat(' ', $level); + } +} diff --git a/vendor/psr/container/.gitignore b/vendor/psr/container/.gitignore new file mode 100644 index 0000000..b2395aa --- /dev/null +++ b/vendor/psr/container/.gitignore @@ -0,0 +1,3 @@ +composer.lock +composer.phar +/vendor/ diff --git a/vendor/psr/container/LICENSE b/vendor/psr/container/LICENSE new file mode 100644 index 0000000..2877a48 --- /dev/null +++ b/vendor/psr/container/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013-2016 container-interop +Copyright (c) 2016 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/psr/container/README.md b/vendor/psr/container/README.md new file mode 100644 index 0000000..1b9d9e5 --- /dev/null +++ b/vendor/psr/container/README.md @@ -0,0 +1,13 @@ +Container interface +============== + +This repository holds all interfaces related to [PSR-11 (Container Interface)][psr-url]. + +Note that this is not a Container implementation of its own. It is merely abstractions that describe the components of a Dependency Injection Container. + +The installable [package][package-url] and [implementations][implementation-url] are listed on Packagist. + +[psr-url]: https://www.php-fig.org/psr/psr-11/ +[package-url]: https://packagist.org/packages/psr/container +[implementation-url]: https://packagist.org/providers/psr/container-implementation + diff --git a/vendor/psr/container/composer.json b/vendor/psr/container/composer.json new file mode 100644 index 0000000..baf6cd1 --- /dev/null +++ b/vendor/psr/container/composer.json @@ -0,0 +1,27 @@ +{ + "name": "psr/container", + "type": "library", + "description": "Common Container Interface (PHP FIG PSR-11)", + "keywords": ["psr", "psr-11", "container", "container-interop", "container-interface"], + "homepage": "https://github.com/php-fig/container", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "require": { + "php": ">=7.4.0" + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + } +} diff --git a/vendor/psr/container/src/ContainerExceptionInterface.php b/vendor/psr/container/src/ContainerExceptionInterface.php new file mode 100644 index 0000000..0f213f2 --- /dev/null +++ b/vendor/psr/container/src/ContainerExceptionInterface.php @@ -0,0 +1,12 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Command\CompleteCommand; +use Symfony\Component\Console\Command\DumpCompletionCommand; +use Symfony\Component\Console\Command\HelpCommand; +use Symfony\Component\Console\Command\LazyCommand; +use Symfony\Component\Console\Command\ListCommand; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Event\ConsoleAlarmEvent; +use Symfony\Component\Console\Event\ConsoleCommandEvent; +use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleSignalEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; +use Symfony\Component\Console\Exception\CommandNotFoundException; +use Symfony\Component\Console\Exception\ExceptionInterface; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Exception\NamespaceNotFoundException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Helper\DebugFormatterHelper; +use Symfony\Component\Console\Helper\DescriptorHelper; +use Symfony\Component\Console\Helper\FormatterHelper; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Helper\ProcessHelper; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputAwareInterface; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\SignalRegistry\SignalRegistry; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\ErrorHandler\ErrorHandler; +use Symfony\Component\EventDispatcher\EventDispatcherInterface as ComponentEventDispatcherInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\Service\ResetInterface; + +/** + * An Application is the container for a collection of commands. + * + * It is the main entry point of a Console application. + * + * This class is optimized for a standard CLI environment. + * + * Usage: + * + * $app = new Application('myapp', '1.0 (stable)'); + * $app->addCommand(new SimpleCommand()); + * $app->run(); + * + * @author Fabien Potencier + */ +class Application implements ResetInterface +{ + private array $commands = []; + private bool $wantHelps = false; + private ?Command $runningCommand = null; + private ?CommandLoaderInterface $commandLoader = null; + private bool $catchExceptions = true; + private bool $catchErrors = false; + private bool $autoExit = true; + private InputDefinition $definition; + private HelperSet $helperSet; + private ?EventDispatcherInterface $dispatcher = null; + private Terminal $terminal; + private string $defaultCommand; + private bool $singleCommand = false; + private bool $initialized = false; + private ?SignalRegistry $signalRegistry = null; + private array $signalsToDispatchEvent = []; + private ?int $alarmInterval = null; + + public function __construct( + private string $name = 'UNKNOWN', + private string $version = 'UNKNOWN', + ) { + $this->terminal = new Terminal(); + $this->defaultCommand = 'list'; + if (\defined('SIGINT') && SignalRegistry::isSupported()) { + $this->signalRegistry = new SignalRegistry(); + $this->signalsToDispatchEvent = [\SIGINT, \SIGQUIT, \SIGTERM, \SIGUSR1, \SIGUSR2, \SIGALRM]; + } + } + + /** + * @final + */ + public function setDispatcher(EventDispatcherInterface $dispatcher): void + { + $this->dispatcher = $dispatcher; + } + + public function setCommandLoader(CommandLoaderInterface $commandLoader): void + { + $this->commandLoader = $commandLoader; + } + + public function getSignalRegistry(): SignalRegistry + { + if (!$this->signalRegistry) { + throw new RuntimeException('Signals are not supported. Make sure that the "pcntl" extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); + } + + return $this->signalRegistry; + } + + public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent): void + { + $this->signalsToDispatchEvent = $signalsToDispatchEvent; + } + + /** + * Sets the interval to schedule a SIGALRM signal in seconds. + */ + public function setAlarmInterval(?int $seconds): void + { + $this->alarmInterval = $seconds; + $this->scheduleAlarm(); + } + + /** + * Gets the interval in seconds on which a SIGALRM signal is dispatched. + */ + public function getAlarmInterval(): ?int + { + return $this->alarmInterval; + } + + private function scheduleAlarm(): void + { + if (null !== $this->alarmInterval) { + $this->getSignalRegistry()->scheduleAlarm($this->alarmInterval); + } + } + + /** + * Runs the current application. + * + * @return int 0 if everything went fine, or an error code + * + * @throws \Exception When running fails. Bypass this when {@link setCatchExceptions()}. + */ + public function run(?InputInterface $input = null, ?OutputInterface $output = null): int + { + if (\function_exists('putenv')) { + @putenv('LINES='.$this->terminal->getHeight()); + @putenv('COLUMNS='.$this->terminal->getWidth()); + } + + $input ??= new ArgvInput(); + $output ??= new ConsoleOutput(); + + $renderException = function (\Throwable $e) use ($output) { + if ($output instanceof ConsoleOutputInterface) { + $this->renderThrowable($e, $output->getErrorOutput()); + } else { + $this->renderThrowable($e, $output); + } + }; + if ($phpHandler = set_exception_handler($renderException)) { + restore_exception_handler(); + if (!\is_array($phpHandler) || !$phpHandler[0] instanceof ErrorHandler) { + $errorHandler = true; + } elseif ($errorHandler = $phpHandler[0]->setExceptionHandler($renderException)) { + $phpHandler[0]->setExceptionHandler($errorHandler); + } + } + + $empty = new \stdClass(); + $prevShellVerbosity = [$_ENV['SHELL_VERBOSITY'] ?? $empty, $_SERVER['SHELL_VERBOSITY'] ?? $empty, getenv('SHELL_VERBOSITY')]; + + try { + $this->configureIO($input, $output); + + $exitCode = $this->doRun($input, $output); + } catch (\Throwable $e) { + if ($e instanceof \Exception && !$this->catchExceptions) { + throw $e; + } + if (!$e instanceof \Exception && !$this->catchErrors) { + throw $e; + } + + $renderException($e); + + $exitCode = $e->getCode(); + if (is_numeric($exitCode)) { + $exitCode = (int) $exitCode; + if ($exitCode <= 0) { + $exitCode = 1; + } + } else { + $exitCode = 1; + } + } finally { + // if the exception handler changed, keep it + // otherwise, unregister $renderException + if (!$phpHandler) { + if (set_exception_handler($renderException) === $renderException) { + restore_exception_handler(); + } + restore_exception_handler(); + } elseif (!$errorHandler) { + $finalHandler = $phpHandler[0]->setExceptionHandler(null); + if ($finalHandler !== $renderException) { + $phpHandler[0]->setExceptionHandler($finalHandler); + } + } + + // SHELL_VERBOSITY is set by Application::configureIO so we need to unset/reset it + // to its previous value to avoid one command verbosity to spread to other commands + if ($empty === $_ENV['SHELL_VERBOSITY'] = $prevShellVerbosity[0]) { + unset($_ENV['SHELL_VERBOSITY']); + } + if ($empty === $_SERVER['SHELL_VERBOSITY'] = $prevShellVerbosity[1]) { + unset($_SERVER['SHELL_VERBOSITY']); + } + if (\function_exists('putenv')) { + @putenv('SHELL_VERBOSITY'.(false === ($prevShellVerbosity[2] ?? false) ? '' : '='.$prevShellVerbosity[2])); + } + } + + if ($this->autoExit) { + if ($exitCode > 255) { + $exitCode = 255; + } + + exit($exitCode); + } + + return $exitCode; + } + + /** + * Runs the current application. + * + * @return int 0 if everything went fine, or an error code + */ + public function doRun(InputInterface $input, OutputInterface $output): int + { + if ($input->hasParameterOption(['--version', '-V'], true)) { + $output->writeln($this->getLongVersion()); + + return 0; + } + + try { + // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument. + $input->bind($this->getDefinition()); + } catch (ExceptionInterface) { + // Errors must be ignored, full binding/validation happens later when the command is known. + } + + $name = $this->getCommandName($input); + if ($input->hasParameterOption(['--help', '-h'], true)) { + if (!$name) { + $name = 'help'; + $input = new ArrayInput(['command_name' => $this->defaultCommand]); + } else { + $this->wantHelps = true; + } + } + + if (!$name) { + $name = $this->defaultCommand; + $definition = $this->getDefinition(); + $definition->setArguments(array_merge( + $definition->getArguments(), + [ + 'command' => new InputArgument('command', InputArgument::OPTIONAL, $definition->getArgument('command')->getDescription(), $name), + ] + )); + } + + try { + $this->runningCommand = null; + // the command name MUST be the first element of the input + $command = $this->find($name); + } catch (\Throwable $e) { + if (($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) && 1 === \count($alternatives = $e->getAlternatives()) && $input->isInteractive()) { + $alternative = $alternatives[0]; + + $style = new SymfonyStyle($input, $output); + $output->writeln(''); + $formattedBlock = (new FormatterHelper())->formatBlock(\sprintf('Command "%s" is not defined.', $name), 'error', true); + $output->writeln($formattedBlock); + if (!$style->confirm(\sprintf('Do you want to run "%s" instead? ', $alternative), false)) { + if (null !== $this->dispatcher) { + $event = new ConsoleErrorEvent($input, $output, $e); + $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); + + return $event->getExitCode(); + } + + return 1; + } + + $command = $this->find($alternative); + } else { + if (null !== $this->dispatcher) { + $event = new ConsoleErrorEvent($input, $output, $e); + $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); + + if (0 === $event->getExitCode()) { + return 0; + } + + $e = $event->getError(); + } + + try { + if ($e instanceof CommandNotFoundException && $namespace = $this->findNamespace($name)) { + $helper = new DescriptorHelper(); + $helper->describe($output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output, $this, [ + 'format' => 'txt', + 'raw_text' => false, + 'namespace' => $namespace, + 'short' => false, + ]); + + return isset($event) ? $event->getExitCode() : 1; + } + + throw $e; + } catch (NamespaceNotFoundException) { + throw $e; + } + } + } + + if ($command instanceof LazyCommand) { + $command = $command->getCommand(); + } + + $this->runningCommand = $command; + $exitCode = $this->doRunCommand($command, $input, $output); + $this->runningCommand = null; + + return $exitCode; + } + + public function reset(): void + { + } + + public function setHelperSet(HelperSet $helperSet): void + { + $this->helperSet = $helperSet; + } + + /** + * Get the helper set associated with the command. + */ + public function getHelperSet(): HelperSet + { + return $this->helperSet ??= $this->getDefaultHelperSet(); + } + + public function setDefinition(InputDefinition $definition): void + { + $this->definition = $definition; + } + + /** + * Gets the InputDefinition related to this Application. + */ + public function getDefinition(): InputDefinition + { + $this->definition ??= $this->getDefaultInputDefinition(); + + if ($this->singleCommand) { + $this->definition->setArguments(); + } + + return $this->definition; + } + + /** + * Adds suggestions to $suggestions for the current completion input (e.g. option or argument). + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ( + CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() + && 'command' === $input->getCompletionName() + ) { + foreach ($this->all() as $name => $command) { + // skip hidden commands and aliased commands as they already get added below + if ($command->isHidden() || $command->getName() !== $name) { + continue; + } + $suggestions->suggestValue(new Suggestion($command->getName(), $command->getDescription())); + foreach ($command->getAliases() as $name) { + $suggestions->suggestValue(new Suggestion($name, $command->getDescription())); + } + } + + return; + } + + if (CompletionInput::TYPE_OPTION_NAME === $input->getCompletionType()) { + $suggestions->suggestOptions($this->getDefinition()->getOptions()); + } + + if ( + CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType() + && ($definition = $this->getDefinition())->hasOption($input->getCompletionName()) + ) { + $definition->getOption($input->getCompletionName())->complete($input, $suggestions); + + return; + } + } + + /** + * Gets the help message. + */ + public function getHelp(): string + { + return $this->getLongVersion(); + } + + /** + * Gets whether to catch exceptions or not during commands execution. + */ + public function areExceptionsCaught(): bool + { + return $this->catchExceptions; + } + + /** + * Sets whether to catch exceptions or not during commands execution. + */ + public function setCatchExceptions(bool $boolean): void + { + $this->catchExceptions = $boolean; + } + + /** + * Sets whether to catch errors or not during commands execution. + */ + public function setCatchErrors(bool $catchErrors = true): void + { + $this->catchErrors = $catchErrors; + } + + /** + * Gets whether to automatically exit after a command execution or not. + */ + public function isAutoExitEnabled(): bool + { + return $this->autoExit; + } + + /** + * Sets whether to automatically exit after a command execution or not. + */ + public function setAutoExit(bool $boolean): void + { + $this->autoExit = $boolean; + } + + /** + * Gets the name of the application. + */ + public function getName(): string + { + return $this->name; + } + + /** + * Sets the application name. + */ + public function setName(string $name): void + { + $this->name = $name; + } + + /** + * Gets the application version. + */ + public function getVersion(): string + { + return $this->version; + } + + /** + * Sets the application version. + */ + public function setVersion(string $version): void + { + $this->version = $version; + } + + /** + * Returns the long version of the application. + */ + public function getLongVersion(): string + { + if ('UNKNOWN' !== $this->getName()) { + if ('UNKNOWN' !== $this->getVersion()) { + return \sprintf('%s %s', $this->getName(), $this->getVersion()); + } + + return $this->getName(); + } + + return 'Console Tool'; + } + + /** + * Registers a new command. + */ + public function register(string $name): Command + { + return $this->addCommand(new Command($name)); + } + + /** + * Adds an array of command objects. + * + * If a Command is not enabled it will not be added. + * + * @param callable[]|Command[] $commands An array of commands + */ + public function addCommands(array $commands): void + { + foreach ($commands as $command) { + $this->addCommand($command); + } + } + + /** + * @deprecated since Symfony 7.4, use Application::addCommand() instead + */ + public function add(Command $command): ?Command + { + trigger_deprecation('symfony/console', '7.4', 'The "%s()" method is deprecated and will be removed in Symfony 8.0, use "%s::addCommand()" instead.', __METHOD__, self::class); + + return $this->addCommand($command); + } + + /** + * Adds a command object. + * + * If a command with the same name already exists, it will be overridden. + * If the command is not enabled it will not be added. + */ + public function addCommand(callable|Command $command): ?Command + { + $this->init(); + + if (!$command instanceof Command) { + $command = new Command(null, $command); + } + + $command->setApplication($this); + + if (!$command->isEnabled()) { + $command->setApplication(null); + + return null; + } + + if (!$command instanceof LazyCommand) { + // Will throw if the command is not correctly initialized. + $command->getDefinition(); + } + + if (!$command->getName()) { + throw new LogicException(\sprintf('The command defined in "%s" cannot have an empty name.', get_debug_type($command))); + } + + $this->commands[$command->getName()] = $command; + + foreach ($command->getAliases() as $alias) { + $this->commands[$alias] = $command; + } + + return $command; + } + + /** + * Returns a registered command by name or alias. + * + * @throws CommandNotFoundException When given command name does not exist + */ + public function get(string $name): Command + { + $this->init(); + + if (!$this->has($name)) { + throw new CommandNotFoundException(\sprintf('The command "%s" does not exist.', $name)); + } + + // When the command has a different name than the one used at the command loader level + if (!isset($this->commands[$name])) { + throw new CommandNotFoundException(\sprintf('The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".', $name)); + } + + $command = $this->commands[$name]; + + if ($this->wantHelps) { + $this->wantHelps = false; + + $helpCommand = $this->get('help'); + $helpCommand->setCommand($command); + + return $helpCommand; + } + + return $command; + } + + /** + * Returns true if the command exists, false otherwise. + */ + public function has(string $name): bool + { + $this->init(); + + return isset($this->commands[$name]) || ($this->commandLoader?->has($name) && $this->addCommand($this->commandLoader->get($name))); + } + + /** + * Returns an array of all unique namespaces used by currently registered commands. + * + * It does not return the global namespace which always exists. + * + * @return string[] + */ + public function getNamespaces(): array + { + $namespaces = []; + foreach ($this->all() as $command) { + if ($command->isHidden()) { + continue; + } + + $namespaces[] = $this->extractAllNamespaces($command->getName()); + + foreach ($command->getAliases() as $alias) { + $namespaces[] = $this->extractAllNamespaces($alias); + } + } + + return array_values(array_unique(array_filter(array_merge([], ...$namespaces)))); + } + + /** + * Finds a registered namespace by a name or an abbreviation. + * + * @throws NamespaceNotFoundException When namespace is incorrect or ambiguous + */ + public function findNamespace(string $namespace): string + { + $allNamespaces = $this->getNamespaces(); + $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*'; + $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces); + + if (!$namespaces) { + $message = \sprintf('There are no commands defined in the "%s" namespace.', $namespace); + + if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) { + if (1 == \count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + + $message .= implode("\n ", $alternatives); + } + + throw new NamespaceNotFoundException($message, $alternatives); + } + + $exact = \in_array($namespace, $namespaces, true); + if (\count($namespaces) > 1 && !$exact) { + throw new NamespaceNotFoundException(\sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces)); + } + + return $exact ? $namespace : reset($namespaces); + } + + /** + * Finds a command by name or alias. + * + * Contrary to get, this command tries to find the best + * match if you give it an abbreviation of a name or alias. + * + * @throws CommandNotFoundException When command name is incorrect or ambiguous + */ + public function find(string $name): Command + { + $this->init(); + + $aliases = []; + + foreach ($this->commands as $command) { + foreach ($command->getAliases() as $alias) { + if (!$this->has($alias)) { + $this->commands[$alias] = $command; + } + } + } + + if ($this->has($name)) { + return $this->get($name); + } + + $allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands); + $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $name))).'[^:]*'; + $commands = preg_grep('{^'.$expr.'}', $allCommands); + + if (!$commands) { + $commands = preg_grep('{^'.$expr.'}i', $allCommands); + } + + // if no commands matched or we just matched namespaces + if (!$commands || \count(preg_grep('{^'.$expr.'$}i', $commands)) < 1) { + if (false !== $pos = strrpos($name, ':')) { + // check if a namespace exists and contains commands + $this->findNamespace(substr($name, 0, $pos)); + } + + $message = \sprintf('Command "%s" is not defined.', $name); + + if ($alternatives = $this->findAlternatives($name, $allCommands)) { + $wantHelps = $this->wantHelps; + $this->wantHelps = false; + + // remove hidden commands + if ($alternatives = array_filter($alternatives, fn ($name) => !$this->get($name)->isHidden())) { + $message .= \sprintf("\n\nDid you mean %s?\n %s", 1 === \count($alternatives) ? 'this' : 'one of these', implode("\n ", $alternatives)); + } + $this->wantHelps = $wantHelps; + } + + throw new CommandNotFoundException($message, array_values($alternatives)); + } + + // filter out aliases for commands which are already on the list + if (\count($commands) > 1) { + $commandList = $this->commandLoader ? array_merge(array_flip($this->commandLoader->getNames()), $this->commands) : $this->commands; + $commands = array_unique(array_filter($commands, function ($nameOrAlias) use (&$commandList, $commands, &$aliases) { + if (!$commandList[$nameOrAlias] instanceof Command) { + $commandList[$nameOrAlias] = $this->commandLoader->get($nameOrAlias); + } + + $commandName = $commandList[$nameOrAlias]->getName(); + + $aliases[$nameOrAlias] = $commandName; + + return $commandName === $nameOrAlias || !\in_array($commandName, $commands, true); + })); + } + + // check whether all commands left are aliases to the same one + if (\count($commands) > 1) { + $uniqueCommands = array_unique(array_map(function ($nameOrAlias) use (&$commandList) { + if (!$commandList[$nameOrAlias] instanceof Command) { + $commandList[$nameOrAlias] = $this->commandLoader->get($nameOrAlias); + } + + return $commandList[$nameOrAlias]->getName(); + }, $commands)); + + if (1 === \count($uniqueCommands)) { + $commands = [reset($uniqueCommands)]; + } + } + + if (\count($commands) > 1) { + $usableWidth = $this->terminal->getWidth() - 10; + $abbrevs = array_values($commands); + $maxLen = 0; + foreach ($abbrevs as $abbrev) { + $maxLen = max(Helper::width($abbrev), $maxLen); + } + $abbrevs = array_map(static function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) { + if ($commandList[$cmd]->isHidden()) { + unset($commands[array_search($cmd, $commands)]); + + return false; + } + + $abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription(); + + return Helper::width($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev; + }, array_values($commands)); + + if (\count($commands) > 1) { + $suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs)); + + throw new CommandNotFoundException(\sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands)); + } + } + + $command = $commands ? $this->get(reset($commands)) : null; + + if (!$command || $command->isHidden()) { + throw new CommandNotFoundException(\sprintf('The command "%s" does not exist.', $name)); + } + + return $command; + } + + /** + * Gets the commands (registered in the given namespace if provided). + * + * The array keys are the full names and the values the command instances. + * + * @return Command[] + */ + public function all(?string $namespace = null): array + { + $this->init(); + + if (null === $namespace) { + if (!$this->commandLoader) { + return $this->commands; + } + + $commands = $this->commands; + foreach ($this->commandLoader->getNames() as $name) { + if (!isset($commands[$name]) && $this->has($name)) { + $commands[$name] = $this->get($name); + } + } + + return $commands; + } + + $commands = []; + foreach ($this->commands as $name => $command) { + if ($namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) { + $commands[$name] = $command; + } + } + + if ($this->commandLoader) { + foreach ($this->commandLoader->getNames() as $name) { + if (!isset($commands[$name]) && $namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1) && $this->has($name)) { + $commands[$name] = $this->get($name); + } + } + } + + return $commands; + } + + /** + * Returns an array of possible abbreviations given a set of names. + * + * @return string[][] + */ + public static function getAbbreviations(array $names): array + { + $abbrevs = []; + foreach ($names as $name) { + for ($len = \strlen($name); $len > 0; --$len) { + $abbrev = substr($name, 0, $len); + $abbrevs[$abbrev][] = $name; + } + } + + return $abbrevs; + } + + public function renderThrowable(\Throwable $e, OutputInterface $output): void + { + $output->writeln('', OutputInterface::VERBOSITY_QUIET); + + $this->doRenderThrowable($e, $output); + + if (null !== $this->runningCommand) { + $output->writeln(\sprintf('%s', OutputFormatter::escape(\sprintf($this->runningCommand->getSynopsis(), $this->getName()))), OutputInterface::VERBOSITY_QUIET); + $output->writeln('', OutputInterface::VERBOSITY_QUIET); + } + } + + protected function doRenderThrowable(\Throwable $e, OutputInterface $output): void + { + do { + $message = trim($e->getMessage()); + if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { + $class = get_debug_type($e); + $title = \sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : ''); + $len = Helper::width($title); + } else { + $len = 0; + } + + if (str_contains($message, "@anonymous\0")) { + $message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', static fn ($m) => class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0], $message); + } + + $width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : \PHP_INT_MAX; + $lines = []; + foreach ('' !== $message ? preg_split('/\r?\n/', $message) : [] as $line) { + foreach ($this->splitStringByWidth($line, $width - 4) as $line) { + // pre-format lines to get the right string length + $lineLength = Helper::width($line) + 4; + $lines[] = [$line, $lineLength]; + + $len = max($lineLength, $len); + } + } + + $messages = []; + if (!$e instanceof ExceptionInterface || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { + $messages[] = \sprintf('%s', OutputFormatter::escape(\sprintf('In %s line %s:', basename($e->getFile()) ?: 'n/a', $e->getLine() ?: 'n/a'))); + } + $messages[] = $emptyLine = \sprintf('%s', str_repeat(' ', $len)); + if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { + $messages[] = \sprintf('%s%s', $title, str_repeat(' ', max(0, $len - Helper::width($title)))); + } + foreach ($lines as $line) { + $messages[] = \sprintf(' %s %s', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1])); + } + $messages[] = $emptyLine; + $messages[] = ''; + + $output->writeln($messages, OutputInterface::VERBOSITY_QUIET); + + if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { + $output->writeln('Exception trace:', OutputInterface::VERBOSITY_QUIET); + + // exception related properties + $trace = $e->getTrace(); + + array_unshift($trace, [ + 'function' => '', + 'file' => $e->getFile() ?: 'n/a', + 'line' => $e->getLine() ?: 'n/a', + 'args' => [], + ]); + + for ($i = 0, $count = \count($trace); $i < $count; ++$i) { + $class = $trace[$i]['class'] ?? ''; + $type = $trace[$i]['type'] ?? ''; + $function = $trace[$i]['function'] ?? ''; + $file = $trace[$i]['file'] ?? 'n/a'; + $line = $trace[$i]['line'] ?? 'n/a'; + + $output->writeln(\sprintf(' %s%s at %s:%s', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET); + } + + $output->writeln('', OutputInterface::VERBOSITY_QUIET); + } + } while ($e = $e->getPrevious()); + } + + /** + * Configures the input and output instances based on the user arguments and options. + */ + protected function configureIO(InputInterface $input, OutputInterface $output): void + { + if ($input->hasParameterOption(['--ansi'], true)) { + $output->setDecorated(true); + } elseif ($input->hasParameterOption(['--no-ansi'], true)) { + $output->setDecorated(false); + } + + $shellVerbosity = match (true) { + $input->hasParameterOption(['--silent'], true) => -2, + $input->hasParameterOption(['--quiet', '-q'], true) => -1, + $input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true) => 3, + $input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true) => 2, + $input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true) => 1, + default => (int) ($_ENV['SHELL_VERBOSITY'] ?? $_SERVER['SHELL_VERBOSITY'] ?? getenv('SHELL_VERBOSITY')), + }; + + $output->setVerbosity(match ($shellVerbosity) { + -2 => OutputInterface::VERBOSITY_SILENT, + -1 => OutputInterface::VERBOSITY_QUIET, + 1 => OutputInterface::VERBOSITY_VERBOSE, + 2 => OutputInterface::VERBOSITY_VERY_VERBOSE, + 3 => OutputInterface::VERBOSITY_DEBUG, + default => ($shellVerbosity = 0) ?: $output->getVerbosity(), + }); + + if (0 > $shellVerbosity || $input->hasParameterOption(['--no-interaction', '-n'], true)) { + $input->setInteractive(false); + } + + if (\function_exists('putenv')) { + @putenv('SHELL_VERBOSITY='.$shellVerbosity); + } + $_ENV['SHELL_VERBOSITY'] = $shellVerbosity; + $_SERVER['SHELL_VERBOSITY'] = $shellVerbosity; + } + + /** + * Runs the current command. + * + * If an event dispatcher has been attached to the application, + * events are also dispatched during the life-cycle of the command. + * + * @return int 0 if everything went fine, or an error code + */ + protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output): int + { + foreach ($command->getHelperSet() as $helper) { + if ($helper instanceof InputAwareInterface) { + $helper->setInput($input); + } + } + + $registeredSignals = false; + if (($commandSignals = $command->getSubscribedSignals()) || $this->dispatcher && $this->signalsToDispatchEvent) { + $signalRegistry = $this->getSignalRegistry(); + + $registeredSignals = true; + $this->getSignalRegistry()->pushCurrentHandlers(); + + if ($this->dispatcher) { + // Build the lazy listeners before a signal handler needs them: a signal can interrupt the autoloading of a class they use, which cannot be loaded again from the handler + if ($this->signalsToDispatchEvent && $this->dispatcher instanceof ComponentEventDispatcherInterface) { + foreach ([ConsoleEvents::SIGNAL, ConsoleAlarmEvent::class, ConsoleEvents::TERMINATE] as $eventName) { + $this->dispatcher->getListeners($eventName); + } + } + + // We register application signals, so that we can dispatch the event + foreach ($this->signalsToDispatchEvent as $signal) { + // When this application runs inside another application that handles the signal, the outer application decides if the process exits + $previousHandler = pcntl_signal_get_handler($signal); + $signalEvent = new ConsoleSignalEvent($command, $input, $output, $signal, \is_array($previousHandler) && $previousHandler[0] instanceof SignalRegistry ? false : 0); + $alarmEvent = \SIGALRM === $signal ? new ConsoleAlarmEvent($command, $input, $output) : null; + + $signalRegistry->register($signal, function ($signal) use ($signalEvent, $alarmEvent, $command, $commandSignals, $input, $output) { + $this->dispatcher->dispatch($signalEvent, ConsoleEvents::SIGNAL); + $exitCode = $signalEvent->getExitCode(); + + if (null !== $alarmEvent) { + if (false !== $exitCode) { + $alarmEvent->setExitCode($exitCode); + } else { + $alarmEvent->abortExit(); + } + $this->dispatcher->dispatch($alarmEvent); + $exitCode = $alarmEvent->getExitCode(); + } + + // If the command is signalable, we call the handleSignal() method + if (\in_array($signal, $commandSignals, true)) { + $exitCode = $command->handleSignal($signal, $exitCode); + } + + if (\SIGALRM === $signal) { + $this->scheduleAlarm(); + } + + if (false !== $exitCode) { + $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode, $signal); + $this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE); + + exit($event->getExitCode()); + } + }); + } + + // then we register command signals, but not if already handled after the dispatcher + $commandSignals = array_diff($commandSignals, $this->signalsToDispatchEvent); + } + + foreach ($commandSignals as $signal) { + $signalRegistry->register($signal, function (int $signal) use ($command): void { + if (\SIGALRM === $signal) { + $this->scheduleAlarm(); + } + + if (false !== $exitCode = $command->handleSignal($signal)) { + exit($exitCode); + } + }); + } + } + + if (null === $this->dispatcher) { + try { + return $command->run($input, $output); + } finally { + if ($registeredSignals) { + $this->getSignalRegistry()->popPreviousHandlers(); + } + } + } + + // bind before the console.command event, so the listeners have access to input options/arguments + try { + $command->mergeApplicationDefinition(); + $input->bind($command->getDefinition()); + } catch (ExceptionInterface) { + // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition + } + + $event = new ConsoleCommandEvent($command, $input, $output); + $e = null; + + try { + $this->dispatcher->dispatch($event, ConsoleEvents::COMMAND); + + if ($event->commandShouldRun()) { + $exitCode = $command->run($input, $output); + } else { + $exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED; + } + } catch (\Throwable $e) { + $event = new ConsoleErrorEvent($input, $output, $e, $command); + $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); + $e = $event->getError(); + + if (0 === $exitCode = $event->getExitCode()) { + $e = null; + } + } finally { + if ($registeredSignals) { + $this->getSignalRegistry()->popPreviousHandlers(); + } + } + + $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode); + $this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE); + + if (null !== $e) { + throw $e; + } + + return $event->getExitCode(); + } + + /** + * Gets the name of the command based on input. + */ + protected function getCommandName(InputInterface $input): ?string + { + return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument(); + } + + /** + * Gets the default input definition. + */ + protected function getDefaultInputDefinition(): InputDefinition + { + return new InputDefinition([ + new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), + new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display help for the given command. When no command is given display help for the '.$this->defaultCommand.' command'), + new InputOption('--silent', null, InputOption::VALUE_NONE, 'Do not output any message'), + new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Only errors are displayed. All other output is suppressed'), + new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'), + new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'), + new InputOption('--ansi', '', InputOption::VALUE_NEGATABLE, 'Force (or disable --no-ansi) ANSI output', null), + new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'), + ]); + } + + /** + * Gets the default commands that should always be available. + * + * @return Command[] + */ + protected function getDefaultCommands(): array + { + return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()]; + } + + /** + * Gets the default helper set with the helpers that should always be available. + */ + protected function getDefaultHelperSet(): HelperSet + { + return new HelperSet([ + new FormatterHelper(), + new DebugFormatterHelper(), + new ProcessHelper(), + new QuestionHelper(), + ]); + } + + /** + * Returns abbreviated suggestions in string format. + */ + private function getAbbreviationSuggestions(array $abbrevs): string + { + return ' '.implode("\n ", $abbrevs); + } + + /** + * Returns the namespace part of the command name. + * + * This method is not part of public API and should not be used directly. + */ + public function extractNamespace(string $name, ?int $limit = null): string + { + $parts = explode(':', $name, -1); + + return implode(':', null === $limit ? $parts : \array_slice($parts, 0, $limit)); + } + + /** + * Finds alternative of $name among $collection, + * if nothing is found in $collection, try in $abbrevs. + * + * @return string[] + */ + private function findAlternatives(string $name, iterable $collection): array + { + $threshold = 1e3; + $alternatives = []; + + $collectionParts = []; + foreach ($collection as $item) { + $collectionParts[$item] = explode(':', $item); + } + + foreach (explode(':', $name) as $i => $subname) { + foreach ($collectionParts as $collectionName => $parts) { + $exists = isset($alternatives[$collectionName]); + if (!isset($parts[$i]) && $exists) { + $alternatives[$collectionName] += $threshold; + continue; + } elseif (!isset($parts[$i])) { + continue; + } + + $lev = levenshtein($subname, $parts[$i]); + if ($lev <= \strlen($subname) / 3 || '' !== $subname && str_contains($parts[$i], $subname)) { + $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev; + } elseif ($exists) { + $alternatives[$collectionName] += $threshold; + } + } + } + + foreach ($collection as $item) { + $lev = levenshtein($name, $item); + if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) { + $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; + } + } + + $alternatives = array_filter($alternatives, static fn ($lev) => $lev < 2 * $threshold); + ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE); + + return array_keys($alternatives); + } + + /** + * Sets the default Command name. + * + * @return $this + */ + public function setDefaultCommand(string $commandName, bool $isSingleCommand = false): static + { + $this->defaultCommand = explode('|', ltrim($commandName, '|'))[0]; + + if ($isSingleCommand) { + // Ensure the command exist + $this->find($commandName); + + $this->singleCommand = true; + } + + return $this; + } + + /** + * @internal + */ + public function isSingleCommand(): bool + { + return $this->singleCommand; + } + + private function splitStringByWidth(string $string, int $width): array + { + // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly. + // additionally, array_slice() is not enough as some character has doubled width. + // we need a function to split string not by character count but by string width + if (false === $encoding = mb_detect_encoding($string, null, true)) { + return str_split($string, $width); + } + + $utf8String = mb_convert_encoding($string, 'utf8', $encoding); + $lines = []; + $line = ''; + + $offset = 0; + while (preg_match('/.{1,10000}/u', $utf8String, $m, 0, $offset)) { + $offset += \strlen($m[0]); + + foreach (preg_split('//u', $m[0]) as $char) { + // test if $char could be appended to current line + if (Helper::width($line.$char) <= $width) { + $line .= $char; + continue; + } + // if not, push current line to array and make new line + $lines[] = str_pad($line, $width); + $line = $char; + } + } + + $lines[] = \count($lines) ? str_pad($line, $width) : $line; + + return mb_convert_encoding($lines, $encoding, 'utf8'); + } + + /** + * Returns all namespaces of the command name. + * + * @return string[] + */ + private function extractAllNamespaces(string $name): array + { + // -1 as third argument is needed to skip the command short name when exploding + $parts = explode(':', $name, -1); + $namespaces = []; + + foreach ($parts as $part) { + if ($namespaces) { + $namespaces[] = end($namespaces).':'.$part; + } else { + $namespaces[] = $part; + } + } + + return $namespaces; + } + + private function init(): void + { + if ($this->initialized) { + return; + } + $this->initialized = true; + + if ((new \ReflectionMethod($this, 'add'))->getDeclaringClass()->getName() !== (new \ReflectionMethod($this, 'addCommand'))->getDeclaringClass()->getName()) { + $adder = $this->add(...); + } else { + $adder = $this->addCommand(...); + } + + foreach ($this->getDefaultCommands() as $command) { + $adder($command); + } + } +} diff --git a/vendor/symfony/console/Attribute/Argument.php b/vendor/symfony/console/Attribute/Argument.php new file mode 100644 index 0000000..7c5ba20 --- /dev/null +++ b/vendor/symfony/console/Attribute/Argument.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Attribute; + +use Symfony\Component\Console\Attribute\Reflection\ReflectionMember; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\String\UnicodeString; + +#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)] +class Argument +{ + private const ALLOWED_TYPES = ['string', 'bool', 'int', 'float', 'array']; + + private string|bool|int|float|array|null $default = null; + private array|\Closure $suggestedValues; + private ?int $mode = null; + /** + * @var string|class-string<\BackedEnum> + */ + private string $typeName = ''; + private ?InteractiveAttributeInterface $interactiveAttribute = null; + + /** + * Represents a console command definition. + * + * If unset, the `name` value will be inferred from the parameter definition. + * + * @param array|callable(CompletionInput):list $suggestedValues The values used for input completion + */ + public function __construct( + public string $description = '', + public string $name = '', + array|callable $suggestedValues = [], + ) { + $this->suggestedValues = \is_callable($suggestedValues) ? $suggestedValues(...) : $suggestedValues; + } + + /** + * @internal + */ + public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member): ?self + { + $reflection = new ReflectionMember($member); + + if (!$self = $reflection->getAttribute(self::class)) { + return null; + } + + $type = $reflection->getType(); + $name = $reflection->getName(); + + if (!$type instanceof \ReflectionNamedType) { + throw new LogicException(\sprintf('The %s "$%s" of "%s" must have a named type. Untyped, Union or Intersection types are not supported for command arguments.', $reflection->getMemberName(), $name, $reflection->getSourceName())); + } + + $self->typeName = $type->getName(); + $isBackedEnum = is_subclass_of($self->typeName, \BackedEnum::class); + + if (!\in_array($self->typeName, self::ALLOWED_TYPES, true) && !$isBackedEnum) { + throw new LogicException(\sprintf('The type "%s" on %s "$%s" of "%s" is not supported as a command argument. Only "%s" types and backed enums are allowed.', $self->typeName, $reflection->getMemberName(), $name, $reflection->getSourceName(), implode('", "', self::ALLOWED_TYPES))); + } + + if (!$self->name) { + $self->name = (new UnicodeString($name))->kebab(); + } + + $self->default = $reflection->hasDefaultValue() ? $reflection->getDefaultValue() : null; + + $isOptional = $reflection->hasDefaultValue() || $reflection->isNullable(); + $self->mode = $isOptional ? InputArgument::OPTIONAL : InputArgument::REQUIRED; + if ('array' === $self->typeName) { + $self->mode |= InputArgument::IS_ARRAY; + } + + if (\is_array($self->suggestedValues) && !\is_callable($self->suggestedValues) && 2 === \count($self->suggestedValues) && ($instance = $reflection->getSourceThis()) && $instance::class === $self->suggestedValues[0] && \is_callable([$instance, $self->suggestedValues[1]])) { + // In case that the callback is declared as a static method `[Foo::class, 'methodName']` - yet it is not callable, + // while non-static method `[Foo $instance, 'methodName']` would be callable, we transform the callback on the fly into a non-static version. + $self->suggestedValues = [$instance, $self->suggestedValues[1]]; + } + + if ($isBackedEnum && !$self->suggestedValues) { + $self->suggestedValues = array_column($self->typeName::cases(), 'value'); + } + + $self->interactiveAttribute = Ask::tryFrom($member, $self->name); + + if ($self->interactiveAttribute && $isOptional) { + throw new LogicException(\sprintf('The %s "$%s" argument of "%s" cannot be both interactive and optional.', $reflection->getMemberName(), $self->name, $reflection->getSourceName())); + } + + return $self; + } + + /** + * @internal + */ + public function toInputArgument(): InputArgument + { + $suggestedValues = \is_callable($this->suggestedValues) ? ($this->suggestedValues)(...) : $this->suggestedValues; + + return new InputArgument($this->name, $this->mode, $this->description, $this->default, $suggestedValues); + } + + /** + * @internal + */ + public function resolveValue(InputInterface $input): mixed + { + $value = $input->getArgument($this->name); + + if (is_subclass_of($this->typeName, \BackedEnum::class) && (\is_string($value) || \is_int($value))) { + return $this->typeName::tryFrom($value) ?? throw InvalidArgumentException::fromEnumValue($this->name, $value, $this->suggestedValues); + } + + if (\is_string($value) && \in_array($this->typeName, ['int', 'float'], true) && !is_numeric($value)) { + throw InvalidArgumentException::fromInvalidType($this->name, $value, $this->typeName); + } + + return $value; + } + + /** + * @internal + */ + public function getInteractiveAttribute(): ?InteractiveAttributeInterface + { + return $this->interactiveAttribute; + } + + /** + * @internal + */ + public function isRequired(): bool + { + return InputArgument::REQUIRED === (InputArgument::REQUIRED & $this->mode); + } +} diff --git a/vendor/symfony/console/Attribute/AsCommand.php b/vendor/symfony/console/Attribute/AsCommand.php new file mode 100644 index 0000000..02f1562 --- /dev/null +++ b/vendor/symfony/console/Attribute/AsCommand.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Attribute; + +/** + * Service tag to autoconfigure commands. + * + * @final since Symfony 7.3 + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class AsCommand +{ + /** + * @param string $name The name of the command, used when calling it (i.e. "cache:clear") + * @param string|null $description The description of the command, displayed with the help page + * @param string[] $aliases The list of aliases of the command. The command will be executed when using one of them (i.e. "cache:clean") + * @param bool $hidden If true, the command won't be shown when listing all the available commands, but it can still be run as any other command + * @param string|null $help The help content of the command, displayed with the help page + * @param string[] $usages The list of usage examples, displayed with the help page + */ + public function __construct( + public string $name, + public ?string $description = null, + array $aliases = [], + bool $hidden = false, + public ?string $help = null, + public array $usages = [], + ) { + if (!$hidden && !$aliases) { + return; + } + + $name = explode('|', $name); + $name = array_merge($name, $aliases); + + if ($hidden && '' !== $name[0]) { + array_unshift($name, ''); + } + + $this->name = implode('|', $name); + } +} diff --git a/vendor/symfony/console/Attribute/Ask.php b/vendor/symfony/console/Attribute/Ask.php new file mode 100644 index 0000000..771d90a --- /dev/null +++ b/vendor/symfony/console/Attribute/Ask.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Attribute; + +use Symfony\Component\Console\Attribute\Reflection\ReflectionMember; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Style\SymfonyStyle; + +#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)] +class Ask implements InteractiveAttributeInterface +{ + public ?\Closure $normalizer; + public ?\Closure $validator; + private \Closure $closure; + + /** + * @param string $question The question to ask the user + * @param string|bool|int|float|null $default The default answer to return if the user enters nothing + * @param bool $hidden Whether the user response must be hidden or not + * @param bool $multiline Whether the user response should accept newline characters + * @param bool $trimmable Whether the user response must be trimmed or not + * @param int|null $timeout The maximum time the user has to answer the question in seconds + * @param callable|null $validator The validator for the question + * @param int|null $maxAttempts The maximum number of attempts allowed to answer the question. + * Null means an unlimited number of attempts + */ + public function __construct( + public string $question, + public string|bool|int|float|null $default = null, + public bool $hidden = false, + public bool $multiline = false, + public bool $trimmable = true, + public ?int $timeout = null, + ?callable $normalizer = null, + ?callable $validator = null, + public ?int $maxAttempts = null, + ) { + $this->normalizer = $normalizer ? $normalizer(...) : null; + $this->validator = $validator ? $validator(...) : null; + } + + /** + * @internal + */ + public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member, string $name): ?self + { + $reflection = new ReflectionMember($member); + + if (!$self = $reflection->getAttribute(self::class)) { + return null; + } + + $type = $reflection->getType(); + + if (!$type instanceof \ReflectionNamedType) { + throw new LogicException(\sprintf('The %s "$%s" of "%s" must have a named type. Untyped, Union or Intersection types are not supported for interactive questions.', $reflection->getMemberName(), $name, $reflection->getSourceName())); + } + + if ('array' === $type->getName() && null !== $self->default) { + throw new LogicException(\sprintf('The "%s::$default" value is not supported for the array "$%s" of "%s", because an empty answer ends the collection.', self::class, $name, $reflection->getSourceName())); + } + + $self->closure = function (SymfonyStyle $io, InputInterface $input) use ($self, $reflection, $name, $type) { + if ($reflection->isProperty() && isset($this->{$reflection->getName()})) { + return; + } + + if ($reflection->isParameter() && !\in_array($input->getArgument($name), [null, []], true)) { + return; + } + + if ('bool' === $type->getName()) { + $self->default ??= false; + + if (!\is_bool($self->default)) { + throw new LogicException(\sprintf('The "%s::$default" value for the %s "$%s" of "%s" must be a boolean.', self::class, $reflection->getMemberName(), $name, $reflection->getSourceName())); + } + + $question = new ConfirmationQuestion($self->question, $self->default); + } else { + $question = new Question($self->question, $self->default); + } + $question->setHidden($self->hidden); + $question->setMultiline($self->multiline); + $question->setTrimmable($self->trimmable); + $question->setTimeout($self->timeout); + + if (!$self->validator && $reflection->isProperty() && 'array' !== $type->getName()) { + $self->validator = fn (mixed $value): mixed => $this->{$reflection->getName()} = $value; + } + + $question->setValidator($self->validator); + $question->setMaxAttempts($self->maxAttempts); + + if ($self->normalizer) { + $question->setNormalizer($self->normalizer); + } elseif (is_subclass_of($type->getName(), \BackedEnum::class)) { + /** @var class-string<\BackedEnum> $backedType */ + $backedType = $reflection->getType()->getName(); + $question->setNormalizer(static fn (string|int $value) => $backedType::tryFrom($value) ?? throw InvalidArgumentException::fromEnumValue($reflection->getName(), $value, array_column($backedType::cases(), 'value'))); + } + + if ('array' === $type->getName()) { + $value = []; + while (null !== $v = $io->askQuestion($question)) { + if ($question->isTrimmable()) { + $v = trim($v); + } + if ("\x4" === $v || \PHP_EOL === $v || '' === $v) { + break; + } + $value[] = $v; + } + } else { + $value = $io->askQuestion($question); + } + + if (null === $value && !$reflection->isNullable()) { + return; + } + + if ($reflection->isProperty()) { + $this->{$reflection->getName()} = $value; + } else { + $input->setArgument($name, $value); + } + }; + + return $self; + } + + /** + * @internal + */ + public function getFunction(object $instance): \ReflectionFunction + { + return new \ReflectionFunction($this->closure->bindTo($instance, $instance::class)); + } +} diff --git a/vendor/symfony/console/Attribute/Interact.php b/vendor/symfony/console/Attribute/Interact.php new file mode 100644 index 0000000..188071f --- /dev/null +++ b/vendor/symfony/console/Attribute/Interact.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Attribute; + +use Symfony\Component\Console\Exception\LogicException; + +#[\Attribute(\Attribute::TARGET_METHOD)] +class Interact implements InteractiveAttributeInterface +{ + private \ReflectionMethod $method; + + /** + * @internal + */ + public static function tryFrom(\ReflectionMethod $method): ?self + { + /** @var self|null $self */ + if (!$self = ($method->getAttributes(self::class)[0] ?? null)?->newInstance()) { + return null; + } + + if (!$method->isPublic() || $method->isStatic()) { + throw new LogicException(\sprintf('The interactive method "%s::%s()" must be public and non-static.', $method->getDeclaringClass()->getName(), $method->getName())); + } + + if ('__invoke' === $method->getName()) { + throw new LogicException(\sprintf('The "%s::__invoke()" method cannot be used as an interactive method.', $method->getDeclaringClass()->getName())); + } + + $self->method = $method; + + return $self; + } + + /** + * @internal + */ + public function getFunction(object $instance): \ReflectionFunction + { + return new \ReflectionFunction($this->method->getClosure($instance)); + } +} diff --git a/vendor/symfony/console/Attribute/InteractiveAttributeInterface.php b/vendor/symfony/console/Attribute/InteractiveAttributeInterface.php new file mode 100644 index 0000000..0b02d7d --- /dev/null +++ b/vendor/symfony/console/Attribute/InteractiveAttributeInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Attribute; + +/** + * @internal + */ +interface InteractiveAttributeInterface +{ + public function getFunction(object $instance): \ReflectionFunction; +} diff --git a/vendor/symfony/console/Attribute/MapInput.php b/vendor/symfony/console/Attribute/MapInput.php new file mode 100644 index 0000000..0b50836 --- /dev/null +++ b/vendor/symfony/console/Attribute/MapInput.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Attribute; + +use Symfony\Component\Console\Attribute\Reflection\ReflectionMember; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Interaction\Interaction; + +/** + * Maps a command input into an object (DTO). + */ +#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)] +final class MapInput +{ + /** + * @var array + */ + private array $definition = []; + + private \ReflectionClass $class; + + /** + * @var list + */ + private array $interactiveAttributes = []; + + /** + * @internal + */ + public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member): ?self + { + $reflection = new ReflectionMember($member); + + if (!$self = $reflection->getAttribute(self::class)) { + return null; + } + + $type = $reflection->getType(); + + if (!$type instanceof \ReflectionNamedType) { + throw new LogicException(\sprintf('The input %s "%s" must have a named type.', $reflection->getMemberName(), $member->name)); + } + + if (!class_exists($class = $type->getName())) { + throw new LogicException(\sprintf('The input class "%s" does not exist.', $type->getName())); + } + + $self->class = new \ReflectionClass($class); + + foreach ($self->class->getProperties() as $property) { + if ($argument = Argument::tryFrom($property)) { + $self->definition[$property->name] = $argument; + } elseif ($option = Option::tryFrom($property)) { + $self->definition[$property->name] = $option; + } elseif ($input = self::tryFrom($property)) { + $self->definition[$property->name] = $input; + } + + if (isset($self->definition[$property->name]) && (!$property->isPublic() || $property->isStatic())) { + throw new LogicException(\sprintf('The input property "%s::$%s" must be public and non-static.', $self->class->name, $property->name)); + } + } + + if (!$self->definition) { + throw new LogicException(\sprintf('The input class "%s" must have at least one argument or option.', $self->class->name)); + } + + foreach ($self->class->getMethods() as $method) { + if ($attribute = Interact::tryFrom($method)) { + $self->interactiveAttributes[] = $attribute; + } + } + + return $self; + } + + /** + * @internal + */ + public function resolveValue(InputInterface $input): object + { + $instance = $this->class->newInstanceWithoutConstructor(); + + foreach ($this->definition as $name => $spec) { + // ignore required arguments that are not set yet (may happen in interactive mode) + if ($spec instanceof Argument && $spec->isRequired() && \in_array($input->getArgument($spec->name), [null, []], true)) { + continue; + } + + $instance->$name = $spec->resolveValue($input); + } + + return $instance; + } + + /** + * @internal + */ + public function setValue(InputInterface $input, object $object): void + { + foreach ($this->definition as $name => $spec) { + $property = $this->class->getProperty($name); + + if (!$property->isInitialized($object) || \in_array($value = $property->getValue($object), [null, []], true)) { + continue; + } + + match (true) { + $spec instanceof Argument => $input->setArgument($spec->name, $value), + $spec instanceof Option => $input->setOption($spec->name, $value), + $spec instanceof self => $spec->setValue($input, $value), + default => throw new LogicException('Unexpected specification type.'), + }; + } + } + + /** + * @return iterable + */ + public function getArguments(): iterable + { + foreach ($this->definition as $spec) { + if ($spec instanceof Argument) { + yield $spec; + } elseif ($spec instanceof self) { + yield from $spec->getArguments(); + } + } + } + + /** + * @return iterable