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