@portel/photon 1.2.0 → 1.4.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 (63) hide show
  1. package/README.md +319 -60
  2. package/dist/cli-alias.d.ts +21 -0
  3. package/dist/cli-alias.d.ts.map +1 -0
  4. package/dist/cli-alias.js +232 -0
  5. package/dist/cli-alias.js.map +1 -0
  6. package/dist/cli-formatter.d.ts +25 -0
  7. package/dist/cli-formatter.d.ts.map +1 -0
  8. package/dist/cli-formatter.js +326 -0
  9. package/dist/cli-formatter.js.map +1 -0
  10. package/dist/cli.js +961 -484
  11. package/dist/cli.js.map +1 -1
  12. package/dist/daemon/client.d.ts +15 -0
  13. package/dist/daemon/client.d.ts.map +1 -0
  14. package/dist/daemon/client.js +126 -0
  15. package/dist/daemon/client.js.map +1 -0
  16. package/dist/daemon/manager.d.ts +32 -0
  17. package/dist/daemon/manager.d.ts.map +1 -0
  18. package/dist/daemon/manager.js +155 -0
  19. package/dist/daemon/manager.js.map +1 -0
  20. package/dist/daemon/protocol.d.ts +48 -0
  21. package/dist/daemon/protocol.d.ts.map +1 -0
  22. package/dist/daemon/protocol.js +7 -0
  23. package/dist/daemon/protocol.js.map +1 -0
  24. package/dist/daemon/server.d.ts +12 -0
  25. package/dist/daemon/server.d.ts.map +1 -0
  26. package/dist/daemon/server.js +215 -0
  27. package/dist/daemon/server.js.map +1 -0
  28. package/dist/daemon/session-manager.d.ts +46 -0
  29. package/dist/daemon/session-manager.d.ts.map +1 -0
  30. package/dist/daemon/session-manager.js +120 -0
  31. package/dist/daemon/session-manager.js.map +1 -0
  32. package/dist/index.d.ts +13 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +15 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/loader.d.ts +16 -1
  37. package/dist/loader.d.ts.map +1 -1
  38. package/dist/loader.js +125 -19
  39. package/dist/loader.js.map +1 -1
  40. package/dist/path-resolver.d.ts +12 -19
  41. package/dist/path-resolver.d.ts.map +1 -1
  42. package/dist/path-resolver.js +35 -61
  43. package/dist/path-resolver.js.map +1 -1
  44. package/dist/photon-cli-runner.d.ts +15 -0
  45. package/dist/photon-cli-runner.d.ts.map +1 -0
  46. package/dist/photon-cli-runner.js +1124 -0
  47. package/dist/photon-cli-runner.js.map +1 -0
  48. package/dist/photon-doc-extractor.d.ts +10 -0
  49. package/dist/photon-doc-extractor.d.ts.map +1 -1
  50. package/dist/photon-doc-extractor.js +89 -9
  51. package/dist/photon-doc-extractor.js.map +1 -1
  52. package/dist/schema-extractor.d.ts +27 -0
  53. package/dist/schema-extractor.d.ts.map +1 -1
  54. package/dist/schema-extractor.js +333 -2
  55. package/dist/schema-extractor.js.map +1 -1
  56. package/dist/server.d.ts.map +1 -1
  57. package/dist/server.js +16 -8
  58. package/dist/server.js.map +1 -1
  59. package/dist/template-manager.js +1 -1
  60. package/dist/types.d.ts +1 -0
  61. package/dist/types.d.ts.map +1 -1
  62. package/dist/types.js.map +1 -1
  63. package/package.json +7 -4
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import * as readline from 'readline';
13
13
  import { PhotonServer } from './server.js';
14
14
  import { FileWatcher } from './watcher.js';
15
15
  import { resolvePhotonPath, listPhotonMCPs, ensureWorkingDir, DEFAULT_WORKING_DIR } from './path-resolver.js';
16
- import { SchemaExtractor } from './schema-extractor.js';
16
+ import { SchemaExtractor } from '@portel/photon-core';
17
17
  import { createRequire } from 'module';
18
18
  import { fileURLToPath } from 'url';
19
19
  const require = createRequire(import.meta.url);
