|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +namespace Chorale\Console; |
| 6 | + |
| 7 | +use Chorale\Console\Style\ConsoleStyleFactory; |
| 8 | +use Chorale\Run\RunnerInterface; |
| 9 | +use Chorale\Plan\PlanStepInterface; |
| 10 | +use Symfony\Component\Console\Command\Command; |
| 11 | +use Symfony\Component\Console\Input\InputInterface; |
| 12 | +use Symfony\Component\Console\Input\InputOption; |
| 13 | +use Symfony\Component\Console\Output\OutputInterface; |
| 14 | +use Symfony\Component\Console\Style\SymfonyStyle; |
| 15 | + |
| 16 | +final class RunCommand extends Command |
| 17 | +{ |
| 18 | + protected static $defaultName = 'run'; |
| 19 | + protected static $defaultDescription = 'Plan and apply steps.'; |
| 20 | + |
| 21 | + public function __construct( |
| 22 | + private readonly ConsoleStyleFactory $styleFactory, |
| 23 | + private readonly RunnerInterface $runner, |
| 24 | + ) { |
| 25 | + parent::__construct(); |
| 26 | + } |
| 27 | + |
| 28 | + protected function configure(): void |
| 29 | + { |
| 30 | + $this |
| 31 | + ->setName('run') |
| 32 | + ->setDescription('Plan and immediately apply steps.') |
| 33 | + ->setHelp(<<<'HELP' |
| 34 | +Builds a plan for the repository and applies it in a single command. |
| 35 | +This is equivalent to running `chorale plan` followed by `chorale apply`. |
| 36 | +
|
| 37 | +Examples: |
| 38 | + chorale run |
| 39 | + chorale run --paths packages/acme --strict |
| 40 | +HELP) |
| 41 | + ->addOption('project-root', null, InputOption::VALUE_REQUIRED, 'Project root (default: CWD).') |
| 42 | + ->addOption('paths', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Limit to specific package paths', []) |
| 43 | + ->addOption('force-split', null, InputOption::VALUE_NONE, 'Force split steps even if unchanged.') |
| 44 | + ->addOption('verify-remote', null, InputOption::VALUE_NONE, 'Verify remote state if lockfile is missing/stale.') |
| 45 | + ->addOption('strict', null, InputOption::VALUE_NONE, 'Fail on missing root version / unresolved conflicts / remote failures.') |
| 46 | + ->addOption('show-all', null, InputOption::VALUE_NONE, 'Show no-op summaries.'); |
| 47 | + } |
| 48 | + |
| 49 | + protected function execute(InputInterface $input, OutputInterface $output): int |
| 50 | + { |
| 51 | + $io = $this->styleFactory->create($input, $output); |
| 52 | + $root = rtrim((string) ($input->getOption('project-root') ?: getcwd()), '/'); |
| 53 | + /** @var list<string> $paths */ |
| 54 | + $paths = (array) $input->getOption('paths'); |
| 55 | + $force = (bool) $input->getOption('force-split'); |
| 56 | + $verify = (bool) $input->getOption('verify-remote'); |
| 57 | + $strict = (bool) $input->getOption('strict'); |
| 58 | + $showAll = (bool) $input->getOption('show-all'); |
| 59 | + |
| 60 | + try { |
| 61 | + $result = $this->runner->run($root, [ |
| 62 | + 'paths' => $paths, |
| 63 | + 'force_split' => $force, |
| 64 | + 'verify_remote' => $verify, |
| 65 | + 'strict' => $strict, |
| 66 | + 'show_all' => $showAll, |
| 67 | + ]); |
| 68 | + } catch (\RuntimeException $e) { |
| 69 | + $io->error($e->getMessage()); |
| 70 | + return 2; |
| 71 | + } |
| 72 | + |
| 73 | + $this->renderHuman($io, $result['steps'], $showAll ? ($result['noop'] ?? []) : []); |
| 74 | + $io->success(sprintf('Applied %d step(s).', count($result['steps']))); |
| 75 | + return (int) ($result['exit_code'] ?? 0); |
| 76 | + } |
| 77 | + |
| 78 | + /** @param list<PlanStepInterface> $steps */ |
| 79 | + private function renderHuman(SymfonyStyle $io, array $steps, array $noop): void |
| 80 | + { |
| 81 | + $io->title('Chorale Run'); |
| 82 | + $byType = []; |
| 83 | + foreach ($steps as $s) { |
| 84 | + $byType[$s->type()][] = $s; |
| 85 | + } |
| 86 | + |
| 87 | + $sections = [ |
| 88 | + 'split' => 'Split steps', |
| 89 | + 'package-version-update' => 'Package versions', |
| 90 | + 'package-metadata-sync' => 'Package metadata', |
| 91 | + 'composer-root-update' => 'Root composer: aggregator', |
| 92 | + 'composer-root-merge' => 'Root composer: dependency merge', |
| 93 | + 'composer-root-rebuild' => 'Root composer: maintenance', |
| 94 | + ]; |
| 95 | + |
| 96 | + $any = false; |
| 97 | + foreach ($sections as $type => $label) { |
| 98 | + if (empty($byType[$type])) { |
| 99 | + continue; |
| 100 | + } |
| 101 | + |
| 102 | + $any = true; |
| 103 | + $io->section($label); |
| 104 | + foreach ($byType[$type] as $s) { |
| 105 | + $a = $s->toArray(); |
| 106 | + $io->writeln(' • ' . $this->humanLine($type, $a)); |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + if (!$any) { |
| 111 | + $io->writeln('No steps. Nothing to do.'); |
| 112 | + } |
| 113 | + |
| 114 | + if ($noop !== []) { |
| 115 | + $io->newLine(); |
| 116 | + $io->section('No-op summary (debug)'); |
| 117 | + foreach ($noop as $group => $rows) { |
| 118 | + $io->writeln(sprintf(' - %s: ', $group) . count($rows)); |
| 119 | + } |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + /** @param array<string,mixed> $a */ |
| 124 | + private function humanLine(string $type, array $a): string |
| 125 | + { |
| 126 | + return match ($type) { |
| 127 | + 'split' => sprintf( |
| 128 | + '%s → %s [%s]%s', |
| 129 | + $a['path'] ?? '', |
| 130 | + $a['repo'] ?? '', |
| 131 | + $a['splitter'] ?? '', |
| 132 | + empty($a['reasons']) ? '' : ' {' . implode(',', (array) $a['reasons']) . '}' |
| 133 | + ), |
| 134 | + 'package-version-update' => sprintf('%s — set version %s', $a['name'] ?? $a['path'] ?? '', $a['version'] ?? ''), |
| 135 | + 'package-metadata-sync' => sprintf( |
| 136 | + '%s — mirror %s', |
| 137 | + $a['name'] ?? $a['path'] ?? '', |
| 138 | + implode(',', array_keys((array) ($a['apply'] ?? []))) |
| 139 | + ), |
| 140 | + 'composer-root-update' => sprintf( |
| 141 | + 'update %s (version %s, require %d, replace %d)', |
| 142 | + $a['root'] ?? '', |
| 143 | + $a['root_version'] ?? 'n/a', |
| 144 | + isset($a['require']) ? count((array) $a['require']) : 0, |
| 145 | + isset($a['replace']) ? count((array) $a['replace']) : 0 |
| 146 | + ), |
| 147 | + 'composer-root-merge' => sprintf( |
| 148 | + 'require %d, require-dev %d%s', |
| 149 | + isset($a['require']) ? count((array) $a['require']) : 0, |
| 150 | + isset($a['require-dev']) ? count((array) $a['require-dev']) : 0, |
| 151 | + empty($a['conflicts']) ? '' : ' [conflicts: ' . count((array) $a['conflicts']) . ']' |
| 152 | + ), |
| 153 | + 'composer-root-rebuild' => sprintf('actions: %s', implode(',', (array) ($a['actions'] ?? []))), |
| 154 | + default => json_encode($a, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: $type, |
| 155 | + }; |
| 156 | + } |
| 157 | +} |
0 commit comments