11import merge from 'lodash.merge' ;
22import { join } from 'node:path' ;
33import { z } from 'zod' ;
4- import { getConfigPath , getNotInitializedMessage , loadPluginsConfig } from '../config/loader' ;
5- import { DIR_CURSOR , FILE_AIPM_CONFIG , FILE_AIPM_CONFIG_LOCAL } from '../constants' ;
4+ import { loadClaudeCodeMarketplaces , loadPluginsConfig } from '../config/loader' ;
5+ import { DIR_CURSOR } from '../constants' ;
66import { getErrorMessage } from '../errors' ;
7- import { loadTargetConfig , saveConfig } from '../helpers/aipm -config' ;
7+ import { isClaudeCodeInstalled } from '../helpers/claude-code -config' ;
88import { fileExists } from '../helpers/fs' ;
99import { resolveMarketplacePath } from '../helpers/git' ;
1010import { defaultIO } from '../helpers/io' ;
@@ -15,9 +15,7 @@ import { formatSyncResult, syncMetaPluginToCursor, syncPluginToCursor } from '..
1515const PluginInstallOptionsSchema = z . object ( {
1616 pluginId : z . string ( ) . min ( 1 ) ,
1717 cwd : z . string ( ) . optional ( ) ,
18- local : z . boolean ( ) . optional ( ) ,
1918 dryRun : z . boolean ( ) . optional ( ) ,
20- force : z . boolean ( ) . optional ( ) ,
2119} ) ;
2220
2321export async function pluginInstall ( options : unknown ) : Promise < void > {
@@ -26,54 +24,86 @@ export async function pluginInstall(options: unknown): Promise<void> {
2624 const cwd = cmd . cwd || process . cwd ( ) ;
2725
2826 try {
29- const { config , sources } = await loadPluginsConfig ( cwd ) ;
27+ const { pluginName , marketplaceName } = parsePluginId ( cmd . pluginId ) ;
3028
31- if ( ! sources . project && ! sources . local ) {
32- const error = new Error ( getNotInitializedMessage ( ) ) ;
33- defaultIO . logError ( error . message ) ;
34- throw error ;
29+ // Load marketplaces from both AIPM config (optional) and Claude Code (auto-discovered)
30+ let aipmMarketplaces : Record < string , any > = { } ;
31+ try {
32+ const config = await loadPluginsConfig ( cwd ) ;
33+ aipmMarketplaces = config . config . marketplaces || { } ;
34+ } catch ( error : unknown ) {
35+ // AIPM config is optional - proceed without it
36+ // Log at info level so users know about config issues but can continue
37+ if ( ! ( error instanceof Error && error . message . includes ( 'not found' ) ) ) {
38+ const message = getErrorMessage ( error ) ;
39+ defaultIO . logInfo ( `ℹ️ Config loading: ${ message } ` ) ;
40+ }
3541 }
36- const configName = cmd . local ? getConfigPath ( FILE_AIPM_CONFIG_LOCAL ) : getConfigPath ( FILE_AIPM_CONFIG ) ;
3742
38- const { pluginName , marketplaceName } = parsePluginId ( cmd . pluginId ) ;
39-
40- const marketplace = config . marketplaces [ marketplaceName ] ;
43+ const claudeMarketplaces = await loadClaudeCodeMarketplaces ( ) ;
44+ const allMarketplaces = merge ( { } , claudeMarketplaces , aipmMarketplaces ) ;
45+ const marketplace = allMarketplaces [ marketplaceName ] ;
4146
4247 if ( ! marketplace ) {
43- const error = new Error ( `Marketplace '${ marketplaceName } ' not found. Add it first with 'marketplace add'.` ) ;
48+ const availableMarketplaces = Object . keys ( allMarketplaces ) ;
49+
50+ let errorMessage : string ;
51+ if ( ! availableMarketplaces . includes ( marketplaceName ) && marketplaceName . startsWith ( 'claude/' ) ) {
52+ const claudeCodeInstalled = await isClaudeCodeInstalled ( ) ;
53+ if ( ! claudeCodeInstalled ) {
54+ errorMessage = `Claude Code is not installed. Install Claude Code to use Claude Code marketplaces like '${ marketplaceName } '.` ;
55+ } else {
56+ errorMessage =
57+ `Marketplace '${ marketplaceName } ' not found in Claude Code. ` +
58+ `Available marketplaces: ${ availableMarketplaces . join ( ', ' ) } ` ;
59+ }
60+ } else if ( availableMarketplaces . length === 0 ) {
61+ errorMessage = `Marketplace '${ marketplaceName } ' not found. No marketplaces available.` ;
62+ } else {
63+ errorMessage =
64+ `Marketplace '${ marketplaceName } ' not found. ` +
65+ `Available marketplaces: ${ availableMarketplaces . join ( ', ' ) } ` ;
66+ }
67+
68+ const error = new Error ( errorMessage ) ;
4469 defaultIO . logError ( error . message ) ;
4570 throw error ;
4671 }
4772
48- const marketplacePath = await resolveMarketplacePath ( marketplaceName , marketplace , cwd , {
49- dryRun : cmd . dryRun ,
50- } ) ;
73+ let marketplacePath : string | null = null ;
74+
75+ // For git/url sources, resolve the path (clone/download if needed)
76+ if ( marketplace . source && marketplace . source !== 'directory' ) {
77+ marketplacePath = await resolveMarketplacePath ( marketplaceName , marketplace , cwd , {
78+ dryRun : cmd . dryRun ,
79+ } ) ;
80+ } else {
81+ // For directory sources, use the path directly
82+ marketplacePath = marketplace . path ;
83+ }
5184
5285 if ( ! marketplacePath ) {
5386 const error = new Error ( `Marketplace '${ marketplaceName } ' has no path/url configured` ) ;
5487 defaultIO . logError ( error . message ) ;
5588 throw error ;
5689 }
5790
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-
6391 let manifest : Awaited < ReturnType < typeof loadMarketplaceManifest > > = null ;
6492 let pluginPath : string | null = null ;
6593
66- if ( ! shouldSkipValidation ) {
94+ // Always validate for directory sources; skip expensive operations for git/url in dry-run
95+ const isDirectorySource = ! marketplace . source || marketplace . source === 'directory' ;
96+ const shouldValidate = isDirectorySource || ! cmd . dryRun ;
97+
98+ if ( shouldValidate ) {
6799 manifest = await loadMarketplaceManifest ( marketplacePath , getMarketplaceType ( marketplaceName ) ) ;
68100
69101 // Resolve plugin path: try manifest first, then search recursively if needed
70102 pluginPath = await resolvePluginPath ( marketplacePath , pluginName , manifest ) ;
71103
72104 if ( ! ( await fileExists ( pluginPath ) ) ) {
73105 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.` ,
106+ `Plugin '${ pluginName } ' not found in marketplace '${ marketplaceName } '. ` + `Checked path: ${ pluginPath } .` ,
77107 ) ;
78108 defaultIO . logError ( error . message ) ;
79109 throw error ;
@@ -90,40 +120,35 @@ export async function pluginInstall(options: unknown): Promise<void> {
90120 }
91121 }
92122
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-
100123 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/` ) ;
124+ defaultIO . logInfo ( `[DRY RUN] Would sync ${ cmd . pluginId } to .cursor/skills/` ) ;
103125 return ;
104126 }
105127
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-
114- // pluginPath is guaranteed to be set here since we skip validation only in dry-run mode,
115- // and dry-run mode returns early above
128+ // For git/url sources in dry-run, we skip validation so pluginPath won't be set
129+ // In this case, we can't perform the sync, so we already returned above
116130 if ( ! pluginPath ) {
117131 throw new Error ( `Plugin path not resolved for '${ pluginName } '` ) ;
118132 }
119133
120134 const cursorDir = join ( cwd , DIR_CURSOR ) ;
121135 const isMetaPluginCheck = isMetaPlugin ( marketplacePath , pluginPath , manifest ) ;
122136
123- const syncResult =
124- isMetaPluginCheck && manifest
125- ? await syncMetaPluginToCursor ( marketplacePath , manifest , pluginName , marketplaceName , cursorDir )
126- : await syncPluginToCursor ( pluginPath , marketplaceName , pluginName , cursorDir ) ;
137+ // Check if it's a meta-plugin with skills defined in the manifest
138+ let syncResult ;
139+ if ( isMetaPluginCheck && manifest ) {
140+ const pluginEntry = manifest . plugins . find ( ( p ) => p . name === pluginName ) ;
141+ if ( pluginEntry && pluginEntry . skills && pluginEntry . skills . length > 0 ) {
142+ // True meta-plugin with skills in manifest
143+ syncResult = await syncMetaPluginToCursor ( marketplacePath , manifest , pluginName , marketplaceName , cursorDir ) ;
144+ } else {
145+ // Meta-plugin path but skills are in plugin.json (not in marketplace manifest)
146+ // Fall back to regular plugin sync
147+ syncResult = await syncPluginToCursor ( pluginPath , marketplaceName , pluginName , cursorDir ) ;
148+ }
149+ } else {
150+ syncResult = await syncPluginToCursor ( pluginPath , marketplaceName , pluginName , cursorDir ) ;
151+ }
127152
128153 const summary = formatSyncResult ( syncResult ) ;
129154
0 commit comments