Skip to content

Commit 3fa51d9

Browse files
committed
fix: claude code marketplace schema
1 parent 79b3e66 commit 3fa51d9

17 files changed

+361
-83
lines changed

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/helpers/claude-code-config.ts

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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
*/
204305
export 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
}

src/helpers/git.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { rm, writeFile } from 'node:fs/promises';
2+
import * as path from 'node:path';
23
import { join } from 'node:path';
34
import { DIR_CACHE } from '../constants';
45
import type { MarketplaceSource } from '../schema';
@@ -17,8 +18,12 @@ export async function resolveMarketplacePath(
1718
if (!marketplace.path) {
1819
return null;
1920
}
20-
const path = join(cwd, marketplace.path);
21-
return path;
21+
// If path is absolute, use it directly; otherwise join with cwd
22+
if (path.isAbsolute(marketplace.path) || path.win32.isAbsolute(marketplace.path)) {
23+
return marketplace.path;
24+
}
25+
const resolvedPath = join(cwd, marketplace.path);
26+
return resolvedPath;
2227
}
2328

2429
case 'git': {

src/helpers/marketplace.ts

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,83 @@
11
import { readdir, realpath } from 'node:fs/promises';
22
import { join, relative, sep } from 'node:path';
33
import { DIR_CLAUDE_PLUGIN, FILE_MARKETPLACE_MANIFEST } from '../constants';
4-
import type { MarketplaceManifest } from '../schema';
4+
import { isNodeError } from '../errors';
5+
import type { MarketplaceManifest, MarketplaceType } from '../schema';
56
import { MarketplaceManifestSchema } from '../schema';
6-
import { fileExists, readJsonFile } from './fs';
7+
import { fileExists, JsonFileError, readJsonFile } from './fs';
8+
9+
/**
10+
* Determines the marketplace type based on the marketplace name.
11+
* Claude Code marketplaces are prefixed with 'claude:'.
12+
*/
13+
export function getMarketplaceType(marketplaceName: string): MarketplaceType {
14+
return marketplaceName.startsWith('claude:') ? 'claude' : 'aipm';
15+
}
716

8-
export async function loadMarketplaceManifest(marketplacePath: string): Promise<MarketplaceManifest | null> {
17+
export async function loadAipmMarketplaceManifest(marketplacePath: string): Promise<MarketplaceManifest | null> {
918
const manifestPath = join(marketplacePath, FILE_MARKETPLACE_MANIFEST);
1019

1120
try {
12-
const manifest = await readJsonFile(manifestPath, MarketplaceManifestSchema);
13-
return manifest;
14-
} catch (_error) {
21+
return await readJsonFile(manifestPath, MarketplaceManifestSchema);
22+
} catch (error) {
23+
// Handle missing file gracefully
24+
if (isNodeError(error) && error.code === 'ENOENT') {
25+
return null;
26+
}
27+
28+
// Handle JSON parsing/validation errors
29+
if (error instanceof JsonFileError) {
30+
console.warn('\n⚠️ AIPM: Failed to parse marketplace manifest');
31+
console.warn(` File: ${manifestPath}`);
32+
if (error.cause && typeof error.cause === 'object' && 'issues' in error.cause) {
33+
console.warn(` Error: ${JSON.stringify(error.cause.issues, null, 2)}`);
34+
} else if (error.cause) {
35+
console.warn(` Error: ${JSON.stringify(error.cause, null, 2)}`);
36+
}
37+
console.warn(' This might be due to a corrupted or invalid manifest file.');
38+
console.warn(' Please report this at: https://github.com/TrogonStack/aipm/discussions/categories/buggy\n');
39+
}
1540
return null;
1641
}
1742
}
1843

44+
export async function loadClaudeCodeMarketplaceManifest(marketplacePath: string): Promise<MarketplaceManifest | null> {
45+
const claudePluginManifestPath = join(marketplacePath, DIR_CLAUDE_PLUGIN, FILE_MARKETPLACE_MANIFEST);
46+
47+
try {
48+
return await readJsonFile(claudePluginManifestPath, MarketplaceManifestSchema);
49+
} catch (error) {
50+
// Handle missing file gracefully
51+
if (isNodeError(error) && error.code === 'ENOENT') {
52+
return null;
53+
}
54+
55+
// Handle JSON parsing/validation errors
56+
if (error instanceof JsonFileError) {
57+
console.warn('\n⚠️ AIPM: Failed to parse Claude Code marketplace manifest');
58+
console.warn(` File: ${claudePluginManifestPath}`);
59+
if (error.cause && typeof error.cause === 'object' && 'issues' in error.cause) {
60+
console.warn(` Error: ${JSON.stringify(error.cause.issues, null, 2)}`);
61+
} else if (error.cause) {
62+
console.warn(` Error: ${JSON.stringify(error.cause, null, 2)}`);
63+
}
64+
console.warn(' This might be due to a corrupted or invalid manifest file.');
65+
console.warn(' Please report this at: https://github.com/TrogonStack/aipm/discussions/categories/buggy\n');
66+
}
67+
return null;
68+
}
69+
}
70+
71+
export async function loadMarketplaceManifest(
72+
marketplacePath: string,
73+
marketplaceType: MarketplaceType,
74+
): Promise<MarketplaceManifest | null> {
75+
if (marketplaceType === 'claude') {
76+
return await loadClaudeCodeMarketplaceManifest(marketplacePath);
77+
}
78+
return await loadAipmMarketplaceManifest(marketplacePath);
79+
}
80+
1981
export async function fetchRemoteMarketplaceManifest(url: string): Promise<MarketplaceManifest> {
2082
const response = await fetch(url);
2183

src/schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const MarketplaceSourceSchema = z.object({
88
branch: z.string().optional(),
99
});
1010

11+
export const MarketplaceTypeSchema = z.enum(['aipm', 'claude']);
12+
1113
export const MarketplaceOwnerSchema = z.object({
1214
name: z.string(),
1315
email: z.string().optional(),
@@ -70,6 +72,7 @@ export const GlobalMarketplaceConfigSchema = z.object({
7072
});
7173

7274
export type MarketplaceSource = z.infer<typeof MarketplaceSourceSchema>;
75+
export type MarketplaceType = z.infer<typeof MarketplaceTypeSchema>;
7376
export type MarketplaceMetadata = z.infer<typeof MarketplaceMetadataSchema>;
7477
export type MarketplaceOwner = z.infer<typeof MarketplaceOwnerSchema>;
7578
export type MarketplacePluginEntry = z.infer<typeof MarketplacePluginEntrySchema>;

0 commit comments

Comments
 (0)