meritum/cli#
meritum/cli extends georgeff/kernel with a command-based console runtime. Everything in the kernel guide
applies here — modules, service definitions, tagging, decoration, and the boot lifecycle all work identically.
The console runtime is named Sage, after Jefferson’s sobriquet “the Sage of Monticello.”
composer require meritum/cliEntry Point#
CliKernel is the concrete implementation. Wire it up in your entry point script:
use App\ModuleRepository;
use Meritum\Cli\CliKernel;
use Georgeff\Kernel\Support\Env;
use Georgeff\Kernel\Environment;
$environment = match (Env::get('APP_ENV', 'production')) {
'local' => Environment::Local,
'test', 'testing' => Environment::Testing,
'dev', 'develop', 'development' => Environment::Development,
'stage', 'staging' => Environment::Staging,
default => Environment::Production,
};
$kernel = new CliKernel($environment, debug: Env::get('APP_DEBUG', false));
$kernel->addRepository(new ModuleRepository());
$kernel->boot();
exit($kernel->run());run() delegates to handle(), which reads $argv, dispatches to the matched command, and returns an integer exit code.
Application Name and Version#
By default the application is named Sage and the version is auto-detected from Composer’s installed package data. Override either before boot():
$kernel->setName('Acme Console');
$kernel->setVersion('1.0.0');Both throw KernelException if called after boot().
Commands#
Commands extend the abstract Command class and implement __invoke(). Define the command’s metadata in the constructor using the fluent builder methods; implement the execution logic in __invoke().
use Meritum\Cli\ExitCode;
use Meritum\Cli\Command\Command;
use Meritum\Cli\Output\SageStyleInterface;
final class GreetCommand extends Command
{
public function __construct()
{
$this->setName('app:greet')
->setDescription('Greet a user by name');
$this->addArgument('name')
->description('The name to greet')
->optional('World');
}
public function __invoke(SageStyleInterface $io): ExitCode
{
$io->success('Hello, ' . $io->argument('name') . '!');
return ExitCode::Success;
}
}The __invoke() method receives a SageStyleInterface
instance and must return an ExitCode
.
Arguments#
Add arguments to a command with addArgument(). It returns an Argument
you can configure with a fluent API:
// Required argument (default)
$this->addArgument('path')
->description('Path to the target file');
// Optional argument with a default value
$this->addArgument('env')
->description('The environment to target')
->optional('production');
// Array argument — accepts multiple values
$this->addArgument('files')
->description('Files to process')
->array();
// With completion suggestions
$this->addArgument('driver')
->description('Database driver')
->suggest('mysql', 'pgsql', 'sqlite');Read argument values inside __invoke() via $io->argument():
$path = $io->argument('path');
$files = $io->argument('files'); // array when ->array() was called
Options#
Add options with addOption(). It returns an Option
you can configure:
// Flag option (no value, defaults to false)
$this->addOption('verbose')
->description('Enable verbose output')
->shortcut('v');
// Option that requires a value
$this->addOption('format')
->description('Output format')
->required()
->suggest('json', 'table');
// Option with an optional value
$this->addOption('output')
->description('Output file path')
->optional('/dev/stdout');
// Array option — can be passed multiple times
$this->addOption('tag')
->description('Tags to apply')
->required()
->array();
// Negatable flag (--feature / --no-feature)
$this->addOption('cache')
->description('Use the cache')
->negatable();Read option values inside __invoke() via $io->option():
$format = $io->option('format');
$verbose = $io->option('verbose'); // bool for flags
Output#
The $io parameter passed to __invoke() is a SageStyleInterface
, which extends symfony’s StyleInterface and OutputInterface. All symfony output methods are available:
$io->title('My Application');
$io->section('Running tasks');
$io->text('Processing...');
$io->success('Done!');
$io->error('Something went wrong');
$io->warning('This may cause issues');
$io->info('Just so you know');
$io->note('Additional context');
$io->table(
['Name', 'Status'],
[['deploy', 'ok'], ['migrate', 'pending']]
);Refer to the symfony Console documentation for the full list of available output methods.
Exit Codes#
Return ExitCode::Success
on success and ExitCode::Error on failure:
public function __invoke(SageStyleInterface $io): ExitCode
{
if (!$this->canProceed()) {
$io->error('Cannot proceed.');
return ExitCode::Error;
}
$this->doWork();
return ExitCode::Success;
}ExitCode::Success maps to 0; ExitCode::Error maps to 1.
Registering Commands#
Commands are registered using the standard kernel tagging API. Tag any defined service with 'cli.commands' and the CLI kernel will pick it up automatically during boot:
use Georgeff\Kernel\KernelInterface;
use Georgeff\Kernel\Module\ModuleInterface;
final class AppModule implements ModuleInterface
{
public function register(KernelInterface $kernel): void
{
$kernel->define(GreetCommand::class, fn() => new GreetCommand())
->tag('cli.commands');
}
}Constructor injection works because commands are resolved from the container:
$kernel->define(SendReportCommand::class, fn($c) => new SendReportCommand(
$c->get(MailerInterface::class),
$c->get(ReportBuilder::class)
))->tag('cli.commands');Calling Other Commands#
From within a command, call another registered command using $this->call():
public function __invoke(SageStyleInterface $io): ExitCode
{
$this->call('db:migrate', $io);
$this->call('cache:clear', $io);
return ExitCode::Success;
}Pass input parameters as the third argument:
$this->call('db:seed', $io, ['--class' => 'UserSeeder']);If no output is provided the called command runs silently.
Exception Handling#
By default, exceptions that escape a command bubble up and symfony renders its own error output. To handle them yourself, register an implementation of ExceptionHandlerInterface in the container:
use Throwable;
use Meritum\Cli\Output\SageStyleInterface;
use Meritum\Cli\Exception\ExceptionHandlerInterface;
final class ExceptionHandler implements ExceptionHandlerInterface
{
public function handle(Throwable $exception, SageStyleInterface $io): void
{
$io->error($exception->getMessage());
}
}Register it in a module:
use Meritum\Cli\Exception\ExceptionHandlerInterface;
$kernel->define(ExceptionHandlerInterface::class, fn() => new ExceptionHandler());The kernel checks for ExceptionHandlerInterface::class in the container on every handle() call. If present, caught exceptions are passed to it and handle() returns ExitCode::Error (1). If absent, exceptions bubble.
meritum/structured-loggingprovides pipeline tooling that can be wired into the exception handler to capture and structure error context for observability.
Default Container Registrations#
CliKernel registers one service automatically during the boot sequence:
| ID | Resolves to |
|---|---|
'cli.app' |
The symfony Application instance (internal) |
Do not overwrite 'cli.app' unless you intend to replace the entire console runtime.