Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
SkillsCommand
100.00% covered (success)
100.00%
44 / 44
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%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
38 / 38
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 FastForward\DevTools\Console\Input\HasJsonOption;
24use FastForward\DevTools\Filesystem\FilesystemInterface;
25use FastForward\DevTools\Path\DevToolsPathResolver;
26use FastForward\DevTools\Sync\PackagedDirectorySynchronizer;
27use Psr\Log\LoggerInterface;
28use Symfony\Component\Console\Attribute\AsCommand;
29use Symfony\Component\Console\Command\Command;
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: 'agents:skills',
49    description: 'Synchronizes Fast Forward skills into .agents/skills directory.',
50    aliases: ['skills']
51)]
52 class SkillsCommand extends Command
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->setHelp(
84            'This command ensures the consumer repository contains linked Fast Forward skills by creating'
85            . ' symlinks to the packaged skills and removing broken links.'
86        );
87        $this->addJsonOption();
88    }
89
90    /**
91     * Executes the skills synchronization workflow.
92     *
93     * This method SHALL:
94     * - announce the start of synchronization;
95     * - resolve the packaged skills path and consumer target directory;
96     * - fail when the packaged skills directory does not exist;
97     * - create the target directory when it is missing;
98     * - delegate synchronization to {@see PackagedDirectorySynchronizer};
99     * - return a success or failure exit code based on the synchronization result.
100     *
101     * The command MUST return {@see self::FAILURE} when packaged skills are not
102     * available or when the synchronizer reports a failure. It MUST return
103     * {@see self::SUCCESS} only when synchronization completes successfully.
104     *
105     * @param InputInterface $input the console input instance provided by Symfony
106     * @param OutputInterface $output the console output instance used to report progress
107     *
108     * @return int The process exit status. This MUST be {@see self::SUCCESS} on
109     *             success and {@see self::FAILURE} on failure.
110     */
111    protected function execute(InputInterface $input, OutputInterface $output): int
112    {
113        $packageSkillsPath = DevToolsPathResolver::getPackagePath(self::SKILLS_DIRECTORY);
114        $skillsDir = $this->filesystem->getAbsolutePath(self::SKILLS_DIRECTORY);
115        $this->logger->info('Starting skills synchronization...');
116
117        if (! $this->filesystem->exists($packageSkillsPath)) {
118            return $this->failure(
119                'No packaged skills found at: {packaged_skills_path}',
120                $input,
121                [
122                    'packaged_skills_path' => $packageSkillsPath,
123                    'skills_dir' => $skillsDir,
124                    'directory_created' => false,
125                ],
126            );
127        }
128
129        $directoryCreated = false;
130
131        if (! $this->filesystem->exists($skillsDir)) {
132            $this->filesystem->mkdir($skillsDir);
133            $directoryCreated = true;
134            $this->logger->info('Created .agents/skills directory.');
135        }
136
137        $result = $this->synchronizer->synchronize($skillsDir, $packageSkillsPath, self::SKILLS_DIRECTORY);
138
139        if ($result->failed()) {
140            return $this->failure(
141                'Skills synchronization failed.',
142                $input,
143                [
144                    'packaged_skills_path' => $packageSkillsPath,
145                    'skills_dir' => $skillsDir,
146                    'directory_created' => $directoryCreated,
147                ],
148            );
149        }
150
151        return $this->success(
152            'Skills synchronization completed successfully.',
153            $input,
154            [
155                'packaged_skills_path' => $packageSkillsPath,
156                'skills_dir' => $skillsDir,
157                'directory_created' => $directoryCreated,
158            ],
159        );
160    }
161}