setName('app:remove') ->setDescription('Remove an app from this Nextcloud instance') ->setHelp( "Removes the specified app and, if present, runs the app's uninstall steps.\n" . "\n" . "By default, this command runs the app's uninstall steps (which may delete data) and then removes the app files.\n" . "Use `--keep-data` to skip uninstall steps and preserve app data (database tables, configuration, and stored files).\n" . "Note: Some apps may still preserve data either way, depending on their uninstall implementation.\n" ) ->addArgument( 'app-id', InputArgument::REQUIRED, 'remove the specified app' ) ->addOption( 'keep-data', null, InputOption::VALUE_NONE, 'Do not run uninstall tasks; preserve app data and configuration' ); } #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { $appId = (string)$input->getArgument('app-id'); $keepData = (bool)$input->getOption('keep-data'); // Prevent removal of shipped/core apps if ($this->manager->isShipped($appId)) { $output->writeln("App '$appId' is a shipped/core app and cannot be removed."); return self::FAILURE; } // Prevent removal of apps that aren't even installed (note: don't use isInstalled(); it's a misnomer) try { $this->manager->getAppPath($appId); } catch (AppPathNotFoundException $e) { $output->writeln("App '$appId' is not installed. Nothing to remove."); return self::FAILURE; // one could argue this a no-op and should be considered a success (?) } $appVersion = $this->manager->getAppVersion($appId); // Do not run the specified app's uninstall tasks -- preserving app data/config -- if requested if ($keepData) { $message = "Removing app '$appId' but keeping app data (uninstall hooks skipped)."; $output->writeln($message); $this->logger->info($message, [ 'app' => 'CLI', ]); } else { // Disable the app before removing to trigger uninstall steps try { $this->manager->disableApp($appId); $message = "Disabled app '$appId' (uninstall steps executed)."; $output->writeln($message); $this->logger->info($message, [ 'app' => 'CLI', ]); } catch (Throwable $e) { $message = "Failed to disable app '$appId' (version $appVersion) - app removal skipped."; $output->writeln('Error: ' . $e->getMessage() . ''); $output->writeln("\n" . $message); $this->logger->error($message, [ 'app' => 'CLI', 'exception' => $e, ]); return self::FAILURE; } } // Remove the specified app try { $removeSuccess = $this->installer->removeApp($appId); } catch (Throwable $e) { $removeSuccess = false; $output->writeln('Error: ' . $e->getMessage() . ''); $this->logger->error("Failed to remove app '$appId': " . $e->getMessage(), [ 'app' => 'CLI', 'exception' => $e, ]); } // Something went wrong during removeApp(); probably no removal took place or incomplete if (!$removeSuccess) { $message = "\nFailed to remove app '$appId' (version $appVersion) - app files/registration were not removed."; $output->writeln($message); $this->logger->error($message, [ 'app' => 'CLI', ]); return self::FAILURE; } $message = "Removed app '$appId' (version $appVersion)."; $output->writeln($message); $this->logger->info($message, [ 'app' => 'CLI', ]); return self::SUCCESS; } /** * @param string $optionName * @param CompletionContext $context * @return string[] */ #[\Override] public function completeOptionValues($optionName, CompletionContext $context): array { return []; } /** * @param string $argumentName * @param CompletionContext $context * @return string[] */ #[\Override] public function completeArgumentValues($argumentName, CompletionContext $context): array { if ($argumentName === 'app-id') { // TODO: Include disabled apps too return $this->manager->getEnabledApps(); } return []; } }