fix(commands): surface core fetch errors, run post-install in the project, exit 1 on invalid project command; document compat flow

This commit is contained in:
kj
2026-10-05 17:36:08 -03:00
parent eb5e6eeed4
commit 6925b814e6
6 changed files with 97 additions and 55 deletions
+22 -8
View File
@@ -233,6 +233,19 @@ Nunca bloquea: se confirma con =--yes=. Al core se le sigue la pista por sus
tags semver (=vX.Y.Z=, empezando por =v0.1.0=); si no hay tags, la versión es tags semver (=vX.Y.Z=, empezando por =v0.1.0=); si no hay tags, la versión es
desconocida y se fija por commit. desconocida y se fija por commit.
Los valores =tested= de los manifiestos no son decorativos: los escribe sólo
quien corrió el *barrido de compatibilidad* (=make compat=), que monta un
proyecto temporal por paquete contra el core real publicado y lo ejercita por
HTTP (migraciones sobre sqlite, runner phpunit real; los clientes con servicio
externo — =aria2=, =proxy=, =compreFace= — sólo a nivel de carga/instantiación,
consignado así en =tests/compat/report.json=). Cualquier
=Deprecated=/=Warning=/=Notice= durante el barrido cuenta como fallo.
Un test de la suite (=group11=) exige que cada =tested= declarado tenga su =ok=
en ese reporte y que no se estampen componentes deliberadamente sin datos
(=commands:cli=/=commands:toolbox=, que son el instalador y no consumen el
core). Al aparecer un tag nuevo del core, el ritual es: =make compat= → arreglar
lo que falle → estampar → =make release=.
* Versionado y releases * Versionado y releases
Hay *dos* sistemas de versión y no hay que confundirlos: Hay *dos* sistemas de versión y no hay que confundirlos:
@@ -265,14 +278,15 @@ Hay *dos* sistemas de versión y no hay que confundirlos:
=make help= lista los objetivos: =make help= lista los objetivos:
| Objetivo | Qué hace | | Objetivo | Qué hace |
|---------------+------------------------------------------------------------| |--------------+-------------------------------------------------------------------------------------------|
| =make test= | Corre la suite (=php tests/run.php=). En verde antes de commitear. | | =make test= | Corre la suite (=php tests/run.php=). En verde antes de commitear. |
| =make build= | Genera =dist/duckbrain.phar= + =dist/duckbrain.phar.sha256=. | | =make build= | Genera =dist/duckbrain.phar= + =dist/duckbrain.phar.sha256=. |
| =make release= | Asistente interactivo de releases de paquetes (=ARGS="pkg minor --yes"= para modo directo). | | =make compat= | Barrido de compatibilidad contra el core real (=scripts/compat-sweep=); requiere red. =ARGS="--only pkg"= para un paquete. |
| =make publish= | =build= + publica la release =v$(Config::VERSION)= con los assets. Requiere =FORGEJO_TOKEN=. | | =make release= | Asistente interactivo de releases de paquetes (=ARGS="pkg minor --yes"= para modo directo). |
| =make check= | =self-update --check= contra el servidor: =actual/disponible=, exit 1 si hay nuevo. | | =make publish= | =build= + publica la release =v$(Config::VERSION)= con los assets. Requiere =FORGEJO_TOKEN=. |
| =make clean= | Borra =dist/=. | | =make check= | =self-update --check= contra el servidor: =actual/disponible=, exit 1 si hay nuevo. |
| =make clean= | Borra =dist/=. |
El token para publicar es un token personal de la API de Forgejo El token para publicar es un token personal de la API de Forgejo
(*Ajustes → Aplicaciones*, scope =write:repository=, con caducidad y revocable). (*Ajustes → Aplicaciones*, scope =write:repository=, con caducidad y revocable).
+4 -2
View File
@@ -28,7 +28,8 @@ if ($command === null || in_array($command, $toolboxCommands, true)) {
$coreCommand = 'Commands\\' . ucfirst($command); $coreCommand = 'Commands\\' . ucfirst($command);
if (!class_exists($coreCommand) && !file_exists(ROOT_CORE . '/Commands/' . ucfirst($command) . '.php')) { if (!class_exists($coreCommand) && !file_exists(ROOT_CORE . '/Commands/' . ucfirst($command) . '.php')) {
exit("Invalid command.\n"); fwrite(STDERR, "Invalid command.\n");
exit(1);
} }
$cli = new $coreCommand(); $cli = new $coreCommand();
@@ -36,5 +37,6 @@ $cli = new $coreCommand();
if ($cli instanceof Interfaces\CommandInterface) { if ($cli instanceof Interfaces\CommandInterface) {
call_user_func_array([$cli, 'run'], array_slice($argv, 2)); call_user_func_array([$cli, 'run'], array_slice($argv, 2));
} else { } else {
exit("Invalid command.\n"); fwrite(STDERR, "La clase $coreCommand no implementa CommandInterface.\n");
exit(1);
} }
+32 -28
View File
@@ -1,31 +1,35 @@
{ {
"name": "commands", "name": "commands",
"version": "0.2.0", "version": "0.2.0",
"description": "CLI y migraciones para Duckbrain", "description": "CLI y migraciones para Duckbrain",
"components": { "components": {
"cli": { "cli": {
"files": [ "files": [
"duckbrain", "duckbrain",
"src/Interfaces/CommandInterface.php" "src/Interfaces/CommandInterface.php"
] ]
},
"migration": {
"files": [
"src/Commands/Migration.php",
"src/Libs/Migration.php"
],
"require": {
"commands:cli": "*"
}
},
"toolbox": {
"files": [
"src/Toolbox"
]
}
}, },
"default": [ "migration": {
"cli", "files": [
"migration" "src/Commands/Migration.php",
] "src/Libs/Migration.php"
],
"require": {
"commands:cli": "*"
},
"core": {
"requires": ">=0.1",
"tested": "0.1"
}
},
"toolbox": {
"files": [
"src/Toolbox"
]
}
},
"default": [
"cli",
"migration"
]
} }
@@ -36,7 +36,12 @@ final class CoreProvider
throw new ToolboxException('No se pudo clonar el core: ' . implode("\n", $out)); throw new ToolboxException('No se pudo clonar el core: ' . implode("\n", $out));
} }
} else { } else {
exec('git -C ' . escapeshellarg($dir) . ' fetch -q --tags 2>&1'); exec('git -C ' . escapeshellarg($dir) . ' remote set-url origin ' . escapeshellarg($url) . ' 2>&1');
$out = [];
exec('git -C ' . escapeshellarg($dir) . ' fetch -q --tags 2>&1', $out, $code);
if ($code !== 0) {
throw new ToolboxException('No se pudo actualizar el core: ' . implode("\n", $out));
}
} }
$provider = new self($dir); $provider = new self($dir);
+2 -1
View File
@@ -191,7 +191,8 @@ final class Installer
$manifest->postInstall, $manifest->postInstall,
$this->console, $this->console,
(bool) ($options['yes'] ?? false), (bool) ($options['yes'] ?? false),
$options['postRunner'] ?? null $options['postRunner'] ?? null,
$this->root,
); );
} }
} }
+31 -15
View File
@@ -11,35 +11,51 @@ final class PostInstall
/** /**
* @param list<string> $commands * @param list<string> $commands
* @param callable(string):int|null $runner * @param callable(string):int|null $runner
* @param string|null $cwd Directorio de trabajo del proyecto: los
* comandos se ejecutan alli, no donde arranco
* el instalador.
* @return list<string> comandos ejecutados * @return list<string> comandos ejecutados
*/ */
public static function run( public static function run(
array $commands, array $commands,
Console $console, Console $console,
bool $assumeYes = false, bool $assumeYes = false,
?callable $runner = null ?callable $runner = null,
?string $cwd = null
): array { ): array {
$executed = []; $executed = [];
$previous = null;
foreach ($commands as $command) { if ($cwd !== null && is_dir($cwd)) {
$console->out(' post-install: ' . $command); $previous = getcwd();
chdir($cwd);
}
if (!$console->confirm(' ¿Ejecutar?', $assumeYes)) { try {
$console->warn('post-install omitido: ' . $command); foreach ($commands as $command) {
$console->out(' post-install: ' . $command);
continue; if (!$console->confirm(' ¿Ejecutar?', $assumeYes)) {
} $console->warn('post-install omitido: ' . $command);
if ($runner !== null) { continue;
$runner($command);
} else {
passthru($command, $code);
if (($code ?? 0) !== 0) {
throw new ToolboxException("post-install falló: {$command}");
} }
}
$executed[] = $command; if ($runner !== null) {
$runner($command);
} else {
passthru($command, $code);
if (($code ?? 0) !== 0) {
throw new ToolboxException("post-install falló: {$command}");
}
}
$executed[] = $command;
}
} finally {
if ($previous !== null) {
chdir($previous);
}
} }
return $executed; return $executed;