implement console

This commit is contained in:
root
2026-01-07 00:05:55 -05:00
parent 965d839a8c
commit 3324157ed9
7 changed files with 1064 additions and 8 deletions

View File

@@ -2,16 +2,116 @@
<?php
/**
* Console entry point - Symfony console has been removed.
* Add your own CLI commands here or integrate an alternative CLI library.
* Console Entry Point
*
* Bootstraps the application container and registers console commands
* from core and modules using lazy loading via Symfony Console.
*/
declare(strict_types=1);
use KTXC\Application;
use KTXC\Kernel;
use KTXC\Module\ModuleManager;
use KTXF\Module\ModuleConsoleInterface;
use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Console\Command\LazyCommand;
// Check dependencies
if (!is_dir(dirname(__DIR__).'/vendor')) {
throw new LogicException('Dependencies are missing. Try running "composer install".');
fwrite(STDERR, "Dependencies are missing. Run 'composer install' first.\n");
exit(1);
}
require_once dirname(__DIR__).'/vendor/autoload.php';
echo "Console commands not available. Symfony console has been removed.\n";
echo "To add CLI functionality, integrate an alternative CLI library.\n";
exit(0);
try {
// Bootstrap the application
$projectRoot = dirname(__DIR__);
$app = new Application($projectRoot);
// Boot kernel to initialize container and modules
$app->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);
}

View File

@@ -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"

680
composer.lock generated
View File

@@ -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": [

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace KTXC\Console;
use KTXC\Module\ModuleManager;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Module Disable Command
*
* Disables an enabled module.
*/
#[AsCommand(
name: 'module:disable',
description: 'Disable a module',
)]
class ModuleDisableCommand extends Command
{
public function __construct(
private readonly ModuleManager $moduleManager,
private readonly LoggerInterface $logger
) {
parent::__construct();
}
protected function configure(): void
{
$this
->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;
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace KTXC\Console;
use KTXC\Module\ModuleManager;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Module Enable Command
*
* Enables a disabled module.
*/
#[AsCommand(
name: 'module:enable',
description: 'Enable a module',
)]
class ModuleEnableCommand extends Command
{
public function __construct(
private readonly ModuleManager $moduleManager,
private readonly LoggerInterface $logger
) {
parent::__construct();
}
protected function configure(): void
{
$this
->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;
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace KTXC\Console;
use KTXC\Module\ModuleManager;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Module List Command
*
* Lists all modules with their status and version information.
*/
#[AsCommand(
name: 'module:list',
description: 'List all modules with their status and versions',
)]
class ModuleListCommand extends Command
{
public function __construct(
private readonly ModuleManager $moduleManager
) {
parent::__construct();
}
protected function configure(): void
{
$this
->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() ? '<fg=green>Enabled</>' : '<fg=yellow>Disabled</>';
$upgrade = $module->needsUpgrade() ? '<fg=red>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;
}
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace KTXF\Module;
/**
* Module Console Interface
*/
interface ModuleConsoleInterface
{
public function registerCI(): array;
}