diff --git a/bin/console b/bin/console index f70ba97..619d251 100755 --- a/bin/console +++ b/bin/console @@ -2,16 +2,116 @@ kernel()->boot(); + + // Get the container + $container = $app->container(); + + // Create Symfony Console Application + $console = new ConsoleApplication('Ktrix Console', Kernel::VERSION); + + // Collect all command classes + $commandClasses = []; + + // Collect commands from modules + /** @var ModuleManager $moduleManager */ + $moduleManager = $container->get(ModuleManager::class); + + foreach ($moduleManager->list() as $module) { + $moduleInstance = $module->instance(); + + // Skip if module instance is not available + if ($moduleInstance === null) { + continue; + } + + // Check if module implements console command provider + if ($moduleInstance instanceof ModuleConsoleInterface) { + try { + $commands = $moduleInstance->registerCI(); + + foreach ($commands as $commandClass) { + if (!class_exists($commandClass)) { + fwrite(STDERR, "Warning: Command class not found: {$commandClass}\n"); + continue; + } + $commandClasses[] = $commandClass; + } + } catch (\Throwable $e) { + fwrite(STDERR, "Warning: Failed to load commands from module {$module->handle()}: {$e->getMessage()}\n"); + } + } + } + + // Register commands using lazy loading + foreach ($commandClasses as $commandClass) { + try { + // Use reflection to read #[AsCommand] attribute without instantiation + $reflection = new \ReflectionClass($commandClass); + $attributes = $reflection->getAttributes(\Symfony\Component\Console\Attribute\AsCommand::class); + + if (empty($attributes)) { + fwrite(STDERR, "Warning: Command {$commandClass} missing #[AsCommand] attribute\n"); + continue; + } + + // Get attribute instance + /** @var \Symfony\Component\Console\Attribute\AsCommand $commandAttr */ + $commandAttr = $attributes[0]->newInstance(); + + // Create lazy command wrapper that defers instantiation + $lazyCommand = new LazyCommand( + $commandAttr->name, + [], + $commandAttr->description ?? '', + $commandAttr->hidden ?? false, + fn() => $container->get($commandClass) // Only instantiate when executed + ); + + $console->add($lazyCommand); + + } catch (\Throwable $e) { + fwrite(STDERR, "Warning: Failed to register command {$commandClass}: {$e->getMessage()}\n"); + } + } + + // Run the console application + $exitCode = $console->run(); + exit($exitCode); + +} catch (\Throwable $e) { + fwrite(STDERR, "Fatal error: " . $e->getMessage() . "\n"); + if (isset($app) && $app->debug()) { + fwrite(STDERR, $e->getTraceAsString() . "\n"); + } + exit(1); +} diff --git a/composer.json b/composer.json index ec78a02..6c38df4 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ "ext-iconv": "*", "mongodb/mongodb": "^2.1", "php-di/php-di": "*", - "phpseclib/phpseclib": "^3.0" + "phpseclib/phpseclib": "^3.0", + "symfony/console": "^7.0" }, "require-dev": { "phpunit/phpunit": "^11.0" diff --git a/composer.lock b/composer.lock index 44b9483..198aeb1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9b9f3e5ee2fe48f2f736eb5d0a74be7c", + "content-hash": "2798ba3263d47fe84e91ee7dfd47e310", "packages": [ { "name": "laravel/serializable-closure", @@ -604,6 +604,506 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "symfony/console", + "version": "v7.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/732a9ca6cd9dfd940c639062d5edbde2f6727fb6", + "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-12-23T14:50:43+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, { "name": "symfony/polyfill-php85", "version": "v1.33.0", @@ -683,6 +1183,184 @@ } ], "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003", + "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-27T13:27:24+00:00" } ], "packages-dev": [ diff --git a/core/lib/Console/ModuleDisableCommand.php b/core/lib/Console/ModuleDisableCommand.php new file mode 100644 index 0000000..b4881d7 --- /dev/null +++ b/core/lib/Console/ModuleDisableCommand.php @@ -0,0 +1,92 @@ +addArgument('handle', InputArgument::REQUIRED, 'Module handle to disable') + ->setHelp('This command disables an enabled module without uninstalling it.') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $handle = $input->getArgument('handle'); + + $io->title('Disable Module'); + + try { + // Prevent disabling core module + if ($handle === 'core') { + $io->error('Cannot disable the core module.'); + return Command::FAILURE; + } + + // Find the module + $modules = $this->moduleManager->list(installedOnly: true, enabledOnly: false); + $module = $modules[$handle] ?? null; + + if (!$module) { + $io->error("Module '{$handle}' not found or not installed."); + return Command::FAILURE; + } + + if (!$module->enabled()) { + $io->warning("Module '{$handle}' is already disabled."); + return Command::SUCCESS; + } + + // Disable the module + $io->text("Disabling module '{$handle}'..."); + $this->moduleManager->disable($handle); + + $this->logger->info('Module disabled via console', [ + 'handle' => $handle, + 'command' => $this->getName(), + ]); + + $io->success("Module '{$handle}' disabled successfully!"); + + return Command::SUCCESS; + + } catch (\Throwable $e) { + $io->error('Failed to disable module: ' . $e->getMessage()); + $this->logger->error('Module disable failed', [ + 'handle' => $handle, + 'error' => $e->getMessage(), + ]); + return Command::FAILURE; + } + } +} diff --git a/core/lib/Console/ModuleEnableCommand.php b/core/lib/Console/ModuleEnableCommand.php new file mode 100644 index 0000000..297b2d7 --- /dev/null +++ b/core/lib/Console/ModuleEnableCommand.php @@ -0,0 +1,86 @@ +addArgument('handle', InputArgument::REQUIRED, 'Module handle to enable') + ->setHelp('This command enables a previously disabled module.') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $handle = $input->getArgument('handle'); + + $io->title('Enable Module'); + + try { + // Find the module + $modules = $this->moduleManager->list(installedOnly: true, enabledOnly: false); + $module = $modules[$handle] ?? null; + + if (!$module) { + $io->error("Module '{$handle}' not found or not installed."); + return Command::FAILURE; + } + + if ($module->enabled()) { + $io->warning("Module '{$handle}' is already enabled."); + return Command::SUCCESS; + } + + // Enable the module + $io->text("Enabling module '{$handle}'..."); + $this->moduleManager->enable($handle); + + $this->logger->info('Module enabled via console', [ + 'handle' => $handle, + 'command' => $this->getName(), + ]); + + $io->success("Module '{$handle}' enabled successfully!"); + + return Command::SUCCESS; + + } catch (\Throwable $e) { + $io->error('Failed to enable module: ' . $e->getMessage()); + $this->logger->error('Module enable failed', [ + 'handle' => $handle, + 'error' => $e->getMessage(), + ]); + return Command::FAILURE; + } + } +} diff --git a/core/lib/Console/ModuleListCommand.php b/core/lib/Console/ModuleListCommand.php new file mode 100644 index 0000000..a27c49b --- /dev/null +++ b/core/lib/Console/ModuleListCommand.php @@ -0,0 +1,86 @@ +addOption('all', 'a', InputOption::VALUE_NONE, 'Show all modules including disabled ones') + ->setHelp('This command lists all installed modules with their status and version information.') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $showAll = $input->getOption('all'); + + $io->title('Installed Modules'); + + try { + $modules = $this->moduleManager->list( + installedOnly: true, + enabledOnly: !$showAll + ); + + if (count($modules) === 0) { + $io->warning('No modules found.'); + return Command::SUCCESS; + } + + $rows = []; + foreach ($modules as $module) { + $status = $module->enabled() ? 'Enabled' : 'Disabled'; + $upgrade = $module->needsUpgrade() ? 'Yes' : ''; + + $rows[] = [ + $module->handle(), + $module->version(), + $status, + $upgrade, + $module->namespace() ?? 'N/A', + ]; + } + + $io->table( + ['Handle', 'Version', 'Status', 'Needs Upgrade', 'Namespace'], + $rows + ); + + $io->success(sprintf('Found %d module(s).', count($modules))); + + return Command::SUCCESS; + + } catch (\Throwable $e) { + $io->error('Failed to list modules: ' . $e->getMessage()); + return Command::FAILURE; + } + } +} diff --git a/shared/lib/Module/ModuleConsoleInterface.php b/shared/lib/Module/ModuleConsoleInterface.php new file mode 100644 index 0000000..ff5b352 --- /dev/null +++ b/shared/lib/Module/ModuleConsoleInterface.php @@ -0,0 +1,13 @@ +