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' ;
77import { loadTargetConfig , saveConfig } from '../helpers/aipm-config' ;
8+ import { isClaudeCodeInstalled } from '../helpers/claude-code-config' ;
89import { fileExists } from '../helpers/fs' ;
910import { resolveMarketplacePath } from '../helpers/git' ;
1011import { defaultIO } from '../helpers/io' ;
@@ -15,9 +16,7 @@ import { formatSyncResult, syncMetaPluginToCursor, syncPluginToCursor } from '..
1516const PluginInstallOptionsSchema = z . object ( {
1617 pluginId : z . string ( ) . min ( 1 ) ,
1718 cwd : z . string ( ) . optional ( ) ,
18- local : z . boolean ( ) . optional ( ) ,
1919 dryRun : z . boolean ( ) . optional ( ) ,
20- force : z . boolean ( ) . optional ( ) ,
2120} ) ;
2221
2322export async function pluginInstall ( options : unknown ) : Promise < void > {
@@ -26,54 +25,92 @@ export async function pluginInstall(options: unknown): Promise<void> {
2625 const cwd = cmd . cwd || process . cwd ( ) ;
2726
2827 try {
29- const { config , sources } = await loadPluginsConfig ( cwd ) ;
28+ const { pluginName , marketplaceName } = parsePluginId ( cmd . pluginId ) ;
3029
31- if ( ! sources . project && ! sources . local ) {
32- const error = new Error ( getNotInitializedMessage ( ) ) ;
33- defaultIO . logError ( error . message ) ;
34- throw error ;
30+ // Load AIPM config if available (optional for zero-config Claude Code mode)
31+ let config : any = { } ;
32+ let aipmInitialized = false ;
33+ try {
34+ const loaded = await loadPluginsConfig ( cwd ) ;
35+ config = loaded . config ;
36+ aipmInitialized = true ;
37+ } catch {
38+ // AIPM not initialized - this is OK for Claude Code zero-config mode
39+ config = { marketplaces : { } , plugins : { } } ;
3540 }
36- const configName = cmd . local ? getConfigPath ( FILE_AIPM_CONFIG_LOCAL ) : getConfigPath ( FILE_AIPM_CONFIG ) ;
3741
38- const { pluginName , marketplaceName } = parsePluginId ( cmd . pluginId ) ;
42+ const aipmMarketplaces : Record < string , any > = config . marketplaces || { } ;
3943
40- const marketplace = config . marketplaces [ marketplaceName ] ;
44+ // Check if plugin is already installed
45+ if ( aipmInitialized && config . plugins [ cmd . pluginId ] ) {
46+ defaultIO . logInfo ( `Plugin '${ cmd . pluginId } ' is already installed` ) ;
47+ return ;
48+ }
49+
50+ const claudeMarketplaces = await loadClaudeCodeMarketplaces ( ) ;
51+ const allMarketplaces = merge ( { } , claudeMarketplaces , aipmMarketplaces ) ;
52+ const marketplace = allMarketplaces [ marketplaceName ] ;
4153
4254 if ( ! marketplace ) {
43- const error = new Error ( `Marketplace '${ marketplaceName } ' not found. Add it first with 'marketplace add'.` ) ;
55+ const availableMarketplaces = Object . keys ( allMarketplaces ) ;
56+
57+ let errorMessage : string ;
58+ if ( ! availableMarketplaces . includes ( marketplaceName ) && marketplaceName . startsWith ( 'claude/' ) ) {
59+ const claudeCodeInstalled = await isClaudeCodeInstalled ( ) ;
60+ if ( ! claudeCodeInstalled ) {
61+ errorMessage = `Claude Code is not installed. Install Claude Code to use Claude Code marketplaces like '${ marketplaceName } '.` ;
62+ } else {
63+ errorMessage =
64+ `Marketplace '${ marketplaceName } ' not found in Claude Code. ` +
65+ `Available marketplaces: ${ availableMarketplaces . join ( ', ' ) } ` ;
66+ }
67+ } else if ( availableMarketplaces . length === 0 ) {
68+ errorMessage = `Marketplace '${ marketplaceName } ' not found. No marketplaces available.` ;
69+ } else {
70+ errorMessage =
71+ `Marketplace '${ marketplaceName } ' not found. ` +
72+ `Available marketplaces: ${ availableMarketplaces . join ( ', ' ) } ` ;
73+ }
74+
75+ const error = new Error ( errorMessage ) ;
4476 defaultIO . logError ( error . message ) ;
4577 throw error ;
4678 }
4779
48- const marketplacePath = await resolveMarketplacePath ( marketplaceName , marketplace , cwd , {
49- dryRun : cmd . dryRun ,
50- } ) ;
80+ let marketplacePath : string | null = null ;
81+
82+ // For git/url sources, resolve the path (clone/download if needed)
83+ if ( marketplace . source && marketplace . source !== 'directory' ) {
84+ marketplacePath = await resolveMarketplacePath ( marketplaceName , marketplace , cwd , {
85+ dryRun : cmd . dryRun ,
86+ } ) ;
87+ } else {
88+ // For directory sources, use the path directly
89+ marketplacePath = marketplace . path ;
90+ }
5191
5292 if ( ! marketplacePath ) {
5393 const error = new Error ( `Marketplace '${ marketplaceName } ' has no path/url configured` ) ;
5494 defaultIO . logError ( error . message ) ;
5595 throw error ;
5696 }
5797
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-
6398 let manifest : Awaited < ReturnType < typeof loadMarketplaceManifest > > = null ;
6499 let pluginPath : string | null = null ;
65100
66- if ( ! shouldSkipValidation ) {
101+ // Always validate for directory sources; skip expensive operations for git/url in dry-run
102+ const isDirectorySource = ! marketplace . source || marketplace . source === 'directory' ;
103+ const shouldValidate = isDirectorySource || ! cmd . dryRun ;
104+
105+ if ( shouldValidate ) {
67106 manifest = await loadMarketplaceManifest ( marketplacePath , getMarketplaceType ( marketplaceName ) ) ;
68107
69108 // Resolve plugin path: try manifest first, then search recursively if needed
70109 pluginPath = await resolvePluginPath ( marketplacePath , pluginName , manifest ) ;
71110
72111 if ( ! ( await fileExists ( pluginPath ) ) ) {
73112 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.` ,
113+ `Plugin '${ pluginName } ' not found in marketplace '${ marketplaceName } '. ` + `Checked path: ${ pluginPath } .` ,
77114 ) ;
78115 defaultIO . logError ( error . message ) ;
79116 throw error ;
@@ -90,44 +127,51 @@ export async function pluginInstall(options: unknown): Promise<void> {
90127 }
91128 }
92129
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-
100130 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/` ) ;
131+ defaultIO . logInfo ( `[DRY RUN] Would sync ${ cmd . pluginId } to .cursor/skills/` ) ;
103132 return ;
104133 }
105134
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
135+ // For git/url sources in dry-run, we skip validation so pluginPath won't be set
136+ // In this case, we can't perform the sync, so we already returned above
116137 if ( ! pluginPath ) {
117138 throw new Error ( `Plugin path not resolved for '${ pluginName } '` ) ;
118139 }
119140
120141 const cursorDir = join ( cwd , DIR_CURSOR ) ;
121142 const isMetaPluginCheck = isMetaPlugin ( marketplacePath , pluginPath , manifest ) ;
122143
123- const syncResult =
124- isMetaPluginCheck && manifest
125- ? await syncMetaPluginToCursor ( marketplacePath , manifest , pluginName , marketplaceName , cursorDir )
126- : await syncPluginToCursor ( pluginPath , marketplaceName , pluginName , cursorDir ) ;
144+ // Check if it's a meta-plugin with skills defined in the manifest
145+ let syncResult ;
146+ if ( isMetaPluginCheck && manifest ) {
147+ const pluginEntry = manifest . plugins . find ( ( p ) => p . name === pluginName ) ;
148+ if ( pluginEntry && pluginEntry . skills && pluginEntry . skills . length > 0 ) {
149+ // True meta-plugin with skills in manifest
150+ syncResult = await syncMetaPluginToCursor ( marketplacePath , manifest , pluginName , marketplaceName , cursorDir ) ;
151+ } else {
152+ // Meta-plugin path but skills are in plugin.json (not in marketplace manifest)
153+ // Fall back to regular plugin sync
154+ syncResult = await syncPluginToCursor ( pluginPath , marketplaceName , pluginName , cursorDir ) ;
155+ }
156+ } else {
157+ syncResult = await syncPluginToCursor ( pluginPath , marketplaceName , pluginName , cursorDir ) ;
158+ }
159+
160+ // Save plugin to AIPM config if initialized (enables sync and uninstall)
161+ if ( aipmInitialized ) {
162+ const targetConfig = await loadTargetConfig ( cwd , false ) ;
163+ const updatedConfig = merge ( { } , targetConfig , {
164+ plugins : { [ cmd . pluginId ] : { enabled : true } } ,
165+ } ) ;
166+ await saveConfig ( cwd , updatedConfig , false ) ;
167+ }
127168
128169 const summary = formatSyncResult ( syncResult ) ;
129170
130171 defaultIO . logSuccess ( `Installed ${ cmd . pluginId } ` ) ;
172+ if ( aipmInitialized ) {
173+ defaultIO . logSuccess ( `Added plugin '${ cmd . pluginId } ' to .aipm/config.json` ) ;
174+ }
131175 console . log ( `\n✨ Plugin '${ cmd . pluginId } ' installed successfully! (${ summary } )\n` ) ;
132176 } catch ( error : unknown ) {
133177 const message = getErrorMessage ( error ) ;
0 commit comments