@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.
- package/README.md +319 -60
- package/dist/cli-alias.d.ts +21 -0
- package/dist/cli-alias.d.ts.map +1 -0
- package/dist/cli-alias.js +232 -0
- package/dist/cli-alias.js.map +1 -0
- package/dist/cli-formatter.d.ts +25 -0
- package/dist/cli-formatter.d.ts.map +1 -0
- package/dist/cli-formatter.js +326 -0
- package/dist/cli-formatter.js.map +1 -0
- package/dist/cli.js +961 -484
- package/dist/cli.js.map +1 -1
- package/dist/daemon/client.d.ts +15 -0
- package/dist/daemon/client.d.ts.map +1 -0
- package/dist/daemon/client.js +126 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/manager.d.ts +32 -0
- package/dist/daemon/manager.d.ts.map +1 -0
- package/dist/daemon/manager.js +155 -0
- package/dist/daemon/manager.js.map +1 -0
- package/dist/daemon/protocol.d.ts +48 -0
- package/dist/daemon/protocol.d.ts.map +1 -0
- package/dist/daemon/protocol.js +7 -0
- package/dist/daemon/protocol.js.map +1 -0
- package/dist/daemon/server.d.ts +12 -0
- package/dist/daemon/server.d.ts.map +1 -0
- package/dist/daemon/server.js +215 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/session-manager.d.ts +46 -0
- package/dist/daemon/session-manager.d.ts.map +1 -0
- package/dist/daemon/session-manager.js +120 -0
- package/dist/daemon/session-manager.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +16 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +125 -19
- package/dist/loader.js.map +1 -1
- package/dist/path-resolver.d.ts +12 -19
- package/dist/path-resolver.d.ts.map +1 -1
- package/dist/path-resolver.js +35 -61
- package/dist/path-resolver.js.map +1 -1
- package/dist/photon-cli-runner.d.ts +15 -0
- package/dist/photon-cli-runner.d.ts.map +1 -0
- package/dist/photon-cli-runner.js +1124 -0
- package/dist/photon-cli-runner.js.map +1 -0
- package/dist/photon-doc-extractor.d.ts +10 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +89 -9
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/schema-extractor.d.ts +27 -0
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +333 -2
- package/dist/schema-extractor.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +16 -8
- package/dist/server.js.map +1 -1
- package/dist/template-manager.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- 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 '
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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('--
|
|
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().
|
|
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.
|
|
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
|
-
|
|
551
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
720
|
+
infoData.description = photonMetadata.description;
|
|
598
721
|
}
|
|
599
722
|
if (metadata) {
|
|
600
|
-
|
|
723
|
+
infoData.installed = new Date(metadata.installedAt).toLocaleDateString();
|
|
724
|
+
infoData.source = metadata.marketplace;
|
|
601
725
|
if (isModified) {
|
|
602
|
-
|
|
726
|
+
infoData.status = 'modified locally';
|
|
603
727
|
}
|
|
604
728
|
}
|
|
605
729
|
const toolCount = photonMetadata.tools?.length || 0;
|
|
606
730
|
if (toolCount > 0) {
|
|
607
|
-
|
|
731
|
+
infoData.tools = toolCount;
|
|
608
732
|
}
|
|
609
|
-
|
|
733
|
+
formatOutput(infoData, 'tree');
|
|
734
|
+
console.log('');
|
|
610
735
|
// Show appropriate run command
|
|
611
736
|
if (metadata && !isModified) {
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
617
|
-
|
|
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
|
-
|
|
745
|
+
printInfo(`Run with: photon mcp ${name} --dev`);
|
|
621
746
|
}
|
|
622
|
-
console.
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
640
|
-
|
|
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
|
-
|
|
646
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
703
|
-
|
|
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
|
-
|
|
846
|
+
tableData.push({
|
|
847
|
+
name: mcpName,
|
|
848
|
+
version: '-',
|
|
849
|
+
source: 'local',
|
|
850
|
+
status: STATUS.OK,
|
|
851
|
+
});
|
|
708
852
|
}
|
|
709
853
|
}
|
|
710
|
-
|
|
854
|
+
formatOutput(tableData, 'table');
|
|
855
|
+
console.log('');
|
|
711
856
|
}
|
|
712
857
|
else {
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
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
|
-
|
|
729
|
-
|
|
730
|
-
const
|
|
731
|
-
|
|
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
|
-
|
|
880
|
+
photonList['...'] = `${count - 3} more`;
|
|
736
881
|
}
|
|
882
|
+
marketplaceTree[`${marketplace.name} (${marketplace.repo})`] = photonList;
|
|
737
883
|
}
|
|
738
884
|
}
|
|
739
885
|
else {
|
|
740
|
-
|
|
886
|
+
marketplaceTree[marketplace.name] = 'no manifest';
|
|
741
887
|
}
|
|
742
888
|
}
|
|
743
|
-
|
|
744
|
-
console.
|
|
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
|
-
|
|
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
|
-
|
|
914
|
+
printInfo('Refreshed marketplace data...\n');
|
|
765
915
|
}
|
|
766
|
-
|
|
916
|
+
printInfo(`Searching for '${query}' in marketplaces...`);
|
|
767
917
|
const results = await manager.search(query);
|
|
768
918
|
if (results.size === 0) {
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
|
|
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
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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.
|
|
937
|
+
console.log('');
|
|
938
|
+
formatOutput(tableData, 'table');
|
|
939
|
+
printInfo(`\nInstall with: photon add <name>`);
|
|
791
940
|
}
|
|
792
941
|
catch (error) {
|
|
793
|
-
|
|
942
|
+
const { printError } = await import('./cli-formatter.js');
|
|
943
|
+
printError(error.message);
|
|
794
944
|
process.exit(1);
|
|
795
945
|
}
|
|
796
946
|
});
|
|
797
|
-
//
|
|
798
|
-
const
|
|
799
|
-
.command('
|
|
800
|
-
.description('
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
.
|
|
804
|
-
.
|
|
805
|
-
.
|
|
806
|
-
.
|
|
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
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
-
|
|
821
|
-
|
|
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
|
-
//
|
|
827
|
-
|
|
828
|
-
.command('
|
|
829
|
-
.
|
|
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
|
-
.
|
|
837
|
-
.
|
|
838
|
-
|
|
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
|
-
|
|
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
|
-
|
|
851
|
-
marketplace
|
|
1067
|
+
maker
|
|
852
1068
|
.command('init')
|
|
853
|
-
.
|
|
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 (
|
|
1074
|
+
.action(async (options) => {
|
|
859
1075
|
try {
|
|
860
|
-
const
|
|
861
|
-
|
|
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
|
-
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
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().
|
|
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().
|
|
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
|
-
|
|
1341
|
-
|
|
1479
|
+
printError(`MCP not found: ${name}`);
|
|
1480
|
+
printInfo(`Searched in: ${workingDir}`);
|
|
1342
1481
|
process.exit(1);
|
|
1343
1482
|
}
|
|
1344
|
-
|
|
1483
|
+
printInfo(`Checking ${name} for updates...`);
|
|
1345
1484
|
const versionInfo = await checker.checkForUpdate(name, filePath);
|
|
1346
1485
|
if (!versionInfo.local) {
|
|
1347
|
-
|
|
1486
|
+
printWarning('Could not determine local version');
|
|
1348
1487
|
return;
|
|
1349
1488
|
}
|
|
1350
1489
|
if (!versionInfo.remote) {
|
|
1351
|
-
|
|
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
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
-
|
|
1504
|
+
printSuccess(`Already up to date (${versionInfo.local})`);
|
|
1365
1505
|
return;
|
|
1366
1506
|
}
|
|
1367
|
-
|
|
1507
|
+
printInfo(`Upgrading ${name}: ${versionInfo.local} → ${versionInfo.remote}`);
|
|
1368
1508
|
const success = await checker.updateMCP(name, filePath);
|
|
1369
1509
|
if (success) {
|
|
1370
|
-
|
|
1510
|
+
printSuccess(`Successfully upgraded ${name} to ${versionInfo.remote}`);
|
|
1371
1511
|
}
|
|
1372
1512
|
else {
|
|
1373
|
-
|
|
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
|
-
|
|
1519
|
+
printInfo(`Checking all MCPs in ${workingDir}...\n`);
|
|
1380
1520
|
const updates = await checker.checkAllUpdates(workingDir);
|
|
1381
1521
|
if (updates.size === 0) {
|
|
1382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1557
|
+
console.log('');
|
|
1558
|
+
printSuccess('All MCPs are up to date!');
|
|
1401
1559
|
return;
|
|
1402
1560
|
}
|
|
1403
1561
|
if (options.check) {
|
|
1404
|
-
console.
|
|
1405
|
-
|
|
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.
|
|
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
|
-
|
|
1574
|
+
printSuccess(`Upgraded ${mcpName}`);
|
|
1415
1575
|
}
|
|
1416
1576
|
else {
|
|
1417
|
-
|
|
1577
|
+
printError(`Failed to upgrade ${mcpName}`);
|
|
1418
1578
|
}
|
|
1419
1579
|
}
|
|
1420
1580
|
}
|
|
1421
1581
|
}
|
|
1422
1582
|
catch (error) {
|
|
1423
|
-
|
|
1583
|
+
const { printError } = await import('./cli-formatter.js');
|
|
1584
|
+
printError(error.message);
|
|
1424
1585
|
process.exit(1);
|
|
1425
1586
|
}
|
|
1426
1587
|
});
|
|
1427
|
-
//
|
|
1588
|
+
// Clear-cache command: clear compiled photon cache
|
|
1428
1589
|
program
|
|
1429
|
-
.command('
|
|
1430
|
-
.argument('[name]', 'MCP name to
|
|
1431
|
-
.
|
|
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
|
|
1435
|
-
const
|
|
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
|
-
//
|
|
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
|
-
|
|
1603
|
+
printError(`Photon not found: ${name}`);
|
|
1604
|
+
printInfo(`Tip: Use 'photon info' to see installed photons`);
|
|
1444
1605
|
process.exit(1);
|
|
1445
1606
|
}
|
|
1446
|
-
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
const
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
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
|
-
//
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
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
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
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
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
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
|
-
|
|
1663
|
+
const { printError } = await import('./cli-formatter.js');
|
|
1664
|
+
printError(error.message);
|
|
1516
1665
|
process.exit(1);
|
|
1517
1666
|
}
|
|
1518
1667
|
});
|
|
1519
|
-
//
|
|
1668
|
+
// Doctor command: diagnose photon environment
|
|
1520
1669
|
program
|
|
1521
|
-
.command('
|
|
1522
|
-
.
|
|
1523
|
-
.
|
|
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 {
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|