11import { cp , readdir , readFile , writeFile } from 'node:fs/promises' ;
22import { basename , extname , join } from 'node:path' ;
3- import { DIR_AIPM_NAMESPACE } from '../constants' ;
3+ import { DIR_AIPM_NAMESPACE , DIR_SKILLS } from '../constants' ;
44import { DirectoryNotFoundError , isFileNotFoundError } from '../errors' ;
5+ import type { MarketplaceManifest } from '../schema' ;
56import { applyCursorFrontmatter } from './frontmatter' ;
67import { ensureDir , fileExists } from './fs' ;
8+ import { hasPluginName } from './marketplace' ;
79
8- export type SyncResult = {
10+ export type AipmPluginSyncResult = {
11+ type : 'aipm' ;
912 commandsCount : number ;
1013 rulesCount : number ;
1114 agentsCount : number ;
1215 skillsCount : number ;
1316 hooksCount : number ;
1417} ;
1518
19+ export type ClaudeCodePluginSyncResult = {
20+ type : 'claudecode' ;
21+ skillsCount : number ;
22+ } ;
23+
24+ export type SyncResult = AipmPluginSyncResult | ClaudeCodePluginSyncResult ;
25+
1626/**
1727 * Syncs a plugin to the correct Cursor directories:
1828 * - commands/*.md → .cursor/commands/aipm/marketplace-name/plugin-name/
@@ -26,8 +36,9 @@ export async function syncPluginToCursor(
2636 marketplaceName : string ,
2737 pluginName : string ,
2838 cursorDir : string ,
29- ) : Promise < SyncResult > {
30- const result : SyncResult = {
39+ ) : Promise < AipmPluginSyncResult > {
40+ const result : AipmPluginSyncResult = {
41+ type : 'aipm' ,
3142 commandsCount : 0 ,
3243 rulesCount : 0 ,
3344 agentsCount : 0 ,
@@ -193,26 +204,65 @@ async function syncDirectory(sourceDir: string, targetDir: string, extensions: s
193204 return count ;
194205}
195206
207+ /**
208+ * Syncs a Claude Code meta-plugin to Cursor directories.
209+ * Meta-plugins point to the marketplace root and define skills in the manifest.
210+ * We sync each skill directory listed in the manifest's skills array.
211+ */
212+ export async function syncMetaPluginToCursor (
213+ marketplacePath : string ,
214+ marketplaceManifest : MarketplaceManifest ,
215+ pluginName : string ,
216+ marketplaceName : string ,
217+ cursorDir : string ,
218+ ) : Promise < ClaudeCodePluginSyncResult > {
219+ const pluginEntry = marketplaceManifest . plugins . find ( hasPluginName ( pluginName ) ) ;
220+ if ( ! pluginEntry || ! pluginEntry . skills ) {
221+ return { type : 'claudecode' , skillsCount : 0 } ;
222+ }
223+
224+ let skillsCount = 0 ;
225+
226+ for ( const skillPath of pluginEntry . skills ) {
227+ const normalizedSkillPath = skillPath . replace ( / ^ \. \/ / , '' ) ;
228+ const fullSkillPath = join ( marketplacePath , normalizedSkillPath ) ;
229+
230+ const marketplaceSegments = marketplaceName . split ( '/' ) ;
231+ const targetPath = join ( cursorDir , DIR_SKILLS , DIR_AIPM_NAMESPACE , ...marketplaceSegments , normalizedSkillPath ) ;
232+
233+ const count = await syncDirectory ( fullSkillPath , targetPath , [ '.md' ] ) ;
234+ skillsCount += count ;
235+ }
236+
237+ return { type : 'claudecode' , skillsCount } ;
238+ }
239+
196240/**
197241 * Gets a summary string for sync results
198242 */
199243export function formatSyncResult ( result : SyncResult ) : string {
200244 const parts : string [ ] = [ ] ;
201245
202- if ( result . commandsCount > 0 ) {
203- parts . push ( `${ result . commandsCount } command(s)` ) ;
204- }
205- if ( result . rulesCount > 0 ) {
206- parts . push ( `${ result . rulesCount } rule(s)` ) ;
207- }
208- if ( result . agentsCount > 0 ) {
209- parts . push ( `${ result . agentsCount } agent(s)` ) ;
210- }
211- if ( result . skillsCount > 0 ) {
212- parts . push ( `${ result . skillsCount } skill(s)` ) ;
213- }
214- if ( result . hooksCount > 0 ) {
215- parts . push ( `${ result . hooksCount } hook(s)` ) ;
246+ if ( result . type === 'aipm' ) {
247+ if ( result . commandsCount > 0 ) {
248+ parts . push ( `${ result . commandsCount } command(s)` ) ;
249+ }
250+ if ( result . rulesCount > 0 ) {
251+ parts . push ( `${ result . rulesCount } rule(s)` ) ;
252+ }
253+ if ( result . agentsCount > 0 ) {
254+ parts . push ( `${ result . agentsCount } agent(s)` ) ;
255+ }
256+ if ( result . skillsCount > 0 ) {
257+ parts . push ( `${ result . skillsCount } skill(s)` ) ;
258+ }
259+ if ( result . hooksCount > 0 ) {
260+ parts . push ( `${ result . hooksCount } hook(s)` ) ;
261+ }
262+ } else if ( result . type === 'claudecode' ) {
263+ if ( result . skillsCount > 0 ) {
264+ parts . push ( `${ result . skillsCount } skill(s)` ) ;
265+ }
216266 }
217267
218268 return parts . length > 0 ? parts . join ( ', ' ) : 'no files' ;
0 commit comments