@@ -23,10 +23,39 @@ 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 that accepts both formats:
49+ * 1. Array format: { marketplaces: [...] }
50+ * 2. Object format: { "marketplace-name": { ... } }
51+ */
52+ const ClaudeKnownMarketplacesSchema = z . union ( [
53+ z . object ( {
54+ marketplaces : z . array ( ClaudeMarketplaceEntrySchema ) ,
55+ } ) ,
56+ z . record ( z . string ( ) , ClaudeMarketplaceObjectEntrySchema ) ,
57+ ] ) ;
58+
3059/**
3160 * Schema for Claude Code's installed_plugins.json
3261 */
@@ -76,6 +105,78 @@ export async function isClaudeCodeInstalled(): Promise<boolean> {
76105 }
77106}
78107
108+ /**
109+ * Normalize Claude Code marketplace data to our standard format
110+ */
111+ function normalizeClaudeMarketplaces ( data : z . infer < typeof ClaudeKnownMarketplacesSchema > ) : ClaudeMarketplaceEntry [ ] {
112+ // Handle array format: { marketplaces: [...] }
113+ if ( 'marketplaces' in data && Array . isArray ( data . marketplaces ) ) {
114+ return data . marketplaces ;
115+ }
116+
117+ // Handle object format: { "marketplace-name": { ... } }
118+ const entries : ClaudeMarketplaceEntry [ ] = [ ] ;
119+ const objectData = data as Record < string , z . infer < typeof ClaudeMarketplaceObjectEntrySchema > > ;
120+
121+ for ( const [ name , config ] of Object . entries ( objectData ) ) {
122+ // Determine source type
123+ let source : 'directory' | 'git' | 'url' = 'directory' ;
124+ let url : string | undefined ;
125+ let branch : string | undefined ;
126+ let path : string | undefined ;
127+
128+ if ( config . source ) {
129+ if ( typeof config . source === 'string' ) {
130+ source = config . source as 'directory' | 'git' | 'url' ;
131+ } else if ( typeof config . source === 'object' ) {
132+ if ( config . source . source === 'github' && config . source . repo ) {
133+ source = 'git' ;
134+ url = `https://github.com/${ config . source . repo } .git` ;
135+ branch = config . source . branch ;
136+ } else if ( config . source . url ) {
137+ source = config . source . url . startsWith ( 'http' ) ? 'url' : 'git' ;
138+ url = config . source . url ;
139+ branch = config . source . branch ;
140+ }
141+ }
142+ }
143+
144+ // Use installLocation or path if available
145+ if ( config . installLocation ) {
146+ path = config . installLocation ;
147+ // If installLocation exists, Claude Code has already cloned/synced the marketplace
148+ // So we should treat it as a directory source, not git/url
149+ source = 'directory' ;
150+ } else if ( config . path ) {
151+ path = config . path ;
152+ }
153+
154+ // Fallback to url/branch from top level if not set (only if we don't have installLocation)
155+ if ( ! path ) {
156+ if ( ! url && config . url ) {
157+ url = config . url ;
158+ if ( source === 'directory' ) {
159+ source = url . startsWith ( 'http' ) ? 'url' : 'git' ;
160+ }
161+ }
162+ if ( ! branch && config . branch ) {
163+ branch = config . branch ;
164+ }
165+ }
166+
167+ entries . push ( {
168+ name,
169+ source,
170+ path,
171+ url,
172+ branch,
173+ enabled : config . enabled ,
174+ } ) ;
175+ }
176+
177+ return entries ;
178+ }
179+
79180/**
80181 * Read Claude Code's known_marketplaces.json
81182 */
@@ -95,13 +196,13 @@ export async function readClaudeCodeMarketplaces(): Promise<ClaudeMarketplaceEnt
95196 if ( ! result . success ) {
96197 console . warn ( '\n⚠️ AIPM: Failed to parse Claude Code marketplaces' ) ;
97198 console . warn ( ` File: ${ marketplacesPath } ` ) ;
98- console . warn ( ` Error: ${ result . error . message } ` ) ;
199+ console . warn ( ` Error: ${ JSON . stringify ( result . error . issues , null , 2 ) } ` ) ;
99200 console . warn ( ' This might be due to a Claude Code format change.' ) ;
100201 console . warn ( ' Please report this at: https://github.com/TrogonStack/aipm/discussions/categories/buggy\n' ) ;
101202 return [ ] ;
102203 }
103204
104- return result . data . marketplaces ;
205+ return normalizeClaudeMarketplaces ( result . data ) ;
105206 } catch ( error ) {
106207 console . warn ( '\n⚠️ AIPM: Failed to read Claude Code marketplaces' ) ;
107208 console . warn ( ` Error: ${ error } ` ) ;
@@ -202,12 +303,20 @@ export function getClaudeCodeMarketplacePath(marketplace: ClaudeMarketplaceEntry
202303 * Convert Claude Code marketplace to AIPM marketplace format
203304 */
204305export function convertClaudeMarketplaceToAIPM ( marketplace : ClaudeMarketplaceEntry ) {
205- const path = getClaudeCodeMarketplacePath ( marketplace ) ;
306+ // If it's a directory source, use the path directly
307+ if ( marketplace . source === 'directory' && marketplace . path ) {
308+ return {
309+ source : 'directory' ,
310+ path : marketplace . path ,
311+ } ;
312+ }
206313
314+ // For git/url sources, return the source info
315+ // Note: If installLocation was set, we already converted it to directory above
207316 return {
208317 source : marketplace . source ,
209318 url : marketplace . url ,
210- path : marketplace . source === 'directory' ? path : undefined ,
319+ path : undefined ,
211320 branch : marketplace . branch ,
212321 } ;
213322}
0 commit comments