@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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +952 -0
  3. package/dist/base.d.ts +58 -0
  4. package/dist/base.d.ts.map +1 -0
  5. package/dist/base.js +92 -0
  6. package/dist/base.js.map +1 -0
  7. package/dist/cli.d.ts +8 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +1441 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/dependency-manager.d.ts +49 -0
  12. package/dist/dependency-manager.d.ts.map +1 -0
  13. package/dist/dependency-manager.js +165 -0
  14. package/dist/dependency-manager.js.map +1 -0
  15. package/dist/loader.d.ts +86 -0
  16. package/dist/loader.d.ts.map +1 -0
  17. package/dist/loader.js +612 -0
  18. package/dist/loader.js.map +1 -0
  19. package/dist/marketplace-manager.d.ts +261 -0
  20. package/dist/marketplace-manager.d.ts.map +1 -0
  21. package/dist/marketplace-manager.js +767 -0
  22. package/dist/marketplace-manager.js.map +1 -0
  23. package/dist/path-resolver.d.ts +21 -0
  24. package/dist/path-resolver.d.ts.map +1 -0
  25. package/dist/path-resolver.js +71 -0
  26. package/dist/path-resolver.js.map +1 -0
  27. package/dist/photon-doc-extractor.d.ts +89 -0
  28. package/dist/photon-doc-extractor.d.ts.map +1 -0
  29. package/dist/photon-doc-extractor.js +228 -0
  30. package/dist/photon-doc-extractor.js.map +1 -0
  31. package/dist/readme-syncer.d.ts +33 -0
  32. package/dist/readme-syncer.d.ts.map +1 -0
  33. package/dist/readme-syncer.js +93 -0
  34. package/dist/readme-syncer.js.map +1 -0
  35. package/dist/registry-manager.d.ts +76 -0
  36. package/dist/registry-manager.d.ts.map +1 -0
  37. package/dist/registry-manager.js +220 -0
  38. package/dist/registry-manager.js.map +1 -0
  39. package/dist/schema-extractor.d.ts +83 -0
  40. package/dist/schema-extractor.d.ts.map +1 -0
  41. package/dist/schema-extractor.js +396 -0
  42. package/dist/schema-extractor.js.map +1 -0
  43. package/dist/security-scanner.d.ts +52 -0
  44. package/dist/security-scanner.d.ts.map +1 -0
  45. package/dist/security-scanner.js +172 -0
  46. package/dist/security-scanner.js.map +1 -0
  47. package/dist/server.d.ts +73 -0
  48. package/dist/server.d.ts.map +1 -0
  49. package/dist/server.js +474 -0
  50. package/dist/server.js.map +1 -0
  51. package/dist/template-manager.d.ts +56 -0
  52. package/dist/template-manager.d.ts.map +1 -0
  53. package/dist/template-manager.js +509 -0
  54. package/dist/template-manager.js.map +1 -0
  55. package/dist/test-client.d.ts +52 -0
  56. package/dist/test-client.d.ts.map +1 -0
  57. package/dist/test-client.js +168 -0
  58. package/dist/test-client.js.map +1 -0
  59. package/dist/test-marketplace-sources.d.ts +5 -0
  60. package/dist/test-marketplace-sources.d.ts.map +1 -0
  61. package/dist/test-marketplace-sources.js +53 -0
  62. package/dist/test-marketplace-sources.js.map +1 -0
  63. package/dist/types.d.ts +108 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +12 -0
  66. package/dist/types.js.map +1 -0
  67. package/dist/version-checker.d.ts +48 -0
  68. package/dist/version-checker.d.ts.map +1 -0
  69. package/dist/version-checker.js +128 -0
  70. package/dist/version-checker.js.map +1 -0
  71. package/dist/watcher.d.ts +26 -0
  72. package/dist/watcher.d.ts.map +1 -0
  73. package/dist/watcher.js +72 -0
  74. package/dist/watcher.js.map +1 -0
  75. package/package.json +79 -0
  76. 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