Skip to content

Commit 609727e

Browse files
authored
fix: claude code marketplace schema (#29)
Signed-off-by: Yordis Prieto <[email protected]>
1 parent 512639f commit 609727e

19 files changed

+523
-256
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"aipm": "./src/cli.ts"
1919
},
2020
"scripts": {
21+
"ci:all": "bun run format:check && bun run typecheck && bun run test",
2122
"test": "bun test",
2223
"test:coverage": "bun test --coverage",
2324
"typecheck": "tsc --noEmit",

src/commands/info.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { loadPluginsConfig } from '../config/loader';
55
import { DirectoryNotFoundError, isNodeError, PluginNotFoundError } from '../errors';
66
import { resolveMarketplacePath } from '../helpers/git';
77
import { defaultIO } from '../helpers/io';
8-
import { getPluginSourcePath, loadMarketplaceManifest } from '../helpers/marketplace';
8+
import { getMarketplaceType, getPluginSourcePath, loadMarketplaceManifest } from '../helpers/marketplace';
99
import { loadPluginManifest } from '../helpers/plugin';
1010

1111
const InfoOptionsSchema = z.object({
@@ -52,7 +52,7 @@ export async function info(options: unknown): Promise<void> {
5252
return;
5353
}
5454

55-
const marketplaceManifest = await loadMarketplaceManifest(marketplacePath);
55+
const marketplaceManifest = await loadMarketplaceManifest(marketplacePath, getMarketplaceType(marketplaceName));
5656
const pluginPath = getPluginSourcePath(marketplacePath, pluginName, marketplaceManifest);
5757

5858
let pluginStats;

src/commands/plugin-install.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import { DIR_CURSOR } from '../constants';
55
import { fileExists, writeJsonFile } from '../helpers/fs';
66
import { resolveMarketplacePath } from '../helpers/git';
77
import { defaultIO } from '../helpers/io';
8-
import { getPluginSourcePath, isPluginInManifest, loadMarketplaceManifest } from '../helpers/marketplace';
8+
import {
9+
getMarketplaceType,
10+
getPluginSourcePath,
11+
isPluginInManifest,
12+
loadMarketplaceManifest,
13+
} from '../helpers/marketplace';
914
import { validatePluginStructure } from '../helpers/plugin';
1015
import { formatSyncResult, syncPluginToCursor } from '../helpers/sync-strategy';
1116
import { PluginsConfigSchema } from '../schema';
@@ -67,7 +72,9 @@ export async function pluginInstall(options: unknown): Promise<void> {
6772
throw error;
6873
}
6974

70-
const manifest = !cmd.dryRun ? await loadMarketplaceManifest(marketplacePath) : null;
75+
const manifest = !cmd.dryRun
76+
? await loadMarketplaceManifest(marketplacePath, getMarketplaceType(marketplaceName))
77+
: null;
7178

7279
if (!isPluginInManifest(pluginName, manifest)) {
7380
const error = new Error(`Plugin '${pluginName}' not found in marketplace '${marketplaceName}'`);

src/commands/plugin-search.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { getConfigPaths, loadPluginsConfig } from '../config/loader';
33
import { fileExists } from '../helpers/fs';
44
import { resolveMarketplacePath } from '../helpers/git';
55
import { defaultIO } from '../helpers/io';
6-
import { getAvailablePlugins, loadMarketplaceManifest } from '../helpers/marketplace';
6+
import { getAvailablePlugins, getMarketplaceType, loadMarketplaceManifest } from '../helpers/marketplace';
77

88
const PluginSearchOptionsSchema = z.object({
99
query: z.string().optional(),
@@ -48,7 +48,7 @@ export async function pluginSearch(options: unknown): Promise<void> {
4848
continue;
4949
}
5050

51-
const manifest = await loadMarketplaceManifest(marketplacePath);
51+
const manifest = await loadMarketplaceManifest(marketplacePath, getMarketplaceType(marketplaceName));
5252
const availablePlugins = await getAvailablePlugins(marketplacePath, manifest);
5353

5454
for (const pluginName of availablePlugins) {

src/commands/plugin-update.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import { DIR_CURSOR } from '../constants';
55
import { fileExists } from '../helpers/fs';
66
import { resolveMarketplacePath } from '../helpers/git';
77
import { defaultIO } from '../helpers/io';
8-
import { getPluginSourcePath, isPluginInManifest, loadMarketplaceManifest } from '../helpers/marketplace';
8+
import {
9+
getMarketplaceType,
10+
getPluginSourcePath,
11+
isPluginInManifest,
12+
loadMarketplaceManifest,
13+
} from '../helpers/marketplace';
914
import { validatePluginStructure } from '../helpers/plugin';
1015
import { formatSyncResult, syncPluginToCursor } from '../helpers/sync-strategy';
1116

@@ -67,7 +72,9 @@ export async function pluginUpdate(options: unknown): Promise<void> {
6772
throw error;
6873
}
6974

70-
const manifest = !cmd.dryRun ? await loadMarketplaceManifest(marketplacePath) : null;
75+
const manifest = !cmd.dryRun
76+
? await loadMarketplaceManifest(marketplacePath, getMarketplaceType(marketplaceName))
77+
: null;
7178

7279
if (!isPluginInManifest(pluginName, manifest)) {
7380
const error = new Error(`Plugin '${pluginName}' not found in marketplace '${marketplaceName}'`);

src/commands/sync.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { DIR_CURSOR, DIR_MARKETPLACE } from '../constants';
66
import { ensureDir, fileExists } from '../helpers/fs';
77
import { resolveMarketplacePath } from '../helpers/git';
88
import { defaultIO } from '../helpers/io';
9-
import { getPluginSourcePath, isPluginInManifest, loadMarketplaceManifest } from '../helpers/marketplace';
9+
import {
10+
getMarketplaceType,
11+
getPluginSourcePath,
12+
isPluginInManifest,
13+
loadMarketplaceManifest,
14+
} from '../helpers/marketplace';
1015
import { formatSyncResult, syncPluginToCursor } from '../helpers/sync-strategy';
1116

1217
const SyncOptionsSchema = z.object({
@@ -100,7 +105,9 @@ export async function sync(options: SyncOptions = {}): Promise<void> {
100105
continue;
101106
}
102107

103-
const manifest = !cmd.dryRun ? await loadMarketplaceManifest(marketplacePath) : null;
108+
const manifest = !cmd.dryRun
109+
? await loadMarketplaceManifest(marketplacePath, getMarketplaceType(marketplaceName))
110+
: null;
104111

105112
if (!isPluginInManifest(pluginName, manifest)) {
106113
defaultIO.logError(`Plugin '${pluginName}' not found in marketplace.json for '${marketplaceName}'`);

src/config/loader.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ export async function loadPluginsConfig(baseDir: string): Promise<PluginsConfig
8585
if (await isClaudeCodeInstalled()) {
8686
const claudeCodeMarketplaces = await readClaudeCodeMarketplaces();
8787

88-
for (const marketplace of claudeCodeMarketplaces) {
89-
const prefixedName = `claude:${marketplace.name}`;
88+
for (const [marketplaceName, marketplaceConfig] of Object.entries(claudeCodeMarketplaces)) {
89+
const prefixedName = `claude:${marketplaceName}`;
9090

9191
if (globalMarketplaces[prefixedName] || config.marketplaces[prefixedName] || localMarketplaces[prefixedName]) {
9292
console.warn(
@@ -95,7 +95,7 @@ export async function loadPluginsConfig(baseDir: string): Promise<PluginsConfig
9595
continue;
9696
}
9797

98-
claudeMarketplaces[prefixedName] = convertClaudeMarketplaceToAIPM(marketplace);
98+
claudeMarketplaces[prefixedName] = convertClaudeMarketplaceToAIPM(marketplaceName, marketplaceConfig);
9999
}
100100
}
101101

src/helpers/claude-code-config.ts

Lines changed: 155 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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

5578
export type ClaudeMarketplaceEntry = z.infer<typeof ClaudeMarketplaceEntrySchema>;
79+
export type ClaudeMarketplaceObjectEntry = z.infer<typeof ClaudeMarketplaceObjectEntrySchema>;
80+
export type ClaudeKnownMarketplaces = z.infer<typeof ClaudeKnownMarketplacesSchema>;
5681
export type ClaudeInstalledPlugin = z.infer<typeof ClaudeInstalledPluginSchema>;
5782
export 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

Comments
 (0)