@@ -23,10 +23,33 @@ const ClaudeMarketplaceEntrySchema = z.object({
2323 enabled : z . boolean ( ) . optional ( ) ,
2424} ) ;
2525
26- const ClaudeKnownMarketplacesSchema = z . object ( {
27- marketplaces : z . array ( ClaudeMarketplaceEntrySchema ) ,
26+ /**
27+ * Schema for Claude Code's actual marketplace format (object with marketplace names as keys)
28+ */
29+ const ClaudeMarketplaceObjectEntrySchema = z . object ( {
30+ source : z
31+ . object ( {
32+ source : z . string ( ) ,
33+ repo : z . string ( ) . optional ( ) ,
34+ url : z . string ( ) . optional ( ) ,
35+ branch : z . string ( ) . optional ( ) ,
36+ } )
37+ . or ( z . string ( ) )
38+ . optional ( ) ,
39+ installLocation : z . string ( ) . optional ( ) ,
40+ lastUpdated : z . string ( ) . optional ( ) ,
41+ path : z . string ( ) . optional ( ) ,
42+ url : z . string ( ) . optional ( ) ,
43+ branch : z . string ( ) . optional ( ) ,
44+ enabled : z . boolean ( ) . optional ( ) ,
2845} ) ;
2946
47+ /**
48+ * Schema for Claude Code's known_marketplaces.json
49+ * Claude Code uses object format: { "marketplace-name": { ... } }
50+ */
51+ const ClaudeKnownMarketplacesSchema = z . record ( z . string ( ) , ClaudeMarketplaceObjectEntrySchema ) ;
52+
3053/**
3154 * Schema for Claude Code's installed_plugins.json
3255 */
@@ -53,6 +76,8 @@ const ClaudeCodeConfigSchema = z
5376 . loose ( ) ;
5477
5578export type ClaudeMarketplaceEntry = z . infer < typeof ClaudeMarketplaceEntrySchema > ;
79+ export type ClaudeMarketplaceObjectEntry = z . infer < typeof ClaudeMarketplaceObjectEntrySchema > ;
80+ export type ClaudeKnownMarketplaces = z . infer < typeof ClaudeKnownMarketplacesSchema > ;
5681export type ClaudeInstalledPlugin = z . infer < typeof ClaudeInstalledPluginSchema > ;
5782export type ClaudeCodeConfig = z . infer < typeof ClaudeCodeConfigSchema > ;
5883
@@ -78,14 +103,15 @@ export async function isClaudeCodeInstalled(): Promise<boolean> {
78103
79104/**
80105 * Read Claude Code's known_marketplaces.json
106+ * Returns Claude Code's format as-is: { "marketplace-name": { ... } }
81107 */
82- export async function readClaudeCodeMarketplaces ( ) : Promise < ClaudeMarketplaceEntry [ ] > {
108+ export async function readClaudeCodeMarketplaces ( ) : Promise < ClaudeKnownMarketplaces > {
83109 try {
84110 const claudePluginsDir = getClaudeCodePluginsDir ( ) ;
85111 const marketplacesPath = join ( claudePluginsDir , FILE_CLAUDE_KNOWN_MARKETPLACES ) ;
86112
87113 if ( ! ( await fileExists ( marketplacesPath ) ) ) {
88- return [ ] ;
114+ return { } ;
89115 }
90116
91117 const file = Bun . file ( marketplacesPath ) ;
@@ -95,18 +121,18 @@ export async function readClaudeCodeMarketplaces(): Promise<ClaudeMarketplaceEnt
95121 if ( ! result . success ) {
96122 console . warn ( '\n⚠️ AIPM: Failed to parse Claude Code marketplaces' ) ;
97123 console . warn ( ` File: ${ marketplacesPath } ` ) ;
98- console . warn ( ` Error: ${ result . error . message } ` ) ;
124+ console . warn ( ` Error: ${ JSON . stringify ( result . error . issues , null , 2 ) } ` ) ;
99125 console . warn ( ' This might be due to a Claude Code format change.' ) ;
100126 console . warn ( ' Please report this at: https://github.com/TrogonStack/aipm/discussions/categories/buggy\n' ) ;
101- return [ ] ;
127+ return { } ;
102128 }
103129
104- return result . data . marketplaces ;
130+ return result . data ;
105131 } catch ( error ) {
106132 console . warn ( '\n⚠️ AIPM: Failed to read Claude Code marketplaces' ) ;
107133 console . warn ( ` Error: ${ error } ` ) ;
108134 console . warn ( ' Please report this at: https://github.com/TrogonStack/aipm/discussions/categories/buggy\n' ) ;
109- return [ ] ;
135+ return { } ;
110136 }
111137}
112138
@@ -180,34 +206,141 @@ export async function readClaudeCodeConfig(): Promise<ClaudeCodeConfig | null> {
180206
181207/**
182208 * Get the full path to a Claude Code marketplace
209+ *
210+ * Resolves relative paths relative to Claude Code's plugins directory.
211+ * Returns absolute paths as-is (supports both POSIX and Windows paths).
212+ * For git/url sources without a path, returns the expected cache location.
213+ *
214+ * @param marketplaceName - The name of the marketplace
215+ * @param marketplaceConfig - The Claude Code marketplace configuration
216+ * @returns The resolved absolute path to the marketplace
183217 */
184- export function getClaudeCodeMarketplacePath ( marketplace : ClaudeMarketplaceEntry ) : string {
218+ export function getClaudeCodeMarketplacePath (
219+ marketplaceName : string ,
220+ marketplaceConfig : ClaudeMarketplaceObjectEntry ,
221+ ) : string {
185222 const claudePluginsDir = getClaudeCodePluginsDir ( ) ;
186223
187- if ( marketplace . source === 'directory' && marketplace . path ) {
224+ // Use installLocation or path if available
225+ const marketplacePath = marketplaceConfig . installLocation || marketplaceConfig . path ;
226+ if ( marketplacePath ) {
188227 // Check if path is absolute on either Windows or POSIX systems
189- // This allows Claude Code configs to work cross-platform
190- if ( path . isAbsolute ( marketplace . path ) || path . win32 . isAbsolute ( marketplace . path ) ) {
191- return marketplace . path ;
228+ if ( path . isAbsolute ( marketplacePath ) || path . win32 . isAbsolute ( marketplacePath ) ) {
229+ return marketplacePath ;
192230 }
193- return join ( claudePluginsDir , 'marketplaces' , marketplace . path ) ;
231+ return join ( claudePluginsDir , 'marketplaces' , marketplacePath ) ;
194232 }
195233
196234 // For git/url sources, Claude Code caches them in the marketplaces directory
197- // This assumes Claude Code has already cloned/synced the marketplace
198- return join ( claudePluginsDir , 'marketplaces' , marketplace . name ) ;
235+ return join ( claudePluginsDir , 'marketplaces' , marketplaceName ) ;
236+ }
237+
238+ /**
239+ * Extract source info from Claude Code marketplace config
240+ * Validates and normalizes the source type
241+ */
242+ function extractSourceInfo ( source : ClaudeMarketplaceObjectEntry [ 'source' ] ) : {
243+ source : 'directory' | 'git' | 'url' ;
244+ url ?: string ;
245+ branch ?: string ;
246+ } {
247+ if ( ! source ) {
248+ return { source : 'directory' } ;
249+ }
250+
251+ if ( typeof source === 'string' ) {
252+ // Validate source type
253+ if ( source === 'directory' || source === 'git' || source === 'url' ) {
254+ return { source } ;
255+ }
256+ // Default to directory for unknown string values
257+ return { source : 'directory' } ;
258+ }
259+
260+ // Handle nested source object
261+ if ( source . source === 'github' && source . repo ) {
262+ return {
263+ source : 'git' ,
264+ url : `https://github.com/${ source . repo } .git` ,
265+ branch : source . branch ,
266+ } ;
267+ }
268+
269+ if ( source . url ) {
270+ const sourceType = source . url . startsWith ( 'http' ) ? 'url' : 'git' ;
271+ return {
272+ source : sourceType ,
273+ url : source . url ,
274+ branch : source . branch ,
275+ } ;
276+ }
277+
278+ return { source : 'directory' } ;
279+ }
280+
281+ /**
282+ * Determine path from Claude Code marketplace config
283+ */
284+ function determinePath ( config : ClaudeMarketplaceObjectEntry ) : string | undefined {
285+ return config . installLocation || config . path ;
286+ }
287+
288+ /**
289+ * Apply fallback URL/branch from top-level config if not already set
290+ * Determines final source type based on available information
291+ */
292+ function applyFallbackUrlBranch (
293+ sourceInfo : ReturnType < typeof extractSourceInfo > ,
294+ config : ClaudeMarketplaceObjectEntry ,
295+ ) {
296+ const url = sourceInfo . url || config . url ;
297+ const branch = sourceInfo . branch || config . branch ;
298+
299+ // Determine source type: if we got URL from fallback and source was directory, infer from URL
300+ const source =
301+ ! sourceInfo . url && url && sourceInfo . source === 'directory'
302+ ? url . startsWith ( 'http' )
303+ ? ( 'url' as const )
304+ : ( 'git' as const )
305+ : sourceInfo . source ;
306+
307+ return { source, url, branch } ;
199308}
200309
201310/**
202311 * Convert Claude Code marketplace to AIPM marketplace format
312+ * Resolves paths relative to Claude Code's plugins directory
203313 */
204- export function convertClaudeMarketplaceToAIPM ( marketplace : ClaudeMarketplaceEntry ) {
205- const path = getClaudeCodeMarketplacePath ( marketplace ) ;
314+ export function convertClaudeMarketplaceToAIPM (
315+ marketplaceName : string ,
316+ marketplaceConfig : ClaudeMarketplaceObjectEntry ,
317+ ) {
318+ // If installLocation exists, treat as directory (already cloned)
319+ if ( marketplaceConfig . installLocation ) {
320+ return {
321+ source : 'directory' ,
322+ path : marketplaceConfig . installLocation ,
323+ } ;
324+ }
325+
326+ // Extract source info and apply fallbacks
327+ const sourceInfo = extractSourceInfo ( marketplaceConfig . source ) ;
328+ const path = determinePath ( marketplaceConfig ) ;
329+ const finalSourceInfo = applyFallbackUrlBranch ( sourceInfo , marketplaceConfig ) ;
330+
331+ // Return AIPM format
332+ if ( finalSourceInfo . source === 'directory' && path ) {
333+ // Resolve path relative to Claude Code's plugins directory
334+ const resolvedPath = getClaudeCodeMarketplacePath ( marketplaceName , marketplaceConfig ) ;
335+ return {
336+ source : 'directory' ,
337+ path : resolvedPath ,
338+ } ;
339+ }
206340
207341 return {
208- source : marketplace . source ,
209- url : marketplace . url ,
210- path : marketplace . source === 'directory' ? path : undefined ,
211- branch : marketplace . branch ,
342+ source : finalSourceInfo . source ,
343+ url : finalSourceInfo . url ,
344+ branch : finalSourceInfo . branch ,
212345 } ;
213346}
0 commit comments