@@ -73,6 +73,8 @@ component singleton {
7373 FUNCTION_PATTERN = createObject ( " java" , " java.util.regex.Pattern" ).compile (
7474 " (?i)(?:^|\s)(?:public\s+)?(?:\w+\s+)?function\s+(\w+)\s*\("
7575 )
76+ Files = createObject ( " java" , " java.nio.file.Files" )
77+ Paths = createObject ( " java" , " java.nio.file.Paths" )
7678 }
7779
7880 // Expose them as instance properties for easier access in commands
@@ -192,15 +194,16 @@ component singleton {
192194 * @directory The project directory
193195 * @agent The agent name (claude, copilot, cursor, codex, gemini, opencode)
194196 *
195- * @return Absolute path string, or null if the agent has no dedicated skills directory
197+ * @return Absolute path string, or empty
196198 */
197199 function getAgentSkillsDirectory (
198200 required string directory ,
199201 required string agent
200202 ){
201- var relPath = static .AGENT_SKILLS_DIRS [ arguments .agent ] ?: " "
202- if ( ! relPath .len () ) {
203- return javacast ( " null" , " " )
203+ var relPath = static .AGENT_SKILLS_DIRS [ arguments .agent ]
204+ // If empty, then return it as it means the agent has no dedicated skills directory
205+ if ( relPath .isEmpty () ) {
206+ return relPath
204207 }
205208 // Normalize trailing separator
206209 var dir = arguments .directory
@@ -228,39 +231,39 @@ component singleton {
228231 required string skillName ,
229232 required array agents
230233 ){
231- var canonicalSkillDir = " #arguments .directory #/.agents/skills/#arguments .skillName #"
232- var Files = createObject ( " java" , " java.nio.file.Files" )
233- var Paths = createObject ( " java" , " java.nio.file.Paths" )
234- // Store loop variables outside closure to avoid scope issues
235- var dir = arguments .directory
236- var skill = arguments .skillName
237- var canonical = canonicalSkillDir
234+ var fromSkillDirectory = " #arguments .directory #/.agents/skills/#arguments .skillName #"
238235
239236 for ( var agent in arguments .agents ) {
240- var agentSkillsDir = getAgentSkillsDirectory ( dir , agent )
241- if ( isNull ( agentSkillsDir ) ) {
237+ var agentSkillsDir = getAgentSkillsDirectory ( arguments .directory , agent )
238+
239+ // Skip if agent has no dedicated skills directory, it uses the canonical one instead
240+ if ( agentSkillsDir .isEmpty () ) {
242241 continue ;
243242 }
244243
245- var linkPath = " #agentSkillsDir #/#skill #"
244+ // Create the symlink path for the agent's skills directory
245+ var linkPath = " #agentSkillsDir #/#arguments .skillName #"
246246
247247 // Skip if link/directory already exists
248- if ( directoryExists ( linkPath ) || fileExists ( linkPath ) ) {
248+ if ( directoryExists ( linkPath ) ) {
249249 continue ;
250250 }
251251
252252 try {
253- // Create parent directory if needed
254- if ( ! directoryExists ( agentSkillsDir ) ) {
255- directoryCreate ( agentSkillsDir , true )
256- }
253+ // Create parent directories if needed
254+ directoryCreate ( agentSkillsDir , true , true )
257255
258256 // Compute a relative path from the link's parent dir → canonical dir
259- var agentDirPath = Paths .get ( agentSkillsDir )
260- var targetDirPath = Paths .get ( canonical )
261- var relativePath = agentDirPath .relativize ( targetDirPath )
262-
263- Files .createSymbolicLink ( Paths .get ( linkPath ), relativePath )
257+ var relativeTarget = relativize (
258+ getDirectoryFromPath ( linkPath ),
259+ fromSkillDirectory
260+ )
261+
262+ static .Files .createSymbolicLink (
263+ Paths .get ( linkPath , [] ),
264+ Paths .get ( relativeTarget , [] ),
265+ []
266+ )
264267 } catch ( any e ) {
265268 variables .print
266269 .yellowLine ( " ⚠️ Could not create symlink for agent '#agent #': #e .message #" )
@@ -297,7 +300,7 @@ component singleton {
297300 var linkPath = " #agentSkillsDir #/#skill #"
298301
299302 try {
300- var path = Paths .get ( linkPath )
303+ var path = Paths .get ( linkPath , [] )
301304 if ( Files .isSymbolicLink ( path ) ) {
302305 Files .delete ( path )
303306 }
@@ -1328,4 +1331,48 @@ component singleton {
13281331 return lines .toList ( chr ( 10 ) )
13291332 }
13301333
1334+ /**
1335+ * Relativize a path from one location to another, we use this
1336+ * since Lucee's reflection fails, BoxLang works.
1337+ * TODO: Remove once we are in BoxLang CLI full.
1338+ *
1339+ * @fromPath The source path (absolute or relative)
1340+ * @toPath The target path (absolute or relative)
1341+ *
1342+ * @return Relative path from the source to the target
1343+ */
1344+ function relativize( required string fromPath , required string toPath ) {
1345+ // Normalize separators
1346+ arguments .fromPath = replace ( arguments .fromPath , " \" , " /" , " all" )
1347+ arguments .toPath = replace ( arguments .toPath , " \" , " /" , " all" )
1348+
1349+ // Split into path segments
1350+ var fromParts = listToArray ( arguments .fromPath , " /" )
1351+ var toParts = listToArray ( arguments .toPath , " /" )
1352+
1353+ // Find common prefix
1354+ var i = 1
1355+ while (
1356+ i <= arrayLen ( fromParts ) &&
1357+ i <= arrayLen ( toParts ) &&
1358+ fromParts [ i ] == toParts [ i ]
1359+ ) {
1360+ i ++
1361+ }
1362+
1363+ var relative = []
1364+
1365+ // Go up from the source
1366+ for ( var x = i ; x <= arrayLen ( fromParts ); x ++ ) {
1367+ relative .append ( " .." )
1368+ }
1369+
1370+ // Go down to the target
1371+ for ( var x = i ; x <= arrayLen ( toParts ); x ++ ) {
1372+ relative .append ( toParts [ x ] )
1373+ }
1374+
1375+ return arrayToList ( relative , " /" )
1376+ }
1377+
13311378}
0 commit comments