first commit.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Toolbox\Commands;
|
||||
|
||||
use Toolbox\Installer;
|
||||
use Toolbox\ToolboxException;
|
||||
|
||||
final class AddCommand extends Command
|
||||
{
|
||||
public static function describe(): string
|
||||
{
|
||||
return 'Instala componentes o paquetes (alta con copia real)';
|
||||
}
|
||||
|
||||
public function run(array $args): int
|
||||
{
|
||||
$parsed = $this->parse($args);
|
||||
|
||||
if ($parsed['args'] === []) {
|
||||
$this->console->error('Indica al menos un componente o paquete.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$root = $this->root($parsed['options']);
|
||||
$toolbox = $this->toolboxDir($parsed['options']);
|
||||
$installer = new Installer($root, $toolbox, $this->console, $this->rootCore($parsed['options'], $root));
|
||||
|
||||
try {
|
||||
$result = $installer->add($parsed['args'], $this->behaviorOptions($parsed['options']));
|
||||
} catch (ToolboxException $e) {
|
||||
$this->console->error($e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
foreach ($result['installed'] as $id) {
|
||||
$this->console->ok($id);
|
||||
}
|
||||
foreach ($result['warnings'] as $warning) {
|
||||
$this->console->warn($warning);
|
||||
}
|
||||
foreach ($result['aborted'] as $aborted) {
|
||||
$this->console->error('abortado: ' . $aborted);
|
||||
}
|
||||
|
||||
return $result['aborted'] === [] ? 0 : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace Toolbox\Commands;
|
||||
|
||||
use Toolbox\Destination;
|
||||
use Toolbox\DirectorySource;
|
||||
use Toolbox\Drift;
|
||||
use Toolbox\FileExpander;
|
||||
use Toolbox\Installer;
|
||||
use Toolbox\ToolboxException;
|
||||
|
||||
final class AdoptCommand extends Command
|
||||
{
|
||||
public static function describe(): string
|
||||
{
|
||||
return 'Adopta el core y las copias existentes y las registra en el lock';
|
||||
}
|
||||
|
||||
public function run(array $args): int
|
||||
{
|
||||
$parsed = $this->parse($args);
|
||||
$root = $this->root($parsed['options']);
|
||||
$toolbox = $this->toolboxDir($parsed['options']);
|
||||
$rootCore = $this->rootCore($parsed['options'], $root) ?? $root . '/src';
|
||||
$dryRun = $this->flag($parsed['options'], 'dry-run');
|
||||
$installer = new Installer($root, $toolbox, $this->console, $rootCore);
|
||||
|
||||
$catalog = $installer->catalog();
|
||||
$lock = $installer->lock();
|
||||
$source = new DirectorySource(rtrim($toolbox, '/') . '/packages');
|
||||
|
||||
$adopted = [];
|
||||
$divergent = [];
|
||||
|
||||
foreach ($catalog->packages() as $package) {
|
||||
foreach ($package->components() as $name => $component) {
|
||||
$id = $package->name . ':' . $name;
|
||||
if ($lock->has($id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$files = [];
|
||||
$present = 0;
|
||||
$hasLocal = false;
|
||||
|
||||
foreach (FileExpander::expand($source, $package->name, $component->files) as $file) {
|
||||
$relative = Destination::relative($file, $root, $rootCore, $component->destinations);
|
||||
$absolute = $root . '/' . $relative;
|
||||
|
||||
if (!is_file($absolute)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$present++;
|
||||
$canonical = $source->read($package->name, $file);
|
||||
|
||||
if ($canonical !== null) {
|
||||
// Se registra el hash CANÓNICO: si la copia local difiere,
|
||||
// queda marcada como edición local (drift) y `update`
|
||||
// preguntará antes de pisarla.
|
||||
$files[$relative] = 'sha256:' . hash('sha256', $canonical);
|
||||
if (file_get_contents($absolute) !== $canonical) {
|
||||
$hasLocal = true;
|
||||
}
|
||||
} else {
|
||||
$files[$relative] = Drift::hash($absolute);
|
||||
$hasLocal = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($present === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lock->setComponent($id, [
|
||||
'package' => $package->name,
|
||||
'version' => $package->version,
|
||||
'toolbox_commit' => null,
|
||||
'mode' => 'copy',
|
||||
'origin' => $hasLocal ? 'local' : 'adopted',
|
||||
'files' => $files,
|
||||
]);
|
||||
|
||||
$adopted[] = $id;
|
||||
if ($hasLocal) {
|
||||
$divergent[] = $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$coreAdopted = $this->adoptCore($parsed['options'], $root, $lock, $dryRun);
|
||||
|
||||
if (!$dryRun) {
|
||||
$lock->save($root);
|
||||
}
|
||||
|
||||
foreach ($adopted as $id) {
|
||||
$mark = in_array($id, $divergent, true) ? ' (con ediciones locales)' : '';
|
||||
$this->console->ok('adoptado ' . $id . $mark);
|
||||
}
|
||||
if ($coreAdopted > 0) {
|
||||
$this->console->ok("adoptado el core ({$coreAdopted} archivos)");
|
||||
}
|
||||
if ($adopted === [] && $coreAdopted === 0) {
|
||||
$this->console->out('No se encontró nada que adoptar.');
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra el core existente: sólo los archivos que también existen en el
|
||||
* core de referencia, con su hash actual. No toca archivos.
|
||||
*
|
||||
* @param array<string, string|bool> $options
|
||||
*/
|
||||
private function adoptCore(array $options, string $root, $lock, bool $dryRun): int
|
||||
{
|
||||
if ($lock->core() !== null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
$provider = $this->coreProvider($options);
|
||||
} catch (ToolboxException $e) {
|
||||
$this->console->warn('no se pudo resolver el core para adoptar: ' . $e->getMessage());
|
||||
return 0;
|
||||
}
|
||||
|
||||
$files = [];
|
||||
foreach ($provider->files() as $relative) {
|
||||
$absolute = $root . '/' . $relative;
|
||||
if (is_file($absolute)) {
|
||||
$files[$relative] = Drift::hash($absolute);
|
||||
}
|
||||
}
|
||||
|
||||
if ($files === []) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!$dryRun) {
|
||||
$lock->setCore([
|
||||
'version' => $provider->version(),
|
||||
'commit' => $provider->commit(),
|
||||
'files' => $files,
|
||||
]);
|
||||
}
|
||||
|
||||
return count($files);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Toolbox\Commands;
|
||||
|
||||
use Toolbox\Catalog;
|
||||
use Toolbox\Lock;
|
||||
|
||||
final class CatalogCommand extends Command
|
||||
{
|
||||
public static function describe(): string
|
||||
{
|
||||
return 'Lista los paquetes y componentes disponibles';
|
||||
}
|
||||
|
||||
public function run(array $args): int
|
||||
{
|
||||
$parsed = $this->parse($args);
|
||||
$root = $this->root($parsed['options']);
|
||||
$toolbox = $this->toolboxDir($parsed['options']);
|
||||
|
||||
$catalog = Catalog::fromRoot($toolbox);
|
||||
$lock = Lock::load($root);
|
||||
|
||||
$rows = [['PAQUETE', 'COMPONENTE', 'ESTADO', 'DESCRIPCIÓN']];
|
||||
foreach ($catalog->packages() as $package) {
|
||||
$first = true;
|
||||
foreach ($package->components() as $name => $component) {
|
||||
$id = $package->name . ':' . $name;
|
||||
$installed = $lock->has($id) ? 'instalado' : '-';
|
||||
$rows[] = [
|
||||
$first ? $package->name . ' (' . $package->version . ')' : '',
|
||||
$name,
|
||||
$installed,
|
||||
$first ? $package->description : '',
|
||||
];
|
||||
$first = false;
|
||||
}
|
||||
}
|
||||
|
||||
$this->console->table($rows);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Toolbox\Commands;
|
||||
|
||||
use Toolbox\Config;
|
||||
use Toolbox\Console;
|
||||
use Toolbox\CoreProvider;
|
||||
|
||||
abstract class Command implements CommandInterface
|
||||
{
|
||||
public function __construct(protected readonly Console $console)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $args
|
||||
* @return array{options: array<string, string|bool>, args: list<string>}
|
||||
*/
|
||||
protected function parse(array $args): array
|
||||
{
|
||||
$options = [];
|
||||
$positional = [];
|
||||
|
||||
foreach ($args as $arg) {
|
||||
if (str_starts_with($arg, '--')) {
|
||||
$token = substr($arg, 2);
|
||||
if (str_contains($token, '=')) {
|
||||
[$key, $value] = explode('=', $token, 2);
|
||||
$options[$key] = $value;
|
||||
} else {
|
||||
$options[$token] = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$positional[] = $arg;
|
||||
}
|
||||
|
||||
return ['options' => $options, 'args' => $positional];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|bool> $options
|
||||
*/
|
||||
protected function option(array $options, string $key, ?string $default = null): ?string
|
||||
{
|
||||
$value = $options[$key] ?? null;
|
||||
|
||||
if ($value === true || $value === null) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|bool> $options
|
||||
*/
|
||||
protected function flag(array $options, string $key): bool
|
||||
{
|
||||
return array_key_exists($key, $options);
|
||||
}
|
||||
|
||||
protected function root(array $options): string
|
||||
{
|
||||
return rtrim($this->option($options, 'root', getcwd() ?: '.') ?? '.', '/');
|
||||
}
|
||||
|
||||
protected function toolboxDir(array $options): string
|
||||
{
|
||||
return rtrim($this->option($options, 'toolbox', getcwd() ?: '.') ?? '.', '/');
|
||||
}
|
||||
|
||||
protected function rootCore(array $options, string $root): ?string
|
||||
{
|
||||
$core = $this->option($options, 'core');
|
||||
|
||||
return $core === null ? null : rtrim($core, '/');
|
||||
}
|
||||
|
||||
protected function coreProvider(array $options): CoreProvider
|
||||
{
|
||||
$dir = $this->option($options, 'core-dir');
|
||||
if ($dir !== null) {
|
||||
return CoreProvider::fromDirectory($dir);
|
||||
}
|
||||
|
||||
return CoreProvider::fromUrl(Config::coreUrl(), Config::cacheDir(), $this->console);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|bool> $options
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
protected function behaviorOptions(array $options): array
|
||||
{
|
||||
return [
|
||||
'force' => $this->flag($options, 'force'),
|
||||
'backup' => $this->flag($options, 'backup'),
|
||||
'keepLocal' => $this->flag($options, 'keep-local'),
|
||||
'dryRun' => $this->flag($options, 'dry-run'),
|
||||
'yes' => $this->flag($options, 'yes') || $this->flag($options, 'y'),
|
||||
'link' => $this->flag($options, 'link'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Toolbox\Commands;
|
||||
|
||||
interface CommandInterface
|
||||
{
|
||||
/**
|
||||
* @param list<string> $args
|
||||
*/
|
||||
public function run(array $args): int;
|
||||
|
||||
public static function describe(): string;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Toolbox\Commands;
|
||||
|
||||
use Toolbox\Catalog;
|
||||
use Toolbox\Compatibility;
|
||||
use Toolbox\Lock;
|
||||
|
||||
final class DoctorCommand extends Command
|
||||
{
|
||||
public static function describe(): string
|
||||
{
|
||||
return 'Matriz de compatibilidad de los componentes instalados con el core';
|
||||
}
|
||||
|
||||
public function run(array $args): int
|
||||
{
|
||||
$parsed = $this->parse($args);
|
||||
$root = $this->root($parsed['options']);
|
||||
$toolbox = $this->toolboxDir($parsed['options']);
|
||||
|
||||
$lock = Lock::load($root);
|
||||
$catalog = Catalog::fromRoot($toolbox);
|
||||
|
||||
$coreVersion = $lock->core()['version'] ?? null;
|
||||
|
||||
if ($lock->components() === []) {
|
||||
$this->console->out('No hay componentes instalados.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$rows = [['COMPONENTE', 'VER', 'CORE REQ', 'CORE PROBADO', 'CORE ACTUAL', 'ESTADO']];
|
||||
foreach ($lock->components() as $id => $entry) {
|
||||
[$package, $component] = array_pad(explode(':', $id, 2), 2, null);
|
||||
|
||||
$requires = null;
|
||||
$tested = null;
|
||||
if ($component !== null && $package !== null && $catalog->has($package)) {
|
||||
$manifest = $catalog->get($package);
|
||||
if ($manifest->hasComponent($component)) {
|
||||
$requires = $manifest->coreRequirement($component);
|
||||
$tested = $manifest->testedCore($component);
|
||||
}
|
||||
}
|
||||
|
||||
$result = Compatibility::evaluateCore($coreVersion, $requires, $tested);
|
||||
|
||||
$rows[] = [
|
||||
$id,
|
||||
(string) ($entry['version'] ?? ''),
|
||||
$requires ?? '—',
|
||||
$tested ?? '—',
|
||||
$coreVersion ?? '—',
|
||||
Compatibility::label($result['status']),
|
||||
];
|
||||
}
|
||||
|
||||
$this->console->table($rows);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Toolbox\Commands;
|
||||
|
||||
use Toolbox\Installer;
|
||||
use Toolbox\ToolboxException;
|
||||
|
||||
final class InstallCommand extends Command
|
||||
{
|
||||
public static function describe(): string
|
||||
{
|
||||
return 'Descarga el core Duckbrain en un directorio (bootstrap)';
|
||||
}
|
||||
|
||||
public function run(array $args): int
|
||||
{
|
||||
$parsed = $this->parse($args);
|
||||
$dir = rtrim($parsed['args'][0] ?? '.', '/');
|
||||
|
||||
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
|
||||
$this->console->error('No se pudo crear el directorio: ' . $dir);
|
||||
return 1;
|
||||
}
|
||||
|
||||
$toolbox = $this->toolboxDir($parsed['options']);
|
||||
$installer = new Installer($dir, $toolbox, $this->console, $this->rootCore($parsed['options'], $dir));
|
||||
|
||||
try {
|
||||
$provider = $this->coreProvider($parsed['options']);
|
||||
$result = $installer->installCore($provider, $this->behaviorOptions($parsed['options']));
|
||||
} catch (ToolboxException $e) {
|
||||
$this->console->error($e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->console->ok('core ' . ($result['version'] ?? '?') . ' en ' . $dir);
|
||||
foreach ($result['aborted'] as $aborted) {
|
||||
$this->console->error('abortado: ' . $aborted);
|
||||
}
|
||||
|
||||
return $result['aborted'] === [] ? 0 : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Toolbox\Commands;
|
||||
|
||||
use Toolbox\Installer;
|
||||
use Toolbox\ToolboxException;
|
||||
|
||||
final class ListCommand extends Command
|
||||
{
|
||||
public static function describe(): string
|
||||
{
|
||||
return 'Lista el core y los componentes instalados';
|
||||
}
|
||||
|
||||
public function run(array $args): int
|
||||
{
|
||||
$parsed = $this->parse($args);
|
||||
$root = $this->root($parsed['options']);
|
||||
$toolbox = $this->toolboxDir($parsed['options']);
|
||||
$installer = new Installer($root, $toolbox, $this->console, $this->rootCore($parsed['options'], $root));
|
||||
|
||||
$rows = [['TIPO', 'ID', 'VERSIÓN', 'MODO']];
|
||||
|
||||
$core = $installer->lock()->core();
|
||||
if ($core !== null) {
|
||||
$rows[] = ['core', 'core', (string) ($core['version'] ?? '?'), 'copy'];
|
||||
}
|
||||
|
||||
foreach ($installer->lock()->components() as $id => $entry) {
|
||||
$rows[] = [$entry['package'], $id, $entry['version'], $entry['mode']];
|
||||
}
|
||||
|
||||
if (count($rows) === 1) {
|
||||
$this->console->out('No hay nada instalado.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->console->table($rows);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Toolbox\Commands;
|
||||
|
||||
use Toolbox\Installer;
|
||||
use Toolbox\ToolboxException;
|
||||
|
||||
final class RemoveCommand extends Command
|
||||
{
|
||||
public static function describe(): string
|
||||
{
|
||||
return 'Quita componentes instalados (sólo sus archivos)';
|
||||
}
|
||||
|
||||
public function run(array $args): int
|
||||
{
|
||||
$parsed = $this->parse($args);
|
||||
|
||||
if ($parsed['args'] === []) {
|
||||
$this->console->error('Indica al menos un componente (paquete:componente).');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$root = $this->root($parsed['options']);
|
||||
$toolbox = $this->toolboxDir($parsed['options']);
|
||||
$installer = new Installer($root, $toolbox, $this->console, $this->rootCore($parsed['options'], $root));
|
||||
|
||||
try {
|
||||
$result = $installer->remove($parsed['args'], $this->behaviorOptions($parsed['options']));
|
||||
} catch (ToolboxException $e) {
|
||||
$this->console->error($e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
foreach ($result['removed'] as $id) {
|
||||
$this->console->ok('quitado ' . $id);
|
||||
}
|
||||
foreach ($result['aborted'] as $aborted) {
|
||||
$this->console->error('abortado (edición local): ' . $aborted);
|
||||
}
|
||||
|
||||
return $result['aborted'] === [] ? 0 : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Toolbox\Commands;
|
||||
|
||||
use Toolbox\Config;
|
||||
use Toolbox\SelfUpdater;
|
||||
use Toolbox\ToolboxException;
|
||||
|
||||
final class SelfUpdateCommand extends Command
|
||||
{
|
||||
public static function describe(): string
|
||||
{
|
||||
return 'Actualiza el instalador global (script/phar)';
|
||||
}
|
||||
|
||||
public function run(array $args): int
|
||||
{
|
||||
$parsed = $this->parse($args);
|
||||
$from = $this->option($parsed['options'], 'from', Config::updateUrl());
|
||||
$target = $this->option($parsed['options'], 'target', Config::binPath());
|
||||
$temp = $this->option($parsed['options'], 'temp', sys_get_temp_dir());
|
||||
|
||||
if ($from === null || $target === null) {
|
||||
$this->console->error('Faltan --from o --target.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
(new SelfUpdater($this->console))->update($from, $target, $temp);
|
||||
} catch (ToolboxException $e) {
|
||||
$this->console->error($e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Toolbox\Commands;
|
||||
|
||||
use Toolbox\Installer;
|
||||
use Toolbox\ToolboxException;
|
||||
|
||||
final class UpdateCommand extends Command
|
||||
{
|
||||
public static function describe(): string
|
||||
{
|
||||
return 'Actualiza el core, componentes o todo el proyecto';
|
||||
}
|
||||
|
||||
public function run(array $args): int
|
||||
{
|
||||
$parsed = $this->parse($args);
|
||||
$options = $this->behaviorOptions($parsed['options']);
|
||||
$root = $this->root($parsed['options']);
|
||||
$toolbox = $this->toolboxDir($parsed['options']);
|
||||
$installer = new Installer($root, $toolbox, $this->console, $this->rootCore($parsed['options'], $root));
|
||||
|
||||
try {
|
||||
if (($parsed['args'][0] ?? null) === 'core') {
|
||||
$provider = $this->coreProvider($parsed['options']);
|
||||
$result = $installer->updateCore($provider, $options);
|
||||
$this->console->ok('core ' . ($result['version'] ?? '?'));
|
||||
foreach ($result['aborted'] as $aborted) {
|
||||
$this->console->error('abortado: ' . $aborted);
|
||||
}
|
||||
|
||||
return $result['aborted'] === [] ? 0 : 1;
|
||||
}
|
||||
|
||||
$ids = $parsed['args'] !== []
|
||||
? $parsed['args']
|
||||
: array_keys($installer->lock()->components());
|
||||
|
||||
if ($ids === []) {
|
||||
$this->console->out('No hay componentes instalados que actualizar.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$result = $installer->update($ids, $options);
|
||||
} catch (ToolboxException $e) {
|
||||
$this->console->error($e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
foreach ($result['installed'] as $id) {
|
||||
$this->console->ok('actualizado ' . $id);
|
||||
}
|
||||
foreach ($result['warnings'] as $warning) {
|
||||
$this->console->warn($warning);
|
||||
}
|
||||
foreach ($result['aborted'] as $aborted) {
|
||||
$this->console->error('abortado: ' . $aborted);
|
||||
}
|
||||
|
||||
return $result['aborted'] === [] ? 0 : 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user