1818
1919namespace FastForward \DevTools \Agent \Skills ;
2020
21- use Psr \Log \LoggerInterface ;
21+ use Psr \Log \LoggerAwareInterface ;
22+ use Psr \Log \LoggerAwareTrait ;
23+ use Psr \Log \NullLogger ;
2224use Symfony \Component \Filesystem \Filesystem ;
2325use Symfony \Component \Finder \Finder ;
2426use Symfony \Component \Filesystem \Path ;
3032 * repository to the skills packaged within the fast-forward/dev-tools dependency.
3133 * It handles initial sync, idempotent re-runs, and cleanup of broken links.
3234 */
33- final class SkillsSynchronizer
35+ final class SkillsSynchronizer implements LoggerAwareInterface
3436{
35- private readonly Filesystem $ filesystem ;
37+ use LoggerAwareTrait ;
3638
3739 /**
40+ * Initializes the synchronizer with an optional filesystem instance.
41+ *
42+ * If no filesystem is provided, a default {@see Filesystem} instance is created.
43+ *
3844 * @param Filesystem|null $filesystem Filesystem instance for file operations
45+ * @param Finder $finder Finder instance for locating skill directories in the package
3946 */
40- public function __construct (?Filesystem $ filesystem = null )
41- {
42- $ this ->filesystem = $ filesystem ?? new Filesystem ();
47+ public function __construct (
48+ private readonly Filesystem $ filesystem = new Filesystem (),
49+ private readonly Finder $ finder = new Finder (),
50+ ) {
51+ $ this ->logger = new NullLogger ();
4352 }
4453
4554 /**
@@ -51,30 +60,26 @@ public function __construct(?Filesystem $filesystem = null)
5160 *
5261 * @param string $skillsDir Absolute path to the consumer's .agents/skills directory
5362 * @param string $packageSkillsPath Absolute path to the packaged skills in the dependency
54- * @param LoggerInterface $logger Logger for reporting sync operations
5563 *
5664 * @return SynchronizeResult Result containing counts of created, preserved, and removed links
5765 */
58- public function synchronize (
59- string $ skillsDir ,
60- string $ packageSkillsPath ,
61- LoggerInterface $ logger ,
62- ): SynchronizeResult {
66+ public function synchronize (string $ skillsDir , string $ packageSkillsPath ): SynchronizeResult
67+ {
6368 $ result = new SynchronizeResult ();
6469
6570 if (! $ this ->filesystem ->exists ($ packageSkillsPath )) {
66- $ logger ->error ('No packaged skills found at: ' . $ packageSkillsPath );
71+ $ this -> logger ->error ('No packaged skills found at: ' . $ packageSkillsPath );
6772 $ result ->markFailed ();
6873
6974 return $ result ;
7075 }
7176
7277 if (! $ this ->filesystem ->exists ($ skillsDir )) {
7378 $ this ->filesystem ->mkdir ($ skillsDir );
74- $ logger ->info ('Created .agents/skills directory. ' );
79+ $ this -> logger ->info ('Created .agents/skills directory. ' );
7580 }
7681
77- $ this ->syncPackageSkills ($ skillsDir , $ packageSkillsPath , $ logger , $ result );
82+ $ this ->syncPackageSkills ($ skillsDir , $ packageSkillsPath , $ result );
7883
7984 return $ result ;
8085 }
@@ -87,16 +92,14 @@ public function synchronize(
8792 *
8893 * @param string $skillsDir Target directory for symlinks
8994 * @param string $packageSkillsPath Source directory containing packaged skills
90- * @param LoggerInterface $logger Logger for operation feedback
9195 * @param SynchronizeResult $result Result object to track outcomes
9296 */
9397 private function syncPackageSkills (
9498 string $ skillsDir ,
9599 string $ packageSkillsPath ,
96- LoggerInterface $ logger ,
97100 SynchronizeResult $ result ,
98101 ): void {
99- $ finder = Finder:: create ()
102+ $ finder = $ this -> finder
100103 ->directories ()
101104 ->in ($ packageSkillsPath )
102105 ->depth ('== 0 ' );
@@ -106,7 +109,7 @@ private function syncPackageSkills(
106109 $ targetLink = Path::makeAbsolute ($ skillName , $ skillsDir );
107110 $ sourcePath = $ skillDir ->getRealPath ();
108111
109- $ this ->processSkillLink ($ skillName , $ targetLink , $ sourcePath , $ logger , $ result );
112+ $ this ->processSkillLink ($ skillName , $ targetLink , $ sourcePath , $ result );
110113 }
111114 }
112115
@@ -119,29 +122,27 @@ private function syncPackageSkills(
119122 * @param string $skillName Name of the skill being processed
120123 * @param string $targetLink Absolute path where the symlink should exist
121124 * @param string $sourcePath Absolute path to the packaged skill directory
122- * @param LoggerInterface $logger Logger for feedback on actions taken
123125 * @param SynchronizeResult $result Result tracker for reporting outcomes
124126 */
125127 private function processSkillLink (
126128 string $ skillName ,
127129 string $ targetLink ,
128130 string $ sourcePath ,
129- LoggerInterface $ logger ,
130131 SynchronizeResult $ result ,
131132 ): void {
132133 if (! $ this ->filesystem ->exists ($ targetLink )) {
133- $ this ->createNewLink ($ skillName , $ targetLink , $ sourcePath , $ logger , $ result );
134+ $ this ->createNewLink ($ skillName , $ targetLink , $ sourcePath , $ result );
134135
135136 return ;
136137 }
137138
138139 if (! $ this ->isSymlink ($ targetLink )) {
139- $ this ->preserveExistingNonSymlink ($ skillName , $ logger , $ result );
140+ $ this ->preserveExistingNonSymlink ($ skillName , $ result );
140141
141142 return ;
142143 }
143144
144- $ this ->processExistingSymlink ($ skillName , $ targetLink , $ sourcePath , $ logger , $ result );
145+ $ this ->processExistingSymlink ($ skillName , $ targetLink , $ sourcePath , $ result );
145146 }
146147
147148 /**
@@ -153,18 +154,16 @@ private function processSkillLink(
153154 * @param string $skillName Name identifying the skill
154155 * @param string $targetLink Absolute path where the symlink will be created
155156 * @param string $sourcePath Absolute path to the packaged skill directory
156- * @param LoggerInterface $logger Logger for confirmation message
157157 * @param SynchronizeResult $result Result object for tracking creation
158158 */
159159 private function createNewLink (
160160 string $ skillName ,
161161 string $ targetLink ,
162162 string $ sourcePath ,
163- LoggerInterface $ logger ,
164163 SynchronizeResult $ result ,
165164 ): void {
166165 $ this ->filesystem ->symlink ($ sourcePath , $ targetLink );
167- $ logger ->info ('Created link: ' . $ skillName . ' -> ' . $ sourcePath );
166+ $ this -> logger ->info ('Created link: ' . $ skillName . ' -> ' . $ sourcePath );
168167 $ result ->addCreatedLink ($ skillName );
169168 }
170169
@@ -176,15 +175,11 @@ private function createNewLink(
176175 * replaced to avoid accidental data loss.
177176 *
178177 * @param string $skillName Name of the skill with the conflicting item
179- * @param LoggerInterface $logger Logger for the preservation notice
180178 * @param SynchronizeResult $result Result tracker for preserved items
181179 */
182- private function preserveExistingNonSymlink (
183- string $ skillName ,
184- LoggerInterface $ logger ,
185- SynchronizeResult $ result ,
186- ): void {
187- $ logger ->notice ('Existing non-symlink found: ' . $ skillName . ' (keeping as is, skipping link creation) ' );
180+ private function preserveExistingNonSymlink (string $ skillName , SynchronizeResult $ result ): void
181+ {
182+ $ this ->logger ->notice ('Existing non-symlink found: ' . $ skillName . ' (keeping as is, skipping link creation) ' );
188183 $ result ->addPreservedLink ($ skillName );
189184 }
190185
@@ -197,25 +192,23 @@ private function preserveExistingNonSymlink(
197192 * @param string $skillName Name of the skill with the existing symlink
198193 * @param string $targetLink Absolute path to the existing symlink
199194 * @param string $sourcePath Absolute path to the expected source directory
200- * @param LoggerInterface $logger Logger for preservation or repair messages
201195 * @param SynchronizeResult $result Result tracker for preserved or removed links
202196 */
203197 private function processExistingSymlink (
204198 string $ skillName ,
205199 string $ targetLink ,
206200 string $ sourcePath ,
207- LoggerInterface $ logger ,
208201 SynchronizeResult $ result ,
209202 ): void {
210203 $ linkPath = $ this ->filesystem ->readlink ($ targetLink , true );
211204
212205 if (! $ linkPath || ! $ this ->filesystem ->exists ($ linkPath )) {
213- $ this ->repairBrokenLink ($ skillName , $ targetLink , $ sourcePath , $ logger , $ result );
206+ $ this ->repairBrokenLink ($ skillName , $ targetLink , $ sourcePath , $ result );
214207
215208 return ;
216209 }
217210
218- $ logger ->notice ('Preserved existing link: ' . $ skillName );
211+ $ this -> logger ->notice ('Preserved existing link: ' . $ skillName );
219212 $ result ->addPreservedLink ($ skillName );
220213 }
221214
@@ -229,21 +222,19 @@ private function processExistingSymlink(
229222 * @param string $skillName Name of the skill with the broken symlink
230223 * @param string $targetLink Absolute path to the broken symlink
231224 * @param string $sourcePath Absolute path to the current packaged skill
232- * @param LoggerInterface $logger Logger for repair and creation messages
233225 * @param SynchronizeResult $result Result tracker for removed and created items
234226 */
235227 private function repairBrokenLink (
236228 string $ skillName ,
237229 string $ targetLink ,
238230 string $ sourcePath ,
239- LoggerInterface $ logger ,
240231 SynchronizeResult $ result ,
241232 ): void {
242233 $ this ->filesystem ->remove ($ targetLink );
243- $ logger ->notice ('Existing link is broken: ' . $ skillName . ' (removing and recreating) ' );
234+ $ this -> logger ->notice ('Existing link is broken: ' . $ skillName . ' (removing and recreating) ' );
244235 $ result ->addRemovedBrokenLink ($ skillName );
245236
246- $ this ->createNewLink ($ skillName , $ targetLink , $ sourcePath , $ logger , $ result );
237+ $ this ->createNewLink ($ skillName , $ targetLink , $ sourcePath , $ result );
247238 }
248239
249240 /**
@@ -253,6 +244,6 @@ private function repairBrokenLink(
253244 */
254245 private function isSymlink (string $ path ): bool
255246 {
256- return is_link ($ path );
247+ return null !== $ this -> filesystem -> readlink ($ path );
257248 }
258249}
0 commit comments