1- import merge from 'lodash.merge' ;
21import { join } from 'node:path' ;
32import { z } from 'zod' ;
4- import { getConfigPath , getNotInitializedMessage , loadPluginsConfig } from '../config/loader' ;
5- import { DIR_CURSOR , FILE_AIPM_CONFIG , FILE_AIPM_CONFIG_LOCAL } from '../constants' ;
3+ import { loadClaudeCodeMarketplaces } from '../config/loader' ;
4+ import { DIR_CURSOR } from '../constants' ;
65import { getErrorMessage } from '../errors' ;
7- import { loadTargetConfig , saveConfig } from '../helpers/aipm-config' ;
86import { fileExists } from '../helpers/fs' ;
9- import { resolveMarketplacePath } from '../helpers/git' ;
107import { defaultIO } from '../helpers/io' ;
118import { getMarketplaceType , loadMarketplaceManifest , resolvePluginPath } from '../helpers/marketplace' ;
129import { isMetaPlugin , parsePluginId , validatePluginStructure } from '../helpers/plugin' ;
@@ -26,54 +23,42 @@ export async function pluginInstall(options: unknown): Promise<void> {
2623 const cwd = cmd . cwd || process . cwd ( ) ;
2724
2825 try {
29- const { config, sources } = await loadPluginsConfig ( cwd ) ;
30-
31- if ( ! sources . project && ! sources . local ) {
32- const error = new Error ( getNotInitializedMessage ( ) ) ;
33- defaultIO . logError ( error . message ) ;
34- throw error ;
35- }
36- const configName = cmd . local ? getConfigPath ( FILE_AIPM_CONFIG_LOCAL ) : getConfigPath ( FILE_AIPM_CONFIG ) ;
37-
3826 const { pluginName, marketplaceName } = parsePluginId ( cmd . pluginId ) ;
3927
40- const marketplace = config . marketplaces [ marketplaceName ] ;
28+ // Auto-discover Claude Code marketplaces
29+ const claudeMarketplaces = await loadClaudeCodeMarketplaces ( ) ;
30+
31+ const marketplace = claudeMarketplaces [ marketplaceName ] ;
4132
4233 if ( ! marketplace ) {
43- const error = new Error ( `Marketplace '${ marketplaceName } ' not found. Add it first with 'marketplace add'.` ) ;
34+ const error = new Error (
35+ `Marketplace '${ marketplaceName } ' not found in Claude Code. ` +
36+ `Available marketplaces: ${ Object . keys ( claudeMarketplaces ) . join ( ', ' ) } ` ,
37+ ) ;
4438 defaultIO . logError ( error . message ) ;
4539 throw error ;
4640 }
4741
48- const marketplacePath = await resolveMarketplacePath ( marketplaceName , marketplace , cwd , {
49- dryRun : cmd . dryRun ,
50- } ) ;
42+ const marketplacePath = marketplace . path ;
5143
5244 if ( ! marketplacePath ) {
53- const error = new Error ( `Marketplace '${ marketplaceName } ' has no path/url configured ` ) ;
45+ const error = new Error ( `Marketplace '${ marketplaceName } ' has no path` ) ;
5446 defaultIO . logError ( error . message ) ;
5547 throw error ;
5648 }
5749
58- // In dry-run mode, skip validation for git/url marketplaces that haven't been cached yet.
59- // Directory marketplaces can still be validated since they exist locally.
60- const isDirectoryMarketplace = marketplace . source === 'directory' ;
61- const shouldSkipValidation = cmd . dryRun && ! isDirectoryMarketplace ;
62-
6350 let manifest : Awaited < ReturnType < typeof loadMarketplaceManifest > > = null ;
6451 let pluginPath : string | null = null ;
6552
66- if ( ! shouldSkipValidation ) {
53+ if ( ! cmd . dryRun ) {
6754 manifest = await loadMarketplaceManifest ( marketplacePath , getMarketplaceType ( marketplaceName ) ) ;
6855
6956 // Resolve plugin path: try manifest first, then search recursively if needed
7057 pluginPath = await resolvePluginPath ( marketplacePath , pluginName , manifest ) ;
7158
7259 if ( ! ( await fileExists ( pluginPath ) ) ) {
7360 const error = new Error (
74- `Plugin '${ pluginName } ' not found in marketplace '${ marketplaceName } '. ` +
75- `Checked path: ${ pluginPath } . ` +
76- `If the plugin is in a nested directory, use the full path shown in search results.` ,
61+ `Plugin '${ pluginName } ' not found in marketplace '${ marketplaceName } '. ` + `Checked path: ${ pluginPath } .` ,
7762 ) ;
7863 defaultIO . logError ( error . message ) ;
7964 throw error ;
@@ -90,27 +75,11 @@ export async function pluginInstall(options: unknown): Promise<void> {
9075 }
9176 }
9277
93- const isAlreadyEnabled = config . plugins [ cmd . pluginId ] ?. enabled ;
94-
95- if ( isAlreadyEnabled && ! cmd . force ) {
96- defaultIO . logInfo ( `Plugin '${ cmd . pluginId } ' is already installed` ) ;
97- return ;
98- }
99-
10078 if ( cmd . dryRun ) {
101- defaultIO . logInfo ( `[DRY RUN] Would enable plugin '${ cmd . pluginId } ' in ${ configName } ` ) ;
102- defaultIO . logInfo ( `[DRY RUN] Would sync ${ cmd . pluginId } to .cursor/` ) ;
79+ defaultIO . logInfo ( `[DRY RUN] Would sync ${ cmd . pluginId } to .cursor/skills/` ) ;
10380 return ;
10481 }
10582
106- const targetConfig = await loadTargetConfig ( cwd , cmd . local ) ;
107- const updatedConfig = merge ( { } , targetConfig , {
108- plugins : { [ cmd . pluginId ] : { enabled : true } } ,
109- } ) ;
110-
111- await saveConfig ( cwd , updatedConfig , cmd . local ) ;
112- defaultIO . logSuccess ( `Enabled plugin '${ cmd . pluginId } ' in ${ configName } ` ) ;
113-
11483 // pluginPath is guaranteed to be set here since we skip validation only in dry-run mode,
11584 // and dry-run mode returns early above
11685 if ( ! pluginPath ) {
@@ -120,10 +89,21 @@ export async function pluginInstall(options: unknown): Promise<void> {
12089 const cursorDir = join ( cwd , DIR_CURSOR ) ;
12190 const isMetaPluginCheck = isMetaPlugin ( marketplacePath , pluginPath , manifest ) ;
12291
123- const syncResult =
124- isMetaPluginCheck && manifest
125- ? await syncMetaPluginToCursor ( marketplacePath , manifest , pluginName , marketplaceName , cursorDir )
126- : await syncPluginToCursor ( pluginPath , marketplaceName , pluginName , cursorDir ) ;
92+ // Check if it's a meta-plugin with skills defined in the manifest
93+ let syncResult ;
94+ if ( isMetaPluginCheck && manifest ) {
95+ const pluginEntry = manifest . plugins . find ( ( p ) => p . name === pluginName ) ;
96+ if ( pluginEntry && pluginEntry . skills && pluginEntry . skills . length > 0 ) {
97+ // True meta-plugin with skills in manifest
98+ syncResult = await syncMetaPluginToCursor ( marketplacePath , manifest , pluginName , marketplaceName , cursorDir ) ;
99+ } else {
100+ // Meta-plugin path but skills are in plugin.json (not in marketplace manifest)
101+ // Fall back to regular plugin sync
102+ syncResult = await syncPluginToCursor ( pluginPath , marketplaceName , pluginName , cursorDir ) ;
103+ }
104+ } else {
105+ syncResult = await syncPluginToCursor ( pluginPath , marketplaceName , pluginName , cursorDir ) ;
106+ }
127107
128108 const summary = formatSyncResult ( syncResult ) ;
129109
0 commit comments