diff --git a/README.md b/README.md index e4b25ad..623d16e 100644 --- a/README.md +++ b/README.md @@ -8,23 +8,33 @@ Actual commands reside on Magento instances. When a new instance (context) is ad The tool itself only provides following commands: - * ```magento instance:add [name] [url]``` - register a new managed remote instance - * ```magento instance:remove [name]``` - unregister a managed instance from the tool instance list - * ```magento instance:list``` - list registered remote instances - * ```magento instance:update``` - load a list of commands supported by the instance - * ```magento context:set [name]``` - select the default instance to be used in commands + * `./bin/magento instance:add [name] [type] [url]` - register a new managed remote instance + * `./bin/magento instance:remove [name]` - unregister a managed instance from the tool instance list + * `./bin/magento instance:list` - list registered remote instances + * `./bin/magento instance:get` - show current context + * `./bin/magento context:set [name]` - select the default instance to be used in commands -### Magento instance endpoints +#### Remote types -This tool only works with magento instances that support remote calls and metadata sharing. +Tool works with both remote and local calls. -Use https://gist.github.com/antonkril/405d5025038fbc0d333dcd59482e58f4 in root folder for metadata sharing. +### Add remote context: +```bash +./bin/magento context:add cloud remote +``` + +### Add local context: + +```bash +./bin/magento context:add local local +``` + #### Security -The prototype MUST NOT be used in production systems. +The tool is using SSH for remote calls. ### Evolution plan -* Add authentication * Add install/deploy commands for standard environments (local, docker, vagrant, kubernetes) +* Optimize IO operations diff --git a/bin/magento b/bin/magento new file mode 100755 index 0000000..a8e060f --- /dev/null +++ b/bin/magento @@ -0,0 +1,16 @@ +#!/usr/bin/env php +instance(\Illuminate\Contracts\Container\Container::class, $container); +$container->make(\Magento\Console\Application::class, [ + 'container' => $container +])->run(); diff --git a/bootstrap.php b/bootstrap.php new file mode 100644 index 0000000..09fd1b4 --- /dev/null +++ b/bootstrap.php @@ -0,0 +1,18 @@ +=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-10-23T01:57:42+00:00" + }, { "name": "symfony/console", - "version": "v4.1.3", + "version": "v4.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "9dc2299a016497f9ee620be94524e6c0af0280a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/9dc2299a016497f9ee620be94524e6c0af0280a9", + "reference": "9dc2299a016497f9ee620be94524e6c0af0280a9", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/contracts": "^1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~3.4|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.4|~4.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2019-02-23T15:17:42+00:00" + }, + { + "name": "symfony/contracts", + "version": "v1.0.2", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "ca80b8ced97cf07390078b29773dc384c39eee1f" + "url": "https://github.com/symfony/contracts.git", + "reference": "1aa7ab2429c3d594dd70689604b5cf7421254cdf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/ca80b8ced97cf07390078b29773dc384c39eee1f", - "reference": "ca80b8ced97cf07390078b29773dc384c39eee1f", + "url": "https://api.github.com/repos/symfony/contracts/zipball/1aa7ab2429c3d594dd70689604b5cf7421254cdf", + "reference": "1aa7ab2429c3d594dd70689604b5cf7421254cdf", "shasum": "" }, "require": { - "php": "^7.1.3", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/dependency-injection": "<3.4", - "symfony/process": "<3.3" + "php": "^7.1.3" }, "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~3.4|~4.0", - "symfony/dependency-injection": "~3.4|~4.0", - "symfony/event-dispatcher": "~3.4|~4.0", - "symfony/lock": "~3.4|~4.0", - "symfony/process": "~3.4|~4.0" + "psr/cache": "^1.0", + "psr/container": "^1.0" }, "suggest": { - "psr/log-implementation": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" + "psr/cache": "When using the Cache contracts", + "psr/container": "When using the Service contracts", + "symfony/cache-contracts-implementation": "", + "symfony/service-contracts-implementation": "", + "symfony/translation-contracts-implementation": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "1.0-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\Console\\": "" + "Symfony\\Contracts\\": "" + }, + "exclude-from-classmap": [ + "**/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A set of abstractions extracted out of the Symfony components", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2018-12-05T08:06:11+00:00" + }, + { + "name": "symfony/finder", + "version": "v4.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/267b7002c1b70ea80db0833c3afe05f0fbde580a", + "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -70,22 +661,80 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Console Component", + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2019-02-23T15:42:05+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", "homepage": "https://symfony.com", - "time": "2018-07-26T11:24:31+00:00" + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.9.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8" + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d0cd638f4634c16d8df4508e847f14e9e43168b8", - "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494", + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494", "shasum": "" }, "require": { @@ -131,7 +780,188 @@ "portable", "shim" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2018-09-21T13:07:52+00:00" + }, + { + "name": "symfony/process", + "version": "v4.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "6c05edb11fbeff9e2b324b4270ecb17911a8b7ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/6c05edb11fbeff9e2b324b4270ecb17911a8b7ad", + "reference": "6c05edb11fbeff9e2b324b4270ecb17911a8b7ad", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2019-01-24T22:05:03+00:00" + }, + { + "name": "symfony/translation", + "version": "v4.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "748464177a77011f8f4cdd076773862ce4915f8f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/748464177a77011f8f4cdd076773862ce4915f8f", + "reference": "748464177a77011f8f4cdd076773862ce4915f8f", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/contracts": "^1.0.2", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/config": "<3.4", + "symfony/dependency-injection": "<3.4", + "symfony/yaml": "<3.4" + }, + "provide": { + "symfony/translation-contracts-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.4|~4.0", + "symfony/console": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/finder": "~2.8|~3.0|~4.0", + "symfony/intl": "~3.4|~4.0", + "symfony/yaml": "~3.4|~4.0" + }, + "suggest": { + "psr/log-implementation": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Translation Component", + "homepage": "https://symfony.com", + "time": "2019-02-27T03:31:50+00:00" + }, + { + "name": "symfony/yaml", + "version": "v4.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "761fa560a937fd7686e5274ff89dcfa87a5047df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/761fa560a937fd7686e5274ff89dcfa87a5047df", + "reference": "761fa560a937fd7686e5274ff89dcfa87a5047df", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/console": "<3.4" + }, + "require-dev": { + "symfony/console": "~3.4|~4.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2019-02-23T15:17:42+00:00" } ], "packages-dev": [], @@ -140,6 +970,10 @@ "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, - "platform": [], + "platform": { + "php": "^7.1.3", + "ext-curl": "*", + "ext-json": "*" + }, "platform-dev": [] } diff --git a/magento b/magento deleted file mode 100755 index f70ae3b..0000000 --- a/magento +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env php - $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FOLLOWLOCATION => true - ]); - $result = curl_exec($ch); - curl_close($ch); - return $result; -} - -$app = new Application(); -$contextList = new \Magento\Console\ContextList($homeDir); -$commands = [ - 'context' => new ContextCommand\Get($contextList), - 'context:load' => new ContextCommand\Load($contextList), - 'context:list' => new ContextCommand\GetList($contextList), - 'context:add' => new ContextCommand\Add($contextList), - 'context:remove' => new ContextCommand\Remove($contextList), - 'context:set' => new ContextCommand\Set($contextList), -]; - -if ($contextList->getCurrent()) { - $contextData = $contextList->read()[$contextList->getCurrent()]; - if (isset($contextData->commands)) { - foreach ($contextData->commands as $name => $commandData) { - $command = new \Magento\Console\Command\Remote($contextData); - $command->setDescription($commandData->description) - ->setHelp($commandData->help); - foreach ($commandData->definition->arguments as $argumentName => $argumentMeta) { - $command->addArgument( - $argumentName, - $argumentMeta->is_required ? InputArgument::REQUIRED : InputArgument::OPTIONAL, - $argumentMeta->description, - $argumentMeta->default - ); - } - foreach ($commandData->definition->options as $argumentName => $argumentMeta) { - $command->addOption( - $argumentName, - $argumentMeta->shortcut, - $argumentMeta->is_required ? InputOption::VALUE_REQUIRED : InputOption::VALUE_OPTIONAL, - $argumentMeta->description, - $argumentMeta->default - ); - } - - $commands[$commandData->name] = $command; - } - } -} - -array_walk($commands, function ($command, $name) use ($app) { - $command->setName($name); - $app->add($command); -}); - -$app->run(); diff --git a/src/Application.php b/src/Application.php new file mode 100644 index 0000000..4878db6 --- /dev/null +++ b/src/Application.php @@ -0,0 +1,115 @@ +container = $container; + $this->contextList = $contextList; + + parent::__construct(); + } + + /** + * @return array + * @throws BindingResolutionException + */ + protected function getDefaultCommands(): array + { + $stdCommands = [ + $this->container->make(Command\Context\GetCommand::class), + $this->container->make(Command\Context\GetCommand::class), + $this->container->make(Command\Context\ListCommand::class), + $this->container->make(Command\Context\AddCommand::class), + $this->container->make(Command\Context\RemoveCommand::class), + $this->container->make(Command\Context\SetCommand::class) + ]; + $magentoCommands = $this->fetchMagentoCommands(); + + return array_merge( + parent::getDefaultCommands(), + $stdCommands, + $magentoCommands + ); + } + + /** + * @return array + * @throws BindingResolutionException + */ + private function fetchMagentoCommands(): array + { + if (!$this->contextList->getCurrentName()) { + return []; + } + + $context = $this->contextList->getCurrent(); + $commands = []; + + foreach ($context->get('commands') as $cData) { + if (in_array($cData['name'], ['list', 'help'])) { + continue; + } + + /** @var Command\Remote $command */ + $command = $this->container->make(Command\Remote::class); + $command->setName($cData['name']) + ->setDescription($cData['description']) + ->setHelp($cData['help']); + + foreach ($cData['definition']['arguments'] as $aName => $aData) { + $command->addArgument( + $aName, + $aData['mode'] ?? InputArgument::OPTIONAL, + $aData['description'] ?? '' + ); + } + + foreach ($cData['definition']['options'] as $oName => $oData) { + if (in_array($oName, ['help', 'quiet', 'verbose', 'version', 'ansi', 'no-ansi', 'no-interaction'])) { + continue; + } + + $command->addOption( + $oName, + $oData['shortcut'] ?? null, + $oData['mode'] ?? InputOption::VALUE_OPTIONAL, + $oData['description'] ?? '' + ); + } + + $commands[] = $command; + } + + return $commands; + } +} diff --git a/src/Application/ErrorHandler.php b/src/Application/ErrorHandler.php new file mode 100644 index 0000000..dbb3a1c --- /dev/null +++ b/src/Application/ErrorHandler.php @@ -0,0 +1,66 @@ + 'Error', + E_WARNING => 'Warning', + E_PARSE => 'Parse Error', + E_NOTICE => 'Notice', + E_CORE_ERROR => 'Core Error', + E_CORE_WARNING => 'Core Warning', + E_COMPILE_ERROR => 'Compile Error', + E_COMPILE_WARNING => 'Compile Warning', + E_USER_ERROR => 'User Error', + E_USER_WARNING => 'User Warning', + E_USER_NOTICE => 'User Notice', + E_STRICT => 'Strict Notice', + E_RECOVERABLE_ERROR => 'Recoverable Error', + E_DEPRECATED => 'Deprecated Functionality', + E_USER_DEPRECATED => 'User Deprecated Functionality', + ]; + + /** + * Custom error handler. + * + * @param int $errorNo + * @param string $errorStr + * @param string $errorFile + * @param int $errorLine + * @return bool + * @throws \RuntimeException + */ + public function handle(int $errorNo, string $errorStr, string $errorFile, int $errorLine): bool + { + if (strpos($errorStr, 'DateTimeZone::__construct') !== false) { + /** + * There's no way to distinguish between caught system exceptions and warnings. + */ + return false; + } + + $errorNo &= error_reporting(); + + if ($errorNo === 0) { + return false; + } + + $msg = self::$errorPhrases[$errorNo] ?? "Unknown error ({$errorNo})"; + $msg .= ": {$errorStr} in {$errorFile} on line {$errorLine}"; + + throw new \RuntimeException($msg); + } +} diff --git a/src/Command/Context.php b/src/Command/Context.php deleted file mode 100644 index 5f500d1..0000000 --- a/src/Command/Context.php +++ /dev/null @@ -1,27 +0,0 @@ -contextList = $contextList; - parent::__construct(); - } -} \ No newline at end of file diff --git a/src/Command/Context/Add.php b/src/Command/Context/Add.php deleted file mode 100644 index 0dffc74..0000000 --- a/src/Command/Context/Add.php +++ /dev/null @@ -1,29 +0,0 @@ -addArgument('name', \Symfony\Component\Console\Input\InputArgument::REQUIRED); - $this->addArgument('url', \Symfony\Component\Console\Input\InputArgument::REQUIRED); - } - - protected function execute(InputInterface $input, OutputInterface $output) - { - $contexts = $this->contextList->read(); - - $contexts[$input->getArgument('name')] = ['url' => $input->getArgument('url')]; - $this->contextList->write($contexts); - } -} \ No newline at end of file diff --git a/src/Command/Context/AddCommand.php b/src/Command/Context/AddCommand.php new file mode 100644 index 0000000..66f1875 --- /dev/null +++ b/src/Command/Context/AddCommand.php @@ -0,0 +1,98 @@ +contextList = $contextList; + $this->sshFactory = $shellFactory; + + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure(): void + { + $this->setName(self::NAME) + ->setDescription('Add context') + ->addArgument(self::ARG_NAME, InputArgument::REQUIRED, 'Name of context') + ->addArgument( + self::ARG_TYPE, + InputArgument::REQUIRED, + 'Type one of ' . implode(', ', [ShellFactory::TYPE_LOCAL, ShellFactory::TYPE_REMOTE])) + ->addArgument(self::ARG_URL, InputArgument::REQUIRED, 'URL address'); + + parent::configure(); + } + + /** + * @inheritdoc + * + * @throws \Exception + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $name = $input->getArgument(self::ARG_NAME); + + $process = $this->sshFactory->create( + $input->getArgument(self::ARG_TYPE), + $input->getArgument(self::ARG_URL), + 'list --format=json' + ); + $process->mustRun(); + + $this->contextList->add( + $name, + $input->getArgument(self::ARG_TYPE), + $input->getArgument(self::ARG_URL), + json_decode($process->getOutput(), true)['commands'] + ); + + $output->writeln('Context added.'); + + if (!$this->contextList->getCurrentName()) { + $this->getApplication() + ->find(SetCommand::NAME) + ->run(new ArrayInput([SetCommand::ARG_NAME => $name]), $output); + } + } +} diff --git a/src/Command/Context/Get.php b/src/Command/Context/Get.php deleted file mode 100644 index 114e4b9..0000000 --- a/src/Command/Context/Get.php +++ /dev/null @@ -1,20 +0,0 @@ -writeln($this->contextList->getCurrent()); - } -} diff --git a/src/Command/Context/GetCommand.php b/src/Command/Context/GetCommand.php new file mode 100644 index 0000000..9be71d8 --- /dev/null +++ b/src/Command/Context/GetCommand.php @@ -0,0 +1,63 @@ +contextList = $contextList; + + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure(): void + { + $this->setName(self::NAME) + ->setDescription('Display current context'); + + parent::configure(); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return int|null|void + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + if ($context = $this->contextList->getCurrentName()) { + $output->writeln($context); + + return; + } + + $output->writeln('No context set.'); + } +} diff --git a/src/Command/Context/GetList.php b/src/Command/Context/GetList.php deleted file mode 100644 index 63056ee..0000000 --- a/src/Command/Context/GetList.php +++ /dev/null @@ -1,25 +0,0 @@ -contextList->read(); - $current = $this->contextList->getCurrent(); - $strings = array_map(function ($key, $value) use ($current) { - return ($current === $key ? '* ' : ' ') . str_pad($key, 15) . $value; - }, array_keys($contextList), array_map(function ($item) { return $item->url; }, $contextList)); - $output->write(join("\n", $strings) . "\n"); - } -} diff --git a/src/Command/Context/ListCommand.php b/src/Command/Context/ListCommand.php new file mode 100644 index 0000000..3247cb4 --- /dev/null +++ b/src/Command/Context/ListCommand.php @@ -0,0 +1,71 @@ +contextList = $contextList; + + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure(): void + { + $this->setName('context:list') + ->setDescription('Display all contexts'); + + parent::configure(); + } + + /** + * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $rows = []; + + foreach ($this->contextList->getAll() as $name => $data) { + $rows[] = [ + $name, + $data->get('type'), + $data->get('url'), + ]; + } + + if (!$rows) { + $output->writeln('No available contexts.'); + + return; + } + + $table = new Table($output); + $table->setHeaders(['Name', 'Type', 'URL']) + ->setRows($rows) + ->render(); + } +} diff --git a/src/Command/Context/Load.php b/src/Command/Context/Load.php deleted file mode 100644 index 97212f8..0000000 --- a/src/Command/Context/Load.php +++ /dev/null @@ -1,27 +0,0 @@ -contextList->read(); - $contextData = $contexts[$this->contextList->getCurrent()]; - $meta = json_decode(loadContextMetadata($contextData->url . "/manage.php?config")); - if (is_null($meta)) { - throw new \Exception("Could not load context metadata"); - } - $contexts[$this->contextList->getCurrent()]->commands = $meta; - $this->contextList->write($contexts); - } -} \ No newline at end of file diff --git a/src/Command/Context/Remove.php b/src/Command/Context/Remove.php deleted file mode 100644 index c60e976..0000000 --- a/src/Command/Context/Remove.php +++ /dev/null @@ -1,27 +0,0 @@ -addArgument('name', \Symfony\Component\Console\Input\InputArgument::REQUIRED); - } - - protected function execute(InputInterface $input, OutputInterface $output) - { - $contexts = $this->contextList->read(); - unset($contexts[$input->getArgument('name')]); - $this->contextList->write($contexts); - } -} \ No newline at end of file diff --git a/src/Command/Context/RemoveCommand.php b/src/Command/Context/RemoveCommand.php new file mode 100644 index 0000000..e224e3a --- /dev/null +++ b/src/Command/Context/RemoveCommand.php @@ -0,0 +1,68 @@ +contextList = $contextList; + + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure(): void + { + $this->setName(self::NAME) + ->setDescription('Remove context') + ->addArgument(self::ARG_NAME, InputArgument::REQUIRED); + + parent::configure(); + } + + /** + * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $name = $input->getArgument(self::ARG_NAME); + + if ($this->contextList->has($name)) { + $this->contextList->remove($name); + + $output->writeln('Context removed'); + + return; + } + + $output->writeln('No such context'); + } +} diff --git a/src/Command/Context/Set.php b/src/Command/Context/Set.php deleted file mode 100644 index 6c78a38..0000000 --- a/src/Command/Context/Set.php +++ /dev/null @@ -1,30 +0,0 @@ -addArgument('name', \Symfony\Component\Console\Input\InputArgument::REQUIRED); - } - - protected function execute(InputInterface $input, OutputInterface $output) - { - $contexts = $this->contextList->read(); - if (!isset($contexts[$input->getArgument('name')])) { - $output->writeln('Context ' . $input->getArgument('name') . ' does not exist'); - } else { - $this->contextList->setCurrent($input->getArgument('name')); - } - } -} \ No newline at end of file diff --git a/src/Command/Context/SetCommand.php b/src/Command/Context/SetCommand.php new file mode 100644 index 0000000..e6512cf --- /dev/null +++ b/src/Command/Context/SetCommand.php @@ -0,0 +1,68 @@ +contextList = $contextList; + + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure(): void + { + $this->setName(self::NAME) + ->setDescription('Switch current context') + ->addArgument(self::ARG_NAME, InputArgument::REQUIRED); + } + + /** + * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $name = $input->getArgument('name'); + + if (!$this->contextList->has($name)) { + $output->writeln(sprintf('Context "%s" does not exists', $name)); + + return; + } + + $this->contextList->setCurrent( + $input->getArgument('name') + ); + + $output->writeln('Context switched'); + } +} diff --git a/src/Command/Remote.php b/src/Command/Remote.php index 9669c08..2169304 100644 --- a/src/Command/Remote.php +++ b/src/Command/Remote.php @@ -5,40 +5,54 @@ */ namespace Magento\Console\Command; +use Magento\Console\Context\ContextList; +use Magento\Console\Shell\ShellFactory; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +/** + * Generic remote command + */ class Remote extends Command { /** - * @var \stdClass + * @var ContextList + */ + private $contextList; + + /** + * @var ShellFactory */ - private $contextData; + private $shellFactory; /** - * @param \stdClass $contextData - * @param null $name + * @param ContextList $contextList + * @param ShellFactory $shellFactory */ - public function __construct($contextData, $name = null) + public function __construct(ContextList $contextList, ShellFactory $shellFactory) { - $this->contextData = $contextData; - parent::__construct($name); + $this->contextList = $contextList; + $this->shellFactory = $shellFactory; + + parent::__construct(); } + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return int|null|void + */ protected function execute(InputInterface $input, OutputInterface $output) { - $postData = json_encode(['arguments' => $input->getArguments(), 'options' => $input->getOptions()]); - $ch = curl_init(); - curl_setopt_array($ch, [ - CURLOPT_URL => $this->contextData->url . '/manage.php', - CURLOPT_RETURNTRANSFER => true, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $postData, - CURLOPT_FOLLOWLOCATION => true - ]); - $result = curl_exec($ch); - $output->writeln($result); - curl_close($ch); + $process = $this->shellFactory->create( + $this->contextList->getCurrent()->get('type'), + $this->contextList->getCurrent()->get('url'), + (string)$input + ); + + $process->mustRun(function ($type, string $buffer) use ($output) { + $output->write($buffer); + }); } } diff --git a/src/Config/Reader.php b/src/Config/Reader.php new file mode 100644 index 0000000..d8e7f9e --- /dev/null +++ b/src/Config/Reader.php @@ -0,0 +1,42 @@ +filesystem = $filesystem; + } + + /** + * @return array + */ + public function read(): array + { + $configFile = HOME_DIR . '/config.yaml'; + + if ($this->filesystem->exists($configFile)) { + return Yaml::parseFile($configFile); + } + + return []; + } +} diff --git a/src/Config/Writer.php b/src/Config/Writer.php new file mode 100644 index 0000000..d18bab0 --- /dev/null +++ b/src/Config/Writer.php @@ -0,0 +1,42 @@ +filesystem = $filesystem; + } + + /** + * @param array $data + * @return int + */ + public function write(array $data): int + { + $configFile = HOME_DIR . '/config.yaml'; + + return $this->filesystem->put( + $configFile, + Yaml::dump($data, 4, 2) + ); + } +} diff --git a/src/Context/ContextList.php b/src/Context/ContextList.php new file mode 100644 index 0000000..a4bf4df --- /dev/null +++ b/src/Context/ContextList.php @@ -0,0 +1,152 @@ +reader = $reader; + $this->writer = $writer; + } + + /** + * @param string $name + * @return bool + */ + public function has(string $name): bool + { + $config = $this->reader->read(); + + return isset($config['contexts'][$name]); + } + + /** + * @param string $name + * @param string $type + * @param string $url + * @param array $commands + */ + public function add(string $name, string $type, string $url, array $commands): void + { + $config = $this->reader->read(); + $config['contexts'][$name] = [ + 'name' => $name, + 'type' => $type, + 'url' => $url, + 'commands' => $commands, + ]; + + $this->writer->write($config); + } + + /** + * @param string $name + */ + public function remove(string $name): void + { + $config = $this->reader->read(); + + unset($config['contexts'][$name]); + + if (isset($config['current']) && $config['current'] === $name) { + unset($config['current']); + } + + $this->writer->write($config); + } + + /** + * @return Repository + */ + public function getCurrent(): Repository + { + $config = $this->reader->read(); + $current = $this->getCurrentName(); + + if (!$current) { + throw new \RuntimeException('Current context not set'); + } + + return new Repository($config['contexts'][$current]); + } + + /** + * @return bool + */ + public function hasCurrent(): bool + { + return $this->getCurrentName() !== null; + } + + /** + * @return null|string + */ + public function getCurrentName(): ?string + { + $config = $this->reader->read(); + + return $config['current'] ?? null; + } + + /** + * @return Repository[] + */ + public function getAll(): array + { + $config = $this->reader->read(); + + if (!array_key_exists('contexts', $config)) { + return []; + } + + $contexts = []; + + foreach ($config['contexts'] as $name => $context) { + $contexts[$name] = new Repository($context); + } + + return $contexts; + } + + /** + * Set and persist current context. + * All following commands will be executed in this context + * + * @param string $name + * @return bool|int + */ + public function setCurrent(string $name): bool + { + $config = $this->reader->read(); + $config['current'] = $name; + + return $this->writer->write($config); + } +} diff --git a/src/ContextList.php b/src/ContextList.php deleted file mode 100644 index 8a7f4d5..0000000 --- a/src/ContextList.php +++ /dev/null @@ -1,95 +0,0 @@ -listPersistenceFile = $homeDir . DIRECTORY_SEPARATOR . 'contexts'; - $this->currentContextFile = $homeDir . DIRECTORY_SEPARATOR . 'context'; - } - - /** - * Read list of contexts available to work with - * - * @return array - */ - public function read() : array - { - if ($this->list !== null) { - return $this->list; - } - $contexts = []; - if (is_readable($this->listPersistenceFile)) { - $contexts = json_decode(file_get_contents($this->listPersistenceFile)); - } - $this->list = (array) $contexts; - return $this->list; - } - - /** - * Persist available context list - * - * @param [string] $contexts - * @return bool - */ - public function write(array $contexts) : bool - { - $this->list = $contexts; - return !!file_put_contents($this->listPersistenceFile, json_encode($contexts)); - } - - /** - * @return string - */ - public function getCurrent() : ?string - { - if ($this->current !== null) { - return $this->current; - } - if (is_readable($this->currentContextFile)) { - return json_decode(file_get_contents($this->currentContextFile)); - } - return null; - } - - /** - * Set and persist current context. All following commands will be executed in this context - * @param string $name - * @return bool|int - */ - public function setCurrent(string $name) : bool - { - $this->current = $name; - return !!file_put_contents($this->currentContextFile, json_encode($name)); - } -} \ No newline at end of file diff --git a/src/Shell/ShellFactory.php b/src/Shell/ShellFactory.php new file mode 100644 index 0000000..e3a860f --- /dev/null +++ b/src/Shell/ShellFactory.php @@ -0,0 +1,35 @@ +