@portel/photon 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +952 -0
- package/dist/base.d.ts +58 -0
- package/dist/base.d.ts.map +1 -0
- package/dist/base.js +92 -0
- package/dist/base.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1441 -0
- package/dist/cli.js.map +1 -0
- package/dist/dependency-manager.d.ts +49 -0
- package/dist/dependency-manager.d.ts.map +1 -0
- package/dist/dependency-manager.js +165 -0
- package/dist/dependency-manager.js.map +1 -0
- package/dist/loader.d.ts +86 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +612 -0
- package/dist/loader.js.map +1 -0
- package/dist/marketplace-manager.d.ts +261 -0
- package/dist/marketplace-manager.d.ts.map +1 -0
- package/dist/marketplace-manager.js +767 -0
- package/dist/marketplace-manager.js.map +1 -0
- package/dist/path-resolver.d.ts +21 -0
- package/dist/path-resolver.d.ts.map +1 -0
- package/dist/path-resolver.js +71 -0
- package/dist/path-resolver.js.map +1 -0
- package/dist/photon-doc-extractor.d.ts +89 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -0
- package/dist/photon-doc-extractor.js +228 -0
- package/dist/photon-doc-extractor.js.map +1 -0
- package/dist/readme-syncer.d.ts +33 -0
- package/dist/readme-syncer.d.ts.map +1 -0
- package/dist/readme-syncer.js +93 -0
- package/dist/readme-syncer.js.map +1 -0
- package/dist/registry-manager.d.ts +76 -0
- package/dist/registry-manager.d.ts.map +1 -0
- package/dist/registry-manager.js +220 -0
- package/dist/registry-manager.js.map +1 -0
- package/dist/schema-extractor.d.ts +83 -0
- package/dist/schema-extractor.d.ts.map +1 -0
- package/dist/schema-extractor.js +396 -0
- package/dist/schema-extractor.js.map +1 -0
- package/dist/security-scanner.d.ts +52 -0
- package/dist/security-scanner.d.ts.map +1 -0
- package/dist/security-scanner.js +172 -0
- package/dist/security-scanner.js.map +1 -0
- package/dist/server.d.ts +73 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +474 -0
- package/dist/server.js.map +1 -0
- package/dist/template-manager.d.ts +56 -0
- package/dist/template-manager.d.ts.map +1 -0
- package/dist/template-manager.js +509 -0
- package/dist/template-manager.js.map +1 -0
- package/dist/test-client.d.ts +52 -0
- package/dist/test-client.d.ts.map +1 -0
- package/dist/test-client.js +168 -0
- package/dist/test-client.js.map +1 -0
- package/dist/test-marketplace-sources.d.ts +5 -0
- package/dist/test-marketplace-sources.d.ts.map +1 -0
- package/dist/test-marketplace-sources.js +53 -0
- package/dist/test-marketplace-sources.js.map +1 -0
- package/dist/types.d.ts +108 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/version-checker.d.ts +48 -0
- package/dist/version-checker.d.ts.map +1 -0
- package/dist/version-checker.js +128 -0
- package/dist/version-checker.js.map +1 -0
- package/dist/watcher.d.ts +26 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +72 -0
- package/dist/watcher.js.map +1 -0
- package/package.json +79 -0
- package/templates/photon.template.ts +55 -0
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marketplace Manager - Manage multiple MCP marketplaces
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as os from 'os';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import * as crypto from 'crypto';
|
|
9
|
+
const CONFIG_DIR = path.join(os.homedir(), '.photon');
|
|
10
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'marketplaces.json');
|
|
11
|
+
const CACHE_DIR = path.join(CONFIG_DIR, '.cache', 'marketplaces');
|
|
12
|
+
const METADATA_FILE = path.join(CONFIG_DIR, '.metadata.json');
|
|
13
|
+
// Cache is considered stale after 24 hours
|
|
14
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
15
|
+
const DEFAULT_MARKETPLACE = {
|
|
16
|
+
name: 'photons',
|
|
17
|
+
repo: 'portel-dev/photons',
|
|
18
|
+
url: 'https://raw.githubusercontent.com/portel-dev/photons/main',
|
|
19
|
+
sourceType: 'github',
|
|
20
|
+
source: 'portel-dev/photons',
|
|
21
|
+
enabled: true,
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Calculate SHA-256 hash of file content
|
|
25
|
+
*/
|
|
26
|
+
export async function calculateFileHash(filePath) {
|
|
27
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
28
|
+
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
29
|
+
return `sha256:${hash}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Calculate SHA-256 hash of string content
|
|
33
|
+
*/
|
|
34
|
+
export function calculateHash(content) {
|
|
35
|
+
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
36
|
+
return `sha256:${hash}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Read local installation metadata
|
|
40
|
+
*/
|
|
41
|
+
export async function readLocalMetadata() {
|
|
42
|
+
try {
|
|
43
|
+
if (existsSync(METADATA_FILE)) {
|
|
44
|
+
const data = await fs.readFile(METADATA_FILE, 'utf-8');
|
|
45
|
+
return JSON.parse(data);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// File doesn't exist or is invalid
|
|
50
|
+
}
|
|
51
|
+
return { photons: {} };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Write local installation metadata
|
|
55
|
+
*/
|
|
56
|
+
export async function writeLocalMetadata(metadata) {
|
|
57
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
58
|
+
await fs.writeFile(METADATA_FILE, JSON.stringify(metadata, null, 2), 'utf-8');
|
|
59
|
+
}
|
|
60
|
+
export class MarketplaceManager {
|
|
61
|
+
config = { marketplaces: [] };
|
|
62
|
+
async initialize() {
|
|
63
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
64
|
+
await fs.mkdir(CACHE_DIR, { recursive: true });
|
|
65
|
+
if (existsSync(CONFIG_FILE)) {
|
|
66
|
+
const data = await fs.readFile(CONFIG_FILE, 'utf-8');
|
|
67
|
+
this.config = JSON.parse(data);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// Initialize with default marketplace
|
|
71
|
+
this.config = {
|
|
72
|
+
marketplaces: [DEFAULT_MARKETPLACE],
|
|
73
|
+
};
|
|
74
|
+
await this.save();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async save() {
|
|
78
|
+
await fs.writeFile(CONFIG_FILE, JSON.stringify(this.config, null, 2), 'utf-8');
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get all marketplaces
|
|
82
|
+
*/
|
|
83
|
+
getAll() {
|
|
84
|
+
return this.config.marketplaces;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get enabled marketplaces
|
|
88
|
+
*/
|
|
89
|
+
getEnabled() {
|
|
90
|
+
return this.config.marketplaces.filter((m) => m.enabled);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get marketplace by name
|
|
94
|
+
*/
|
|
95
|
+
get(name) {
|
|
96
|
+
return this.config.marketplaces.find((m) => m.name === name);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Parse marketplace source into structured info
|
|
100
|
+
* Supports:
|
|
101
|
+
* 1. GitHub shorthand: username/repo
|
|
102
|
+
* 2. GitHub HTTPS: https://github.com/username/repo[.git]
|
|
103
|
+
* 3. GitHub SSH: git@github.com:username/repo.git
|
|
104
|
+
* 4. Direct URL: https://example.com/photons.json
|
|
105
|
+
* 5. Local path: ./path/to/marketplace or /absolute/path
|
|
106
|
+
*/
|
|
107
|
+
parseMarketplaceSource(input) {
|
|
108
|
+
// Pattern 1: username/repo (GitHub shorthand)
|
|
109
|
+
const shorthandMatch = input.match(/^([a-zA-Z0-9-]+)\/([a-zA-Z0-9-_.]+)$/);
|
|
110
|
+
if (shorthandMatch) {
|
|
111
|
+
const [, username, repo] = shorthandMatch;
|
|
112
|
+
return {
|
|
113
|
+
name: repo,
|
|
114
|
+
repo: input,
|
|
115
|
+
url: `https://raw.githubusercontent.com/${username}/${repo}/main`,
|
|
116
|
+
sourceType: 'github',
|
|
117
|
+
source: input,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// Pattern 2: https://github.com/username/repo[.git] (GitHub HTTPS)
|
|
121
|
+
const httpsMatch = input.match(/^https?:\/\/github\.com\/([a-zA-Z0-9-]+)\/([a-zA-Z0-9-_.]+?)(\.git)?$/);
|
|
122
|
+
if (httpsMatch) {
|
|
123
|
+
const [, username, repo] = httpsMatch;
|
|
124
|
+
return {
|
|
125
|
+
name: repo,
|
|
126
|
+
repo: `${username}/${repo}`,
|
|
127
|
+
url: `https://raw.githubusercontent.com/${username}/${repo}/main`,
|
|
128
|
+
sourceType: 'github',
|
|
129
|
+
source: input,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Pattern 3: git@github.com:username/repo.git (GitHub SSH)
|
|
133
|
+
const sshMatch = input.match(/^git@github\.com:([a-zA-Z0-9-]+)\/([a-zA-Z0-9-_.]+?)(\.git)?$/);
|
|
134
|
+
if (sshMatch) {
|
|
135
|
+
const [, username, repo] = sshMatch;
|
|
136
|
+
const repoName = repo.replace(/\.git$/, '');
|
|
137
|
+
return {
|
|
138
|
+
name: repoName,
|
|
139
|
+
repo: `${username}/${repoName}`,
|
|
140
|
+
url: `https://raw.githubusercontent.com/${username}/${repoName}/main`,
|
|
141
|
+
sourceType: 'git-ssh',
|
|
142
|
+
source: input,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Pattern 4: https://example.com/photons.json (Direct URL)
|
|
146
|
+
if (input.startsWith('http://') || input.startsWith('https://')) {
|
|
147
|
+
// Extract name from URL
|
|
148
|
+
const urlObj = new URL(input);
|
|
149
|
+
const pathParts = urlObj.pathname.split('/');
|
|
150
|
+
const fileName = pathParts[pathParts.length - 1];
|
|
151
|
+
const name = fileName.replace(/\.(json|ts)$/, '') || urlObj.hostname;
|
|
152
|
+
// Base URL is the directory containing the photons.json
|
|
153
|
+
const baseUrl = input.replace(/\/[^/]*$/, '');
|
|
154
|
+
return {
|
|
155
|
+
name,
|
|
156
|
+
repo: '', // Not a repo
|
|
157
|
+
url: baseUrl,
|
|
158
|
+
sourceType: 'url',
|
|
159
|
+
source: input,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Pattern 5: Local filesystem (Unix and Windows paths)
|
|
163
|
+
// Unix: ./path, ../path, /absolute, ~/path
|
|
164
|
+
// Windows: C:\path, D:\Users\..., etc.
|
|
165
|
+
const isLocalPath = input.startsWith('./') ||
|
|
166
|
+
input.startsWith('../') ||
|
|
167
|
+
input.startsWith('/') ||
|
|
168
|
+
input.startsWith('~') ||
|
|
169
|
+
/^[A-Za-z]:[\\/]/.test(input); // Windows drive letter (C:\, D:\, etc.)
|
|
170
|
+
if (isLocalPath) {
|
|
171
|
+
// Resolve to absolute path (handles ~ expansion)
|
|
172
|
+
const absolutePath = path.resolve(input.replace(/^~/, os.homedir()));
|
|
173
|
+
const name = path.basename(absolutePath);
|
|
174
|
+
// Normalize path separators for file:// URL
|
|
175
|
+
// On Windows, path.resolve returns backslashes, but file:// needs forward slashes
|
|
176
|
+
const normalizedPath = absolutePath.replace(/\\/g, '/');
|
|
177
|
+
return {
|
|
178
|
+
name,
|
|
179
|
+
repo: '', // Not a repo
|
|
180
|
+
url: `file://${normalizedPath}`,
|
|
181
|
+
sourceType: 'local',
|
|
182
|
+
source: input,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get next available name with numeric suffix if name already exists
|
|
189
|
+
* e.g., if 'photon-mcps' exists, returns 'photon-mcps-2'
|
|
190
|
+
* if 'photon-mcps' and 'photon-mcps-2' exist, returns 'photon-mcps-3'
|
|
191
|
+
*/
|
|
192
|
+
getUniqueName(baseName) {
|
|
193
|
+
// If base name doesn't exist, use it as-is
|
|
194
|
+
if (!this.get(baseName)) {
|
|
195
|
+
return baseName;
|
|
196
|
+
}
|
|
197
|
+
// Find next available number
|
|
198
|
+
let suffix = 2;
|
|
199
|
+
while (this.get(`${baseName}-${suffix}`)) {
|
|
200
|
+
suffix++;
|
|
201
|
+
}
|
|
202
|
+
return `${baseName}-${suffix}`;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Check if a marketplace with the same source already exists
|
|
206
|
+
*/
|
|
207
|
+
findBySource(source) {
|
|
208
|
+
return this.config.marketplaces.find((m) => m.source === source);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Add a new marketplace
|
|
212
|
+
* Supports:
|
|
213
|
+
* - GitHub: username/repo, https://github.com/username/repo, git@github.com:username/repo.git
|
|
214
|
+
* - Direct URL: https://example.com/photons.json
|
|
215
|
+
* - Local path: ./path/to/marketplace, /absolute/path
|
|
216
|
+
*
|
|
217
|
+
* If a marketplace with the same name already exists, automatically appends a numeric suffix (-2, -3, etc.)
|
|
218
|
+
* If the exact same source already exists, returns the existing marketplace without creating a duplicate.
|
|
219
|
+
*
|
|
220
|
+
* @returns Object with marketplace info and 'added' flag (false if already existed)
|
|
221
|
+
*/
|
|
222
|
+
async add(source) {
|
|
223
|
+
const parsed = this.parseMarketplaceSource(source);
|
|
224
|
+
if (!parsed) {
|
|
225
|
+
throw new Error(`Invalid marketplace source format. Supported formats:
|
|
226
|
+
- GitHub: username/repo
|
|
227
|
+
- GitHub HTTPS: https://github.com/username/repo
|
|
228
|
+
- GitHub SSH: git@github.com:username/repo.git
|
|
229
|
+
- Direct URL: https://example.com/photons.json
|
|
230
|
+
- Local path: ./path/to/marketplace or /absolute/path`);
|
|
231
|
+
}
|
|
232
|
+
// Check if this exact source is already added
|
|
233
|
+
const existing = this.findBySource(parsed.source);
|
|
234
|
+
if (existing) {
|
|
235
|
+
return {
|
|
236
|
+
marketplace: {
|
|
237
|
+
name: existing.name,
|
|
238
|
+
repo: existing.repo,
|
|
239
|
+
url: existing.url,
|
|
240
|
+
sourceType: existing.sourceType,
|
|
241
|
+
source: existing.source,
|
|
242
|
+
},
|
|
243
|
+
added: false,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
// Get unique name (adds numeric suffix if name already exists)
|
|
247
|
+
const uniqueName = this.getUniqueName(parsed.name);
|
|
248
|
+
const finalParsed = { ...parsed, name: uniqueName };
|
|
249
|
+
const marketplace = {
|
|
250
|
+
...finalParsed,
|
|
251
|
+
enabled: true,
|
|
252
|
+
lastUpdated: new Date().toISOString(),
|
|
253
|
+
};
|
|
254
|
+
this.config.marketplaces.push(marketplace);
|
|
255
|
+
await this.save();
|
|
256
|
+
return {
|
|
257
|
+
marketplace: finalParsed,
|
|
258
|
+
added: true,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Remove a marketplace
|
|
263
|
+
*/
|
|
264
|
+
async remove(name) {
|
|
265
|
+
const index = this.config.marketplaces.findIndex((m) => m.name === name);
|
|
266
|
+
if (index === -1) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
// Prevent removing the default marketplace
|
|
270
|
+
if (this.config.marketplaces[index].url === DEFAULT_MARKETPLACE.url) {
|
|
271
|
+
throw new Error('Cannot remove the default photons marketplace');
|
|
272
|
+
}
|
|
273
|
+
this.config.marketplaces.splice(index, 1);
|
|
274
|
+
await this.save();
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Enable/disable a marketplace
|
|
279
|
+
*/
|
|
280
|
+
async setEnabled(name, enabled) {
|
|
281
|
+
const marketplace = this.get(name);
|
|
282
|
+
if (!marketplace) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
marketplace.enabled = enabled;
|
|
286
|
+
await this.save();
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Get cache file path for marketplace
|
|
291
|
+
*/
|
|
292
|
+
getCacheFile(marketplaceName) {
|
|
293
|
+
return path.join(CACHE_DIR, `${marketplaceName}.json`);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Fetch photons.json manifest from various sources
|
|
297
|
+
*/
|
|
298
|
+
async fetchManifest(marketplace) {
|
|
299
|
+
try {
|
|
300
|
+
if (marketplace.sourceType === 'local') {
|
|
301
|
+
// Local filesystem
|
|
302
|
+
const localPath = marketplace.url.replace('file://', '');
|
|
303
|
+
const manifestPath = path.join(localPath, '.marketplace', 'photons.json');
|
|
304
|
+
if (existsSync(manifestPath)) {
|
|
305
|
+
const data = await fs.readFile(manifestPath, 'utf-8');
|
|
306
|
+
return JSON.parse(data);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
else if (marketplace.sourceType === 'url') {
|
|
310
|
+
// Direct URL - the source already points to photons.json
|
|
311
|
+
const response = await fetch(marketplace.source);
|
|
312
|
+
if (response.ok) {
|
|
313
|
+
return await response.json();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
// GitHub sources (github, git-ssh)
|
|
318
|
+
const manifestUrl = `${marketplace.url}/.marketplace/photons.json`;
|
|
319
|
+
const response = await fetch(manifestUrl);
|
|
320
|
+
if (response.ok) {
|
|
321
|
+
return await response.json();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
// Marketplace doesn't have a manifest or fetch failed
|
|
327
|
+
console.error(`Failed to fetch manifest from ${marketplace.name}:`, error);
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Update marketplace cache (fetch and save photons.json manifest)
|
|
333
|
+
*/
|
|
334
|
+
async updateMarketplaceCache(name) {
|
|
335
|
+
const marketplace = this.get(name);
|
|
336
|
+
if (!marketplace) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
const manifest = await this.fetchManifest(marketplace);
|
|
340
|
+
if (manifest) {
|
|
341
|
+
// Save to cache
|
|
342
|
+
const cacheFile = this.getCacheFile(name);
|
|
343
|
+
await fs.writeFile(cacheFile, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
344
|
+
// Update lastUpdated timestamp
|
|
345
|
+
marketplace.lastUpdated = new Date().toISOString();
|
|
346
|
+
await this.save();
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Update all enabled marketplace caches
|
|
353
|
+
*/
|
|
354
|
+
async updateAllCaches() {
|
|
355
|
+
const results = new Map();
|
|
356
|
+
const enabled = this.getEnabled();
|
|
357
|
+
for (const marketplace of enabled) {
|
|
358
|
+
const success = await this.updateMarketplaceCache(marketplace.name);
|
|
359
|
+
results.set(marketplace.name, success);
|
|
360
|
+
}
|
|
361
|
+
return results;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Get cached marketplace manifest
|
|
365
|
+
*/
|
|
366
|
+
async getCachedManifest(marketplaceName) {
|
|
367
|
+
try {
|
|
368
|
+
const cacheFile = this.getCacheFile(marketplaceName);
|
|
369
|
+
if (existsSync(cacheFile)) {
|
|
370
|
+
const data = await fs.readFile(cacheFile, 'utf-8');
|
|
371
|
+
return JSON.parse(data);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// Cache doesn't exist or is invalid
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Get Photon metadata from cached manifest
|
|
381
|
+
*/
|
|
382
|
+
async getPhotonMetadata(photonName) {
|
|
383
|
+
const enabled = this.getEnabled();
|
|
384
|
+
for (const marketplace of enabled) {
|
|
385
|
+
const manifest = await this.getCachedManifest(marketplace.name);
|
|
386
|
+
if (manifest) {
|
|
387
|
+
const photon = manifest.photons.find((p) => p.name === photonName);
|
|
388
|
+
if (photon) {
|
|
389
|
+
return { metadata: photon, marketplace };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Get all Photons with metadata from all enabled marketplaces
|
|
397
|
+
*/
|
|
398
|
+
async getAllPhotons() {
|
|
399
|
+
const photons = new Map();
|
|
400
|
+
const enabled = this.getEnabled();
|
|
401
|
+
for (const marketplace of enabled) {
|
|
402
|
+
const manifest = await this.getCachedManifest(marketplace.name);
|
|
403
|
+
if (manifest) {
|
|
404
|
+
for (const photon of manifest.photons) {
|
|
405
|
+
// First marketplace wins if Photon exists in multiple
|
|
406
|
+
if (!photons.has(photon.name)) {
|
|
407
|
+
photons.set(photon.name, { metadata: photon, marketplace });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return photons;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Get count of available Photons per marketplace
|
|
416
|
+
*/
|
|
417
|
+
async getMarketplaceCounts() {
|
|
418
|
+
const counts = new Map();
|
|
419
|
+
const all = this.getAll();
|
|
420
|
+
for (const marketplace of all) {
|
|
421
|
+
const manifest = await this.getCachedManifest(marketplace.name);
|
|
422
|
+
counts.set(marketplace.name, manifest?.photons.length || 0);
|
|
423
|
+
}
|
|
424
|
+
return counts;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Check if marketplace cache is stale
|
|
428
|
+
*/
|
|
429
|
+
isCacheStale(marketplace) {
|
|
430
|
+
if (!marketplace.lastUpdated) {
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
const lastUpdate = new Date(marketplace.lastUpdated).getTime();
|
|
434
|
+
const now = Date.now();
|
|
435
|
+
return (now - lastUpdate) > CACHE_TTL_MS;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Auto-update stale caches
|
|
439
|
+
* Returns true if any updates were performed
|
|
440
|
+
*/
|
|
441
|
+
async autoUpdateStaleCaches() {
|
|
442
|
+
const enabled = this.getEnabled();
|
|
443
|
+
let updated = false;
|
|
444
|
+
for (const marketplace of enabled) {
|
|
445
|
+
if (this.isCacheStale(marketplace)) {
|
|
446
|
+
const success = await this.updateMarketplaceCache(marketplace.name);
|
|
447
|
+
if (success) {
|
|
448
|
+
updated = true;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return updated;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Try to fetch MCP from all enabled marketplaces
|
|
456
|
+
* Returns content, marketplace info, and metadata (version, hash)
|
|
457
|
+
*/
|
|
458
|
+
async fetchMCP(mcpName) {
|
|
459
|
+
const enabled = this.getEnabled();
|
|
460
|
+
for (const marketplace of enabled) {
|
|
461
|
+
try {
|
|
462
|
+
let content = null;
|
|
463
|
+
if (marketplace.sourceType === 'local') {
|
|
464
|
+
// Local filesystem
|
|
465
|
+
const localPath = marketplace.url.replace('file://', '');
|
|
466
|
+
const mcpPath = path.join(localPath, `${mcpName}.photon.ts`);
|
|
467
|
+
if (existsSync(mcpPath)) {
|
|
468
|
+
content = await fs.readFile(mcpPath, 'utf-8');
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
// Remote fetch (GitHub, URL)
|
|
473
|
+
const url = `${marketplace.url}/${mcpName}.photon.ts`;
|
|
474
|
+
const response = await fetch(url);
|
|
475
|
+
if (response.ok) {
|
|
476
|
+
content = await response.text();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (content) {
|
|
480
|
+
// Try to fetch metadata from manifest
|
|
481
|
+
const manifest = await this.getCachedManifest(marketplace.name);
|
|
482
|
+
const metadata = manifest?.photons.find(p => p.name === mcpName);
|
|
483
|
+
return { content, marketplace, metadata };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
// Try next marketplace
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Fetch version from all enabled marketplaces
|
|
494
|
+
*/
|
|
495
|
+
async fetchVersion(mcpName) {
|
|
496
|
+
const enabled = this.getEnabled();
|
|
497
|
+
for (const marketplace of enabled) {
|
|
498
|
+
try {
|
|
499
|
+
let content = null;
|
|
500
|
+
if (marketplace.sourceType === 'local') {
|
|
501
|
+
// Local filesystem
|
|
502
|
+
const localPath = marketplace.url.replace('file://', '');
|
|
503
|
+
const mcpPath = path.join(localPath, `${mcpName}.photon.ts`);
|
|
504
|
+
if (existsSync(mcpPath)) {
|
|
505
|
+
content = await fs.readFile(mcpPath, 'utf-8');
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
// Remote fetch (GitHub, URL)
|
|
510
|
+
const url = `${marketplace.url}/${mcpName}.photon.ts`;
|
|
511
|
+
const response = await fetch(url);
|
|
512
|
+
if (response.ok) {
|
|
513
|
+
content = await response.text();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (content) {
|
|
517
|
+
const versionMatch = content.match(/@version\s+(\d+\.\d+\.\d+)/);
|
|
518
|
+
if (versionMatch) {
|
|
519
|
+
return { version: versionMatch[1], marketplace };
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// Try next marketplace
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Search for Photon in all marketplaces
|
|
531
|
+
* Searches in name, description, tags, and author fields
|
|
532
|
+
*/
|
|
533
|
+
async search(query) {
|
|
534
|
+
const results = new Map();
|
|
535
|
+
const enabled = this.getEnabled();
|
|
536
|
+
const lowerQuery = query.toLowerCase();
|
|
537
|
+
for (const marketplace of enabled) {
|
|
538
|
+
// First, try to search in cached manifest
|
|
539
|
+
const manifest = await this.getCachedManifest(marketplace.name);
|
|
540
|
+
if (manifest) {
|
|
541
|
+
// Search in manifest metadata
|
|
542
|
+
for (const photon of manifest.photons) {
|
|
543
|
+
const nameMatch = photon.name.toLowerCase().includes(lowerQuery);
|
|
544
|
+
const descMatch = photon.description?.toLowerCase().includes(lowerQuery);
|
|
545
|
+
const tagMatch = photon.tags?.some(tag => tag.toLowerCase().includes(lowerQuery));
|
|
546
|
+
const authorMatch = photon.author?.toLowerCase().includes(lowerQuery);
|
|
547
|
+
if (nameMatch || descMatch || tagMatch || authorMatch) {
|
|
548
|
+
const existing = results.get(photon.name) || [];
|
|
549
|
+
existing.push({ metadata: photon, marketplace });
|
|
550
|
+
results.set(photon.name, existing);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
// Fallback: check if exact filename exists (for marketplaces without manifest)
|
|
556
|
+
try {
|
|
557
|
+
const url = `${marketplace.url}/${query}.photon.ts`;
|
|
558
|
+
const response = await fetch(url, { method: 'HEAD' });
|
|
559
|
+
if (response.ok) {
|
|
560
|
+
const existing = results.get(query) || [];
|
|
561
|
+
existing.push({ marketplace });
|
|
562
|
+
results.set(query, existing);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
// Skip this marketplace
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return results;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* List all available MCPs from a marketplace
|
|
574
|
+
* Note: Requires marketplace to have a .marketplace/photons.json file
|
|
575
|
+
*/
|
|
576
|
+
async listFromMarketplace(marketplaceName) {
|
|
577
|
+
const marketplace = this.get(marketplaceName);
|
|
578
|
+
if (!marketplace) {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
// Try to fetch photons.json manifest
|
|
583
|
+
const manifest = await this.fetchManifest(marketplace);
|
|
584
|
+
if (manifest) {
|
|
585
|
+
return manifest.photons.map(p => p.name);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
// No manifest file available
|
|
590
|
+
}
|
|
591
|
+
return [];
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Save installation metadata for a Photon
|
|
595
|
+
*/
|
|
596
|
+
async savePhotonMetadata(fileName, marketplace, metadata, contentHash) {
|
|
597
|
+
const localMetadata = await readLocalMetadata();
|
|
598
|
+
localMetadata.photons[fileName] = {
|
|
599
|
+
marketplace: marketplace.name,
|
|
600
|
+
marketplaceRepo: marketplace.repo,
|
|
601
|
+
version: metadata.version,
|
|
602
|
+
originalHash: metadata.hash || contentHash,
|
|
603
|
+
installedAt: new Date().toISOString(),
|
|
604
|
+
};
|
|
605
|
+
await writeLocalMetadata(localMetadata);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Get local installation metadata for a Photon
|
|
609
|
+
*/
|
|
610
|
+
async getPhotonInstallMetadata(fileName) {
|
|
611
|
+
const localMetadata = await readLocalMetadata();
|
|
612
|
+
return localMetadata.photons[fileName] || null;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Check if a Photon file has been modified since installation
|
|
616
|
+
*/
|
|
617
|
+
async isPhotonModified(filePath, fileName) {
|
|
618
|
+
const metadata = await this.getPhotonInstallMetadata(fileName);
|
|
619
|
+
if (!metadata) {
|
|
620
|
+
return false; // No metadata, can't determine
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
const currentHash = await calculateFileHash(filePath);
|
|
624
|
+
return currentHash !== metadata.originalHash;
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Find all marketplaces that have a specific MCP (for conflict detection)
|
|
632
|
+
*/
|
|
633
|
+
async findAllSources(mcpName) {
|
|
634
|
+
const sources = [];
|
|
635
|
+
const enabled = this.getEnabled();
|
|
636
|
+
for (const marketplace of enabled) {
|
|
637
|
+
try {
|
|
638
|
+
let content = null;
|
|
639
|
+
if (marketplace.sourceType === 'local') {
|
|
640
|
+
// Local filesystem
|
|
641
|
+
const localPath = marketplace.url.replace('file://', '');
|
|
642
|
+
const mcpPath = path.join(localPath, `${mcpName}.photon.ts`);
|
|
643
|
+
if (existsSync(mcpPath)) {
|
|
644
|
+
content = await fs.readFile(mcpPath, 'utf-8');
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
// Remote fetch (GitHub, URL)
|
|
649
|
+
const url = `${marketplace.url}/${mcpName}.photon.ts`;
|
|
650
|
+
const response = await fetch(url);
|
|
651
|
+
if (response.ok) {
|
|
652
|
+
content = await response.text();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (content) {
|
|
656
|
+
// Try to fetch metadata from manifest
|
|
657
|
+
const manifest = await this.getCachedManifest(marketplace.name);
|
|
658
|
+
const metadata = manifest?.photons.find(p => p.name === mcpName);
|
|
659
|
+
sources.push({
|
|
660
|
+
marketplace,
|
|
661
|
+
metadata,
|
|
662
|
+
content,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
// Skip marketplace on error
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return sources;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Detect all MCP conflicts across marketplaces
|
|
674
|
+
*/
|
|
675
|
+
async detectAllConflicts() {
|
|
676
|
+
const conflicts = new Map();
|
|
677
|
+
const enabled = this.getEnabled();
|
|
678
|
+
if (enabled.length <= 1) {
|
|
679
|
+
return conflicts; // No conflicts possible with 0 or 1 marketplace
|
|
680
|
+
}
|
|
681
|
+
// Collect all MCPs from all marketplaces
|
|
682
|
+
const mcpsByName = new Map();
|
|
683
|
+
for (const marketplace of enabled) {
|
|
684
|
+
const manifest = await this.getCachedManifest(marketplace.name);
|
|
685
|
+
if (manifest && manifest.photons) {
|
|
686
|
+
for (const photon of manifest.photons) {
|
|
687
|
+
if (!mcpsByName.has(photon.name)) {
|
|
688
|
+
mcpsByName.set(photon.name, []);
|
|
689
|
+
}
|
|
690
|
+
mcpsByName.get(photon.name).push({
|
|
691
|
+
marketplace,
|
|
692
|
+
metadata: photon,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// Find MCPs that appear in multiple marketplaces
|
|
698
|
+
for (const [name, sources] of mcpsByName.entries()) {
|
|
699
|
+
if (sources.length > 1) {
|
|
700
|
+
conflicts.set(name, sources);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return conflicts;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Check if adding/upgrading an MCP would create a conflict
|
|
707
|
+
*/
|
|
708
|
+
async checkConflict(mcpName, targetMarketplace) {
|
|
709
|
+
const sources = await this.findAllSources(mcpName);
|
|
710
|
+
if (sources.length === 0) {
|
|
711
|
+
return { hasConflict: false, sources: [] };
|
|
712
|
+
}
|
|
713
|
+
if (sources.length === 1) {
|
|
714
|
+
return { hasConflict: false, sources };
|
|
715
|
+
}
|
|
716
|
+
// Multiple sources found - determine recommendation
|
|
717
|
+
let recommendation;
|
|
718
|
+
// If target marketplace specified, recommend it
|
|
719
|
+
if (targetMarketplace) {
|
|
720
|
+
const targetSource = sources.find(s => s.marketplace.name === targetMarketplace);
|
|
721
|
+
if (targetSource) {
|
|
722
|
+
recommendation = targetMarketplace;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
// Otherwise, recommend based on priority: version, then marketplace order
|
|
726
|
+
if (!recommendation) {
|
|
727
|
+
// Sort by version (semver) if available
|
|
728
|
+
const withVersions = sources
|
|
729
|
+
.filter(s => s.metadata?.version)
|
|
730
|
+
.sort((a, b) => {
|
|
731
|
+
const vA = a.metadata.version;
|
|
732
|
+
const vB = b.metadata.version;
|
|
733
|
+
return this.compareVersions(vB, vA); // Descending (newest first)
|
|
734
|
+
});
|
|
735
|
+
if (withVersions.length > 0) {
|
|
736
|
+
recommendation = withVersions[0].marketplace.name;
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
// Default to first enabled marketplace
|
|
740
|
+
recommendation = sources[0].marketplace.name;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
hasConflict: true,
|
|
745
|
+
sources,
|
|
746
|
+
recommendation,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Compare two semver versions
|
|
751
|
+
* Returns: positive if v1 > v2, negative if v1 < v2, 0 if equal
|
|
752
|
+
*/
|
|
753
|
+
compareVersions(v1, v2) {
|
|
754
|
+
const parts1 = v1.split('.').map(Number);
|
|
755
|
+
const parts2 = v2.split('.').map(Number);
|
|
756
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
757
|
+
const p1 = parts1[i] || 0;
|
|
758
|
+
const p2 = parts2[i] || 0;
|
|
759
|
+
if (p1 > p2)
|
|
760
|
+
return 1;
|
|
761
|
+
if (p1 < p2)
|
|
762
|
+
return -1;
|
|
763
|
+
}
|
|
764
|
+
return 0;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
//# sourceMappingURL=marketplace-manager.js.map
|