@@ -431,4 +431,174 @@ describe('plugin-install', () => {
431431 expect ( config . plugins [ 'plugin-2@local' ] ) . toBeDefined ( ) ;
432432 } ) ;
433433 } ) ;
434+
435+ describe ( 'nested plugin directories' , ( ) => {
436+ test ( 'should install plugin from nested directory when using simple name' , async ( ) => {
437+ const marketplaceDir = join ( testDir , 'marketplace' ) ;
438+ await mkdir ( marketplaceDir , { recursive : true } ) ;
439+
440+ // Create plugin in nested directory: available-plugins/code-review-ai
441+ const nestedPluginDir = join ( marketplaceDir , 'available-plugins' , 'code-review-ai' ) ;
442+ await mkdir ( join ( nestedPluginDir , '.claude-plugin' ) , { recursive : true } ) ;
443+ await writeFile (
444+ join ( nestedPluginDir , '.claude-plugin' , 'plugin.json' ) ,
445+ JSON . stringify ( {
446+ name : 'code-review-ai' ,
447+ version : '1.2.0' ,
448+ author : 'Test Author' ,
449+ } ) ,
450+ ) ;
451+
452+ // Create a command file
453+ await mkdir ( join ( nestedPluginDir , 'commands' ) , { recursive : true } ) ;
454+ await writeFile ( join ( nestedPluginDir , 'commands' , 'review.md' ) , '# Review command' ) ;
455+
456+ const pluginsPath = join ( testDir , '.aipm' , 'config.json' ) ;
457+ const aipmDir = join ( testDir , '.aipm' ) ;
458+ await mkdir ( aipmDir , { recursive : true } ) ;
459+ await writeFile (
460+ pluginsPath ,
461+ JSON . stringify ( {
462+ marketplaces : {
463+ local : { source : 'directory' , path : './marketplace' } ,
464+ } ,
465+ plugins : { } ,
466+ } ) ,
467+ ) ;
468+
469+ const options = {
470+ pluginId : 'code-review-ai@local' ,
471+ cwd : testDir ,
472+ } ;
473+
474+ await pluginInstall ( options ) ;
475+
476+ // Verify plugin was installed
477+ const config = JSON . parse ( await Bun . file ( pluginsPath ) . text ( ) ) ;
478+ expect ( config . plugins [ 'code-review-ai@local' ] ) . toBeDefined ( ) ;
479+ expect ( config . plugins [ 'code-review-ai@local' ] . enabled ) . toBe ( true ) ;
480+
481+ // Verify files were synced to .cursor/
482+ const commandsPath = join ( testDir , '.cursor' , 'commands' , 'aipm' , 'local' , 'code-review-ai' , 'review.md' ) ;
483+ expect ( await fileExists ( commandsPath ) ) . toBe ( true ) ;
484+ } ) ;
485+
486+ test ( 'should install plugin from nested directory when manifest has incorrect source path' , async ( ) => {
487+ const marketplaceDir = join ( testDir , 'marketplace' ) ;
488+ await mkdir ( marketplaceDir , { recursive : true } ) ;
489+
490+ // Create marketplace manifest with incorrect source path
491+ await mkdir ( join ( marketplaceDir , '.claude-plugin' ) , { recursive : true } ) ;
492+ await writeFile (
493+ join ( marketplaceDir , '.claude-plugin' , 'marketplace.json' ) ,
494+ JSON . stringify ( {
495+ name : 'test-marketplace' ,
496+ owner : { name : 'Test Owner' } ,
497+ plugins : [
498+ {
499+ name : 'code-review-ai' ,
500+ source : './wrong-path/code-review-ai' , // Incorrect path
501+ } ,
502+ ] ,
503+ } ) ,
504+ ) ;
505+
506+ // Create plugin in actual nested directory: available-plugins/code-review-ai
507+ const nestedPluginDir = join ( marketplaceDir , 'available-plugins' , 'code-review-ai' ) ;
508+ await mkdir ( join ( nestedPluginDir , '.claude-plugin' ) , { recursive : true } ) ;
509+ await writeFile (
510+ join ( nestedPluginDir , '.claude-plugin' , 'plugin.json' ) ,
511+ JSON . stringify ( {
512+ name : 'code-review-ai' ,
513+ version : '1.2.0' ,
514+ author : 'Test Author' ,
515+ } ) ,
516+ ) ;
517+
518+ await mkdir ( join ( nestedPluginDir , 'commands' ) , { recursive : true } ) ;
519+ await writeFile ( join ( nestedPluginDir , 'commands' , 'review.md' ) , '# Review command' ) ;
520+
521+ const pluginsPath = join ( testDir , '.aipm' , 'config.json' ) ;
522+ const aipmDir = join ( testDir , '.aipm' ) ;
523+ await mkdir ( aipmDir , { recursive : true } ) ;
524+ await writeFile (
525+ pluginsPath ,
526+ JSON . stringify ( {
527+ marketplaces : {
528+ local : { source : 'directory' , path : './marketplace' } ,
529+ } ,
530+ plugins : { } ,
531+ } ) ,
532+ ) ;
533+
534+ const options = {
535+ pluginId : 'code-review-ai@local' ,
536+ cwd : testDir ,
537+ } ;
538+
539+ await pluginInstall ( options ) ;
540+
541+ // Verify plugin was installed despite incorrect manifest path
542+ const config = JSON . parse ( await Bun . file ( pluginsPath ) . text ( ) ) ;
543+ expect ( config . plugins [ 'code-review-ai@local' ] ) . toBeDefined ( ) ;
544+ expect ( config . plugins [ 'code-review-ai@local' ] . enabled ) . toBe ( true ) ;
545+
546+ // Verify files were synced from the correct nested path
547+ const commandsPath = join ( testDir , '.cursor' , 'commands' , 'aipm' , 'local' , 'code-review-ai' , 'review.md' ) ;
548+ expect ( await fileExists ( commandsPath ) ) . toBe ( true ) ;
549+ } ) ;
550+
551+ test ( 'should handle multiple nested plugins with same base name' , async ( ) => {
552+ const marketplaceDir = join ( testDir , 'marketplace' ) ;
553+ await mkdir ( marketplaceDir , { recursive : true } ) ;
554+
555+ // Create two plugins with same base name in different nested directories
556+ const plugin1Dir = join ( marketplaceDir , 'category-a' , 'my-plugin' ) ;
557+ await mkdir ( join ( plugin1Dir , '.claude-plugin' ) , { recursive : true } ) ;
558+ await writeFile (
559+ join ( plugin1Dir , '.claude-plugin' , 'plugin.json' ) ,
560+ JSON . stringify ( {
561+ name : 'my-plugin' ,
562+ version : '1.0.0' ,
563+ author : 'Test Author' ,
564+ } ) ,
565+ ) ;
566+
567+ const plugin2Dir = join ( marketplaceDir , 'category-b' , 'my-plugin' ) ;
568+ await mkdir ( join ( plugin2Dir , '.claude-plugin' ) , { recursive : true } ) ;
569+ await writeFile (
570+ join ( plugin2Dir , '.claude-plugin' , 'plugin.json' ) ,
571+ JSON . stringify ( {
572+ name : 'my-plugin' ,
573+ version : '2.0.0' ,
574+ author : 'Test Author' ,
575+ } ) ,
576+ ) ;
577+
578+ const pluginsPath = join ( testDir , '.aipm' , 'config.json' ) ;
579+ const aipmDir = join ( testDir , '.aipm' ) ;
580+ await mkdir ( aipmDir , { recursive : true } ) ;
581+ await writeFile (
582+ pluginsPath ,
583+ JSON . stringify ( {
584+ marketplaces : {
585+ local : { source : 'directory' , path : './marketplace' } ,
586+ } ,
587+ plugins : { } ,
588+ } ) ,
589+ ) ;
590+
591+ const options = {
592+ pluginId : 'my-plugin@local' ,
593+ cwd : testDir ,
594+ } ;
595+
596+ // Should install the first matching plugin found
597+ await pluginInstall ( options ) ;
598+
599+ const config = JSON . parse ( await Bun . file ( pluginsPath ) . text ( ) ) ;
600+ expect ( config . plugins [ 'my-plugin@local' ] ) . toBeDefined ( ) ;
601+ expect ( config . plugins [ 'my-plugin@local' ] . enabled ) . toBe ( true ) ;
602+ } ) ;
603+ } ) ;
434604} ) ;
0 commit comments