Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
SkillsCommand
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
3 / 3
6
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 configure
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5/**
6 * Fast Forward Development Tools for PHP projects.
7 *
8 * This file is part of fast-forward/dev-tools project.
9 *
10 * @author   Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
11 * @license  https://opensource.org/licenses/MIT MIT License
12 *
13 * @see      https://github.com/php-fast-forward/
14 * @see      https://github.com/php-fast-forward/dev-tools
15 * @see      https://github.com/php-fast-forward/dev-tools/issues
16 * @see      https://php-fast-forward.github.io/dev-tools/
17 * @see      https://datatracker.ietf.org/doc/html/rfc2119
18 */
19
20namespace FastForward\DevTools\Console\Command;
21
22use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
23use Composer\Command\BaseCommand;
24use FastForward\DevTools\Console\Input\HasJsonOption;
25use FastForward\DevTools\Filesystem\FilesystemInterface;
26use FastForward\DevTools\Path\DevToolsPathResolver;
27use FastForward\DevTools\Sync\PackagedDirectorySynchronizer;
28use Psr\Log\LoggerInterface;
29use Symfony\Component\Console\Attribute\AsCommand;
30use Symfony\Component\Console\Input\InputInterface;
31use Symfony\Component\Console\Output\OutputInterface;
32
33/**
34 * Synchronizes packaged Fast Forward skills into the consumer repository.
35 *
36 * This command SHALL ensure that the consumer repository contains the expected
37 * `.agents/skills` directory structure backed by the packaged skill set. The
38 * command MUST verify that the packaged skills directory exists before any
39 * synchronization is attempted. If the target skills directory does not exist,
40 * it SHALL be created before the synchronization process begins.
41 *
42 * The synchronization workflow is delegated to {@see PackagedDirectorySynchronizer}. This
43 * command MUST act as an orchestration layer only: it prepares the source and
44 * target paths, triggers synchronization, and translates the resulting status
45 * into Symfony Console output and process exit codes.
46 */
47#[AsCommand(
48    name: 'skills',
49    description: 'Synchronizes Fast Forward skills into .agents/skills directory.',
50    help: 'This command ensures the consumer repository contains linked Fast Forward skills by creating symlinks to the packaged skills and removing broken links.'
51)]
52 class SkillsCommand extends BaseCommand implements LoggerAwareCommandInterface
53{
54    use HasJsonOption;
55    use LogsCommandResults;
56
57    private const string SKILLS_DIRECTORY = '.agents/skills';
58
59    /**
60     * Initializes the command with an optional skills synchronizer instance.
61     *
62     * @param PackagedDirectorySynchronizer $synchronizer the synchronizer responsible
63     *                                                    for applying the skills
64     *                                                    synchronization process
65     * @param FilesystemInterface $filesystem filesystem used to resolve
66     *                                        and manage the skills
67     *                                        directory structure
68     * @param LoggerInterface $logger logger used for command feedback
69     */
70    public function __construct(
71        private  PackagedDirectorySynchronizer $synchronizer,
72        private  FilesystemInterface $filesystem,
73        private  LoggerInterface $logger,
74    ) {
75        parent::__construct();
76    }
77
78    /**
79     * Configures the supported JSON output option.
80     */
81    protected function configure(): void
82    {
83        $this->addJsonOption();
84    }
85
86    /**
87     * Executes the skills synchronization workflow.
88     *
89     * This method SHALL:
90     * - announce the start of synchronization;
91     * - resolve the packaged skills path and consumer target directory;
92     * - fail when the packaged skills directory does not exist;
93     * - create the target directory when it is missing;
94     * - delegate synchronization to {@see PackagedDirectorySynchronizer};
95     * - return a success or failure exit code based on the synchronization result.
96     *
97     * The command MUST return {@see self::FAILURE} when packaged skills are not
98     * available or when the synchronizer reports a failure. It MUST return
99     * {@see self::SUCCESS} only when synchronization completes successfully.
100     *
101     * @param InputInterface $input the console input instance provided by Symfony
102     * @param OutputInterface $output the console output instance used to report progress
103     *
104     * @return int The process exit status. This MUST be {@see self::SUCCESS} on
105     *             success and {@see self::FAILURE} on failure.
106     */
107    protected function execute(InputInterface $input, OutputInterface $output): int
108    {
109        $packageSkillsPath = DevToolsPathResolver::getPackagePath(self::SKILLS_DIRECTORY);
110        $skillsDir = $this->filesystem->getAbsolutePath(self::SKILLS_DIRECTORY);
111        $this->logger->info('Starting skills synchronization...');
112
113        if (! $this->filesystem->exists($packageSkillsPath)) {
114            return $this->failure(
115                'No packaged skills found at: {packaged_skills_path}',
116                $input,
117                [
118                    'packaged_skills_path' => $packageSkillsPath,
119                    'skills_dir' => $skillsDir,
120                    'directory_created' => false,
121                ],
122            );
123        }
124
125        $directoryCreated = false;
126
127        if (! $this->filesystem->exists($skillsDir)) {
128            $this->filesystem->mkdir($skillsDir);
129            $directoryCreated = true;
130            $this->logger->info('Created .agents/skills directory.');
131        }
132
133        $this->synchronizer->setLogger($this->getIO());
134
135        $result = $this->synchronizer->synchronize($skillsDir, $packageSkillsPath, self::SKILLS_DIRECTORY);
136
137        if ($result->failed()) {
138            return $this->failure(
139                'Skills synchronization failed.',
140                $input,
141                [
142                    'packaged_skills_path' => $packageSkillsPath,
143                    'skills_dir' => $skillsDir,
144                    'directory_created' => $directoryCreated,
145                ],
146            );
147        }
148
149        return $this->success(
150            'Skills synchronization completed successfully.',
151            $input,
152            [
153                'packaged_skills_path' => $packageSkillsPath,
154                'skills_dir' => $skillsDir,
155                'directory_created' => $directoryCreated,
156            ],
157        );
158    }
159}