11import { existsSync , readFileSync } from 'node:fs'
2- import { isAbsolute , relative , resolve } from 'node:path'
2+ import { dirname , isAbsolute , relative , resolve } from 'node:path'
33import { fail } from '../cli-error.js'
44import { scanOptionsFromGlobalFlags } from '../cli-support.js'
55import { resolveSkillUse } from '../resolver.js'
66import { parseSkillUse } from '../skill-use.js'
7+ import { toPosixPath } from '../utils.js'
78import type { GlobalScanFlags } from '../cli-support.js'
89import type { ScanOptions , ScanResult } from '../types.js'
910
@@ -27,6 +28,314 @@ function isPathInsidePackageRoot(path: string, packageRoot: string): boolean {
2728 )
2829}
2930
31+ function splitDestinationSuffix ( destination : string ) : {
32+ pathPart : string
33+ suffix : string
34+ } {
35+ const hashIndex = destination . indexOf ( '#' )
36+ const queryIndex = destination . indexOf ( '?' )
37+ const suffixIndex =
38+ hashIndex === - 1
39+ ? queryIndex
40+ : queryIndex === - 1
41+ ? hashIndex
42+ : Math . min ( hashIndex , queryIndex )
43+
44+ if ( suffixIndex === - 1 ) {
45+ return { pathPart : destination , suffix : '' }
46+ }
47+
48+ return {
49+ pathPart : destination . slice ( 0 , suffixIndex ) ,
50+ suffix : destination . slice ( suffixIndex ) ,
51+ }
52+ }
53+
54+ function isExternalOrAbsoluteDestination ( destination : string ) : boolean {
55+ return (
56+ destination === '' ||
57+ destination . startsWith ( '#' ) ||
58+ destination . startsWith ( '?' ) ||
59+ destination . startsWith ( '//' ) ||
60+ / ^ [ A - Z a - z ] [ A - Z a - z 0 - 9 + . - ] * : / . test ( destination ) ||
61+ isAbsolute ( destination )
62+ )
63+ }
64+
65+ interface MarkdownDestinationRewriteContext {
66+ cwd : string
67+ resolvedPackageRoot : string
68+ skillDir : string
69+ }
70+
71+ function findClosingBracket ( line : string , start : number ) : number {
72+ let depth = 0
73+
74+ for ( let index = start ; index < line . length ; index ++ ) {
75+ const char = line [ index ] !
76+ if ( char === '\\' ) {
77+ index ++
78+ continue
79+ }
80+ if ( char === '[' ) {
81+ depth ++
82+ continue
83+ }
84+ if ( char === ']' ) {
85+ depth --
86+ if ( depth === 0 ) return index
87+ }
88+ }
89+
90+ return - 1
91+ }
92+
93+ function findClosingParen ( line : string , start : number ) : number {
94+ for ( let index = start ; index < line . length ; index ++ ) {
95+ const char = line [ index ] !
96+ if ( char === '\\' ) {
97+ index ++
98+ continue
99+ }
100+ if ( char === ')' ) return index
101+ }
102+
103+ return - 1
104+ }
105+
106+ function readBareDestination (
107+ line : string ,
108+ start : number ,
109+ ) : { destinationEnd : number ; endParen : number } | null {
110+ let depth = 0
111+
112+ for ( let index = start ; index < line . length ; index ++ ) {
113+ const char = line [ index ] !
114+ if ( char === '\\' ) {
115+ index ++
116+ continue
117+ }
118+ if ( char === '(' ) {
119+ depth ++
120+ continue
121+ }
122+ if ( char === ')' ) {
123+ if ( depth === 0 ) {
124+ return { destinationEnd : index , endParen : index }
125+ }
126+ depth --
127+ continue
128+ }
129+ if ( / \s / . test ( char ) && depth === 0 ) {
130+ const endParen = findClosingParen ( line , index )
131+ if ( endParen === - 1 ) return null
132+ return { destinationEnd : index , endParen }
133+ }
134+ }
135+
136+ return null
137+ }
138+
139+ function readMarkdownDestination (
140+ line : string ,
141+ start : number ,
142+ ) : {
143+ destination : string
144+ destinationStart : number
145+ destinationEnd : number
146+ endParen : number
147+ } | null {
148+ let cursor = start
149+ while ( cursor < line . length && / \s / . test ( line [ cursor ] ! ) ) cursor ++
150+
151+ if ( line [ cursor ] === '<' ) {
152+ const destinationStart = cursor + 1
153+ const destinationEnd = line . indexOf ( '>' , destinationStart )
154+ if ( destinationEnd === - 1 ) return null
155+ const endParen = findClosingParen ( line , destinationEnd + 1 )
156+ if ( endParen === - 1 ) return null
157+ return {
158+ destination : line . slice ( destinationStart , destinationEnd ) ,
159+ destinationStart,
160+ destinationEnd,
161+ endParen,
162+ }
163+ }
164+
165+ const read = readBareDestination ( line , cursor )
166+ if ( ! read ) return null
167+
168+ return {
169+ destination : line . slice ( cursor , read . destinationEnd ) ,
170+ destinationStart : cursor ,
171+ destinationEnd : read . destinationEnd ,
172+ endParen : read . endParen ,
173+ }
174+ }
175+
176+ function getCodeFenceMarker ( line : string ) : '`' | '~' | null {
177+ const match = line . match ( / ^ \s * ( ` { 3 , } | ~ { 3 , } ) / )
178+ const marker = match ?. [ 1 ] ?. [ 0 ]
179+ return marker === '`' || marker === '~' ? marker : null
180+ }
181+
182+ function rewriteMarkdownDestination ( {
183+ context,
184+ destination,
185+ } : {
186+ context : MarkdownDestinationRewriteContext
187+ destination : string
188+ } ) : string {
189+ if ( isExternalOrAbsoluteDestination ( destination ) ) return destination
190+
191+ const { pathPart, suffix } = splitDestinationSuffix ( destination )
192+ if ( isExternalOrAbsoluteDestination ( pathPart ) ) return destination
193+
194+ const resolvedDestinationPath = resolve ( context . skillDir , pathPart )
195+ const relativeToPackageRoot = relative (
196+ context . resolvedPackageRoot ,
197+ resolvedDestinationPath ,
198+ )
199+ if (
200+ relativeToPackageRoot . startsWith ( '..' ) ||
201+ isAbsolute ( relativeToPackageRoot )
202+ ) {
203+ return destination
204+ }
205+
206+ const relativeToCwd = relative ( context . cwd , resolvedDestinationPath )
207+ const rewrittenPath =
208+ relativeToCwd &&
209+ ! relativeToCwd . startsWith ( '..' ) &&
210+ ! isAbsolute ( relativeToCwd )
211+ ? relativeToCwd
212+ : resolvedDestinationPath
213+
214+ return `${ toPosixPath ( rewrittenPath ) } ${ suffix } `
215+ }
216+
217+ function rewriteMarkdownLineDestinations ( {
218+ context,
219+ line,
220+ } : {
221+ context : MarkdownDestinationRewriteContext
222+ line : string
223+ } ) : string {
224+ if ( ! line . includes ( '[' ) ) return line
225+
226+ let output = ''
227+ let cursor = 0
228+
229+ while ( cursor < line . length ) {
230+ const nextCodeStart = line . indexOf ( '`' , cursor )
231+ const nextLinkStart = line . indexOf ( '[' , cursor )
232+
233+ if ( nextLinkStart === - 1 ) {
234+ output += line . slice ( cursor )
235+ break
236+ }
237+
238+ if ( nextCodeStart !== - 1 && nextCodeStart < nextLinkStart ) {
239+ output += line . slice ( cursor , nextCodeStart )
240+ cursor = nextCodeStart
241+ const codeStart = cursor
242+ while ( cursor < line . length && line [ cursor ] === '`' ) cursor ++
243+ const marker = line . slice ( codeStart , cursor )
244+ const codeEnd = line . indexOf ( marker , cursor )
245+ if ( codeEnd === - 1 ) {
246+ output += line . slice ( codeStart )
247+ break
248+ }
249+ output += line . slice ( codeStart , codeEnd + marker . length )
250+ cursor = codeEnd + marker . length
251+ continue
252+ }
253+
254+ const linkStart =
255+ nextLinkStart > 0 && line [ nextLinkStart - 1 ] === '!'
256+ ? nextLinkStart - 1
257+ : nextLinkStart
258+ output += line . slice ( cursor , linkStart )
259+
260+ const labelStart = nextLinkStart
261+ const labelEnd = findClosingBracket ( line , labelStart )
262+ if ( labelEnd === - 1 ) {
263+ output += line . slice ( linkStart )
264+ break
265+ }
266+
267+ if ( line [ labelEnd + 1 ] !== '(' ) {
268+ output += line . slice ( linkStart , nextLinkStart + 1 )
269+ cursor = nextLinkStart + 1
270+ continue
271+ }
272+
273+ const destination = readMarkdownDestination ( line , labelEnd + 2 )
274+ if ( ! destination ) {
275+ output += line . slice ( linkStart , nextLinkStart + 1 )
276+ cursor = nextLinkStart + 1
277+ continue
278+ }
279+
280+ const rewritten = rewriteMarkdownDestination ( {
281+ context,
282+ destination : destination . destination ,
283+ } )
284+ output +=
285+ line . slice ( linkStart , destination . destinationStart ) +
286+ rewritten +
287+ line . slice ( destination . destinationEnd , destination . endParen + 1 )
288+ cursor = destination . endParen + 1
289+ }
290+
291+ return output
292+ }
293+
294+ function rewriteLoadedSkillMarkdownDestinations ( {
295+ content,
296+ packageRoot,
297+ skillFilePath,
298+ } : {
299+ content : string
300+ packageRoot : string
301+ skillFilePath : string
302+ } ) : string {
303+ const context : MarkdownDestinationRewriteContext = {
304+ cwd : process . cwd ( ) ,
305+ resolvedPackageRoot : resolveFromCwd ( packageRoot ) ,
306+ skillDir : dirname ( skillFilePath ) ,
307+ }
308+ let inFence : '`' | '~' | null = null
309+ const parts = content . split ( / ( \r ? \n ) / )
310+ let output = ''
311+
312+ for ( let index = 0 ; index < parts . length ; index += 2 ) {
313+ const line = parts [ index ] ?? ''
314+ const newline = parts [ index + 1 ] ?? ''
315+ const marker = getCodeFenceMarker ( line )
316+
317+ if ( inFence ) {
318+ output += line + newline
319+ if ( marker === inFence ) inFence = null
320+ continue
321+ }
322+
323+ if ( marker ) {
324+ inFence = marker
325+ output += line + newline
326+ continue
327+ }
328+
329+ output +=
330+ rewriteMarkdownLineDestinations ( {
331+ context,
332+ line,
333+ } ) + newline
334+ }
335+
336+ return output
337+ }
338+
30339export async function runLoadCommand (
31340 use : string | undefined ,
32341 options : LoadCommandOptions ,
@@ -64,7 +373,11 @@ export async function runLoadCommand(
64373 return
65374 }
66375
67- const content = readFileSync ( resolvedPath , 'utf8' )
376+ const content = rewriteLoadedSkillMarkdownDestinations ( {
377+ content : readFileSync ( resolvedPath , 'utf8' ) ,
378+ packageRoot : resolved . packageRoot ,
379+ skillFilePath : resolvedPath ,
380+ } )
68381
69382 if ( options . json ) {
70383 console . log (
0 commit comments