UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

576 lines (575 loc) 22.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.TemplateMarketplace = void 0; exports.createTemplateMarketplace = createTemplateMarketplace; exports.getGlobalTemplateMarketplace = getGlobalTemplateMarketplace; exports.setGlobalTemplateMarketplace = setGlobalTemplateMarketplace; const events_1 = require("events"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const template_validator_1 = require("./template-validator"); const template_versioning_1 = require("./template-versioning"); class TemplateMarketplace extends events_1.EventEmitter { constructor(localTemplatesDir, config = {}) { super(); this.localTemplatesDir = localTemplatesDir; this.config = config; this.cache = new Map(); this.offlineTemplates = new Map(); this.defaultConfig = { apiUrl: 'https://api.re-shell.dev/marketplace', cacheTTL: 5 * 60 * 1000, // 5 minutes enableAnalytics: true, enableOfflineMode: true, maxCacheSize: 100 * 1024 * 1024, // 100MB userAgent: 'Re-Shell CLI' }; this.config = { ...this.defaultConfig, ...config }; this.validator = new template_validator_1.TemplateValidator(); this.versionManager = new template_versioning_1.TemplateVersionManager(localTemplatesDir); this.initialize(); } async initialize() { if (this.config.cacheDir) { await fs.ensureDir(this.config.cacheDir); } if (this.config.enableOfflineMode) { await this.loadOfflineTemplates(); } } async loadOfflineTemplates() { const offlinePath = path.join(this.localTemplatesDir, '.marketplace-cache.json'); if (await fs.pathExists(offlinePath)) { try { const data = await fs.readJson(offlinePath); for (const template of data.templates || []) { this.offlineTemplates.set(template.id, template); } } catch (error) { this.emit('error', { type: 'offline_load', error }); } } } async search(options = {}) { const cacheKey = this.getCacheKey('search', options); const cached = this.getFromCache(cacheKey); if (cached) { return cached; } try { const result = await this.searchOnline(options); this.setCache(cacheKey, result); // Update offline cache if (this.config.enableOfflineMode) { await this.updateOfflineCache(result.templates); } return result; } catch (error) { // Fallback to offline search if (this.config.enableOfflineMode) { return this.searchOffline(options); } throw error; } } async searchOnline(options) { const params = new URLSearchParams(); if (options.query) params.append('q', options.query); if (options.category) params.append('category', options.category); if (options.tags) params.append('tags', options.tags.join(',')); if (options.author) params.append('author', options.author); if (options.minRating) params.append('minRating', options.minRating.toString()); if (options.verified !== undefined) params.append('verified', options.verified.toString()); if (options.featured !== undefined) params.append('featured', options.featured.toString()); if (options.sortBy) params.append('sort', options.sortBy); if (options.order) params.append('order', options.order); if (options.limit) params.append('limit', options.limit.toString()); if (options.offset) params.append('offset', options.offset.toString()); const response = await this.apiRequest(`/templates?${params}`); return { templates: response.data.map((t) => this.transformTemplate(t)), total: response.total, page: response.page, pageSize: response.pageSize, totalPages: response.totalPages, facets: response.facets }; } searchOffline(options) { let templates = Array.from(this.offlineTemplates.values()); // Apply filters if (options.query) { const query = options.query.toLowerCase(); templates = templates.filter(t => t.name.toLowerCase().includes(query) || t.description.toLowerCase().includes(query) || t.tags.some(tag => tag.toLowerCase().includes(query))); } if (options.category) { templates = templates.filter(t => t.category === options.category); } if (options.tags && options.tags.length > 0) { templates = templates.filter(t => options.tags.some(tag => t.tags.includes(tag))); } if (options.author) { templates = templates.filter(t => t.author === options.author); } if (options.minRating) { templates = templates.filter(t => t.rating >= options.minRating); } if (options.verified !== undefined) { templates = templates.filter(t => t.verified === options.verified); } if (options.featured !== undefined) { templates = templates.filter(t => t.featured === options.featured); } // Sort templates = this.sortTemplates(templates, options.sortBy, options.order); // Paginate const limit = options.limit || 20; const offset = options.offset || 0; const paginatedTemplates = templates.slice(offset, offset + limit); return { templates: paginatedTemplates, total: templates.length, page: Math.floor(offset / limit) + 1, pageSize: limit, totalPages: Math.ceil(templates.length / limit) }; } sortTemplates(templates, sortBy = 'downloads', order = 'desc') { const sorted = [...templates]; sorted.sort((a, b) => { let compareValue = 0; switch (sortBy) { case 'downloads': compareValue = a.downloads - b.downloads; break; case 'rating': compareValue = a.rating - b.rating; break; case 'updated': compareValue = a.updatedAt.getTime() - b.updatedAt.getTime(); break; case 'created': compareValue = a.createdAt.getTime() - b.createdAt.getTime(); break; case 'trending': compareValue = a.stats.weeklyDownloads - b.stats.weeklyDownloads; break; } return order === 'asc' ? compareValue : -compareValue; }); return sorted; } async getTemplate(templateId) { const cacheKey = this.getCacheKey('template', templateId); const cached = this.getFromCache(cacheKey); if (cached) { return cached; } try { const response = await this.apiRequest(`/templates/${templateId}`); const template = this.transformTemplate(response); this.setCache(cacheKey, template); if (this.config.enableOfflineMode) { await this.updateOfflineCache([template]); } return template; } catch (error) { // Fallback to offline if (this.config.enableOfflineMode) { return this.offlineTemplates.get(templateId) || null; } throw error; } } async getReviews(templateId, limit = 10, offset = 0) { const cacheKey = this.getCacheKey('reviews', { templateId, limit, offset }); const cached = this.getFromCache(cacheKey); if (cached) { return cached; } try { const response = await this.apiRequest(`/templates/${templateId}/reviews?limit=${limit}&offset=${offset}`); const reviews = response.data.map((r) => ({ ...r, createdAt: new Date(r.createdAt), updatedAt: r.updatedAt ? new Date(r.updatedAt) : undefined })); this.setCache(cacheKey, reviews); return reviews; } catch (error) { this.emit('error', { type: 'reviews_fetch', templateId, error }); return []; } } async submitReview(templateId, rating, title, comment) { if (rating < 1 || rating > 5) { throw new Error('Rating must be between 1 and 5'); } try { await this.apiRequest(`/templates/${templateId}/reviews`, { method: 'POST', body: JSON.stringify({ rating, title, comment }) }); // Invalidate cache this.invalidateCache('reviews', templateId); this.invalidateCache('template', templateId); this.emit('review:submitted', { templateId, rating }); return true; } catch (error) { this.emit('error', { type: 'review_submit', templateId, error }); return false; } } async install(templateId, version) { this.emit('install:start', { templateId, version }); try { // Get template metadata const template = await this.getTemplate(templateId); if (!template) { throw new Error(`Template ${templateId} not found`); } // Download template const targetVersion = version || template.version; const downloadUrl = await this.getDownloadUrl(templateId, targetVersion); const installPath = path.join(this.localTemplatesDir, templateId); // Download and extract await this.downloadAndExtract(downloadUrl, installPath); // Validate template const templateData = await this.loadTemplateFromPath(installPath); const validation = await this.validator.validate(templateData); if (!validation.valid && validation.errors.some(e => e.severity === 'critical')) { await fs.remove(installPath); throw new Error(`Template validation failed: ${validation.errors[0].message}`); } // Register with version manager await this.versionManager.registerTemplate(templateData, installPath); // Track analytics if (this.config.enableAnalytics) { this.trackInstall(templateId, targetVersion).catch(() => { }); } this.emit('install:complete', { templateId, version: targetVersion, path: installPath }); return installPath; } catch (error) { this.emit('install:error', { templateId, error }); throw error; } } async publish(templatePath, options = {}) { this.emit('publish:start', { templatePath, options }); try { // Load and validate template const template = await this.loadTemplateFromPath(templatePath); const validation = await this.validator.validate(template); if (!validation.valid) { return { success: false, templateId: template.id, version: template.version, errors: validation.errors.map(e => e.message), warnings: validation.warnings.map(w => w.message) }; } // Dry run if (options.dryRun) { return { success: true, templateId: template.id, version: template.version, warnings: ['This is a dry run. Template was not published.'] }; } // Package template const packagePath = await this.packageTemplate(templatePath); // Upload to marketplace const formData = new FormData(); const packageBuffer = await fs.readFile(packagePath); formData.append('template', new Blob([packageBuffer]), path.basename(packagePath)); formData.append('metadata', JSON.stringify({ private: options.private, beta: options.beta, force: options.force })); const response = await this.apiRequest('/templates', { method: 'POST', body: formData }); // Cleanup await fs.remove(packagePath); this.emit('publish:complete', { templateId: template.id, version: template.version }); return { success: true, templateId: template.id, version: template.version, url: response.url, warnings: validation.warnings.map(w => w.message) }; } catch (error) { this.emit('publish:error', { templatePath, error }); return { success: false, templateId: '', version: '', errors: [error.message] }; } } async unpublish(templateId, version) { try { const endpoint = version ? `/templates/${templateId}/versions/${version}` : `/templates/${templateId}`; await this.apiRequest(endpoint, { method: 'DELETE' }); this.invalidateCache('template', templateId); this.emit('unpublish:complete', { templateId, version }); return true; } catch (error) { this.emit('unpublish:error', { templateId, version, error }); return false; } } async getFeaturedTemplates() { return (await this.search({ featured: true, limit: 10 })).templates; } async getTrendingTemplates() { return (await this.search({ sortBy: 'trending', limit: 10 })).templates; } async getTemplatesByAuthor(author) { return (await this.search({ author, limit: 100 })).templates; } async getAuthorProfile(username) { const cacheKey = this.getCacheKey('author', username); const cached = this.getFromCache(cacheKey); if (cached) { return cached; } try { const response = await this.apiRequest(`/authors/${username}`); const profile = { ...response, joinedAt: new Date(response.joinedAt) }; this.setCache(cacheKey, profile); return profile; } catch (error) { return null; } } async apiRequest(endpoint, options = {}) { const url = `${this.config.apiUrl}${endpoint}`; const headers = { 'User-Agent': this.config.userAgent, ...options.headers }; if (this.config.apiKey) { headers['Authorization'] = `Bearer ${this.config.apiKey}`; } if (options.body && typeof options.body === 'string') { headers['Content-Type'] = 'application/json'; } const response = await fetch(url, { ...options, headers }); if (!response.ok) { const error = await response.text(); throw new Error(`API request failed: ${response.status} - ${error}`); } return await response.json(); } transformTemplate(data) { return { ...data, createdAt: new Date(data.createdAt), updatedAt: new Date(data.updatedAt), stats: { ...data.stats, lastCommit: new Date(data.stats.lastCommit) } }; } async getDownloadUrl(templateId, version) { const response = await this.apiRequest(`/templates/${templateId}/download?version=${version}`); return response.url; } async downloadAndExtract(url, destination) { const response = await fetch(url); if (!response.ok) { throw new Error(`Download failed: ${response.statusText}`); } const tempFile = path.join(this.localTemplatesDir, `.tmp-${Date.now()}.tar.gz`); const buffer = await response.arrayBuffer(); await fs.writeFile(tempFile, Buffer.from(buffer)); // Extract await fs.ensureDir(destination); const tar = require('tar'); await tar.extract({ file: tempFile, cwd: destination }); // Cleanup await fs.remove(tempFile); } async loadTemplateFromPath(templatePath) { const manifestPath = path.join(templatePath, 'template.yaml'); const yaml = require('js-yaml'); const content = await fs.readFile(manifestPath, 'utf8'); return yaml.load(content); } async packageTemplate(templatePath) { const tar = require('tar'); const packagePath = path.join(this.localTemplatesDir, `.tmp-package-${Date.now()}.tar.gz`); await tar.create({ gzip: true, file: packagePath, cwd: templatePath, filter: (path) => !path.includes('node_modules') && !path.startsWith('.') }, ['.']); return packagePath; } async trackInstall(templateId, version) { try { await this.apiRequest('/analytics/install', { method: 'POST', body: JSON.stringify({ templateId, version }) }); } catch { // Ignore analytics errors } } async updateOfflineCache(templates) { for (const template of templates) { this.offlineTemplates.set(template.id, template); } // Save to file const offlinePath = path.join(this.localTemplatesDir, '.marketplace-cache.json'); await fs.writeJson(offlinePath, { templates: Array.from(this.offlineTemplates.values()), updated: new Date() }); // Cleanup old entries await this.cleanupOfflineCache(); } async cleanupOfflineCache() { if (!this.config.maxCacheSize) return; const offlinePath = path.join(this.localTemplatesDir, '.marketplace-cache.json'); const stats = await fs.stat(offlinePath); if (stats.size > this.config.maxCacheSize) { // Remove oldest templates const templates = Array.from(this.offlineTemplates.values()) .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()); while (templates.length > 100) { // Keep top 100 const removed = templates.pop(); this.offlineTemplates.delete(removed.id); } await fs.writeJson(offlinePath, { templates: Array.from(this.offlineTemplates.values()), updated: new Date() }); } } getCacheKey(type, data) { return `${type}:${JSON.stringify(data)}`; } getFromCache(key) { const cached = this.cache.get(key); if (!cached) return null; if (Date.now() - cached.timestamp > this.config.cacheTTL) { this.cache.delete(key); return null; } return cached.data; } setCache(key, data) { this.cache.set(key, { data, timestamp: Date.now() }); // Limit cache size if (this.cache.size > 1000) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } } invalidateCache(type, id) { for (const key of this.cache.keys()) { if (key.startsWith(`${type}:`) && key.includes(id)) { this.cache.delete(key); } } } clearCache() { this.cache.clear(); this.emit('cache:cleared'); } } exports.TemplateMarketplace = TemplateMarketplace; // Global marketplace instance let globalMarketplace = null; function createTemplateMarketplace(localTemplatesDir, config) { return new TemplateMarketplace(localTemplatesDir, config); } function getGlobalTemplateMarketplace() { if (!globalMarketplace) { const templatesDir = path.join(process.cwd(), 'templates'); globalMarketplace = new TemplateMarketplace(templatesDir); } return globalMarketplace; } function setGlobalTemplateMarketplace(marketplace) { globalMarketplace = marketplace; }