├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .yarnrc.yml ├── Classes ├── Command │ ├── EvaluateEelExpressionCommand.php │ ├── FlushCacheCommand.php │ ├── NodeRepairCommand.php │ ├── SearchCommand.php │ └── TerminalCommandInterface.php ├── Controller │ └── TerminalCommandController.php ├── Domain │ ├── CommandContext.php │ ├── CommandInvocationResult.php │ └── Dto │ │ └── NodeResult.php ├── Exception.php ├── Security │ ├── TerminalCommandPrivilege.php │ └── TerminalCommandPrivilegeSubject.php └── Service │ ├── EelEvaluationService.php │ ├── SerializationService.php │ └── TerminalCommandService.php ├── Configuration ├── Development │ └── Settings.yaml ├── Policy.yaml ├── Routes.yaml ├── Settings.Flow.yaml └── Settings.yaml ├── Documentation └── shel-neos-terminal-example.jpg ├── LICENSE.txt ├── Readme.md ├── Resources ├── Private │ ├── JavaScript │ │ └── Terminal │ │ │ └── src │ │ │ ├── Terminal.tsx │ │ │ ├── actions │ │ │ └── index.ts │ │ │ ├── components │ │ │ ├── ReplWrapper.css │ │ │ ├── ReplWrapper.tsx │ │ │ ├── SponsorshipBadge.css │ │ │ └── SponsorshipBadge.tsx │ │ │ ├── helpers │ │ │ ├── doInvokeCommand.ts │ │ │ ├── fetchCommands.ts │ │ │ └── logger.ts │ │ │ ├── index.js │ │ │ ├── interfaces │ │ │ ├── Command.ts │ │ │ ├── CommandList.ts │ │ │ ├── Feedback.ts │ │ │ ├── I18nRegistry.ts │ │ │ ├── NeosRootState.ts │ │ │ ├── Node.ts │ │ │ ├── RegistrationKey.ts │ │ │ └── index.ts │ │ │ ├── manifest.js │ │ │ ├── provider │ │ │ └── CommandsProvider.tsx │ │ │ ├── registry │ │ │ └── TerminalCommandRegistry.tsx │ │ │ └── typings │ │ │ └── global.d.ts │ └── Translations │ │ ├── de │ │ └── Main.xlf │ │ └── en │ │ └── Main.xlf └── Public │ └── Assets │ ├── Plugin.css │ ├── Plugin.css.map │ ├── Plugin.js │ ├── Plugin.js.LICENSE.txt │ └── Plugin.js.map ├── Tests └── Functional │ └── EvaluateEelExpressionTest.php └── composer.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.{yml,yaml,json}] 12 | indent_size = 2 13 | 14 | [*.md] 15 | indent_size = 2 16 | trim_trailing_whitespace = false 17 | 18 | [*.{ts,tsx}] 19 | ij_typescript_spaces_within_object_literal_braces = true 20 | ij_typescript_spaces_within_imports = true 21 | ij_html_space_inside_empty_tag = true 22 | ij_javascript_spaces_within_imports = true 23 | ij_typescript_use_double_quotes = false 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react/recommended', 8 | 'plugin:prettier/recommended', 9 | ], 10 | plugins: ['@typescript-eslint', 'prettier', 'react', 'react-hooks'], 11 | settings: { 12 | react: { 13 | version: 'detect', 14 | }, 15 | }, 16 | env: { 17 | browser: true, 18 | es6: true, 19 | node: true, 20 | }, 21 | rules: { 22 | '@typescript-eslint/explicit-function-return-type': 'off', 23 | '@typescript-eslint/explicit-module-boundary-types': 'off', 24 | '@typescript-eslint/no-explicit-any': 'off', 25 | '@typescript-eslint/no-var-requires': 'off', 26 | '@typescript-eslint/ban-ts-ignore': 'off', 27 | '@typescript-eslint/ban-ts-comment': 'off', 28 | 'react/prop-types': 'off', 29 | 'prettier/prettier': ['error'], 30 | 'react-hooks/rules-of-hooks': 'error', 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.map binary 2 | Plugin.js binary 3 | Plugin.css binary 4 | 5 | .yarn export-ignore 6 | .github export-ignore 7 | .babelrc export-ignore 8 | .eslintignore export-ignore 9 | .eslintrc export-ignore 10 | .nvmrc export-ignore 11 | .prettierrc export-ignore 12 | .yarnrc export-ignore 13 | .github export-ignore 14 | patches export-ignore 15 | tsconfig.json export-ignore 16 | yarn.lock export-ignore 17 | package.json export-ignore 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .yarn/install-state.gz 3 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableGlobalCache: true 2 | nodeLinker: node-modules 3 | yarnPath: .yarn/releases/yarn-3.3.1.cjs 4 | pnpMode: loose 5 | 6 | plugins: 7 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 8 | spec: "@yarnpkg/plugin-interactive-tools" 9 | -------------------------------------------------------------------------------- /Classes/Command/EvaluateEelExpressionCommand.php: -------------------------------------------------------------------------------- 1 | getSynopsis(); 46 | } 47 | 48 | public static function getInputDefinition(): InputDefinition 49 | { 50 | return new InputDefinition([ 51 | new InputArgument('expression', InputArgument::REQUIRED | InputArgument::IS_ARRAY), 52 | ]); 53 | } 54 | 55 | public function invokeCommand(string $argument, CommandContext $commandContext): CommandInvocationResult 56 | { 57 | $input = new StringInput($argument); 58 | 59 | try { 60 | $input->bind(self::getInputDefinition()); 61 | $input->validate(); 62 | } catch (RuntimeException $e) { 63 | return new CommandInvocationResult(false, $e->getMessage()); 64 | } 65 | 66 | $success = true; 67 | 68 | $evaluationContext = [ 69 | 'site' => $commandContext->getSiteNode(), 70 | 'documentNode' => $commandContext->getDocumentNode(), 71 | 'node' => $commandContext->getFocusedNode(), 72 | ]; 73 | 74 | try { 75 | $result = $this->eelEvaluationService->evaluateEelExpression('${' . $argument . '}', $evaluationContext); 76 | } catch (\Exception $e) { 77 | $success = false; 78 | $result = $e->getMessage(); 79 | } 80 | 81 | return new CommandInvocationResult($success, $result); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Classes/Command/FlushCacheCommand.php: -------------------------------------------------------------------------------- 1 | getSynopsis(); 54 | } 55 | 56 | public static function getInputDefinition(): InputDefinition 57 | { 58 | return new InputDefinition([ 59 | new InputArgument('cacheIdentifier', InputArgument::OPTIONAL), 60 | ]); 61 | } 62 | 63 | public function invokeCommand(string $argument, CommandContext $commandContext): CommandInvocationResult 64 | { 65 | $input = new StringInput($argument); 66 | $input->bind(self::getInputDefinition()); 67 | 68 | try { 69 | $input->validate(); 70 | } catch (RuntimeException $e) { 71 | return new CommandInvocationResult(false, $e->getMessage()); 72 | } 73 | 74 | $cacheIdentifier = $input->getArgument('cacheIdentifier'); 75 | $success = true; 76 | 77 | if ($cacheIdentifier) { 78 | if ($this->cacheManager->hasCache($cacheIdentifier)) { 79 | $this->cacheManager->getCache($cacheIdentifier)->flush(); 80 | $result = $this->translator->translateById('command.flushCache.flushedOne', 81 | ['cacheIdentifier' => $cacheIdentifier], null, null, 'Main', 'Shel.Neos.Terminal'); 82 | } else { 83 | $success = false; 84 | $result = $this->translator->translateById('command.flushCache.cacheDoesNotExist', 85 | ['cacheIdentifier' => $cacheIdentifier], null, null, 'Main', 'Shel.Neos.Terminal'); 86 | } 87 | } else { 88 | $result = $this->translator->translateById('command.flushCache.flushedAll', [], null, null, 'Main', 89 | 'Shel.Neos.Terminal'); 90 | $this->cacheManager->flushCaches(); 91 | } 92 | 93 | // Echo response as we have to exit the process prematurely or the application 94 | // will throw errors due to the flushed caches. 95 | // TODO: Find out if there is a better way to do this 96 | header('Content-Type: application/json'); 97 | echo json_encode([ 98 | 'success' => $success, 99 | 'result' => $result, 100 | ]); 101 | exit; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Classes/Command/NodeRepairCommand.php: -------------------------------------------------------------------------------- 1 | getSynopsis(); 66 | } 67 | 68 | public static function getInputDefinition(): InputDefinition 69 | { 70 | return new InputDefinition([ 71 | new InputArgument('methodName', InputArgument::REQUIRED), 72 | new InputArgument('nodeType', InputArgument::REQUIRED), 73 | new InputOption('workspace', 'w', InputOption::VALUE_REQUIRED), 74 | new InputOption('dryRun', 'd', InputOption::VALUE_NONE, 'Desc'), 75 | ]); 76 | } 77 | 78 | public function invokeCommand(string $argument, CommandContext $commandContext): CommandInvocationResult 79 | { 80 | $input = new StringInput($argument); 81 | $input->bind(self::getInputDefinition()); 82 | 83 | try { 84 | $input->validate(); 85 | } catch (RuntimeException $e) { 86 | return new CommandInvocationResult(false, $e->getMessage()); 87 | } 88 | 89 | $success = false; 90 | 91 | $methodName = $input->getArgument('methodName'); 92 | $nodeTypeName = $input->getArgument('nodeType'); 93 | $workspace = $input->getOption('workspace'); 94 | $dryRun = $input->getOption('dryRun'); 95 | 96 | if ($this->nodeTypeManager->hasNodeType($nodeTypeName)) { 97 | $nodeType = $this->nodeTypeManager->getNodeType($nodeTypeName); 98 | 99 | $bufferedOutput = new BufferedOutput(); 100 | $consoleOutput = new ConsoleOutput(); 101 | $consoleOutput->setOutput($bufferedOutput); 102 | 103 | $plugins = $this->getPlugins(); 104 | foreach ($plugins as $plugin) { 105 | if ($plugin instanceof EventDispatchingNodeCommandControllerPluginInterface) { 106 | $this->attachPluginEventHandlers($plugin, $dryRun, $consoleOutput); 107 | } 108 | $plugin->invokeSubCommand( 109 | 'repair', 110 | $consoleOutput, 111 | $nodeType, 112 | $workspace ?? 'live', 113 | $dryRun, 114 | true, 115 | null, 116 | $methodName 117 | ); 118 | } 119 | $result = strip_tags($bufferedOutput->fetch()); 120 | 121 | if ($result) { 122 | $success = true; 123 | } else { 124 | $result = $this->translator->translateById( 125 | 'command.nodeRepair.noMatchingMethodName', 126 | ['methodName' => $methodName], 127 | null, 128 | null, 129 | 'Main', 130 | 'Shel.Neos.Terminal' 131 | ); 132 | } 133 | } else { 134 | $result = $this->translator->translateById( 135 | 'command.nodeRepair.nodeTypeNotFound', 136 | ['nodeType' => $nodeTypeName], 137 | null, 138 | null, 139 | 'Main', 140 | 'Shel.Neos.Terminal' 141 | ); 142 | } 143 | 144 | return new CommandInvocationResult($success, $result); 145 | } 146 | 147 | /** 148 | * Get plugins for the repair command 149 | * 150 | * @return array 151 | */ 152 | protected function getPlugins(): array 153 | { 154 | $plugins = []; 155 | $classNames = $this->objectManager->get(ReflectionService::class)->getAllImplementationClassNamesForInterface(NodeCommandControllerPluginInterface::class); 156 | foreach ($classNames as $className) { 157 | /** @var NodeCommandControllerPluginInterface $plugin */ 158 | $plugin = $this->objectManager->get($this->objectManager->getObjectNameByClassName($className)); 159 | $plugins[$className] = $plugin; 160 | } 161 | return $plugins; 162 | } 163 | 164 | /** 165 | * Attach plugin events to write to output 166 | */ 167 | protected function attachPluginEventHandlers(EventDispatchingNodeCommandControllerPluginInterface $plugin, bool $dryRun, ConsoleOutput $consoleOutput): void 168 | { 169 | $plugin->on(EventDispatchingNodeCommandControllerPluginInterface::EVENT_NOTICE, function (string $text) use ($consoleOutput) { 170 | $consoleOutput->outputLine($text); 171 | }); 172 | $plugin->on(EventDispatchingNodeCommandControllerPluginInterface::EVENT_TASK, function (string $description, \Closure $task, bool $requiresConfirmation = false) use ($dryRun, $consoleOutput) { 173 | $text = sprintf(' ❱ %s ', $description); 174 | 175 | $consoleOutput->outputLine($text); 176 | if ($dryRun) { 177 | $consoleOutput->outputLine(' skipped (dry run)'); 178 | } else { 179 | $task(); 180 | $consoleOutput->outputLine(' applied ✔'); 181 | } 182 | }); 183 | $plugin->on(EventDispatchingNodeCommandControllerPluginInterface::EVENT_WARNING, function (string $text) use ($consoleOutput) { 184 | $consoleOutput->outputLine('WARNING: %s', [$text]); 185 | }); 186 | $plugin->on(EventDispatchingNodeCommandControllerPluginInterface::EVENT_ERROR, function (string $text) use ($consoleOutput) { 187 | $consoleOutput->outputLine('%s', [$text]); 188 | }); 189 | } 190 | } 191 | 192 | -------------------------------------------------------------------------------- /Classes/Command/SearchCommand.php: -------------------------------------------------------------------------------- 1 | getSynopsis(); 62 | } 63 | 64 | public static function getInputDefinition(): InputDefinition 65 | { 66 | return new InputDefinition([ 67 | new InputArgument('searchword', InputArgument::REQUIRED | InputArgument::IS_ARRAY), 68 | new InputOption('contextNode', 'c', InputOption::VALUE_OPTIONAL), 69 | new InputOption('nodeTypes', 'n', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL), 70 | ]); 71 | } 72 | 73 | public function invokeCommand(string $argument, CommandContext $commandContext): CommandInvocationResult 74 | { 75 | $input = new StringInput($argument); 76 | $input->bind(self::getInputDefinition()); 77 | 78 | try { 79 | $input->validate(); 80 | } catch (RuntimeException $e) { 81 | return new CommandInvocationResult(false, $e->getMessage()); 82 | } 83 | 84 | $siteNode = $commandContext->getSiteNode(); 85 | $contextNode = match ($input->getOption('contextNode')) { 86 | 'node', 'focusedNode' => $commandContext->getFocusedNode(), 87 | 'document', 'documentNode' => $commandContext->getDocumentNode(), 88 | default => $siteNode, 89 | }; 90 | 91 | if (!$contextNode) { 92 | return new CommandInvocationResult( 93 | false, 94 | $this->translator->translateById( 95 | 'command.search.noContext', 96 | [], 97 | null, 98 | null, 99 | 'Main', 100 | 'Shel.Neos.Terminal' 101 | ) 102 | ); 103 | } 104 | 105 | // The NodeSearchInterface does not yet have a 4th argument for the startingPoint but all known implementations do 106 | $searchTerm = $input->getArgument('searchword'); 107 | if (is_array($searchTerm)) { 108 | $searchTerm = implode(' ', $searchTerm); 109 | } 110 | 111 | $nodes = $this->nodeSearchService->findByProperties( 112 | $searchTerm, 113 | $input->getOption('nodeTypes'), 114 | $contextNode->getContext(), 115 | $contextNode 116 | ); 117 | 118 | $results = array_map(function ($node) use ($commandContext) { 119 | return NodeResult::fromNode( 120 | $node, 121 | $this->getUriForNode($commandContext->getControllerContext(), $node) 122 | ); 123 | }, $nodes); 124 | 125 | return new CommandInvocationResult(true, $results); 126 | } 127 | 128 | protected function getUriForNode( 129 | ControllerContext $controllerContext, 130 | NodeInterface $node 131 | ): string { 132 | // Get the closest document to create uri from for navigation 133 | $closestDocumentNode = $node; 134 | while ($closestDocumentNode && !$closestDocumentNode->getNodeType()->isOfType('Neos.Neos:Document')) { 135 | $closestDocumentNode = $closestDocumentNode->getParent(); 136 | } 137 | if (!$closestDocumentNode) { 138 | return ''; 139 | } 140 | 141 | try { 142 | return $this->linkingService->createNodeUri( 143 | $controllerContext, 144 | $closestDocumentNode, 145 | null, 146 | 'html', 147 | true 148 | ); 149 | } catch (\Exception) { 150 | } 151 | return ''; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Classes/Command/TerminalCommandInterface.php: -------------------------------------------------------------------------------- 1 | JsonView::class, 48 | ]; 49 | 50 | /** 51 | * @Flow\Inject 52 | * @var Translator 53 | */ 54 | protected $translator; 55 | 56 | /** 57 | * @Flow\Inject 58 | * @var FeedbackCollection 59 | */ 60 | protected $feedbackCollection; 61 | 62 | /** 63 | * @Flow\InjectConfiguration(path="frontendConfiguration", package="Neos.Neos.Ui") 64 | * @var array 65 | */ 66 | protected $frontendConfiguration; 67 | 68 | /** 69 | * @Flow\Inject 70 | * @var TerminalCommandService 71 | */ 72 | protected $terminalCommandService; 73 | 74 | /** 75 | * @Flow\Inject 76 | * @var PrivilegeManagerInterface 77 | */ 78 | protected $privilegeManager; 79 | 80 | public function getCommandsAction(): void 81 | { 82 | if (!$this->privilegeManager->isPrivilegeTargetGranted('Neos.Neos:Backend.GeneralAccess')) { 83 | $this->view->assign('value', ['success' => false, 'result' => []]); 84 | return; 85 | } 86 | 87 | $commandNames = $this->terminalCommandService->getCommandNames(); 88 | 89 | $availableCommandNames = array_filter($commandNames, function ($commandName) { 90 | return $this->privilegeManager->isGranted(TerminalCommandPrivilege::class, 91 | new TerminalCommandPrivilegeSubject($commandName)); 92 | }); 93 | 94 | $commandDefinitions = array_reduce($availableCommandNames, function (array $carry, string $commandName) { 95 | $command = $this->terminalCommandService->getCommand($commandName); 96 | $carry[$commandName] = [ 97 | 'name' => $commandName, 98 | 'description' => $command::getCommandDescription(), 99 | 'usage' => $command::getCommandUsage(), 100 | ]; 101 | return $carry; 102 | }, []); 103 | 104 | $this->view->assign('value', ['success' => true, 'result' => $commandDefinitions]); 105 | } 106 | 107 | public function invokeCommandAction( 108 | string $commandName, 109 | string $argument = null, 110 | NodeInterface $siteNode = null, 111 | NodeInterface $documentNode = null, 112 | NodeInterface $focusedNode = null 113 | ): void { 114 | $this->response->setContentType('application/json'); 115 | 116 | $command = $this->terminalCommandService->getCommand($commandName); 117 | 118 | $commandContext = (new CommandContext($this->getControllerContext())) 119 | ->withSiteNode($siteNode) 120 | ->withDocumentNode($documentNode) 121 | ->withFocusedNode($focusedNode) 122 | ->withFocusedNode($focusedNode); 123 | 124 | $this->getControllerContext()->getRequest()->getMainRequest()->setFormat('html'); 125 | 126 | try { 127 | $result = $command->invokeCommand($argument, $commandContext); 128 | } catch (AccessDeniedException $e) { 129 | $result = new CommandInvocationResult(false, 130 | $this->translateById('commandNotGranted', ['command' => $commandName])); 131 | } 132 | 133 | if (!$result) { 134 | $result = new CommandInvocationResult(false, 135 | $this->translateById('commandNotFound', ['command' => $commandName])); 136 | } 137 | 138 | // TODO: Move the feedback related logic into a separate service 139 | if ($result->getUiFeedback()) { 140 | // Change format to prevent url generation errors when serialising url based feedback 141 | foreach ($result->getUiFeedback() as $feedback) { 142 | $this->feedbackCollection->add($feedback); 143 | } 144 | } 145 | 146 | $this->view->assign('value', [ 147 | 'success' => $result->isSuccess(), 148 | 'result' => SerializationService::serialize($result->getResult()), 149 | 'uiFeedback' => $this->feedbackCollection, 150 | ]); 151 | } 152 | 153 | /** 154 | * @throws UnsupportedRequestTypeException 155 | */ 156 | protected function initializeController(ActionRequest $request, ActionResponse $response): void 157 | { 158 | parent::initializeController($request, $response); 159 | $this->feedbackCollection->setControllerContext($this->getControllerContext()); 160 | } 161 | 162 | /** 163 | * Throws an exception when terminal is disabled 164 | * 165 | * @throws TerminalException 166 | */ 167 | protected function initializeAction(): void 168 | { 169 | $terminalConfiguration = $this->frontendConfiguration['Shel.Neos.Terminal:Terminal']; 170 | 171 | $terminalEnabled = $terminalConfiguration['enabled'] ?? false; 172 | if (!$terminalEnabled) { 173 | throw new TerminalException($this->translateById('disabled')); 174 | } 175 | 176 | parent::initializeAction(); 177 | } 178 | 179 | protected function translateById(string $id, array $arguments = []): string 180 | { 181 | try { 182 | return $this->translator->translateById('disabled', $arguments); 183 | } catch (InvalidFormatPlaceholderException | IndexOutOfBoundsException $e) { 184 | } 185 | return $id; 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /Classes/Domain/CommandContext.php: -------------------------------------------------------------------------------- 1 | controllerContext = $controllerContext; 45 | } 46 | 47 | public function getControllerContext(): ControllerContext 48 | { 49 | return $this->controllerContext; 50 | } 51 | 52 | public function getSiteNode(): ?NodeInterface 53 | { 54 | return $this->siteNode; 55 | } 56 | 57 | public function withSiteNode(NodeInterface $siteNode = null): CommandContext 58 | { 59 | $instance = clone $this; 60 | $instance->siteNode = $siteNode; 61 | return $instance; 62 | } 63 | 64 | public function getDocumentNode(): ?NodeInterface 65 | { 66 | return $this->documentNode; 67 | } 68 | 69 | public function withDocumentNode(NodeInterface $documentNode = null): CommandContext 70 | { 71 | $instance = clone $this; 72 | $instance->documentNode = $documentNode; 73 | return $instance; 74 | } 75 | 76 | public function getFocusedNode(): ?NodeInterface 77 | { 78 | return $this->focusedNode; 79 | } 80 | 81 | public function withFocusedNode(NodeInterface $focusedNode = null): CommandContext 82 | { 83 | $instance = clone $this; 84 | $instance->focusedNode = $focusedNode; 85 | return $instance; 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /Classes/Domain/CommandInvocationResult.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | protected array $uiFeedback; 31 | 32 | /** 33 | * @param mixed $result has to be json serializable 34 | */ 35 | public function __construct(bool $success, $result, array $uiFeedback = []) 36 | { 37 | $this->success = $success; 38 | $this->result = $result; 39 | $this->uiFeedback = $uiFeedback; 40 | } 41 | 42 | public function isSuccess(): bool 43 | { 44 | return $this->success; 45 | } 46 | 47 | /** 48 | * @return mixed 49 | */ 50 | public function getResult() 51 | { 52 | return $this->result; 53 | } 54 | 55 | public function getUiFeedback(): array 56 | { 57 | return $this->uiFeedback; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Classes/Domain/Dto/NodeResult.php: -------------------------------------------------------------------------------- 1 | getParent(); 27 | while ($parent) { 28 | if ($parent->getNodeType()->isOfType('Neos.Neos:Node')) { 29 | $breadcrumbs[] = $parent->getLabel(); 30 | } 31 | $parent = $parent->getParent(); 32 | } 33 | 34 | return new self( 35 | $node->getIdentifier(), 36 | $node->getLabel(), 37 | $node->getNodeType()->getLabel(), 38 | $node->getNodeType()->getConfiguration('ui.icon') ?? 'question', 39 | implode(' / ', array_reverse($breadcrumbs)), 40 | $uri, 41 | $score, 42 | ); 43 | } 44 | 45 | /** 46 | * @return array{__typename: string, identifier: string, label: string, nodeType: string, icon: string, breadcrumb: string, uri: string, score: float} 47 | */ 48 | public function toArray(): array 49 | { 50 | return [ 51 | '__typename' => 'NodeResult', 52 | 'identifier' => $this->identifier, 53 | 'label' => $this->label, 54 | 'nodeType' => $this->nodeType, 55 | 'icon' => $this->icon, 56 | 'breadcrumb' => $this->breadcrumb, 57 | 'uri' => $this->uri, 58 | 'score' => $this->score, 59 | ]; 60 | } 61 | 62 | /** 63 | * @return array{__typename: string, identifier: string, label: string, nodeType: string, icon: string, breadcrumb: string, uri: string, score: float} 64 | */ 65 | public function jsonSerialize(): array 66 | { 67 | return $this->toArray(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Classes/Exception.php: -------------------------------------------------------------------------------- 1 | initialized) { 44 | return; 45 | } 46 | $this->initialized = true; 47 | 48 | if ($this->getParsedMatcher() === '*') { 49 | $methodPrivilegeMatcher = 'within(' . TerminalCommandInterface::class . ') && method(public .*->invokeCommand())'; 50 | } else { 51 | $commandNames = TerminalCommandService::detectCommandNames($this->objectManager); 52 | if (array_key_exists($this->getParsedMatcher(), $commandNames)) { 53 | $commandClassName = $commandNames[$this->getParsedMatcher()]; 54 | } else { 55 | throw new InvalidPolicyException(sprintf('Command %s not found', $this->getParsedMatcher()), 1614933733); 56 | } 57 | $methodPrivilegeMatcher = 'method(' . $commandClassName . '->invokeCommand())'; 58 | } 59 | $methodPrivilegeTarget = new PrivilegeTarget($this->privilegeTarget->getIdentifier() . '__methodPrivilege', MethodPrivilege::class, $methodPrivilegeMatcher); 60 | $methodPrivilegeTarget->injectObjectManager($this->objectManager); 61 | $this->methodPrivilege = $methodPrivilegeTarget->createPrivilege($this->getPermission(), $this->getParameters()); 62 | } 63 | 64 | /** 65 | * Returns a string which distinctly identifies this object and thus can be used as an identifier for cache entries 66 | * related to this object. 67 | * 68 | * @throws SecurityException 69 | */ 70 | public function getCacheEntryIdentifier(): string 71 | { 72 | $this->initialize(); 73 | return $this->methodPrivilege->getCacheEntryIdentifier(); 74 | } 75 | 76 | /** 77 | * Returns true, if this privilege covers the given subject 78 | * 79 | * @throws InvalidPrivilegeTypeException|SecurityException if the given $subject is not supported by the privilege 80 | */ 81 | public function matchesSubject(PrivilegeSubjectInterface $subject): bool 82 | { 83 | if (!($subject instanceof TerminalCommandPrivilegeSubject) && !($subject instanceof MethodPrivilegeSubject)) { 84 | throw new InvalidPrivilegeTypeException( 85 | sprintf( 86 | 'Privileges of type "%s" only support subjects of type "%s" or "%s", but we got a subject of type: "%s".', 87 | self::class, 88 | TerminalCommandPrivilegeSubject::class, 89 | MethodPrivilegeSubject::class, 90 | get_class($subject) 91 | ), 92 | 1614872267 93 | ); 94 | } 95 | $this->initialize(); 96 | if ($subject instanceof MethodPrivilegeSubject) { 97 | return $this->methodPrivilege->matchesSubject($subject); 98 | } 99 | return $this->getParsedMatcher() === '*' || $subject->getCommandName() === $this->getParsedMatcher(); 100 | } 101 | 102 | /** 103 | * @param string $className 104 | * @param string $methodName 105 | * @throws SecurityException 106 | */ 107 | public function matchesMethod($className, $methodName): bool 108 | { 109 | $this->initialize(); 110 | return $this->methodPrivilege->matchesMethod($className, $methodName); 111 | } 112 | 113 | /** 114 | * @return PointcutFilterInterface 115 | * @throws SecurityException 116 | */ 117 | public function getPointcutFilterComposite(): PointcutFilterInterface 118 | { 119 | $this->initialize(); 120 | return $this->methodPrivilege->getPointcutFilterComposite(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Classes/Security/TerminalCommandPrivilegeSubject.php: -------------------------------------------------------------------------------- 1 | commandName = $commandName; 25 | } 26 | 27 | public function getCommandName(): string 28 | { 29 | return $this->commandName; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Classes/Service/EelEvaluationService.php: -------------------------------------------------------------------------------- 1 | defaultContextVariables === null) { 48 | $this->defaultContextVariables = EelUtility::getDefaultContextVariables($this->defaultContext); 49 | } 50 | $contextVariables = array_merge($this->defaultContextVariables, $contextVariables); 51 | return EelUtility::evaluateEelExpression($expression, $this->eelEvaluator, $contextVariables); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Classes/Service/SerializationService.php: -------------------------------------------------------------------------------- 1 | $node->getIdentifier(), 50 | '_nodeType' => $node->getNodeType()->getName(), 51 | '_name' => $node->getName(), 52 | '_workspace' => $node->getWorkspace()->getName(), 53 | '_path' => $node->getPath(), 54 | ]; 55 | 56 | try { 57 | foreach ($node->getProperties()->getIterator() as $key => $property) { 58 | if (is_object($property)) { 59 | $property = get_class($property); 60 | } 61 | if (is_array($property)) { 62 | $property = '[…]'; 63 | } 64 | $result[$key] = $property; 65 | } 66 | } catch (\Exception $e) { 67 | } 68 | 69 | ksort($result); 70 | 71 | return $result; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Classes/Service/TerminalCommandService.php: -------------------------------------------------------------------------------- 1 | objectManager = $objectManager; 21 | } 22 | 23 | /** 24 | * Detects plugins for this command controller 25 | * 26 | * @Flow\CompileStatic 27 | * @return array 28 | */ 29 | public static function detectCommandNames(ObjectManagerInterface $objectManager): array 30 | { 31 | $commandConfiguration = []; 32 | $classNames = $objectManager->get(ReflectionService::class)->getAllImplementationClassNamesForInterface(TerminalCommandInterface::class); 33 | foreach ($classNames as $className) { 34 | $objectName = $objectManager->getObjectNameByClassName($className); 35 | /** @var TerminalCommandInterface $objectName */ 36 | if ($objectName) { 37 | $commandConfiguration[$objectName::getCommandName()] = $className; 38 | } 39 | } 40 | return $commandConfiguration; 41 | } 42 | 43 | public function getCommand(string $commandName): TerminalCommandInterface 44 | { 45 | /** @var TerminalCommandInterface $command */ 46 | $command = $this->objectManager->get($this->getCommandClassName($commandName)); 47 | return $command; 48 | } 49 | 50 | /** 51 | * @return array 52 | */ 53 | public function getCommandNames(): array 54 | { 55 | return array_keys(self::detectCommandNames($this->objectManager)); 56 | } 57 | 58 | public function getCommandClassName(string $commandName): string 59 | { 60 | $commandNames = self::detectCommandNames($this->objectManager); 61 | 62 | if (!array_key_exists($commandName, $commandNames)) { 63 | // TODO: Add message 64 | throw new \InvalidArgumentException($commandName . json_encode($commandNames), 1614873907); 65 | } 66 | 67 | return $commandNames[$commandName]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Configuration/Development/Settings.yaml: -------------------------------------------------------------------------------- 1 | Neos: 2 | Neos: 3 | Ui: 4 | frontendConfiguration: 5 | 'Shel.Neos.Terminal:Terminal': 6 | enabled: true 7 | -------------------------------------------------------------------------------- /Configuration/Policy.yaml: -------------------------------------------------------------------------------- 1 | privilegeTargets: 2 | 'Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege': 3 | 'Shel.Neos.Terminal:ExecuteCommands': 4 | matcher: 'method(Shel\Neos\Terminal\Controller\TerminalCommandController->(?!initialize).*Action())' 5 | 6 | 'Shel.Neos.Terminal:GetCommands': 7 | matcher: 'method(Shel\Neos\Terminal\Controller\TerminalCommandController->getCommandsAction())' 8 | 9 | 'Shel\Neos\Terminal\Security\TerminalCommandPrivilege': 10 | 'Shel.Neos.Terminal:Command.All': 11 | matcher: '*' 12 | 'Shel.Neos.Terminal:Command.Eel': 13 | matcher: 'eel' 14 | 'Shel.Neos.Terminal:Command.FlushCache': 15 | matcher: 'flushCache' 16 | 'Shel.Neos.Terminal:Command.Search': 17 | matcher: 'search' 18 | 'Shel.Neos.Terminal:Command.NodeRepair': 19 | matcher: 'nodeRepair' 20 | 21 | roles: 22 | 'Neos.Flow:Everybody': 23 | privileges: 24 | # Allow everybody to load commands to prevent 403 errors for users without access in the UI. 25 | # The command list will still be empty in the response as all commands have their own privileges. 26 | - privilegeTarget: 'Shel.Neos.Terminal:GetCommands' 27 | permission: GRANT 28 | 29 | 'Shel.Neos.Terminal:TerminalUser': 30 | label: 'Terminal user' 31 | description: 'Grants access to run read-only eel and search terminal commands' 32 | privileges: 33 | - privilegeTarget: 'Shel.Neos.Terminal:ExecuteCommands' 34 | permission: GRANT 35 | - privilegeTarget: 'Shel.Neos.Terminal:Command.Eel' 36 | permission: GRANT 37 | - privilegeTarget: 'Shel.Neos.Terminal:Command.Search' 38 | permission: GRANT 39 | 40 | 'Neos.Neos:Administrator': 41 | privileges: 42 | - privilegeTarget: 'Shel.Neos.Terminal:ExecuteCommands' 43 | permission: GRANT 44 | - privilegeTarget: 'Shel.Neos.Terminal:Command.All' 45 | permission: GRANT 46 | -------------------------------------------------------------------------------- /Configuration/Routes.yaml: -------------------------------------------------------------------------------- 1 | - name: 'Shel.Neos.Terminal:Commands' 2 | uriPattern: 'neos/shel-neos-terminal/{@action}' 3 | defaults: 4 | '@package': 'Shel.Neos.Terminal' 5 | '@controller': 'TerminalCommand' 6 | '@format': 'json' 7 | appendExceedingArguments: true 8 | -------------------------------------------------------------------------------- /Configuration/Settings.Flow.yaml: -------------------------------------------------------------------------------- 1 | Neos: 2 | Flow: 3 | mvc: 4 | routes: 5 | 'Shel.Neos.Terminal': 6 | position: 'start' 7 | 8 | security: 9 | authentication: 10 | providers: 11 | 'Neos.Neos:Backend': 12 | requestPatterns: 13 | 'Shel.Neos.Terminal:Commands': 14 | pattern: ControllerObjectName 15 | patternOptions: 16 | controllerObjectNamePattern: 'Shel\Neos\Terminal\Controller\.*' 17 | -------------------------------------------------------------------------------- /Configuration/Settings.yaml: -------------------------------------------------------------------------------- 1 | Neos: 2 | Neos: 3 | Ui: 4 | resources: 5 | javascript: 6 | Shel.Neos.Terminal:Terminal: 7 | resource: resource://Shel.Neos.Terminal/Public/Assets/Plugin.js 8 | stylesheets: 9 | Shel.Neos.Terminal:Terminal: 10 | resource: resource://Shel.Neos.Terminal/Public/Assets/Plugin.css 11 | 12 | frontendConfiguration: 13 | 'Shel.Neos.Terminal:Terminal': 14 | enabled: false 15 | welcomeMessage: 'Shel.Neos.Terminal:Main:welcome' 16 | registrationKey: 17 | id: '' 18 | signature: '' 19 | showThankYouMessage: true 20 | getCommandsEndPoint: '/neos/shel-neos-terminal/getcommands' 21 | invokeCommandEndPoint: '/neos/shel-neos-terminal/invokecommand' 22 | theme: 23 | contentStyle: 24 | color: '#adadad' 25 | styleEchoBack: 'fullInherit' 26 | promptLabelStyle: 27 | color: '#ff8700' 28 | whitespace: 'nowrap' 29 | inputTextStyle: 30 | color: '#ffffff' 31 | messageStyle: 32 | whiteSpace: 'pre-wrap' 33 | color: '#00adee' 34 | 35 | hotkeys: 36 | 'Shel.Neos.Terminal.toggle': 't t' 37 | 38 | userInterface: 39 | translation: 40 | autoInclude: 41 | Shel.Neos.Terminal: ['Main'] 42 | 43 | -------------------------------------------------------------------------------- /Documentation/shel-neos-terminal-example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sebobo/Shel.Neos.Terminal/9356ac203f0595816c034b10828290b00ab6c6f7/Documentation/shel-neos-terminal-example.jpg -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Sebastian Helzle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Neos CMS terminal for running Eel expressions and other commands in the UI 2 | 3 | [![Tests](https://github.com/Sebobo/Shel.Neos.Terminal/actions/workflows/tests.yml/badge.svg)](https://github.com/Sebobo/Shel.Neos.Terminal/actions/workflows/tests.yml) 4 | 5 | This package provides a Terminal emulator plugin for the [Neos CMS](https://www.neos.io) UI. 6 | Several commands are provided to save time during development & debugging of Neos CMS projects. 7 | 8 | All commands and their output are also automatically available in the browser dev console 9 | as `NeosTerminal` - for easier handling of JSON results and persistent history. 10 | 11 | It uses the great [terminal component](https://github.com/linuswillner/react-console-emulator) by [Linus Willner](https://github.com/linuswillner). 12 | 13 | **Quick overview:** 14 | 15 | * Run commands via the Neos UI or in the browser console 16 | * Evaluate Eel expressions on the currently selected page & content 17 | * Search for nodes by their properties 18 | * Flush caches 19 | * Repair nodes 20 | * Autocompletion for command names 21 | * Open Terminal via `t+t` shortcut 22 | * Limit commands to backend roles 23 | * Create your own commands and provide them in your own packages 24 | * Full support for the [Shel.Neos.CommandBar](https://github.com/Sebobo/Shel.Neos.CommandBar) 25 | 26 | ## How it looks 27 | 28 | Watch the [video](https://vimeo.com/manage/videos/538570712) 29 | 30 | ![Terminal example in the Neos CMS backend](Documentation/shel-neos-terminal-example.jpg) 31 | 32 | ## Installation 33 | 34 | Run the following command in your Neos project: 35 | 36 | ```shell 37 | composer require shel/neos-terminal 38 | ``` 39 | 40 | ### Supported Neos versions 41 | 42 | Due to required React features, the Terminal UI integration is only available 43 | for more recent Neos versions which provide React >= 16.8. 44 | But the commands are still registered and available via the `NeosTerminal` global 45 | window object even when the React version is too old. 46 | 47 | | Neos version | Terminal | 48 | | ------------- | ---------------- | 49 | | 4.3 | Commands are only available via the browser console | 50 | | 5.2 - 5.3 | Full support | 51 | | 7.0+ | Full support | 52 | | 8.0+ | Full support | 53 | 54 | ## Usage 55 | 56 | There are three ways to access the terminal functionality: 57 | 58 | 1. Open the terminal by clicking on the terminal icon in the top menu bar in the Neos backend. 59 | 2. Open the terminal with the `t t` hotkey (configured via the Neos hotkey API in the `Settings.yaml`). 60 | 3. Access the terminal commands from the browser dev console via the global `NeosTerminal` object. 61 | 62 | Now you can run any of the provided commands, or your own. 63 | 64 | ## Included commands 65 | 66 | Available default commands: 67 | 68 | * `eel` - Eel expression parser 69 | * `flushCache` - Flush one or all Neos/Flow caches 70 | * `help` - Show command list and their arguments 71 | * `clear` - Clear terminal 72 | * `search` - Search for nodes by their properties 73 | * `nodeRepair` - Repair nodes 74 | 75 | You can add [custom commands](#adding-your-own-commands). 76 | 77 | ### Eel evaluator 78 | 79 | The `eel` command allows you to run any Eel expression. 80 | 81 | You can run simple expressions: 82 | 83 | ``` 84 | eel 5+2 85 | ``` 86 | 87 | Read a specific setting: 88 | 89 | ``` 90 | eel Configuration.setting('Neos.Flow.core.context') 91 | ``` 92 | 93 | Get a list of all Eel helpers: 94 | 95 | ``` 96 | eel Configuration.setting('Neos.Fusion.defaultContext') 97 | ``` 98 | 99 | Or more complex ones. The following call will return the labels of all subpages of your homepage: 100 | 101 | ``` 102 | eel Array.map(q(site).children().get(), page => page.label) 103 | ``` 104 | 105 | By default, the current `site`, `documentNode` and the currently selected `node` are 106 | available in your expression context. 107 | 108 | *Note:* The command will run some conversions on the result: 109 | 110 | * If the result is a node or a list of nodes, each node will be replaced 111 | with a list of their `properties`. 112 | * Properties that are objects are replaced with their classname. 113 | 114 | This will be optimised in future releases and should improve the readability of the output. 115 | 116 | ### Flush caches 117 | 118 | The `flushCache` command allows you to flush all caches or a single cache. 119 | 120 | E.g. the following call will flush the Fusion rendering cache: 121 | 122 | ``` 123 | flushCache Neos_Fusion_Content 124 | ``` 125 | 126 | If the cache identifier is omitted, all caches are flushed. 127 | 128 | Please use this command only when absolutely necessary. 129 | Caching issues can be fixed in the implementation. 130 | 131 | ### Repair nodes 132 | 133 | The `nodeRepair` command allows you to repair nodes by their nodetype. 134 | It uses the same plugins and methods as the `./flow node:repair` CLI command. 135 | 136 | E.g. the following call will remove undefined properties from the Neos example text nodes: 137 | 138 | ``` 139 | nodeRepair removeUndefinedProperties Neos.NodeTypes:Text 140 | ``` 141 | 142 | You can also do a dry run by adding the `-d` option and see what the method would do: 143 | 144 | ``` 145 | nodeRepair --dryRun removeUndefinedProperties Neos.NodeTypes:Text 146 | ``` 147 | 148 | To filter by workspace you can add the name of the workspace: 149 | 150 | ``` 151 | nodeRepair --workspace user-admin removeUndefinedProperties Neos.NodeTypes:Text 152 | ``` 153 | 154 | **Warning:** Some repair methods would ask you for confirmation when you run them via CLI. 155 | Currently they would execute without asking for confirmation. 156 | 157 | ## Configuration 158 | 159 | ### Enabling the plugin in Production context 160 | 161 | By default, the plugin is only loaded in *Development* context. 162 | If you want to have it active in *Production*, you have to override the setting in your `Settings.yaml`: 163 | 164 | ```yaml 165 | Neos: 166 | Neos: 167 | Ui: 168 | frontendConfiguration: 169 | 'Shel.Neos.Terminal:Terminal': 170 | enabled: true 171 | ``` 172 | 173 | ### Security 174 | 175 | Executing commands in the Neos backend opens up a possible security risk. 176 | 177 | Therefore, if you use this plugin in production, make sure only a limited 178 | number of users have access to it. 179 | 180 | When creating your own commands, keep in mind to make sure nothing bad can happen to your 181 | database or other systems. 182 | 183 | Example: If you have your own Eel helper that can send API requests to another system 184 | with full write access, this could be abused by someone if a backend user with 185 | enough privileges is hacked. 186 | 187 | ### Theming 188 | 189 | Have a look at the [Settings.yaml](Configuration/Settings.yaml) in this package and its `frontendConfiguration`. 190 | It allows you to override the theme with your own. 191 | 192 | ### Command policies 193 | 194 | By default, any *Administrator* has full access to all existing and added commands. 195 | 196 | Additionally, the role `Shel.Neos.Terminal:TerminalUser` exists which by default can only run the `eel` command. 197 | You can add more privileges to this role to allow more commands and assign it to users or as a `parentRole` for other roles. 198 | See [Policy.yaml](Configuration/Policy.yaml) in this package for examples. 199 | 200 | ## Adding your own commands 201 | 202 | Adding your commands takes just a few steps (depending on what you plan to do). 203 | 204 | Create a new class named `MyCommand` and implement the `TerminalCommandControllerPluginInterface` from 205 | this package or inherit from `AbstractTerminalCommand`. 206 | As soon as you implemented all required methods, you are good to go! 207 | 208 | As an example, you can create a command to show the joke of the day with the following class. 209 | Just adapt the namespace depending on your own package key. 210 | 211 | ```php 212 | ]'; 239 | } 240 | 241 | public function invokeCommand(string $argument, CommandContext $commandContext): CommandInvocationResult 242 | { 243 | $browser = new Browser(); 244 | $browser->setRequestEngine(new CurlEngine()); 245 | $jokeResponse = json_decode($browser->request('https://api.jokes.one/jod')->getBody()->getContents()); 246 | $joke = $jokeResponse->contents->jokes[0]->joke; 247 | 248 | $result = $joke->title . ': ' . $joke->text; 249 | 250 | return new CommandInvocationResult(true, $result); 251 | } 252 | } 253 | ``` 254 | 255 | Did you create awesome commands that could be helpful to others? 256 | Send a link to a [gist](https://gist.github.com) containing the PHP class or a link to your repo, and we can add it to the docs. 257 | 258 | ### Providing feedback to Neos UI 259 | 260 | The Neos UI supports `ServerFeedbacks`. Those are commonly used to trigger reload of 261 | nodes and documents or changing the state of nodes after manipulating them. 262 | 263 | You can add those feedbacks to the `CommandInvocationResult`. 264 | 265 | An example of a invocation method which triggers a reload of the Neos UI guestframe 266 | after a node has been updated execution would look like this: 267 | 268 | ```php 269 | public function invokeCommand(string $argument, CommandContext $commandContext): CommandInvocationResult 270 | { 271 | $newNodeTitle = $argument; 272 | $commandContext->getFocusedNode()->setProperty('title', $newNodeTitle); 273 | $result = 'I updated the node title'; 274 | $feedback = [ 275 | new \Neos\Neos\Ui\Domain\Model\Feedback\Operations\ReloadDocument() 276 | ]; 277 | 278 | return new CommandInvocationResult(true, $result, $feedback); 279 | } 280 | ``` 281 | 282 | If you have a Neos UI plugin that has its own registered feedbacks you can trigger them too. 283 | 284 | ### Providing commands in other packages 285 | 286 | If you have a package that provides a command, you should check whether the 287 | Terminal is installed in your code when defining the command. 288 | 289 | In order to achieve that, you have to wrap the command class in a condition 290 | and use the fully qualified name to reference classes and interfaces from the 291 | Terminal package: 292 | 293 | ```php 294 | if (interface_exists('Shel\Neos\Terminal\Command\TerminalCommandInterface', false)) { 295 | class JokeCommand implements \Shel\Neos\Terminal\Command\TerminalCommandInterface 296 | { 297 | public static function getCommandName(): string { ... } 298 | public static function getCommandDescription(): string { ... } 299 | public static function getCommandUsage(): string { ... } 300 | 301 | public function invokeCommand(string $argument, \Shel\Neos\Terminal\Domain\CommandContext $commandContext): \Shel\Neos\Terminal\Domain\CommandInvocationResult { 302 | ... 303 | 304 | return new \Shel\Neos\Terminal\Domain\CommandInvocationResult(true, $result); 305 | } 306 | } 307 | } else { 308 | class JodCommand {} 309 | } 310 | ``` 311 | 312 | ## Supporting this plugin / how to get rid of the sponsorship badge 313 | 314 | Creating and maintaining a plugin like this takes a lot of time. 315 | Therefore, I decided to add a small nagging badge to promote financial support for my work. 316 | 317 | There are several ways to get rid of the little sponsoring badge in the terminal: 318 | 319 | 1. Get in touch with [me](sponsor@helzle.it) for a direct sponsoring of 100€ (excl. VAT) / registration key 320 | 2. Become a sponsor via [Github](https://github.com/sebobo) 20$+/month level 321 | 3. Become a [patreon](https://www.patreon.com/shelzle) 20$+/month level 322 | 323 | In return, you will feel much better, and you get a registration key you can put 324 | into your settings which will disable the mentioned badge. 325 | 326 | This will help me to further develop this and other plugins. 327 | Of course, I'll also do my best to react quickly to issues & questions. 328 | 329 | There is a 4th way: Fork this repo and patch the verification check (or whatever other way you might find). 330 | Sure you can do that. But you will receive bad karma, and you won't be helping the future of this plugin. 331 | 332 | If the badge doesn't bother you, that's fine too. Keep it and enjoy the plugin :). 333 | 334 | ## Contribute 335 | 336 | Contributions are very welcome. 337 | 338 | For code contributions, please create a fork and create a PR against the lowest maintained 339 | branch of this repository (currently master). 340 | 341 | * Don't include any generated file in `/Resources/Public/` in your change. 342 | * Please provide a thorough explanation of your change and well-formed commit messages in your commits. 343 | 344 | ### Run Tests 345 | 346 | Make sure you have the behat dependency required in your `composer.json` and run the following command: 347 | 348 | ```console 349 | FLOW_CONTEXT=Testing bin/phpunit -c Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Plugins/Shel.Neos.Terminal/Tests/Functional 350 | ``` 351 | 352 | ## License 353 | 354 | See [License](LICENSE.txt) 355 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/Terminal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import PropTypes from 'prop-types'; 4 | 5 | // Neos dependencies are provided by the UI 6 | // @ts-ignore 7 | import { neos } from '@neos-project/neos-ui-decorators'; 8 | // @ts-ignore 9 | import { selectors, actions } from '@neos-project/neos-ui-redux-store'; 10 | 11 | import ReplWrapper from './components/ReplWrapper'; 12 | import { CommandsProvider } from './provider/CommandsProvider'; 13 | import { Node, I18nRegistry, FeedbackEnvelope, NeosRootState } from './interfaces'; 14 | import { actions as terminalActions, selectors as terminalSelectors } from './actions'; 15 | 16 | interface TerminalProps { 17 | config: TerminalConfig; 18 | siteNode: Node; 19 | documentNode: Node; 20 | focusedNodes: string[]; 21 | i18nRegistry: I18nRegistry; 22 | terminalOpen: boolean; 23 | handleServerFeedback: (feedback: FeedbackEnvelope) => void; 24 | } 25 | 26 | class Terminal extends React.PureComponent { 27 | static propTypes = { 28 | config: PropTypes.object.isRequired, 29 | i18nRegistry: PropTypes.object.isRequired, 30 | user: PropTypes.object.isRequired, 31 | siteNode: PropTypes.object, 32 | documentNode: PropTypes.object, 33 | focusedNodes: PropTypes.array, 34 | terminalOpen: PropTypes.bool, 35 | toggleNeosTerminal: PropTypes.func, 36 | handleServerFeedback: PropTypes.func, 37 | }; 38 | 39 | render() { 40 | const { config } = this.props as TerminalProps; 41 | 42 | return ( 43 | 0 ? this.props.focusedNodes[0] : null} 47 | i18nRegistry={this.props.i18nRegistry} 48 | handleServerFeedback={this.props.handleServerFeedback} 49 | config={config} 50 | > 51 | 52 | 53 | ); 54 | } 55 | } 56 | 57 | const mapStateToProps = (state: NeosRootState) => ({ 58 | user: state?.user?.name, 59 | siteNode: selectors.CR.Nodes.siteNodeSelector(state), 60 | documentNode: selectors.CR.Nodes.documentNodeSelector(state), 61 | focusedNodes: selectors.CR.Nodes.focusedNodePathsSelector(state), 62 | terminalOpen: terminalSelectors.terminalOpen(state), 63 | }); 64 | 65 | const mapDispatchToProps = () => ({ handleServerFeedback: actions.ServerFeedback.handleServerFeedback }); 66 | 67 | const mapGlobalRegistryToProps = neos((globalRegistry: any) => ({ 68 | i18nRegistry: globalRegistry.get('i18n'), 69 | config: globalRegistry.get('frontendConfiguration').get('Shel.Neos.Terminal:Terminal'), 70 | })); 71 | 72 | export default connect(() => ({}), { toggleNeosTerminal: terminalActions.toggleNeosTerminal })( 73 | connect(mapStateToProps, mapDispatchToProps)(mapGlobalRegistryToProps(Terminal)) 74 | ); 75 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/actions/index.ts: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions } from 'redux-actions'; 2 | 3 | export const actionTypes = { 4 | TOGGLE_NEOS_TERMINAL: 'TOGGLE_NEOS_TERMINAL', 5 | }; 6 | 7 | const toggleNeosTerminal = createAction(actionTypes.TOGGLE_NEOS_TERMINAL); 8 | 9 | export const actions = { 10 | toggleNeosTerminal, 11 | }; 12 | 13 | export const reducer = handleActions( 14 | { 15 | TOGGLE_NEOS_TERMINAL: (state, action) => ({ 16 | ...state, 17 | plugins: { 18 | ...state.plugins, 19 | neosTerminal: { 20 | open: action.payload !== undefined ? action.payload.open : !state.plugins?.neosTerminal?.open, 21 | }, 22 | }, 23 | }), 24 | }, 25 | { 26 | plugins: { 27 | neosTerminal: { 28 | open: false, 29 | }, 30 | }, 31 | } 32 | ); 33 | 34 | export const selectors = { 35 | terminalOpen: (state) => state.plugins?.neosTerminal?.open, 36 | }; 37 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/components/ReplWrapper.css: -------------------------------------------------------------------------------- 1 | .replWrapper { 2 | position: relative; 3 | } 4 | 5 | .terminalWrapper { 6 | position: fixed; 7 | box-shadow: 0 5px 10px 5px rgba(0, 0, 0, .25); 8 | width: 100vw; 9 | left: 0; 10 | } 11 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/components/ReplWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo, useRef } from 'react'; 2 | import Terminal from 'react-console-emulator'; 3 | 4 | // @ts-ignore 5 | import { themr } from '@friendsofreactjs/react-css-themr'; 6 | 7 | // @ts-ignore 8 | import { IconButton } from '@neos-project/react-ui-components'; 9 | 10 | import { Node, RegistrationKey } from '../interfaces'; 11 | import SponsorshipBadge from './SponsorshipBadge'; 12 | import { useCommands } from '../provider/CommandsProvider'; 13 | 14 | // @ts-ignore 15 | import style from './ReplWrapper.css'; 16 | 17 | interface ReplProps { 18 | config: { 19 | theme: TerminalTheme; 20 | welcomeMessage?: string; 21 | registrationKey?: RegistrationKey; 22 | }; 23 | user: { 24 | firstName: string; 25 | lastName: string; 26 | fullName: string; 27 | }; 28 | siteNode: Node; 29 | documentNode: Node; 30 | className?: string; 31 | theme?: Record; 32 | terminalOpen?: boolean; 33 | toggleNeosTerminal: (visible?: boolean) => void; 34 | } 35 | 36 | const ReplWrapper: React.FC = ({ 37 | config, 38 | theme, 39 | user, 40 | siteNode, 41 | documentNode, 42 | terminalOpen, 43 | toggleNeosTerminal, 44 | }) => { 45 | const { invokeCommand, commands, translate } = useCommands(); 46 | const terminal = useRef(); 47 | 48 | const promptLabel = useMemo(() => { 49 | const currentPath = 50 | siteNode?.contextPath === documentNode?.contextPath ? '~' : documentNode?.properties?.uriPathSegment; 51 | return `${user.firstName}@${siteNode?.name}:${currentPath}$`; 52 | }, [user.firstName, siteNode?.name, documentNode?.contextPath, documentNode?.properties?.uriPathSegment]); 53 | 54 | const commandsDefinition = useMemo(() => { 55 | const newCommands = Object.keys(commands).reduce((carry, commandName) => { 56 | const command = commands[commandName]; 57 | 58 | // Register command globally 59 | window.NeosTerminal[commandName] = (...args) => invokeCommand(commandName, args); 60 | 61 | carry[commandName] = { 62 | ...command, 63 | description: translate(command.description ?? ''), 64 | fn: (...args) => { 65 | const currentTerminal = terminal.current; 66 | invokeCommand(commandName, args) 67 | .then((result) => { 68 | currentTerminal.state.stdout.pop(); 69 | let output = result; 70 | if (!result) { 71 | output = translate('command.empty'); 72 | } 73 | currentTerminal.pushToStdout(output); 74 | }) 75 | .catch((error) => { 76 | console.error( 77 | error, 78 | translate( 79 | 'command.invocationError', 80 | 'An error occurred during invocation of the "{commandName}" command', 81 | { commandName } 82 | ) 83 | ); 84 | currentTerminal.state.stdout.pop(); 85 | currentTerminal.pushToStdout(translate('command.error')); 86 | }); 87 | return translate('command.evaluating'); 88 | }, 89 | }; 90 | return carry; 91 | }, {}); 92 | 93 | newCommands['help'] = { 94 | name: 'help', 95 | description: translate('command.help.description'), 96 | usage: 'help ', 97 | fn: (commandName) => { 98 | const currentTerminal = terminal.current; 99 | if (!commandName) { 100 | currentTerminal.showHelp(); 101 | } else if (!commands[commandName]) { 102 | currentTerminal.pushToStdout(translate('command.help.unknownCommand')); 103 | } else { 104 | const command = commands[commandName]; 105 | currentTerminal.pushToStdout(`${translate(command.description)} - ${command.usage}`); 106 | } 107 | }, 108 | }; 109 | 110 | newCommands['clear'] = { 111 | name: 'clear', 112 | description: translate('command.clear.description'), 113 | usage: 'clear', 114 | fn: () => terminal.current.clearStdout(), 115 | }; 116 | 117 | return newCommands; 118 | }, [commands, invokeCommand]); 119 | 120 | const autocomplete = useCallback( 121 | (input) => { 122 | const commandNames = Object.keys(commands); 123 | const currentValue = input.value; 124 | const matches = commandNames.filter((key) => key.startsWith(currentValue)); 125 | 126 | if (!matches) return; 127 | 128 | if (matches.length === 1) { 129 | input.value = matches[0] + ' '; 130 | } else { 131 | const currentTerminal = terminal.current; 132 | const [lastItem] = currentTerminal.state.stdout.slice(-1); 133 | const message = translate('matchingCommands', 'Matching commands: {commands}', { 134 | commands: matches.join(' '), 135 | }); 136 | if (lastItem.message !== message) { 137 | currentTerminal.pushToStdout(message, { isEcho: true }); 138 | } 139 | } 140 | }, 141 | [commands] 142 | ); 143 | 144 | // Focus terminal when opened 145 | useEffect(() => { 146 | if (terminalOpen) { 147 | setTimeout(() => terminal.current?.focusTerminal(), 0); 148 | } 149 | }, [terminalOpen]); 150 | 151 | // Close terminal on ESC 152 | const onKeyUp = useCallback( 153 | (e) => { 154 | if (terminalOpen && e.keyCode === 27) { 155 | toggleNeosTerminal(false); 156 | } 157 | }, 158 | [terminalOpen] 159 | ); 160 | 161 | if (!Object.keys(commands).length) return null; 162 | 163 | return ( 164 |
165 | toggleNeosTerminal()} 167 | isActive={terminalOpen} 168 | title={translate('toggleTerminal')} 169 | icon="terminal" 170 | /> 171 |
172 | 184 | 185 |
186 |
187 | ); 188 | }; 189 | 190 | export default React.memo(themr('shel-neos-terminal/replWrapper', style)(ReplWrapper)); 191 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/components/SponsorshipBadge.css: -------------------------------------------------------------------------------- 1 | .sponsorshipWidget { 2 | position: absolute; 3 | right: 0; 4 | bottom: 0; 5 | } 6 | 7 | .sponsorshipWidget a { 8 | background: #323232; 9 | border-left: 1px solid #3f3f3f; 10 | border-top: 1px solid #3f3f3f; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | color: white; 15 | padding: .5rem 1rem; 16 | font-weight: bold; 17 | } 18 | 19 | .sponsorshipWidget a:hover { 20 | color: #00adee; 21 | } 22 | 23 | .sponsorshipWidget svg { 24 | margin-left: 1rem; 25 | width: 30px; 26 | height: auto; 27 | } 28 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/components/SponsorshipBadge.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | // @ts-ignore 4 | import style from './SponsorshipBadge.css'; 5 | 6 | import { useCommands } from '../provider/CommandsProvider'; 7 | import { RegistrationKey } from '../interfaces'; 8 | 9 | interface SponsorshipBadgeProps { 10 | registrationKey?: RegistrationKey; 11 | } 12 | 13 | const CONSOLE_PREFIX = 'Shel.Neos.Terminal:'; 14 | 15 | const SponsorshipBadge: React.FC = ({ registrationKey }) => { 16 | const { translate } = useCommands(); 17 | const [verified, setVerified] = useState(false); 18 | 19 | useEffect(() => { 20 | if (!registrationKey || !registrationKey.id || !registrationKey.signature) { 21 | return console.info(CONSOLE_PREFIX, translate('sponsorship.missing')); 22 | } 23 | 24 | const { id, signature } = registrationKey; 25 | const result = [...id.split('')].reduce((acc, char) => { 26 | acc = (acc << 5) - acc + char.charCodeAt(0); 27 | return acc & acc; 28 | }, 0); 29 | 30 | if ('V1' + result === atob(signature)) { 31 | if (registrationKey.showThankYouMessage) { 32 | console.info(CONSOLE_PREFIX, translate('sponsorship.verified')); 33 | } 34 | setVerified(true); 35 | } else { 36 | console.warn(CONSOLE_PREFIX, translate('sponsorship.invalid')); 37 | } 38 | }, [registrationKey]); 39 | 40 | if (verified) return null; 41 | 42 | return ( 43 | 76 | ); 77 | }; 78 | 79 | export default React.memo(SponsorshipBadge); 80 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/helpers/doInvokeCommand.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { fetchWithErrorHandling } from '@neos-project/neos-ui-backend-connector'; 3 | import { FeedbackEnvelope } from '../interfaces'; 4 | 5 | interface CommandInvocationResult { 6 | success: boolean; 7 | result: any; 8 | uiFeedback: FeedbackEnvelope; 9 | } 10 | 11 | const doInvokeCommand = async ( 12 | endPoint: string, 13 | commandName: string, 14 | args: string[], 15 | siteNode: string = null, 16 | focusedNode: string = null, 17 | documentNode: string = null 18 | ): Promise => { 19 | return fetchWithErrorHandling 20 | .withCsrfToken((csrfToken) => ({ 21 | url: endPoint, 22 | method: 'POST', 23 | credentials: 'include', 24 | headers: { 25 | 'X-Flow-Csrftoken': csrfToken, 26 | 'Content-Type': 'application/json', 27 | }, 28 | body: JSON.stringify({ 29 | commandName, 30 | argument: args.join(' '), 31 | siteNode, 32 | focusedNode, 33 | documentNode, 34 | }), 35 | })) 36 | .then((response) => response && response.json()); 37 | }; 38 | 39 | export default doInvokeCommand; 40 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/helpers/fetchCommands.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { fetchWithErrorHandling } from '@neos-project/neos-ui-backend-connector'; 3 | import { CommandList } from '../interfaces'; 4 | import logToConsole from './logger'; 5 | 6 | const fetchCommands = async (endPoint: string): Promise<{ success: boolean; result: CommandList }> => { 7 | return fetchWithErrorHandling 8 | .withCsrfToken((csrfToken: string) => ({ 9 | url: endPoint, 10 | method: 'GET', 11 | credentials: 'include', 12 | headers: { 13 | 'X-Flow-Csrftoken': csrfToken, 14 | 'Content-Type': 'application/json', 15 | }, 16 | })) 17 | .then((response: Response) => { 18 | if (!response.ok) { 19 | return { 20 | success: false, 21 | result: {}, 22 | }; 23 | } 24 | 25 | return ( 26 | response && 27 | response 28 | .json() 29 | .then((data: CommandList) => { 30 | return data; 31 | }) 32 | .catch((error: Error) => { 33 | return { 34 | success: false, 35 | result: {}, 36 | }; 37 | }) 38 | ); 39 | }) 40 | .catch((error: Error) => { 41 | logToConsole('error', error.message); 42 | return { 43 | success: false, 44 | result: {}, 45 | }; 46 | }); 47 | }; 48 | 49 | export default fetchCommands; 50 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/helpers/logger.ts: -------------------------------------------------------------------------------- 1 | const ConsoleStyle = { 2 | base: ['color: #fff', 'background-color: #00adee', 'font-weight: bold', 'padding: 2px 4px', 'border-radius: 2px'], 3 | error: ['color: #fff', 'background-color: red'], 4 | success: ['color: #fff', 'background-color: #00a338'], 5 | text: ['color:#fff'], 6 | }; 7 | 8 | type LogType = 'log' | 'error' | 'info' | 'warn'; 9 | 10 | const logToConsole = (type: LogType = 'log', text: string, ...args: any[]) => { 11 | let finalStyle = ConsoleStyle.base.join(';') + ';'; 12 | if (ConsoleStyle[type]) { 13 | finalStyle += ConsoleStyle[type].join(';'); 14 | } 15 | console[type](`%c[Neos.Terminal]%c ${text}:`, finalStyle, ConsoleStyle.text.join(';'), ...args); 16 | }; 17 | 18 | export default logToConsole; 19 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/index.js: -------------------------------------------------------------------------------- 1 | require('./manifest'); 2 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/interfaces/Command.ts: -------------------------------------------------------------------------------- 1 | export default interface Command { 2 | description: string; 3 | usage: string; 4 | name: string; 5 | fn?: (args?: any[]) => void; 6 | } 7 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/interfaces/CommandList.ts: -------------------------------------------------------------------------------- 1 | import Command from './Command'; 2 | 3 | export default interface CommandList { 4 | [key: string]: Command; 5 | } 6 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/interfaces/Feedback.ts: -------------------------------------------------------------------------------- 1 | // TODO: This is a copy of the interface in Neos.Ui and should preferably be made available to plugins 2 | export type Feedback = Readonly<{ 3 | type: string; 4 | description: string; 5 | payload: unknown; 6 | }>; 7 | 8 | export type FeedbackEnvelope = Readonly<{ 9 | feedbacks: Feedback[]; 10 | }>; 11 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/interfaces/I18nRegistry.ts: -------------------------------------------------------------------------------- 1 | // TODO: This is a copy of the interface in Neos.Ui and should preferably be made available to plugins 2 | export default interface I18nRegistry { 3 | translate: ( 4 | id?: string, 5 | fallback?: string, 6 | params?: Record | string[], 7 | packageKey?: string, 8 | sourceName?: string 9 | ) => string; 10 | } 11 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/interfaces/NeosRootState.ts: -------------------------------------------------------------------------------- 1 | import { DefaultRootState } from 'react-redux'; 2 | 3 | export default interface NeosRootState extends DefaultRootState { 4 | user?: { 5 | name?: string; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/interfaces/Node.ts: -------------------------------------------------------------------------------- 1 | // Copied from neos-ui 2 | export interface NodeChild { 3 | contextPath: NodeContextPath; 4 | nodeType: NodeTypeName; 5 | } 6 | export type NodeTypeName = string; 7 | export type NodeContextPath = string; 8 | export type DimensionPresetName = string; 9 | export type DimensionValue = string; 10 | export interface DimensionCombination { 11 | [propName: string]: DimensionValue[]; 12 | } 13 | export interface DimensionPresetCombination { 14 | [propName: string]: DimensionPresetName; 15 | } 16 | export type NodePolicy = Readonly<{ 17 | disallowedNodeTypes: NodeTypeName[]; 18 | canRemove: boolean; 19 | canEdit: boolean; 20 | disallowedProperties: string[]; 21 | }>; 22 | 23 | export interface Node { 24 | contextPath: NodeContextPath; 25 | name: string; 26 | identifier: string; 27 | nodeType: NodeTypeName; 28 | label: string; 29 | isAutoCreated: boolean; 30 | depth: number; 31 | children: NodeChild[]; 32 | matchesCurrentDimensions: boolean; 33 | properties: { 34 | [propName: string]: any; 35 | }; 36 | isFullyLoaded: boolean; 37 | uri: string; 38 | parent: NodeContextPath; 39 | policy?: NodePolicy; 40 | dimensions?: DimensionPresetCombination; 41 | otherNodeVariants?: DimensionPresetCombination[]; 42 | } 43 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/interfaces/RegistrationKey.ts: -------------------------------------------------------------------------------- 1 | export default interface RegistrationKey { 2 | id?: string; 3 | signature?: string; 4 | showThankYouMessage?: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | import Command from './Command'; 2 | import CommandList from './CommandList'; 3 | import { Node, NodeContextPath } from './Node'; 4 | import RegistrationKey from './RegistrationKey'; 5 | import I18nRegistry from './I18nRegistry'; 6 | import { Feedback, FeedbackEnvelope } from './Feedback'; 7 | import NeosRootState from './NeosRootState'; 8 | 9 | export { 10 | RegistrationKey, 11 | CommandList, 12 | Command, 13 | Node, 14 | NodeContextPath, 15 | I18nRegistry, 16 | Feedback, 17 | FeedbackEnvelope, 18 | NeosRootState, 19 | }; 20 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/manifest.js: -------------------------------------------------------------------------------- 1 | import manifest from '@neos-project/neos-ui-extensibility'; 2 | 3 | import { reducer, actions } from './actions'; 4 | import Terminal from './Terminal'; 5 | import getTerminalCommandRegistry from './registry/TerminalCommandRegistry'; 6 | 7 | window['NeosTerminal'] = window.NeosTerminal || {}; 8 | 9 | manifest('Shel.Neos.Terminal:Terminal', {}, (globalRegistry, { store, frontendConfiguration }) => { 10 | const config = frontendConfiguration['Shel.Neos.Terminal:Terminal']; 11 | 12 | if (!config.enabled) return; 13 | 14 | const i18nRegistry = globalRegistry.get('i18n'); 15 | const terminalCommandRegistry = getTerminalCommandRegistry(config, i18nRegistry, store); 16 | 17 | globalRegistry.get('reducers').set('Shel.Neos.Terminal', { reducer }); 18 | globalRegistry.get('containers').set('PrimaryToolbar/Middle/Terminal', Terminal); 19 | 20 | // TODO: Don't register hotkey if terminal is not accessible for the current user 21 | if (frontendConfiguration.hotkeys !== null && frontendConfiguration.hotkeys.length !== 0) { 22 | globalRegistry.get('hotkeys').set('Shel.Neos.Terminal.toggle', { 23 | description: 'Toggle Neos Terminal', 24 | action: actions.toggleNeosTerminal, 25 | }); 26 | } 27 | 28 | // Register commands for command bar if installed and commands are available 29 | const commandBarRegistry = globalRegistry.get('Shel.Neos.CommandBar'); 30 | if (commandBarRegistry) { 31 | commandBarRegistry.set('plugins/terminal', terminalCommandRegistry.getCommandsForCommandBar); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/provider/CommandsProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { createContext, useCallback, useContext, useEffect, useState } from 'react'; 3 | 4 | import { FeedbackEnvelope, I18nRegistry, CommandList, NodeContextPath } from '../interfaces'; 5 | import doInvokeCommand from '../helpers/doInvokeCommand'; 6 | import logToConsole from '../helpers/logger'; 7 | import getTerminalCommandRegistry from '../registry/TerminalCommandRegistry'; 8 | 9 | interface CommandsContextProps { 10 | children: React.ReactElement; 11 | config: TerminalConfig; 12 | siteNode: NodeContextPath; 13 | documentNode: NodeContextPath; 14 | focusedNode?: NodeContextPath; 15 | i18nRegistry: I18nRegistry; 16 | handleServerFeedback: (feedback: FeedbackEnvelope) => void; 17 | } 18 | 19 | interface CommandsContextValues { 20 | commands: CommandList; 21 | invokeCommand: (endPoint: string, param: string[]) => Promise; 22 | translate: ( 23 | id: string, 24 | fallback?: string, 25 | params?: Record | string[], 26 | packageKey?: string, 27 | sourceName?: string 28 | ) => string; 29 | } 30 | 31 | export const CommandsContext = createContext({} as CommandsContextValues); 32 | export const useCommands = (): CommandsContextValues => useContext(CommandsContext); 33 | 34 | export const CommandsProvider = ({ 35 | config, 36 | children, 37 | documentNode, 38 | focusedNode, 39 | siteNode, 40 | i18nRegistry, 41 | handleServerFeedback, 42 | }: CommandsContextProps) => { 43 | const [commands, setCommands] = useState({}); 44 | 45 | useEffect(() => { 46 | getTerminalCommandRegistry().getCommands().then(setCommands); 47 | }, []); 48 | 49 | const translate = useCallback( 50 | ( 51 | id: string, 52 | fallback = '', 53 | params: Record | string[] = [], 54 | packageKey = 'Shel.Neos.Terminal', 55 | sourceName = 'Main' 56 | ): string => { 57 | return i18nRegistry.translate(id, fallback, params, packageKey, sourceName); 58 | }, 59 | [] 60 | ); 61 | 62 | const invokeCommand = useCallback( 63 | async (commandName: string, args: string[]): Promise => { 64 | const command = commands[commandName]; 65 | 66 | if (!command) 67 | throw Error( 68 | translate('command.doesNotExist', `The command {commandName} does not exist!`, { commandName }) 69 | ); 70 | 71 | // TODO: Use TerminalCommandRegistry for invocation - needs some refactoring 72 | const { success, result, uiFeedback } = await doInvokeCommand( 73 | config.invokeCommandEndPoint, 74 | commandName, 75 | args, 76 | siteNode, 77 | focusedNode, 78 | documentNode 79 | ); 80 | let parsedResult = result; 81 | let textResult = result; 82 | // Try to prettify json results 83 | try { 84 | parsedResult = JSON.parse(result); 85 | if (typeof parsedResult !== 'string') { 86 | textResult = JSON.stringify(parsedResult, null, 2); 87 | } else { 88 | textResult = parsedResult; 89 | } 90 | } catch (e) { 91 | /* empty */ 92 | } 93 | logToConsole( 94 | success ? 'log' : 'error', 95 | translate('command.output', `"{commandName} {argument}":`, { 96 | commandName, 97 | argument: args.join(' '), 98 | }), 99 | parsedResult 100 | ); 101 | // Forward server feedback to the Neos UI 102 | if (uiFeedback) { 103 | handleServerFeedback(uiFeedback); 104 | } 105 | return textResult; 106 | }, 107 | [commands, siteNode, documentNode, focusedNode] 108 | ); 109 | 110 | return ( 111 | {children} 112 | ); 113 | }; 114 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/registry/TerminalCommandRegistry.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // @ts-ignore 4 | import { selectors, actions } from '@neos-project/neos-ui-redux-store'; 5 | 6 | import fetchCommands from '../helpers/fetchCommands'; 7 | import { CommandList, I18nRegistry, NeosRootState } from '../interfaces'; 8 | import doInvokeCommand from '../helpers/doInvokeCommand'; 9 | import Command from '../interfaces/Command'; 10 | import logToConsole from '../helpers/logger'; 11 | 12 | interface NeosStore { 13 | getState: () => NeosRootState; 14 | dispatch: (action: any) => void; 15 | } 16 | 17 | // noinspection JSPotentiallyInvalidUsageOfClassThis 18 | /** 19 | * Provides a registry for terminal commands for the Shel.Neos.CommandBar plugin 20 | */ 21 | class TerminalCommandRegistry { 22 | constructor(readonly config: TerminalConfig, readonly i18nRegistry: I18nRegistry, readonly store: NeosStore) { 23 | this.invokeCommand = this.invokeCommand.bind(this); 24 | } 25 | 26 | private commands: CommandList; 27 | private loading = false; 28 | 29 | public getCommands = async (): Promise => { 30 | // Wait for commands to be loaded if another call already requested them 31 | let i = 0; 32 | while (this.loading) { 33 | i++; 34 | await new Promise((resolve) => setTimeout(resolve, 100)); 35 | if (i > 100) { 36 | logToConsole('warn', 'Loading commands timed out'); 37 | break; 38 | } 39 | } 40 | if (this.commands) return this.commands; 41 | 42 | // Load commands 43 | this.loading = true; 44 | return (this.commands = await fetchCommands(this.config.getCommandsEndPoint) 45 | .then(({ result }) => result) 46 | .finally(() => (this.loading = false))); 47 | }; 48 | 49 | public translate = ( 50 | id: string, 51 | fallback = '', 52 | params: Record | string[] = [], 53 | packageKey = 'Shel.Neos.Terminal', 54 | sourceName = 'Main' 55 | ): string => { 56 | return this.i18nRegistry.translate(id, fallback, params, packageKey, sourceName); 57 | }; 58 | 59 | public getCommandsForCommandBar = async (): Promise> => { 60 | const commands = await this.getCommands(); 61 | const invokeCommand = this.invokeCommand; 62 | return Object.keys(commands).length > 0 63 | ? { 64 | 'shel.neos.terminal': { 65 | name: 'Terminal', 66 | description: 'Execute terminal commands', 67 | icon: 'terminal', 68 | subCommands: Object.values(commands).reduce((acc, { name, description }) => { 69 | acc[name] = { 70 | name, 71 | icon: 'terminal', 72 | description: this.translate(description), 73 | action: async function* (arg) { 74 | yield* invokeCommand(name, arg); 75 | }, 76 | canHandleQueries: true, 77 | executeManually: true 78 | }; 79 | return acc; 80 | }, {}) 81 | } 82 | } 83 | : {}; 84 | }; 85 | 86 | public invokeCommand = async function* (commandName: string, arg = '') { 87 | const state = this.store.getState(); 88 | const siteNode = selectors.CR.Nodes.siteNodeSelector(state); 89 | const documentNode = selectors.CR.Nodes.documentNodeSelector(state); 90 | const focusedNodes = selectors.CR.Nodes.focusedNodePathsSelector(state); 91 | const setActiveContentCanvasSrc = actions.UI.ContentCanvas.setSrc; 92 | const command = this.commands[commandName] as Command; 93 | 94 | if (!arg) { 95 | yield { 96 | success: true, 97 | message: this.translate( 98 | 'TerminalCommandRegistry.message.provideArguments', 99 | `Please provide arguments for command "${commandName}"`, 100 | { commandName } 101 | ), 102 | view: ( 103 |
104 |

{this.translate(command.description)}

105 | {command.usage} 106 |
107 | ) 108 | }; 109 | } else { 110 | const response = await doInvokeCommand( 111 | this.config.invokeCommandEndPoint, 112 | commandName, 113 | [arg], 114 | siteNode.contextPath, 115 | focusedNodes[0]?.contextPath, 116 | documentNode.contextPath 117 | ); 118 | 119 | let { success, result, uiFeedback } = response; 120 | 121 | if (uiFeedback) { 122 | this.store.dispatch(actions.ServerFeedback.handleServerFeedback(uiFeedback)); 123 | } 124 | 125 | // Try to prettify json results 126 | try { 127 | let parsedResult = JSON.parse(result); 128 | if (typeof parsedResult !== 'string') { 129 | if (typeof parsedResult === 'object') { 130 | parsedResult = Object.values(parsedResult); 131 | } 132 | if (Array.isArray(parsedResult)) { 133 | const resultType = parsedResult[0].__typename ?? ''; 134 | if (resultType === 'NodeResult') { 135 | yield { 136 | success: true, 137 | message: this.translate( 138 | 'TerminalCommandRegistry.message.nodeResults', 139 | `${parsedResult.length} results`, 140 | { matches: parsedResult.length } 141 | ), 142 | options: (parsedResult as NodeResult[]).reduce((carry, { 143 | identifier, 144 | label, 145 | nodeType, 146 | breadcrumb, 147 | uri, 148 | icon, 149 | score 150 | }) => { 151 | if (!uri) { 152 | // Skip nodes without uri 153 | return carry; 154 | } 155 | 156 | carry[identifier] = { 157 | id: identifier, 158 | name: label + (score ? ` ${score}` : ''), 159 | description: breadcrumb, 160 | category: nodeType, 161 | action: async () => { 162 | this.store.dispatch(setActiveContentCanvasSrc(uri)); 163 | }, 164 | closeOnExecute: true, 165 | icon 166 | }; 167 | return carry; 168 | }, {}) 169 | }; 170 | return; 171 | } 172 | } 173 | result = ( 174 |
175 |                             {JSON.stringify(parsedResult, null, 2)}
176 |                         
177 | ); 178 | } else { 179 | result =

{result.replace(/\\n/g, '\n')}

; 180 | } 181 | } catch (e) { 182 | // Treat result as simple string 183 | } 184 | 185 | yield { 186 | success, 187 | message: this.translate( 188 | 'TerminalCommandRegistry.message.result', 189 | `Result of command "${commandName}"`, 190 | { commandName } 191 | ), 192 | view: result 193 | }; 194 | } 195 | }; 196 | } 197 | 198 | let singleton = null; 199 | 200 | export default function getTerminalCommandRegistry( 201 | config?: TerminalConfig, 202 | i18nRegistry?: I18nRegistry, 203 | store?: NeosStore 204 | ): TerminalCommandRegistry { 205 | if (singleton) return singleton; 206 | if (!config) throw Error('No config provided for TerminalCommandRegistry'); 207 | if (!i18nRegistry) throw Error('No i18nRegistry provided for TerminalCommandRegistry'); 208 | if (!store) throw Error('No store provided for TerminalCommandRegistry'); 209 | return (singleton = new TerminalCommandRegistry(config, i18nRegistry, store)); 210 | } 211 | -------------------------------------------------------------------------------- /Resources/Private/JavaScript/Terminal/src/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | NeosTerminal: { 3 | [key: string]: (...args) => Promise; 4 | }; 5 | } 6 | 7 | interface TerminalConfig { 8 | getCommandsEndPoint: string; 9 | invokeCommandEndPoint: string; 10 | theme: TerminalTheme; 11 | welcomeMessage?: string; 12 | } 13 | 14 | interface TerminalTheme { 15 | contentStyle: Record | string; 16 | styleEchoBack: Record | string; 17 | promptLabelStyle: Record | string; 18 | inputTextStyle: Record | string; 19 | } 20 | 21 | interface NodeResult { 22 | __typename: 'NodeResult'; 23 | identifier: string; 24 | label: string; 25 | nodeType: string; 26 | icon: string; 27 | breadcrumb: string; 28 | uri: string; 29 | score: string; 30 | } 31 | -------------------------------------------------------------------------------- /Resources/Private/Translations/de/Main.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Terminal commands are disabled 7 | Terminalbefehle sind inaktiv 8 | 9 | 10 | Command "{command}" not found 11 | Befehl "{command}" nicht gefunden 12 | 13 | 14 | Matching commands: {commands} 15 | Passende Befehle: {commands} 16 | 17 | 18 | You are not permitted to invoke the "{command}" command 19 | Sie dürfen den Befehl "{command}" nicht ausführen 20 | 21 | 22 | Become a plugin sponsor 23 | Unterstütze diesen Plugin 24 | 25 | 26 | Sponsor this plugin to get rid of this badge :) 27 | Unterstütze diesen Plugin um diese Box verschwinden zu lassen :) 28 | 29 | 30 | Registration missing. You are using an unregistered version of this plugin. 31 | Registrierung fehlt. Sie verwenden eine unregistrierte Version dieses Plugins. 32 | 33 | 34 | Registration verified. Thanks for supporting the development of this plugin! 35 | Registrierung bestätigt. Danke für die Unterstützung dieses Plugins! 36 | 37 | 38 | Registration is invalid! Please get in touch to get support. 39 | Registrierung ist ungültig! Bitte kontaktieren Sie uns um Unterstützung zu erhalten. 40 | 41 | 42 | Welcome to the Neos terminal! Type 'help' to get a list of commands. 43 | Willkommen im Neos Terminal! Gebe 'help' ein für eine Liste aller Befehle. 44 | 45 | 46 | Toggle Neos Terminal 47 | Neos Terminal umschalten 48 | 49 | 50 | 51 | Evaluating… 52 | Auswerten… 53 | 54 | 55 | An error occurred during invocation of the "{commandName}" command 56 | Beim Ausführen des Befehls "{commandName}" ist ein Fehler aufgetreten 57 | 58 | 59 | An error has occurred. 60 | Ein Fehler ist aufgetreten. 61 | 62 | 63 | The command {commandName} does not exist! 64 | Der Befehl {commandName} existiert nicht! 65 | 66 | 67 | Output of command "{commandName} {argument}" 68 | Ausgabe des Befehls "{commandName} {argument}" 69 | 70 | 71 | The command returned an empty result. 72 | Der Befehl hat ein leeres Ergebnis zurückgegeben. 73 | 74 | 75 | 76 | Run an expression through the eel parser 77 | Einen Ausdruck durch den Eel-Parser ausführen lassen 78 | 79 | 80 | 81 | Search for nodes by their properties 82 | Nodes anhand ihrer Eigenschaften suchen 83 | 84 | 85 | Context node not available 86 | Context Node nicht verfügbar 87 | 88 | 89 | 90 | Flush all or a single cache 91 | Alle oder einen einzelnen Cache leeren 92 | 93 | 94 | Flushed all caches 95 | Alle Caches geleert 96 | 97 | 98 | The cache "{cacheIdentifier}" has been flushed 99 | Der Cache "{cacheIdentifier}" wurde geleert 100 | 101 | 102 | The cache "{cacheIdentifier}" does not exist 103 | Der Cache "{cacheIdentifier}" existiert nicht 104 | 105 | 106 | 107 | Repair nodes 108 | Inhalte reparieren 109 | 110 | 111 | NodeType "{nodeType}" not found 112 | Inhaltstyp "{nodeType}" nicht gefunden 113 | 114 | 115 | No result for the method "{methodName}" 116 | Kein Ergebnis für die Methode "{methodName}" 117 | 118 | 119 | 120 | Show help 121 | Hilfe anzeigen 122 | 123 | 124 | Unknown command 125 | Unbekannter Befehl 126 | 127 | 128 | 129 | Clear terminal 130 | Terminal löschen 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /Resources/Private/Translations/en/Main.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Terminal commands are disabled 7 | 8 | 9 | Command "{command}" not found 10 | 11 | 12 | Matching commands: {commands} 13 | 14 | 15 | You are not permitted to invoke the "{command}" command 16 | 17 | 18 | Become a plugin sponsor 19 | 20 | 21 | Sponsor this plugin to get rid of this badge :) 22 | 23 | 24 | Registration missing. You are using an unregistered version of this plugin. 25 | 26 | 27 | Registration verified. Thanks for supporting the development of this plugin! 28 | 29 | 30 | Registration is invalid! Please get in touch to get help. 31 | 32 | 33 | Welcome to the Neos terminal! Type 'help' to get a list of commands. 34 | 35 | 36 | Toggle Neos Terminal 37 | 38 | 39 | 40 | Evaluating… 41 | 42 | 43 | An error occurred during invocation of the "{commandName}" command 44 | 45 | 46 | An error has occurred. 47 | 48 | 49 | The command {commandName} does not exist! 50 | 51 | 52 | Output of command "{commandName} {argument}" 53 | 54 | 55 | The command returned an empty result. 56 | 57 | 58 | 59 | Run an expression through the eel parser 60 | 61 | 62 | 63 | Search for nodes by their properties 64 | 65 | 66 | Context node not available 67 | 68 | 69 | 70 | Flush all or a single cache 71 | 72 | 73 | Flushed all caches 74 | 75 | 76 | The cache "{cacheIdentifier}" has been flushed 77 | 78 | 79 | The cache "{cacheIdentifier}" does not exist 80 | 81 | 82 | 83 | Repair nodes 84 | 85 | 86 | NodeType "{nodeType}" not found 87 | 88 | 89 | No result for the method "{methodName}" 90 | 91 | 92 | 93 | Show help 94 | 95 | 96 | Unknown command 97 | 98 | 99 | 100 | Clear terminal 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /Resources/Public/Assets/Plugin.css: -------------------------------------------------------------------------------- 1 | .SponsorshipBadge__sponsorshipWidget___2tT6K { 2 | position: absolute; 3 | right: 0; 4 | bottom: 0; 5 | } 6 | 7 | .SponsorshipBadge__sponsorshipWidget___2tT6K a { 8 | background: #323232; 9 | border-left: 1px solid #3f3f3f; 10 | border-top: 1px solid #3f3f3f; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | color: white; 15 | padding: .5rem 1rem; 16 | font-weight: bold; 17 | } 18 | 19 | .SponsorshipBadge__sponsorshipWidget___2tT6K a:hover { 20 | color: #00adee; 21 | } 22 | 23 | .SponsorshipBadge__sponsorshipWidget___2tT6K svg { 24 | margin-left: 1rem; 25 | width: 30px; 26 | height: auto; 27 | } 28 | 29 | .ReplWrapper__replWrapper___2DBp3 { 30 | position: relative; 31 | } 32 | 33 | .ReplWrapper__terminalWrapper___2CSvE { 34 | position: fixed; 35 | box-shadow: 0 5px 10px 5px rgba(0, 0, 0, .25); 36 | width: 100vw; 37 | left: 0; 38 | } 39 | 40 | -------------------------------------------------------------------------------- /Resources/Public/Assets/Plugin.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///./src/components/SponsorshipBadge.css","webpack:///./src/components/ReplWrapper.css"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AC1BA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA","file":"./Plugin.css","sourcesContent":[".SponsorshipBadge__sponsorshipWidget___2tT6K {\n position: absolute;\n right: 0;\n bottom: 0;\n}\n\n.SponsorshipBadge__sponsorshipWidget___2tT6K a {\n background: #323232;\n border-left: 1px solid #3f3f3f;\n border-top: 1px solid #3f3f3f;\n display: flex;\n justify-content: center;\n align-items: center;\n color: white;\n padding: .5rem 1rem;\n font-weight: bold;\n}\n\n.SponsorshipBadge__sponsorshipWidget___2tT6K a:hover {\n color: #00adee;\n}\n\n.SponsorshipBadge__sponsorshipWidget___2tT6K svg {\n margin-left: 1rem;\n width: 30px;\n height: auto;\n}\n",".ReplWrapper__replWrapper___2DBp3 {\n position: relative;\n}\n\n.ReplWrapper__terminalWrapper___2CSvE {\n position: fixed;\n box-shadow: 0 5px 10px 5px rgba(0, 0, 0, .25);\n width: 100vw;\n left: 0;\n}\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /Resources/Public/Assets/Plugin.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=32)}([function(e,t,r){"use strict";var n,o=r(3),i=(n=o)&&n.__esModule?n:{default:n};e.exports=(0,i.default)("vendor")().React},function(e,t,r){"use strict";var n,o=r(3),i=(n=o)&&n.__esModule?n:{default:n};e.exports=(0,i.default)("vendor")().PropTypes},function(e,t,r){"use strict";var n,o=r(3),i=(n=o)&&n.__esModule?n:{default:n};e.exports=(0,i.default)("NeosProjectPackages")().NeosUiReduxStore},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){return function(){var t;if(window["@Neos:HostPluginAPI"]&&window["@Neos:HostPluginAPI"]["@"+e])return(t=window["@Neos:HostPluginAPI"])["@"+e].apply(t,arguments);throw new Error("You are trying to read from a consumer api that hasn't been initialized yet!")}}},function(e,t){e.exports=function(e){return e&&e.__esModule?e:{default:e}},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t,r){"use strict";const n={base:["color: #fff","background-color: #00adee","font-weight: bold","padding: 2px 4px","border-radius: 2px"],error:["color: #fff","background-color: red"],success:["color: #fff","background-color: #00a338"],text:["color:#fff"]};t.a=(e="log",t,...r)=>{let o=n.base.join(";")+";";n[e]&&(o+=n[e].join(";")),console[e](`%c[Neos.Terminal]%c ${t}:`,o,n.text.join(";"),...r)}},function(e,t){function r(t){return e.exports=r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e.exports.__esModule=!0,e.exports.default=e.exports,r(t)}e.exports=r,e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t){e.exports=function(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t,r){"use strict";r.r(t),r.d(t,"actionTypes",(function(){return o})),r.d(t,"actions",(function(){return i})),r.d(t,"reducer",(function(){return a})),r.d(t,"selectors",(function(){return s}));var n=r(13);const o={TOGGLE_NEOS_TERMINAL:"TOGGLE_NEOS_TERMINAL"},i={toggleNeosTerminal:Object(n.createAction)(o.TOGGLE_NEOS_TERMINAL)},a=Object(n.handleActions)({TOGGLE_NEOS_TERMINAL:(e,t)=>{var r,n;return{...e,plugins:{...e.plugins,neosTerminal:{open:void 0!==t.payload?t.payload.open:!(null===(n=null===(r=e.plugins)||void 0===r?void 0:r.neosTerminal)||void 0===n?void 0:n.open)}}}}},{plugins:{neosTerminal:{open:!1}}}),s={terminalOpen:e=>{var t,r;return null===(r=null===(t=e.plugins)||void 0===t?void 0:t.neosTerminal)||void 0===r?void 0:r.open}}},function(e,t,r){"use strict";var n=r(10);t.a=async(e,t,r,o=null,i=null,a=null)=>n.fetchWithErrorHandling.withCsrfToken(n=>({url:e,method:"POST",credentials:"include",headers:{"X-Flow-Csrftoken":n,"Content-Type":"application/json"},body:JSON.stringify({commandName:t,argument:r.join(" "),siteNode:o,focusedNode:i,documentNode:a})})).then(e=>e&&e.json())},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.fetchWithErrorHandling=void 0;var n,o=r(3),i=(n=o)&&n.__esModule?n:{default:n};t.default=(0,i.default)("NeosProjectPackages")().NeosUiBackendConnectorDefault;var a=(0,i.default)("NeosProjectPackages")().NeosUiBackendConnector.fetchWithErrorHandling;t.fetchWithErrorHandling=a},function(e,t,r){var n=r(50);e.exports=function(e,t){return e=e||{},Object.keys(t).forEach((function(r){void 0===e[r]&&(e[r]=n(t[r]))})),e}},function(e,t,r){"use strict";r.r(t),r.d(t,"default",(function(){return d}));var n=r(0),o=r.n(n),i=r(2),a=r(10),s=r(5);var u=async e=>a.fetchWithErrorHandling.withCsrfToken(t=>({url:e,method:"GET",credentials:"include",headers:{"X-Flow-Csrftoken":t,"Content-Type":"application/json"}})).then(e=>e.ok?e&&e.json().then(e=>e).catch(e=>({success:!1,result:{}})):{success:!1,result:{}}).catch(e=>(Object(s.a)("error",e.message),{success:!1,result:{}})),l=r(9);class c{constructor(e,t,r){this.config=e,this.i18nRegistry=t,this.store=r,this.loading=!1,this.getCommands=async()=>{let e=0;for(;this.loading;)if(e++,await new Promise(e=>setTimeout(e,100)),e>100){Object(s.a)("warn","Loading commands timed out");break}return this.commands?this.commands:(this.loading=!0,this.commands=await u(this.config.getCommandsEndPoint).then(({result:e})=>e).finally(()=>this.loading=!1))},this.translate=(e,t="",r=[],n="Shel.Neos.Terminal",o="Main")=>this.i18nRegistry.translate(e,t,r,n,o),this.getCommandsForCommandBar=async()=>{const e=await this.getCommands(),t=this.invokeCommand;return Object.keys(e).length>0?{"shel.neos.terminal":{name:"Terminal",description:"Execute terminal commands",icon:"terminal",subCommands:Object.values(e).reduce((e,{name:r,description:n})=>(e[r]={name:r,icon:"terminal",description:this.translate(n),action:async function*(e){yield*t(r,e)},canHandleQueries:!0,executeManually:!0},e),{})}}:{}},this.invokeCommand=async function*(e,t=""){var r,n;const a=this.store.getState(),s=i.selectors.CR.Nodes.siteNodeSelector(a),u=i.selectors.CR.Nodes.documentNodeSelector(a),c=i.selectors.CR.Nodes.focusedNodePathsSelector(a),f=i.actions.UI.ContentCanvas.setSrc,d=this.commands[e];if(t){const a=await Object(l.a)(this.config.invokeCommandEndPoint,e,[t],s.contextPath,null===(r=c[0])||void 0===r?void 0:r.contextPath,u.contextPath);let{success:d,result:p,uiFeedback:h}=a;h&&this.store.dispatch(i.actions.ServerFeedback.handleServerFeedback(h));try{let e=JSON.parse(p);if("string"!=typeof e){if("object"==typeof e&&(e=Object.values(e)),Array.isArray(e)){if("NodeResult"===(null!==(n=e[0].__typename)&&void 0!==n?n:""))return void(yield{success:!0,message:this.translate("TerminalCommandRegistry.message.nodeResults",e.length+" results",{matches:e.length}),options:e.reduce((e,{identifier:t,label:r,nodeType:n,breadcrumb:o,uri:i,icon:a,score:s})=>i?(e[t]={id:t,name:r+(s?" "+s:""),description:o,category:n,action:async()=>{this.store.dispatch(f(i))},closeOnExecute:!0,icon:a},e):e,{})})}p=o.a.createElement("pre",null,o.a.createElement("code",null,JSON.stringify(e,null,2)))}else p=o.a.createElement("p",null,p.replace(/\\n/g,"\n"))}catch(e){}yield{success:d,message:this.translate("TerminalCommandRegistry.message.result",`Result of command "${e}"`,{commandName:e}),view:p}}else yield{success:!0,message:this.translate("TerminalCommandRegistry.message.provideArguments",`Please provide arguments for command "${e}"`,{commandName:e}),view:o.a.createElement("div",null,o.a.createElement("p",null,this.translate(d.description)),o.a.createElement("code",null,d.usage))}},this.invokeCommand=this.invokeCommand.bind(this)}}let f=null;function d(e,t,r){if(f)return f;if(!e)throw Error("No config provided for TerminalCommandRegistry");if(!t)throw Error("No i18nRegistry provided for TerminalCommandRegistry");if(!r)throw Error("No store provided for TerminalCommandRegistry");return f=new c(e,t,r)}},function(e,t,r){"use strict";var n,o=r(3),i=(n=o)&&n.__esModule?n:{default:n};e.exports=(0,i.default)("vendor")().reduxActions},function(e,t,r){"use strict";var n,o=r(3),i=(n=o)&&n.__esModule?n:{default:n};e.exports=(0,i.default)("vendor")().reactRedux},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n=i(r(38)),o=i(r(39));function i(e){return e&&e.__esModule?e:{default:e}}var a=class extends n.default{constructor(e){super(e),this._registry=[]}set(e,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;if("string"!=typeof e)throw new Error("Key must be a string");if("string"!=typeof r&&"number"!=typeof r)throw new Error("Position must be a string or a number");var n={key:e,value:t};r&&(n.position=r);var o=this._registry.findIndex((function(t){return t.key===e}));return-1===o?this._registry.push(n):this._registry[o]=n,t}get(e){if("string"!=typeof e)return console.error("Key must be a string"),null;var t=this._registry.find((function(t){return t.key===e}));return t?t.value:null}_getChildrenWrapped(e){var t=this._registry.filter((function(t){return 0===t.key.indexOf(e+"/")}));return(0,o.default)(t)}getChildrenAsObject(e){var t={};return this._getChildrenWrapped(e).forEach((function(e){t[e.key]=e.value})),t}getChildren(e){return this._getChildrenWrapped(e).map((function(e){return e.value}))}has(e){return"string"!=typeof e?(console.error("Key must be a string"),!1):Boolean(this._registry.find((function(t){return t.key===e})))}_getAllWrapped(){return(0,o.default)(this._registry)}getAllAsObject(){var e={};return this._getAllWrapped().forEach((function(t){e[t.key]=t.value})),e}getAllAsList(){return this._getAllWrapped().map((function(e){return Object.assign({id:e.key},e.value)}))}};t.default=a},function(e,t){e.exports=function(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r1&&void 0!==arguments[1]?arguments[1]:"position",r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"key",n="string"==typeof t?function(e){return e[t]}:t,o={},i={},a={},s={},u={},l={};e.forEach((function(e,t){var c=e[r]?e[r]:String(t);o[c]=t;var f=n(e),d=String(f||t),p=!1;if(d.startsWith("start")){var h=d.match(/start\s+(\d+)/),y=h&&h[1]?Number(h[1]):0;a[y]||(a[y]=[]),a[y].push(c)}else if(d.startsWith("end")){var m=d.match(/end\s+(\d+)/),g=m&&m[1]?Number(m[1]):0;s[g]||(s[g]=[]),s[g].push(c)}else if(d.startsWith("before")){var v=d.match(/before\s+(\S+)(\s+(\d+))?/);if(v){var b=v[1],w=v[3]?Number(v[3]):0;u[b]||(u[b]={}),u[b][w]||(u[b][w]=[]),u[b][w].push(c)}else p=!0}else if(d.startsWith("after")){var _=d.match(/after\s+(\S+)(\s+(\d+))?/);if(_){var x=_[1],O=_[3]?Number(_[3]):0;l[x]||(l[x]={}),l[x][O]||(l[x][O]=[]),l[x][O].push(c)}else p=!0}else p=!0;if(p){var E=parseFloat(d);!isNaN(E)&&isFinite(E)||(E=t),i[E]||(i[E]=[]),i[E].push(c)}}));var c=[],f=[],d=[],p=[],h=function(e,t){var r=Object.keys(e).map((function(e){return Number(e)})).sort((function(e,t){return e-t}));return t?r:r.reverse()},y=function e(t,r){t.forEach((function(t){if(!(p.indexOf(t)>=0)){if(p.push(t),u[t]){var n=h(u[t],!0),o=!0,i=!1,a=void 0;try{for(var s,c=n[Symbol.iterator]();!(o=(s=c.next()).done);o=!0){var f=s.value;e(u[t][f],r)}}catch(e){i=!0,a=e}finally{try{!o&&c.return&&c.return()}finally{if(i)throw a}}}if(r.push(t),l[t]){var d=h(l[t],!1),y=!0,m=!1,g=void 0;try{for(var v,b=d[Symbol.iterator]();!(y=(v=b.next()).done);y=!0){var w=v.value;e(l[t][w],r)}}catch(e){m=!0,g=e}finally{try{!y&&b.return&&b.return()}finally{if(m)throw g}}}}}))},m=!0,g=!1,v=void 0;try{for(var b,w=h(a,!1)[Symbol.iterator]();!(m=(b=w.next()).done);m=!0){var _=b.value;y(a[_],c)}}catch(e){g=!0,v=e}finally{try{!m&&w.return&&w.return()}finally{if(g)throw v}}var x=!0,O=!1,E=void 0;try{for(var P,S=h(i,!0)[Symbol.iterator]();!(x=(P=S.next()).done);x=!0){var j=P.value;y(i[j],f)}}catch(e){O=!0,E=e}finally{try{!x&&S.return&&S.return()}finally{if(O)throw E}}var T=!0,A=!1,N=void 0;try{for(var R,C=h(s,!0)[Symbol.iterator]();!(T=(R=C.next()).done);T=!0){var M=R.value;y(s[M],d)}}catch(e){A=!0,N=e}finally{try{!T&&C.return&&C.return()}finally{if(A)throw N}}var k=!0,I=!1,B=void 0;try{for(var L,U=Object.keys(u)[Symbol.iterator]();!(k=(L=U.next()).done);k=!0){var D=L.value;if(!(p.indexOf(D)>=0)){var Y=!0,F=!1,W=void 0;try{for(var H,z=h(u[D],!1)[Symbol.iterator]();!(Y=(H=z.next()).done);Y=!0){var G=H.value;y(u[D][G],c)}}catch(e){F=!0,W=e}finally{try{!Y&&z.return&&z.return()}finally{if(F)throw W}}}}}catch(e){I=!0,B=e}finally{try{!k&&U.return&&U.return()}finally{if(I)throw B}}var $=!0,K=!1,V=void 0;try{for(var J,q=Object.keys(l)[Symbol.iterator]();!($=(J=q.next()).done);$=!0){var X=J.value;if(!(p.indexOf(X)>=0)){var Z=!0,Q=!1,ee=void 0;try{for(var te,re=h(l[X],!1)[Symbol.iterator]();!(Z=(te=re.next()).done);Z=!0){var ne=te.value;y(l[X][ne],f)}}catch(e){Q=!0,ee=e}finally{try{!Z&&re.return&&re.return()}finally{if(Q)throw ee}}}}}catch(e){K=!0,V=e}finally{try{!$&&q.return&&q.return()}finally{if(K)throw V}}var oe=[].concat(c,f,d);return oe.map((function(e){return o[e]})).map((function(t){return e[t]}))}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n,o=r(15),i=(n=o)&&n.__esModule?n:{default:n};var a=class extends i.default{set(e,t){if("d8a5aa78-978e-11e6-ae22-56b6b6499611"!==t.SERIAL_VERSION_UID)throw new Error("You can only add registries to a meta registry");return super.set(e,t)}};t.default=a},function(e,t,r){e.exports=r(42)},function(e,t,r){var n=function(e){"use strict";var t=Object.prototype,r=t.hasOwnProperty,n="function"==typeof Symbol?Symbol:{},o=n.iterator||"@@iterator",i=n.asyncIterator||"@@asyncIterator",a=n.toStringTag||"@@toStringTag";function s(e,t,r){return Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}),e[t]}try{s({},"")}catch(e){s=function(e,t,r){return e[t]=r}}function u(e,t,r,n){var o=t&&t.prototype instanceof f?t:f,i=Object.create(o.prototype),a=new O(n||[]);return i._invoke=function(e,t,r){var n="suspendedStart";return function(o,i){if("executing"===n)throw new Error("Generator is already running");if("completed"===n){if("throw"===o)throw i;return P()}for(r.method=o,r.arg=i;;){var a=r.delegate;if(a){var s=w(a,r);if(s){if(s===c)continue;return s}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if("suspendedStart"===n)throw n="completed",r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);n="executing";var u=l(e,t,r);if("normal"===u.type){if(n=r.done?"completed":"suspendedYield",u.arg===c)continue;return{value:u.arg,done:r.done}}"throw"===u.type&&(n="completed",r.method="throw",r.arg=u.arg)}}}(e,r,a),i}function l(e,t,r){try{return{type:"normal",arg:e.call(t,r)}}catch(e){return{type:"throw",arg:e}}}e.wrap=u;var c={};function f(){}function d(){}function p(){}var h={};s(h,o,(function(){return this}));var y=Object.getPrototypeOf,m=y&&y(y(E([])));m&&m!==t&&r.call(m,o)&&(h=m);var g=p.prototype=f.prototype=Object.create(h);function v(e){["next","throw","return"].forEach((function(t){s(e,t,(function(e){return this._invoke(t,e)}))}))}function b(e,t){var n;this._invoke=function(o,i){function a(){return new t((function(n,a){!function n(o,i,a,s){var u=l(e[o],e,i);if("throw"!==u.type){var c=u.arg,f=c.value;return f&&"object"==typeof f&&r.call(f,"__await")?t.resolve(f.__await).then((function(e){n("next",e,a,s)}),(function(e){n("throw",e,a,s)})):t.resolve(f).then((function(e){c.value=e,a(c)}),(function(e){return n("throw",e,a,s)}))}s(u.arg)}(o,i,n,a)}))}return n=n?n.then(a,a):a()}}function w(e,t){var r=e.iterator[t.method];if(void 0===r){if(t.delegate=null,"throw"===t.method){if(e.iterator.return&&(t.method="return",t.arg=void 0,w(e,t),"throw"===t.method))return c;t.method="throw",t.arg=new TypeError("The iterator does not provide a 'throw' method")}return c}var n=l(r,e.iterator,t.arg);if("throw"===n.type)return t.method="throw",t.arg=n.arg,t.delegate=null,c;var o=n.arg;return o?o.done?(t[e.resultName]=o.value,t.next=e.nextLoc,"return"!==t.method&&(t.method="next",t.arg=void 0),t.delegate=null,c):o:(t.method="throw",t.arg=new TypeError("iterator result is not an object"),t.delegate=null,c)}function _(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function x(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function O(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(_,this),this.reset(!0)}function E(e){if(e){var t=e[o];if(t)return t.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var n=-1,i=function t(){for(;++n=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return n("end");if(i.tryLoc<=this.prev){var s=r.call(i,"catchLoc"),u=r.call(i,"finallyLoc");if(s&&u){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),x(r),c}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;x(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,r){return this.delegate={iterator:E(e),resultName:t,nextLoc:r},"next"===this.method&&(this.arg=void 0),c}},e}(e.exports);try{regeneratorRuntime=n}catch(e){"object"==typeof globalThis?globalThis.regeneratorRuntime=n:Function("r","regeneratorRuntime = r")(n)}},function(e,t,r){var n=r(44),o=r(45),i=r(46),a=r(47);e.exports=function(e){return n(e)||o(e)||i(e)||a()},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t,r){var n=r(16);e.exports=function(e){if(Array.isArray(e))return n(e)},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t){e.exports=function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t,r){var n=r(16);e.exports=function(e,t){if(e){if("string"==typeof e)return n(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);return"Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r?Array.from(e):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?n(e,t):void 0}},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t){e.exports=function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t){function r(e,t,r,n,o,i,a){try{var s=e[i](a),u=s.value}catch(e){return void r(e)}s.done?t(u):Promise.resolve(u).then(n,o)}e.exports=function(e){return function(){var t=this,n=arguments;return new Promise((function(o,i){var a=e.apply(t,n);function s(e){r(a,o,i,s,u,"next",e)}function u(e){r(a,o,i,s,u,"throw",e)}s(void 0)}))}},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t){function r(t,n){return e.exports=r=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},e.exports.__esModule=!0,e.exports.default=e.exports,r(t,n)}e.exports=r,e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t,r){(function(t){var r=function(){"use strict";function e(r,o,i,a){"object"==typeof o&&(i=o.depth,a=o.prototype,o.filter,o=o.circular);var s=[],u=[],l=void 0!==t;return void 0===o&&(o=!0),void 0===i&&(i=1/0),function r(i,c){if(null===i)return null;if(0==c)return i;var f,d;if("object"!=typeof i)return i;if(e.__isArray(i))f=[];else if(e.__isRegExp(i))f=new RegExp(i.source,n(i)),i.lastIndex&&(f.lastIndex=i.lastIndex);else if(e.__isDate(i))f=new Date(i.getTime());else{if(l&&t.isBuffer(i))return f=t.allocUnsafe?t.allocUnsafe(i.length):new t(i.length),i.copy(f),f;void 0===a?(d=Object.getPrototypeOf(i),f=Object.create(d)):(f=Object.create(a),d=a)}if(o){var p=s.indexOf(i);if(-1!=p)return u[p];s.push(i),u.push(f)}for(var h in i){var y;d&&(y=Object.getOwnPropertyDescriptor(d,h)),y&&null==y.set||(f[h]=r(i[h],c-1))}return f}(r,i)}function r(e){return Object.prototype.toString.call(e)}function n(e){var t="";return e.global&&(t+="g"),e.ignoreCase&&(t+="i"),e.multiline&&(t+="m"),t}return e.clonePrototype=function(e){if(null===e)return null;var t=function(){};return t.prototype=e,new t},e.__objToStr=r,e.__isDate=function(e){return"object"==typeof e&&"[object Date]"===r(e)},e.__isArray=function(e){return"object"==typeof e&&"[object Array]"===r(e)},e.__isRegExp=function(e){return"object"==typeof e&&"[object RegExp]"===r(e)},e.__getRegExpFlags=n,e}();e.exports&&(e.exports=r)}).call(this,r(51).Buffer)},function(e,t,r){"use strict";(function(e){ 2 | /*! 3 | * The buffer module from node.js, for the browser. 4 | * 5 | * @author Feross Aboukhadijeh 6 | * @license MIT 7 | */ 8 | var n=r(53),o=r(54),i=r(55);function a(){return u.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function s(e,t){if(a()=a())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+a().toString(16)+" bytes");return 0|e}function h(e,t){if(u.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var r=e.length;if(0===r)return 0;for(var n=!1;;)switch(t){case"ascii":case"latin1":case"binary":return r;case"utf8":case"utf-8":case void 0:return Y(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*r;case"hex":return r>>>1;case"base64":return F(e).length;default:if(n)return Y(e).length;t=(""+t).toLowerCase(),n=!0}}function y(e,t,r){var n=!1;if((void 0===t||t<0)&&(t=0),t>this.length)return"";if((void 0===r||r>this.length)&&(r=this.length),r<=0)return"";if((r>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return A(this,t,r);case"utf8":case"utf-8":return S(this,t,r);case"ascii":return j(this,t,r);case"latin1":case"binary":return T(this,t,r);case"base64":return P(this,t,r);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return N(this,t,r);default:if(n)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),n=!0}}function m(e,t,r){var n=e[t];e[t]=e[r],e[r]=n}function g(e,t,r,n,o){if(0===e.length)return-1;if("string"==typeof r?(n=r,r=0):r>2147483647?r=2147483647:r<-2147483648&&(r=-2147483648),r=+r,isNaN(r)&&(r=o?0:e.length-1),r<0&&(r=e.length+r),r>=e.length){if(o)return-1;r=e.length-1}else if(r<0){if(!o)return-1;r=0}if("string"==typeof t&&(t=u.from(t,n)),u.isBuffer(t))return 0===t.length?-1:v(e,t,r,n,o);if("number"==typeof t)return t&=255,u.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?o?Uint8Array.prototype.indexOf.call(e,t,r):Uint8Array.prototype.lastIndexOf.call(e,t,r):v(e,[t],r,n,o);throw new TypeError("val must be string, number or Buffer")}function v(e,t,r,n,o){var i,a=1,s=e.length,u=t.length;if(void 0!==n&&("ucs2"===(n=String(n).toLowerCase())||"ucs-2"===n||"utf16le"===n||"utf-16le"===n)){if(e.length<2||t.length<2)return-1;a=2,s/=2,u/=2,r/=2}function l(e,t){return 1===a?e[t]:e.readUInt16BE(t*a)}if(o){var c=-1;for(i=r;is&&(r=s-u),i=r;i>=0;i--){for(var f=!0,d=0;do&&(n=o):n=o;var i=t.length;if(i%2!=0)throw new TypeError("Invalid hex string");n>i/2&&(n=i/2);for(var a=0;a>8,o=r%256,i.push(o),i.push(n);return i}(t,e.length-r),e,r,n)}function P(e,t,r){return 0===t&&r===e.length?n.fromByteArray(e):n.fromByteArray(e.slice(t,r))}function S(e,t,r){r=Math.min(e.length,r);for(var n=[],o=t;o239?4:l>223?3:l>191?2:1;if(o+f<=r)switch(f){case 1:l<128&&(c=l);break;case 2:128==(192&(i=e[o+1]))&&(u=(31&l)<<6|63&i)>127&&(c=u);break;case 3:i=e[o+1],a=e[o+2],128==(192&i)&&128==(192&a)&&(u=(15&l)<<12|(63&i)<<6|63&a)>2047&&(u<55296||u>57343)&&(c=u);break;case 4:i=e[o+1],a=e[o+2],s=e[o+3],128==(192&i)&&128==(192&a)&&128==(192&s)&&(u=(15&l)<<18|(63&i)<<12|(63&a)<<6|63&s)>65535&&u<1114112&&(c=u)}null===c?(c=65533,f=1):c>65535&&(c-=65536,n.push(c>>>10&1023|55296),c=56320|1023&c),n.push(c),o+=f}return function(e){var t=e.length;if(t<=4096)return String.fromCharCode.apply(String,e);var r="",n=0;for(;n0&&(e=this.toString("hex",0,r).match(/.{2}/g).join(" "),this.length>r&&(e+=" ... ")),""},u.prototype.compare=function(e,t,r,n,o){if(!u.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===r&&(r=e?e.length:0),void 0===n&&(n=0),void 0===o&&(o=this.length),t<0||r>e.length||n<0||o>this.length)throw new RangeError("out of range index");if(n>=o&&t>=r)return 0;if(n>=o)return-1;if(t>=r)return 1;if(this===e)return 0;for(var i=(o>>>=0)-(n>>>=0),a=(r>>>=0)-(t>>>=0),s=Math.min(i,a),l=this.slice(n,o),c=e.slice(t,r),f=0;fo)&&(r=o),e.length>0&&(r<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");n||(n="utf8");for(var i=!1;;)switch(n){case"hex":return b(this,e,t,r);case"utf8":case"utf-8":return w(this,e,t,r);case"ascii":return _(this,e,t,r);case"latin1":case"binary":return x(this,e,t,r);case"base64":return O(this,e,t,r);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return E(this,e,t,r);default:if(i)throw new TypeError("Unknown encoding: "+n);n=(""+n).toLowerCase(),i=!0}},u.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};function j(e,t,r){var n="";r=Math.min(e.length,r);for(var o=t;on)&&(r=n);for(var o="",i=t;ir)throw new RangeError("Trying to access beyond buffer length")}function C(e,t,r,n,o,i){if(!u.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>o||te.length)throw new RangeError("Index out of range")}function M(e,t,r,n){t<0&&(t=65535+t+1);for(var o=0,i=Math.min(e.length-r,2);o>>8*(n?o:1-o)}function k(e,t,r,n){t<0&&(t=4294967295+t+1);for(var o=0,i=Math.min(e.length-r,4);o>>8*(n?o:3-o)&255}function I(e,t,r,n,o,i){if(r+n>e.length)throw new RangeError("Index out of range");if(r<0)throw new RangeError("Index out of range")}function B(e,t,r,n,i){return i||I(e,0,r,4),o.write(e,t,r,n,23,4),r+4}function L(e,t,r,n,i){return i||I(e,0,r,8),o.write(e,t,r,n,52,8),r+8}u.prototype.slice=function(e,t){var r,n=this.length;if((e=~~e)<0?(e+=n)<0&&(e=0):e>n&&(e=n),(t=void 0===t?n:~~t)<0?(t+=n)<0&&(t=0):t>n&&(t=n),t0&&(o*=256);)n+=this[e+--t]*o;return n},u.prototype.readUInt8=function(e,t){return t||R(e,1,this.length),this[e]},u.prototype.readUInt16LE=function(e,t){return t||R(e,2,this.length),this[e]|this[e+1]<<8},u.prototype.readUInt16BE=function(e,t){return t||R(e,2,this.length),this[e]<<8|this[e+1]},u.prototype.readUInt32LE=function(e,t){return t||R(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},u.prototype.readUInt32BE=function(e,t){return t||R(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},u.prototype.readIntLE=function(e,t,r){e|=0,t|=0,r||R(e,t,this.length);for(var n=this[e],o=1,i=0;++i=(o*=128)&&(n-=Math.pow(2,8*t)),n},u.prototype.readIntBE=function(e,t,r){e|=0,t|=0,r||R(e,t,this.length);for(var n=t,o=1,i=this[e+--n];n>0&&(o*=256);)i+=this[e+--n]*o;return i>=(o*=128)&&(i-=Math.pow(2,8*t)),i},u.prototype.readInt8=function(e,t){return t||R(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},u.prototype.readInt16LE=function(e,t){t||R(e,2,this.length);var r=this[e]|this[e+1]<<8;return 32768&r?4294901760|r:r},u.prototype.readInt16BE=function(e,t){t||R(e,2,this.length);var r=this[e+1]|this[e]<<8;return 32768&r?4294901760|r:r},u.prototype.readInt32LE=function(e,t){return t||R(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},u.prototype.readInt32BE=function(e,t){return t||R(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},u.prototype.readFloatLE=function(e,t){return t||R(e,4,this.length),o.read(this,e,!0,23,4)},u.prototype.readFloatBE=function(e,t){return t||R(e,4,this.length),o.read(this,e,!1,23,4)},u.prototype.readDoubleLE=function(e,t){return t||R(e,8,this.length),o.read(this,e,!0,52,8)},u.prototype.readDoubleBE=function(e,t){return t||R(e,8,this.length),o.read(this,e,!1,52,8)},u.prototype.writeUIntLE=function(e,t,r,n){(e=+e,t|=0,r|=0,n)||C(this,e,t,r,Math.pow(2,8*r)-1,0);var o=1,i=0;for(this[t]=255&e;++i=0&&(i*=256);)this[t+o]=e/i&255;return t+r},u.prototype.writeUInt8=function(e,t,r){return e=+e,t|=0,r||C(this,e,t,1,255,0),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},u.prototype.writeUInt16LE=function(e,t,r){return e=+e,t|=0,r||C(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):M(this,e,t,!0),t+2},u.prototype.writeUInt16BE=function(e,t,r){return e=+e,t|=0,r||C(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):M(this,e,t,!1),t+2},u.prototype.writeUInt32LE=function(e,t,r){return e=+e,t|=0,r||C(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):k(this,e,t,!0),t+4},u.prototype.writeUInt32BE=function(e,t,r){return e=+e,t|=0,r||C(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):k(this,e,t,!1),t+4},u.prototype.writeIntLE=function(e,t,r,n){if(e=+e,t|=0,!n){var o=Math.pow(2,8*r-1);C(this,e,t,r,o-1,-o)}var i=0,a=1,s=0;for(this[t]=255&e;++i>0)-s&255;return t+r},u.prototype.writeIntBE=function(e,t,r,n){if(e=+e,t|=0,!n){var o=Math.pow(2,8*r-1);C(this,e,t,r,o-1,-o)}var i=r-1,a=1,s=0;for(this[t+i]=255&e;--i>=0&&(a*=256);)e<0&&0===s&&0!==this[t+i+1]&&(s=1),this[t+i]=(e/a>>0)-s&255;return t+r},u.prototype.writeInt8=function(e,t,r){return e=+e,t|=0,r||C(this,e,t,1,127,-128),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},u.prototype.writeInt16LE=function(e,t,r){return e=+e,t|=0,r||C(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):M(this,e,t,!0),t+2},u.prototype.writeInt16BE=function(e,t,r){return e=+e,t|=0,r||C(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):M(this,e,t,!1),t+2},u.prototype.writeInt32LE=function(e,t,r){return e=+e,t|=0,r||C(this,e,t,4,2147483647,-2147483648),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):k(this,e,t,!0),t+4},u.prototype.writeInt32BE=function(e,t,r){return e=+e,t|=0,r||C(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):k(this,e,t,!1),t+4},u.prototype.writeFloatLE=function(e,t,r){return B(this,e,t,!0,r)},u.prototype.writeFloatBE=function(e,t,r){return B(this,e,t,!1,r)},u.prototype.writeDoubleLE=function(e,t,r){return L(this,e,t,!0,r)},u.prototype.writeDoubleBE=function(e,t,r){return L(this,e,t,!1,r)},u.prototype.copy=function(e,t,r,n){if(r||(r=0),n||0===n||(n=this.length),t>=e.length&&(t=e.length),t||(t=0),n>0&&n=this.length)throw new RangeError("sourceStart out of bounds");if(n<0)throw new RangeError("sourceEnd out of bounds");n>this.length&&(n=this.length),e.length-t=0;--o)e[o+t]=this[o+r];else if(i<1e3||!u.TYPED_ARRAY_SUPPORT)for(o=0;o>>=0,r=void 0===r?this.length:r>>>0,e||(e=0),"number"==typeof e)for(i=t;i55295&&r<57344){if(!o){if(r>56319){(t-=3)>-1&&i.push(239,191,189);continue}if(a+1===n){(t-=3)>-1&&i.push(239,191,189);continue}o=r;continue}if(r<56320){(t-=3)>-1&&i.push(239,191,189),o=r;continue}r=65536+(o-55296<<10|r-56320)}else o&&(t-=3)>-1&&i.push(239,191,189);if(o=null,r<128){if((t-=1)<0)break;i.push(r)}else if(r<2048){if((t-=2)<0)break;i.push(r>>6|192,63&r|128)}else if(r<65536){if((t-=3)<0)break;i.push(r>>12|224,r>>6&63|128,63&r|128)}else{if(!(r<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;i.push(r>>18|240,r>>12&63|128,r>>6&63|128,63&r|128)}}return i}function F(e){return n.toByteArray(function(e){if((e=function(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}(e).replace(U,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function W(e,t,r,n){for(var o=0;o=t.length||o>=e.length);++o)t[o+r]=e[o];return o}}).call(this,r(52))},function(e,t){var r;r=function(){return this}();try{r=r||new Function("return this")()}catch(e){"object"==typeof window&&(r=window)}e.exports=r},function(e,t,r){"use strict";t.byteLength=function(e){var t=l(e),r=t[0],n=t[1];return 3*(r+n)/4-n},t.toByteArray=function(e){var t,r,n=l(e),a=n[0],s=n[1],u=new i(function(e,t,r){return 3*(t+r)/4-r}(0,a,s)),c=0,f=s>0?a-4:a;for(r=0;r>16&255,u[c++]=t>>8&255,u[c++]=255&t;2===s&&(t=o[e.charCodeAt(r)]<<2|o[e.charCodeAt(r+1)]>>4,u[c++]=255&t);1===s&&(t=o[e.charCodeAt(r)]<<10|o[e.charCodeAt(r+1)]<<4|o[e.charCodeAt(r+2)]>>2,u[c++]=t>>8&255,u[c++]=255&t);return u},t.fromByteArray=function(e){for(var t,r=e.length,o=r%3,i=[],a=0,s=r-o;as?s:a+16383));1===o?(t=e[r-1],i.push(n[t>>2]+n[t<<4&63]+"==")):2===o&&(t=(e[r-2]<<8)+e[r-1],i.push(n[t>>10]+n[t>>4&63]+n[t<<2&63]+"="));return i.join("")};for(var n=[],o=[],i="undefined"!=typeof Uint8Array?Uint8Array:Array,a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",s=0,u=a.length;s0)throw new Error("Invalid string. Length must be a multiple of 4");var r=e.indexOf("=");return-1===r&&(r=t),[r,r===t?0:4-r%4]}function c(e,t,r){for(var o,i,a=[],s=t;s>18&63]+n[i>>12&63]+n[i>>6&63]+n[63&i]);return a.join("")}o["-".charCodeAt(0)]=62,o["_".charCodeAt(0)]=63},function(e,t){ 9 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 10 | t.read=function(e,t,r,n,o){var i,a,s=8*o-n-1,u=(1<>1,c=-7,f=r?o-1:0,d=r?-1:1,p=e[t+f];for(f+=d,i=p&(1<<-c)-1,p>>=-c,c+=s;c>0;i=256*i+e[t+f],f+=d,c-=8);for(a=i&(1<<-c)-1,i>>=-c,c+=n;c>0;a=256*a+e[t+f],f+=d,c-=8);if(0===i)i=1-l;else{if(i===u)return a?NaN:1/0*(p?-1:1);a+=Math.pow(2,n),i-=l}return(p?-1:1)*a*Math.pow(2,i-n)},t.write=function(e,t,r,n,o,i){var a,s,u,l=8*i-o-1,c=(1<>1,d=23===o?Math.pow(2,-24)-Math.pow(2,-77):0,p=n?0:i-1,h=n?1:-1,y=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(s=isNaN(t)?1:0,a=c):(a=Math.floor(Math.log(t)/Math.LN2),t*(u=Math.pow(2,-a))<1&&(a--,u*=2),(t+=a+f>=1?d/u:d*Math.pow(2,1-f))*u>=2&&(a++,u/=2),a+f>=c?(s=0,a=c):a+f>=1?(s=(t*u-1)*Math.pow(2,o),a+=f):(s=t*Math.pow(2,f-1)*Math.pow(2,o),a=0));o>=8;e[r+p]=255&s,p+=h,s/=256,o-=8);for(a=a<0;e[r+p]=255&a,p+=h,a/=256,l-=8);e[r+p-h]|=128*y}},function(e,t){var r={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==r.call(e)}},function(e,t){var r="undefined"!=typeof Element,n="function"==typeof Map,o="function"==typeof Set,i="function"==typeof ArrayBuffer&&!!ArrayBuffer.isView;e.exports=function(e,t){try{return function e(t,a){if(t===a)return!0;if(t&&a&&"object"==typeof t&&"object"==typeof a){if(t.constructor!==a.constructor)return!1;var s,u,l,c;if(Array.isArray(t)){if((s=t.length)!=a.length)return!1;for(u=s;0!=u--;)if(!e(t[u],a[u]))return!1;return!0}if(n&&t instanceof Map&&a instanceof Map){if(t.size!==a.size)return!1;for(c=t.entries();!(u=c.next()).done;)if(!a.has(u.value[0]))return!1;for(c=t.entries();!(u=c.next()).done;)if(!e(u.value[1],a.get(u.value[0])))return!1;return!0}if(o&&t instanceof Set&&a instanceof Set){if(t.size!==a.size)return!1;for(c=t.entries();!(u=c.next()).done;)if(!a.has(u.value[0]))return!1;return!0}if(i&&ArrayBuffer.isView(t)&&ArrayBuffer.isView(a)){if((s=t.length)!=a.length)return!1;for(u=s;0!=u--;)if(t[u]!==a[u])return!1;return!0}if(t.constructor===RegExp)return t.source===a.source&&t.flags===a.flags;if(t.valueOf!==Object.prototype.valueOf)return t.valueOf()===a.valueOf();if(t.toString!==Object.prototype.toString)return t.toString()===a.toString();if((s=(l=Object.keys(t)).length)!==Object.keys(a).length)return!1;for(u=s;0!=u--;)if(!Object.prototype.hasOwnProperty.call(a,l[u]))return!1;if(r&&t instanceof Element)return!1;for(u=s;0!=u--;)if(("_owner"!==l[u]&&"__v"!==l[u]&&"__o"!==l[u]||!t.$$typeof)&&!e(t[l[u]],a[l[u]]))return!1;return!0}return t!=t&&a!=a}(e,t)}catch(e){if((e.message||"").match(/stack|recursion/i))return console.warn("react-fast-compare cannot handle circular refs"),!1;throw e}}},function(e,t,r){"use strict";var n=r(4),o=r(6);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=n(r(58)),a=n(r(17)),s=n(r(18)),u=n(r(20)),l=n(r(21)),c=n(r(22)),f=n(r(7)),d=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!==o(e)&&"function"!=typeof e)return{default:e};var r=g(t);if(r&&r.has(e))return r.get(e);var n={},i=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!=a&&Object.prototype.hasOwnProperty.call(e,a)){var s=i?Object.getOwnPropertyDescriptor(e,a):null;s&&(s.get||s.set)?Object.defineProperty(n,a,s):n[a]=e[a]}return n.default=e,r&&r.set(e,n),n}(r(0)),p=n(r(59)),h=n(r(11)),y=n(r(60)),m=n(r(23));function g(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(g=function(e){return e?r:t})(e)}function v(e){var t=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}();return function(){var r,n=(0,c.default)(e);if(t){var o=(0,c.default)(this).constructor;r=Reflect.construct(n,arguments,o)}else r=n.apply(this,arguments);return(0,l.default)(this,r)}}var b=function(e){function t(){return(0,a.default)(this,t),r.apply(this,arguments)}(0,u.default)(t,e);var r=v(t);return(0,s.default)(t,[{key:"render",value:function(){var e=this.props,t=e.content,r=e.style,n=e.className,o={message:(0,h.default)(r,m.default)};return this.props.dangerMode&&"string"==typeof t?d.default.createElement("div",(0,i.default)({className:n,style:o.message},(0,p.default)(t))):d.default.createElement("div",{className:n,style:o.message},t)}}]),t}(d.Component);t.default=b,(0,f.default)(b,"propTypes",y.default)},function(e,t){function r(){return e.exports=r=Object.assign||function(e){for(var t=1;t=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a=!0,s=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return a=e.done,e},e:function(e){s=!0,e},f:function e(){try{a||null==r.return||r.return()}finally{if(s)throw e}}}}function i(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);rObject(n.useContext)(b),_=({config:e,children:t,documentNode:r,focusedNode:o,siteNode:i,i18nRegistry:a,handleServerFeedback:s})=>{const[u,l]=Object(n.useState)({});Object(n.useEffect)(()=>{Object(v.default)().getCommands().then(l)},[]);const c=Object(n.useCallback)((e,t="",r=[],n="Shel.Neos.Terminal",o="Main")=>a.translate(e,t,r,n,o),[]),f=Object(n.useCallback)(async(t,n)=>{if(!u[t])throw Error(c("command.doesNotExist","The command {commandName} does not exist!",{commandName:t}));const{success:a,result:l,uiFeedback:f}=await Object(m.a)(e.invokeCommandEndPoint,t,n,i,o,r);let d=l,p=l;try{d=JSON.parse(l),p="string"!=typeof d?JSON.stringify(d,null,2):d}catch(e){}return Object(g.a)(a?"log":"error",c("command.output",'"{commandName} {argument}":',{commandName:t,argument:n.join(" ")}),d),f&&s(f),p},[u,i,r,o]);return n.createElement(b.Provider,{value:{invokeCommand:f,commands:u,translate:c}},t)};var x=o.a.memo(({registrationKey:e})=>{const{translate:t}=w(),[r,i]=Object(n.useState)(!1);return Object(n.useEffect)(()=>{if(!e||!e.id||!e.signature)return console.info("Shel.Neos.Terminal:",t("sponsorship.missing"));const{id:r,signature:n}=e;"V1"+[...r.split("")].reduce((e,t)=>(e=(e<<5)-e+t.charCodeAt(0))&e,0)===atob(n)?(e.showThankYouMessage&&console.info("Shel.Neos.Terminal:",t("sponsorship.verified")),i(!0)):console.warn("Shel.Neos.Terminal:",t("sponsorship.invalid"))},[e]),r?null:o.a.createElement("div",{className:y.a.sponsorshipWidget},o.a.createElement("a",{href:"https://github.com/Sebobo/Shel.Neos.Terminal",target:"_blank",rel:"noreferrer noopener",title:t("sponsorship.title")},o.a.createElement("span",null,t("sponsorship")),o.a.createElement("svg",{width:"178",height:"181",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 178 181"},o.a.createElement("path",{d:"M0 180.017h37.287L71.35 52.898H34.061zM48.717 0l-9.083 33.898h36.675L85.392 0z",fill:"#b9b7b3"}),o.a.createElement("defs",null,o.a.createElement("linearGradient",{id:"a",gradientUnits:"userSpaceOnUse",x1:"84.788",y1:"116.678",x2:"177.799",y2:"116.678"},o.a.createElement("stop",{offset:"0",stopColor:"#287aac"}),o.a.createElement("stop",{offset:"1",stopColor:"#54aada"}))),o.a.createElement("path",{d:"M140.51 53.119H92.788l-8 28.306h47.691l-28.03 98.813h39.287l34.063-127.119z",fill:"url(#a)"}))))}),O=r(31),E=r.n(O);var P=o.a.memo(Object(d.themr)("shel-neos-terminal/replWrapper",E.a)(({config:e,theme:t,user:r,siteNode:i,documentNode:a,terminalOpen:s,toggleNeosTerminal:u})=>{var l;const{invokeCommand:c,commands:d,translate:h}=w(),y=Object(n.useRef)(),m=Object(n.useMemo)(()=>{var e;const t=(null==i?void 0:i.contextPath)===(null==a?void 0:a.contextPath)?"~":null===(e=null==a?void 0:a.properties)||void 0===e?void 0:e.uriPathSegment;return`${r.firstName}@${null==i?void 0:i.name}:${t}$`},[r.firstName,null==i?void 0:i.name,null==a?void 0:a.contextPath,null===(l=null==a?void 0:a.properties)||void 0===l?void 0:l.uriPathSegment]),g=Object(n.useMemo)(()=>{const e=Object.keys(d).reduce((e,t)=>{var r;const n=d[t];return window.NeosTerminal[t]=(...e)=>c(t,e),e[t]={...n,description:h(null!==(r=n.description)&&void 0!==r?r:""),fn:(...e)=>{const r=y.current;return c(t,e).then(e=>{r.state.stdout.pop();let t=e;e||(t=h("command.empty")),r.pushToStdout(t)}).catch(e=>{console.error(e,h("command.invocationError",'An error occurred during invocation of the "{commandName}" command',{commandName:t})),r.state.stdout.pop(),r.pushToStdout(h("command.error"))}),h("command.evaluating")}},e},{});return e.help={name:"help",description:h("command.help.description"),usage:"help ",fn:e=>{const t=y.current;if(e)if(d[e]){const r=d[e];t.pushToStdout(`${h(r.description)} - ${r.usage}`)}else t.pushToStdout(h("command.help.unknownCommand"));else t.showHelp()}},e.clear={name:"clear",description:h("command.clear.description"),usage:"clear",fn:()=>y.current.clearStdout()},e},[d,c]),v=Object(n.useCallback)(e=>{const t=Object.keys(d),r=e.value,n=t.filter(e=>e.startsWith(r));if(n)if(1===n.length)e.value=n[0]+" ";else{const e=y.current,[t]=e.state.stdout.slice(-1),r=h("matchingCommands","Matching commands: {commands}",{commands:n.join(" ")});t.message!==r&&e.pushToStdout(r,{isEcho:!0})}},[d]);Object(n.useEffect)(()=>{s&&setTimeout(()=>{var e;return null===(e=y.current)||void 0===e?void 0:e.focusTerminal()},0)},[s]);const b=Object(n.useCallback)(e=>{s&&27===e.keyCode&&u(!1)},[s]);return Object.keys(d).length?o.a.createElement("div",{className:t.replWrapper,onKeyUp:b},o.a.createElement(p.IconButton,{onClick:()=>u(),isActive:s,title:h("toggleTerminal"),icon:"terminal"}),o.a.createElement("div",{className:t.terminalWrapper,style:{display:s?"block":"none"}},o.a.createElement(f.a,{autoFocus:!0,ref:y,commands:g,ignoreCommandCase:!0,welcomeMessage:h(e.welcomeMessage),promptLabel:m,onTab:v,noDefaults:!0,style:{borderRadius:0,maxHeight:"50vh"},...e.theme}),o.a.createElement(x,{registrationKey:e.registrationKey}))):null})),S=r(8);class j extends n.PureComponent{render(){var e,t,r;const{config:o}=this.props;return n.createElement(_,{siteNode:null===(e=this.props.siteNode)||void 0===e?void 0:e.contextPath,documentNode:null===(t=this.props.documentNode)||void 0===t?void 0:t.contextPath,focusedNode:(null===(r=this.props.focusedNodes)||void 0===r?void 0:r.length)>0?this.props.focusedNodes[0]:null,i18nRegistry:this.props.i18nRegistry,handleServerFeedback:this.props.handleServerFeedback,config:o},n.createElement(P,{...this.props}))}}j.propTypes={config:s.a.object.isRequired,i18nRegistry:s.a.object.isRequired,user:s.a.object.isRequired,siteNode:s.a.object,documentNode:s.a.object,focusedNodes:s.a.array,terminalOpen:s.a.bool,toggleNeosTerminal:s.a.func,handleServerFeedback:s.a.func};const T=Object(u.neos)(e=>({i18nRegistry:e.get("i18n"),config:e.get("frontendConfiguration").get("Shel.Neos.Terminal:Terminal")}));t.default=Object(i.connect)(()=>({}),{toggleNeosTerminal:S.actions.toggleNeosTerminal})(Object(i.connect)(e=>{var t;return{user:null===(t=null==e?void 0:e.user)||void 0===t?void 0:t.name,siteNode:l.selectors.CR.Nodes.siteNodeSelector(e),documentNode:l.selectors.CR.Nodes.documentNodeSelector(e),focusedNodes:l.selectors.CR.Nodes.focusedNodePathsSelector(e),terminalOpen:S.selectors.terminalOpen(e)}},()=>({handleServerFeedback:l.actions.ServerFeedback.handleServerFeedback}))(T(j)))}]); -------------------------------------------------------------------------------- /Resources/Public/Assets/Plugin.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * The buffer module from node.js, for the browser. 3 | * 4 | * @author Feross Aboukhadijeh 5 | * @license MIT 6 | */ 7 | 8 | /*! ***************************************************************************** 9 | Copyright (c) Microsoft Corporation. 10 | 11 | Permission to use, copy, modify, and/or distribute this software for any 12 | purpose with or without fee is hereby granted. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 15 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 16 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 17 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 18 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 19 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 20 | PERFORMANCE OF THIS SOFTWARE. 21 | ***************************************************************************** */ 22 | 23 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 24 | -------------------------------------------------------------------------------- /Tests/Functional/EvaluateEelExpressionTest.php: -------------------------------------------------------------------------------- 1 | evaluateEelExpressionCommand = $this->objectManager->get(EvaluateEelExpressionCommand::class); 41 | $context = $this->getMockBuilder(Context::class)->disableOriginalConstructor()->getMock(); 42 | 43 | $siteNodeData = $this->getMockBuilder(NodeData::class)->disableOriginalConstructor()->getMock(); 44 | $siteNodeData->method('getName')->willReturn(self::HOMEPAGE); 45 | $documentNodeData = $this->getMockBuilder(NodeData::class)->disableOriginalConstructor()->getMock(); 46 | $documentNodeData->method('getName')->willReturn(self::ABOUTUS); 47 | $focusedNodeData = $this->getMockBuilder(NodeData::class)->disableOriginalConstructor()->getMock(); 48 | $focusedNodeData->method('getName')->willReturn(self::HEADLINE); 49 | 50 | $this->commandContext = (new CommandContext($this->createControllerContext())) 51 | ->withSiteNode(new Node($siteNodeData, $context)) 52 | ->withDocumentNode(new Node($documentNodeData, $context)) 53 | ->withFocusedNode(new Node($focusedNodeData, $context)); 54 | } 55 | 56 | /** 57 | * @test 58 | */ 59 | public function failOnMissingExpression(): void 60 | { 61 | $expression = ''; 62 | 63 | $result = $this->evaluateEelExpressionCommand->invokeCommand($expression, $this->commandContext); 64 | 65 | $this->assertFalse($result->isSuccess(), 'Command should fail on missing expression'); 66 | } 67 | 68 | /** 69 | * @test 70 | */ 71 | public function evaluateSimpleMathExpression(): void 72 | { 73 | $expression = '1 + 1'; 74 | 75 | $result = $this->evaluateEelExpressionCommand->invokeCommand($expression, $this->commandContext); 76 | 77 | $this->assertTrue($result->isSuccess(), 'Evaluation of expression "' . $expression . '" failed'); 78 | $this->assertEquals(2, $result->getResult()); 79 | } 80 | 81 | /** 82 | * @test 83 | */ 84 | public function evaluateSimpleStringConcatenationExpression(): void 85 | { 86 | $singleQuotedStringsExpression = "'a' + 'b'"; 87 | $doubleQuotedStringsExpression = '"a" + "b"'; 88 | $mixedQuotedStringsExpression = "\"a\" + 'b'"; 89 | 90 | $resultSingleQuoted = $this->evaluateEelExpressionCommand->invokeCommand($singleQuotedStringsExpression, 91 | $this->commandContext); 92 | $resultDoubleQuoted = $this->evaluateEelExpressionCommand->invokeCommand($doubleQuotedStringsExpression, 93 | $this->commandContext); 94 | $resultMixedQuoted = $this->evaluateEelExpressionCommand->invokeCommand($mixedQuotedStringsExpression, 95 | $this->commandContext); 96 | 97 | $this->assertTrue($resultSingleQuoted->isSuccess(), 'Single quoted strings are not supported'); 98 | $this->assertTrue($resultDoubleQuoted->isSuccess(), 'Double quoted strings are not supported'); 99 | $this->assertTrue($resultMixedQuoted->isSuccess(), 'Mixed quoted strings are not supported'); 100 | 101 | $this->assertEquals('ab', $resultSingleQuoted->getResult(), 'Concatenation of single quoted strings failed'); 102 | $this->assertEquals('ab', $resultDoubleQuoted->getResult(), 'Concatenation of double quoted strings failed'); 103 | $this->assertEquals('ab', $resultMixedQuoted->getResult(), 'Concatenation of mixed quoted strings failed'); 104 | } 105 | 106 | /** 107 | * @test 108 | */ 109 | public function failOnIncompleteExpression(): void 110 | { 111 | $expression = 'q(site).find("'; 112 | 113 | $result = $this->evaluateEelExpressionCommand->invokeCommand($expression, $this->commandContext); 114 | 115 | $this->assertFalse($result->isSuccess(), 'Evaluation of expression "' . $expression . '" should fail'); 116 | } 117 | 118 | /** 119 | * @test 120 | */ 121 | public function evaluateExpressionWithSiteNodeContext(): void 122 | { 123 | $expression = 'site'; 124 | 125 | $result = $this->evaluateEelExpressionCommand->invokeCommand($expression, $this->commandContext); 126 | 127 | $this->assertTrue($result->isSuccess(), 'Evaluation of expression "' . $expression . '" failed'); 128 | $this->assertInstanceOf(NodeInterface::class, $result->getResult(), 129 | 'Evaluation of expression "' . $expression . '" should return a node'); 130 | $this->assertEquals(self::HOMEPAGE, $result->getResult()->getName(), 131 | 'Evaluation of expression "' . $expression . '" should return the site node'); 132 | } 133 | 134 | /** 135 | * @test 136 | */ 137 | public function evaluateExpressionWithDocumentNodeContext(): void 138 | { 139 | $expression = 'documentNode'; 140 | 141 | $result = $this->evaluateEelExpressionCommand->invokeCommand($expression, $this->commandContext); 142 | 143 | $this->assertTrue($result->isSuccess(), 'Evaluation of expression "' . $expression . '" failed'); 144 | $this->assertInstanceOf(NodeInterface::class, $result->getResult(), 145 | 'Evaluation of expression "' . $expression . '" should return a node'); 146 | $this->assertEquals(self::ABOUTUS, $result->getResult()->getName(), 147 | 'Evaluation of expression "' . $expression . '" should return the "about us" document node'); 148 | } 149 | 150 | /** 151 | * @test 152 | */ 153 | public function evaluateExpressionWithFocusedNodeContext(): void 154 | { 155 | $expression = 'node'; 156 | 157 | $result = $this->evaluateEelExpressionCommand->invokeCommand($expression, $this->commandContext); 158 | 159 | $this->assertTrue($result->isSuccess(), 'Evaluation of expression "' . $expression . '" failed'); 160 | $this->assertInstanceOf(NodeInterface::class, $result->getResult(), 161 | 'Evaluation of expression "' . $expression . '" should return a node'); 162 | $this->assertEquals(self::HEADLINE, $result->getResult()->getName(), 163 | 'Evaluation of expression "' . $expression . '" should return the focused headline content node'); 164 | } 165 | 166 | /** 167 | * @test 168 | */ 169 | public function evaluateComplexEelExpression(): void 170 | { 171 | $expression = 'Array.map([1,2,3], (i) => i * 2)'; 172 | 173 | $result = $this->evaluateEelExpressionCommand->invokeCommand($expression, $this->commandContext); 174 | 175 | $this->assertTrue($result->isSuccess(), 'Evaluation of expression "' . $expression . '" failed'); 176 | $this->assertEquals([2, 4, 6], $result->getResult(), 177 | 'Evaluation of expression "' . $expression . '" should return an array'); 178 | } 179 | 180 | /** 181 | * Create a simple controller context which can be used to instantiate a Fusion runtime etc. 182 | */ 183 | protected function createControllerContext(): ControllerContext 184 | { 185 | $httpRequest = new ServerRequest('POST', 'http://localhost'); 186 | $request = ActionRequest::fromHttpRequest($httpRequest); 187 | $response = new ActionResponse(); 188 | $arguments = new Arguments([]); 189 | $uriBuilder = new UriBuilder(); 190 | $uriBuilder->setRequest($request); 191 | 192 | return new ControllerContext($request, $response, $arguments, $uriBuilder); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Neos CMS Ui terminal for running Eel expressions and other commands", 3 | "type": "neos-plugin", 4 | "name": "shel/neos-terminal", 5 | "license": "MIT", 6 | "keywords": [ 7 | "flow", 8 | "neoscms", 9 | "terminal", 10 | "console", 11 | "eel" 12 | ], 13 | "require": { 14 | "php": ">=8.1", 15 | "neos/neos": "^8.3", 16 | "neos/neos-ui": "^8.3", 17 | "symfony/console": "^4.2 || ^5.1 || ^6.1" 18 | }, 19 | "suggest": { 20 | "shel/neos-commandbar": "The terminal provides a plugin integration for the Neos command bar" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Shel\\Neos\\Terminal\\": "Classes" 25 | } 26 | }, 27 | "extra": { 28 | "neos": { 29 | "package-key": "Shel.Neos.Terminal" 30 | } 31 | } 32 | } 33 | --------------------------------------------------------------------------------