@@ -27,15 +27,49 @@ export const allSkills = ['playwright-cli', 'playwright-component-testing', 'pla
2727export type SkillName = typeof allSkills [ number ] ;
2828export type SkillTarget = 'claude' | 'agents' ;
2929
30- export async function installSkills ( skills : readonly SkillName [ ] , target : SkillTarget = 'claude' , options ?: { global ?: boolean } ) {
30+ // Source skill docs use this as the command. Kept literal so GitHub/search still
31+ // reads as a real CLI. At install time it is rewritten when the invoker differs
32+ // (e.g. `npx`/`yarn`/`pnpm exec` playwright cli), while skill identity and
33+ // artifact paths stay put.
34+ const sourceCliCommand = 'playwright-cli' ;
35+
36+ export async function installSkills ( skills : readonly SkillName [ ] , target : SkillTarget = 'claude' , options ?: { global ?: boolean , cliCommand ?: string } ) {
3137 const cwd = process . cwd ( ) ;
3238 const baseDir = options ?. global ? os . homedir ( ) : cwd ;
3339 for ( const skill of skills ) {
3440 const sourceDir = libPath ( 'tools' , 'skills' , skill ) ;
3541 if ( ! fs . existsSync ( sourceDir ) )
3642 throw new Error ( `Skill source directory not found: ${ sourceDir } ` ) ;
3743 const destDir = path . join ( baseDir , `.${ target } ` , 'skills' , skill ) ;
38- await fs . promises . cp ( sourceDir , destDir , { recursive : true } ) ;
44+ await copySkillDir ( sourceDir , destDir , options ?. cliCommand || sourceCliCommand ) ;
3945 console . log ( `✅ Skill installed to \`${ options ?. global ? destDir : path . relative ( cwd , destDir ) } \`.` ) ;
4046 }
4147}
48+
49+ async function copySkillDir ( sourceDir : string , destDir : string , cliCommand : string ) {
50+ await fs . promises . mkdir ( destDir , { recursive : true } ) ;
51+ const entries = await fs . promises . readdir ( sourceDir , { withFileTypes : true } ) ;
52+ for ( const entry of entries ) {
53+ const from = path . join ( sourceDir , entry . name ) ;
54+ const to = path . join ( destDir , entry . name ) ;
55+ if ( entry . isDirectory ( ) ) {
56+ await copySkillDir ( from , to , cliCommand ) ;
57+ continue ;
58+ }
59+ if ( ! entry . isFile ( ) )
60+ continue ;
61+ const content = await fs . promises . readFile ( from , 'utf8' ) ;
62+ await fs . promises . writeFile ( to , rewriteCliCommand ( content , cliCommand ) ) ;
63+ }
64+ }
65+
66+ function rewriteCliCommand ( content : string , cliCommand : string ) : string {
67+ // Must not contain `playwright-cli`, or the bulk replace below would rewrite it.
68+ const protectedToken = '\0PWCLI\0' ;
69+ return content
70+ . replaceAll ( `name: ${ sourceCliCommand } ` , `name: ${ protectedToken } ` )
71+ . replaceAll ( `Bash(${ sourceCliCommand } :*)` , `Bash(${ protectedToken } :*)` )
72+ . replaceAll ( `.${ sourceCliCommand } /` , `.${ protectedToken } /` )
73+ . replaceAll ( sourceCliCommand , cliCommand )
74+ . replaceAll ( protectedToken , sourceCliCommand ) ;
75+ }
0 commit comments