Skip to content

Commit 139d7a0

Browse files
add Annabel CLI with built-in commands and lazy-loaded database migration tooling
1 parent 9f6d625 commit 139d7a0

15 files changed

+842
-4
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
All notable changes to **codemonster-ru/annabel** will be documented in this file.
44

5-
## [Unreleased]
5+
## [1.12.0]
66

77
### Added
88

99
- New `annabel` CLI entry point (`php vendor/bin/annabel`) with built-in help output inspired by Laravel's artisan, including colorized terminal formatting.
10+
- Added initial CLI commands: `about`, `route:list`, `config:get`, `container:list`, and `serve`.
11+
- When `codemonster-ru/database` is installed, Annabel CLI now auto-loads database commands (`make:migration`, `migrate`, `migrate:rollback`, `migrate:status`) via the package's CLI kernel.
12+
- Database CLI integration is now lazy: migration repository/connection initialize only when commands run, so commands are visible even without an active DB connection.
1013

1114
## [1.11.0] – 2025-12-09
1215

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,14 @@ router()->get('/', fn() => view('home', ['title' => 'Welcome to Annabel']));
3939

4040
## CLI
4141

42-
Annabel ships with a lightweight CLI similar to Laravel's `artisan`. For now it provides helpful usage information and a command list.
42+
Annabel ships with a lightweight CLI similar to Laravel's `artisan`. It already supports:
43+
44+
- `about` — show version, base path, and loaded providers
45+
- `route:list` — list registered routes
46+
- `config:get key` — read a config value
47+
- `container:list` — show container bindings/instances
48+
- `serve` — run PHP built-in server (default 127.0.0.1:8000)
49+
- With `codemonster-ru/database` installed: `make:migration`, `migrate`, `migrate:rollback`, `migrate:status` (appear in `annabel list`; connection is checked when commands run)
4350

