@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
JavaScript
"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;
}