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 Symfony\Component\Console\Attribute\AsCommand;
28use Symfony\Component\Console\Command\Command;
29use Symfony\Component\Console\Input\InputInterface;
30use Symfony\Component\Console\Output\OutputInterface;
31
32/**
33 * Synchronizes packaged Fast Forward skills into the consumer repository.
34 *
35 * This command SHALL ensure that the consumer repository contains the expected
36 * `.agents/skills` directory structure backed by the packaged skill set. The
37 * command MUST verify that the packaged skills directory exists before any
38 * synchronization is attempted. If the target skills directory does not exist,
39 * it SHALL be created before the synchronization process begins.
40 *
41 * The synchronization workflow is delegated to {@see PackagedDirectorySynchronizer}. This
42 * command MUST act as an orchestration layer only: it prepares the source and
43 * target paths, triggers synchronization, and translates the resulting status
44 * into Symfony Console output and process exit codes.
45 */
46#[AsCommand(
47    name: 'agents:skills',
48    description: 'Synchronizes Fast Forward skills into .agents/skills directory.',
49    aliases: ['skills']
50)]
51 class SkillsCommand extends Command
52{
53    use HasJsonOption;
54    use LogsCommandResults;
55
56    private const string SKILLS_DIRECTORY = '.agents/skills';
57
58    /**
59     * Initializes the command with an optional skills synchronizer instance.
60     *
61     * @param PackagedDirectorySynchronizer $synchronizer the synchronizer responsible
62     *                                                    for applying the skills
63     *                                                    synchronization process
64     * @param FilesystemInterface $filesystem filesystem used to resolve
65     *                                        and manage the skills
66     *                                        directory structure
67     */
68    public function __construct(
69        private  PackagedDirectorySynchronizer $synchronizer,
70        private  FilesystemInterface $filesystem,
71    ) {
72        parent::__construct();
73    }
74
75    /**
76     * Configures the supported JSON output option.
77     */
78    protected function configure(): void
79    {
80        $this->setHelp(
81            'This command ensures the consumer repository contains linked Fast Forward skills by creating'
82            . ' symlinks to the packaged skills and removing broken links.'
83        );
84        $this->addJsonOption();
85    }
86
87    /**
88     * Executes the skills synchronization workflow.
89     *
90     * This method SHALL:
91     * - announce the start of synchronization;
92     * - resolve the packaged skills path and consumer target directory;
93     * - fail when the packaged skills directory does not exist;
94     * - create the target directory when it is missing;
95     * - delegate synchronization to {@see PackagedDirectorySynchronizer};
96     * - return a success or failure exit code based on the synchronization result.
97     *
98     * The command MUST return {@see self::FAILURE} when packaged skills are not
99     * available or when the synchronizer reports a failure. It MUST return
100     * {@see self::SUCCESS} only when synchronization completes successfully.
101     *
102     * @param InputInterface $input the console input instance provided by Symfony
103     * @param OutputInterface $output the console output instance used to report progress
104     *
105     * @return int The process exit status. This MUST be {@see self::SUCCESS} on
106     *             success and {@see self::FAILURE} on failure.
107     */
108    protected function execute(InputInterface $input, OutputInterface $output): int
109    {
110        $packageSkillsPath = DevToolsPathResolver::getPackagePath(self::SKILLS_DIRECTORY);
111        $skillsDir = $this->filesystem->getAbsolutePath(self::SKILLS_DIRECTORY);
112        $this->log('Starting skills synchronization...', $input);
113
114        if (! $this->filesystem->exists($packageSkillsPath)) {
115            return $this->failure(
116                'No packaged skills found at: {packaged_skills_path}',
117                $input,
118                [
119                    'packaged_skills_path' => $packageSkillsPath,
120                    'skills_dir' => $skillsDir,
121                    'directory_created' => false,
122                ],
123            );
124        }
125
126        $directoryCreated = false;
127
128        if (! $this->filesystem->exists($skillsDir)) {
129            $this->filesystem->mkdir($skillsDir);
130            $directoryCreated = true;
131            $this->log('Created .agents/skills directory.', $input);
132        }
133
134        $result = $this->synchronizer->synchronize($skillsDir, $packageSkillsPath, self::SKILLS_DIRECTORY);
135
136        if ($result->failed()) {
137            return $this->failure(
138                'Skills synchronization failed.',
139                $input,
140                [
141                    'packaged_skills_path' => $packageSkillsPath,
142                    'skills_dir' => $skillsDir,
143                    'directory_created' => $directoryCreated,
144                ],
145            );
146        }
147
148        return $this->success(
149            'Skills synchronization completed successfully.',
150            $input,
151            [
152                'packaged_skills_path' => $packageSkillsPath,
153                'skills_dir' => $skillsDir,
154                'directory_created' => $directoryCreated,
155            ],
156        );
157    }
158}