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

529 lines (528 loc) 21 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.TemplateVersionManager = void 0; exports.createTemplateVersionManager = createTemplateVersionManager; exports.getGlobalTemplateVersionManager = getGlobalTemplateVersionManager; exports.setGlobalTemplateVersionManager = setGlobalTemplateVersionManager; const events_1 = require("events"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const semver = __importStar(require("semver")); const crypto = __importStar(require("crypto")); class TemplateVersionManager extends events_1.EventEmitter { constructor(templatesDir, config = {}) { super(); this.templatesDir = templatesDir; this.config = config; this.manifests = new Map(); this.migrations = new Map(); this.defaultConfig = { registryUrl: 'https://registry.re-shell.dev/templates', checkInterval: 24 * 60 * 60 * 1000, // 24 hours autoCheck: true, autoUpdate: false, backupBeforeUpdate: true, maxBackups: 5 }; this.config = { ...this.defaultConfig, ...config }; this.cacheDir = this.config.cacheDirectory || path.join(templatesDir, '.cache'); this.initialize(); } async initialize() { await fs.ensureDir(this.cacheDir); await this.loadManifests(); if (this.config.autoCheck) { this.startUpdateChecking(); } } async loadManifests() { const manifestPath = path.join(this.cacheDir, 'manifests.json'); if (await fs.pathExists(manifestPath)) { try { const data = await fs.readJson(manifestPath); for (const [id, manifest] of Object.entries(data)) { this.manifests.set(id, manifest); } } catch (error) { this.emit('error', { type: 'manifest_load', error }); } } } async saveManifests() { const manifestPath = path.join(this.cacheDir, 'manifests.json'); const data = {}; for (const [id, manifest] of this.manifests) { data[id] = manifest; } await fs.writeJson(manifestPath, data, { spaces: 2 }); } startUpdateChecking() { // Initial check this.checkAllUpdates().catch(error => { this.emit('error', { type: 'update_check', error }); }); // Periodic checks if (this.config.checkInterval) { this.updateCheckInterval = setInterval(() => { this.checkAllUpdates().catch(error => { this.emit('error', { type: 'update_check', error }); }); }, this.config.checkInterval); } } async registerTemplate(template, localPath) { const manifest = { id: template.id, name: template.name, description: template.description, latestVersion: template.version, versions: [{ version: template.version, releaseDate: new Date(), changelog: 'Initial version', breaking: false, checksum: await this.calculateChecksum(localPath), size: await this.calculateSize(localPath) }], repository: template.repository, updateChannel: 'stable', autoUpdate: false, installedVersion: template.version, installedPath: localPath }; this.manifests.set(template.id, manifest); await this.saveManifests(); this.emit('template:registered', manifest); return manifest; } async checkUpdate(templateId) { const manifest = this.manifests.get(templateId); if (!manifest || !manifest.installedVersion) { return null; } try { // Fetch latest manifest from registry const latestManifest = await this.fetchManifestFromRegistry(templateId); if (!latestManifest) { return null; } // Update local manifest with remote data manifest.latestVersion = latestManifest.latestVersion; manifest.versions = latestManifest.versions; manifest.lastChecked = new Date(); await this.saveManifests(); // Compare versions const updateAvailable = semver.gt(latestManifest.latestVersion, manifest.installedVersion); if (!updateAvailable) { return { id: templateId, currentVersion: manifest.installedVersion, latestVersion: manifest.latestVersion, updateAvailable: false, isBreaking: false }; } // Check for breaking changes const isBreaking = semver.major(latestManifest.latestVersion) > semver.major(manifest.installedVersion); // Get changelog const changelog = this.getChangelogBetweenVersions(manifest.installedVersion, latestManifest.latestVersion, latestManifest.versions); const result = { id: templateId, currentVersion: manifest.installedVersion, latestVersion: latestManifest.latestVersion, updateAvailable: true, isBreaking, changelog, installCommand: `re-shell template update ${templateId}`, updateUrl: manifest.repository }; this.emit('update:available', result); return result; } catch (error) { this.emit('error', { type: 'update_check', templateId, error }); return null; } } async checkAllUpdates() { const results = []; for (const [templateId, manifest] of this.manifests) { if (manifest.installedVersion) { const result = await this.checkUpdate(templateId); if (result && result.updateAvailable) { results.push(result); } } } return results; } async updateTemplate(templateId, targetVersion, options = {}) { const manifest = this.manifests.get(templateId); if (!manifest || !manifest.installedVersion || !manifest.installedPath) { throw new Error(`Template ${templateId} not found or not installed`); } try { this.emit('update:start', { templateId, currentVersion: manifest.installedVersion }); // Determine target version const version = targetVersion || manifest.latestVersion; // Validate version if (!semver.valid(version)) { throw new Error(`Invalid version: ${version}`); } // Check if version is available const versionInfo = manifest.versions.find(v => v.version === version); if (!versionInfo) { throw new Error(`Version ${version} not found`); } // Check if already at target version if (manifest.installedVersion === version) { this.emit('update:skipped', { templateId, version, reason: 'Already at target version' }); return true; } // Backup current version if requested if (options.backup !== false && this.config.backupBeforeUpdate) { await this.backupTemplate(templateId, manifest.installedVersion); } // Dry run mode if (options.dryRun) { this.emit('update:dry_run', { templateId, fromVersion: manifest.installedVersion, toVersion: version, breaking: semver.major(version) > semver.major(manifest.installedVersion) }); return true; } // Download new version const downloadPath = await this.downloadTemplateVersion(templateId, version); // Load and validate new template const newTemplate = await this.loadTemplateFromPath(downloadPath); // Run migrations if needed const migrated = await this.runMigrations(templateId, manifest.installedVersion, version, newTemplate); // Replace old version with new await fs.remove(manifest.installedPath); await fs.copy(downloadPath, manifest.installedPath); // Update manifest manifest.installedVersion = version; await this.saveManifests(); // Cleanup download await fs.remove(downloadPath); this.emit('update:complete', { templateId, version, previousVersion: manifest.installedVersion }); return true; } catch (error) { this.emit('update:error', { templateId, error }); // Attempt rollback if update failed if (options.backup !== false && this.config.backupBeforeUpdate) { try { await this.rollbackTemplate(templateId, manifest.installedVersion); this.emit('update:rolled_back', { templateId, version: manifest.installedVersion }); } catch (rollbackError) { this.emit('error', { type: 'rollback', templateId, error: rollbackError }); } } throw error; } } async rollbackTemplate(templateId, version) { const backupPath = this.getBackupPath(templateId, version); const manifest = this.manifests.get(templateId); if (!manifest || !manifest.installedPath) { throw new Error(`Template ${templateId} not found`); } if (!await fs.pathExists(backupPath)) { throw new Error(`Backup not found for version ${version}`); } // Restore from backup await fs.remove(manifest.installedPath); await fs.copy(backupPath, manifest.installedPath); // Update manifest manifest.installedVersion = version; await this.saveManifests(); } registerMigration(templateId, migration) { if (!this.migrations.has(templateId)) { this.migrations.set(templateId, []); } const migrations = this.migrations.get(templateId); migrations.push(migration); // Sort by version migrations.sort((a, b) => semver.compare(a.fromVersion, b.fromVersion)); } async runMigrations(templateId, fromVersion, toVersion, template) { const migrations = this.migrations.get(templateId) || []; const applicableMigrations = migrations.filter(m => semver.gte(m.fromVersion, fromVersion) && semver.lte(m.toVersion, toVersion)); if (applicableMigrations.length === 0) { return template; } this.emit('migration:start', { templateId, fromVersion, toVersion, migrations: applicableMigrations.length }); let migratedTemplate = template; const context = { fromVersion, toVersion, templateId }; for (const migration of applicableMigrations) { try { this.emit('migration:step', { templateId, migration: migration.description, fromVersion: migration.fromVersion, toVersion: migration.toVersion }); if (migration.automatic) { migratedTemplate = await migration.script(migratedTemplate, context); } else { // Manual migration required this.emit('migration:manual', { templateId, migration: migration.description, instructions: `Manual migration required from ${migration.fromVersion} to ${migration.toVersion}` }); } } catch (error) { this.emit('migration:error', { templateId, migration, error }); throw error; } } this.emit('migration:complete', { templateId, fromVersion, toVersion }); return migratedTemplate; } async fetchManifestFromRegistry(templateId) { if (!this.config.registryUrl) { return null; } try { const url = `${this.config.registryUrl}/${templateId}/manifest.json`; const response = await fetch(url); if (!response.ok) { return null; } return await response.json(); } catch (error) { this.emit('error', { type: 'registry_fetch', templateId, error }); return null; } } async downloadTemplateVersion(templateId, version) { if (!this.config.registryUrl) { throw new Error('No registry URL configured'); } const url = `${this.config.registryUrl}/${templateId}/${version}.tar.gz`; const downloadPath = path.join(this.cacheDir, 'downloads', `${templateId}-${version}`); await fs.ensureDir(path.dirname(downloadPath)); // Download template archive const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to download template: ${response.statusText}`); } const buffer = await response.arrayBuffer(); const archivePath = `${downloadPath}.tar.gz`; await fs.writeFile(archivePath, Buffer.from(buffer)); // Extract archive const tar = require('tar'); await tar.extract({ file: archivePath, cwd: downloadPath }); // Cleanup archive await fs.remove(archivePath); return downloadPath; } 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 backupTemplate(templateId, version) { const manifest = this.manifests.get(templateId); if (!manifest || !manifest.installedPath) { return; } const backupPath = this.getBackupPath(templateId, version); await fs.ensureDir(path.dirname(backupPath)); await fs.copy(manifest.installedPath, backupPath); // Cleanup old backups await this.cleanupOldBackups(templateId); this.emit('backup:created', { templateId, version, path: backupPath }); } async cleanupOldBackups(templateId) { if (!this.config.maxBackups) return; const backupDir = path.join(this.cacheDir, 'backups', templateId); if (!await fs.pathExists(backupDir)) return; const backups = await fs.readdir(backupDir); if (backups.length <= this.config.maxBackups) return; // Sort by version backups.sort((a, b) => semver.compare(a, b)); // Remove oldest backups const toRemove = backups.slice(0, backups.length - this.config.maxBackups); for (const backup of toRemove) { await fs.remove(path.join(backupDir, backup)); } } getBackupPath(templateId, version) { return path.join(this.cacheDir, 'backups', templateId, version); } getChangelogBetweenVersions(fromVersion, toVersion, versions) { const relevantVersions = versions.filter(v => semver.gt(v.version, fromVersion) && semver.lte(v.version, toVersion)); const changelog = []; for (const version of relevantVersions) { changelog.push(`## ${version.version} (${version.releaseDate})`); if (version.breaking) { changelog.push('**BREAKING CHANGES**'); } changelog.push(version.changelog); changelog.push(''); } return changelog.join('\n'); } async calculateChecksum(templatePath) { const hash = crypto.createHash('sha256'); const files = await this.getAllFiles(templatePath); for (const file of files.sort()) { const content = await fs.readFile(file); hash.update(content); } return hash.digest('hex'); } async calculateSize(templatePath) { const files = await this.getAllFiles(templatePath); let totalSize = 0; for (const file of files) { const stats = await fs.stat(file); totalSize += stats.size; } return totalSize; } async getAllFiles(dir) { const files = []; const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { files.push(...await this.getAllFiles(fullPath)); } else { files.push(fullPath); } } return files; } // Query methods getManifest(templateId) { return this.manifests.get(templateId); } getAllManifests() { return Array.from(this.manifests.values()); } getInstalledTemplates() { return Array.from(this.manifests.values()) .filter(m => m.installedVersion && m.installedPath); } getOutdatedTemplates() { return this.getInstalledTemplates() .filter(m => semver.lt(m.installedVersion, m.latestVersion)); } getBackups(templateId) { const backupDir = path.join(this.cacheDir, 'backups', templateId); try { if (fs.existsSync(backupDir)) { return fs.readdirSync(backupDir).sort(semver.rcompare); } } catch { // Ignore errors } return []; } // Configuration updateConfig(config) { Object.assign(this.config, config); // Restart update checking if interval changed if (config.checkInterval !== undefined || config.autoCheck !== undefined) { if (this.updateCheckInterval) { clearInterval(this.updateCheckInterval); this.updateCheckInterval = undefined; } if (this.config.autoCheck) { this.startUpdateChecking(); } } } stop() { if (this.updateCheckInterval) { clearInterval(this.updateCheckInterval); this.updateCheckInterval = undefined; } } } exports.TemplateVersionManager = TemplateVersionManager; // Global version manager let globalVersionManager = null; function createTemplateVersionManager(templatesDir, config) { return new TemplateVersionManager(templatesDir, config); } function getGlobalTemplateVersionManager() { if (!globalVersionManager) { const templatesDir = path.join(process.cwd(), 'templates'); globalVersionManager = new TemplateVersionManager(templatesDir); } return globalVersionManager; } function setGlobalTemplateVersionManager(manager) { globalVersionManager = manager; }