@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
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.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;
}