@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
package/dist/cli.js ADDED
@@ -0,0 +1,1441 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Photon MCP CLI
4
+ *
5
+ * Command-line interface for running .photon.ts files as MCP servers
6
+ */
7
+ import { Command } from 'commander';
8
+ import * as path from 'path';
9
+ import * as fs from 'fs/promises';
10
+ import { existsSync } from 'fs';
11
+ import * as os from 'os';
12
+ import * as readline from 'readline';
13
+ import { PhotonServer } from './server.js';
14
+ import { FileWatcher } from './watcher.js';
15
+ import { resolvePhotonPath, listPhotonMCPs, ensureWorkingDir, DEFAULT_WORKING_DIR } from './path-resolver.js';
16
+ import { SchemaExtractor } from './schema-extractor.js';
17
+ import { createRequire } from 'module';
18
+ import { fileURLToPath } from 'url';
19
+ const require = createRequire(import.meta.url);
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ /**
22
+ * Extract constructor parameters from a Photon MCP file
23
+ */
24
+ async function extractConstructorParams(filePath) {
25
+ try {
26
+ const source = await fs.readFile(filePath, 'utf-8');
27
+ const extractor = new SchemaExtractor();
28
+ return extractor.extractConstructorParams(source);
29
+ }
30
+ catch (error) {
31
+ console.error(`Failed to extract constructor params: ${error.message}`);
32
+ return [];
33
+ }
34
+ }
35
+ /**
36
+ * Convert MCP name and parameter name to environment variable name
37
+ */
38
+ function toEnvVarName(mcpName, paramName) {
39
+ const mcpPrefix = mcpName.toUpperCase().replace(/-/g, '_');
40
+ const paramSuffix = paramName
41
+ .replace(/([A-Z])/g, '_$1')
42
+ .toUpperCase()
43
+ .replace(/^_/, '');
44
+ return `${mcpPrefix}_${paramSuffix}`;
45
+ }
46
+ /**
47
+ * Ensure .gitignore includes marketplace template directory
48
+ */
49
+ async function ensureGitignore(workingDir) {
50
+ const gitignorePath = path.join(workingDir, '.gitignore');
51
+ const templatesPattern = '.marketplace/_templates/';
52
+ try {
53
+ let gitignoreContent = '';
54
+ if (existsSync(gitignorePath)) {
55
+ gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
56
+ }
57
+ // Check if pattern already exists
58
+ if (gitignoreContent.includes(templatesPattern)) {
59
+ return; // Already configured
60
+ }
61
+ // Add templates pattern to .gitignore
62
+ const newContent = gitignoreContent.endsWith('\n')
63
+ ? gitignoreContent + templatesPattern + '\n'
64
+ : gitignoreContent + '\n' + templatesPattern + '\n';
65
+ await fs.writeFile(gitignorePath, newContent, 'utf-8');
66
+ console.error(' ✓ Added .marketplace/_templates/ to .gitignore');
67
+ }
68
+ catch (error) {
69
+ // Non-fatal - just warn
70
+ console.error(` ⚠ Could not update .gitignore: ${error.message}`);
71
+ }
72
+ }
73
+ /**
74
+ * Perform marketplace sync - generates documentation files
75
+ */
76
+ async function performMarketplaceSync(dirPath, options) {
77
+ const resolvedPath = path.resolve(dirPath);
78
+ if (!existsSync(resolvedPath)) {
79
+ console.error(`❌ Directory not found: ${resolvedPath}`);
80
+ process.exit(1);
81
+ }
82
+ // Scan for .photon.ts files
83
+ console.error('📦 Scanning for .photon.ts files...');
84
+ const files = await fs.readdir(resolvedPath);
85
+ const photonFiles = files.filter(f => f.endsWith('.photon.ts'));
86
+ if (photonFiles.length === 0) {
87
+ console.error(`❌ No .photon.ts files found in ${resolvedPath}`);
88
+ process.exit(1);
89
+ }
90
+ console.error(` Found ${photonFiles.length} photons\n`);
91
+ // Initialize template manager
92
+ const { TemplateManager } = await import('./template-manager.js');
93
+ const templateMgr = new TemplateManager(resolvedPath);
94
+ console.error('📝 Ensuring templates...');
95
+ await templateMgr.ensureTemplates();
96
+ // Ensure .gitignore excludes templates
97
+ await ensureGitignore(resolvedPath);
98
+ console.error('');
99
+ // Extract metadata from each Photon
100
+ console.error('📄 Extracting documentation...');
101
+ const { calculateFileHash } = await import('./marketplace-manager.js');
102
+ const { PhotonDocExtractor } = await import('./photon-doc-extractor.js');
103
+ const photons = [];
104
+ for (const file of photonFiles.sort()) {
105
+ const filePath = path.join(resolvedPath, file);
106
+ // Extract full metadata
107
+ const extractor = new PhotonDocExtractor(filePath);
108
+ const metadata = await extractor.extractFullMetadata();
109
+ // Calculate hash
110
+ const hash = await calculateFileHash(filePath);
111
+ console.error(` ✓ ${metadata.name} (${metadata.tools?.length || 0} tools)`);
112
+ // Build manifest entry
113
+ photons.push({
114
+ name: metadata.name,
115
+ version: metadata.version,
116
+ description: metadata.description,
117
+ author: metadata.author || options.owner || 'Unknown',
118
+ license: metadata.license || 'MIT',
119
+ repository: metadata.repository,
120
+ homepage: metadata.homepage,
121
+ source: `../${file}`,
122
+ hash,
123
+ tools: metadata.tools?.map(t => t.name),
124
+ });
125
+ // Generate individual photon documentation
126
+ const photonMarkdown = await templateMgr.renderTemplate('photon.md', metadata);
127
+ const docPath = path.join(resolvedPath, `${metadata.name}.md`);
128
+ await fs.writeFile(docPath, photonMarkdown, 'utf-8');
129
+ }
130
+ // Create manifest
131
+ console.error('\n📋 Updating manifest...');
132
+ const baseName = path.basename(resolvedPath);
133
+ const manifest = {
134
+ name: options.name || baseName,
135
+ version: '1.0.0',
136
+ description: options.description || undefined,
137
+ owner: options.owner ? {
138
+ name: options.owner,
139
+ } : undefined,
140
+ photons,
141
+ };
142
+ const marketplaceDir = path.join(resolvedPath, '.marketplace');
143
+ await fs.mkdir(marketplaceDir, { recursive: true });
144
+ const manifestPath = path.join(marketplaceDir, 'photons.json');
145
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
146
+ console.error(' ✓ .marketplace/photons.json');
147
+ // Sync README with generated content
148
+ console.error('\n📖 Syncing README.md...');
149
+ const { ReadmeSyncer } = await import('./readme-syncer.js');
150
+ const readmePath = path.join(resolvedPath, 'README.md');
151
+ const syncer = new ReadmeSyncer(readmePath);
152
+ // Render README section from template
153
+ const readmeContent = await templateMgr.renderTemplate('readme.md', {
154
+ marketplaceName: manifest.name,
155
+ marketplaceDescription: manifest.description || '',
156
+ photons: photons.map(p => ({
157
+ name: p.name,
158
+ description: p.description,
159
+ version: p.version,
160
+ license: p.license,
161
+ tools: p.tools || [],
162
+ })),
163
+ });
164
+ const isUpdate = await syncer.sync(readmeContent);
165
+ if (isUpdate) {
166
+ console.error(' ✓ README.md synced (user content preserved)');
167
+ }
168
+ else {
169
+ console.error(' ✓ README.md created');
170
+ }
171
+ console.error('\n✅ Marketplace synced successfully!');
172
+ console.error(`\n Marketplace: ${manifest.name}`);
173
+ console.error(` Photons: ${photons.length}`);
174
+ console.error(` Documentation: ${photons.length} markdown files generated`);
175
+ console.error(`\n Generated files:`);
176
+ console.error(` • .marketplace/photons.json (manifest)`);
177
+ console.error(` • *.md (${photons.length} documentation files at root)`);
178
+ console.error(` • README.md (auto-generated table)`);
179
+ }
180
+ /**
181
+ * Format default value for display in config
182
+ */
183
+ function formatDefaultValue(value) {
184
+ if (typeof value === 'string') {
185
+ // Check if it's a function call expression
186
+ if (value.includes('homedir()')) {
187
+ // Replace homedir() with actual home directory
188
+ return value.replace(/join\(homedir\(\),\s*['"]([^'"]+)['"]\)/g, (_, folderName) => {
189
+ return path.join(os.homedir(), folderName);
190
+ });
191
+ }
192
+ if (value.includes('process.cwd()')) {
193
+ return process.cwd();
194
+ }
195
+ return value;
196
+ }
197
+ if (typeof value === 'number' || typeof value === 'boolean') {
198
+ return String(value);
199
+ }
200
+ // For other complex expressions
201
+ return String(value);
202
+ }
203
+ /**
204
+ * Get OS-specific MCP client config path
205
+ */
206
+ function getConfigPath() {
207
+ const platform = process.platform;
208
+ if (platform === 'darwin') {
209
+ return path.join(os.homedir(), 'Library/Application Support/Claude/claude_desktop_config.json');
210
+ }
211
+ else if (platform === 'win32') {
212
+ return path.join(process.env.APPDATA || '', 'Claude/claude_desktop_config.json');
213
+ }
214
+ else {
215
+ // Linux/other
216
+ return path.join(os.homedir(), '.config/Claude/claude_desktop_config.json');
217
+ }
218
+ }
219
+ /**
220
+ * Validate configuration for an MCP
221
+ */
222
+ async function validateConfiguration(filePath, mcpName) {
223
+ console.log(`🔍 Validating configuration for: ${mcpName}\n`);
224
+ const params = await extractConstructorParams(filePath);
225
+ if (params.length === 0) {
226
+ console.log('✅ No configuration required');
227
+ return;
228
+ }
229
+ let hasErrors = false;
230
+ const results = [];
231
+ for (const param of params) {
232
+ const envVarName = toEnvVarName(mcpName, param.name);
233
+ const envValue = process.env[envVarName];
234
+ const isRequired = !param.isOptional && !param.hasDefault;
235
+ if (isRequired && !envValue) {
236
+ hasErrors = true;
237
+ results.push({
238
+ name: param.name,
239
+ envVar: envVarName,
240
+ status: '❌ MISSING (required)',
241
+ });
242
+ }
243
+ else if (envValue) {
244
+ results.push({
245
+ name: param.name,
246
+ envVar: envVarName,
247
+ status: '✅ SET',
248
+ value: envValue.length > 20 ? envValue.substring(0, 17) + '...' : envValue,
249
+ });
250
+ }
251
+ else {
252
+ results.push({
253
+ name: param.name,
254
+ envVar: envVarName,
255
+ status: '⚪ Optional',
256
+ value: param.hasDefault ? `default: ${formatDefaultValue(param.defaultValue)}` : undefined,
257
+ });
258
+ }
259
+ }
260
+ // Print results
261
+ console.log('Configuration Status:\n');
262
+ results.forEach(r => {
263
+ console.log(` ${r.status} ${r.envVar}`);
264
+ if (r.value) {
265
+ console.log(` Value: ${r.value}`);
266
+ }
267
+ console.log();
268
+ });
269
+ if (hasErrors) {
270
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
271
+ console.log('❌ Validation failed: Missing required environment variables');
272
+ console.log('\nRun: photon mcp ' + mcpName + ' --config');
273
+ console.log(' To see configuration template');
274
+ process.exit(1);
275
+ }
276
+ else {
277
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
278
+ console.log('✅ Configuration valid!');
279
+ console.log('\nYou can now run: photon mcp ' + mcpName);
280
+ }
281
+ }
282
+ /**
283
+ * Show configuration template for an MCP
284
+ */
285
+ async function showConfigTemplate(filePath, mcpName) {
286
+ console.log(`📋 Configuration template for: ${mcpName}\n`);
287
+ const params = await extractConstructorParams(filePath);
288
+ if (params.length === 0) {
289
+ console.log('✅ No configuration required for this MCP');
290
+ return;
291
+ }
292
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
293
+ console.log('Environment Variables:\n');
294
+ params.forEach(param => {
295
+ const envVarName = toEnvVarName(mcpName, param.name);
296
+ const isRequired = !param.isOptional && !param.hasDefault;
297
+ const status = isRequired ? '[REQUIRED]' : '[OPTIONAL]';
298
+ console.log(` ${envVarName} ${status}`);
299
+ console.log(` Type: ${param.type}`);
300
+ if (param.hasDefault) {
301
+ console.log(` Default: ${formatDefaultValue(param.defaultValue)}`);
302
+ }
303
+ console.log();
304
+ });
305
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
306
+ console.log('Claude Desktop Configuration:\n');
307
+ const envExample = {};
308
+ params.forEach(param => {
309
+ const envVarName = toEnvVarName(mcpName, param.name);
310
+ if (!param.isOptional && !param.hasDefault) {
311
+ envExample[envVarName] = `<your-${param.name}>`;
312
+ }
313
+ });
314
+ const config = {
315
+ mcpServers: {
316
+ [mcpName]: {
317
+ command: 'npx',
318
+ args: ['@portel/photon', 'mcp', mcpName],
319
+ env: envExample,
320
+ },
321
+ },
322
+ };
323
+ console.log(JSON.stringify(config, null, 2));
324
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
325
+ console.log(`\nAdd this to: ${getConfigPath()}`);
326
+ console.log('\nValidate: photon mcp ' + mcpName + ' --validate');
327
+ }
328
+ // Get version from package.json
329
+ let version = '1.0.0';
330
+ try {
331
+ const packageJson = require('../package.json');
332
+ version = packageJson.version;
333
+ }
334
+ catch {
335
+ // Fallback version
336
+ }
337
+ const program = new Command();
338
+ program
339
+ .name('photon')
340
+ .description('Universal runtime for single-file TypeScript programs')
341
+ .version(version)
342
+ .option('--working-dir <dir>', 'Working directory for Photons (default: ~/.photon)', DEFAULT_WORKING_DIR);
343
+ // MCP Runtime: run a .photon.ts file as MCP server
344
+ program
345
+ .command('mcp')
346
+ .argument('<name>', 'MCP name (without .photon.ts extension)')
347
+ .description('Run a Photon as MCP server')
348
+ .option('--dev', 'Enable development mode with hot reload')
349
+ .option('--validate', 'Validate configuration without running server')
350
+ .option('--config', 'Show configuration template and exit')
351
+ .action(async (name, options, command) => {
352
+ try {
353
+ // Get working directory from global options
354
+ const workingDir = program.opts().workingDir || DEFAULT_WORKING_DIR;
355
+ // Resolve file path from name in working directory
356
+ const filePath = await resolvePhotonPath(name, workingDir);
357
+ if (!filePath) {
358
+ console.error(`❌ MCP not found: ${name}`);
359
+ console.error(`Searched in: ${workingDir}`);
360
+ console.error(`Tip: Use 'photon list' to see available MCPs`);
361
+ process.exit(1);
362
+ }
363
+ // Handle --validate flag
364
+ if (options.validate) {
365
+ await validateConfiguration(filePath, name);
366
+ return;
367
+ }
368
+ // Handle --config flag
369
+ if (options.config) {
370
+ await showConfigTemplate(filePath, name);
371
+ return;
372
+ }
373
+ // Start MCP server
374
+ const server = new PhotonServer({
375
+ filePath,
376
+ devMode: options.dev,
377
+ });
378
+ // Handle shutdown signals
379
+ const shutdown = async () => {
380
+ console.error('\nShutting down...');
381
+ await server.stop();
382
+ process.exit(0);
383
+ };
384
+ process.on('SIGINT', shutdown);
385
+ process.on('SIGTERM', shutdown);
386
+ // Start the server
387
+ await server.start();
388
+ // Start file watcher in dev mode
389
+ if (options.dev) {
390
+ const watcher = new FileWatcher(server, filePath);
391
+ watcher.start();
392
+ // Clean up watcher on shutdown
393
+ process.on('SIGINT', async () => {
394
+ await watcher.stop();
395
+ });
396
+ process.on('SIGTERM', async () => {
397
+ await watcher.stop();
398
+ });
399
+ }
400
+ }
401
+ catch (error) {
402
+ console.error(`❌ Error: ${error.message}`);
403
+ process.exit(1);
404
+ }
405
+ });
406
+ // Init command: create a new .photon.ts from template
407
+ program
408
+ .command('init')
409
+ .argument('<name>', 'Name for the new Photon MCP')
410
+ .description('Create a new .photon.ts from template')
411
+ .action(async (name, options, command) => {
412
+ try {
413
+ // Get working directory from global options
414
+ const workingDir = command.parent?.opts().workingDir || DEFAULT_WORKING_DIR;
415
+ // Ensure working directory exists
416
+ await ensureWorkingDir(workingDir);
417
+ const fileName = `${name}.photon.ts`;
418
+ const filePath = path.join(workingDir, fileName);
419
+ // Check if file already exists
420
+ try {
421
+ await fs.access(filePath);
422
+ console.error(`❌ File already exists: ${filePath}`);
423
+ process.exit(1);
424
+ }
425
+ catch {
426
+ // File doesn't exist, good
427
+ }
428
+ // Read template
429
+ const templatePath = path.join(__dirname, '..', 'templates', 'photon.template.ts');
430
+ let template;
431
+ try {
432
+ template = await fs.readFile(templatePath, 'utf-8');
433
+ }
434
+ catch {
435
+ // Fallback inline template if file not found
436
+ template = getInlineTemplate();
437
+ }
438
+ // Replace placeholders
439
+ // Convert kebab-case to PascalCase for class name
440
+ const className = name
441
+ .split(/[-_]/)
442
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
443
+ .join('');
444
+ const content = template
445
+ .replace(/TemplateName/g, className)
446
+ .replace(/template-name/g, name);
447
+ // Write file
448
+ await fs.writeFile(filePath, content, 'utf-8');
449
+ console.error(`✅ Created ${fileName} in ${workingDir}`);
450
+ console.error(`Run with: photon mcp ${name} --dev`);
451
+ }
452
+ catch (error) {
453
+ console.error(`❌ Error: ${error.message}`);
454
+ process.exit(1);
455
+ }
456
+ });
457
+ // Validate command: check syntax and schemas
458
+ program
459
+ .command('validate')
460
+ .argument('<name>', 'MCP name (without .photon.ts extension)')
461
+ .description('Validate syntax and schemas without running')
462
+ .action(async (name, options, command) => {
463
+ try {
464
+ // Get working directory from global options
465
+ const workingDir = command.parent?.opts().workingDir || DEFAULT_WORKING_DIR;
466
+ // Resolve file path from name in working directory
467
+ const filePath = await resolvePhotonPath(name, workingDir);
468
+ if (!filePath) {
469
+ console.error(`❌ MCP not found: ${name}`);
470
+ console.error(`Searched in: ${workingDir}`);
471
+ console.error(`Tip: Use 'photon list' to see available MCPs`);
472
+ process.exit(1);
473
+ }
474
+ console.error(`Validating ${path.basename(filePath)}...\n`);
475
+ // Import loader and try to load
476
+ const { PhotonLoader } = await import('./loader.js');
477
+ const loader = new PhotonLoader(false); // quiet mode for inspection
478
+ const mcp = await loader.loadFile(filePath);
479
+ console.error(`✅ Valid Photon MCP`);
480
+ console.error(`Name: ${mcp.name}`);
481
+ console.error(`Tools: ${mcp.tools.length}`);
482
+ for (const tool of mcp.tools) {
483
+ console.error(` - ${tool.name}: ${tool.description}`);
484
+ }
485
+ process.exit(0);
486
+ }
487
+ catch (error) {
488
+ console.error(`❌ Validation failed: ${error.message}`);
489
+ process.exit(1);
490
+ }
491
+ });
492
+ // Get command: list all Photons or show details for one
493
+ program
494
+ .command('get')
495
+ .argument('[name]', 'Photon name to show details for (shows all if omitted)')
496
+ .option('--mcp', 'Output as MCP server configuration')
497
+ .description('List Photons or show details for one')
498
+ .action(async (name, options, command) => {
499
+ try {
500
+ // Get working directory from global/parent options
501
+ const parentOpts = command.parent?.opts() || {};
502
+ const workingDir = parentOpts.workingDir || DEFAULT_WORKING_DIR;
503
+ const asMcp = options.mcp || false;
504
+ const mcps = await listPhotonMCPs(workingDir);
505
+ if (mcps.length === 0) {
506
+ console.error(`No Photons found in ${workingDir}`);
507
+ console.error(`Create one with: photon init <name>`);
508
+ return;
509
+ }
510
+ // Show single Photon details
511
+ if (name) {
512
+ const filePath = await resolvePhotonPath(name, workingDir);
513
+ if (!filePath) {
514
+ console.error(`❌ Photon not found: ${name}`);
515
+ console.error(`Searched in: ${workingDir}`);
516
+ console.error(`Tip: Use 'photon get' to see available Photons`);
517
+ process.exit(1);
518
+ }
519
+ if (asMcp) {
520
+ // Show as MCP config for single Photon
521
+ const constructorParams = await extractConstructorParams(filePath);
522
+ const env = {};
523
+ for (const param of constructorParams) {
524
+ const envVarName = toEnvVarName(name, param.name);
525
+ const defaultDisplay = param.defaultValue !== undefined
526
+ ? formatDefaultValue(param.defaultValue)
527
+ : `<your-${param.name}>`;
528
+ env[envVarName] = defaultDisplay;
529
+ }
530
+ const config = {
531
+ command: 'npx',
532
+ args: ['@portel/photon', 'mcp', name],
533
+ ...(Object.keys(env).length > 0 && { env }),
534
+ };
535
+ // Get OS-specific config path
536
+ const configPath = getConfigPath();
537
+ console.log(`# Photon MCP Server Configuration: ${name}`);
538
+ console.log(`# Add to mcpServers in: ${configPath}\n`);
539
+ console.log(JSON.stringify({ [name]: config }, null, 2));
540
+ }
541
+ else {
542
+ // Show Photon details
543
+ const { PhotonLoader } = await import('./loader.js');
544
+ const loader = new PhotonLoader(false); // quiet mode for inspection
545
+ const mcp = await loader.loadFile(filePath);
546
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
547
+ const manager = new MarketplaceManager();
548
+ await manager.initialize();
549
+ const fileName = `${name}.photon.ts`;
550
+ const metadata = await manager.getPhotonInstallMetadata(fileName);
551
+ const isModified = metadata ? await manager.isPhotonModified(filePath, fileName) : false;
552
+ console.error(`📦 ${name}\n`);
553
+ console.error(`Location: ${filePath}`);
554
+ // Show marketplace metadata if available
555
+ if (metadata) {
556
+ console.error(`Version: ${metadata.version}`);
557
+ console.error(`Marketplace: ${metadata.marketplace} (${metadata.marketplaceRepo})`);
558
+ console.error(`Installed: ${new Date(metadata.installedAt).toLocaleDateString()}`);
559
+ if (isModified) {
560
+ console.error(`Status: ⚠️ Modified locally`);
561
+ }
562
+ }
563
+ console.error(`Tools: ${mcp.tools.length}`);
564
+ console.error(`Templates: ${mcp.templates.length}`);
565
+ console.error(`Resources: ${mcp.statics.length}\n`);
566
+ if (mcp.tools.length > 0) {
567
+ console.error('Tools:');
568
+ for (const tool of mcp.tools) {
569
+ console.error(` • ${tool.name}: ${tool.description || 'No description'}`);
570
+ }
571
+ console.error('');
572
+ }
573
+ if (mcp.templates.length > 0) {
574
+ console.error('Templates:');
575
+ for (const template of mcp.templates) {
576
+ console.error(` • ${template.name}: ${template.description || 'No description'}`);
577
+ }
578
+ console.error('');
579
+ }
580
+ if (mcp.statics.length > 0) {
581
+ console.error('Resources:');
582
+ for (const resource of mcp.statics) {
583
+ console.error(` • ${resource.uri}: ${resource.name || 'No name'}`);
584
+ }
585
+ console.error('');
586
+ }
587
+ console.error(`Run with: photon mcp ${name} --dev`);
588
+ }
589
+ return;
590
+ }
591
+ // Show all Photons
592
+ if (asMcp) {
593
+ // MCP config mode for all Photons
594
+ const allConfigs = {};
595
+ for (const mcpName of mcps) {
596
+ const filePath = await resolvePhotonPath(mcpName, workingDir);
597
+ if (!filePath)
598
+ continue;
599
+ const constructorParams = await extractConstructorParams(filePath);
600
+ const env = {};
601
+ for (const param of constructorParams) {
602
+ const envVarName = toEnvVarName(mcpName, param.name);
603
+ const defaultDisplay = param.defaultValue !== undefined
604
+ ? formatDefaultValue(param.defaultValue)
605
+ : `<your-${param.name}>`;
606
+ env[envVarName] = defaultDisplay;
607
+ }
608
+ allConfigs[mcpName] = {
609
+ command: 'npx',
610
+ args: ['@portel/photon', 'mcp', mcpName],
611
+ ...(Object.keys(env).length > 0 && { env }),
612
+ };
613
+ }
614
+ // Get OS-specific config path
615
+ const configPath = getConfigPath();
616
+ console.log(`# Photon MCP Server Configuration (${mcps.length} servers)`);
617
+ console.log(`# Add to mcpServers in: ${configPath}\n`);
618
+ console.log(JSON.stringify({ mcpServers: allConfigs }, null, 2));
619
+ return;
620
+ }
621
+ // Normal list mode - show with metadata
622
+ console.error(`Photons in ${workingDir} (${mcps.length}):\n`);
623
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
624
+ const manager = new MarketplaceManager();
625
+ await manager.initialize();
626
+ for (const mcpName of mcps) {
627
+ const fileName = `${mcpName}.photon.ts`;
628
+ const filePath = path.join(workingDir, fileName);
629
+ // Get installation metadata
630
+ const metadata = await manager.getPhotonInstallMetadata(fileName);
631
+ if (metadata) {
632
+ // Has metadata - show version and status
633
+ const isModified = await manager.isPhotonModified(filePath, fileName);
634
+ const modifiedMark = isModified ? ' ⚠️ modified' : '';
635
+ console.error(` 📦 ${mcpName} (v${metadata.version} from ${metadata.marketplace})${modifiedMark}`);
636
+ }
637
+ else {
638
+ // No metadata - local or pre-metadata Photon
639
+ console.error(` 📦 ${mcpName}`);
640
+ }
641
+ }
642
+ console.error(`\nRun: photon mcp <name> --dev`);
643
+ console.error(`Details: photon get <name>`);
644
+ console.error(`MCP config: photon get --mcp`);
645
+ }
646
+ catch (error) {
647
+ console.error(`❌ Error: ${error.message}`);
648
+ process.exit(1);
649
+ }
650
+ });
651
+ // Search command: search for MCPs across marketplaces
652
+ program
653
+ .command('search')
654
+ .argument('<query>', 'MCP name or keyword to search for')
655
+ .description('Search for MCP in all enabled marketplaces')
656
+ .action(async (query) => {
657
+ try {
658
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
659
+ const manager = new MarketplaceManager();
660
+ await manager.initialize();
661
+ // Auto-update stale caches
662
+ const updated = await manager.autoUpdateStaleCaches();
663
+ if (updated) {
664
+ console.error('🔄 Refreshed marketplace data...\n');
665
+ }
666
+ console.error(`Searching for '${query}' in marketplaces...`);
667
+ const results = await manager.search(query);
668
+ if (results.size === 0) {
669
+ console.error(`❌ No results found for '${query}'`);
670
+ console.error(`Tip: Run 'photon marketplace update' to manually refresh marketplace data`);
671
+ return;
672
+ }
673
+ console.error('');
674
+ for (const [mcpName, entries] of results) {
675
+ for (const entry of entries) {
676
+ if (entry.metadata) {
677
+ console.error(` 📦 ${mcpName} (v${entry.metadata.version})`);
678
+ console.error(` ${entry.metadata.description}`);
679
+ console.error(` ${entry.marketplace.name} (${entry.marketplace.repo})`);
680
+ if (entry.metadata.tags && entry.metadata.tags.length > 0) {
681
+ console.error(` Tags: ${entry.metadata.tags.join(', ')}`);
682
+ }
683
+ }
684
+ else {
685
+ console.error(` 📦 ${mcpName}`);
686
+ console.error(` ✓ ${entry.marketplace.name} (${entry.marketplace.repo})`);
687
+ }
688
+ }
689
+ }
690
+ console.error('');
691
+ }
692
+ catch (error) {
693
+ console.error(`❌ Error: ${error.message}`);
694
+ process.exit(1);
695
+ }
696
+ });
697
+ // Info command: show detailed MCP information
698
+ program
699
+ .command('info')
700
+ .argument('<name>', 'MCP name to show information for')
701
+ .description('Show detailed MCP information from marketplaces')
702
+ .action(async (name) => {
703
+ try {
704
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
705
+ const manager = new MarketplaceManager();
706
+ await manager.initialize();
707
+ // Auto-update stale caches
708
+ await manager.autoUpdateStaleCaches();
709
+ const result = await manager.getPhotonMetadata(name);
710
+ if (!result) {
711
+ console.error(`❌ MCP '${name}' not found in any marketplace`);
712
+ console.error(`Tip: Use 'photon search ${name}' to find similar MCPs`);
713
+ process.exit(1);
714
+ }
715
+ const { metadata, marketplace } = result;
716
+ console.error('');
717
+ console.error(` 📦 ${metadata.name} (v${metadata.version})`);
718
+ console.error(` ${metadata.description}`);
719
+ console.error('');
720
+ console.error(` Marketplace: ${marketplace.name} (${marketplace.repo})`);
721
+ if (metadata.author) {
722
+ console.error(` Author: ${metadata.author}`);
723
+ }
724
+ if (metadata.license) {
725
+ console.error(` License: ${metadata.license}`);
726
+ }
727
+ if (metadata.homepage) {
728
+ console.error(` Homepage: ${metadata.homepage}`);
729
+ }
730
+ if (metadata.tags && metadata.tags.length > 0) {
731
+ console.error(` Tags: ${metadata.tags.join(', ')}`);
732
+ }
733
+ if (metadata.tools && metadata.tools.length > 0) {
734
+ console.error(` Tools: ${metadata.tools.join(', ')}`);
735
+ }
736
+ console.error('');
737
+ console.error(` To add: photon add ${metadata.name}`);
738
+ console.error('');
739
+ }
740
+ catch (error) {
741
+ console.error(`❌ Error: ${error.message}`);
742
+ process.exit(1);
743
+ }
744
+ });
745
+ // Sync command: synchronize local resources
746
+ const sync = program
747
+ .command('sync')
748
+ .description('Synchronize local resources');
749
+ sync
750
+ .command('marketplace')
751
+ .argument('[path]', 'Directory containing Photons (defaults to current directory)', '.')
752
+ .option('--name <name>', 'Marketplace name')
753
+ .option('--description <desc>', 'Marketplace description')
754
+ .option('--owner <owner>', 'Owner name')
755
+ .description('Generate/sync marketplace manifest and documentation')
756
+ .action(async (dirPath, options) => {
757
+ try {
758
+ await performMarketplaceSync(dirPath, options);
759
+ }
760
+ catch (error) {
761
+ console.error(`❌ Error: ${error.message}`);
762
+ if (process.env.DEBUG) {
763
+ console.error(error.stack);
764
+ }
765
+ process.exit(1);
766
+ }
767
+ });
768
+ // Marketplace command: manage MCP marketplaces
769
+ const marketplace = program
770
+ .command('marketplace')
771
+ .description('Manage MCP marketplaces');
772
+ marketplace
773
+ .command('sync', { hidden: true })
774
+ .argument('[path]', 'Directory containing Photons (defaults to current directory)', '.')
775
+ .option('--name <name>', 'Marketplace name')
776
+ .option('--description <desc>', 'Marketplace description')
777
+ .option('--owner <owner>', 'Owner name')
778
+ .description('(Deprecated: use "photon sync marketplace") Generate/sync marketplace manifest and documentation')
779
+ .action(async (dirPath, options) => {
780
+ console.error('⚠️ Note: "photon marketplace sync" is deprecated. Use "photon sync marketplace" instead.\n');
781
+ try {
782
+ await performMarketplaceSync(dirPath, options);
783
+ }
784
+ catch (error) {
785
+ console.error(`❌ Error: ${error.message}`);
786
+ if (process.env.DEBUG) {
787
+ console.error(error.stack);
788
+ }
789
+ process.exit(1);
790
+ }
791
+ });
792
+ // Backwards compatibility: 'init' as hidden alias for 'sync'
793
+ marketplace
794
+ .command('init', { hidden: true })
795
+ .argument('[path]', 'Directory containing Photons (defaults to current directory)', '.')
796
+ .option('--name <name>', 'Marketplace name')
797
+ .option('--description <desc>', 'Marketplace description')
798
+ .option('--owner <owner>', 'Owner name')
799
+ .description('(Deprecated: use "sync") Initialize a directory as a Photon marketplace')
800
+ .action(async (dirPath, options) => {
801
+ console.error('⚠️ "marketplace init" is deprecated. Please use "marketplace sync" instead.\n');
802
+ // Call the same handler - will be refactored into shared function later
803
+ await program.commands.find(cmd => cmd.name() === 'marketplace')
804
+ ?.commands.find(cmd => cmd.name() === 'sync')
805
+ ?.parseAsync(['sync', dirPath], { from: 'user' });
806
+ });
807
+ marketplace
808
+ .command('list')
809
+ .description('List all configured marketplaces')
810
+ .action(async () => {
811
+ try {
812
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
813
+ const manager = new MarketplaceManager();
814
+ await manager.initialize();
815
+ const marketplaces = manager.getAll();
816
+ if (marketplaces.length === 0) {
817
+ console.error('No marketplaces configured');
818
+ return;
819
+ }
820
+ // Get MCP counts
821
+ const counts = await manager.getMarketplaceCounts();
822
+ console.error(`Configured marketplaces (${marketplaces.length}):\n`);
823
+ for (const marketplace of marketplaces) {
824
+ const status = marketplace.enabled ? '✅' : '❌';
825
+ const count = counts.get(marketplace.name) || 0;
826
+ const countStr = count > 0 ? `${count} available` : 'no manifest';
827
+ console.error(` ${status} ${marketplace.name}`);
828
+ console.error(` ${marketplace.repo}`);
829
+ console.error(` ${countStr}`);
830
+ if (marketplace.lastUpdated) {
831
+ const date = new Date(marketplace.lastUpdated);
832
+ console.error(` Updated ${date.toLocaleDateString()}`);
833
+ }
834
+ }
835
+ console.error('');
836
+ }
837
+ catch (error) {
838
+ console.error(`❌ Error: ${error.message}`);
839
+ process.exit(1);
840
+ }
841
+ });
842
+ marketplace
843
+ .command('add')
844
+ .argument('<repo>', 'GitHub repository (username/repo or github.com URL)')
845
+ .description('Add a new MCP marketplace from GitHub')
846
+ .action(async (repo) => {
847
+ try {
848
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
849
+ const manager = new MarketplaceManager();
850
+ await manager.initialize();
851
+ const { marketplace: result, added } = await manager.add(repo);
852
+ if (added) {
853
+ console.error(`✅ Added marketplace: ${result.name}`);
854
+ console.error(`Source: ${repo}`);
855
+ console.error(`URL: ${result.url}`);
856
+ // Auto-fetch marketplace.json
857
+ console.error(`Fetching marketplace metadata...`);
858
+ const success = await manager.updateMarketplaceCache(result.name);
859
+ if (success) {
860
+ console.error(`✅ Marketplace ready to use`);
861
+ }
862
+ }
863
+ else {
864
+ console.error(`ℹ️ Marketplace already exists: ${result.name}`);
865
+ console.error(`Source: ${result.source}`);
866
+ console.error(`Skipping duplicate addition`);
867
+ }
868
+ }
869
+ catch (error) {
870
+ console.error(`❌ Error: ${error.message}`);
871
+ process.exit(1);
872
+ }
873
+ });
874
+ marketplace
875
+ .command('remove')
876
+ .argument('<name>', 'Marketplace name')
877
+ .description('Remove a marketplace')
878
+ .action(async (name) => {
879
+ try {
880
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
881
+ const manager = new MarketplaceManager();
882
+ await manager.initialize();
883
+ const removed = await manager.remove(name);
884
+ if (removed) {
885
+ console.error(`✅ Removed marketplace: ${name}`);
886
+ }
887
+ else {
888
+ console.error(`❌ Marketplace '${name}' not found`);
889
+ process.exit(1);
890
+ }
891
+ }
892
+ catch (error) {
893
+ console.error(`❌ Error: ${error.message}`);
894
+ process.exit(1);
895
+ }
896
+ });
897
+ marketplace
898
+ .command('enable')
899
+ .argument('<name>', 'Marketplace name')
900
+ .description('Enable a marketplace')
901
+ .action(async (name) => {
902
+ try {
903
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
904
+ const manager = new MarketplaceManager();
905
+ await manager.initialize();
906
+ const success = await manager.setEnabled(name, true);
907
+ if (success) {
908
+ console.error(`✅ Enabled marketplace: ${name}`);
909
+ }
910
+ else {
911
+ console.error(`❌ Marketplace '${name}' not found`);
912
+ process.exit(1);
913
+ }
914
+ }
915
+ catch (error) {
916
+ console.error(`❌ Error: ${error.message}`);
917
+ process.exit(1);
918
+ }
919
+ });
920
+ marketplace
921
+ .command('disable')
922
+ .argument('<name>', 'Marketplace name')
923
+ .description('Disable a marketplace')
924
+ .action(async (name) => {
925
+ try {
926
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
927
+ const manager = new MarketplaceManager();
928
+ await manager.initialize();
929
+ const success = await manager.setEnabled(name, false);
930
+ if (success) {
931
+ console.error(`✅ Disabled marketplace: ${name}`);
932
+ }
933
+ else {
934
+ console.error(`❌ Marketplace '${name}' not found`);
935
+ process.exit(1);
936
+ }
937
+ }
938
+ catch (error) {
939
+ console.error(`❌ Error: ${error.message}`);
940
+ process.exit(1);
941
+ }
942
+ });
943
+ marketplace
944
+ .command('update')
945
+ .argument('[name]', 'Marketplace name to update (updates all if omitted)')
946
+ .description('Update marketplace metadata from remote')
947
+ .action(async (name) => {
948
+ try {
949
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
950
+ const manager = new MarketplaceManager();
951
+ await manager.initialize();
952
+ if (name) {
953
+ // Update specific marketplace
954
+ console.error(`Updating ${name}...`);
955
+ const success = await manager.updateMarketplaceCache(name);
956
+ if (success) {
957
+ console.error(`✅ Updated ${name}`);
958
+ }
959
+ else {
960
+ console.error(`❌ Failed to update ${name} (not found or no manifest)`);
961
+ process.exit(1);
962
+ }
963
+ }
964
+ else {
965
+ // Update all enabled marketplaces
966
+ console.error(`Updating all marketplaces...\n`);
967
+ const results = await manager.updateAllCaches();
968
+ for (const [marketplaceName, success] of results) {
969
+ if (success) {
970
+ console.error(` ✅ ${marketplaceName}`);
971
+ }
972
+ else {
973
+ console.error(` ⚠️ ${marketplaceName} (no manifest)`);
974
+ }
975
+ }
976
+ const successCount = Array.from(results.values()).filter(Boolean).length;
977
+ console.error(`\nUpdated ${successCount}/${results.size} marketplaces`);
978
+ }
979
+ }
980
+ catch (error) {
981
+ console.error(`❌ Error: ${error.message}`);
982
+ process.exit(1);
983
+ }
984
+ });
985
+ // Add command: add MCP from marketplace
986
+ program
987
+ .command('add')
988
+ .argument('<name>', 'MCP name to add')
989
+ .option('--marketplace <name>', 'Specific marketplace to use')
990
+ .option('-y, --yes', 'Automatically select first suggestion without prompting')
991
+ .description('Add an MCP from a marketplace')
992
+ .action(async (name, options, command) => {
993
+ try {
994
+ // Get working directory from global options
995
+ const workingDir = command.parent?.opts().workingDir || DEFAULT_WORKING_DIR;
996
+ await ensureWorkingDir(workingDir);
997
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
998
+ const manager = new MarketplaceManager();
999
+ await manager.initialize();
1000
+ // Check for conflicts
1001
+ let conflict = await manager.checkConflict(name, options.marketplace);
1002
+ if (!conflict.sources || conflict.sources.length === 0) {
1003
+ console.error(`❌ MCP '${name}' not found in any enabled marketplace\n`);
1004
+ // Search for similar names
1005
+ const searchResults = await manager.search(name);
1006
+ if (searchResults.size > 0) {
1007
+ console.error(`Did you mean one of these?\n`);
1008
+ // Convert search results to array for selection
1009
+ const suggestions = [];
1010
+ let count = 0;
1011
+ for (const [mcpName, sources] of searchResults) {
1012
+ if (count >= 5)
1013
+ break; // Limit to 5 suggestions
1014
+ const source = sources[0]; // Use first marketplace
1015
+ const version = source.metadata?.version || 'unknown';
1016
+ const description = source.metadata?.description || 'No description';
1017
+ suggestions.push({ name: mcpName, version, description });
1018
+ console.error(` [${count + 1}] ${mcpName} (v${version})`);
1019
+ console.error(` ${description}`);
1020
+ count++;
1021
+ }
1022
+ // Interactive selection or auto-select with -y
1023
+ let selectedIndex;
1024
+ if (options.yes) {
1025
+ // Auto-select first suggestion
1026
+ selectedIndex = 0;
1027
+ }
1028
+ else {
1029
+ // Interactive selection
1030
+ selectedIndex = await new Promise((resolve) => {
1031
+ const rl = readline.createInterface({
1032
+ input: process.stdin,
1033
+ output: process.stderr,
1034
+ });
1035
+ const askQuestion = () => {
1036
+ rl.question(`\nWhich one? [1-${suggestions.length}] (or press Enter to cancel): `, (answer) => {
1037
+ const trimmed = answer.trim();
1038
+ // Empty input = cancel
1039
+ if (trimmed === '') {
1040
+ rl.close();
1041
+ resolve(null);
1042
+ return;
1043
+ }
1044
+ const choice = parseInt(trimmed, 10);
1045
+ // Validate input
1046
+ if (isNaN(choice) || choice < 1 || choice > suggestions.length) {
1047
+ console.error(`Invalid choice. Please enter a number between 1 and ${suggestions.length}.`);
1048
+ askQuestion();
1049
+ }
1050
+ else {
1051
+ rl.close();
1052
+ resolve(choice - 1);
1053
+ }
1054
+ });
1055
+ };
1056
+ askQuestion();
1057
+ });
1058
+ if (selectedIndex === null) {
1059
+ console.error('\nCancelled.');
1060
+ process.exit(0);
1061
+ }
1062
+ }
1063
+ // Update name to the selected MCP
1064
+ name = suggestions[selectedIndex].name;
1065
+ console.error(`\n✓ Selected: ${name}`);
1066
+ // Re-check for conflicts with the new name
1067
+ conflict = await manager.checkConflict(name, options.marketplace);
1068
+ if (!conflict.sources || conflict.sources.length === 0) {
1069
+ console.error(`❌ MCP '${name}' is no longer available`);
1070
+ process.exit(1);
1071
+ }
1072
+ }
1073
+ else {
1074
+ console.error(`Run 'photon get' to see all available MCPs`);
1075
+ process.exit(1);
1076
+ }
1077
+ }
1078
+ // Check if already exists locally
1079
+ const filePath = path.join(workingDir, `${name}.photon.ts`);
1080
+ const fileName = `${name}.photon.ts`;
1081
+ if (existsSync(filePath)) {
1082
+ console.error(`⚠️ MCP '${name}' already exists`);
1083
+ console.error(`Use 'photon upgrade ${name}' to update it`);
1084
+ process.exit(1);
1085
+ }
1086
+ // Handle conflicts
1087
+ let selectedMarketplace;
1088
+ let selectedMetadata;
1089
+ if (conflict.hasConflict) {
1090
+ console.error(`⚠️ MCP '${name}' found in multiple marketplaces:\n`);
1091
+ conflict.sources.forEach((source, index) => {
1092
+ const marker = source.marketplace.name === conflict.recommendation ? '→' : ' ';
1093
+ const version = source.metadata?.version || 'unknown';
1094
+ console.error(` ${marker} [${index + 1}] ${source.marketplace.name} (v${version})`);
1095
+ console.error(` ${source.marketplace.repo || source.marketplace.url}`);
1096
+ });
1097
+ if (conflict.recommendation) {
1098
+ console.error(`\n💡 Recommended: ${conflict.recommendation} (newest version)`);
1099
+ }
1100
+ // Get default choice (recommended or first)
1101
+ const recommendedIndex = conflict.sources.findIndex(s => s.marketplace.name === conflict.recommendation);
1102
+ const defaultChoice = recommendedIndex !== -1 ? recommendedIndex + 1 : 1;
1103
+ // Interactive selection
1104
+ const selectedIndex = await new Promise((resolve) => {
1105
+ const rl = readline.createInterface({
1106
+ input: process.stdin,
1107
+ output: process.stderr,
1108
+ });
1109
+ const askQuestion = () => {
1110
+ rl.question(`\nWhich marketplace? [1-${conflict.sources.length}] (default: ${defaultChoice}): `, (answer) => {
1111
+ const trimmed = answer.trim();
1112
+ // Empty input = use default
1113
+ if (trimmed === '') {
1114
+ rl.close();
1115
+ resolve(defaultChoice - 1);
1116
+ return;
1117
+ }
1118
+ const choice = parseInt(trimmed, 10);
1119
+ // Validate input
1120
+ if (isNaN(choice) || choice < 1 || choice > conflict.sources.length) {
1121
+ console.error(`Invalid choice. Please enter a number between 1 and ${conflict.sources.length}.`);
1122
+ askQuestion();
1123
+ }
1124
+ else {
1125
+ rl.close();
1126
+ resolve(choice - 1);
1127
+ }
1128
+ });
1129
+ };
1130
+ askQuestion();
1131
+ });
1132
+ const selectedSource = conflict.sources[selectedIndex];
1133
+ selectedMarketplace = selectedSource.marketplace;
1134
+ selectedMetadata = selectedSource.metadata;
1135
+ console.error(`\n✓ Using: ${selectedMarketplace.name}`);
1136
+ }
1137
+ else {
1138
+ selectedMarketplace = conflict.sources[0].marketplace;
1139
+ selectedMetadata = conflict.sources[0].metadata;
1140
+ console.error(`Adding ${name} from ${selectedMarketplace.name}...`);
1141
+ }
1142
+ // Fetch content from selected marketplace
1143
+ const result = await manager.fetchMCP(name);
1144
+ if (!result) {
1145
+ console.error(`❌ Failed to fetch MCP content`);
1146
+ process.exit(1);
1147
+ }
1148
+ const content = result.content;
1149
+ // Write file
1150
+ await fs.writeFile(filePath, content, 'utf-8');
1151
+ // Save installation metadata if we have it
1152
+ if (selectedMetadata) {
1153
+ const { calculateHash } = await import('./marketplace-manager.js');
1154
+ const contentHash = calculateHash(content);
1155
+ await manager.savePhotonMetadata(fileName, selectedMarketplace, selectedMetadata, contentHash);
1156
+ }
1157
+ console.error(`✅ Added ${name} from ${selectedMarketplace.name}`);
1158
+ if (selectedMetadata?.version) {
1159
+ console.error(`Version: ${selectedMetadata.version}`);
1160
+ }
1161
+ console.error(`Location: ${filePath}`);
1162
+ console.error(`Run with: photon mcp ${name} --dev`);
1163
+ }
1164
+ catch (error) {
1165
+ console.error(`❌ Error: ${error.message}`);
1166
+ process.exit(1);
1167
+ }
1168
+ });
1169
+ // Upgrade command: update MCPs from marketplace
1170
+ program
1171
+ .command('upgrade')
1172
+ .argument('[name]', 'MCP name to upgrade (upgrades all if omitted)')
1173
+ .option('--check', 'Check for updates without upgrading')
1174
+ .description('Upgrade MCP(s) from marketplaces')
1175
+ .action(async (name, options, command) => {
1176
+ try {
1177
+ // Get working directory from global options
1178
+ const workingDir = command.parent?.opts().workingDir || DEFAULT_WORKING_DIR;
1179
+ const { VersionChecker } = await import('./version-checker.js');
1180
+ const checker = new VersionChecker();
1181
+ await checker.initialize();
1182
+ if (name) {
1183
+ // Upgrade single MCP
1184
+ const filePath = await resolvePhotonPath(name, workingDir);
1185
+ if (!filePath) {
1186
+ console.error(`❌ MCP not found: ${name}`);
1187
+ console.error(`Searched in: ${workingDir}`);
1188
+ process.exit(1);
1189
+ }
1190
+ console.error(`Checking ${name} for updates...`);
1191
+ const versionInfo = await checker.checkForUpdate(name, filePath);
1192
+ if (!versionInfo.local) {
1193
+ console.error(`⚠️ Could not determine local version`);
1194
+ return;
1195
+ }
1196
+ if (!versionInfo.remote) {
1197
+ console.error(`⚠️ Not found in any marketplace. This might be a local-only MCP.`);
1198
+ return;
1199
+ }
1200
+ if (options.check) {
1201
+ if (versionInfo.needsUpdate) {
1202
+ console.error(`🔄 Update available: ${versionInfo.local} → ${versionInfo.remote}`);
1203
+ }
1204
+ else {
1205
+ console.error(`✅ Already up to date (${versionInfo.local})`);
1206
+ }
1207
+ return;
1208
+ }
1209
+ if (!versionInfo.needsUpdate) {
1210
+ console.error(`✅ Already up to date (${versionInfo.local})`);
1211
+ return;
1212
+ }
1213
+ console.error(`🔄 Upgrading ${name}: ${versionInfo.local} → ${versionInfo.remote}`);
1214
+ const success = await checker.updateMCP(name, filePath);
1215
+ if (success) {
1216
+ console.error(`✅ Successfully upgraded ${name} to ${versionInfo.remote}`);
1217
+ }
1218
+ else {
1219
+ console.error(`❌ Failed to upgrade ${name}`);
1220
+ process.exit(1);
1221
+ }
1222
+ }
1223
+ else {
1224
+ // Check/upgrade all MCPs
1225
+ console.error(`Checking all MCPs in ${workingDir}...\n`);
1226
+ const updates = await checker.checkAllUpdates(workingDir);
1227
+ if (updates.size === 0) {
1228
+ console.error(`No MCPs found`);
1229
+ return;
1230
+ }
1231
+ const needsUpdate = [];
1232
+ for (const [mcpName, info] of updates) {
1233
+ const status = checker.formatVersionInfo(info);
1234
+ if (info.needsUpdate) {
1235
+ console.error(` 🔄 ${mcpName}: ${status}`);
1236
+ needsUpdate.push(mcpName);
1237
+ }
1238
+ else if (info.local && info.remote) {
1239
+ console.error(` ✅ ${mcpName}: ${status}`);
1240
+ }
1241
+ else {
1242
+ console.error(` 📦 ${mcpName}: ${status}`);
1243
+ }
1244
+ }
1245
+ if (needsUpdate.length === 0) {
1246
+ console.error(`\nAll MCPs are up to date!`);
1247
+ return;
1248
+ }
1249
+ if (options.check) {
1250
+ console.error(`\n${needsUpdate.length} MCP(s) have updates available`);
1251
+ console.error(`Run 'photon upgrade' to upgrade all`);
1252
+ return;
1253
+ }
1254
+ // Upgrade all that need updates
1255
+ console.error(`\nUpgrading ${needsUpdate.length} MCP(s)...`);
1256
+ for (const mcpName of needsUpdate) {
1257
+ const filePath = path.join(workingDir, `${mcpName}.photon.ts`);
1258
+ const success = await checker.updateMCP(mcpName, filePath);
1259
+ if (success) {
1260
+ console.error(`✅ Upgraded ${mcpName}`);
1261
+ }
1262
+ else {
1263
+ console.error(`❌ Failed to upgrade ${mcpName}`);
1264
+ }
1265
+ }
1266
+ }
1267
+ }
1268
+ catch (error) {
1269
+ console.error(`❌ Error: ${error.message}`);
1270
+ process.exit(1);
1271
+ }
1272
+ });
1273
+ // Audit command: security scan for MCP dependencies
1274
+ program
1275
+ .command('audit')
1276
+ .argument('[name]', 'MCP name to audit (audits all if omitted)')
1277
+ .description('Security audit of MCP dependencies')
1278
+ .action(async (name, options, command) => {
1279
+ try {
1280
+ const workingDir = command.parent?.opts().workingDir || DEFAULT_WORKING_DIR;
1281
+ const { DependencyManager } = await import('./dependency-manager.js');
1282
+ const { SecurityScanner } = await import('./security-scanner.js');
1283
+ const depManager = new DependencyManager();
1284
+ const scanner = new SecurityScanner();
1285
+ if (name) {
1286
+ // Audit single MCP
1287
+ const filePath = await resolvePhotonPath(name, workingDir);
1288
+ if (!filePath) {
1289
+ console.error(`❌ MCP not found: ${name}`);
1290
+ process.exit(1);
1291
+ }
1292
+ console.error(`🔍 Auditing dependencies for: ${name}\n`);
1293
+ const dependencies = await depManager.extractDependencies(filePath);
1294
+ if (dependencies.length === 0) {
1295
+ console.error('✅ No dependencies to audit');
1296
+ return;
1297
+ }
1298
+ const depStrings = dependencies.map(d => `${d.name}@${d.version}`);
1299
+ const result = await scanner.auditMCP(name, depStrings);
1300
+ console.error(scanner.formatAuditResult(result));
1301
+ if (result.totalVulnerabilities > 0) {
1302
+ console.error('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1303
+ console.error('Vulnerabilities found:');
1304
+ result.dependencies.forEach(dep => {
1305
+ if (dep.hasVulnerabilities) {
1306
+ console.error(`\n📦 ${dep.dependency}@${dep.version}`);
1307
+ dep.vulnerabilities.forEach(vuln => {
1308
+ const symbol = scanner.getSeveritySymbol(vuln.severity);
1309
+ console.error(` ${symbol} ${vuln.severity.toUpperCase()}: ${vuln.title}`);
1310
+ if (vuln.url) {
1311
+ console.error(` ${vuln.url}`);
1312
+ }
1313
+ });
1314
+ }
1315
+ });
1316
+ console.error('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1317
+ console.error('\n💡 To fix vulnerabilities:');
1318
+ console.error(` 1. Update dependency versions in @dependencies JSDoc tag`);
1319
+ console.error(` 2. Clear cache: photon clear-cache`);
1320
+ console.error(` 3. Restart MCP to reinstall with new versions`);
1321
+ if (result.criticalCount > 0 || result.highCount > 0) {
1322
+ process.exit(1);
1323
+ }
1324
+ }
1325
+ }
1326
+ else {
1327
+ // Audit all MCPs
1328
+ console.error('🔍 Auditing all MCPs...\n');
1329
+ const mcps = await listPhotonMCPs(workingDir);
1330
+ if (mcps.length === 0) {
1331
+ console.error('No MCPs found');
1332
+ return;
1333
+ }
1334
+ let totalVulnerabilities = 0;
1335
+ let mcpsWithVulnerabilities = 0;
1336
+ for (const mcp of mcps) {
1337
+ const mcpPath = path.join(workingDir, mcp);
1338
+ const mcpName = path.basename(mcp, '.photon.ts');
1339
+ const dependencies = await depManager.extractDependencies(mcpPath);
1340
+ if (dependencies.length === 0) {
1341
+ console.error(`✅ ${mcpName}: No dependencies`);
1342
+ continue;
1343
+ }
1344
+ const depStrings = dependencies.map(d => `${d.name}@${d.version}`);
1345
+ const result = await scanner.auditMCP(mcpName, depStrings);
1346
+ console.error(scanner.formatAuditResult(result));
1347
+ if (result.totalVulnerabilities > 0) {
1348
+ totalVulnerabilities += result.totalVulnerabilities;
1349
+ mcpsWithVulnerabilities++;
1350
+ }
1351
+ }
1352
+ console.error('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1353
+ console.error(`\nSummary: ${totalVulnerabilities} vulnerabilities in ${mcpsWithVulnerabilities}/${mcps.length} MCPs`);
1354
+ if (totalVulnerabilities > 0) {
1355
+ console.error('\nRun: photon audit <name> # for detailed vulnerability info');
1356
+ process.exit(1);
1357
+ }
1358
+ }
1359
+ }
1360
+ catch (error) {
1361
+ console.error(`❌ Error: ${error.message}`);
1362
+ process.exit(1);
1363
+ }
1364
+ });
1365
+ // Conflicts command: show MCPs available in multiple marketplaces
1366
+ program
1367
+ .command('conflicts')
1368
+ .description('Show MCPs available in multiple marketplaces')
1369
+ .action(async () => {
1370
+ try {
1371
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
1372
+ const manager = new MarketplaceManager();
1373
+ await manager.initialize();
1374
+ console.error('🔍 Scanning for MCP conflicts across marketplaces...\n');
1375
+ const conflicts = await manager.detectAllConflicts();
1376
+ if (conflicts.size === 0) {
1377
+ console.error('✅ No conflicts detected');
1378
+ console.error('\nAll MCPs are uniquely available from single marketplaces.');
1379
+ return;
1380
+ }
1381
+ console.error(`⚠️ Found ${conflicts.size} MCP(s) in multiple marketplaces:\n`);
1382
+ for (const [mcpName, sources] of conflicts) {
1383
+ console.error(`📦 ${mcpName}`);
1384
+ sources.forEach(source => {
1385
+ const version = source.metadata?.version || 'unknown';
1386
+ const description = source.metadata?.description || '';
1387
+ console.error(` → ${source.marketplace.name} (v${version})`);
1388
+ if (description) {
1389
+ console.error(` ${description.substring(0, 60)}${description.length > 60 ? '...' : ''}`);
1390
+ }
1391
+ });
1392
+ // Show recommendation
1393
+ const conflict = await manager.checkConflict(mcpName);
1394
+ if (conflict.recommendation) {
1395
+ console.error(` 💡 Recommended: ${conflict.recommendation}`);
1396
+ }
1397
+ console.error('');
1398
+ }
1399
+ console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1400
+ console.error('\n💡 Tip: Use --marketplace flag to specify which to use:');
1401
+ console.error(' photon add <name> --marketplace <marketplace-name>');
1402
+ console.error('\nOr disable marketplaces you don\'t need:');
1403
+ console.error(' photon marketplace disable <marketplace-name>');
1404
+ }
1405
+ catch (error) {
1406
+ console.error(`❌ Error: ${error.message}`);
1407
+ process.exit(1);
1408
+ }
1409
+ });
1410
+ program.parse();
1411
+ /**
1412
+ * Inline template fallback
1413
+ */
1414
+ function getInlineTemplate() {
1415
+ return `/**
1416
+ * TemplateName Photon MCP
1417
+ *
1418
+ * Single-file MCP server using Photon
1419
+ */
1420
+
1421
+ export default class TemplateName {
1422
+ /**
1423
+ * Example tool
1424
+ * @param message Message to echo
1425
+ */
1426
+ async echo(params: { message: string }) {
1427
+ return \`Echo: \${params.message}\`;
1428
+ }
1429
+
1430
+ /**
1431
+ * Add two numbers
1432
+ * @param a First number
1433
+ * @param b Second number
1434
+ */
1435
+ async add(params: { a: number; b: number }) {
1436
+ return params.a + params.b;
1437
+ }
1438
+ }
1439
+ `;
1440
+ }
1441
+ //# sourceMappingURL=cli.js.map