1616use Chorale \Discovery \PatternMatcherInterface ;
1717use Chorale \IO \JsonReporterInterface ;
1818use Chorale \Repo \RepoResolverInterface ;
19- use Chorale \Rules \ConflictDetectorInterface ;
2019use Chorale \Rules \RequiredFilesCheckerInterface ;
2120use Chorale \Telemetry \RunSummaryInterface ;
22- use Symfony \Component \Console \Attribute \AsCommand ;
2321use Symfony \Component \Console \Command \Command ;
2422use Symfony \Component \Console \Input \InputInterface ;
2523use Symfony \Component \Console \Input \InputOption ;
24+ use Symfony \Component \Console \Output \OutputInterface ;
2625use Symfony \Component \Console \Question \ConfirmationQuestion ;
2726use Symfony \Component \Console \Style \SymfonyStyle ;
2827
2928//#[AsCommand(name: 'setup')]
3029final class SetupCommand extends Command
3130{
3231 protected static $ defaultName = 'setup ' ;
32+
3333 protected static $ defaultDescription = 'Create or update chorale.yaml by scanning src/ and applying defaults. ' ;
3434
3535 public function __construct (
@@ -44,7 +44,6 @@ public function __construct(
4444 private readonly RepoResolverInterface $ resolver ,
4545 private readonly PackageIdentityInterface $ identity ,
4646 private readonly RequiredFilesCheckerInterface $ requiredFiles ,
47- private readonly ConflictDetectorInterface $ conflicts ,
4847 private readonly JsonReporterInterface $ jsonReporter ,
4948 private readonly RunSummaryInterface $ summary ,
5049 private readonly ComposerMetadataInterface $ composerMeta ,
@@ -70,14 +69,14 @@ protected function configure(): void
7069 // ─────────────────────────────────────────────────────────────────────────────
7170 // Orchestrator
7271 // ─────────────────────────────────────────────────────────────────────────────
73- protected function execute (InputInterface $ input , \ Symfony \ Component \ Console \ Output \ OutputInterface $ output ): int
72+ protected function execute (InputInterface $ input , OutputInterface $ output ): int
7473 {
7574 $ io = $ this ->styleFactory ->create ($ input , $ output );
7675 $ opts = $ this ->gatherOptions ($ input );
7776
7877 [$ config , $ firstRun ] = $ this ->loadOrSeedConfig ($ opts ['root ' ]);
7978
80- if ($ msgs = $ this ->validateSchema ($ config, $ opts [ ' strict ' ]) ) {
79+ if (( $ msgs = $ this ->validateSchema ($ config)) !== [] ) {
8180 $ this ->printIssues ($ io , $ msgs );
8281 if ($ opts ['strict ' ]) {
8382 $ io ->error ('Strict mode: schema validation failed. ' );
@@ -101,6 +100,7 @@ protected function execute(InputInterface $input, \Symfony\Component\Console\Out
101100 $ found = $ this ->scanner ->scan ($ opts ['root ' ], $ r , $ opts ['paths ' ]);
102101 $ discPaths = array_merge ($ discPaths , $ found );
103102 }
103+
104104 $ discPaths = array_values (array_unique ($ discPaths ));
105105 sort ($ discPaths );
106106
@@ -155,6 +155,7 @@ protected function execute(InputInterface $input, \Symfony\Component\Console\Out
155155 $ tot ['issues ' ] ?? 0 ,
156156 $ tot ['conflicts ' ] ?? 0
157157 ));
158+ $ io ->note ('You can run this command as many times as you want. Run this after you create a new package. ' );
158159 return 0 ;
159160 }
160161
@@ -227,18 +228,12 @@ private function loadOrSeedConfig(string $root): array
227228 }
228229
229230 /** @return list<string> */
230- private function validateSchema (array $ config, bool $ strict ): array
231+ private function validateSchema (array $ config ): array
231232 {
232233 // Keeping this simple (we already do type checks in SchemaValidator)
233234 return $ this ->schemaValidator ->validate ($ config , 'tools/chorale/config/chorale.schema.yaml ' );
234235 }
235236
236- /** @return list<string> */
237- private function discoverPaths (string $ root , array $ paths ): array
238- {
239- return $ this ->scanner ->scan ($ root , $ paths );
240- }
241-
242237 private function printIssues (SymfonyStyle $ io , array $ messages ): void
243238 {
244239 foreach ($ messages as $ m ) {
@@ -251,6 +246,7 @@ private function confirmWrite(SymfonyStyle $io, array $opts): bool
251246 if ($ opts ['write ' ] || $ opts ['acceptAll ' ] || $ opts ['nonInteractive ' ]) {
252247 return true ;
253248 }
249+
254250 $ helper = $ this ->getHelper ('question ' );
255251 $ confirm = new ConfirmationQuestion ('Proceed? [Y/n] ' , true );
256252
@@ -260,20 +256,22 @@ private function confirmWrite(SymfonyStyle $io, array $opts): bool
260256 /** @return list<string> e.g. ["src","packages"] */
261257 private function determineRoots (array $ patterns , string $ root ): array
262258 {
263- if ($ patterns ) {
259+ if ($ patterns !== [] ) {
264260 $ roots = [];
265261 foreach ($ patterns as $ p ) {
266262 $ m = (string ) ($ p ['match ' ] ?? '' );
267263 if ($ m === '' ) {
268264 continue ;
269265 }
266+
270267 // root is first segment before slash
271268 $ seg = explode ('/ ' , ltrim ($ m , '/ ' ), 2 )[0 ] ?? '' ;
272269 if ($ seg !== '' && !in_array ($ seg , $ roots , true )) {
273270 $ roots [] = $ seg ;
274271 }
275272 }
276- return $ roots ?: ['src ' ]; // safe fallback if patterns are odd
273+
274+ return $ roots !== [] ? $ roots : ['src ' ]; // safe fallback if patterns are odd
277275 }
278276
279277 // First run: probe both src and packages
@@ -284,6 +282,7 @@ private function determineRoots(array $patterns, string $root): array
284282 $ roots [] = $ cand ;
285283 }
286284 }
285+
287286 return $ roots ;
288287 }
289288
@@ -297,6 +296,7 @@ private function displayNameFor(string $projectRoot, string $pkgPath): string
297296 $ last = str_contains ($ name , '/ ' ) ? substr ($ name , strrpos ($ name , '/ ' ) + 1 ) : $ name ;
298297 return $ last ;
299298 }
299+
300300 return basename ($ pkgPath );
301301 }
302302
@@ -330,7 +330,7 @@ private function classifyAll(string $root, array $defaults, array $patterns, arr
330330 foreach ($ byPath as $ oldPath => $ target ) {
331331 if (!in_array ($ oldPath , $ discovered , true )) {
332332 // If target points to a path that no longer exists but a new path with same identity does, propose rename
333- $ maybe = $ this ->findRenameTarget ($ root , $ oldPath , $ target , $ defaults , $ patterns , $ discovered );
333+ $ maybe = $ this ->findRenameTarget ($ oldPath , $ target , $ defaults , $ patterns , $ discovered );
334334 if ($ maybe !== null ) {
335335 $ groups ['renamed ' ][] = $ maybe ;
336336 }
@@ -370,11 +370,13 @@ private function classifyOne(string $root, string $pkgPath, array $defaults, arr
370370 // Truly untracked: no pattern covers it
371371 return ['group ' => 'new ' , 'data ' => ['path ' => $ pkgPath , 'repo ' => $ repo , 'package ' => $ pkgName , 'reason ' => 'no-pattern ' ]];
372372 }
373+
373374 // Covered by pattern → OK
374375 $ ok = ['path ' => $ pkgPath , 'repo ' => $ repo , 'covered_by_pattern ' => true , 'package ' => $ pkgName ];
375- if ($ conflictData ) {
376+ if ($ conflictData !== null && $ conflictData !== [] ) {
376377 $ ok ['conflict ' ] = $ conflictData ['patterns ' ];
377378 }
379+
378380 return ['group ' => 'ok ' , 'data ' => $ ok ];
379381 }
380382
@@ -399,9 +401,10 @@ private function classifyOne(string $root, string $pkgPath, array $defaults, arr
399401
400402
401403 $ ok = ['path ' => $ pkgPath , 'repo ' => $ repo , 'package ' => $ pkgName ];
402- if ($ conflictData ) {
404+ if ($ conflictData !== null && $ conflictData !== [] ) {
403405 $ ok ['conflict ' ] = $ conflictData ['patterns ' ];
404406 }
407+
405408 return ['group ' => 'ok ' , 'data ' => $ ok ];
406409 }
407410
@@ -413,7 +416,7 @@ private function classifyOne(string $root, string $pkgPath, array $defaults, arr
413416 * @param list<string> $discovered
414417 * @return array<string,mixed>|null
415418 */
416- private function findRenameTarget (string $ root , string $ oldPath , array $ target , array $ defaults , array $ patterns , array $ discovered ): ?array
419+ private function findRenameTarget (string $ oldPath , array $ target , array $ defaults , array $ patterns , array $ discovered ): ?array
417420 {
418421 $ oldRepo = $ this ->resolver ->resolve ($ defaults , $ this ->firstPatternFor ($ patterns , $ oldPath ), $ target , $ oldPath , basename ($ oldPath ));
419422 $ oldId = $ this ->identity ->identityFor ($ oldPath , $ oldRepo );
@@ -430,6 +433,7 @@ private function findRenameTarget(string $root, string $oldPath, array $target,
430433 ];
431434 }
432435 }
436+
433437 return null ;
434438 }
435439
@@ -464,7 +468,7 @@ private function buildActions(array $groups, array $existingTargets): array
464468
465469 foreach ($ groups ['new ' ] as $ row ) {
466470 // New only occurs when no pattern covers it → we must add a target (or new pattern, future)
467- $ actions [] = ['type ' => 'add-target ' , 'path ' => $ row ['path ' ], 'name ' => basename ($ row ['path ' ])];
471+ $ actions [] = ['type ' => 'add-target ' , 'path ' => $ row ['path ' ], 'name ' => basename (( string ) $ row ['path ' ])];
468472 }
469473
470474 foreach ($ groups ['renamed ' ] as $ row ) {
@@ -512,6 +516,7 @@ private function applyActions(array $config, array $actions): array
512516 break ;
513517 }
514518 }
519+
515520 unset($ t );
516521 }
517522 }
@@ -533,29 +538,32 @@ private function renderHumanReport(SymfonyStyle $io, array $groups): void
533538 $ io ->section ('Auto-discovery (src/) ' );
534539
535540 $ this ->printGroup ($ io , 'OK ' , $ groups ['ok ' ], function (array $ r ): string {
536- $ suffix = ! empty ($ r ['covered_by_pattern ' ]) ? ' (pattern) ' : '' ;
541+ $ suffix = empty ($ r ['covered_by_pattern ' ]) ? '' : ' (pattern) ' ;
537542 if (!empty ($ r ['conflict ' ])) {
538543 $ suffix .= sprintf (' (conflict: patterns %s) ' , implode (', ' , (array ) $ r ['conflict ' ]));
539544 }
545+
540546 return sprintf ('%s%s ' , $ r ['package ' ], $ suffix );
541547 });
542548
543- $ this ->printGroup ($ io , 'NEW ' , $ groups ['new ' ], fn (array $ r ) => sprintf ('%s → %s (no matching pattern) ' , $ r ['path ' ], $ r ['repo ' ]));
544- $ this ->printGroup ($ io , 'RENAMED ' , $ groups ['renamed ' ], fn (array $ r ) => sprintf ('%s → %s ' , $ r ['from ' ], $ r ['to ' ]));
549+ $ this ->printGroup ($ io , 'NEW ' , $ groups ['new ' ], fn (array $ r ): string => sprintf ('%s → %s (no matching pattern) ' , $ r ['path ' ], $ r ['repo ' ]));
550+ $ this ->printGroup ($ io , 'RENAMED ' , $ groups ['renamed ' ], fn (array $ r ): string => sprintf ('%s → %s ' , $ r ['from ' ], $ r ['to ' ]));
545551 $ this ->printGroup ($ io , 'DRIFT ' , $ groups ['drift ' ], fn (array $ r ) => $ r ['path ' ]);
546- $ this ->printGroup ($ io , 'ISSUES ' , $ groups ['issues ' ], fn (array $ r ) => sprintf ('%s (missing: %s) ' , $ r ['path ' ], implode (', ' , (array ) ($ r ['missing ' ] ?? []))));
547- $ this ->printGroup ($ io , 'CONFLICTS ' , $ groups ['conflicts ' ], fn (array $ r ) => sprintf ('%s (patterns: %s) ' , $ r ['path ' ], implode (', ' , (array ) ($ r ['patterns ' ] ?? []))));
552+ $ this ->printGroup ($ io , 'ISSUES ' , $ groups ['issues ' ], fn (array $ r ): string => sprintf ('%s (missing: %s) ' , $ r ['path ' ], implode (', ' , (array ) ($ r ['missing ' ] ?? []))));
553+ $ this ->printGroup ($ io , 'CONFLICTS ' , $ groups ['conflicts ' ], fn (array $ r ): string => sprintf ('%s (patterns: %s) ' , $ r ['path ' ], implode (', ' , (array ) ($ r ['patterns ' ] ?? []))));
548554 }
549555
550556 private function printGroup (SymfonyStyle $ io , string $ title , array $ rows , callable $ fmt ): void
551557 {
552558 if ($ rows === []) {
553559 return ;
554560 }
555- $ io ->writeln ("<info> {$ title }</info> " );
561+
562+ $ io ->writeln (sprintf ('<info>%s</info> ' , $ title ));
556563 foreach ($ rows as $ r ) {
557564 $ io ->writeln (' • ' . $ fmt ($ r ));
558565 }
566+
559567 $ io ->newLine ();
560568 }
561569}
0 commit comments