├── .release-please-manifest.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── config └── services.yml ├── release-please-config.json ├── src ├── ApiClient.php ├── Application.php ├── Build │ ├── BuildContainerImageStep.php │ ├── BuildStepInterface.php │ ├── CompressBuildFilesStep.php │ ├── CopyMustUsePluginStep.php │ ├── CopyProjectFilesStep.php │ ├── CopyUploadsDirectoryStep.php │ ├── DebugBuildStep.php │ ├── DownloadWpCliStep.php │ ├── EnsureIntegrationIsInstalledStep.php │ ├── ExecuteBuildCommandsStep.php │ ├── ExtractAssetFilesStep.php │ └── ModifyWordPressConfigurationStep.php ├── CliConfiguration.php ├── Command │ ├── AbstractCommand.php │ ├── AbstractInvocationCommand.php │ ├── AbstractProjectCommand.php │ ├── Cache │ │ ├── AbstractCacheCommand.php │ │ ├── CacheTunnelCommand.php │ │ ├── CreateCacheCommand.php │ │ ├── DeleteCacheCommand.php │ │ ├── ListCachesCommand.php │ │ └── ModifyCacheCommand.php │ ├── Certificate │ │ ├── AbstractCertificateCommand.php │ │ ├── DeleteCertificateCommand.php │ │ ├── GetCertificateInfoCommand.php │ │ ├── ListCertificatesCommand.php │ │ └── RequestCertificateCommand.php │ ├── Database │ │ ├── AbstractDatabaseCommand.php │ │ ├── AbstractDatabaseServerCommand.php │ │ ├── CreateDatabaseCommand.php │ │ ├── CreateDatabaseServerCommand.php │ │ ├── CreateDatabaseUserCommand.php │ │ ├── DatabaseServerTunnelCommand.php │ │ ├── DeleteDatabaseCommand.php │ │ ├── DeleteDatabaseServerCommand.php │ │ ├── DeleteDatabaseUserCommand.php │ │ ├── ExportDatabaseCommand.php │ │ ├── GetDatabaseServerInfoCommand.php │ │ ├── ImportDatabaseCommand.php │ │ ├── ListDatabaseServersCommand.php │ │ ├── ListDatabaseUsersCommand.php │ │ ├── ListDatabasesCommand.php │ │ ├── LockDatabaseServerCommand.php │ │ ├── ModifyDatabaseServerCommand.php │ │ ├── RotateDatabaseServerPasswordCommand.php │ │ ├── RotateDatabaseUserPasswordCommand.php │ │ └── UnlockDatabaseServerCommand.php │ ├── Dns │ │ ├── AbstractDnsCommand.php │ │ ├── ChangeDnsRecordCommand.php │ │ ├── CreateDnsZoneCommand.php │ │ ├── DeleteDnsRecordCommand.php │ │ ├── DeleteDnsZoneCommand.php │ │ ├── ImportDnsRecordsCommand.php │ │ ├── ListDnsRecordsCommand.php │ │ └── ListDnsZonesCommand.php │ ├── Docker │ │ ├── CreateDockerfileCommand.php │ │ └── DeleteDockerImagesCommand.php │ ├── Email │ │ ├── AbstractEmailIdentityCommand.php │ │ ├── CreateEmailIdentityCommand.php │ │ ├── DeleteEmailIdentityCommand.php │ │ ├── GetEmailIdentityInfoCommand.php │ │ └── ListEmailIdentitiesCommand.php │ ├── Environment │ │ ├── AbstractEnvironmentLogsCommand.php │ │ ├── ChangeEnvironmentDomainCommand.php │ │ ├── ChangeEnvironmentSecretCommand.php │ │ ├── ChangeEnvironmentVariableCommand.php │ │ ├── CreateEnvironmentCommand.php │ │ ├── DeleteEnvironmentCommand.php │ │ ├── DeleteEnvironmentSecretCommand.php │ │ ├── DownloadEnvironmentVariablesCommand.php │ │ ├── GetEnvironmentInfoCommand.php │ │ ├── GetEnvironmentMetricsCommand.php │ │ ├── GetEnvironmentUrlCommand.php │ │ ├── InvalidateEnvironmentCacheCommand.php │ │ ├── ListEnvironmentSecretsCommand.php │ │ ├── ListEnvironmentsCommand.php │ │ ├── QueryEnvironmentLogsCommand.php │ │ ├── UploadEnvironmentVariablesCommand.php │ │ └── WatchEnvironmentLogsCommand.php │ ├── InstallIntegrationCommand.php │ ├── LoginCommand.php │ ├── Network │ │ ├── AddBastionHostCommand.php │ │ ├── AddNatGatewayCommand.php │ │ ├── CreateNetworkCommand.php │ │ ├── DeleteNetworkCommand.php │ │ ├── ListNetworksCommand.php │ │ ├── RemoveBastionHostCommand.php │ │ └── RemoveNatGatewayCommand.php │ ├── Php │ │ ├── PhpInfoCommand.php │ │ └── PhpVersionCommand.php │ ├── Project │ │ ├── AbstractProjectDeploymentCommand.php │ │ ├── BuildProjectCommand.php │ │ ├── ConfigureProjectCommand.php │ │ ├── DeleteProjectCommand.php │ │ ├── DeployProjectCommand.php │ │ ├── GetProjectInfoCommand.php │ │ ├── InitializeProjectCommand.php │ │ ├── ListProjectsCommand.php │ │ ├── RedeployProjectCommand.php │ │ ├── RollbackProjectCommand.php │ │ └── ValidateProjectCommand.php │ ├── Provider │ │ ├── AbstractProviderCommand.php │ │ ├── ConnectProviderCommand.php │ │ ├── DeleteProviderCommand.php │ │ ├── ListProvidersCommand.php │ │ └── UpdateProviderCommand.php │ ├── Team │ │ ├── CreateTeamCommand.php │ │ ├── CurrentTeamCommand.php │ │ ├── ListTeamsCommand.php │ │ └── SelectTeamCommand.php │ ├── Uploads │ │ └── ImportUploadsCommand.php │ └── WpCliCommand.php ├── Console │ ├── ChoiceQuestion.php │ ├── HiddenInputOption.php │ ├── Input.php │ ├── InputDefinition.php │ └── Output.php ├── Database │ ├── Connection.php │ ├── Mysqldump.php │ └── PDO.php ├── Deployment │ ├── DeploymentStepInterface.php │ ├── ProcessAssetsStep.php │ ├── StartAndMonitorDeploymentStep.php │ └── UploadFunctionCodeStep.php ├── Dockerfile.php ├── EventDispatcher │ └── AutowiredEventDispatcher.php ├── EventListener │ ├── CleanupSubscriber.php │ ├── LoadProjectConfigurationSubscriber.php │ └── VersionCheckSubscriber.php ├── Exception │ ├── CommandCancelledException.php │ ├── Executable │ │ ├── ExecutableNotDetectedException.php │ │ ├── SshPortInUseException.php │ │ └── WpCliException.php │ ├── InvalidInputException.php │ ├── NonInteractiveRequiredArgumentException.php │ └── NonInteractiveRequiredOptionException.php ├── Executable │ ├── AbstractExecutable.php │ ├── ComposerExecutable.php │ ├── DockerExecutable.php │ ├── ExecutableInterface.php │ ├── SshExecutable.php │ └── WpCliExecutable.php ├── FileUploader.php ├── GitHubClient.php ├── Process │ └── Process.php ├── Project │ ├── Configuration │ │ ├── CacheConfigurationChange.php │ │ ├── ConfigurationChangeInterface.php │ │ ├── DomainConfigurationChange.php │ │ ├── ImageDeploymentConfigurationChange.php │ │ ├── ProjectConfiguration.php │ │ └── WordPress │ │ │ ├── AbstractWordPressConfigurationChange.php │ │ │ ├── BeaverBuilderConfigurationChange.php │ │ │ ├── CloudflareConfigurationChange.php │ │ │ ├── ElementorConfigurationChange.php │ │ │ ├── OxygenConfigurationChange.php │ │ │ ├── WooCommerceConfigurationChange.php │ │ │ └── WordPressConfigurationChangeInterface.php │ └── Type │ │ ├── AbstractProjectType.php │ │ ├── AbstractWordPressProjectType.php │ │ ├── BedrockProjectType.php │ │ ├── InstallableProjectTypeInterface.php │ │ ├── ProjectTypeInterface.php │ │ ├── RadicleProjectType.php │ │ └── WordPressProjectType.php └── Support │ └── Arr.php ├── stubs ├── Dockerfile ├── activate-ymir-plugin.php └── ymir-config.php └── ymir /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.51.1" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Carl Alexander 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 | # Ymir CLI 8 | 9 | The [Ymir][1] command-line tool. 10 | 11 | ## Requirements 12 | 13 | * PHP >= 7.2.5 14 | 15 | ## Installation 16 | 17 | Install the Ymir CLI in your project using composer: 18 | 19 | ``` 20 | $ composer require ymirapp/cli 21 | ``` 22 | 23 | Or globally: 24 | 25 | ``` 26 | $ composer global require ymirapp/cli 27 | ``` 28 | 29 | ## Contributing 30 | 31 | Install dependencies using composer: 32 | 33 | ```console 34 | $ composer install 35 | ``` 36 | 37 | ## Links 38 | 39 | * [Documentation][2] 40 | 41 | [1]: https://ymirapp.com 42 | [2]: https://docs.ymirapp.com 43 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ymirapp/cli", 3 | "description": "Ymir command-line tool", 4 | "type": "project", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Carl Alexander", 9 | "email": "support@ymirapp.com", 10 | "homepage": "https://ymirapp.com" 11 | } 12 | ], 13 | "bin": [ 14 | "ymir" 15 | ], 16 | "require": { 17 | "php": ">=7.2.5", 18 | "ext-json": "*", 19 | "ext-pdo": "*", 20 | "ext-zip": "*", 21 | "ext-zlib": "*", 22 | "guzzlehttp/guzzle": "^7.0", 23 | "ifsnop/mysqldump-php": "^2.12", 24 | "illuminate/collections": "^8.0|^9.0|^10.0|^11.0", 25 | "league/flysystem": "^2.1.1|^3.0", 26 | "league/flysystem-ftp": "^2.0|^3.0", 27 | "league/flysystem-sftp-v3": "^2.0|^3.0", 28 | "nesbot/carbon": "^2.40", 29 | "symfony/config": "^5.4|^6.0", 30 | "symfony/console": "^5.4|^6.0", 31 | "symfony/dependency-injection": "^5.4|^6.0", 32 | "symfony/event-dispatcher": "^5.4|^6.0", 33 | "symfony/filesystem": "^5.4|^6.0", 34 | "symfony/finder": "^5.4.3|^6.0.3", 35 | "symfony/polyfill-php80": "^1.27", 36 | "symfony/process": "^5.4|^6.0", 37 | "symfony/yaml": "^5.4|^6.0", 38 | "ymirapp/ymir-sdk-php": "^1.3.0" 39 | }, 40 | "require-dev": { 41 | "fakerphp/faker": "^1.17", 42 | "friendsofphp/php-cs-fixer": "^3.0", 43 | "php-parallel-lint/php-parallel-lint": "^1.1", 44 | "phpro/grumphp": "^1.0", 45 | "phpstan/phpstan": "^1.11.0", 46 | "phpunit/phpunit": "^9.3", 47 | "sebastian/phpcpd": "^6.0.3" 48 | }, 49 | "config": { 50 | "allow-plugins": { 51 | "phpro/grumphp": true 52 | }, 53 | "optimize-autoloader": true, 54 | "preferred-install": "dist", 55 | "sort-packages": true 56 | }, 57 | "autoload": { 58 | "psr-4": { 59 | "Ymir\\Cli\\": "src" 60 | } 61 | }, 62 | "autoload-dev": { 63 | "psr-4": { 64 | "Ymir\\Cli\\Tests\\": "tests" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", 3 | "bootstrap-sha": "fe728980b0bd4cd46b5da158c05c193e4a7d0ab9", 4 | "plugins": [ "sentence-case" ], 5 | "skip-github-release": true, 6 | "packages": { 7 | ".": { 8 | "release-type": "php", 9 | "pull-request-title-pattern": "chore: release ${version}", 10 | "changelog-sections": [ 11 | { "type" :"feat", "section" :"Features", "hidden": false }, 12 | { "type":"fix", "section": "Bug Fixes", "hidden": false }, 13 | { "type":"chore", "section": "Miscellaneous" ,"hidden":true } 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Application.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli; 15 | 16 | use Symfony\Component\Console\Application as BaseApplication; 17 | use Symfony\Component\Console\Input\InputDefinition; 18 | use Symfony\Component\Console\Input\InputOption; 19 | use Symfony\Component\Console\Output\OutputInterface; 20 | use Ymir\Cli\Exception\CommandCancelledException; 21 | 22 | class Application extends BaseApplication 23 | { 24 | /** 25 | * Constructor. 26 | */ 27 | public function __construct(iterable $commands, string $version) 28 | { 29 | parent::__construct('Ymir', $version); 30 | 31 | foreach ($commands as $command) { 32 | $this->add($command); 33 | } 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function renderThrowable(\Throwable $exception, OutputInterface $output): void 40 | { 41 | if ($exception instanceof CommandCancelledException) { 42 | return; 43 | } 44 | 45 | parent::renderThrowable($exception, $output); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | protected function getDefaultInputDefinition(): InputDefinition 52 | { 53 | $definition = parent::getDefaultInputDefinition(); 54 | 55 | $definition->addOption(new InputOption('ymir-file', null, InputOption::VALUE_OPTIONAL, 'Path to Ymir project configuration file', 'ymir.yml')); 56 | 57 | return $definition; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Build/BuildContainerImageStep.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Build; 15 | 16 | use Symfony\Component\Filesystem\Filesystem; 17 | use Symfony\Component\Process\Exception\RuntimeException; 18 | use Ymir\Cli\Executable\DockerExecutable; 19 | use Ymir\Cli\Project\Configuration\ProjectConfiguration; 20 | 21 | class BuildContainerImageStep implements BuildStepInterface 22 | { 23 | /** 24 | * The build directory where the project files are copied to. 25 | * 26 | * @var string 27 | */ 28 | private $buildDirectory; 29 | 30 | /** 31 | * The Docker executable. 32 | * 33 | * @var DockerExecutable 34 | */ 35 | private $dockerExecutable; 36 | 37 | /** 38 | * The file system. 39 | * 40 | * @var Filesystem 41 | */ 42 | private $filesystem; 43 | 44 | /** 45 | * Constructor. 46 | */ 47 | public function __construct(string $buildDirectory, DockerExecutable $dockerExecutable, Filesystem $filesystem) 48 | { 49 | $this->buildDirectory = rtrim($buildDirectory, '/'); 50 | $this->dockerExecutable = $dockerExecutable; 51 | $this->filesystem = $filesystem; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function getDescription(): string 58 | { 59 | return 'Building container image'; 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function perform(string $environment, ProjectConfiguration $projectConfiguration) 66 | { 67 | $dockerfileName = 'Dockerfile'; 68 | 69 | if ($this->filesystem->exists($this->buildDirectory.'/'.$environment.'.'.$dockerfileName)) { 70 | $dockerfileName = $environment.'.'.$dockerfileName; 71 | } 72 | 73 | if (!$this->filesystem->exists($this->buildDirectory.'/'.$dockerfileName)) { 74 | throw new RuntimeException('Unable to find a "Dockerfile" to build the container image'); 75 | } 76 | 77 | $this->dockerExecutable->build($dockerfileName, sprintf('%s:%s', $projectConfiguration->getProjectName(), $environment), $this->buildDirectory); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Build/BuildStepInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Build; 15 | 16 | use Ymir\Cli\Project\Configuration\ProjectConfiguration; 17 | 18 | interface BuildStepInterface 19 | { 20 | /** 21 | * Get the description of the build step. 22 | */ 23 | public function getDescription(): string; 24 | 25 | /** 26 | * Perform the build step. 27 | */ 28 | public function perform(string $environment, ProjectConfiguration $projectConfiguration); 29 | } 30 | -------------------------------------------------------------------------------- /src/Build/CopyMustUsePluginStep.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Build; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Symfony\Component\Filesystem\Filesystem; 18 | use Ymir\Cli\Project\Configuration\ProjectConfiguration; 19 | use Ymir\Cli\Project\Type\AbstractWordPressProjectType; 20 | 21 | class CopyMustUsePluginStep implements BuildStepInterface 22 | { 23 | /** 24 | * The build directory where the project files are copied to. 25 | * 26 | * @var string 27 | */ 28 | private $buildDirectory; 29 | 30 | /** 31 | * The file system. 32 | * 33 | * @var Filesystem 34 | */ 35 | private $filesystem; 36 | 37 | /** 38 | * The directory where the stub files are. 39 | * 40 | * @var string 41 | */ 42 | private $stubDirectory; 43 | 44 | /** 45 | * Constructor. 46 | */ 47 | public function __construct(string $buildDirectory, Filesystem $filesystem, string $stubDirectory) 48 | { 49 | $this->buildDirectory = rtrim($buildDirectory, '/'); 50 | $this->filesystem = $filesystem; 51 | $this->stubDirectory = rtrim($stubDirectory, '/'); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function getDescription(): string 58 | { 59 | return 'Copying Ymir must-use plugin'; 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function perform(string $environment, ProjectConfiguration $projectConfiguration) 66 | { 67 | $mupluginStub = 'activate-ymir-plugin.php'; 68 | $mupluginStubPath = $this->stubDirectory.'/'.$mupluginStub; 69 | 70 | if (!$this->filesystem->exists($mupluginStubPath)) { 71 | throw new RuntimeException(sprintf('Cannot find "%s" stub file', $mupluginStub)); 72 | } 73 | 74 | $projectType = $projectConfiguration->getProjectType(); 75 | 76 | if (!$projectType instanceof AbstractWordPressProjectType) { 77 | throw new RuntimeException('You can only use this build step with WordPress projects'); 78 | } 79 | 80 | $mupluginsDirectory = $projectType->getMustUsePluginsDirectoryPath($this->buildDirectory); 81 | 82 | if (!$this->filesystem->exists($mupluginsDirectory)) { 83 | $this->filesystem->mkdir($mupluginsDirectory); 84 | } 85 | 86 | $this->filesystem->copy($mupluginStubPath, $mupluginsDirectory.'/'.$mupluginStub); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Build/CopyUploadsDirectoryStep.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Build; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Symfony\Component\Filesystem\Filesystem; 18 | use Symfony\Component\Finder\Finder; 19 | use Symfony\Component\Finder\SplFileInfo; 20 | use Ymir\Cli\Project\Configuration\ProjectConfiguration; 21 | use Ymir\Cli\Project\Type\AbstractWordPressProjectType; 22 | 23 | class CopyUploadsDirectoryStep implements BuildStepInterface 24 | { 25 | /** 26 | * The file system. 27 | * 28 | * @var Filesystem 29 | */ 30 | private $filesystem; 31 | 32 | /** 33 | * The project directory where the uploads files are copied from. 34 | * 35 | * @var string 36 | */ 37 | private $projectDirectory; 38 | 39 | /** 40 | * The build "uploads" directory where the files are copied to. 41 | * 42 | * @var string 43 | */ 44 | private $uploadsDirectory; 45 | 46 | /** 47 | * Constructor. 48 | */ 49 | public function __construct(Filesystem $filesystem, string $projectDirectory, string $uploadsDirectory) 50 | { 51 | $this->filesystem = $filesystem; 52 | $this->projectDirectory = rtrim($projectDirectory, '/'); 53 | $this->uploadsDirectory = $uploadsDirectory; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function getDescription(): string 60 | { 61 | return 'Copying "uploads" directory'; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function perform(string $environment, ProjectConfiguration $projectConfiguration) 68 | { 69 | $projectType = $projectConfiguration->getProjectType(); 70 | 71 | if (!$projectType instanceof AbstractWordPressProjectType) { 72 | throw new RuntimeException('You can only use this build step with WordPress projects'); 73 | } 74 | 75 | $files = Finder::create()->files()->in($projectType->getUploadsDirectoryPath($this->projectDirectory)); 76 | 77 | foreach ($files as $file) { 78 | $this->copyFile($file); 79 | } 80 | } 81 | 82 | /** 83 | * Copy an individual file or directory. 84 | */ 85 | private function copyFile(SplFileInfo $file) 86 | { 87 | if ($file->isDir()) { 88 | $this->filesystem->mkdir($this->uploadsDirectory.'/'.$file->getRelativePathname()); 89 | } elseif ($file->isFile() && is_string($file->getRealPath())) { 90 | $this->filesystem->copy($file->getRealPath(), $this->uploadsDirectory.'/'.$file->getRelativePathname()); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Build/DebugBuildStep.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Build; 15 | 16 | use Ymir\Cli\Project\Configuration\ProjectConfiguration; 17 | 18 | class DebugBuildStep implements BuildStepInterface 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function getDescription(): string 24 | { 25 | return 'Debug mode: Press Enter to continue.'; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function perform(string $environment, ProjectConfiguration $projectConfiguration) 32 | { 33 | fgets(STDIN); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Build/DownloadWpCliStep.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Build; 15 | 16 | use Symfony\Component\Filesystem\Filesystem; 17 | use Ymir\Cli\Project\Configuration\ProjectConfiguration; 18 | 19 | class DownloadWpCliStep implements BuildStepInterface 20 | { 21 | /** 22 | * The WP-CLI version to download. 23 | */ 24 | private const VERSION = '2.12.0'; 25 | 26 | /** 27 | * The path to the WP-CLI bin directory. 28 | * 29 | * @var string 30 | */ 31 | private $binDirectory; 32 | 33 | /** 34 | * The file system. 35 | * 36 | * @var Filesystem 37 | */ 38 | private $filesystem; 39 | 40 | /** 41 | * Constructor. 42 | */ 43 | public function __construct(string $buildDirectory, Filesystem $filesystem) 44 | { 45 | $this->binDirectory = rtrim($buildDirectory, '/').'/bin'; 46 | $this->filesystem = $filesystem; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function getDescription(): string 53 | { 54 | return 'Downloading WP-CLI'; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function perform(string $environment, ProjectConfiguration $projectConfiguration) 61 | { 62 | $wpCliPath = $this->binDirectory.'/wp'; 63 | 64 | if (!$this->filesystem->exists($this->binDirectory)) { 65 | $this->filesystem->mkdir($this->binDirectory, 0755); 66 | } 67 | 68 | $this->filesystem->copy(sprintf('https://github.com/wp-cli/wp-cli/releases/download/v%1$s/wp-cli-%1$s.phar', self::VERSION), $wpCliPath, true); 69 | $this->filesystem->chmod($wpCliPath, 0755); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Build/EnsureIntegrationIsInstalledStep.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Build; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Ymir\Cli\Project\Configuration\ProjectConfiguration; 18 | 19 | class EnsureIntegrationIsInstalledStep implements BuildStepInterface 20 | { 21 | /** 22 | * The build directory where the project files are copied to. 23 | * 24 | * @var string 25 | */ 26 | private $buildDirectory; 27 | 28 | /** 29 | * Constructor. 30 | */ 31 | public function __construct(string $buildDirectory) 32 | { 33 | $this->buildDirectory = rtrim($buildDirectory, '/'); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function getDescription(): string 40 | { 41 | return 'Ensuring Ymir integration is installed'; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function perform(string $environment, ProjectConfiguration $projectConfiguration) 48 | { 49 | if (!$projectConfiguration->getProjectType()->isIntegrationInstalled($this->buildDirectory)) { 50 | throw new RuntimeException('Ymir integration is not installed in the build directory'); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Build/ExecuteBuildCommandsStep.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Build; 15 | 16 | use Ymir\Cli\Process\Process; 17 | use Ymir\Cli\Project\Configuration\ProjectConfiguration; 18 | use Ymir\Cli\Support\Arr; 19 | 20 | class ExecuteBuildCommandsStep implements BuildStepInterface 21 | { 22 | /** 23 | * The build directory where the project files are copied to. 24 | * 25 | * @var string 26 | */ 27 | private $buildDirectory; 28 | 29 | /** 30 | * Constructor. 31 | */ 32 | public function __construct(string $buildDirectory) 33 | { 34 | $this->buildDirectory = rtrim($buildDirectory, '/'); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function getDescription(): string 41 | { 42 | return 'Executing build commands'; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function perform(string $environment, ProjectConfiguration $projectConfiguration) 49 | { 50 | $environment = $projectConfiguration->getEnvironment($environment); 51 | 52 | if (empty($environment['build'])) { 53 | return; 54 | } 55 | 56 | $commands = []; 57 | 58 | if (Arr::has($environment, 'build.commands')) { 59 | $commands = (array) Arr::get($environment, 'build.commands'); 60 | } elseif (!Arr::has($environment, 'build.include')) { 61 | $commands = (array) $environment['build']; 62 | } 63 | 64 | foreach ($commands as $command) { 65 | Process::runShellCommandline($command, $this->buildDirectory, null, null, null); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Build/ExtractAssetFilesStep.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Build; 15 | 16 | use Symfony\Component\Filesystem\Filesystem; 17 | use Symfony\Component\Finder\SplFileInfo; 18 | use Ymir\Cli\Project\Configuration\ProjectConfiguration; 19 | 20 | class ExtractAssetFilesStep implements BuildStepInterface 21 | { 22 | /** 23 | * The file system. 24 | * 25 | * @var Filesystem 26 | */ 27 | private $filesystem; 28 | 29 | /** 30 | * The build directory where the asset files are extracted from. 31 | * 32 | * @var string 33 | */ 34 | private $fromDirectory; 35 | 36 | /** 37 | * The assets directory where the asset files are copied to. 38 | * 39 | * @var string 40 | */ 41 | private $toDirectory; 42 | 43 | /** 44 | * Constructor. 45 | */ 46 | public function __construct(string $assetsDirectory, string $buildDirectory, Filesystem $filesystem) 47 | { 48 | $this->toDirectory = rtrim($assetsDirectory, '/'); 49 | $this->fromDirectory = rtrim($buildDirectory, '/'); 50 | $this->filesystem = $filesystem; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function getDescription(): string 57 | { 58 | return 'Extracting asset files'; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function perform(string $environment, ProjectConfiguration $projectConfiguration) 65 | { 66 | if ($this->filesystem->exists($this->toDirectory)) { 67 | $this->filesystem->remove($this->toDirectory); 68 | } 69 | 70 | $this->filesystem->mkdir($this->toDirectory, 0755); 71 | 72 | foreach ($projectConfiguration->getProjectType()->getAssetFiles($this->fromDirectory) as $file) { 73 | $this->moveAssetFile($file); 74 | } 75 | } 76 | 77 | /** 78 | * Move the asset file to the assets directory. 79 | */ 80 | private function moveAssetFile(SplFileInfo $file) 81 | { 82 | if (!$file->isFile() || !is_string($file->getRealPath())) { 83 | return; 84 | } 85 | 86 | $this->filesystem->copy($file->getRealPath(), $this->toDirectory.'/'.$file->getRelativePathname()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Command/AbstractInvocationCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Ymir\Cli\Support\Arr; 18 | 19 | /** 20 | * Base command for invoking a project function. 21 | */ 22 | abstract class AbstractInvocationCommand extends AbstractProjectCommand 23 | { 24 | /** 25 | * Invokes the environment console function with the given PHP command and returns the output. 26 | */ 27 | protected function invokePhpCommand(string $command, string $environment, ?int $timeout = null): array 28 | { 29 | return $this->invokeEnvironmentFunction($environment, [ 30 | 'php' => $command, 31 | ], $timeout); 32 | } 33 | 34 | /** 35 | * Invokes the environment console function with the given WP-CLI command and returns the output. 36 | */ 37 | protected function invokeWpCliCommand(string $command, string $environment, ?int $timeout = null): array 38 | { 39 | if (str_starts_with($command, 'wp ')) { 40 | $command = substr($command, 3); 41 | } 42 | 43 | return $this->invokeEnvironmentFunction($environment, [ 44 | 'php' => sprintf('bin/wp %s', $command), 45 | ], $timeout); 46 | } 47 | 48 | /** 49 | * Invokes the given environment console function with the given payload and returns the output. 50 | */ 51 | private function invokeEnvironmentFunction(string $environment, array $payload, ?int $timeout = null): array 52 | { 53 | $invocationId = $this->apiClient->createInvocation($this->projectConfiguration->getProjectId(), $environment, $payload)->get('id'); 54 | 55 | if (!is_int($invocationId)) { 56 | throw new \RuntimeException('Unable to create command invocation'); 57 | } 58 | 59 | if (0 === $timeout) { 60 | return []; 61 | } elseif (!is_int($timeout)) { 62 | $timeout = (int) Arr::get($this->projectConfiguration->getEnvironment($environment), 'console.timeout', 60); 63 | } 64 | 65 | $invocation = $this->wait(function () use ($invocationId) { 66 | $invocation = $this->apiClient->getInvocation($invocationId); 67 | 68 | return !in_array($invocation->get('status'), ['pending', 'running']) ? $invocation->all() : []; 69 | }, $timeout); 70 | 71 | if (empty($invocation['status']) || 'failed' === $invocation['status']) { 72 | throw new RuntimeException('Running the command failed'); 73 | } elseif (!Arr::has($invocation, ['result.exitCode', 'result.output'])) { 74 | throw new RuntimeException('Unable to get the result of the command from the Ymir API'); 75 | } 76 | 77 | return $invocation['result']; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Command/AbstractProjectCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command; 15 | 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Base command for interacting with a project. 21 | */ 22 | abstract class AbstractProjectCommand extends AbstractCommand 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected function execute(InputInterface $input, OutputInterface $output) 28 | { 29 | $this->projectConfiguration->validate(); 30 | 31 | return parent::execute($input, $output); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Command/Cache/AbstractCacheCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Cache; 15 | 16 | use Illuminate\Support\Collection; 17 | use Symfony\Component\Console\Exception\RuntimeException; 18 | use Ymir\Cli\Command\AbstractCommand; 19 | use Ymir\Cli\Exception\InvalidInputException; 20 | 21 | abstract class AbstractCacheCommand extends AbstractCommand 22 | { 23 | /** 24 | * Determine the cache that the command is interacting with. 25 | */ 26 | protected function determineCache(string $question): array 27 | { 28 | $caches = $this->apiClient->getCaches($this->cliConfiguration->getActiveTeamId()); 29 | $cacheIdOrName = $this->input->getStringArgument('cache'); 30 | 31 | if ($caches->isEmpty()) { 32 | throw new RuntimeException(sprintf('The currently active team has no cache clusters. You can create one with the "%s" command.', CreateCacheCommand::NAME)); 33 | } elseif (empty($cacheIdOrName)) { 34 | $cacheIdOrName = $this->output->choiceWithResourceDetails($question, $caches); 35 | } 36 | 37 | $cache = $caches->firstWhere('id', $cacheIdOrName) ?? $caches->firstWhere('name', $cacheIdOrName); 38 | 39 | if (1 < $caches->where('name', $cacheIdOrName)->count()) { 40 | throw new RuntimeException(sprintf('Unable to select a cache cluster because more than one cache cluster has the name "%s"', $cacheIdOrName)); 41 | } elseif (!is_array($cache) || empty($cache['id'])) { 42 | throw new InvalidInputException(sprintf('Unable to find a cache cluster with "%s" as the ID or name', $cacheIdOrName)); 43 | } 44 | 45 | return $cache; 46 | } 47 | 48 | /** 49 | * Get the descriptions of the cache types for a given provider and engine. 50 | */ 51 | protected function getCacheTypeDescriptions(int $providerId, string $engine): Collection 52 | { 53 | return $this->apiClient->getCacheTypes($providerId)->map(function (array $details) use ($engine) { 54 | return sprintf('%s vCPU, %sGiB RAM (~$%s/month)', $details['cpu'], $details['ram'], $details['price'][$engine]); 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Command/Cache/DeleteCacheCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Cache; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Ymir\Cli\Command\Network\RemoveNatGatewayCommand; 18 | 19 | class DeleteCacheCommand extends AbstractCacheCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'cache:delete'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Delete a cache cluster') 36 | ->addArgument('cache', InputArgument::OPTIONAL, 'The ID or name of the cache cluster to delete'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function perform() 43 | { 44 | $cache = $this->determineCache('Which cache cluster would you like to delete'); 45 | 46 | if (!$this->output->confirm('Are you sure you want to delete this cache cluster?', false)) { 47 | return; 48 | } 49 | 50 | $this->apiClient->deleteCache($cache['id']); 51 | 52 | $this->output->infoWithDelayWarning('Cache cluster deleted'); 53 | $this->output->newLine(); 54 | $this->output->note(sprintf('If you have no other resources using the private subnet, you should remove the network\'s NAT gateway using the "%s" command', RemoveNatGatewayCommand::NAME)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Command/Cache/ListCachesCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Cache; 15 | 16 | use Ymir\Cli\Command\AbstractCommand; 17 | 18 | class ListCachesCommand extends AbstractCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'cache:list'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('List all the cache clusters that the current team has access to'); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | protected function perform() 41 | { 42 | $this->output->table( 43 | ['Id', 'Name', 'Provider', 'Network', 'Region', 'Status', 'Engine', 'Type'], 44 | $this->apiClient->getCaches($this->cliConfiguration->getActiveTeamId())->map(function (array $cache) { 45 | return [ 46 | $cache['id'], 47 | $cache['name'], 48 | $cache['network']['provider']['name'], 49 | $cache['network']['name'], 50 | $cache['region'], 51 | $this->output->formatStatus($cache['status']), 52 | $cache['engine'], 53 | $cache['type'], 54 | ]; 55 | })->all() 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Command/Cache/ModifyCacheCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Cache; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputOption; 18 | use Ymir\Cli\Exception\InvalidInputException; 19 | 20 | class ModifyCacheCommand extends AbstractCacheCommand 21 | { 22 | /** 23 | * The name of the command. 24 | * 25 | * @var string 26 | */ 27 | public const NAME = 'cache:modify'; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function configure() 33 | { 34 | $this 35 | ->setName(self::NAME) 36 | ->setDescription('Modify a cache cluster') 37 | ->addArgument('cache', InputArgument::OPTIONAL, 'The ID or name of the cache cluster to modify') 38 | ->addOption('type', null, InputOption::VALUE_REQUIRED, 'The cache cluster type'); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function perform() 45 | { 46 | $cache = $this->determineCache('Which cache cluster would you like to modify'); 47 | $type = $this->input->getStringOption('type', true); 48 | $types = $this->getCacheTypeDescriptions($cache['provider']['id'], $cache['engine']); 49 | 50 | if (null === $type) { 51 | $type = $this->output->choice(sprintf('What should the cache cluster type be changed to? (Currently: %s)', $cache['type']), $types); 52 | } elseif (!$types->has($type)) { 53 | throw new InvalidInputException(sprintf('The type "%s" isn\'t a valid cache cluster type', $type)); 54 | } 55 | 56 | if (!$this->output->confirm('Modifying the cache cluster will cause your cache cluster to become unavailable for a few minutes. Do you want to proceed?', false)) { 57 | exit; 58 | } 59 | 60 | $this->apiClient->updateCache((int) $cache['id'], $type); 61 | 62 | $this->output->infoWithDelayWarning('Cache cluster modified'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Command/Certificate/AbstractCertificateCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Certificate; 15 | 16 | use Ymir\Cli\Command\AbstractCommand; 17 | use Ymir\Cli\Exception\InvalidInputException; 18 | 19 | abstract class AbstractCertificateCommand extends AbstractCommand 20 | { 21 | /** 22 | * Get the "certificate" argument. 23 | */ 24 | protected function getCertificateArgument(): int 25 | { 26 | $certificateId = $this->input->getStringArgument('certificate'); 27 | 28 | if (!is_numeric($certificateId)) { 29 | throw new InvalidInputException('The "certificate" argument must be the ID of the SSL certificate'); 30 | } 31 | 32 | return (int) $certificateId; 33 | } 34 | 35 | /** 36 | * Parse the certificate details for the certificate validation DNS records. 37 | */ 38 | protected function parseCertificateValidationRecords($certificate): array 39 | { 40 | return !empty($certificate['domains']) 41 | ? collect($certificate['domains']) 42 | ->where('managed', false) 43 | ->pluck('validation_record') 44 | ->filter() 45 | ->unique(function (array $validationRecord) { 46 | return $validationRecord['name'].$validationRecord['value']; 47 | }) 48 | ->map(function (array $validationRecord) { 49 | return ['CNAME', $validationRecord['name'], $validationRecord['value']]; 50 | }) 51 | ->all() 52 | : []; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Command/Certificate/DeleteCertificateCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Certificate; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | 18 | class DeleteCertificateCommand extends AbstractCertificateCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'certificate:delete'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('Delete a SSL certificate') 35 | ->addArgument('certificate', InputArgument::REQUIRED, 'The ID of the SSL certificate to delete'); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function perform() 42 | { 43 | $certificateId = $this->getCertificateArgument(); 44 | 45 | if (!$this->output->confirm('Are you sure you want to delete this SSL certificate?', false)) { 46 | return; 47 | } 48 | 49 | $this->apiClient->deleteCertificate($certificateId); 50 | 51 | $this->output->info('SSL certificate deleted'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Command/Certificate/GetCertificateInfoCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Certificate; 15 | 16 | use Symfony\Component\Console\Helper\TableSeparator; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | 19 | class GetCertificateInfoCommand extends AbstractCertificateCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'certificate:info'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Get information on an SSL certificate') 36 | ->addArgument('certificate', InputArgument::REQUIRED, 'The ID of the SSL certificate to fetch the information of'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function perform() 43 | { 44 | $certificate = $this->apiClient->getCertificate($this->getCertificateArgument()); 45 | 46 | $this->output->horizontalTable( 47 | ['Domains', new TableSeparator(), 'Provider', 'Region', new TableSeparator(), 'Status', 'In Use'], 48 | [[implode(PHP_EOL, $this->getDomainNames($certificate['domains'])), new TableSeparator(), $certificate['provider']['name'], $certificate['region'], new TableSeparator(), $certificate['status'], $this->output->formatBoolean($certificate['in_use'])]] 49 | ); 50 | 51 | $validationRecords = $this->parseCertificateValidationRecords($certificate); 52 | 53 | if (!empty($validationRecords)) { 54 | $this->output->newLine(); 55 | $this->output->important('The following DNS record(s) need to exist on your DNS server at all times:'); 56 | $this->output->newLine(); 57 | $this->output->table( 58 | ['Type', 'Name', 'Value'], 59 | $validationRecords 60 | ); 61 | $this->output->warning('The SSL certificate won\'t be issued or renewed if these DNS record(s) don\'t exist.'); 62 | } 63 | } 64 | 65 | /** 66 | * Get the formatted domain names for a certificate. 67 | */ 68 | private function getDomainNames(array $certificateDomains): array 69 | { 70 | return collect($certificateDomains)->map(function (array $domain) { 71 | return sprintf('%s (%s)', $domain['domain_name'], $domain['validated'] ? 'validated' : 'not validated'); 72 | })->all(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Command/Certificate/ListCertificatesCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Certificate; 15 | 16 | class ListCertificatesCommand extends AbstractCertificateCommand 17 | { 18 | /** 19 | * The name of the command. 20 | * 21 | * @var string 22 | */ 23 | public const NAME = 'certificate:list'; 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | protected function configure() 29 | { 30 | $this 31 | ->setName(self::NAME) 32 | ->setDescription('List the SSL certificates that belong to the currently active team'); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | protected function perform() 39 | { 40 | $certificates = $this->apiClient->getCertificates($this->cliConfiguration->getActiveTeamId()); 41 | 42 | $this->output->table( 43 | ['Id', 'Provider', 'Region', 'Domains', 'Status', 'In Use'], 44 | $certificates->map(function (array $certificate) { 45 | return [$certificate['id'], $certificate['provider']['name'], $certificate['region'], $this->getDomainsList($certificate), $certificate['status'], $this->output->formatBoolean($certificate['in_use'])]; 46 | })->all() 47 | ); 48 | } 49 | 50 | /** 51 | * Get the list of domains from the certificate. 52 | */ 53 | private function getDomainsList($certificate): string 54 | { 55 | return !empty($certificate['domains']) ? implode(PHP_EOL, collect($certificate['domains'])->pluck('domain_name')->all()) : ''; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Command/Database/AbstractDatabaseServerCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Database; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Ymir\Cli\Command\AbstractCommand; 18 | use Ymir\Cli\Exception\InvalidInputException; 19 | 20 | abstract class AbstractDatabaseServerCommand extends AbstractCommand 21 | { 22 | /** 23 | * The name of the Aurora database type. 24 | * 25 | * @var string 26 | */ 27 | protected const AURORA_DATABASE_TYPE = 'aurora-mysql'; 28 | 29 | /** 30 | * Determine the database server that the command is interacting with. 31 | */ 32 | protected function determineDatabaseServer(string $question): array 33 | { 34 | $databases = $this->apiClient->getDatabaseServers($this->cliConfiguration->getActiveTeamId()); 35 | $databaseIdOrName = $this->input->getStringArgument('server'); 36 | 37 | if ($databases->isEmpty()) { 38 | throw new RuntimeException(sprintf('The currently active team has no database servers. You can create one with the "%s" command.', CreateDatabaseServerCommand::NAME)); 39 | } elseif (empty($databaseIdOrName)) { 40 | $databaseIdOrName = $this->output->choiceWithResourceDetails($question, $databases); 41 | } 42 | 43 | $database = $databases->firstWhere('id', $databaseIdOrName) ?? $databases->firstWhere('name', $databaseIdOrName); 44 | 45 | if (1 < $databases->where('name', $databaseIdOrName)->count()) { 46 | throw new RuntimeException(sprintf('Unable to select a database server because more than one database server has the name "%s"', $databaseIdOrName)); 47 | } elseif (empty($database['id'])) { 48 | throw new InvalidInputException(sprintf('Unable to find a database server with "%s" as the ID or name', $databaseIdOrName)); 49 | } 50 | 51 | return $database; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Command/Database/CreateDatabaseCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Database; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputOption; 19 | 20 | class CreateDatabaseCommand extends AbstractDatabaseCommand 21 | { 22 | /** 23 | * The name of the command. 24 | * 25 | * @var string 26 | */ 27 | public const NAME = 'database:create'; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function configure() 33 | { 34 | $this 35 | ->setName(self::NAME) 36 | ->setDescription('Create a new database on a public database server') 37 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the new database') 38 | ->addOption('server', null, InputOption::VALUE_REQUIRED, 'The ID or name of the database server where the database will be created'); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function perform() 45 | { 46 | $databaseServer = $this->determineDatabaseServer('On which database server would you like to create the new database?'); 47 | 48 | if (!$databaseServer['publicly_accessible']) { 49 | throw new RuntimeException('Database on private database servers need to be manually created.'); 50 | } 51 | 52 | $name = $this->input->getStringArgument('name'); 53 | 54 | if (empty($name)) { 55 | $name = $this->output->ask('What is the name of the database'); 56 | } 57 | 58 | $this->apiClient->createDatabase($databaseServer['id'], $name); 59 | 60 | $this->output->info('Database created'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Command/Database/DeleteDatabaseCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Database; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputOption; 19 | 20 | class DeleteDatabaseCommand extends AbstractDatabaseCommand 21 | { 22 | /** 23 | * The name of the command. 24 | * 25 | * @var string 26 | */ 27 | public const NAME = 'database:delete'; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function configure() 33 | { 34 | $this 35 | ->setName(self::NAME) 36 | ->setDescription('Delete a database on a public database server') 37 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the database to delete') 38 | ->addOption('server', null, InputOption::VALUE_REQUIRED, 'The ID or name of the database server where the database will be deleted'); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function perform() 45 | { 46 | $databaseServer = $this->determineDatabaseServer('On which database server would you like to delete a database?'); 47 | 48 | if (!$databaseServer['publicly_accessible']) { 49 | throw new RuntimeException('Database on private database servers need to be manually deleted.'); 50 | } 51 | 52 | $name = $this->input->getStringArgument('name'); 53 | 54 | if (empty($name)) { 55 | $name = (string) $this->output->choice('Which database would you like to delete', $this->apiClient->getDatabases($databaseServer['id'])->filter(function (string $name) { 56 | return !in_array($name, ['information_schema', 'innodb', 'mysql', 'performance_schema', 'sys']); 57 | })->values()); 58 | } 59 | 60 | if (!$this->output->confirm(sprintf('Are you sure you want to delete the "%s" database?', $name), false)) { 61 | return; 62 | } 63 | 64 | $this->apiClient->deleteDatabase($databaseServer['id'], $name); 65 | 66 | $this->output->info('Database deleted'); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Command/Database/DeleteDatabaseServerCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Database; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Ymir\Cli\Command\Network\RemoveNatGatewayCommand; 18 | 19 | class DeleteDatabaseServerCommand extends AbstractDatabaseServerCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'database:server:delete'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Delete a database server') 36 | ->addArgument('server', InputArgument::OPTIONAL, 'The ID or name of the database server to delete'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function perform() 43 | { 44 | $databaseServer = $this->determineDatabaseServer('Which database server would you like to delete'); 45 | 46 | if (!$this->output->confirm(sprintf('Are you sure you want to delete the "%s" database server?', $databaseServer['name']), false)) { 47 | return; 48 | } 49 | 50 | $this->apiClient->deleteDatabaseServer($databaseServer['id']); 51 | 52 | $this->output->infoWithDelayWarning('Database server deleted'); 53 | 54 | if (!$databaseServer['publicly_accessible']) { 55 | $this->output->newLine(); 56 | $this->output->note(sprintf('If you have no other resources using the private subnet, you should remove the network\'s NAT gateway using the "%s" command', RemoveNatGatewayCommand::NAME)); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Command/Database/DeleteDatabaseUserCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Database; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputOption; 19 | use Ymir\Cli\Exception\InvalidInputException; 20 | 21 | class DeleteDatabaseUserCommand extends AbstractDatabaseCommand 22 | { 23 | /** 24 | * The name of the command. 25 | * 26 | * @var string 27 | */ 28 | public const NAME = 'database:user:delete'; 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function configure() 34 | { 35 | $this 36 | ->setName(self::NAME) 37 | ->setDescription('Delete a user on a database') 38 | ->addArgument('username', InputArgument::OPTIONAL, 'The username of the database user to delete') 39 | ->addOption('server', null, InputOption::VALUE_REQUIRED, 'The ID or name of the database server where the database user will be deleted'); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | protected function perform() 46 | { 47 | $databaseServer = $this->determineDatabaseServer('On which database server would you like to create the new database user?'); 48 | $username = $this->input->getStringArgument('username'); 49 | $users = $this->apiClient->getDatabaseUsers($databaseServer['id']); 50 | 51 | if ($users->isEmpty()) { 52 | throw new RuntimeException('The database server doesn\'t have any managed database users'); 53 | } elseif (empty($username)) { 54 | $username = (string) $this->output->choice('Which database user would you like to delete', $users->pluck('username')); 55 | } 56 | 57 | $user = $users->firstWhere('username', $username); 58 | 59 | if (empty($user['id'])) { 60 | throw new InvalidInputException(sprintf('No database user found with the "%s" username', $username)); 61 | } 62 | 63 | if (!$this->output->confirm(sprintf('Are you sure you want to delete the "%s" database user?', $user['username']), false)) { 64 | return; 65 | } 66 | 67 | $this->apiClient->deleteDatabaseUser($databaseServer['id'], $user['id']); 68 | 69 | $this->output->info('Database user deleted'); 70 | 71 | if (!$databaseServer['publicly_accessible']) { 72 | $this->output->newLine(); 73 | $this->output->important('The database user needs to be manually deleted on the database server because it isn\'t publicly accessible. You can use the following query to delete it:'); 74 | $this->output->writeln(sprintf('DROP USER IF EXISTS %s@\'%%\'', $user['username'])); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Command/Database/GetDatabaseServerInfoCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Database; 15 | 16 | use Symfony\Component\Console\Helper\TableSeparator; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | 19 | class GetDatabaseServerInfoCommand extends AbstractDatabaseServerCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'database:server:info'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Get information on a database server') 36 | ->addArgument('server', InputArgument::OPTIONAL, 'The ID or name of the database server to fetch the information of'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function perform() 43 | { 44 | $databaseServer = $this->determineDatabaseServer('Which database server would you like to get information about'); 45 | 46 | $this->output->horizontalTable( 47 | ['Id', 'Name', 'Status', 'Locked', 'Public', new TableSeparator(), 'Provider', 'Network', 'Region', 'Type', 'Storage', 'Endpoint'], 48 | [[ 49 | $databaseServer['id'], 50 | $databaseServer['name'], 51 | $this->output->formatStatus($databaseServer['status']), 52 | $this->output->formatBoolean($databaseServer['locked']), 53 | $this->output->formatBoolean($databaseServer['publicly_accessible']), 54 | new TableSeparator(), 55 | $databaseServer['network']['provider']['name'], 56 | $databaseServer['network']['name'], 57 | $databaseServer['region'], 58 | $databaseServer['type'], 59 | $databaseServer['storage'] ? $databaseServer['storage'].'GB' : 'N/A', 60 | $databaseServer['endpoint'] ?? 'pending', 61 | ]] 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Command/Database/ListDatabaseServersCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Database; 15 | 16 | use Ymir\Cli\Command\AbstractCommand; 17 | 18 | class ListDatabaseServersCommand extends AbstractCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'database:server:list'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('List all the database servers that the current team has access to'); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | protected function perform() 41 | { 42 | $this->output->table( 43 | ['Id', 'Name', 'Provider', 'Network', 'Region', 'Status', 'Locked', 'Public', 'Type', 'Storage'], 44 | $this->apiClient->getDatabaseServers($this->cliConfiguration->getActiveTeamId())->map(function (array $database) { 45 | return [ 46 | $database['id'], 47 | $database['name'], 48 | $database['network']['provider']['name'], 49 | $database['network']['name'], 50 | $database['region'], 51 | $this->output->formatStatus($database['status']), 52 | $this->output->formatBoolean($database['locked']), 53 | $this->output->formatBoolean($database['publicly_accessible']), 54 | $database['type'], 55 | $database['storage'] ? $database['storage'].'GB' : 'N/A', 56 | ]; 57 | })->all() 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Command/Database/ListDatabaseUsersCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Database; 15 | 16 | use Carbon\Carbon; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | 19 | class ListDatabaseUsersCommand extends AbstractDatabaseServerCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'database:user:list'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('List all the managed users on a public database server') 36 | ->addArgument('server', InputArgument::OPTIONAL, 'The ID or name of the database server to list users from'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function perform() 43 | { 44 | $this->output->table( 45 | ['Id', 'Username', 'Created At'], 46 | $this->apiClient->getDatabaseUsers($this->determineDatabaseServer('Which database server would you like to list users from')['id'])->map(function (array $database) { 47 | return [$database['id'], $database['username'], Carbon::parse($database['created_at'])->diffForHumans()]; 48 | })->all() 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Command/Database/ListDatabasesCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Database; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | 19 | class ListDatabasesCommand extends AbstractDatabaseServerCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'database:list'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('List all the databases on a public database server') 36 | ->addArgument('server', InputArgument::OPTIONAL, 'The ID or name of the database server to list databases from'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function perform() 43 | { 44 | $databaseServer = $this->determineDatabaseServer('Which database server would you like to list databases from'); 45 | 46 | if (!$databaseServer['publicly_accessible']) { 47 | throw new RuntimeException('Database on private database servers cannot be listed.'); 48 | } 49 | 50 | $this->output->table( 51 | ['Name'], 52 | $this->apiClient->getDatabases($databaseServer['id'])->map(function (string $name) { 53 | return [$name]; 54 | })->all() 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Command/Database/LockDatabaseServerCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Database; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | 18 | class LockDatabaseServerCommand extends AbstractDatabaseServerCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'database:server:lock'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('Lock the database server which prevents it from being deleted') 35 | ->addArgument('server', InputArgument::OPTIONAL, 'The ID or name of the database server to lock'); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function perform() 42 | { 43 | $databaseServer = $this->determineDatabaseServer('Which database server would you like to lock?'); 44 | 45 | $this->apiClient->changeDatabaseServerLock($databaseServer['id'], true); 46 | 47 | $this->output->info('Database server locked'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Command/Database/RotateDatabaseServerPasswordCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Database; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Ymir\Cli\Command\Project\DeployProjectCommand; 18 | use Ymir\Cli\Command\Project\RedeployProjectCommand; 19 | 20 | class RotateDatabaseServerPasswordCommand extends AbstractDatabaseServerCommand 21 | { 22 | /** 23 | * The name of the command. 24 | * 25 | * @var string 26 | */ 27 | public const NAME = 'database:server:rotate-password'; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function configure() 33 | { 34 | $this 35 | ->setName(self::NAME) 36 | ->setDescription('Rotate the password of the database server\'s "ymir" user') 37 | ->addArgument('server', InputArgument::OPTIONAL, 'The ID or name of the database server to rotate the password of'); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | protected function perform() 44 | { 45 | $databaseServer = $this->determineDatabaseServer('Which database server would you like to rotate the password of?'); 46 | 47 | $this->output->warning(sprintf('All projects that use the "%s" database server with the default user will be unable to connect to the database server until they\'re redeployed.', $databaseServer['name'])); 48 | 49 | if (!$this->output->confirm('Do you want to proceed?', false)) { 50 | return; 51 | } 52 | 53 | $newCredentials = $this->apiClient->rotateDatabaseServerPassword($databaseServer['id']); 54 | 55 | $this->output->horizontalTable( 56 | ['Username', 'Password'], 57 | [[$newCredentials['username'], $newCredentials['password']]] 58 | ); 59 | 60 | $this->output->infoWithDelayWarning('Database server password rotated successfully'); 61 | $this->output->newLine(); 62 | $this->output->important(sprintf('You need to redeploy all projects using this database server with the default user using either the "%s" or "%s" commands for the change to take effect.', DeployProjectCommand::ALIAS, RedeployProjectCommand::ALIAS)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Command/Database/UnlockDatabaseServerCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Database; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | 18 | class UnlockDatabaseServerCommand extends AbstractDatabaseServerCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'database:server:unlock'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('Unlock the database server which allows it to be deleted') 35 | ->addArgument('server', InputArgument::OPTIONAL, 'The ID or name of the database server to unlock'); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function perform() 42 | { 43 | $databaseServer = $this->determineDatabaseServer('Which database server would you like to unlock?'); 44 | 45 | $this->apiClient->changeDatabaseServerLock($databaseServer['id'], false); 46 | 47 | $this->output->info('Database server unlocked'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Command/Dns/AbstractDnsCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Dns; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Ymir\Cli\Command\AbstractCommand; 18 | 19 | abstract class AbstractDnsCommand extends AbstractCommand 20 | { 21 | /** 22 | * Determine the DNS zone that the command is interacting with. 23 | */ 24 | protected function determineDnsZone(string $question): array 25 | { 26 | $zone = null; 27 | $zones = $this->apiClient->getDnsZones($this->cliConfiguration->getActiveTeamId()); 28 | $zoneIdOrName = $this->input->getStringArgument('zone'); 29 | 30 | if ($zones->isEmpty()) { 31 | throw new RuntimeException(sprintf('The currently active team has no DNS zones. You can create one with the "%s" command.', CreateDnsZoneCommand::NAME)); 32 | } 33 | 34 | if (empty($zoneIdOrName)) { 35 | $zoneIdOrName = (string) $this->output->choice($question, $zones->pluck('domain_name')); 36 | } 37 | 38 | if (is_numeric($zoneIdOrName)) { 39 | $zone = $zones->firstWhere('id', $zoneIdOrName); 40 | } elseif (is_string($zoneIdOrName)) { 41 | $zone = $zones->firstWhere('domain_name', $zoneIdOrName); 42 | } 43 | 44 | if (empty($zone['id'])) { 45 | throw new RuntimeException(sprintf('Unable to find a DNS zones with "%s" as the ID or name', $zoneIdOrName)); 46 | } 47 | 48 | return $zone; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Command/Dns/ChangeDnsRecordCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Dns; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | 18 | class ChangeDnsRecordCommand extends AbstractDnsCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'dns:record:change'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('Change the value of a DNS record (Will overwrite existing DNS record if it already exists)') 35 | ->addArgument('zone', InputArgument::REQUIRED, 'The name of the DNS zone that the DNS record belongs to') 36 | ->addArgument('type', InputArgument::REQUIRED, 'The DNS record type') 37 | ->addArgument('name', InputArgument::REQUIRED, 'The name of the DNS record without the domain') 38 | ->addArgument('value', InputArgument::REQUIRED, 'The value of the DNS record'); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function perform() 45 | { 46 | $this->apiClient->changeDnsRecord($this->input->getStringArgument('zone'), $this->input->getStringArgument('type'), $this->input->getStringArgument('name'), $this->input->getStringArgument('value')); 47 | 48 | $this->output->info('DNS record change applied'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Command/Dns/CreateDnsZoneCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Dns; 15 | 16 | use Symfony\Component\Console\Helper\TableSeparator; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputOption; 19 | 20 | class CreateDnsZoneCommand extends AbstractDnsCommand 21 | { 22 | /** 23 | * The name of the command. 24 | * 25 | * @var string 26 | */ 27 | public const NAME = 'dns:zone:create'; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function configure() 33 | { 34 | $this 35 | ->setName(self::NAME) 36 | ->setDescription('Create a new DNS zone') 37 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the domain managed by the created DNS zone') 38 | ->addOption('provider', null, InputOption::VALUE_REQUIRED, 'The cloud provider region where the DNS zone will created'); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function perform() 45 | { 46 | $name = $this->input->getStringArgument('name'); 47 | 48 | if (empty($name)) { 49 | $name = $this->output->ask('What is the name of the domain that the DNS zone will manage'); 50 | } 51 | 52 | $providerId = $this->determineCloudProvider('Enter the ID of the cloud provider where the DNS zone will be created'); 53 | 54 | if (!$this->output->confirm('A DNS zone will cost $0.50/month if it isn\'t deleted in the next 12 hours. Would you like to proceed?', true)) { 55 | return; 56 | } 57 | 58 | $zone = $this->apiClient->createDnsZone($providerId, $name); 59 | 60 | $nameServers = $this->wait(function () use ($zone) { 61 | return $this->apiClient->getDnsZone($zone['id'])->get('name_servers', []); 62 | }); 63 | 64 | if (!empty($nameServers)) { 65 | $this->output->horizontalTable( 66 | ['Domain Name', new TableSeparator(), 'Name Servers'], 67 | [[$zone['domain_name'], new TableSeparator(), implode(PHP_EOL, $nameServers)]] 68 | ); 69 | } 70 | 71 | $this->output->info('DNS zone created'); 72 | 73 | if ($this->output->confirm('Do you want to import the root DNS records for this domain', false)) { 74 | $this->apiClient->importDnsRecords($zone['id']); 75 | } 76 | 77 | if ($this->output->confirm('Do you want to import DNS records for subdomains of this domain', false)) { 78 | $this->invoke(ImportDnsRecordsCommand::NAME, [ 79 | 'zone' => $zone['domain_name'], 80 | ]); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Command/Dns/DeleteDnsZoneCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Dns; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | 18 | class DeleteDnsZoneCommand extends AbstractDnsCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'dns:zone:delete'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('Delete a DNS zone') 35 | ->addArgument('zone', InputArgument::OPTIONAL, 'The ID or name of the DNS zone to delete'); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function perform() 42 | { 43 | $zone = $this->determineDnsZone('Which DNS zone would you like to delete'); 44 | 45 | if (!$this->output->confirm('Are you sure you want to delete this DNS zone?', false)) { 46 | return; 47 | } 48 | 49 | $this->apiClient->deleteDnsZone((int) $zone['id']); 50 | 51 | $this->output->info('DNS zone deleted'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Command/Dns/ImportDnsRecordsCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Dns; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | 18 | class ImportDnsRecordsCommand extends AbstractDnsCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'dns:zone:import-records'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('Import DNS records into a DNS zone') 35 | ->addArgument('zone', InputArgument::REQUIRED, 'The name of the DNS zone that the DNS record belongs to') 36 | ->addArgument('subdomain', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'The subdomain(s) that we want to import'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function perform() 43 | { 44 | $subdomains = $this->input->getArrayArgument('subdomain', false); 45 | 46 | if (empty($subdomains)) { 47 | $subdomains = explode(',', (string) $this->output->ask('Please enter a comma-separated list of subdomains to import DNS records from (leave blank to import the root DNS records)')); 48 | } 49 | 50 | $this->apiClient->importDnsRecords($this->input->getStringArgument('zone'), $subdomains); 51 | 52 | $this->output->info('DNS records imported'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Command/Dns/ListDnsRecordsCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Dns; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | 18 | class ListDnsRecordsCommand extends AbstractDnsCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'dns:record:list'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('List the DNS records belonging to a DNS zone') 35 | ->addArgument('zone', InputArgument::OPTIONAL, 'The ID or name of the DNS zone to list DNS records from'); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function perform() 42 | { 43 | $zone = $this->determineDnsZone('Which DNS zone would you like to list DNS records from'); 44 | 45 | $this->output->table( 46 | ['Id', 'Domain Name', 'Type', 'Value', 'Internal'], 47 | $this->apiClient->getDnsRecords($zone['id'])->map(function (array $record) { 48 | return [$record['id'], $record['name'], $record['type'], str_replace(',', "\n", $record['value']), $this->output->formatBoolean($record['internal'])]; 49 | })->all() 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Command/Dns/ListDnsZonesCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Dns; 15 | 16 | use Ymir\Cli\Command\AbstractCommand; 17 | 18 | class ListDnsZonesCommand extends AbstractCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'dns:zone:list'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('List the DNS zones that belong to the currently active team'); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | protected function perform() 41 | { 42 | $this->output->table( 43 | ['Id', 'Provider', 'Domain Name', 'Name Servers'], 44 | $this->apiClient->getDnsZones($this->cliConfiguration->getActiveTeamId())->map(function (array $zone) { 45 | return [$zone['id'], $zone['provider']['name'], $zone['domain_name'], implode(PHP_EOL, $zone['name_servers'])]; 46 | })->all() 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Command/Email/AbstractEmailIdentityCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Email; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Ymir\Cli\Command\AbstractCommand; 18 | 19 | abstract class AbstractEmailIdentityCommand extends AbstractCommand 20 | { 21 | /** 22 | * Determine the email identity that the command is interacting with. 23 | */ 24 | protected function determineEmailIdentity(string $question): array 25 | { 26 | $identity = null; 27 | $identities = $this->apiClient->getEmailIdentities($this->cliConfiguration->getActiveTeamId()); 28 | $identityIdOrName = $this->input->getStringArgument('identity'); 29 | 30 | if ($identities->isEmpty()) { 31 | throw new RuntimeException(sprintf('The currently active team has no email identities. You can create one with the "%s" command.', CreateEmailIdentityCommand::NAME)); 32 | } 33 | 34 | if (empty($identityIdOrName)) { 35 | $identityIdOrName = (string) $this->output->choice($question, $identities->pluck('name')); 36 | } 37 | 38 | if (is_numeric($identityIdOrName)) { 39 | $identity = $identities->firstWhere('id', $identityIdOrName); 40 | } elseif (is_string($identityIdOrName)) { 41 | $identity = $identities->firstWhere('name', $identityIdOrName); 42 | } 43 | 44 | if (empty($identity['id'])) { 45 | throw new RuntimeException(sprintf('Unable to find an email identity with "%s" as the ID or name', $identityIdOrName)); 46 | } 47 | 48 | return $identity; 49 | } 50 | 51 | /** 52 | * Display warning about DNS records required to authenticate the DKIM signature and verify it. 53 | */ 54 | protected function displayDkimAuthenticationRecords(array $identity) 55 | { 56 | if (empty($identity['dkim_authentication_records']) || $identity['managed']) { 57 | return; 58 | } 59 | 60 | $this->output->newLine(); 61 | $this->output->important('The following DNS records needs to exist on your DNS server at all times to verify the email identity and authenticate its DKIM signature:'); 62 | $this->output->newLine(); 63 | $this->output->table( 64 | ['Name', 'Type', 'Value'], 65 | collect($identity['dkim_authentication_records'])->map(function (array $dkimRecord) { 66 | $dkimRecord['type'] = strtoupper($dkimRecord['type']); 67 | 68 | return $dkimRecord; 69 | })->all() 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Command/Email/CreateEmailIdentityCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Email; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputOption; 18 | 19 | class CreateEmailIdentityCommand extends AbstractEmailIdentityCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'email:identity:create'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Create a new email identity') 36 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the email identity') 37 | ->addOption('provider', null, InputOption::VALUE_REQUIRED, 'The cloud provider where the email identity will be created') 38 | ->addOption('region', null, InputOption::VALUE_REQUIRED, 'The cloud provider region where the email identity will be located'); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function perform() 45 | { 46 | $name = $this->input->getStringArgument('name'); 47 | 48 | if (empty($name)) { 49 | $name = $this->output->ask('What is the name of the email identity'); 50 | } 51 | 52 | $providerId = $this->determineCloudProvider('Enter the ID of the cloud provider where the email identity will be created'); 53 | 54 | $identity = $this->apiClient->createEmailIdentity($providerId, $name, $this->determineRegion('Enter the name of the region where the email identity will be created', $providerId)); 55 | 56 | $this->output->info('Email identity created'); 57 | 58 | if ('domain' === $identity['type']) { 59 | $this->displayDkimAuthenticationRecords($identity->toArray()); 60 | } elseif ('email' === $identity['type']) { 61 | $this->output->newLine(); 62 | $this->output->important(sprintf('A verification email was sent to %s to validate the email identity', $identity['name'])); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Command/Email/DeleteEmailIdentityCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Email; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | 18 | class DeleteEmailIdentityCommand extends AbstractEmailIdentityCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'email:identity:delete'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('Delete an email identity') 35 | ->addArgument('identity', InputArgument::OPTIONAL, 'The ID or name of the email identity to delete'); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function perform() 42 | { 43 | $identity = $this->determineEmailIdentity('Which email identity would you like to delete'); 44 | 45 | if (!$this->output->confirm('Are you sure you want to delete this email identity?', false)) { 46 | return; 47 | } 48 | 49 | $this->apiClient->deleteEmailIdentity((int) $identity['id']); 50 | 51 | $this->output->info('Email identity deleted'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Command/Email/GetEmailIdentityInfoCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Email; 15 | 16 | use Symfony\Component\Console\Helper\TableSeparator; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | 19 | class GetEmailIdentityInfoCommand extends AbstractEmailIdentityCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'email:identity:info'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Get the information on an email identity') 36 | ->addArgument('identity', InputArgument::OPTIONAL, 'The ID or name of the email identity to fetch the information of'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function perform() 43 | { 44 | $identity = $this->determineEmailIdentity('Which email identity would you like to get information about'); 45 | 46 | $this->output->horizontalTable( 47 | ['Name', new TableSeparator(), 'Provider', 'Region', new TableSeparator(), 'Type', 'Verified', 'Managed'], 48 | [[$identity['name'], new TableSeparator(), $identity['provider']['name'], $identity['region'], new TableSeparator(), $identity['type'], $this->output->formatBoolean($identity['verified']), $this->output->formatBoolean($identity['managed'])]] 49 | ); 50 | 51 | $this->displayDkimAuthenticationRecords($identity); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Command/Email/ListEmailIdentitiesCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Email; 15 | 16 | class ListEmailIdentitiesCommand extends AbstractEmailIdentityCommand 17 | { 18 | /** 19 | * The name of the command. 20 | * 21 | * @var string 22 | */ 23 | public const NAME = 'email:identity:list'; 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | protected function configure() 29 | { 30 | $this 31 | ->setName(self::NAME) 32 | ->setDescription('List the email identities that belong to the currently active team'); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | protected function perform() 39 | { 40 | $this->output->table( 41 | ['Id', 'Name', 'Type', 'Provider', 'Region', 'Verified', 'Managed'], 42 | $this->apiClient->getEmailIdentities($this->cliConfiguration->getActiveTeamId())->map(function (array $identity) { 43 | return [$identity['id'], $identity['name'], $identity['type'], $identity['provider']['name'], $identity['region'], $this->output->formatBoolean($identity['verified']), $this->output->formatBoolean($identity['managed'])]; 44 | })->all() 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Command/Environment/AbstractEnvironmentLogsCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Environment; 15 | 16 | use Carbon\Carbon; 17 | use Carbon\Exceptions\InvalidTimeZoneException; 18 | use Illuminate\Support\Collection; 19 | use Ymir\Cli\Command\AbstractProjectCommand; 20 | use Ymir\Cli\Console\Output; 21 | use Ymir\Cli\Exception\InvalidInputException; 22 | 23 | abstract class AbstractEnvironmentLogsCommand extends AbstractProjectCommand 24 | { 25 | /** 26 | * Write the logs to the console output. 27 | */ 28 | protected function writeLogs(Collection $logs, ?string $timezone = null) 29 | { 30 | $logs->each(function (array $log) use ($timezone) { 31 | $timestamp = Carbon::createFromTimestamp($log['timestamp'] / 1000); 32 | 33 | if ($timezone) { 34 | try { 35 | $timestamp->setTimezone($timezone); 36 | } catch (InvalidTimeZoneException $exception) { 37 | throw new InvalidInputException(sprintf('"%s" is not a valid timezone', $timezone)); 38 | } 39 | } 40 | 41 | $this->output->writeln(sprintf('[%s] %s', $timestamp->toDateTimeString(), trim($log['message']))); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Command/Environment/ChangeEnvironmentSecretCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Environment; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Ymir\Cli\Command\AbstractProjectCommand; 18 | 19 | class ChangeEnvironmentSecretCommand extends AbstractProjectCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'environment:secret:change'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Change an environment\'s secret') 36 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment where the secret is', 'staging') 37 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the secret') 38 | ->addArgument('value', InputArgument::OPTIONAL, 'The secret value'); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function perform() 45 | { 46 | $environment = $this->input->getStringArgument('environment'); 47 | $name = $this->input->getStringArgument('name'); 48 | $value = $this->input->getStringArgument('value'); 49 | 50 | if (empty($name)) { 51 | $name = $this->output->ask('What is the name of the secret'); 52 | } 53 | 54 | if (empty($value)) { 55 | $value = $this->output->ask('What is the secret value'); 56 | } 57 | 58 | $this->apiClient->changeSecret($this->projectConfiguration->getProjectId(), $environment, $name, $value); 59 | 60 | $this->output->infoWithRedeployWarning('Secret changed', $environment); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Command/Environment/ChangeEnvironmentVariableCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Environment; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Ymir\Cli\Command\AbstractProjectCommand; 18 | 19 | class ChangeEnvironmentVariableCommand extends AbstractProjectCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'environment:variables:change'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Change an environment variable') 36 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment where the environment variable is', 'staging') 37 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the environment variable') 38 | ->addArgument('value', InputArgument::OPTIONAL, 'The value of the environment variable'); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function perform() 45 | { 46 | $environment = $this->input->getStringArgument('environment'); 47 | $name = $this->input->getStringArgument('name'); 48 | $value = $this->input->getStringArgument('value'); 49 | 50 | if (empty($name)) { 51 | $name = $this->output->ask('What is the name of the environment variable'); 52 | } 53 | 54 | if (empty($value)) { 55 | $value = $this->output->ask('What is the value of the environment variable'); 56 | } 57 | 58 | $this->apiClient->changeEnvironmentVariables($this->projectConfiguration->getProjectId(), $environment, [ 59 | $name => $value, 60 | ]); 61 | 62 | $this->output->infoWithRedeployWarning('Environment variable changed', $environment); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Command/Environment/CreateEnvironmentCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Environment; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputOption; 18 | use Ymir\Cli\Command\AbstractProjectCommand; 19 | 20 | class CreateEnvironmentCommand extends AbstractProjectCommand 21 | { 22 | /** 23 | * The name of the command. 24 | * 25 | * @var string 26 | */ 27 | public const NAME = 'environment:create'; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function configure() 33 | { 34 | $this 35 | ->setName(self::NAME) 36 | ->setDescription('Create a new environment') 37 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the environment to create') 38 | ->addOption('use-image', null, InputOption::VALUE_NONE, 'Whether the environment will be deployed using a container image'); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function perform() 45 | { 46 | $name = $this->input->getStringArgument('name') ?: $this->output->ask('What is the name of the environment'); 47 | $projectId = $this->projectConfiguration->getProjectId(); 48 | $projectType = $this->projectConfiguration->getProjectType(); 49 | 50 | $this->apiClient->createEnvironment($projectId, $name); 51 | 52 | $this->projectConfiguration->addEnvironment($name, $projectType->getEnvironmentConfiguration($name, $this->input->getBooleanOption('use-image') ? ['deployment' => 'image'] : [])); 53 | 54 | $this->output->info('Environment created'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Command/Environment/DeleteEnvironmentSecretCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Environment; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Ymir\Cli\Command\AbstractProjectCommand; 19 | use Ymir\Cli\Exception\InvalidInputException; 20 | 21 | class DeleteEnvironmentSecretCommand extends AbstractProjectCommand 22 | { 23 | /** 24 | * The name of the command. 25 | * 26 | * @var string 27 | */ 28 | public const NAME = 'environment:secret:delete'; 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function configure() 34 | { 35 | $this 36 | ->setName(self::NAME) 37 | ->setDescription('Delete an environment\'s secret') 38 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment where the secret is', 'staging') 39 | ->addArgument('secret', InputArgument::OPTIONAL, 'The ID or name of the secret'); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | protected function perform() 46 | { 47 | $environment = $this->input->getStringArgument('environment'); 48 | $secrets = $this->apiClient->getSecrets($this->projectConfiguration->getProjectId(), $environment); 49 | 50 | if ($secrets->isEmpty()) { 51 | throw new RuntimeException(sprintf('The "%s" environment has no secrets', $environment)); 52 | } 53 | 54 | $secretIdOrName = $this->input->getStringArgument('secret'); 55 | 56 | if (empty($secretIdOrName)) { 57 | $secretIdOrName = (string) $this->output->choice('Which secret would you like to delete', $secrets->pluck('name')); 58 | } 59 | 60 | $secret = is_numeric($secretIdOrName) ? $secrets->firstWhere('id', $secretIdOrName) : $secrets->firstWhere('name', $secretIdOrName); 61 | 62 | if (!is_array($secret) || empty($secret['id'])) { 63 | throw new InvalidInputException(sprintf('Unable to find a secret with "%s" as the ID or name', $secretIdOrName)); 64 | } elseif (!$this->output->confirm('Are you sure you want to delete this secret?', false)) { 65 | return; 66 | } 67 | 68 | $this->apiClient->deleteSecret($secret['id']); 69 | 70 | $this->output->info('Secret deleted'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Command/Environment/DownloadEnvironmentVariablesCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Environment; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Filesystem\Filesystem; 18 | use Ymir\Cli\ApiClient; 19 | use Ymir\Cli\CliConfiguration; 20 | use Ymir\Cli\Command\AbstractProjectCommand; 21 | use Ymir\Cli\Project\Configuration\ProjectConfiguration; 22 | 23 | class DownloadEnvironmentVariablesCommand extends AbstractProjectCommand 24 | { 25 | /** 26 | * The name of the command. 27 | * 28 | * @var string 29 | */ 30 | public const NAME = 'environment:variables:download'; 31 | 32 | /** 33 | * The file system. 34 | * 35 | * @var Filesystem 36 | */ 37 | private $filesystem; 38 | 39 | /** 40 | * The project directory where the project files are copied from. 41 | * 42 | * @var string 43 | */ 44 | private $projectDirectory; 45 | 46 | /** 47 | * Constructor. 48 | */ 49 | public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, Filesystem $filesystem, ProjectConfiguration $projectConfiguration, string $projectDirectory) 50 | { 51 | parent::__construct($apiClient, $cliConfiguration, $projectConfiguration); 52 | 53 | $this->filesystem = $filesystem; 54 | $this->projectDirectory = rtrim($projectDirectory, '/'); 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | protected function configure() 61 | { 62 | $this 63 | ->setName(self::NAME) 64 | ->setDescription('Download an environment\'s environment variables into an environment file') 65 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to download environment variables from', 'staging'); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | protected function perform() 72 | { 73 | $environment = $this->input->getStringArgument('environment'); 74 | $filename = sprintf('.env.%s', $environment); 75 | 76 | $this->filesystem->dumpFile($this->projectDirectory.'/'.$filename, $this->apiClient->getEnvironmentVariables($this->projectConfiguration->getProjectId(), $environment)->sortKeys()->map(function (string $value, string $key) { 77 | return sprintf('%s=%s', $key, $value); 78 | })->join("\n")); 79 | 80 | $this->output->infoWithValue('Environment variables downloaded to', $filename); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Command/Environment/GetEnvironmentUrlCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Environment; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Process\Process; 18 | use Ymir\Cli\Command\AbstractProjectCommand; 19 | 20 | class GetEnvironmentUrlCommand extends AbstractProjectCommand 21 | { 22 | /** 23 | * The name of the command. 24 | * 25 | * @var string 26 | */ 27 | public const NAME = 'environment:url'; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function configure() 33 | { 34 | $this 35 | ->setName(self::NAME) 36 | ->setDescription('Get the environment URL and copy it to the clipboard') 37 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to get the URL of', 'staging'); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | protected function perform() 44 | { 45 | $this->displayEnvironmentUrlAndCopyToClipboard($this->apiClient->getEnvironmentVanityDomainName($this->projectConfiguration->getProjectId(), $this->input->getStringArgument('environment'))); 46 | } 47 | 48 | /** 49 | * Generate the environment URL, copy it to the clipboard and then displays it in the console. 50 | */ 51 | private function displayEnvironmentUrlAndCopyToClipboard(string $domainName) 52 | { 53 | $clipboardCommand = 'WIN' === strtoupper(substr(PHP_OS, 0, 3)) ? 'clip' : 'pbcopy'; 54 | $url = 'https://'.$domainName; 55 | 56 | Process::fromShellCommandline(sprintf('echo %s | %s', $url, $clipboardCommand))->run(); 57 | 58 | $this->output->infoWithValue('Environment URL is', $url, 'copied to clipboard'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Command/Environment/InvalidateEnvironmentCacheCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Environment; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputOption; 18 | use Ymir\Cli\Command\AbstractProjectCommand; 19 | 20 | class InvalidateEnvironmentCacheCommand extends AbstractProjectCommand 21 | { 22 | /** 23 | * The name of the command. 24 | * 25 | * @var string 26 | */ 27 | public const NAME = 'environment:invalidate-cache'; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function configure() 33 | { 34 | $this 35 | ->setName(self::NAME) 36 | ->setDescription('Invalidate the environment\'s content delivery network cache') 37 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to invalidate the cache of', 'staging') 38 | ->addOption('path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to invalidate on the content delivery network', ['*']); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function perform() 45 | { 46 | $this->apiClient->invalidateCache($this->projectConfiguration->getProjectId(), $this->input->getStringArgument('environment'), $this->input->getArrayOption('path')); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Command/Environment/ListEnvironmentSecretsCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Environment; 15 | 16 | use Carbon\Carbon; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Ymir\Cli\Command\AbstractProjectCommand; 19 | 20 | class ListEnvironmentSecretsCommand extends AbstractProjectCommand 21 | { 22 | /** 23 | * The name of the command. 24 | * 25 | * @var string 26 | */ 27 | public const NAME = 'environment:secret:list'; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function configure() 33 | { 34 | $this 35 | ->setName(self::NAME) 36 | ->setDescription('List an environment\'s secrets') 37 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to list secrets of', 'staging'); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | protected function perform() 44 | { 45 | $this->output->table( 46 | ['Id', 'Name', 'Last Updated'], 47 | $this->apiClient->getSecrets($this->projectConfiguration->getProjectId(), $this->input->getStringArgument('environment'))->map(function (array $secret) { 48 | return [$secret['id'], $secret['name'], Carbon::parse($secret['updated_at'])->diffForHumans()]; 49 | })->all() 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Command/Environment/ListEnvironmentsCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Environment; 15 | 16 | use Ymir\Cli\Command\AbstractProjectCommand; 17 | 18 | class ListEnvironmentsCommand extends AbstractProjectCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'environment:list'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('List the project\'s environments'); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | protected function perform() 41 | { 42 | $this->output->table( 43 | ['Id', 'Name', 'URL'], 44 | $this->apiClient->getEnvironments($this->projectConfiguration->getProjectId())->map(function (array $environment) { 45 | return [$environment['id'], $environment['name'], 'https://'.$environment['vanity_domain_name']]; 46 | })->all() 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Command/Environment/QueryEnvironmentLogsCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Environment; 15 | 16 | use Carbon\Carbon; 17 | use Carbon\CarbonInterval; 18 | use Symfony\Component\Console\Input\InputArgument; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Ymir\Cli\Exception\InvalidInputException; 21 | 22 | class QueryEnvironmentLogsCommand extends AbstractEnvironmentLogsCommand 23 | { 24 | /** 25 | * The name of the command. 26 | * 27 | * @var string 28 | */ 29 | public const NAME = 'environment:logs:query'; 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function configure() 35 | { 36 | $this 37 | ->setName(self::NAME) 38 | ->setDescription('Retrieve logs for an environment function') 39 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to get the logs of', 'staging') 40 | ->addArgument('function', InputArgument::OPTIONAL, 'The environment function to get the logs of', 'website') 41 | ->addOption('lines', null, InputOption::VALUE_REQUIRED, 'The number of log lines to display', 10) 42 | ->addOption('order', null, InputOption::VALUE_REQUIRED, 'The order to display the logs in', 'asc') 43 | ->addOption('period', null, InputOption::VALUE_REQUIRED, 'The period of time to get the logs for', '1h') 44 | ->addOption('timezone', null, InputOption::VALUE_REQUIRED, 'The timezone to display the log times in'); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | protected function perform() 51 | { 52 | $environment = $this->input->getStringArgument('environment'); 53 | $function = strtolower($this->input->getStringArgument('function')); 54 | $lines = (int) $this->input->getNumericOption('lines'); 55 | $order = strtolower($this->input->getStringOption('order')); 56 | 57 | if ($lines < 1) { 58 | throw new InvalidInputException('The number of lines must be at least 1'); 59 | } elseif (!in_array($order, ['asc', 'desc'])) { 60 | throw new InvalidInputException('The order must be either "asc" or "desc"'); 61 | } 62 | 63 | $logs = $this->apiClient->getEnvironmentLogs($this->projectConfiguration->getProjectId(), $environment, $function, Carbon::now()->sub(CarbonInterval::fromString($this->input->getStringOption('period')))->getTimestampMs(), 'desc'); 64 | 65 | if ($logs->isEmpty()) { 66 | $this->output->info('No logs found for the given period'); 67 | 68 | return; 69 | } 70 | 71 | $logs = $logs->take($lines); 72 | 73 | if ('asc' === $order) { 74 | $logs = $logs->reverse(); 75 | } 76 | 77 | $this->writeLogs($logs, $this->input->getStringOption('timezone')); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Command/Environment/WatchEnvironmentLogsCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Environment; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputOption; 18 | use Ymir\Cli\Exception\InvalidInputException; 19 | 20 | class WatchEnvironmentLogsCommand extends AbstractEnvironmentLogsCommand 21 | { 22 | /** 23 | * The name of the command. 24 | * 25 | * @var string 26 | */ 27 | public const NAME = 'environment:logs:watch'; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function configure() 33 | { 34 | $this 35 | ->setName(self::NAME) 36 | ->setDescription('Continuously monitor and display the most recent logs for an environment function') 37 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to get the logs of', 'staging') 38 | ->addArgument('function', InputArgument::OPTIONAL, 'The environment function to get the logs of', 'website') 39 | ->addOption('interval', null, InputOption::VALUE_REQUIRED, 'Interval (in seconds) to poll for new logs', 30) 40 | ->addOption('timezone', null, InputOption::VALUE_REQUIRED, 'The timezone to display the log times in'); 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | protected function perform() 47 | { 48 | $environment = $this->input->getStringArgument('environment'); 49 | $function = strtolower($this->input->getStringArgument('function')); 50 | $interval = (int) $this->input->getNumericOption('interval'); 51 | $since = (int) round(microtime(true) * 1000); 52 | 53 | if ($interval < 20) { 54 | throw new InvalidInputException('Polling interval must be at least 20 seconds'); 55 | } 56 | 57 | while (true) { 58 | sleep($interval); 59 | 60 | $this->writeLogs($this->apiClient->getEnvironmentLogs($this->projectConfiguration->getProjectId(), $environment, $function, $since), $this->input->getStringOption('timezone')); 61 | 62 | $since += $interval * 1000 + 1; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Command/InstallIntegrationCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command; 15 | 16 | use Ymir\Cli\ApiClient; 17 | use Ymir\Cli\CliConfiguration; 18 | use Ymir\Cli\Project\Configuration\ProjectConfiguration; 19 | 20 | class InstallIntegrationCommand extends AbstractProjectCommand 21 | { 22 | /** 23 | * The name of the command. 24 | * 25 | * @var string 26 | */ 27 | public const NAME = 'install-integration'; 28 | 29 | /** 30 | * The project directory where we want to install the plugin. 31 | * 32 | * @var string 33 | */ 34 | private $projectDirectory; 35 | 36 | /** 37 | * Constructor. 38 | */ 39 | public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, ProjectConfiguration $projectConfiguration, string $projectDirectory) 40 | { 41 | parent::__construct($apiClient, $cliConfiguration, $projectConfiguration); 42 | 43 | $this->projectDirectory = rtrim($projectDirectory, '/'); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | protected function configure() 50 | { 51 | $this 52 | ->setName(self::NAME) 53 | ->setDescription('Installs the Ymir integration for the project'); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | protected function perform() 60 | { 61 | $projectType = $this->projectConfiguration->getProjectType(); 62 | 63 | if ($projectType->isIntegrationInstalled($this->projectDirectory)) { 64 | $this->output->info('Ymir integration already installed'); 65 | 66 | return; 67 | } 68 | 69 | $this->output->info(sprintf('Installing Ymir %s integration', $projectType->getName())); 70 | 71 | $projectType->installIntegration($this->projectDirectory); 72 | 73 | $this->output->info(sprintf('Ymir %s integration installed', $projectType->getName())); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Command/LoginCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command; 15 | 16 | use Ymir\Sdk\Exception\ClientException; 17 | 18 | class LoginCommand extends AbstractCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'login'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('Authenticate with Ymir API'); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | protected function perform() 41 | { 42 | if ($this->apiClient->isAuthenticated() 43 | && !$this->output->confirm('You are already logged in. Do you want to log in again?', false) 44 | ) { 45 | return; 46 | } 47 | 48 | $email = $this->output->ask('Email'); 49 | $password = $this->output->askHidden('Password'); 50 | 51 | try { 52 | $accessToken = $this->apiClient->getAccessToken($email, $password); 53 | } catch (ClientException $exception) { 54 | if (!$exception->getValidationErrors()->has('authentication_code')) { 55 | throw $exception; 56 | } 57 | 58 | $accessToken = $this->apiClient->getAccessToken($email, $password, $this->output->askHidden('Authentication code')); 59 | } 60 | 61 | $this->apiClient->setAccessToken($accessToken); 62 | $this->cliConfiguration->setAccessToken($accessToken); 63 | 64 | $team = $this->apiClient->getActiveTeam(); 65 | 66 | if (isset($team['id'])) { 67 | $this->cliConfiguration->setActiveTeamId($team['id']); 68 | } 69 | 70 | $this->output->info('Logged in successfully'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Command/Network/AddNatGatewayCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Network; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Ymir\Cli\Command\AbstractCommand; 18 | 19 | class AddNatGatewayCommand extends AbstractCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'network:nat:add'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Add a NAT gateway to a network\'s private subnet') 36 | ->addArgument('network', InputArgument::OPTIONAL, 'The ID or name of the network to add a NAT gateway to'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function perform() 43 | { 44 | $this->apiClient->addNatGateway($this->determineNetwork('Which network would like to add a NAT gateway to')); 45 | 46 | $this->output->infoWithDelayWarning('NAT gateway added'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Command/Network/CreateNetworkCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Network; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputOption; 18 | use Ymir\Cli\Command\AbstractCommand; 19 | 20 | class CreateNetworkCommand extends AbstractCommand 21 | { 22 | /** 23 | * The name of the command. 24 | * 25 | * @var string 26 | */ 27 | public const NAME = 'network:create'; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function configure() 33 | { 34 | $this 35 | ->setName(self::NAME) 36 | ->setDescription('Create a new network') 37 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the network') 38 | ->addOption('provider', null, InputOption::VALUE_REQUIRED, 'The cloud provider region where the network will created') 39 | ->addOption('region', null, InputOption::VALUE_REQUIRED, 'The cloud provider region where the network will be located'); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | protected function perform() 46 | { 47 | $name = $this->input->getStringArgument('name'); 48 | 49 | if (empty($name)) { 50 | $name = $this->output->ask('What is the name of the network being created'); 51 | } 52 | 53 | $providerId = $this->determineCloudProvider('Enter the ID of the cloud provider where the DNS zone will be created'); 54 | 55 | $this->apiClient->createNetwork($providerId, $name, $this->determineRegion('Enter the name of the region where the network will be created', $providerId)); 56 | 57 | $this->output->infoWithDelayWarning('Network created'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Command/Network/DeleteNetworkCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Network; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Ymir\Cli\Command\AbstractCommand; 18 | 19 | class DeleteNetworkCommand extends AbstractCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'network:delete'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Delete a network') 36 | ->addArgument('network', InputArgument::OPTIONAL, 'The ID or name of the network to delete'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function perform() 43 | { 44 | $network = $this->determineNetwork('Which network would you like to delete'); 45 | 46 | if (!$this->output->confirm('Are you sure you want to delete this network?', false)) { 47 | return; 48 | } 49 | 50 | $this->apiClient->deleteNetwork($network); 51 | 52 | $this->output->infoWithDelayWarning('Network deleted'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Command/Network/ListNetworksCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Network; 15 | 16 | use Ymir\Cli\Command\AbstractCommand; 17 | 18 | class ListNetworksCommand extends AbstractCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'network:list'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('List the networks that belong to the currently active team'); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | protected function perform() 41 | { 42 | $networks = $this->apiClient->getNetworks($this->cliConfiguration->getActiveTeamId()); 43 | 44 | $this->output->table( 45 | ['Id', 'Name', 'Provider', 'Region', 'Status', 'NAT Gateway'], 46 | $networks->map(function (array $network) { 47 | return [$network['id'], $network['name'], $network['provider']['name'], $network['region'], $this->output->formatStatus($network['status']), $this->output->formatBoolean($network['has_nat_gateway'])]; 48 | })->all() 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Command/Network/RemoveBastionHostCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Network; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Ymir\Cli\Command\AbstractCommand; 18 | 19 | class RemoveBastionHostCommand extends AbstractCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'network:bastion:remove'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Remove bastion host from a network') 36 | ->addArgument('network', InputArgument::OPTIONAL, 'The ID or name of the network to remove the bastion host from'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function perform() 43 | { 44 | $this->apiClient->removeBastionHost($this->determineNetwork('Which network would like to remove the bastion host from')); 45 | 46 | $this->output->infoWithDelayWarning('Bastion host removed'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Command/Network/RemoveNatGatewayCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Network; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Ymir\Cli\Command\AbstractCommand; 18 | 19 | class RemoveNatGatewayCommand extends AbstractCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'network:nat:remove'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Remove a NAT gateway from a network\'s private subnet') 36 | ->addArgument('network', InputArgument::OPTIONAL, 'The ID or name of the network to remove the NAT gateway from'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function perform() 43 | { 44 | $this->apiClient->removeNatGateway($this->determineNetwork('Which network would like to remove the NAT gateway from')); 45 | 46 | $this->output->infoWithDelayWarning('NAT gateway removed'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Command/Php/PhpInfoCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Php; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Ymir\Cli\Command\AbstractInvocationCommand; 18 | 19 | class PhpInfoCommand extends AbstractInvocationCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'php:info'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Get information about PHP on the cloud provider') 36 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to get PHP information about.', 'staging'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function perform() 43 | { 44 | $environment = $this->input->getStringArgument('environment'); 45 | 46 | $this->output->info(sprintf('Getting information about PHP from the "%s" environment', $environment)); 47 | 48 | $result = $this->invokePhpCommand('--info', $environment); 49 | 50 | $this->output->newLine(); 51 | $this->output->write("{$result['output']}"); 52 | 53 | return $result['exitCode']; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Command/Php/PhpVersionCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Php; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Ymir\Cli\Command\AbstractInvocationCommand; 18 | 19 | class PhpVersionCommand extends AbstractInvocationCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'php:version'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Get the PHP version information on the cloud provider') 36 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to get the PHP version of.', 'staging'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function perform() 43 | { 44 | $environment = $this->input->getStringArgument('environment'); 45 | 46 | $this->output->info(sprintf('Getting PHP version information from the "%s" environment', $environment)); 47 | 48 | $result = $this->invokePhpCommand('--version', $environment); 49 | 50 | $this->output->newLine(); 51 | $this->output->write("{$result['output']}"); 52 | 53 | return $result['exitCode']; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Command/Project/DeleteProjectCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Project; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Ymir\Cli\Command\AbstractCommand; 18 | 19 | class DeleteProjectCommand extends AbstractCommand 20 | { 21 | /** 22 | * The alias of the command. 23 | * 24 | * @var string 25 | */ 26 | public const ALIAS = 'delete'; 27 | 28 | /** 29 | * The name of the command. 30 | * 31 | * @var string 32 | */ 33 | public const NAME = 'project:delete'; 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | protected function configure() 39 | { 40 | $this 41 | ->setName(self::NAME) 42 | ->setDescription('Delete a project') 43 | ->addArgument('project', InputArgument::OPTIONAL, 'The ID or name of the project to delete') 44 | ->setAliases([self::ALIAS]); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | protected function perform() 51 | { 52 | $projectId = $this->determineProject('Which project would you like to delete'); 53 | $project = $this->apiClient->getProject($projectId); 54 | 55 | if (!$this->output->confirm(sprintf('Are you sure you want to delete the %s project?', $project['name']), false)) { 56 | return; 57 | } 58 | 59 | $deleteResources = $this->output->confirm('Do you want to delete all the project resources on the cloud provider?', false); 60 | 61 | $this->apiClient->deleteProject($projectId, $deleteResources); 62 | 63 | if ($this->projectConfiguration->exists() && $projectId === $this->projectConfiguration->getProjectId()) { 64 | $this->projectConfiguration->delete(); 65 | } 66 | 67 | $message = 'Project deleted'; 68 | $deleteResources ? $this->output->infoWithDelayWarning($message) : $this->output->info($message); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Command/Project/GetProjectInfoCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Project; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Ymir\Cli\Command\AbstractCommand; 18 | use Ymir\Cli\Command\Environment\GetEnvironmentInfoCommand; 19 | 20 | class GetProjectInfoCommand extends AbstractCommand 21 | { 22 | /** 23 | * The alias of the command. 24 | * 25 | * @var string 26 | */ 27 | public const ALIAS = 'info'; 28 | 29 | /** 30 | * The name of the command. 31 | * 32 | * @var string 33 | */ 34 | public const NAME = 'project:info'; 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | protected function configure() 40 | { 41 | $this 42 | ->setName(self::NAME) 43 | ->setDescription('Get information on the project') 44 | ->addArgument('project', InputArgument::OPTIONAL, 'The ID or name of the project to fetch the information of') 45 | ->setAliases([self::ALIAS]); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | protected function perform() 52 | { 53 | $projectId = $this->projectConfiguration->exists() ? $this->projectConfiguration->getProjectId() : null; 54 | 55 | if (null === $projectId) { 56 | $projectId = $this->determineProject('Which project would you like to fetch the information on'); 57 | } 58 | 59 | $project = $this->apiClient->getProject($projectId); 60 | 61 | $this->output->horizontalTable( 62 | ['Name', 'Provider', 'Region'], 63 | [[$project['name'], $project['provider']['name'], $project['region']]] 64 | ); 65 | 66 | $this->invoke(GetEnvironmentInfoCommand::NAME); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Command/Project/ListProjectsCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Project; 15 | 16 | use Ymir\Cli\Command\AbstractCommand; 17 | 18 | class ListProjectsCommand extends AbstractCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'project:list'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('List the projects that belong to the currently active team'); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | protected function perform() 41 | { 42 | $projects = $this->apiClient->getProjects($this->cliConfiguration->getActiveTeamId()); 43 | 44 | $this->output->table( 45 | ['Id', 'Name', 'Provider', 'Region'], 46 | $projects->map(function (array $project) { 47 | return [$project['id'], $project['name'], $project['provider']['name'], $project['region']]; 48 | })->all() 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Command/Project/RedeployProjectCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Project; 15 | 16 | use Illuminate\Support\Collection; 17 | use Symfony\Component\Console\Exception\RuntimeException; 18 | use Symfony\Component\Console\Input\InputArgument; 19 | 20 | class RedeployProjectCommand extends AbstractProjectDeploymentCommand 21 | { 22 | /** 23 | * The alias of the command. 24 | * 25 | * @var string 26 | */ 27 | public const ALIAS = 'redeploy'; 28 | 29 | /** 30 | * The name of the command. 31 | * 32 | * @var string 33 | */ 34 | public const NAME = 'project:redeploy'; 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | protected function configure() 40 | { 41 | $this 42 | ->setName(self::NAME) 43 | ->setDescription('Redeploy project to an environment') 44 | ->setAliases([self::ALIAS]) 45 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to redeploy', 'staging'); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | protected function createDeployment(): Collection 52 | { 53 | $redeployment = $this->apiClient->createRedeployment($this->projectConfiguration->getProjectId(), $this->input->getStringArgument('environment')); 54 | 55 | if (!$redeployment->has('id')) { 56 | throw new RuntimeException('There was an error creating the redeployment'); 57 | } 58 | 59 | return $redeployment; 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | protected function getSuccessMessage(string $environment): string 66 | { 67 | return sprintf('Project redeployed successfully to "%s" environment', $environment); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Command/Provider/AbstractProviderCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Provider; 15 | 16 | use Ymir\Cli\ApiClient; 17 | use Ymir\Cli\CliConfiguration; 18 | use Ymir\Cli\Command\AbstractCommand; 19 | use Ymir\Cli\Project\Configuration\ProjectConfiguration; 20 | 21 | abstract class AbstractProviderCommand extends AbstractCommand 22 | { 23 | /** 24 | * The path to the user's home directory. 25 | * 26 | * @var string 27 | */ 28 | private $homeDirectory; 29 | 30 | /** 31 | * Constructor. 32 | */ 33 | public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, ProjectConfiguration $projectConfiguration, string $homeDirectory) 34 | { 35 | parent::__construct($apiClient, $cliConfiguration, $projectConfiguration); 36 | 37 | $this->homeDirectory = rtrim($homeDirectory, '/'); 38 | } 39 | 40 | /** 41 | * Get the AWS credentials. 42 | */ 43 | protected function getAwsCredentials(): array 44 | { 45 | $credentials = $this->getAwsCredentialsFromFile(); 46 | 47 | return !empty($credentials) ? $credentials : [ 48 | 'key' => $this->output->ask('Please enter your AWS user key'), 49 | 'secret' => $this->output->askHidden('Please enter your AWS user secret'), 50 | ]; 51 | } 52 | 53 | /** 54 | * Get the AWS credentials from the credentials file. 55 | */ 56 | private function getAwsCredentialsFromFile(): array 57 | { 58 | $credentialsFilePath = $this->homeDirectory.'/.aws/credentials'; 59 | 60 | if (!is_file($credentialsFilePath) 61 | || !$this->output->confirm('Would you like to import credentials from your AWS credentials file?') 62 | ) { 63 | return []; 64 | } 65 | 66 | $parsedCredentials = collect(parse_ini_file($credentialsFilePath, true)); 67 | 68 | if ($parsedCredentials->isEmpty()) { 69 | return []; 70 | } 71 | 72 | $credentials = $this->output->choice( 73 | 'Enter the name of the credentials to import from your AWS credentials file', 74 | $parsedCredentials->mapWithKeys(function ($credentials, $key) { 75 | return [$key => $key]; 76 | }) 77 | ); 78 | 79 | return [ 80 | 'key' => $parsedCredentials[$credentials]['aws_access_key_id'], 81 | 'secret' => $parsedCredentials[$credentials]['aws_secret_access_key'], 82 | ]; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Command/Provider/ConnectProviderCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Provider; 15 | 16 | class ConnectProviderCommand extends AbstractProviderCommand 17 | { 18 | /** 19 | * The name of the command. 20 | * 21 | * @var string 22 | */ 23 | public const NAME = 'provider:connect'; 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | protected function configure() 29 | { 30 | $this 31 | ->setName(self::NAME) 32 | ->setDescription('Connect a cloud provider to the currently active team'); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | protected function perform() 39 | { 40 | $name = $this->output->ask('Please enter a name for the cloud provider connection', 'AWS'); 41 | 42 | $credentials = $this->getAwsCredentials(); 43 | 44 | $this->apiClient->createProvider($this->cliConfiguration->getActiveTeamId(), $name, $credentials); 45 | 46 | $this->output->info('Cloud provider connected'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Command/Provider/DeleteProviderCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Provider; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | 18 | class DeleteProviderCommand extends AbstractProviderCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'provider:delete'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('Delete a cloud provider') 35 | ->addArgument('provider', InputArgument::REQUIRED, 'The ID of the cloud provider to delete'); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function perform() 42 | { 43 | if (!$this->output->confirm('Are you sure you want to delete this cloud provider? All resources associated to it will also be deleted on Ymir. They won\'t be deleted on your cloud provider.', false)) { 44 | return; 45 | } 46 | 47 | $this->apiClient->deleteProvider($this->input->getNumericArgument('provider')); 48 | 49 | $this->output->info('Cloud provider deleted'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Command/Provider/ListProvidersCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Provider; 15 | 16 | use Ymir\Cli\Command\AbstractCommand; 17 | 18 | class ListProvidersCommand extends AbstractCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'provider:list'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('List the cloud provider accounts connected to the currently active team'); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | protected function perform() 41 | { 42 | $providers = $this->apiClient->getProviders($this->cliConfiguration->getActiveTeamId()); 43 | 44 | $this->output->info('The following cloud providers are connect your team:'); 45 | 46 | $this->output->table( 47 | ['Id', 'Name'], 48 | $providers->map(function (array $provider) { 49 | return [ 50 | $provider['id'], 51 | $provider['name'], 52 | ]; 53 | })->all() 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Command/Provider/UpdateProviderCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Provider; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | 18 | class UpdateProviderCommand extends AbstractProviderCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'provider:update'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('Update a cloud provider') 35 | ->addArgument('provider', InputArgument::REQUIRED, 'The ID of the cloud provider to update'); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function perform() 42 | { 43 | $provider = $this->apiClient->getProvider($this->input->getNumericArgument('provider')); 44 | 45 | $name = (string) $this->output->ask('Please enter a name for the cloud provider connection', $provider->get('name')); 46 | 47 | $this->apiClient->updateProvider($provider->get('id'), $this->getAwsCredentials(), $name); 48 | 49 | $this->output->info('Cloud provider updated'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Command/Team/CreateTeamCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Team; 15 | 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Ymir\Cli\Command\AbstractCommand; 18 | 19 | class CreateTeamCommand extends AbstractCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'team:create'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Create a new team') 36 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the team'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function perform() 43 | { 44 | $name = $this->input->getStringArgument('name'); 45 | 46 | if (empty($name)) { 47 | $name = (string) $this->output->ask('What is the name of the team'); 48 | } 49 | 50 | $this->apiClient->createTeam($name); 51 | 52 | $this->output->info('Team created'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Command/Team/CurrentTeamCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Team; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Ymir\Cli\Command\AbstractCommand; 18 | 19 | class CurrentTeamCommand extends AbstractCommand 20 | { 21 | /** 22 | * The name of the command. 23 | * 24 | * @var string 25 | */ 26 | public const NAME = 'team:current'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | $this 34 | ->setName(self::NAME) 35 | ->setDescription('Get the details on your currently active team'); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function perform() 42 | { 43 | $team = $this->apiClient->getTeam($this->cliConfiguration->getActiveTeamId()); 44 | 45 | if (!isset($team['id'], $team['name'])) { 46 | throw new RuntimeException('Unable to get the details on your currently active team'); 47 | } 48 | 49 | $user = $this->apiClient->getAuthenticatedUser(); 50 | 51 | $this->output->info('Your currently active team is:'); 52 | $this->output->horizontalTable( 53 | ['Id', 'Name', 'Owner'], 54 | [$team->only(['id', 'name', 'owner'])->mapWithKeys(function ($value, $key) use ($user) { 55 | if ('owner' == $key && $value['id'] === $user['id']) { 56 | $value = 'You'; 57 | } elseif ('owner' == $key && $value['id'] !== $user['id']) { 58 | $value = $value['name']; 59 | } 60 | 61 | return [$key => $value]; 62 | })->all()] 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Command/Team/ListTeamsCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Team; 15 | 16 | use Ymir\Cli\Command\AbstractCommand; 17 | 18 | class ListTeamsCommand extends AbstractCommand 19 | { 20 | /** 21 | * The name of the command. 22 | * 23 | * @var string 24 | */ 25 | public const NAME = 'team:list'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('List all the teams that you\'re on'); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | protected function perform() 41 | { 42 | $this->output->info('You are on the following teams:'); 43 | 44 | $user = $this->apiClient->getAuthenticatedUser(); 45 | 46 | $this->output->table( 47 | ['Id', 'Name', 'Owner'], 48 | $this->apiClient->getTeams()->map(function (array $team) use ($user) { 49 | return [ 50 | $team['id'], 51 | $team['name'], 52 | $team['owner']['id'] === $user['id'] ? 'You' : $team['owner']['name'], 53 | ]; 54 | })->all() 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Command/Team/SelectTeamCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command\Team; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Ymir\Cli\Command\AbstractCommand; 19 | use Ymir\Cli\Support\Arr; 20 | 21 | class SelectTeamCommand extends AbstractCommand 22 | { 23 | /** 24 | * The name of the command. 25 | * 26 | * @var string 27 | */ 28 | public const NAME = 'team:select'; 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function configure() 34 | { 35 | $this 36 | ->setName(self::NAME) 37 | ->setDescription('Select a new currently active team') 38 | ->addArgument('team', InputArgument::OPTIONAL, 'The ID of the team to make your currently active team'); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function perform() 45 | { 46 | $teams = $this->apiClient->getTeams(); 47 | 48 | if ($teams->isEmpty()) { 49 | throw new RuntimeException('You\'re not on any team'); 50 | } 51 | 52 | $teamId = $this->input->getNumericArgument('team'); 53 | 54 | if (0 !== $teamId && !$teams->contains('id', $teamId)) { 55 | throw new RuntimeException(sprintf('You\'re not on a team with ID %s', $teamId)); 56 | } elseif (0 === $teamId) { 57 | $user = $this->apiClient->getAuthenticatedUser(); 58 | 59 | $teamId = $this->output->choiceWithId('Enter the ID of the team that you want to switch to', $teams->map(function (array $team) use ($user) { 60 | $owner = (string) Arr::get($team, 'owner.name'); 61 | 62 | if ($user['id'] === Arr::get($team, 'owner.id')) { 63 | $owner = 'You'; 64 | } 65 | 66 | return [ 67 | 'id' => $team['id'], 68 | 'name' => sprintf('%s (Owner: %s)', $team['name'], $owner), 69 | ]; 70 | })); 71 | } 72 | 73 | $this->cliConfiguration->setActiveTeamId($teamId); 74 | 75 | $this->output->infoWithValue('Your active team is now', $teams->firstWhere('id', $teamId)['name']); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Command/WpCliCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Command; 15 | 16 | use Symfony\Component\Console\Command\Command; 17 | use Symfony\Component\Console\Exception\RuntimeException; 18 | use Symfony\Component\Console\Input\InputArgument; 19 | use Symfony\Component\Console\Input\InputOption; 20 | 21 | class WpCliCommand extends AbstractInvocationCommand 22 | { 23 | /** 24 | * The name of the command. 25 | * 26 | * @var string 27 | */ 28 | public const NAME = 'wp'; 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function configure() 34 | { 35 | $this 36 | ->setName(self::NAME) 37 | ->setDescription('Execute a WP-CLI command') 38 | ->addArgument('wp-command', InputArgument::IS_ARRAY, 'The WP-CLI command to execute') 39 | ->addOption('environment', null, InputOption::VALUE_REQUIRED, 'The environment name', 'staging') 40 | ->addOption('async', null, InputOption::VALUE_NONE, 'Execute WP-CLI command asynchronously') 41 | ->addHiddenOption('yolo', null, InputOption::VALUE_NONE); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | protected function perform() 48 | { 49 | $async = $this->input->getBooleanOption('async') || $this->input->getBooleanOption('yolo'); 50 | $command = implode(' ', $this->input->getArrayArgument('wp-command')); 51 | $environment = (string) $this->input->getStringOption('environment'); 52 | $exitCode = Command::SUCCESS; 53 | 54 | if (empty($command)) { 55 | $command = $this->output->ask('Please enter the WP-CLI command to run'); 56 | } 57 | 58 | if (str_starts_with($command, 'wp ')) { 59 | $command = substr($command, 3); 60 | } 61 | 62 | if (in_array($command, ['shell'])) { 63 | throw new RuntimeException(sprintf('The "wp %s" command isn\'t available remotely', $command)); 64 | } elseif (in_array($command, ['db import', 'db export'])) { 65 | throw new RuntimeException(sprintf('Please use the "ymir database:%s" command instead of the "wp %s" command', substr($command, 3), $command)); 66 | } 67 | 68 | $this->output->info(sprintf('Running "wp %s" %s "%s" environment', $command, $async ? 'asynchronously on' : 'on', $environment)); 69 | 70 | $result = $this->invokeWpCliCommand($command, $environment, $async ? 0 : null); 71 | 72 | if (!$async) { 73 | $this->output->newLine(); 74 | $this->output->write("{$result['output']}"); 75 | 76 | $exitCode = $result['exitCode']; 77 | } 78 | 79 | return $exitCode; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Console/ChoiceQuestion.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Console; 15 | 16 | use Symfony\Component\Console\Question\ChoiceQuestion as SymfonyChoiceQuestion; 17 | 18 | class ChoiceQuestion extends SymfonyChoiceQuestion 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | protected function isAssoc($array) 24 | { 25 | return !isset($array[0]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Console/HiddenInputOption.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Console; 15 | 16 | use Symfony\Component\Console\Input\InputOption; 17 | 18 | class HiddenInputOption extends InputOption 19 | { 20 | /** 21 | * Constructor. 22 | */ 23 | public function __construct(string $name, $shortcut = null, ?int $mode = null, $default = null) 24 | { 25 | parent::__construct($name, $shortcut, $mode, '', $default); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Console/InputDefinition.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Console; 15 | 16 | use Symfony\Component\Console\Input\InputDefinition as SymfonyInputDefinition; 17 | use Symfony\Component\Console\Input\InputOption; 18 | 19 | class InputDefinition extends SymfonyInputDefinition 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function getOptions(): array 25 | { 26 | return array_filter(parent::getOptions(), function (InputOption $option) { 27 | return !$option instanceof HiddenInputOption; 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Database/Connection.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Database; 15 | 16 | class Connection 17 | { 18 | /** 19 | * The database the connection is for. 20 | * 21 | * @var string 22 | */ 23 | private $database; 24 | 25 | /** 26 | * The database server the connection is for. 27 | * 28 | * @var array 29 | */ 30 | private $databaseServer; 31 | 32 | /** 33 | * The password the connection is for. 34 | * 35 | * @var string 36 | */ 37 | private $password; 38 | 39 | /** 40 | * The user the connection is for. 41 | * 42 | * @var string 43 | */ 44 | private $user; 45 | 46 | /** 47 | * Constructor. 48 | */ 49 | public function __construct(string $database, array $databaseServer, string $user, string $password) 50 | { 51 | $this->database = $database; 52 | $this->databaseServer = $databaseServer; 53 | $this->user = $user; 54 | $this->password = $password; 55 | } 56 | 57 | public function getDatabase(): string 58 | { 59 | return $this->database; 60 | } 61 | 62 | public function getDatabaseServer(): array 63 | { 64 | return $this->databaseServer; 65 | } 66 | 67 | public function getDsn(): string 68 | { 69 | return sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $this->getHost(), $this->getPort(), $this->getDatabase()); 70 | } 71 | 72 | public function getHost(): string 73 | { 74 | return $this->databaseServer['publicly_accessible'] ? $this->databaseServer['endpoint'] : '127.0.0.1'; 75 | } 76 | 77 | public function getPassword(): string 78 | { 79 | return $this->password; 80 | } 81 | 82 | public function getPort(): string 83 | { 84 | return $this->databaseServer['publicly_accessible'] ? '3306' : '3305'; 85 | } 86 | 87 | public function getUser(): string 88 | { 89 | return $this->user; 90 | } 91 | 92 | /** 93 | * Checks if the connection needs an SSH tunnel to connect to the database server. 94 | */ 95 | public function needsSshTunnel(): bool 96 | { 97 | return !$this->databaseServer['publicly_accessible']; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Database/Mysqldump.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Database; 15 | 16 | use Ifsnop\Mysqldump\Mysqldump as BaseMysqldump; 17 | 18 | class Mysqldump extends BaseMysqldump 19 | { 20 | private const DEFAULT_OPTIONS = [ 21 | 'add-drop-table' => true, 22 | 'default-character-set' => 'utf8mb4', 23 | ]; 24 | 25 | /** 26 | * Create a new Mysqldump object from a Connection object. 27 | */ 28 | public static function fromConnection(Connection $connection, array $options = []): self 29 | { 30 | return new self($connection->getDsn(), $connection->getUser(), $connection->getPassword(), array_merge(self::DEFAULT_OPTIONS, $options)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Database/PDO.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Database; 15 | 16 | use PDO as BasePDO; 17 | 18 | class PDO extends BasePDO 19 | { 20 | /** 21 | * Default PDO options. 22 | * 23 | * @var array 24 | */ 25 | private const DEFAULT_OPTIONS = [ 26 | self::ATTR_ERRMODE => self::ERRMODE_EXCEPTION, 27 | self::ATTR_PERSISTENT => true, 28 | ]; 29 | 30 | /** 31 | * Create a new PDO object from a Connection object. 32 | */ 33 | public static function fromConnection(Connection $connection, array $options = []): self 34 | { 35 | return new self($connection->getDsn(), $connection->getUser(), $connection->getPassword(), array_merge(self::DEFAULT_OPTIONS, $options)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Deployment/DeploymentStepInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Deployment; 15 | 16 | use Illuminate\Support\Collection; 17 | use Ymir\Cli\Console\Input; 18 | use Ymir\Cli\Console\Output; 19 | 20 | interface DeploymentStepInterface 21 | { 22 | /** 23 | * Perform the deployment step and generate the console output. 24 | */ 25 | public function perform(Collection $deployment, string $environment, Input $input, Output $output); 26 | } 27 | -------------------------------------------------------------------------------- /src/EventDispatcher/AutowiredEventDispatcher.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\EventDispatcher; 15 | 16 | use Symfony\Component\EventDispatcher\EventDispatcher; 17 | 18 | class AutowiredEventDispatcher extends EventDispatcher 19 | { 20 | /** 21 | * Constructor. 22 | */ 23 | public function __construct(iterable $eventSubscribers = []) 24 | { 25 | parent::__construct(); 26 | 27 | foreach ($eventSubscribers as $eventSubscriber) { 28 | $this->addSubscriber($eventSubscriber); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/EventListener/CleanupSubscriber.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\EventListener; 15 | 16 | use Symfony\Component\Console\ConsoleEvents; 17 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 18 | use Symfony\Component\Filesystem\Filesystem; 19 | 20 | /** 21 | * Event subscriber that cleans up project folder when command terminates. 22 | */ 23 | class CleanupSubscriber implements EventSubscriberInterface 24 | { 25 | /** 26 | * The file system. 27 | * 28 | * @var Filesystem 29 | */ 30 | private $filesystem; 31 | 32 | /** 33 | * The hidden directory used by Ymir. 34 | * 35 | * @var string 36 | */ 37 | private $hiddenDirectory; 38 | 39 | /** 40 | * Constructor. 41 | */ 42 | public function __construct(Filesystem $filesystem, string $hiddenDirectory) 43 | { 44 | $this->filesystem = $filesystem; 45 | $this->hiddenDirectory = rtrim($hiddenDirectory, '/'); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public static function getSubscribedEvents() 52 | { 53 | return [ 54 | ConsoleEvents::TERMINATE => 'onConsoleTerminate', 55 | ]; 56 | } 57 | 58 | /** 59 | * Remove hidden directory when console terminates. 60 | */ 61 | public function onConsoleTerminate() 62 | { 63 | $this->filesystem->remove($this->hiddenDirectory); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/EventListener/LoadProjectConfigurationSubscriber.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\EventListener; 15 | 16 | use Symfony\Component\Console\ConsoleEvents; 17 | use Symfony\Component\Console\Event\ConsoleCommandEvent; 18 | use Symfony\Component\Console\Exception\RuntimeException; 19 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 20 | use Ymir\Cli\Project\Configuration\ProjectConfiguration; 21 | 22 | class LoadProjectConfigurationSubscriber implements EventSubscriberInterface 23 | { 24 | /** 25 | * The Ymir project configuration. 26 | * 27 | * @var ProjectConfiguration 28 | */ 29 | private $projectConfiguration; 30 | 31 | /** 32 | * Constructor. 33 | */ 34 | public function __construct(ProjectConfiguration $projectConfiguration) 35 | { 36 | $this->projectConfiguration = $projectConfiguration; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public static function getSubscribedEvents() 43 | { 44 | return [ 45 | ConsoleEvents::COMMAND => 'onConsoleCommand', 46 | ]; 47 | } 48 | 49 | /** 50 | * Load the Ymir project configuration. 51 | */ 52 | public function onConsoleCommand(ConsoleCommandEvent $event) 53 | { 54 | $configurationFilePath = $event->getInput()->getOption('ymir-file'); 55 | 56 | if (!is_string($configurationFilePath)) { 57 | throw new RuntimeException('The "--ymir-file" option must be a string value'); 58 | } 59 | 60 | $this->projectConfiguration->loadConfiguration($configurationFilePath); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Exception/CommandCancelledException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Exception; 15 | 16 | class CommandCancelledException extends \RuntimeException 17 | { 18 | /** 19 | * Constructor. 20 | */ 21 | public function __construct(string $message = '') 22 | { 23 | parent::__construct($message, 130); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Exception/Executable/ExecutableNotDetectedException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Exception\Executable; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Ymir\Cli\Executable\ExecutableInterface; 18 | 19 | class ExecutableNotDetectedException extends RuntimeException 20 | { 21 | /** 22 | * Constructor. 23 | */ 24 | public function __construct(ExecutableInterface $executable) 25 | { 26 | parent::__construct(sprintf('Cannot detect %1$s on this computer. Please ensure %1$s is installed and properly configured.', $executable->getDisplayName())); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Exception/Executable/SshPortInUseException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Exception\Executable; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | 18 | class SshPortInUseException extends RuntimeException 19 | { 20 | /** 21 | * Constructor. 22 | */ 23 | public function __construct(int $port) 24 | { 25 | parent::__construct(sprintf('Unable to open SSH tunnel. Local port "%s" is already in use.', $port)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Exception/Executable/WpCliException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Exception\Executable; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | 18 | class WpCliException extends RuntimeException 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/InvalidInputException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Exception; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | 18 | class InvalidInputException extends RuntimeException 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/NonInteractiveRequiredArgumentException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Exception; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | 18 | class NonInteractiveRequiredArgumentException extends RuntimeException 19 | { 20 | /** 21 | * Constructor. 22 | */ 23 | public function __construct(string $argument) 24 | { 25 | parent::__construct(sprintf('You must pass a "%s" argument when running in non-interactive mode', $argument)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Exception/NonInteractiveRequiredOptionException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Exception; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | 18 | class NonInteractiveRequiredOptionException extends RuntimeException 19 | { 20 | /** 21 | * Constructor. 22 | */ 23 | public function __construct(string $option) 24 | { 25 | parent::__construct(sprintf('You must use the "--%s" option when running in non-interactive mode', $option)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Executable/AbstractExecutable.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Executable; 15 | 16 | use Ymir\Cli\Exception\Executable\ExecutableNotDetectedException; 17 | use Ymir\Cli\Process\Process; 18 | 19 | abstract class AbstractExecutable implements ExecutableInterface 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function isInstalled(): bool 25 | { 26 | return $this->isExecutableInstalled($this->getExecutable()); 27 | } 28 | 29 | /** 30 | * Get an unstarted Process object to run the executable with the given command. 31 | */ 32 | protected function getProcess(string $command, ?string $cwd = null, ?float $timeout = 60): Process 33 | { 34 | if (!$this->isInstalled()) { 35 | throw new ExecutableNotDetectedException($this); 36 | } 37 | 38 | return Process::fromShellCommandline(sprintf('%s %s', $this->getExecutable(), $command), $cwd, null, null, $timeout); 39 | } 40 | 41 | /** 42 | * Check if the given executable is installed. 43 | */ 44 | protected function isExecutableInstalled(string $executable): bool 45 | { 46 | return 0 === Process::fromShellCommandline(sprintf('which %s', $executable))->run(); 47 | } 48 | 49 | /** 50 | * Run the executable with the given command and return the Process object used to run it. 51 | */ 52 | protected function run(string $command, ?string $cwd = null, ?float $timeout = 60): Process 53 | { 54 | if (!$this->isInstalled()) { 55 | throw new ExecutableNotDetectedException($this); 56 | } 57 | 58 | return Process::runShellCommandline(sprintf('%s %s', $this->getExecutable(), $command), $cwd, null, null, $timeout); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Executable/ComposerExecutable.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Executable; 15 | 16 | class ComposerExecutable extends AbstractExecutable 17 | { 18 | /** 19 | * Create a new project from the given package into the given directory. 20 | */ 21 | public function createProject(string $package, string $directory = '.') 22 | { 23 | $this->run(sprintf('create-project %s %s', $package, $directory)); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getDisplayName(): string 30 | { 31 | return 'Composer'; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getExecutable(): string 38 | { 39 | return 'composer'; 40 | } 41 | 42 | /** 43 | * Check if the given package is installed. 44 | */ 45 | public function isPackageInstalled(string $package, ?string $cwd = null): bool 46 | { 47 | try { 48 | $this->run(sprintf('show %s', $package), $cwd); 49 | 50 | return true; 51 | } catch (\Throwable $exception) { 52 | return false; 53 | } 54 | } 55 | 56 | /** 57 | * Add the given package to the project's "composer.json" file and install it. 58 | */ 59 | public function require(string $package, ?string $cwd = null) 60 | { 61 | $this->run(sprintf('require %s', $package), $cwd); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Executable/DockerExecutable.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Executable; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | 18 | class DockerExecutable extends AbstractExecutable 19 | { 20 | /** 21 | * Build a docker image. 22 | */ 23 | public function build(string $file, string $tag, ?string $cwd = null) 24 | { 25 | $this->run(sprintf('build --pull --file=%s --tag=%s .', $file, $tag), $cwd, null); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function getDisplayName(): string 32 | { 33 | return 'Docker'; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function getExecutable(): string 40 | { 41 | static $executable; 42 | 43 | if (!is_string($executable)) { 44 | $executable = $this->isExecutableInstalled('podman') ? 'podman' : 'docker'; 45 | } 46 | 47 | return $executable; 48 | } 49 | 50 | /** 51 | * Login to a Docker registry. 52 | */ 53 | public function login(string $username, string $password, string $server, ?string $cwd = null) 54 | { 55 | $this->run(sprintf('login --username %s --password %s %s', $username, $password, $server), $cwd); 56 | } 57 | 58 | /** 59 | * Push a docker image. 60 | */ 61 | public function push(string $image, ?string $cwd = null) 62 | { 63 | $this->run(sprintf('push %s', $image), $cwd, null); 64 | } 65 | 66 | /** 67 | * Remove all images matching grep pattern. 68 | */ 69 | public function removeImagesMatchingPattern(string $pattern, ?string $cwd = null) 70 | { 71 | try { 72 | $this->run(sprintf('rmi -f $(docker images | grep \'%s\')', $pattern), $cwd); 73 | } catch (RuntimeException $exception) { 74 | $throwException = collect([ 75 | '"docker rmi" requires at least 1 argument', 76 | 'Error: No such image', 77 | ])->doesntContain(function (string $ignore) use ($exception) { 78 | return false === stripos($exception->getMessage(), $ignore); 79 | }); 80 | 81 | if ($throwException) { 82 | throw $exception; 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * Create a docker image tag. 89 | */ 90 | public function tag(string $sourceImage, string $targetImage, ?string $cwd = null) 91 | { 92 | $this->run(sprintf('tag %s %s', $sourceImage, $targetImage), $cwd); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Executable/ExecutableInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Executable; 15 | 16 | interface ExecutableInterface 17 | { 18 | /** 19 | * Get the human-readable name for this executable. 20 | */ 21 | public function getDisplayName(): string; 22 | 23 | /** 24 | * Get the actual binary name or command string used to invoke this executable. 25 | */ 26 | public function getExecutable(): string; 27 | 28 | /** 29 | * Determines if this command-line executable is installed and accessible globally. 30 | */ 31 | public function isInstalled(): bool; 32 | } 33 | -------------------------------------------------------------------------------- /src/Executable/SshExecutable.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Executable; 15 | 16 | use Symfony\Component\Console\Exception\InvalidArgumentException; 17 | use Symfony\Component\Filesystem\Filesystem; 18 | use Ymir\Cli\Exception\Executable\SshPortInUseException; 19 | use Ymir\Cli\Process\Process; 20 | 21 | class SshExecutable extends AbstractExecutable 22 | { 23 | /** 24 | * The file system. 25 | * 26 | * @var Filesystem 27 | */ 28 | private $filesystem; 29 | 30 | /** 31 | * The SSH directory. 32 | * 33 | * @var string 34 | */ 35 | private $sshDirectory; 36 | 37 | /** 38 | * Constructor. 39 | */ 40 | public function __construct(Filesystem $filesystem, ?string $sshDirectory = null) 41 | { 42 | $this->filesystem = $filesystem; 43 | $this->sshDirectory = $sshDirectory ?? rtrim((string) getenv('HOME'), '/').'/.ssh'; 44 | } 45 | 46 | public function getDisplayName(): string 47 | { 48 | return 'SSH'; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function getExecutable(): string 55 | { 56 | return 'ssh'; 57 | } 58 | 59 | /** 60 | * Opens an SSH tunnel to a bastion host and returns the running tunnel process. 61 | */ 62 | public function openTunnelToBastionHost(array $bastionHost, int $localPort, string $remoteHost, int $remotePort, ?string $cwd = null): Process 63 | { 64 | if (!isset($bastionHost['endpoint'], $bastionHost['private_key'])) { 65 | throw new InvalidArgumentException('Bastion host configuration must contain an "endpoint" and a "private_key"'); 66 | } 67 | 68 | if (!is_dir($this->sshDirectory)) { 69 | $this->filesystem->mkdir($this->sshDirectory, 0700); 70 | } 71 | 72 | $identityFilePath = $this->sshDirectory.'/ymir-tunnel'; 73 | 74 | $this->filesystem->dumpFile($identityFilePath, $bastionHost['private_key']); 75 | $this->filesystem->chmod($identityFilePath, 0600); 76 | 77 | $process = $this->getProcess(sprintf('ec2-user@%s -i %s -o LogLevel=debug -L %s:%s:%s -N', $bastionHost['endpoint'], $identityFilePath, $localPort, $remoteHost, $remotePort), $cwd, null); 78 | $process->start(function ($type, $buffer) use ($localPort) { 79 | if (Process::ERR === $type && false !== stripos($buffer, sprintf('%s: address already in use', $localPort))) { 80 | throw new SshPortInUseException($localPort); 81 | } 82 | }); 83 | 84 | return $process; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Executable/WpCliExecutable.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Executable; 15 | 16 | use Illuminate\Support\Collection; 17 | use Symfony\Component\Console\Exception\RuntimeException; 18 | use Ymir\Cli\Exception\Executable\WpCliException; 19 | use Ymir\Cli\Process\Process; 20 | 21 | class WpCliExecutable extends AbstractExecutable 22 | { 23 | /** 24 | * Download WordPress. 25 | */ 26 | public function downloadWordPress(?string $cwd = null) 27 | { 28 | $this->run('core download', $cwd); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function getDisplayName(): string 35 | { 36 | return 'WP-CLI'; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function getExecutable(): string 43 | { 44 | return 'wp'; 45 | } 46 | 47 | /** 48 | * Get the WordPress version. 49 | */ 50 | public function getVersion(?string $cwd = null): ?string 51 | { 52 | try { 53 | return trim($this->run('core version', $cwd)->getOutput()); 54 | } catch (WpCliException $exception) { 55 | return null; 56 | } 57 | } 58 | 59 | /** 60 | * Checks if WordPress is installed. 61 | */ 62 | public function isWordPressInstalled(?string $cwd = null): bool 63 | { 64 | try { 65 | $this->run('core is-installed', $cwd); 66 | 67 | return true; 68 | } catch (WpCliException $exception) { 69 | return false; 70 | } 71 | } 72 | 73 | /** 74 | * List all the installed plugins. 75 | */ 76 | public function listPlugins(?string $cwd = null): Collection 77 | { 78 | $process = $this->run('plugin list --fields=file,name,status,title,version --format=json', $cwd); 79 | 80 | $plugins = json_decode($process->getOutput(), true); 81 | 82 | if (JSON_ERROR_NONE !== json_last_error()) { 83 | throw new RuntimeException('Unable to get the list of installed plugins'); 84 | } 85 | 86 | return collect($plugins); 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | protected function run(string $command, ?string $cwd = null, ?float $timeout = 60): Process 93 | { 94 | if (function_exists('posix_geteuid') && 0 === posix_geteuid()) { 95 | throw new WpCliException('WP-CLI commands can only be run as a non-root user'); 96 | } 97 | 98 | try { 99 | return parent::run($command, $cwd, $timeout); 100 | } catch (RuntimeException $exception) { 101 | throw new WpCliException($exception->getMessage(), $exception->getCode(), $exception); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/GitHubClient.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli; 15 | 16 | use GuzzleHttp\ClientInterface; 17 | use Illuminate\Support\Collection; 18 | use Symfony\Component\Console\Exception\RuntimeException; 19 | 20 | class GitHubClient 21 | { 22 | /** 23 | * The HTTP client used to interact with the GitHub API. 24 | * 25 | * @var ClientInterface 26 | */ 27 | private $client; 28 | 29 | /** 30 | * Constructor. 31 | */ 32 | public function __construct(ClientInterface $client) 33 | { 34 | $this->client = $client; 35 | } 36 | 37 | /** 38 | * Download the latest version of a repository from GitHub and return the Zip archive. 39 | */ 40 | public function downloadLatestVersion(string $repository): \ZipArchive 41 | { 42 | $latestTag = $this->getTags($repository)->first(); 43 | 44 | if (empty($latestTag['zipball_url'])) { 45 | throw new RuntimeException('Unable to parse the WordPress plugin versions from the GitHub API'); 46 | } 47 | 48 | $downloadedZipFile = tmpfile(); 49 | 50 | if (!is_resource($downloadedZipFile)) { 51 | throw new RuntimeException('Unable to open a temporary file'); 52 | } 53 | 54 | fwrite($downloadedZipFile, (string) $this->client->request('GET', $latestTag['zipball_url'])->getBody()); 55 | 56 | $downloadedZipArchive = new \ZipArchive(); 57 | 58 | if (true !== $downloadedZipArchive->open(stream_get_meta_data($downloadedZipFile)['uri'])) { 59 | throw new RuntimeException(sprintf('Unable to open the "%s" repository Zip archive from GitHub', $repository)); 60 | } 61 | 62 | return $downloadedZipArchive; 63 | } 64 | 65 | /** 66 | * Get the tags for the given repository. 67 | */ 68 | public function getTags(string $repository): Collection 69 | { 70 | $response = $this->client->request('GET', sprintf('https://api.github.com/repos/%s/tags', $repository)); 71 | 72 | if (200 !== $response->getStatusCode()) { 73 | throw new RuntimeException(sprintf('Unable to get the tags for the "%s" repository from the GitHub API', $repository)); 74 | } 75 | 76 | $tags = json_decode((string) $response->getBody(), true); 77 | 78 | if (JSON_ERROR_NONE !== json_last_error()) { 79 | throw new RuntimeException(sprintf('Failed to decode response from the GitHub API: %s.', json_last_error_msg())); 80 | } 81 | 82 | return collect($tags); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Process/Process.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Process; 15 | 16 | use Symfony\Component\Console\Exception\RuntimeException; 17 | use Symfony\Component\Process\Process as SymfonyProcess; 18 | 19 | class Process extends SymfonyProcess 20 | { 21 | /** 22 | * Run a command-line in a shell wrapper. 23 | */ 24 | public static function runShellCommandline(string $command, ?string $cwd = null, ?array $env = null, $input = null, ?float $timeout = 60): self 25 | { 26 | $process = self::fromShellCommandline($command, $cwd, $env, $input, $timeout); 27 | $process->run(); 28 | 29 | if (!$process->isSuccessful()) { 30 | throw new RuntimeException($process->getErrorOutput() ?: $process->getOutput()); 31 | } 32 | 33 | return $process; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Project/Configuration/CacheConfigurationChange.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Project\Configuration; 15 | 16 | use Ymir\Cli\Project\Type\ProjectTypeInterface; 17 | 18 | class CacheConfigurationChange implements ConfigurationChangeInterface 19 | { 20 | /** 21 | * The name of the cache to add to the configuration. 22 | * 23 | * @var string 24 | */ 25 | private $cache; 26 | 27 | /** 28 | * Constructor. 29 | */ 30 | public function __construct(string $cache) 31 | { 32 | $this->cache = $cache; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function apply(array $options, ProjectTypeInterface $projectType): array 39 | { 40 | return array_merge($options, ['cache' => $this->cache]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Project/Configuration/ConfigurationChangeInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Project\Configuration; 15 | 16 | use Ymir\Cli\Project\Type\ProjectTypeInterface; 17 | 18 | interface ConfigurationChangeInterface 19 | { 20 | /** 21 | * Apply the configuration changes. 22 | */ 23 | public function apply(array $options, ProjectTypeInterface $projectType): array; 24 | } 25 | -------------------------------------------------------------------------------- /src/Project/Configuration/DomainConfigurationChange.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Project\Configuration; 15 | 16 | use Ymir\Cli\Project\Type\ProjectTypeInterface; 17 | 18 | class DomainConfigurationChange implements ConfigurationChangeInterface 19 | { 20 | /** 21 | * The domain to add to the configuration. 22 | * 23 | * @var string 24 | */ 25 | private $domain; 26 | 27 | /** 28 | * Constructor. 29 | */ 30 | public function __construct(string $domain) 31 | { 32 | $this->domain = $domain; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function apply(array $options, ProjectTypeInterface $projectType): array 39 | { 40 | if (empty($options['domain'])) { 41 | $options['domain'] = $this->domain; 42 | } elseif (is_array($options['domain']) || (is_string($options['domain']) && strtolower($options['domain']) !== strtolower($this->domain))) { 43 | $options['domain'] = collect((array) $this->domain)->merge($options['domain'])->unique()->values()->all(); 44 | } 45 | 46 | return $options; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Project/Configuration/ImageDeploymentConfigurationChange.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Project\Configuration; 15 | 16 | use Ymir\Cli\Project\Type\ProjectTypeInterface; 17 | 18 | class ImageDeploymentConfigurationChange implements ConfigurationChangeInterface 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function apply(array $options, ProjectTypeInterface $projectType): array 24 | { 25 | if (isset($options['php'])) { 26 | unset($options['php']); 27 | } 28 | 29 | return array_merge($options, ['deployment' => 'image']); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Project/Configuration/WordPress/AbstractWordPressConfigurationChange.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Project\Configuration\WordPress; 15 | 16 | use Symfony\Component\Console\Exception\InvalidArgumentException; 17 | use Ymir\Cli\Project\Type\AbstractWordPressProjectType; 18 | use Ymir\Cli\Project\Type\ProjectTypeInterface; 19 | use Ymir\Cli\Support\Arr; 20 | 21 | abstract class AbstractWordPressConfigurationChange implements WordPressConfigurationChangeInterface 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function apply(array $options, ProjectTypeInterface $projectType): array 27 | { 28 | if (!$projectType instanceof AbstractWordPressProjectType) { 29 | throw new InvalidArgumentException('Can only apply these configuration changes to WordPress projects'); 30 | } 31 | 32 | $buildIncludePaths = $this->getBuildIncludePaths($projectType); 33 | $optionsToMerge = $this->getOptionsToMerge(); 34 | 35 | if ('image' !== Arr::get($options, 'deployment') && !empty($buildIncludePaths)) { 36 | Arr::set($optionsToMerge, 'build.include', $buildIncludePaths); 37 | } 38 | 39 | return Arr::sortRecursive(Arr::uniqueRecursive(array_merge_recursive($options, $optionsToMerge))); 40 | } 41 | 42 | /** 43 | * Get the base path to use with build include option based on the project type. 44 | */ 45 | protected function getBaseIncludePath(AbstractWordPressProjectType $projectType): string 46 | { 47 | return $projectType->getPluginsDirectoryPath().'/'.$this->getName(); 48 | } 49 | 50 | /** 51 | * Get the build include paths to merge into the options when using zip archive deployment. 52 | */ 53 | protected function getBuildIncludePaths(AbstractWordPressProjectType $projectType): array 54 | { 55 | return []; 56 | } 57 | 58 | /** 59 | * Get the options to merge into the project configuration. 60 | */ 61 | protected function getOptionsToMerge(): array 62 | { 63 | return []; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Project/Configuration/WordPress/BeaverBuilderConfigurationChange.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Project\Configuration\WordPress; 15 | 16 | use Ymir\Cli\Project\Type\AbstractWordPressProjectType; 17 | 18 | class BeaverBuilderConfigurationChange extends AbstractWordPressConfigurationChange 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function getName(): string 24 | { 25 | return 'bb-plugin'; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function getBuildIncludePaths(AbstractWordPressProjectType $projectType): array 32 | { 33 | $basePath = $this->getBaseIncludePath($projectType); 34 | 35 | return [ 36 | $basePath.'/fonts', 37 | $basePath.'/img', 38 | $basePath.'/js', 39 | $basePath.'/json', 40 | ]; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | protected function getOptionsToMerge(): array 47 | { 48 | return [ 49 | 'cdn' => [ 50 | 'excluded_paths' => ['/uploads/bb-plugin/*'], 51 | ], 52 | ]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Project/Configuration/WordPress/CloudflareConfigurationChange.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Project\Configuration\WordPress; 15 | 16 | use Ymir\Cli\Project\Type\AbstractWordPressProjectType; 17 | 18 | class CloudflareConfigurationChange extends AbstractWordPressConfigurationChange 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function getName(): string 24 | { 25 | return 'cloudflare'; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function getBuildIncludePaths(AbstractWordPressProjectType $projectType): array 32 | { 33 | return [ 34 | $this->getBaseIncludePath($projectType).'/config.json', 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Project/Configuration/WordPress/ElementorConfigurationChange.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Project\Configuration\WordPress; 15 | 16 | class ElementorConfigurationChange extends AbstractWordPressConfigurationChange 17 | { 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function getName(): string 22 | { 23 | return 'elementor'; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | protected function getOptionsToMerge(): array 30 | { 31 | return [ 32 | 'cdn' => [ 33 | 'excluded_paths' => ['/uploads/elementor/*'], 34 | ], 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Project/Configuration/WordPress/OxygenConfigurationChange.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Project\Configuration\WordPress; 15 | 16 | use Ymir\Cli\Project\Type\AbstractWordPressProjectType; 17 | 18 | class OxygenConfigurationChange extends AbstractWordPressConfigurationChange 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function getName(): string 24 | { 25 | return 'oxygen'; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function getBuildIncludePaths(AbstractWordPressProjectType $projectType): array 32 | { 33 | return [ 34 | $this->getBaseIncludePath($projectType), 35 | ]; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function getOptionsToMerge(): array 42 | { 43 | return [ 44 | 'cdn' => [ 45 | 'excluded_paths' => ['/uploads/oxygen/*'], 46 | ], 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Project/Configuration/WordPress/WooCommerceConfigurationChange.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Project\Configuration\WordPress; 15 | 16 | use Ymir\Cli\Project\Type\AbstractWordPressProjectType; 17 | 18 | class WooCommerceConfigurationChange extends AbstractWordPressConfigurationChange 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function getName(): string 24 | { 25 | return 'woocommerce'; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function getBuildIncludePaths(AbstractWordPressProjectType $projectType): array 32 | { 33 | return [ 34 | $this->getBaseIncludePath($projectType), 35 | ]; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function getOptionsToMerge(): array 42 | { 43 | return [ 44 | 'cdn' => [ 45 | 'cookies_whitelist' => ['woocommerce_cart_hash', 'woocommerce_items_in_cart', 'woocommerce_recently_viewed', 'wp_woocommerce_session_*'], 46 | 'excluded_paths' => ['/addons', '/cart', '/checkout', '/my-account'], 47 | 'forwarded_headers' => ['authorization', 'origin', 'x-http-method-override', 'x-wp-nonce'], 48 | ], 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Project/Configuration/WordPress/WordPressConfigurationChangeInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Project\Configuration\WordPress; 15 | 16 | use Ymir\Cli\Project\Configuration\ConfigurationChangeInterface; 17 | 18 | interface WordPressConfigurationChangeInterface extends ConfigurationChangeInterface 19 | { 20 | /** 21 | * Get the name of the plugin or theme that this configuration change applies to. 22 | */ 23 | public function getName(): string; 24 | } 25 | -------------------------------------------------------------------------------- /src/Project/Type/InstallableProjectTypeInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Project\Type; 15 | 16 | interface InstallableProjectTypeInterface extends ProjectTypeInterface 17 | { 18 | /** 19 | * Get the message to display to the user when installing the project. 20 | */ 21 | public function getInstallationMessage(): string; 22 | 23 | /** 24 | * Install the project in the given directory. 25 | */ 26 | public function installProject(string $directory); 27 | 28 | /** 29 | * Determines if the project is eligible for installation in the given directory. 30 | * 31 | * Returns true when the project isn't already installed in the given directory and all prerequisites for 32 | * installation are met. 33 | */ 34 | public function isEligibleForInstallation(string $directory): bool; 35 | } 36 | -------------------------------------------------------------------------------- /src/Project/Type/ProjectTypeInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Project\Type; 15 | 16 | use Symfony\Component\Finder\Finder; 17 | 18 | interface ProjectTypeInterface 19 | { 20 | /** 21 | * Get the Finder object for finding all the asset files that we have to extract in the given project directory. 22 | */ 23 | public function getAssetFiles(string $projectDirectory): Finder; 24 | 25 | /** 26 | * Get the Finder object for finding all the files necessary for a build in the given project directory. 27 | */ 28 | public function getBuildFiles(string $projectDirectory): Finder; 29 | 30 | /** 31 | * Get the build steps for the project type. 32 | */ 33 | public function getBuildSteps(): array; 34 | 35 | /** 36 | * Get the configuration for the project type for the given environment. 37 | */ 38 | public function getEnvironmentConfiguration(string $environment, array $baseConfiguration = []): array; 39 | 40 | /** 41 | * Get the project type name. 42 | */ 43 | public function getName(): string; 44 | 45 | /** 46 | * Get the Finder object for finding all the files in the given project directory. 47 | */ 48 | public function getProjectFiles(string $projectDirectory): Finder; 49 | 50 | /** 51 | * Get the project type slug. 52 | */ 53 | public function getSlug(): string; 54 | 55 | /** 56 | * Install the Ymir integration in the given project directory. 57 | */ 58 | public function installIntegration(string $projectDirectory); 59 | 60 | /** 61 | * Check if the Ymir integration is installed in the given project directory. 62 | */ 63 | public function isIntegrationInstalled(string $projectDirectory): bool; 64 | 65 | /** 66 | * Determine whether the project at the given project directory matches this project type. 67 | */ 68 | public function matchesProject(string $projectDirectory): bool; 69 | } 70 | -------------------------------------------------------------------------------- /src/Support/Arr.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Ymir\Cli\Support; 15 | 16 | use Illuminate\Support\Arr as LaravelArrHelper; 17 | 18 | class Arr extends LaravelArrHelper 19 | { 20 | /** 21 | * Recursively removes duplicate values from an array. 22 | */ 23 | public static function uniqueRecursive(array $array, int $flags = SORT_REGULAR): array 24 | { 25 | foreach ($array as &$value) { 26 | if (is_array($value)) { 27 | $value = static::uniqueRecursive($value, $flags); 28 | } 29 | } 30 | 31 | return array_unique($array, $flags); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /stubs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/arm64 ymirapp/arm-php-runtime:php-74 2 | 3 | ENTRYPOINT [] 4 | 5 | CMD ["/bin/sh", "-c", "/opt/bootstrap"] 6 | 7 | COPY . /var/task 8 | -------------------------------------------------------------------------------- /stubs/ymir-config.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | use Symfony\Component\Config\FileLocator; 14 | use Symfony\Component\DependencyInjection\ContainerBuilder; 15 | use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; 16 | use Ymir\Cli\Application; 17 | 18 | /** 19 | * Determine vendor directory. 20 | */ 21 | $vendorDirectory = ''; 22 | 23 | if (file_exists(__DIR__.'/../../autoload.php')) { 24 | $vendorDirectory = __DIR__.'/../..'; 25 | } elseif (file_exists(__DIR__.'/vendor/autoload.php')) { 26 | $vendorDirectory = __DIR__.'/vendor'; 27 | } 28 | 29 | if (empty($vendorDirectory)) { 30 | throw new \RuntimeException('Unable to find vendor directory'); 31 | } 32 | 33 | require $vendorDirectory.'/autoload.php'; 34 | 35 | $container = new ContainerBuilder(); 36 | 37 | // Load manual parameters 38 | $container->setParameter('application_directory', __DIR__); 39 | $container->setParameter('home_directory', rtrim(getenv('HOME'), '/')); 40 | $container->setParameter('vendor_directory', $vendorDirectory); 41 | $container->setParameter('working_directory', rtrim(getcwd(), '/')); 42 | $container->setParameter('ymir_api_url', getenv('YMIR_API_URL') ?: 'https://ymirapp.com/api'); 43 | 44 | // Load container configuration 45 | $loader = new YamlFileLoader($container, new FileLocator()); 46 | $loader->load(__DIR__.'/config/services.yml'); 47 | 48 | // Compile container 49 | $container->compile(); 50 | 51 | // Start the console application. 52 | exit($container->get(Application::class)->run()); 53 | --------------------------------------------------------------------------------