@@ -75,6 +75,7 @@ async function ensureGitignore(workingDir) {
75
75
  */
76
76
  async function performMarketplaceSync(dirPath, options) {
77
77
  const resolvedPath = path.resolve(dirPath);
78
+ const isDefaultDir = resolvedPath === DEFAULT_WORKING_DIR;
78
79
  if (!existsSync(resolvedPath)) {
79
80
  console.error(`❌ Directory not found: ${resolvedPath}`);
80
81
  process.exit(1);
@@ -82,7 +83,22 @@ async function performMarketplaceSync(dirPath, options) {
82
83
  // Scan for .photon.ts files
83
84
  console.error('📦 Scanning for .photon.ts files...');
84
85
  const files = await fs.readdir(resolvedPath);
85
- const photonFiles = files.filter(f => f.endsWith('.photon.ts'));
86
+ let photonFiles = files.filter(f => f.endsWith('.photon.ts'));
87
+ // Filter out installed photons if requested (for ~/.photon)
88
+ if (options.filterInstalled && isDefaultDir) {
89
+ const { readLocalMetadata } = await import('./marketplace-manager.js');
90
+ const metadata = await readLocalMetadata();
91
+ // Metadata keys may include .photon.ts extension
92
+ const installedNames = new Set(Object.keys(metadata.photons || {}).map(k => k.replace(/\.photon\.ts$/, '')));
93
+ const originalCount = photonFiles.length;
94
+ photonFiles = photonFiles.filter(f => {
95
+ const name = f.replace(/\.photon\.ts$/, '');
96
+ return !installedNames.has(name);
97
+ });
98
+ if (originalCount !== photonFiles.length) {
99
+ console.error(` Filtered out ${originalCount - photonFiles.length} installed photons`);
100
+ }
101
+ }
86
102
  if (photonFiles.length === 0) {
87
103
  console.error(`❌ No .photon.ts files found in ${resolvedPath}`);
88
104
  process.exit(1);
@@ -177,6 +193,99 @@ async function performMarketplaceSync(dirPath, options) {
177
193
  console.error(` • *.md (${photons.length} documentation files at root)`);
178
194
  console.error(` • README.md (auto-generated table)`);
179
195
  }
196
+ /**
197
+ * Initialize a marketplace with git hooks
198
+ */
199
+ async function performMarketplaceInit(dirPath, options) {
200
+ const absolutePath = path.resolve(dirPath);
201
+ // Check if directory exists
202
+ if (!existsSync(absolutePath)) {
203
+ await fs.mkdir(absolutePath, { recursive: true });
204
+ console.error(`📁 Created directory: ${absolutePath}`);
205
+ }
206
+ // Check if it's a git repository
207
+ const gitDir = path.join(absolutePath, '.git');
208
+ if (!existsSync(gitDir)) {
209
+ console.error('⚠️ Not a git repository. Initialize with: git init');
210
+ process.exit(1);
211
+ }
212
+ // Create .githooks directory
213
+ const hooksDir = path.join(absolutePath, '.githooks');
214
+ await fs.mkdir(hooksDir, { recursive: true });
215
+ // Create pre-commit hook
216
+ const preCommitHook = `#!/bin/bash
217
+ # Pre-commit hook: Auto-sync marketplace manifest before commit
218
+ # This ensures .marketplace/photons.json and .claude-plugin/ are always up-to-date
219
+
220
+ # Check if any .photon.ts files or marketplace files are being committed
221
+ if git diff --cached --name-only | grep -qE '\\.photon\\.ts$|\\.marketplace/|\\.claude-plugin/'; then
222
+ echo "🔄 Syncing marketplace manifest..."
223
+
224
+ # Run photon maker sync with --claude-code to generate plugin files
225
+ if photon maker sync --dir . --claude-code; then
226
+ # Stage the generated files
227
+ git add .marketplace/photons.json README.md *.md .claude-plugin/ 2>/dev/null
228
+ echo "✅ Marketplace and Claude Code plugin synced and staged"
229
+ else
230
+ echo "❌ Failed to sync marketplace"
231
+ exit 1
232
+ fi
233
+ fi
234
+
235
+ exit 0
236
+ `;
237
+ const preCommitPath = path.join(hooksDir, 'pre-commit');
238
+ await fs.writeFile(preCommitPath, preCommitHook, { mode: 0o755 });
239
+ console.error('✅ Created .githooks/pre-commit');
240
+ // Create setup script
241
+ const setupScript = `#!/bin/bash
242
+ # Setup script to install git hooks for this marketplace
243
+
244
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
245
+ HOOKS_DIR="$REPO_ROOT/.git/hooks"
246
+ SOURCE_HOOKS="$REPO_ROOT/.githooks"
247
+
248
+ echo "🔧 Installing git hooks for Photon marketplace..."
249
+
250
+ # Copy pre-commit hook
251
+ if [ -f "$SOURCE_HOOKS/pre-commit" ]; then
252
+ cp "$SOURCE_HOOKS/pre-commit" "$HOOKS_DIR/pre-commit"
253
+ chmod +x "$HOOKS_DIR/pre-commit"
254
+ echo "✅ Installed pre-commit hook (auto-syncs marketplace manifest)"
255
+ else
256
+ echo "❌ pre-commit hook not found"
257
+ exit 1
258
+ fi
259
+
260
+ echo ""
261
+ echo "✅ Git hooks installed successfully!"
262
+ echo ""
263
+ echo "The pre-commit hook will automatically run 'photon maker sync'"
264
+ echo "whenever you commit changes to .photon.ts files."
265
+ `;
266
+ const setupPath = path.join(hooksDir, 'setup.sh');
267
+ await fs.writeFile(setupPath, setupScript, { mode: 0o755 });
268
+ console.error('✅ Created .githooks/setup.sh');
269
+ // Install hooks to .git/hooks
270
+ const gitHooksDir = path.join(absolutePath, '.git', 'hooks');
271
+ const gitPreCommitPath = path.join(gitHooksDir, 'pre-commit');
272
+ await fs.writeFile(gitPreCommitPath, preCommitHook, { mode: 0o755 });
273
+ console.error('✅ Installed hooks to .git/hooks');
274
+ // Create .marketplace directory
275
+ const marketplaceDir = path.join(absolutePath, '.marketplace');
276
+ await fs.mkdir(marketplaceDir, { recursive: true });
277
+ console.error('✅ Created .marketplace directory');
278
+ // Run initial sync (don't filter installed for marketplace repos)
279
+ console.error('\n🔄 Running initial marketplace sync...\n');
280
+ await performMarketplaceSync(absolutePath, options);
281
+ console.error('\n✅ Marketplace initialized successfully!');
282
+ console.error('\nNext steps:');
283
+ console.error('1. Add your .photon.ts files to this directory');
284
+ console.error('2. Commit your changes (hooks will auto-sync)');
285
+ console.error('3. Push to GitHub to share your marketplace');
286
+ console.error('\nContributors can setup hooks with:');
287
+ console.error(' bash .githooks/setup.sh');
288
+ }
180
289
  /**
181
290
  * Format default value for display in config
182
291
  */
@@ -185,7 +294,8 @@ function formatDefaultValue(value) {
185
294
  // Check if it's a function call expression
186
295
  if (value.includes('homedir()')) {
187
296
  // Replace homedir() with actual home directory
188
- return value.replace(/join\(homedir\(\),\s*['"]([^'"]+)['"]\)/g, (_, folderName) => {
297
+ // Handle both path.join() and join()
298
+ return value.replace(/(?:path\.)?join\(homedir\(\),\s*['"]([^'"]+)['"]\)/g, (_, folderName) => {
189
299
  return path.join(os.homedir(), folderName);
190
300
  });
191
301
  }
@@ -316,7 +426,7 @@ async function validateConfiguration(filePath, mcpName) {
316
426
  /**
317
427
  * Show configuration template for an MCP
318
428
  */
319
- async function showConfigTemplate(filePath, mcpName) {
429
+ async function showConfigTemplate(filePath, mcpName, workingDir = DEFAULT_WORKING_DIR) {
320
430
  console.log(`📋 Configuration template for: ${mcpName}\n`);
321
431
  const params = await extractConstructorParams(filePath);
322
432
  if (params.length === 0) {
@@ -345,11 +455,14 @@ async function showConfigTemplate(filePath, mcpName) {
345
455
  envExample[envVarName] = `<your-${param.name}>`;
346
456
  }
347
457
  });
458
+ const needsWorkingDir = workingDir !== DEFAULT_WORKING_DIR;
348
459
  const config = {
349
460
  mcpServers: {
350
461
  [mcpName]: {
351
462
  command: 'npx',
352
- args: ['@portel/photon', 'mcp', mcpName],
463
+ args: needsWorkingDir
464
+ ? ['@portel/photon', 'mcp', mcpName, '--dir', workingDir]
465
+ : ['@portel/photon', 'mcp', mcpName],
353
466
  env: envExample,
354
467
  },
355
468
  },
@@ -373,10 +486,94 @@ program
373
486
  .name('photon')
374
487
  .description('Universal runtime for single-file TypeScript programs')
375
488
  .version(version)
376
- .option('--working-dir <dir>', 'Working directory for Photons (default: ~/.photon)', DEFAULT_WORKING_DIR);
489
+ .option('--dir <path>', 'Photon directory (default: ~/.photon)', DEFAULT_WORKING_DIR)
490
+ .configureHelp({
491
+ sortSubcommands: false,
492
+ sortOptions: false,
493
+ })
494
+ .addHelpText('after', `
495
+ Runtime Commands:
496
+ mcp <name> Run a photon as MCP server (for AI assistants)
497
+ cli <photon> [method] Run photon methods from command line
498
+
499
+ Package Management:
500
+ add <name> Install a photon from marketplace
501
+ remove <name> Remove an installed photon
502
+ upgrade [name] Upgrade photon(s) to latest version
503
+ search <query> Search marketplaces for photons
504
+ info [name] Show installed photons and details
505
+
506
+ Maintenance:
507
+ update Refresh marketplace indexes & check CLI version
508
+ doctor [name] Diagnose environment and installations
509
+
510
+ Development:
511
+ maker new <name> Create a new photon from template
512
+ maker validate <name> Validate photon syntax and schemas
513
+ maker sync Generate marketplace manifest
514
+ maker init Initialize marketplace with git hooks
515
+
516
+ Advanced:
517
+ marketplace Manage marketplace sources
518
+ alias <photon> Create CLI shortcuts for photons
519
+
520
+ Run 'photon <command> --help' for detailed usage.
521
+ `);
522
+ // Update command: refresh marketplace indexes and check for CLI updates
523
+ program
524
+ .command('update', { hidden: true })
525
+ .description('Update marketplace indexes and check for CLI updates')
526
+ .action(async () => {
527
+ try {
528
+ const { printInfo, printSuccess, printWarning } = await import('./cli-formatter.js');
529
+ // Update all marketplace caches
530
+ printInfo('Refreshing marketplace indexes...\n');
531
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
532
+ const manager = new MarketplaceManager();
533
+ await manager.initialize();
534
+ const results = await manager.updateAllCaches();
535
+ for (const [marketplaceName, success] of results) {
536
+ if (success) {
537
+ printSuccess(`${marketplaceName}`);
538
+ }
539
+ else {
540
+ printWarning(`${marketplaceName} (no manifest)`);
541
+ }
542
+ }
543
+ const successCount = Array.from(results.values()).filter(Boolean).length;
544
+ console.log('');
545
+ printInfo(`Updated ${successCount}/${results.size} marketplaces`);
546
+ // Check for CLI updates
547
+ console.log('');
548
+ printInfo('Checking for Photon CLI updates...');
549
+ try {
550
+ const { execSync } = await import('child_process');
551
+ const latestVersion = execSync('npm view @portel/photon version', {
552
+ encoding: 'utf-8',
553
+ timeout: 10000,
554
+ }).trim();
555
+ if (latestVersion && latestVersion !== version) {
556
+ console.log('');
557
+ printWarning(`New Photon version available: ${version} → ${latestVersion}`);
558
+ printInfo(`Update with: npm install -g @portel/photon`);
559
+ }
560
+ else {
561
+ printSuccess(`Photon CLI is up to date (${version})`);
562
+ }
563
+ }
564
+ catch {
565
+ printWarning('Could not check for CLI updates');
566
+ }
567
+ }
568
+ catch (error) {
569
+ const { printError } = await import('./cli-formatter.js');
570
+ printError(error.message);
571
+ process.exit(1);
572
+ }
573
+ });
377
574
  // MCP Runtime: run a .photon.ts file as MCP server
378
575
  program
379
- .command('mcp')
576
+ .command('mcp', { hidden: true })
380
577
  .argument('<name>', 'MCP name (without .photon.ts extension)')
381
578
  .description('Run a Photon as MCP server')
382
579
  .option('--dev', 'Enable development mode with hot reload')
@@ -385,7 +582,7 @@ program
385
582
  .action(async (name, options, command) => {
386
583
  try {
387
584
  // Get working directory from global options
388
- const workingDir = program.opts().workingDir || DEFAULT_WORKING_DIR;
585
+ const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
389
586
  // Resolve file path from name in working directory
390
587
  const filePath = await resolvePhotonPath(name, workingDir);
391
588
  if (!filePath) {
@@ -401,7 +598,7 @@ program
401
598
  }
402
599
  // Handle --config flag
403
600
  if (options.config) {
404
- await showConfigTemplate(filePath, name);
601
+ await showConfigTemplate(filePath, name, workingDir);
405
602
  return;
406
603
  }
407
604
  // Start MCP server
@@ -437,103 +634,20 @@ program
437
634
  process.exit(1);
438
635
  }
439
636
  });
440
- // Init command: create a new .photon.ts from template
441
- program
442
- .command('init')
443
- .argument('<name>', 'Name for the new Photon MCP')
444
- .description('Create a new .photon.ts from template')
445
- .action(async (name, options, command) => {
446
- try {
447
- // Get working directory from global options
448
- const workingDir = command.parent?.opts().workingDir || DEFAULT_WORKING_DIR;
449
- // Ensure working directory exists
450
- await ensureWorkingDir(workingDir);
451
- const fileName = `${name}.photon.ts`;
452
- const filePath = path.join(workingDir, fileName);
453
- // Check if file already exists
454
- try {
455
- await fs.access(filePath);
456
- console.error(`❌ File already exists: ${filePath}`);
457
- process.exit(1);
458
- }
459
- catch {
460
- // File doesn't exist, good
461
- }
462
- // Read template
463
- const templatePath = path.join(__dirname, '..', 'templates', 'photon.template.ts');
464
- let template;
465
- try {
466
- template = await fs.readFile(templatePath, 'utf-8');
467
- }
468
- catch {
469
- // Fallback inline template if file not found
470
- template = getInlineTemplate();
471
- }
472
- // Replace placeholders
473
- // Convert kebab-case to PascalCase for class name
474
- const className = name
475
- .split(/[-_]/)
476
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
477
- .join('');
478
- const content = template
479
- .replace(/TemplateName/g, className)
480
- .replace(/template-name/g, name);
481
- // Write file
482
- await fs.writeFile(filePath, content, 'utf-8');
483
- console.error(`✅ Created ${fileName} in ${workingDir}`);
484
- console.error(`Run with: photon mcp ${name} --dev`);
485
- }
486
- catch (error) {
487
- console.error(`❌ Error: ${error.message}`);
488
- process.exit(1);
489
- }
490
- });
491
- // Validate command: check syntax and schemas
492
- program
493
- .command('validate')
494
- .argument('<name>', 'MCP name (without .photon.ts extension)')
495
- .description('Validate syntax and schemas without running')
496
- .action(async (name, options, command) => {
497
- try {
498
- // Get working directory from global options
499
- const workingDir = command.parent?.opts().workingDir || DEFAULT_WORKING_DIR;
500
- // Resolve file path from name in working directory
501
- const filePath = await resolvePhotonPath(name, workingDir);
502
- if (!filePath) {
503
- console.error(`❌ MCP not found: ${name}`);
504
- console.error(`Searched in: ${workingDir}`);
505
- console.error(`Tip: Use 'photon info' to see available MCPs`);
506
- process.exit(1);
507
- }
508
- console.error(`Validating ${path.basename(filePath)}...\n`);
509
- // Import loader and try to load
510
- const { PhotonLoader } = await import('./loader.js');
511
- const loader = new PhotonLoader(false); // quiet mode for inspection
512
- const mcp = await loader.loadFile(filePath);
513
- console.error(`✅ Valid Photon MCP`);
514
- console.error(`Name: ${mcp.name}`);
515
- console.error(`Tools: ${mcp.tools.length}`);
516
- for (const tool of mcp.tools) {
517
- console.error(` - ${tool.name}: ${tool.description}`);
518
- }
519
- process.exit(0);
520
- }
521
- catch (error) {
522
- console.error(`❌ Validation failed: ${error.message}`);
523
- process.exit(1);
524
- }
525
- });
526
637
  // Info command: show installed and available Photons
527
638
  program
528
- .command('info')
639
+ .command('info', { hidden: true })
529
640
  .argument('[name]', 'Photon name to show details for (shows all if omitted)')
530
641
  .option('--mcp', 'Output as MCP server configuration')
642
+ .alias('list')
643
+ .alias('ls')
531
644
  .description('Show installed and available Photons')
532
645
  .action(async (name, options, command) => {
533
646
  try {
647
+ const { formatOutput, printInfo, printError, STATUS } = await import('./cli-formatter.js');
534
648
  // Get working directory from global/parent options
535
649
  const parentOpts = command.parent?.opts() || {};
536
- const workingDir = parentOpts.workingDir || DEFAULT_WORKING_DIR;
650
+ const workingDir = parentOpts.dir || DEFAULT_WORKING_DIR;
537
651
  const asMcp = options.mcp || false;
538
652
  const mcps = await listPhotonMCPs(workingDir);
539
653
  // Initialize marketplace manager for all operations
@@ -547,8 +661,8 @@ program
547
661
  if (asMcp) {
548
662
  // MCP config only works for installed photons
549
663
  if (!isInstalled) {
550
- console.error(`❌ '${name}' is not installed`);
551
- console.error(`Install with: photon add ${name}`);
664
+ printError(`'${name}' is not installed`);
665
+ printInfo(`Install with: photon add ${name}`);
552
666
  process.exit(1);
553
667
  }
554
668
  // Show as MCP config for single Photon
@@ -563,15 +677,20 @@ program
563
677
  }
564
678
  // Check for global photon installation
565
679
  const globalPhotonPath = await getGlobalPhotonPath();
680
+ const needsWorkingDir = workingDir !== DEFAULT_WORKING_DIR;
566
681
  const config = globalPhotonPath
567
682
  ? {
568
683
  command: globalPhotonPath,
569
- args: ['mcp', name],
684
+ args: needsWorkingDir
685
+ ? ['mcp', name, '--dir', workingDir]
686
+ : ['mcp', name],
570
687
  ...(Object.keys(env).length > 0 && { env }),
571
688
  }
572
689
  : {
573
690
  command: 'npx',
574
- args: ['@portel/photon', 'mcp', name],
691
+ args: needsWorkingDir
692
+ ? ['@portel/photon', 'mcp', name, '--dir', workingDir]
693
+ : ['@portel/photon', 'mcp', name],
575
694
  ...(Object.keys(env).length > 0 && { env }),
576
695
  };
577
696
  // Get OS-specific config path
@@ -590,60 +709,70 @@ program
590
709
  const fileName = `${name}.photon.ts`;
591
710
  const metadata = await manager.getPhotonInstallMetadata(fileName);
592
711
  const isModified = metadata ? await manager.isPhotonModified(filePath, fileName) : false;
593
- console.error(`INSTALLED IN ${workingDir}:`);
594
- console.error(` 📦 ${name}${photonMetadata.version ? ` (v${photonMetadata.version})` : ''}`);
595
- console.error(` Location: ${filePath}`);
712
+ printInfo(`Installed in ${workingDir}:\n`);
713
+ // Build info as tree structure
714
+ const infoData = {
715
+ name: name,
716
+ version: photonMetadata.version || '-',
717
+ location: filePath,
718
+ };
596
719
  if (photonMetadata.description) {
597
- console.error(` Description: ${photonMetadata.description}`);
720
+ infoData.description = photonMetadata.description;
598
721
  }
599
722
  if (metadata) {
600
- console.error(` Installed: ${new Date(metadata.installedAt).toLocaleDateString()}`);
723
+ infoData.installed = new Date(metadata.installedAt).toLocaleDateString();
724
+ infoData.source = metadata.marketplace;
601
725
  if (isModified) {
602
- console.error(` Status: ⚠️ Modified locally`);
726
+ infoData.status = 'modified locally';
603
727
  }
604
728
  }
605
729
  const toolCount = photonMetadata.tools?.length || 0;
606
730
  if (toolCount > 0) {
607
- console.error(` Tools: ${toolCount}`);
731
+ infoData.tools = toolCount;
608
732
  }
609
- console.error('');
733
+ formatOutput(infoData, 'tree');
734
+ console.log('');
610
735
  // Show appropriate run command
611
736
  if (metadata && !isModified) {
612
- console.error(` Run with: photon mcp ${name}`);
613
- console.error(` To customize: Copy to a new name and run with --dev for hot reload`);
737
+ printInfo(`Run with: photon mcp ${name}`);
738
+ printInfo(`To customize: Copy to a new name and run with --dev for hot reload`);
614
739
  }
615
740
  else if (metadata && isModified) {
616
- console.error(` Run with: photon mcp ${name} --dev`);
617
- console.error(` Note: Modified from marketplace - consider renaming to avoid upgrade conflicts`);
741
+ printInfo(`Run with: photon mcp ${name} --dev`);
742
+ printInfo(`Note: Modified from marketplace - consider renaming to avoid upgrade conflicts`);
618
743
  }
619
744
  else {
620
- console.error(` Run with: photon mcp ${name} --dev`);
745
+ printInfo(`Run with: photon mcp ${name} --dev`);
621
746
  }
622
- console.error('');
747
+ console.log('');
623
748
  }
624
749
  // Show marketplace availability in tree format
625
750
  const searchResults = await manager.search(name);
626
751
  if (searchResults.size > 0) {
627
- console.error('AVAILABLE IN MARKETPLACES:');
752
+ printInfo('Available in marketplaces:\n');
628
753
  // Get all sources for this specific photon
629
754
  const sources = searchResults.get(name);
630
755
  if (sources && sources.length > 0) {
631
756
  // Get local installation info to mark which one is installed
632
757
  const fileName = `${name}.photon.ts`;
633
758
  const installMetadata = await manager.getPhotonInstallMetadata(fileName);
634
- // Group by marketplace
759
+ // Build marketplace data as tree
760
+ const marketplaceData = {};
635
761
  for (const source of sources) {
636
762
  const isCurrentlyInstalled = installMetadata?.marketplace === source.marketplace.name;
637
- const mark = isCurrentlyInstalled ? ' ✓ currently installed' : '';
638
763
  const version = source.metadata?.version || 'unknown';
639
- console.error(` ${source.marketplace.name} (${source.marketplace.repo})`);
640
- console.error(` └─ ${name} (v${version})${mark}`);
764
+ marketplaceData[source.marketplace.name] = {
765
+ version,
766
+ source: source.marketplace.repo,
767
+ status: isCurrentlyInstalled ? 'installed' : '-',
768
+ };
641
769
  }
770
+ formatOutput(marketplaceData, 'tree');
642
771
  }
643
772
  }
644
773
  else if (!isInstalled) {
645
- console.error(`❌ '${name}' not found locally or in any marketplace`);
646
- console.error(`Tip: Use 'photon search <query>' to find similar MCPs`);
774
+ printError(`'${name}' not found locally or in any marketplace`);
775
+ printInfo(`Tip: Use 'photon search <query>' to find similar MCPs`);
647
776
  process.exit(1);
648
777
  }
649
778
  }
@@ -655,6 +784,7 @@ program
655
784
  const allConfigs = {};
656
785
  // Check for global photon installation once
657
786
  const globalPhotonPath = await getGlobalPhotonPath();
787
+ const needsWorkingDir = workingDir !== DEFAULT_WORKING_DIR;
658
788
  for (const mcpName of mcps) {
659
789
  const filePath = await resolvePhotonPath(mcpName, workingDir);
660
790
  if (!filePath)
@@ -671,12 +801,16 @@ program
671
801
  allConfigs[mcpName] = globalPhotonPath
672
802
  ? {
673
803
  command: globalPhotonPath,
674
- args: ['mcp', mcpName],
804
+ args: needsWorkingDir
805
+ ? ['mcp', mcpName, '--dir', workingDir]
806
+ : ['mcp', mcpName],
675
807
  ...(Object.keys(env).length > 0 && { env }),
676
808
  }
677
809
  : {
678
810
  command: 'npx',
679
- args: ['@portel/photon', 'mcp', mcpName],
811
+ args: needsWorkingDir
812
+ ? ['@portel/photon', 'mcp', mcpName, '--dir', workingDir]
813
+ : ['@portel/photon', 'mcp', mcpName],
680
814
  ...(Object.keys(env).length > 0 && { env }),
681
815
  };
682
816
  }
@@ -688,9 +822,10 @@ program
688
822
  return;
689
823
  }
690
824
  // Normal list mode - show both installed and available
691
- // Show installed photons
825
+ // Show installed photons as table
692
826
  if (mcps.length > 0) {
693
- console.error(`INSTALLED IN ${workingDir} (${mcps.length}):`);
827
+ printInfo(`Installed in ${workingDir} (${mcps.length}):\n`);
828
+ const tableData = [];
694
829
  for (const mcpName of mcps) {
695
830
  const fileName = `${mcpName}.photon.ts`;
696
831
  const filePath = path.join(workingDir, fileName);
@@ -699,145 +834,227 @@ program
699
834
  if (metadata) {
700
835
  // Has metadata - show version and status
701
836
  const isModified = await manager.isPhotonModified(filePath, fileName);
702
- const modifiedMark = isModified ? ' ⚠️ modified' : '';
703
- console.error(` 📦 ${mcpName} (v${metadata.version} from ${metadata.marketplace})${modifiedMark}`);
837
+ tableData.push({
838
+ name: mcpName,
839
+ version: metadata.version,
840
+ source: metadata.marketplace,
841
+ status: isModified ? 'modified' : STATUS.OK,
842
+ });
704
843
  }
705
844
  else {
706
845
  // No metadata - local or pre-metadata Photon
707
- console.error(` 📦 ${mcpName}`);
846
+ tableData.push({
847
+ name: mcpName,
848
+ version: '-',
849
+ source: 'local',
850
+ status: STATUS.OK,
851
+ });
708
852
  }
709
853
  }
710
- console.error('');
854
+ formatOutput(tableData, 'table');
855
+ console.log('');
711
856
  }
712
857
  else {
713
- console.error(`No photons installed in ${workingDir}`);
714
- console.error(`Install with: photon add <name>\n`);
858
+ printInfo(`No photons installed in ${workingDir}`);
859
+ printInfo(`Install with: photon add <name>\n`);
715
860
  }
716
861
  // Show marketplace availability in tree format
717
- console.error('AVAILABLE IN MARKETPLACES:');
862
+ printInfo('Available in marketplaces:\n');
718
863
  const marketplaces = manager.getAll().filter(m => m.enabled);
719
864
  const counts = await manager.getMarketplaceCounts();
865
+ // Build marketplace tree
866
+ const marketplaceTree = {};
720
867
  for (const marketplace of marketplaces) {
721
868
  const count = counts.get(marketplace.name) || 0;
722
869
  if (count > 0) {
723
- console.error(` ${marketplace.name} (${marketplace.repo})`);
724
870
  // Get a few sample photons from this marketplace
725
871
  const manifest = await manager['getCachedManifest'](marketplace.name);
726
872
  if (manifest && manifest.photons) {
727
873
  const samples = manifest.photons.slice(0, 3);
728
- samples.forEach((photon, idx) => {
729
- const isLast = idx === samples.length - 1 && samples.length === count;
730
- const branch = isLast ? '└─' : '├─';
731
- const installedMark = mcps.includes(photon.name) ? ' ✓ installed' : '';
732
- console.error(` ${branch} ${photon.name} (v${photon.version})${installedMark}`);
874
+ const photonList = {};
875
+ samples.forEach((photon) => {
876
+ const installedMark = mcps.includes(photon.name) ? ' (installed)' : '';
877
+ photonList[photon.name] = `v${photon.version}${installedMark}`;
733
878
  });
734
879
  if (count > 3) {
735
- console.error(` └─ ... (${count - 3} more)`);
880
+ photonList['...'] = `${count - 3} more`;
736
881
  }
882
+ marketplaceTree[`${marketplace.name} (${marketplace.repo})`] = photonList;
737
883
  }
738
884
  }
739
885
  else {
740
- console.error(` ${marketplace.name} (${marketplace.repo}) - no manifest`);
886
+ marketplaceTree[marketplace.name] = 'no manifest';
741
887
  }
742
888
  }
743
- console.error(`\nDetails: photon info <name>`);
744
- console.error(`MCP config: photon info <name> --mcp`);
889
+ formatOutput(marketplaceTree, 'tree');
890
+ console.log('');
891
+ printInfo(`Details: photon info <name>`);
892
+ printInfo(`MCP config: photon info <name> --mcp`);
745
893
  }
746
894
  catch (error) {
747
- console.error(`❌ Error: ${error.message}`);
895
+ const { printError } = await import('./cli-formatter.js');
896
+ printError(error.message);
748
897
  process.exit(1);
749
898
  }
750
899
  });
751
900
  // Search command: search for MCPs across marketplaces
752
901
  program
753
- .command('search')
902
+ .command('search', { hidden: true })
754
903
  .argument('<query>', 'MCP name or keyword to search for')
755
904
  .description('Search for MCP in all enabled marketplaces')
756
905
  .action(async (query) => {
757
906
  try {
758
907
  const { MarketplaceManager } = await import('./marketplace-manager.js');
908
+ const { formatOutput, printInfo, printError } = await import('./cli-formatter.js');
759
909
  const manager = new MarketplaceManager();
760
910
  await manager.initialize();
761
911
  // Auto-update stale caches
762
912
  const updated = await manager.autoUpdateStaleCaches();
763
913
  if (updated) {
764
- console.error('🔄 Refreshed marketplace data...\n');
914
+ printInfo('Refreshed marketplace data...\n');
765
915
  }
766
- console.error(`Searching for '${query}' in marketplaces...`);
916
+ printInfo(`Searching for '${query}' in marketplaces...`);
767
917
  const results = await manager.search(query);
768
918
  if (results.size === 0) {
769
- console.error(`❌ No results found for '${query}'`);
770
- console.error(`Tip: Run 'photon marketplace update' to manually refresh marketplace data`);
919
+ printError(`No results found for '${query}'`);
920
+ printInfo(`Tip: Run 'photon marketplace update' to manually refresh marketplace data`);
771
921
  return;
772
922
  }
773
- console.error('');
923
+ // Build table data from search results
924
+ const tableData = [];
774
925
  for (const [mcpName, entries] of results) {
775
926
  for (const entry of entries) {
776
- if (entry.metadata) {
777
- console.error(` 📦 ${mcpName} (v${entry.metadata.version})`);
778
- console.error(` ${entry.metadata.description}`);
779
- console.error(` ${entry.marketplace.name} (${entry.marketplace.repo})`);
780
- if (entry.metadata.tags && entry.metadata.tags.length > 0) {
781
- console.error(` Tags: ${entry.metadata.tags.join(', ')}`);
782
- }
783
- }
784
- else {
785
- console.error(` 📦 ${mcpName}`);
786
- console.error(` ✓ ${entry.marketplace.name} (${entry.marketplace.repo})`);
787
- }
927
+ tableData.push({
928
+ name: mcpName,
929
+ version: entry.metadata?.version || '-',
930
+ description: entry.metadata?.description
931
+ ? entry.metadata.description.substring(0, 50) + (entry.metadata.description.length > 50 ? '...' : '')
932
+ : '-',
933
+ marketplace: entry.marketplace.name,
934
+ });
788
935
  }
789
936
  }
790
- console.error('');
937
+ console.log('');
938
+ formatOutput(tableData, 'table');
939
+ printInfo(`\nInstall with: photon add <name>`);
791
940
  }
792
941
  catch (error) {
793
- console.error(`❌ Error: ${error.message}`);
942
+ const { printError } = await import('./cli-formatter.js');
943
+ printError(error.message);
794
944
  process.exit(1);
795
945
  }
796
946
  });
797
- // Sync command: synchronize local resources
798
- const sync = program
799
- .command('sync')
800
- .description('Synchronize local resources');
801
- sync
802
- .command('marketplace')
803
- .argument('[path]', 'Directory containing Photons (defaults to current directory)', '.')
804
- .option('--name <name>', 'Marketplace name')
805
- .option('--description <desc>', 'Marketplace description')
806
- .option('--owner <owner>', 'Owner name')
807
- .option('--claude-code', 'Generate Claude Code plugin files')
808
- .description('Generate/sync marketplace manifest and documentation')
809
- .action(async (dirPath, options) => {
947
+ // Maker command: commands for photon creators/publishers
948
+ const maker = program
949
+ .command('maker', { hidden: true })
950
+ .description('Commands for creating photons and marketplaces');
951
+ // maker new: create a new photon from template
952
+ maker
953
+ .command('new')
954
+ .argument('<name>', 'Name for the new photon')
955
+ .description('Create a new photon from template')
956
+ .action(async (name, options, command) => {
810
957
  try {
811
- await performMarketplaceSync(dirPath, options);
812
- // Generate Claude Code plugin if requested
813
- if (options.claudeCode) {
814
- const { generateClaudeCodePlugin } = await import('./claude-code-plugin.js');
815
- await generateClaudeCodePlugin(dirPath, options);
958
+ // Get working directory from global options
959
+ const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
960
+ // Ensure working directory exists
961
+ await ensureWorkingDir(workingDir);
962
+ const fileName = `${name}.photon.ts`;
963
+ const filePath = path.join(workingDir, fileName);
964
+ // Check if file already exists
965
+ try {
966
+ await fs.access(filePath);
967
+ console.error(`❌ File already exists: ${filePath}`);
968
+ process.exit(1);
969
+ }
970
+ catch {
971
+ // File doesn't exist, good
972
+ }
973
+ // Read template
974
+ const templatePath = path.join(__dirname, '..', 'templates', 'photon.template.ts');
975
+ let template;
976
+ try {
977
+ template = await fs.readFile(templatePath, 'utf-8');
816
978
  }
979
+ catch {
980
+ // Fallback inline template if file not found
981
+ template = getInlineTemplate();
982
+ }
983
+ // Replace placeholders
984
+ // Convert kebab-case to PascalCase for class name
985
+ const className = name
986
+ .split(/[-_]/)
987
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
988
+ .join('');
989
+ const content = template
990
+ .replace(/TemplateName/g, className)
991
+ .replace(/template-name/g, name);
992
+ // Write file
993
+ await fs.writeFile(filePath, content, 'utf-8');
994
+ console.error(`✅ Created ${fileName} in ${workingDir}`);
995
+ console.error(`Run with: photon mcp ${name} --dev`);
817
996
  }
818
997
  catch (error) {
819
998
  console.error(`❌ Error: ${error.message}`);
820
- if (process.env.DEBUG) {
821
- console.error(error.stack);
999
+ process.exit(1);
1000
+ }
1001
+ });
1002
+ // maker validate: validate photon syntax and schemas
1003
+ maker
1004
+ .command('validate')
1005
+ .argument('<name>', 'Photon name (without .photon.ts extension)')
1006
+ .description('Validate photon syntax and schemas')
1007
+ .action(async (name, options, command) => {
1008
+ try {
1009
+ // Get working directory from global options
1010
+ const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
1011
+ // Resolve file path from name in working directory
1012
+ const filePath = await resolvePhotonPath(name, workingDir);
1013
+ if (!filePath) {
1014
+ console.error(`❌ Photon not found: ${name}`);
1015
+ console.error(`Searched in: ${workingDir}`);
1016
+ console.error(`Tip: Use 'photon info' to see available photons`);
1017
+ process.exit(1);
822
1018
  }
1019
+ console.error(`Validating ${path.basename(filePath)}...\n`);
1020
+ // Import loader and try to load
1021
+ const { PhotonLoader } = await import('./loader.js');
1022
+ const loader = new PhotonLoader(false); // quiet mode for inspection
1023
+ const mcp = await loader.loadFile(filePath);
1024
+ console.error(`✅ Valid Photon`);
1025
+ console.error(`Name: ${mcp.name}`);
1026
+ console.error(`Tools: ${mcp.tools.length}`);
1027
+ for (const tool of mcp.tools) {
1028
+ console.error(` - ${tool.name}: ${tool.description}`);
1029
+ }
1030
+ process.exit(0);
1031
+ }
1032
+ catch (error) {
1033
+ console.error(`❌ Validation failed: ${error.message}`);
823
1034
  process.exit(1);
824
1035
  }
825
1036
  });
826
- // Marketplace command: manage MCP marketplaces
827
- const marketplace = program
828
- .command('marketplace')
829
- .description('Manage MCP marketplaces');
830
- marketplace
831
- .command('sync', { hidden: true })
832
- .argument('[path]', 'Directory containing Photons (defaults to current directory)', '.')
1037
+ // maker sync: generate marketplace manifest
1038
+ maker
1039
+ .command('sync')
1040
+ .option('--dir <path>', 'Directory to sync (defaults to current directory)')
833
1041
  .option('--name <name>', 'Marketplace name')
834
1042
  .option('--description <desc>', 'Marketplace description')
835
1043
  .option('--owner <owner>', 'Owner name')
836
- .description('(Deprecated: use "photon sync marketplace") Generate/sync marketplace manifest and documentation')
837
- .action(async (dirPath, options) => {
838
- console.error('⚠️ Note: "photon marketplace sync" is deprecated. Use "photon sync marketplace" instead.\n');
1044
+ .option('--claude-code', 'Generate Claude Code plugin files')
1045
+ .description('Generate marketplace manifest and documentation from your photons')
1046
+ .action(async (options) => {
839
1047
  try {
840
- await performMarketplaceSync(dirPath, options);
1048
+ const dirPath = options.dir || '.';
1049
+ const resolvedPath = path.resolve(dirPath);
1050
+ // Only filter installed photons when syncing ~/.photon
1051
+ const filterInstalled = resolvedPath === DEFAULT_WORKING_DIR;
1052
+ await performMarketplaceSync(dirPath, { ...options, filterInstalled });
1053
+ // Generate Claude Code plugin if requested
1054
+ if (options.claudeCode) {
1055
+ const { generateClaudeCodePlugin } = await import('./claude-code-plugin.js');
1056
+ await generateClaudeCodePlugin(dirPath, options);
1057
+ }
841
1058
  }
842
1059
  catch (error) {
843
1060
  console.error(`❌ Error: ${error.message}`);
@@ -847,107 +1064,17 @@ marketplace
847
1064
  process.exit(1);
848
1065
  }
849
1066
  });
850
- // Initialize a new marketplace with git hooks
851
- marketplace
1067
+ maker
852
1068
  .command('init')
853
- .argument('[path]', 'Directory to initialize as marketplace (defaults to current directory)', '.')
1069
+ .option('--dir <path>', 'Directory to initialize (defaults to current directory)')
854
1070
  .option('--name <name>', 'Marketplace name')
855
1071
  .option('--description <desc>', 'Marketplace description')
856
1072
  .option('--owner <owner>', 'Owner name')
857
1073
  .description('Initialize a directory as a Photon marketplace with git hooks')
858
- .action(async (dirPath, options) => {
1074
+ .action(async (options) => {
859
1075
  try {
860
- const fs = await import('fs/promises');
861
- const { existsSync } = await import('fs');
862
- const path = await import('path');
863
- const absolutePath = path.resolve(dirPath);
864
- // Check if directory exists
865
- if (!existsSync(absolutePath)) {
866
- await fs.mkdir(absolutePath, { recursive: true });
867
- console.error(`📁 Created directory: ${absolutePath}`);
868
- }
869
- // Check if it's a git repository
870
- const gitDir = path.join(absolutePath, '.git');
871
- if (!existsSync(gitDir)) {
872
- console.error('⚠️ Not a git repository. Initialize with: git init');
873
- process.exit(1);
874
- }
875
- // Create .githooks directory
876
- const hooksDir = path.join(absolutePath, '.githooks');
877
- await fs.mkdir(hooksDir, { recursive: true });
878
- // Create pre-commit hook
879
- const preCommitHook = `#!/bin/bash
880
- # Pre-commit hook: Auto-sync marketplace manifest before commit
881
- # This ensures .marketplace/photons.json and .claude-plugin/ are always up-to-date
882
-
883
- # Check if any .photon.ts files or marketplace files are being committed
884
- if git diff --cached --name-only | grep -qE '\\.photon\\.ts$|\\.marketplace/|\\.claude-plugin/'; then
885
- echo "🔄 Syncing marketplace manifest..."
886
-
887
- # Run photon sync marketplace with --claude-code to generate plugin files
888
- if photon sync marketplace --claude-code; then
889
- # Stage the generated files
890
- git add .marketplace/photons.json README.md *.md .claude-plugin/ 2>/dev/null
891
- echo "✅ Marketplace and Claude Code plugin synced and staged"
892
- else
893
- echo "❌ Failed to sync marketplace"
894
- exit 1
895
- fi
896
- fi
897
-
898
- exit 0
899
- `;
900
- const preCommitPath = path.join(hooksDir, 'pre-commit');
901
- await fs.writeFile(preCommitPath, preCommitHook, { mode: 0o755 });
902
- console.error('✅ Created .githooks/pre-commit');
903
- // Create setup script
904
- const setupScript = `#!/bin/bash
905
- # Setup script to install git hooks for this marketplace
906
-
907
- REPO_ROOT="$(git rev-parse --show-toplevel)"
908
- HOOKS_DIR="$REPO_ROOT/.git/hooks"
909
- SOURCE_HOOKS="$REPO_ROOT/.githooks"
910
-
911
- echo "🔧 Installing git hooks for Photon marketplace..."
912
-
913
- # Copy pre-commit hook
914
- if [ -f "$SOURCE_HOOKS/pre-commit" ]; then
915
- cp "$SOURCE_HOOKS/pre-commit" "$HOOKS_DIR/pre-commit"
916
- chmod +x "$HOOKS_DIR/pre-commit"
917
- echo "✅ Installed pre-commit hook (auto-syncs marketplace manifest)"
918
- else
919
- echo "❌ pre-commit hook not found"
920
- exit 1
921
- fi
922
-
923
- echo ""
924
- echo "✅ Git hooks installed successfully!"
925
- echo ""
926
- echo "The pre-commit hook will automatically run 'photon sync marketplace'"
927
- echo "whenever you commit changes to .photon.ts files."
928
- `;
929
- const setupPath = path.join(hooksDir, 'setup.sh');
930
- await fs.writeFile(setupPath, setupScript, { mode: 0o755 });
931
- console.error('✅ Created .githooks/setup.sh');
932
- // Install hooks to .git/hooks
933
- const gitHooksDir = path.join(absolutePath, '.git', 'hooks');
934
- const gitPreCommitPath = path.join(gitHooksDir, 'pre-commit');
935
- await fs.writeFile(gitPreCommitPath, preCommitHook, { mode: 0o755 });
936
- console.error('✅ Installed hooks to .git/hooks');
937
- // Create .marketplace directory
938
- const marketplaceDir = path.join(absolutePath, '.marketplace');
939
- await fs.mkdir(marketplaceDir, { recursive: true });
940
- console.error('✅ Created .marketplace directory');
941
- // Run initial sync
942
- console.error('\n🔄 Running initial marketplace sync...\n');
943
- await performMarketplaceSync(dirPath, options);
944
- console.error('\n✅ Marketplace initialized successfully!');
945
- console.error('\nNext steps:');
946
- console.error('1. Add your .photon.ts files to this directory');
947
- console.error('2. Commit your changes (hooks will auto-sync)');
948
- console.error('3. Push to GitHub to share your marketplace');
949
- console.error('\nContributors can setup hooks with:');
950
- console.error(' bash .githooks/setup.sh');
1076
+ const dirPath = options.dir || '.';
1077
+ await performMarketplaceInit(dirPath, options);
951
1078
  }
952
1079
  catch (error) {
953
1080
  console.error(`❌ Error: ${error.message}`);
@@ -957,38 +1084,40 @@ echo "whenever you commit changes to .photon.ts files."
957
1084
  process.exit(1);
958
1085
  }
959
1086
  });
1087
+ // Marketplace command: manage MCP marketplaces
1088
+ const marketplace = program
1089
+ .command('marketplace', { hidden: true })
1090
+ .description('Manage MCP marketplaces');
960
1091
  marketplace
961
1092
  .command('list')
962
1093
  .description('List all configured marketplaces')
963
1094
  .action(async () => {
964
1095
  try {
965
1096
  const { MarketplaceManager } = await import('./marketplace-manager.js');
1097
+ const { formatOutput, printInfo, STATUS } = await import('./cli-formatter.js');
966
1098
  const manager = new MarketplaceManager();
967
1099
  await manager.initialize();
968
1100
  const marketplaces = manager.getAll();
969
1101
  if (marketplaces.length === 0) {
970
- console.error('No marketplaces configured');
1102
+ printInfo('No marketplaces configured');
1103
+ printInfo('Add one with: photon marketplace add portel-dev/photons');
971
1104
  return;
972
1105
  }
973
1106
  // Get MCP counts
974
1107
  const counts = await manager.getMarketplaceCounts();
975
- console.error(`Configured marketplaces (${marketplaces.length}):\n`);
976
- for (const marketplace of marketplaces) {
977
- const status = marketplace.enabled ? '✅' : '❌';
978
- const count = counts.get(marketplace.name) || 0;
979
- const countStr = count > 0 ? `${count} available` : 'no manifest';
980
- console.error(` ${status} ${marketplace.name}`);
981
- console.error(` ${marketplace.repo}`);
982
- console.error(` ${countStr}`);
983
- if (marketplace.lastUpdated) {
984
- const date = new Date(marketplace.lastUpdated);
985
- console.error(` Updated ${date.toLocaleDateString()}`);
986
- }
987
- }
988
- console.error('');
1108
+ // Build table data
1109
+ const tableData = marketplaces.map(m => ({
1110
+ name: m.name,
1111
+ source: m.source || m.repo || '-',
1112
+ photons: counts.get(m.name) || 0,
1113
+ status: m.enabled ? STATUS.OK : STATUS.OFF,
1114
+ }));
1115
+ printInfo(`Configured marketplaces (${marketplaces.length}):\n`);
1116
+ formatOutput(tableData, 'table');
989
1117
  }
990
1118
  catch (error) {
991
- console.error(`❌ Error: ${error.message}`);
1119
+ const { printError } = await import('./cli-formatter.js');
1120
+ printError(error.message);
992
1121
  process.exit(1);
993
1122
  }
994
1123
  });
@@ -1093,51 +1222,9 @@ marketplace
1093
1222
  process.exit(1);
1094
1223
  }
1095
1224
  });
1096
- marketplace
1097
- .command('update')
1098
- .argument('[name]', 'Marketplace name to update (updates all if omitted)')
1099
- .description('Update marketplace metadata from remote')
1100
- .action(async (name) => {
1101
- try {
1102
- const { MarketplaceManager } = await import('./marketplace-manager.js');
1103
- const manager = new MarketplaceManager();
1104
- await manager.initialize();
1105
- if (name) {
1106
- // Update specific marketplace
1107
- console.error(`Updating ${name}...`);
1108
- const success = await manager.updateMarketplaceCache(name);
1109
- if (success) {
1110
- console.error(`✅ Updated ${name}`);
1111
- }
1112
- else {
1113
- console.error(`❌ Failed to update ${name} (not found or no manifest)`);
1114
- process.exit(1);
1115
- }
1116
- }
1117
- else {
1118
- // Update all enabled marketplaces
1119
- console.error(`Updating all marketplaces...\n`);
1120
- const results = await manager.updateAllCaches();
1121
- for (const [marketplaceName, success] of results) {
1122
- if (success) {
1123
- console.error(` ✅ ${marketplaceName}`);
1124
- }
1125
- else {
1126
- console.error(` ⚠️ ${marketplaceName} (no manifest)`);
1127
- }
1128
- }
1129
- const successCount = Array.from(results.values()).filter(Boolean).length;
1130
- console.error(`\nUpdated ${successCount}/${results.size} marketplaces`);
1131
- }
1132
- }
1133
- catch (error) {
1134
- console.error(`❌ Error: ${error.message}`);
1135
- process.exit(1);
1136
- }
1137
- });
1138
1225
  // Add command: add MCP from marketplace
1139
1226
  program
1140
- .command('add')
1227
+ .command('add', { hidden: true })
1141
1228
  .argument('<name>', 'MCP name to add')
1142
1229
  .option('--marketplace <name>', 'Specific marketplace to use')
1143
1230
  .option('-y, --yes', 'Automatically select first suggestion without prompting')
@@ -1145,7 +1232,7 @@ program
1145
1232
  .action(async (name, options, command) => {
1146
1233
  try {
1147
1234
  // Get working directory from global options
1148
- const workingDir = command.parent?.opts().workingDir || DEFAULT_WORKING_DIR;
1235
+ const workingDir = command.parent?.opts().dir || DEFAULT_WORKING_DIR;
1149
1236
  await ensureWorkingDir(workingDir);
1150
1237
  const { MarketplaceManager } = await import('./marketplace-manager.js');
1151
1238
  const manager = new MarketplaceManager();
@@ -1320,16 +1407,68 @@ program
1320
1407
  process.exit(1);
1321
1408
  }
1322
1409
  });
1410
+ // Remove command: remove an installed photon
1411
+ program
1412
+ .command('remove', { hidden: true })
1413
+ .argument('<name>', 'MCP name to remove')
1414
+ .alias('rm')
1415
+ .option('--keep-cache', 'Keep compiled cache for this photon')
1416
+ .description('Remove an installed photon')
1417
+ .action(async (name, options, command) => {
1418
+ try {
1419
+ const { printInfo, printSuccess, printError } = await import('./cli-formatter.js');
1420
+ const workingDir = command.parent?.opts().dir || DEFAULT_WORKING_DIR;
1421
+ // Find the photon file
1422
+ const filePath = await resolvePhotonPath(name, workingDir);
1423
+ if (!filePath) {
1424
+ printError(`Photon not found: ${name}`);
1425
+ printInfo(`Searched in: ${workingDir}`);
1426
+ printInfo(`Tip: Use 'photon info' to see installed photons`);
1427
+ process.exit(1);
1428
+ }
1429
+ printInfo(`Removing ${name}...`);
1430
+ // Remove the .photon.ts file
1431
+ await fs.unlink(filePath);
1432
+ printSuccess(`Removed ${name}.photon.ts`);
1433
+ // Clear compiled cache unless --keep-cache
1434
+ if (!options.keepCache) {
1435
+ const cacheDir = path.join(os.homedir(), '.cache', 'photon-mcp', 'compiled');
1436
+ const cachedFiles = [
1437
+ path.join(cacheDir, `${name}.js`),
1438
+ path.join(cacheDir, `${name}.js.map`),
1439
+ ];
1440
+ for (const cachedFile of cachedFiles) {
1441
+ try {
1442
+ await fs.unlink(cachedFile);
1443
+ }
1444
+ catch {
1445
+ // Ignore if cache doesn't exist
1446
+ }
1447
+ }
1448
+ printSuccess(`Cleared cache`);
1449
+ }
1450
+ console.log('');
1451
+ printSuccess(`Successfully removed ${name}`);
1452
+ printInfo(`To reinstall: photon add ${name}`);
1453
+ }
1454
+ catch (error) {
1455
+ const { printError } = await import('./cli-formatter.js');
1456
+ printError(error.message);
1457
+ process.exit(1);
1458
+ }
1459
+ });
1323
1460
  // Upgrade command: update MCPs from marketplace
1324
1461
  program
1325
- .command('upgrade')
1462
+ .command('upgrade', { hidden: true })
1326
1463
  .argument('[name]', 'MCP name to upgrade (upgrades all if omitted)')
1327
1464
  .option('--check', 'Check for updates without upgrading')
1465
+ .alias('up')
1328
1466
  .description('Upgrade MCP(s) from marketplaces')
1329
1467
  .action(async (name, options, command) => {
1330
1468
  try {
1469
+ const { formatOutput, printInfo, printSuccess, printWarning, printError, STATUS } = await import('./cli-formatter.js');
1331
1470
  // Get working directory from global options
1332
- const workingDir = command.parent?.opts().workingDir || DEFAULT_WORKING_DIR;
1471
+ const workingDir = command.parent?.opts().dir || DEFAULT_WORKING_DIR;
1333
1472
  const { VersionChecker } = await import('./version-checker.js');
1334
1473
  const checker = new VersionChecker();
1335
1474
  await checker.initialize();
@@ -1337,230 +1476,568 @@ program
1337
1476
  // Upgrade single MCP
1338
1477
  const filePath = await resolvePhotonPath(name, workingDir);
1339
1478
  if (!filePath) {
1340
- console.error(`❌ MCP not found: ${name}`);
1341
- console.error(`Searched in: ${workingDir}`);
1479
+ printError(`MCP not found: ${name}`);
1480
+ printInfo(`Searched in: ${workingDir}`);
1342
1481
  process.exit(1);
1343
1482
  }
1344
- console.error(`Checking ${name} for updates...`);
1483
+ printInfo(`Checking ${name} for updates...`);
1345
1484
  const versionInfo = await checker.checkForUpdate(name, filePath);
1346
1485
  if (!versionInfo.local) {
1347
- console.error(`⚠️ Could not determine local version`);
1486
+ printWarning('Could not determine local version');
1348
1487
  return;
1349
1488
  }
1350
1489
  if (!versionInfo.remote) {
1351
- console.error(`⚠️ Not found in any marketplace. This might be a local-only MCP.`);
1490
+ printWarning('Not found in any marketplace. This might be a local-only MCP.');
1352
1491
  return;
1353
1492
  }
1354
1493
  if (options.check) {
1355
- if (versionInfo.needsUpdate) {
1356
- console.error(`🔄 Update available: ${versionInfo.local} → ${versionInfo.remote}`);
1357
- }
1358
- else {
1359
- console.error(`✅ Already up to date (${versionInfo.local})`);
1360
- }
1494
+ const tableData = [{
1495
+ name,
1496
+ local: versionInfo.local,
1497
+ remote: versionInfo.remote,
1498
+ status: versionInfo.needsUpdate ? STATUS.UPDATE : STATUS.OK,
1499
+ }];
1500
+ formatOutput(tableData, 'table');
1361
1501
  return;
1362
1502
  }
1363
1503
  if (!versionInfo.needsUpdate) {
1364
- console.error(`✅ Already up to date (${versionInfo.local})`);
1504
+ printSuccess(`Already up to date (${versionInfo.local})`);
1365
1505
  return;
1366
1506
  }
1367
- console.error(`🔄 Upgrading ${name}: ${versionInfo.local} → ${versionInfo.remote}`);
1507
+ printInfo(`Upgrading ${name}: ${versionInfo.local} → ${versionInfo.remote}`);
1368
1508
  const success = await checker.updateMCP(name, filePath);
1369
1509
  if (success) {
1370
- console.error(`✅ Successfully upgraded ${name} to ${versionInfo.remote}`);
1510
+ printSuccess(`Successfully upgraded ${name} to ${versionInfo.remote}`);
1371
1511
  }
1372
1512
  else {
1373
- console.error(`❌ Failed to upgrade ${name}`);
1513
+ printError(`Failed to upgrade ${name}`);
1374
1514
  process.exit(1);
1375
1515
  }
1376
1516
  }
1377
1517
  else {
1378
1518
  // Check/upgrade all MCPs
1379
- console.error(`Checking all MCPs in ${workingDir}...\n`);
1519
+ printInfo(`Checking all MCPs in ${workingDir}...\n`);
1380
1520
  const updates = await checker.checkAllUpdates(workingDir);
1381
1521
  if (updates.size === 0) {
1382
- console.error(`No MCPs found`);
1522
+ printInfo('No MCPs found');
1383
1523
  return;
1384
1524
  }
1385
1525
  const needsUpdate = [];
1526
+ // Build table data
1527
+ const tableData = [];
1386
1528
  for (const [mcpName, info] of updates) {
1387
- const status = checker.formatVersionInfo(info);
1388
1529
  if (info.needsUpdate) {
1389
- console.error(` 🔄 ${mcpName}: ${status}`);
1390
1530
  needsUpdate.push(mcpName);
1531
+ tableData.push({
1532
+ name: mcpName,
1533
+ local: info.local || '-',
1534
+ remote: info.remote || '-',
1535
+ status: STATUS.UPDATE,
1536
+ });
1391
1537
  }
1392
1538
  else if (info.local && info.remote) {
1393
- console.error(` ✅ ${mcpName}: ${status}`);
1539
+ tableData.push({
1540
+ name: mcpName,
1541
+ local: info.local,
1542
+ remote: info.remote,
1543
+ status: STATUS.OK,
1544
+ });
1394
1545
  }
1395
1546
  else {
1396
- console.error(` 📦 ${mcpName}: ${status}`);
1547
+ tableData.push({
1548
+ name: mcpName,
1549
+ local: info.local || '-',
1550
+ remote: info.remote || 'local only',
1551
+ status: STATUS.UNKNOWN,
1552
+ });
1397
1553
  }
1398
1554
  }
1555
+ formatOutput(tableData, 'table');
1399
1556
  if (needsUpdate.length === 0) {
1400
- console.error(`\nAll MCPs are up to date!`);
1557
+ console.log('');
1558
+ printSuccess('All MCPs are up to date!');
1401
1559
  return;
1402
1560
  }
1403
1561
  if (options.check) {
1404
- console.error(`\n${needsUpdate.length} MCP(s) have updates available`);
1405
- console.error(`Run 'photon upgrade' to upgrade all`);
1562
+ console.log('');
1563
+ printInfo(`${needsUpdate.length} MCP(s) have updates available`);
1564
+ printInfo(`Run 'photon upgrade' to upgrade all`);
1406
1565
  return;
1407
1566
  }
1408
1567
  // Upgrade all that need updates
1409
- console.error(`\nUpgrading ${needsUpdate.length} MCP(s)...`);
1568
+ console.log('');
1569
+ printInfo(`Upgrading ${needsUpdate.length} MCP(s)...`);
1410
1570
  for (const mcpName of needsUpdate) {
1411
1571
  const filePath = path.join(workingDir, `${mcpName}.photon.ts`);
1412
1572
  const success = await checker.updateMCP(mcpName, filePath);
1413
1573
  if (success) {
1414
- console.error(`✅ Upgraded ${mcpName}`);
1574
+ printSuccess(`Upgraded ${mcpName}`);
1415
1575
  }
1416
1576
  else {
1417
- console.error(`❌ Failed to upgrade ${mcpName}`);
1577
+ printError(`Failed to upgrade ${mcpName}`);
1418
1578
  }
1419
1579
  }
1420
1580
  }
1421
1581
  }
1422
1582
  catch (error) {
1423
- console.error(`❌ Error: ${error.message}`);
1583
+ const { printError } = await import('./cli-formatter.js');
1584
+ printError(error.message);
1424
1585
  process.exit(1);
1425
1586
  }
1426
1587
  });
1427
- // Audit command: security scan for MCP dependencies
1588
+ // Clear-cache command: clear compiled photon cache
1428
1589
  program
1429
- .command('audit')
1430
- .argument('[name]', 'MCP name to audit (audits all if omitted)')
1431
- .description('Security audit of MCP dependencies')
1590
+ .command('clear-cache', { hidden: true })
1591
+ .argument('[name]', 'MCP name to clear cache for (clears all if omitted)')
1592
+ .alias('clean')
1593
+ .description('Clear compiled photon cache')
1432
1594
  .action(async (name, options, command) => {
1433
1595
  try {
1434
- const workingDir = command.parent?.opts().workingDir || DEFAULT_WORKING_DIR;
1435
- const { DependencyManager } = await import('./dependency-manager.js');
1436
- const { SecurityScanner } = await import('./security-scanner.js');
1437
- const depManager = new DependencyManager();
1438
- const scanner = new SecurityScanner();
1596
+ const { printInfo, printSuccess, printError } = await import('./cli-formatter.js');
1597
+ const cacheDir = path.join(os.homedir(), '.cache', 'photon-mcp', 'compiled');
1439
1598
  if (name) {
1440
- // Audit single MCP
1599
+ // Clear cache for specific photon
1600
+ const workingDir = command.parent?.opts().dir || DEFAULT_WORKING_DIR;
1441
1601
  const filePath = await resolvePhotonPath(name, workingDir);
1442
1602
  if (!filePath) {
1443
- console.error(`❌ MCP not found: ${name}`);
1603
+ printError(`Photon not found: ${name}`);
1604
+ printInfo(`Tip: Use 'photon info' to see installed photons`);
1444
1605
  process.exit(1);
1445
1606
  }
1446
- console.error(`🔍 Auditing dependencies for: ${name}\n`);
1447
- const dependencies = await depManager.extractDependencies(filePath);
1448
- if (dependencies.length === 0) {
1449
- console.error('✅ No dependencies to audit');
1450
- return;
1451
- }
1452
- const depStrings = dependencies.map(d => `${d.name}@${d.version}`);
1453
- const result = await scanner.auditMCP(name, depStrings);
1454
- console.error(scanner.formatAuditResult(result));
1455
- if (result.totalVulnerabilities > 0) {
1456
- console.error('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1457
- console.error('Vulnerabilities found:');
1458
- result.dependencies.forEach(dep => {
1459
- if (dep.hasVulnerabilities) {
1460
- console.error(`\n📦 ${dep.dependency}@${dep.version}`);
1461
- dep.vulnerabilities.forEach(vuln => {
1462
- const symbol = scanner.getSeveritySymbol(vuln.severity);
1463
- console.error(` ${symbol} ${vuln.severity.toUpperCase()}: ${vuln.title}`);
1464
- if (vuln.url) {
1465
- console.error(` ${vuln.url}`);
1466
- }
1467
- });
1468
- }
1469
- });
1470
- console.error('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1471
- console.error('\n💡 To fix vulnerabilities:');
1472
- console.error(` 1. Update dependency versions in @dependencies JSDoc tag`);
1473
- console.error(` 2. Clear cache: photon clear-cache`);
1474
- console.error(` 3. Restart MCP to reinstall with new versions`);
1475
- if (result.criticalCount > 0 || result.highCount > 0) {
1476
- process.exit(1);
1607
+ printInfo(`Clearing cache for ${name}...`);
1608
+ const cachedFiles = [
1609
+ path.join(cacheDir, `${name}.js`),
1610
+ path.join(cacheDir, `${name}.js.map`),
1611
+ ];
1612
+ let cleared = false;
1613
+ for (const cachedFile of cachedFiles) {
1614
+ try {
1615
+ await fs.unlink(cachedFile);
1616
+ cleared = true;
1477
1617
  }
1618
+ catch {
1619
+ // Ignore if file doesn't exist
1620
+ }
1621
+ }
1622
+ if (cleared) {
1623
+ printSuccess(`Cleared cache for ${name}`);
1624
+ }
1625
+ else {
1626
+ printInfo(`No cache found for ${name}`);
1478
1627
  }
1479
1628
  }
1480
1629
  else {
1481
- // Audit all MCPs
1482
- console.error('🔍 Auditing all MCPs...\n');
1483
- const mcps = await listPhotonMCPs(workingDir);
1484
- if (mcps.length === 0) {
1485
- console.error('No MCPs found');
1486
- return;
1487
- }
1488
- let totalVulnerabilities = 0;
1489
- let mcpsWithVulnerabilities = 0;
1490
- for (const mcp of mcps) {
1491
- const mcpPath = path.join(workingDir, mcp);
1492
- const mcpName = path.basename(mcp, '.photon.ts');
1493
- const dependencies = await depManager.extractDependencies(mcpPath);
1494
- if (dependencies.length === 0) {
1495
- console.error(`✅ ${mcpName}: No dependencies`);
1496
- continue;
1630
+ // Clear all cache
1631
+ printInfo('Clearing all compiled photon cache...');
1632
+ try {
1633
+ const files = await fs.readdir(cacheDir);
1634
+ let count = 0;
1635
+ for (const file of files) {
1636
+ const filePath = path.join(cacheDir, file);
1637
+ try {
1638
+ await fs.unlink(filePath);
1639
+ count++;
1640
+ }
1641
+ catch {
1642
+ // Ignore errors
1643
+ }
1497
1644
  }
1498
- const depStrings = dependencies.map(d => `${d.name}@${d.version}`);
1499
- const result = await scanner.auditMCP(mcpName, depStrings);
1500
- console.error(scanner.formatAuditResult(result));
1501
- if (result.totalVulnerabilities > 0) {
1502
- totalVulnerabilities += result.totalVulnerabilities;
1503
- mcpsWithVulnerabilities++;
1645
+ if (count > 0) {
1646
+ printSuccess(`Cleared ${count} cached file(s)`);
1647
+ }
1648
+ else {
1649
+ printInfo(`Cache is already empty`);
1504
1650
  }
1505
1651
  }
1506
- console.error('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1507
- console.error(`\nSummary: ${totalVulnerabilities} vulnerabilities in ${mcpsWithVulnerabilities}/${mcps.length} MCPs`);
1508
- if (totalVulnerabilities > 0) {
1509
- console.error('\nRun: photon audit <name> # for detailed vulnerability info');
1510
- process.exit(1);
1652
+ catch (error) {
1653
+ if (error.code === 'ENOENT') {
1654
+ printInfo(`No cache directory found`);
1655
+ }
1656
+ else {
1657
+ throw error;
1658
+ }
1511
1659
  }
1512
1660
  }
1513
1661
  }
1514
1662
  catch (error) {
1515
- console.error(`❌ Error: ${error.message}`);
1663
+ const { printError } = await import('./cli-formatter.js');
1664
+ printError(error.message);
1516
1665
  process.exit(1);
1517
1666
  }
1518
1667
  });
1519
- // Conflicts command: show MCPs available in multiple marketplaces
1668
+ // Doctor command: diagnose photon environment
1520
1669
  program
1521
- .command('conflicts')
1522
- .description('Show MCPs available in multiple marketplaces')
1523
- .action(async () => {
1670
+ .command('doctor', { hidden: true })
1671
+ .argument('[name]', 'Photon name to diagnose (checks environment if omitted)')
1672
+ .description('Run diagnostics on photon environment and installations')
1673
+ .action(async (name, options, command) => {
1524
1674
  try {
1525
- const { MarketplaceManager } = await import('./marketplace-manager.js');
1526
- const manager = new MarketplaceManager();
1527
- await manager.initialize();
1528
- console.error('🔍 Scanning for MCP conflicts across marketplaces...\n');
1529
- const conflicts = await manager.detectAllConflicts();
1530
- if (conflicts.size === 0) {
1531
- console.error('✅ No conflicts detected');
1532
- console.error('\nAll MCPs are uniquely available from single marketplaces.');
1533
- return;
1675
+ const { formatOutput, printInfo, printSuccess, printWarning, STATUS } = await import('./cli-formatter.js');
1676
+ const workingDir = command.parent?.opts().dir || DEFAULT_WORKING_DIR;
1677
+ let issuesFound = 0;
1678
+ printInfo('Running Photon diagnostics...\n');
1679
+ // Build diagnostic data
1680
+ const diagnostics = {};
1681
+ // Check Node version
1682
+ const nodeVersion = process.version;
1683
+ const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
1684
+ diagnostics['Node.js'] = {
1685
+ version: nodeVersion,
1686
+ status: majorVersion >= 18 ? STATUS.OK : STATUS.ERROR,
1687
+ note: majorVersion >= 18 ? 'supported' : 'requires Node.js 18+',
1688
+ };
1689
+ if (majorVersion < 18)
1690
+ issuesFound++;
1691
+ // Check npm/npx
1692
+ try {
1693
+ const { execSync } = await import('child_process');
1694
+ const npmVersion = execSync('npm --version', { encoding: 'utf-8' }).trim();
1695
+ diagnostics['Package Manager'] = {
1696
+ npm: npmVersion,
1697
+ status: STATUS.OK,
1698
+ };
1534
1699
  }
1535
- console.error(`⚠️ Found ${conflicts.size} MCP(s) in multiple marketplaces:\n`);
1536
- for (const [mcpName, sources] of conflicts) {
1537
- console.error(`📦 ${mcpName}`);
1538
- sources.forEach(source => {
1539
- const version = source.metadata?.version || 'unknown';
1540
- const description = source.metadata?.description || '';
1541
- console.error(` → ${source.marketplace.name} (v${version})`);
1542
- if (description) {
1543
- console.error(` ${description.substring(0, 60)}${description.length > 60 ? '...' : ''}`);
1700
+ catch {
1701
+ diagnostics['Package Manager'] = {
1702
+ npm: 'not found',
1703
+ status: STATUS.ERROR,
1704
+ };
1705
+ issuesFound++;
1706
+ }
1707
+ // Check working directory
1708
+ try {
1709
+ await fs.access(workingDir);
1710
+ const stats = await fs.stat(workingDir);
1711
+ if (stats.isDirectory()) {
1712
+ const mcps = await listPhotonMCPs(workingDir);
1713
+ diagnostics['Working Directory'] = {
1714
+ path: workingDir,
1715
+ status: STATUS.OK,
1716
+ photons: mcps.length,
1717
+ };
1718
+ }
1719
+ else {
1720
+ diagnostics['Working Directory'] = {
1721
+ path: workingDir,
1722
+ status: STATUS.ERROR,
1723
+ note: 'not a directory',
1724
+ };
1725
+ issuesFound++;
1726
+ }
1727
+ }
1728
+ catch {
1729
+ diagnostics['Working Directory'] = {
1730
+ path: workingDir,
1731
+ status: STATUS.UNKNOWN,
1732
+ note: 'will be created on first use',
1733
+ };
1734
+ }
1735
+ // Check cache directory
1736
+ const cacheDir = path.join(os.homedir(), '.cache', 'photon-mcp', 'compiled');
1737
+ try {
1738
+ await fs.access(cacheDir);
1739
+ const files = await fs.readdir(cacheDir);
1740
+ diagnostics['Cache Directory'] = {
1741
+ path: cacheDir,
1742
+ status: STATUS.OK,
1743
+ cachedFiles: files.length,
1744
+ };
1745
+ }
1746
+ catch {
1747
+ diagnostics['Cache Directory'] = {
1748
+ path: cacheDir,
1749
+ status: STATUS.UNKNOWN,
1750
+ note: 'will be created on first use',
1751
+ };
1752
+ }
1753
+ // Check marketplaces and conflicts
1754
+ let manager;
1755
+ try {
1756
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
1757
+ manager = new MarketplaceManager();
1758
+ await manager.initialize();
1759
+ const marketplaces = manager.getAll();
1760
+ const enabled = marketplaces.filter((m) => m.enabled);
1761
+ if (enabled.length > 0) {
1762
+ // Check for conflicts
1763
+ const conflicts = await manager.detectAllConflicts();
1764
+ if (conflicts.size > 0) {
1765
+ diagnostics['Marketplaces'] = {
1766
+ enabled: enabled.length,
1767
+ status: STATUS.WARN,
1768
+ sources: enabled.map((m) => m.name),
1769
+ conflicts: `${conflicts.size} photon(s) in multiple marketplaces`,
1770
+ };
1771
+ issuesFound++;
1772
+ }
1773
+ else {
1774
+ diagnostics['Marketplaces'] = {
1775
+ enabled: enabled.length,
1776
+ status: STATUS.OK,
1777
+ sources: enabled.map((m) => m.name),
1778
+ };
1779
+ }
1780
+ }
1781
+ else {
1782
+ diagnostics['Marketplaces'] = {
1783
+ enabled: 0,
1784
+ status: STATUS.UNKNOWN,
1785
+ note: 'Add one with: photon marketplace add portel-dev/photons',
1786
+ };
1787
+ }
1788
+ }
1789
+ catch (error) {
1790
+ diagnostics['Marketplaces'] = {
1791
+ status: STATUS.ERROR,
1792
+ error: error.message,
1793
+ };
1794
+ issuesFound++;
1795
+ }
1796
+ // Check specific photon if provided
1797
+ if (name) {
1798
+ const photonDiag = {};
1799
+ const filePath = await resolvePhotonPath(name, workingDir);
1800
+ if (!filePath) {
1801
+ photonDiag['location'] = 'not found';
1802
+ photonDiag['status'] = STATUS.ERROR;
1803
+ issuesFound++;
1804
+ }
1805
+ else {
1806
+ photonDiag['location'] = filePath;
1807
+ photonDiag['status'] = STATUS.OK;
1808
+ // Check if it compiles
1809
+ try {
1810
+ const source = await fs.readFile(filePath, 'utf-8');
1811
+ const extractor = new SchemaExtractor();
1812
+ const params = extractor.extractConstructorParams(source);
1813
+ photonDiag['syntax'] = STATUS.OK;
1814
+ photonDiag['constructorParams'] = params.length;
1815
+ }
1816
+ catch (error) {
1817
+ photonDiag['syntax'] = STATUS.ERROR;
1818
+ photonDiag['syntaxError'] = error.message;
1819
+ issuesFound++;
1820
+ }
1821
+ // Check cache
1822
+ const cachedFile = path.join(cacheDir, `${name}.js`);
1823
+ try {
1824
+ await fs.access(cachedFile);
1825
+ photonDiag['cache'] = STATUS.OK;
1826
+ }
1827
+ catch {
1828
+ photonDiag['cache'] = 'not cached yet';
1829
+ }
1830
+ // Check dependencies and security
1831
+ try {
1832
+ const { DependencyManager } = await import('@portel/photon-core');
1833
+ const depManager = new DependencyManager();
1834
+ const dependencies = await depManager.extractDependencies(filePath);
1835
+ if (dependencies.length > 0) {
1836
+ photonDiag['dependencies'] = dependencies.map(dep => `${dep.name}@${dep.version}`);
1837
+ // Security audit
1838
+ try {
1839
+ const { SecurityScanner } = await import('./security-scanner.js');
1840
+ const scanner = new SecurityScanner();
1841
+ const depStrings = dependencies.map(d => `${d.name}@${d.version}`);
1842
+ const result = await scanner.auditMCP(name, depStrings);
1843
+ if (result.totalVulnerabilities > 0) {
1844
+ photonDiag['security'] = STATUS.ERROR;
1845
+ photonDiag['vulnerabilities'] = `${result.totalVulnerabilities} (${result.criticalCount} critical, ${result.highCount} high)`;
1846
+ issuesFound++;
1847
+ }
1848
+ else {
1849
+ photonDiag['security'] = STATUS.OK;
1850
+ }
1851
+ }
1852
+ catch {
1853
+ photonDiag['security'] = 'could not check';
1854
+ }
1855
+ }
1856
+ else {
1857
+ photonDiag['dependencies'] = 'none';
1858
+ photonDiag['security'] = STATUS.OK;
1859
+ }
1860
+ }
1861
+ catch (error) {
1862
+ photonDiag['dependencies'] = `error: ${error.message}`;
1544
1863
  }
1545
- });
1546
- // Show recommendation
1547
- const conflict = await manager.checkConflict(mcpName);
1548
- if (conflict.recommendation) {
1549
- console.error(` 💡 Recommended: ${conflict.recommendation}`);
1550
1864
  }
1551
- console.error('');
1865
+ diagnostics[`Photon: ${name}`] = photonDiag;
1866
+ }
1867
+ formatOutput(diagnostics, 'tree');
1868
+ // Summary
1869
+ console.log('');
1870
+ if (issuesFound === 0) {
1871
+ printSuccess('No issues found! Photon environment is healthy.');
1872
+ }
1873
+ else {
1874
+ printWarning(`Found ${issuesFound} issue(s). Please address the items marked with '${STATUS.ERROR}' above.`);
1875
+ process.exit(1);
1552
1876
  }
1553
- console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1554
- console.error('\n💡 Tip: Use --marketplace flag to specify which to use:');
1555
- console.error(' photon add <name> --marketplace <marketplace-name>');
1556
- console.error('\nOr disable marketplaces you don\'t need:');
1557
- console.error(' photon marketplace disable <marketplace-name>');
1558
1877
  }
1559
1878
  catch (error) {
1560
- console.error(`❌ Error: ${error.message}`);
1879
+ const { printError } = await import('./cli-formatter.js');
1880
+ printError(error.message);
1561
1881
  process.exit(1);
1562
1882
  }
1563
1883
  });
1884
+ // CLI command: directly invoke photon methods
1885
+ program
1886
+ .command('cli <photon> [method] [args...]', { hidden: true })
1887
+ .description('Run photon methods directly from the command line')
1888
+ .allowUnknownOption()
1889
+ .helpOption(false) // Disable default help so we can handle it ourselves
1890
+ .action(async (photon, method, args) => {
1891
+ // Handle help flag
1892
+ if (photon === '--help' || photon === '-h') {
1893
+ console.log(`USAGE:
1894
+ photon cli <photon-name> [method] [args...]
1895
+
1896
+ DESCRIPTION:
1897
+ Run photon methods directly from the command line. Photons provide
1898
+ a CLI interface automatically based on their exported methods.
1899
+
1900
+ EXAMPLES:
1901
+ # List all methods for a photon
1902
+ photon cli lg-remote
1903
+
1904
+ # Call a method with no parameters
1905
+ photon cli lg-remote status
1906
+
1907
+ # Call a method with parameters
1908
+ photon cli lg-remote volume 50
1909
+ photon cli lg-remote volume +5
1910
+ photon cli lg-remote volume --level=-3
1911
+
1912
+ # Get method-specific help
1913
+ photon cli lg-remote volume --help
1914
+
1915
+ # Output raw JSON instead of formatted text
1916
+ photon cli lg-remote status --json
1917
+
1918
+ SEE ALSO:
1919
+ photon info List all installed photons
1920
+ photon add <name> Install a photon from marketplace
1921
+ photon alias Create CLI shortcuts for photons
1922
+ `);
1923
+ return;
1924
+ }
1925
+ const { listMethods, runMethod } = await import('./photon-cli-runner.js');
1926
+ if (!method) {
1927
+ // List all methods
1928
+ await listMethods(photon);
1929
+ }
1930
+ else {
1931
+ // Run specific method
1932
+ await runMethod(photon, method, args);
1933
+ }
1934
+ });
1935
+ // Alias commands: create CLI shortcuts for photons
1936
+ program
1937
+ .command('alias', { hidden: true })
1938
+ .argument('<photon>', 'Photon to create alias for')
1939
+ .argument('[alias-name]', 'Custom alias name (defaults to photon name)')
1940
+ .description('Create a CLI alias for a photon')
1941
+ .action(async (photon, aliasName) => {
1942
+ const { createAlias } = await import('./cli-alias.js');
1943
+ await createAlias(photon, aliasName);
1944
+ });
1945
+ program
1946
+ .command('unalias', { hidden: true })
1947
+ .argument('<alias-name>', 'Alias to remove')
1948
+ .description('Remove a CLI alias')
1949
+ .action(async (aliasName) => {
1950
+ const { removeAlias } = await import('./cli-alias.js');
1951
+ await removeAlias(aliasName);
1952
+ });
1953
+ program
1954
+ .command('aliases', { hidden: true })
1955
+ .description('List all CLI aliases')
1956
+ .action(async () => {
1957
+ const { listAliases } = await import('./cli-alias.js');
1958
+ await listAliases();
1959
+ });
1960
+ // All known commands for "did you mean" suggestions
1961
+ const knownCommands = [
1962
+ 'mcp', 'info', 'list', 'ls', 'search',
1963
+ 'add', 'remove', 'rm', 'upgrade', 'up', 'update',
1964
+ 'clear-cache', 'clean', 'doctor',
1965
+ 'cli', 'alias', 'unalias', 'aliases',
1966
+ 'marketplace', 'maker',
1967
+ ];
1968
+ const knownSubcommands = {
1969
+ marketplace: ['list', 'add', 'remove', 'enable', 'disable'],
1970
+ maker: ['new', 'validate', 'sync', 'init'],
1971
+ };
1972
+ /**
1973
+ * Calculate Levenshtein distance between two strings
1974
+ */
1975
+ function levenshteinDistance(a, b) {
1976
+ const matrix = [];
1977
+ for (let i = 0; i <= b.length; i++) {
1978
+ matrix[i] = [i];
1979
+ }
1980
+ for (let j = 0; j <= a.length; j++) {
1981
+ matrix[0][j] = j;
1982
+ }
1983
+ for (let i = 1; i <= b.length; i++) {
1984
+ for (let j = 1; j <= a.length; j++) {
1985
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
1986
+ matrix[i][j] = matrix[i - 1][j - 1];
1987
+ }
1988
+ else {
1989
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
1990
+ matrix[i][j - 1] + 1, // insertion
1991
+ matrix[i - 1][j] + 1 // deletion
1992
+ );
1993
+ }
1994
+ }
1995
+ }
1996
+ return matrix[b.length][a.length];
1997
+ }
1998
+ /**
1999
+ * Find closest matching command
2000
+ */
2001
+ function findClosestCommand(input, commands) {
2002
+ let closest = null;
2003
+ let minDistance = Infinity;
2004
+ for (const cmd of commands) {
2005
+ const distance = levenshteinDistance(input.toLowerCase(), cmd.toLowerCase());
2006
+ // Only suggest if distance is small enough (max 3 edits for short commands, proportional for longer)
2007
+ const maxDistance = Math.max(2, Math.floor(cmd.length / 2));
2008
+ if (distance < minDistance && distance <= maxDistance) {
2009
+ minDistance = distance;
2010
+ closest = cmd;
2011
+ }
2012
+ }
2013
+ return closest;
2014
+ }
2015
+ // Handle unknown commands with "did you mean" suggestions
2016
+ program.on('command:*', async (operands) => {
2017
+ const { printError, printInfo } = await import('./cli-formatter.js');
2018
+ const unknownCommand = operands[0];
2019
+ printError(`Unknown command: ${unknownCommand}`);
2020
+ // Check if it's a subcommand typo for a known parent
2021
+ const args = process.argv.slice(2);
2022
+ const parentIndex = args.findIndex(arg => knownSubcommands[arg]);
2023
+ if (parentIndex !== -1 && parentIndex < args.indexOf(unknownCommand)) {
2024
+ const parent = args[parentIndex];
2025
+ const suggestion = findClosestCommand(unknownCommand, knownSubcommands[parent]);
2026
+ if (suggestion) {
2027
+ printInfo(`Did you mean: photon ${parent} ${suggestion}`);
2028
+ }
2029
+ }
2030
+ else {
2031
+ // Check for top-level command typo
2032
+ const suggestion = findClosestCommand(unknownCommand, knownCommands);
2033
+ if (suggestion) {
2034
+ printInfo(`Did you mean: photon ${suggestion}`);
2035
+ }
2036
+ }
2037
+ console.log('');
2038
+ printInfo(`Run 'photon --help' for usage`);
2039
+ process.exit(1);
2040
+ });
1564
2041
  program.parse();
1565
2042
  /**
1566
2043
  * Inline template fallback