4451
```bash
4552
php vendor/bin/annabel
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace Codemonster\Annabel\Console\Commands;
4+
5+
use Codemonster\Annabel\Console\Command;
6+
7+
class AboutCommand extends Command
8+
{
9+
public function getName(): string
10+
{
11+
return 'about';
12+
}
13+
14+
public function getDescription(): string
15+
{
16+
return 'Show basic information about the Annabel application.';
17+
}
18+
19+
public function handle(array $arguments = []): int
20+
{
21+
$console = $this->console();
22+
$app = $console->getApplication();
23+
24+
$console->writeln($console->color('About', 'label'));
25+
$console->writeln(' Version: ' . $console->color($console->getVersion(), 'command'));
26+
$console->writeln(' Base path: ' . $app->getBasePath());
27+
28+
$providers = $app->getProviders();
29+
$console->writeln(' Providers: ' . count($providers));
30+
31+
foreach ($providers as $provider) {
32+
$console->writeln(' - ' . get_class($provider));
33+
}
34+
35+
return 0;
36+
}
37+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace Codemonster\Annabel\Console\Commands;
4+
5+
use Codemonster\Annabel\Console\Command;
6+
use Codemonster\Config\Config;
7+
8+
class ConfigGetCommand extends Command
9+
{
10+
public function getName(): string
11+
{
12+
return 'config:get';
13+
}
14+
15+
public function getDescription(): string
16+
{
17+
return 'Get a configuration value by key (dot notation).';
18+
}
19+
20+
public function getUsage(): string
21+
{
22+
return 'config:get key';
23+
}
24+
25+
public function handle(array $arguments = []): int
26+
{
27+
$console = $this->console();
28+
29+
if (empty($arguments[0])) {
30+
$console->writeln($console->color('Usage: php vendor/bin/annabel config:get key', 'error'));
31+
32+
return 1;
33+
}
34+
35+
$key = $arguments[0];
36+
$app = $console->getApplication();
37+
38+
$config = $app->make(Config::class);
39+
$value = $config->get($key);
40+
41+
if ($value === null) {
42+
$console->writeln($console->color("null (or key not found): {$key}", 'muted'));
43+
44+
return 0;
45+
}
46+
47+
$console->writeln($console->color($key, 'label') . ':');
48+
$console->writeln($this->formatValue($value));
49+
50+
return 0;
51+
}
52+
53+
protected function formatValue(mixed $value): string
54+
{
55+
if (is_scalar($value) || $value === null) {
56+
return ' ' . var_export($value, true);
57+
}
58+
59+
$json = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
60+
61+
if ($json !== false) {
62+
$lines = explode("\n", $json);
63+
64+
return ' ' . implode("\n ", $lines);
65+
}
66+
67+
return ' ' . print_r($value, true);
68+
}
69+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace Codemonster\Annabel\Console\Commands;
4+
5+
use Codemonster\Annabel\Console\Command;
6+
7+
class ContainerListCommand extends Command
8+
{
9+
public function getName(): string
10+
{
11+
return 'container:list';
12+
}
13+
14+
public function getDescription(): string
15+
{
16+
return 'Show container bindings and instantiated singletons.';
17+
}
18+
19+
public function getUsage(): string
20+
{
21+
return 'container:list';
22+
}
23+
24+
public function handle(array $arguments = []): int
25+
{
26+
$console = $this->console();
27+
$app = $console->getApplication();
28+
$container = $app->getContainer();
29+
30+
$bindings = $container->getBindings();
31+
$instances = $container->getInstances();
32+
33+
$console->writeln($console->color('Bindings:', 'label'));
34+
35+
if (empty($bindings)) {
36+
$console->writeln(' ' . $console->color('none', 'muted'));
37+
} else {
38+
foreach ($bindings as $abstract => $binding) {
39+
$concrete = is_string($binding['concrete']) ? $binding['concrete'] : 'Closure';
40+
$console->writeln(sprintf(
41+
' %s => %s%s',
42+
$console->color($abstract, 'command'),
43+
$concrete,
44+
$binding['singleton'] ? ' (singleton)' : ''
45+
));
46+
}
47+
}
48+
49+
$console->writeln('');
50+
$console->writeln($console->color('Instances:', 'label'));
51+
52+
if (empty($instances)) {
53+
$console->writeln(' ' . $console->color('none', 'muted'));
54+
} else {
55+
foreach ($instances as $abstract => $instance) {
56+
$console->writeln(sprintf(
57+
' %s => %s',
58+
$console->color($abstract, 'command'),
59+
get_class($instance)
60+
));
61+
}
62+
}
63+
64+
return 0;
65+
}
66+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace Codemonster\Annabel\Console\Commands;
4+
5+
use Codemonster\Annabel\Console\Command;
6+
use Codemonster\Database\CLI\DatabaseCLIKernel;
7+
8+
class DatabaseCommand extends Command
9+
{
10+
public function __construct(
11+
protected string $signature,
12+
protected string $description
13+
) {}
14+
15+
public function getName(): string
16+
{
17+
return $this->signature;
18+
}
19+
20+
public function getDescription(): string
21+
{
22+
return $this->description;
23+
}
24+
25+
public function getUsage(): string
26+
{
27+
return $this->signature;
28+
}
29+
30+
public function handle(array $arguments = []): int
31+
{
32+
$console = $this->console();
33+
34+
try {
35+
/** @var DatabaseCLIKernel $kernel */
36+
$kernel = $console->getApplication()->make(DatabaseCLIKernel::class);
37+
$command = $kernel->getRegistry()->get($this->signature);
38+
39+
if (!$command) {
40+
$console->writeln($console->color("Database command [{$this->signature}] not found.", 'error'));
41+
42+
return 1;
43+
}
44+
45+
return $command->handle($arguments);
46+
} catch (\Throwable $e) {
47+
$console->writeln($console->color(
48+
"Cannot run database command [{$this->signature}]. Check database configuration and connection. Error: {$e->getMessage()}",
49+
'error'
50+
));
51+
52+
return 1;
53+
}
54+
}
55+
}

src/Console/Commands/HelpCommand.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,22 @@ protected function renderApplicationHelp(): void
6161
$console->writeln('');
6262
$console->writeln($console->color('Available commands:', 'label'));
6363

64-
foreach ($console->getCommands() as $command) {
64+
$commands = $console->getCommands();
65+
$maxLength = 0;
66+
67+
foreach ($commands as $cmd) {
68+
$maxLength = max($maxLength, strlen($cmd->getName()));
69+
}
70+
71+
$maxLength = max($maxLength, 12);
72+
73+
foreach ($commands as $command) {
6574
$aliases = $console->getAliasesFor($command->getName());
6675
$aliasText = $aliases ? ' [' . implode(', ', $aliases) . ']' : '';
6776

6877
$console->writeln(sprintf(
6978
' %s %s%s',
70-
$console->color(str_pad($command->getName(), 12), 'command'),
79+
$console->color(str_pad($command->getName(), $maxLength), 'command'),
7180
$command->getDescription(),
7281
$aliasText ? ' ' . $console->color($aliasText, 'muted') : ''
7382
));

0 commit comments

Comments
 (0)