@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
490 lines (489 loc) • 19.2 kB
JavaScript
;
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.WorkspaceBackupManager = void 0;
exports.createWorkspaceBackupManager = createWorkspaceBackupManager;
exports.createQuickBackup = createQuickBackup;
exports.compareBackups = compareBackups;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const glob_1 = require("glob");
const error_handler_1 = require("./error-handler");
const workspace_schema_1 = require("./workspace-schema");
const workspace_state_1 = require("./workspace-state");
// Workspace backup manager
class WorkspaceBackupManager {
constructor(rootPath = process.cwd()) {
this.rootPath = rootPath;
this.backupDir = path.join(rootPath, '.re-shell', 'backups');
this.indexPath = path.join(this.backupDir, 'index.json');
this.index = this.createDefaultIndex();
}
// Initialize backup system
async init() {
await fs.ensureDir(this.backupDir);
await this.loadIndex();
}
// Create backup
async createBackup(workspaceFile, options = {}) {
const backupId = this.generateBackupId();
const timestamp = new Date().toISOString();
// Load workspace definition
const workspace = await (0, workspace_schema_1.loadWorkspaceDefinition)(workspaceFile);
// Build backup content
const content = {
metadata: {
id: backupId,
name: options.name || `backup-${timestamp.split('T')[0]}`,
description: options.description,
timestamp,
workspaceFile: path.basename(workspaceFile),
version: '1.0.0',
size: 0,
hash: '',
tags: options.tags,
includeState: options.includeState,
includeCache: options.includeCache,
includeTemplates: options.includeTemplates
},
workspace
};
// Include state if requested
if (options.includeState) {
try {
const stateManager = await (0, workspace_state_1.createWorkspaceStateManager)(this.rootPath);
await stateManager.loadState();
content.state = stateManager.getStateStatistics();
}
catch (error) {
console.warn('Failed to include state in backup:', error.message);
}
}
// Include cache if requested
if (options.includeCache) {
try {
const cacheDir = path.join(this.rootPath, '.re-shell', 'cache');
if (await fs.pathExists(cacheDir)) {
content.cache = await this.backupDirectory(cacheDir);
}
}
catch (error) {
console.warn('Failed to include cache in backup:', error.message);
}
}
// Include templates if requested
if (options.includeTemplates) {
try {
const templatesDir = path.join(this.rootPath, '.re-shell', 'templates');
if (await fs.pathExists(templatesDir)) {
content.templates = await this.backupDirectory(templatesDir);
}
}
catch (error) {
console.warn('Failed to include templates in backup:', error.message);
}
}
// Include files if requested
if (options.includeFiles) {
try {
const patterns = options.filePatterns || ['*.json', '*.yaml', '*.yml', '*.ts', '*.js'];
content.files = await this.backupFiles(patterns);
}
catch (error) {
console.warn('Failed to include files in backup:', error.message);
}
}
// Calculate size and hash
const contentStr = JSON.stringify(content);
content.metadata.size = Buffer.byteLength(contentStr, 'utf8');
content.metadata.hash = crypto.createHash('sha256').update(contentStr).digest('hex');
// Save backup
const backupPath = path.join(this.backupDir, `${backupId}.json`);
await fs.writeJson(backupPath, content, { spaces: 2 });
// Update index
this.index.backups[backupId] = content.metadata;
await this.saveIndex();
return backupId;
}
// List backups
async listBackups() {
return Object.values(this.index.backups).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
}
// Get backup by ID
async getBackup(id) {
if (!this.index.backups[id]) {
return null;
}
const backupPath = path.join(this.backupDir, `${id}.json`);
try {
if (await fs.pathExists(backupPath)) {
return await fs.readJson(backupPath);
}
}
catch (error) {
console.warn(`Failed to load backup ${id}:`, error.message);
}
return null;
}
// Restore backup
async restoreBackup(id, options = {}) {
const backup = await this.getBackup(id);
if (!backup) {
throw new error_handler_1.ValidationError(`Backup '${id}' not found`);
}
const targetPath = options.targetPath || this.rootPath;
await fs.ensureDir(targetPath);
// Restore workspace definition
const workspaceFile = path.join(targetPath, backup.metadata.workspaceFile);
if (!options.force && await fs.pathExists(workspaceFile)) {
throw new error_handler_1.ValidationError(`Workspace file ${backup.metadata.workspaceFile} already exists. Use --force to overwrite.`);
}
await this.saveWorkspaceDefinition(workspaceFile, backup.workspace);
// Restore state if requested and available
if (options.restoreState && backup.state) {
try {
const stateManager = await (0, workspace_state_1.createWorkspaceStateManager)(targetPath);
// State restoration would be implemented here
console.log('State restoration completed');
}
catch (error) {
console.warn('Failed to restore state:', error.message);
}
}
// Restore cache if requested and available
if (options.restoreCache && backup.cache) {
try {
const cacheDir = path.join(targetPath, '.re-shell', 'cache');
await this.restoreDirectory(cacheDir, backup.cache);
console.log('Cache restoration completed');
}
catch (error) {
console.warn('Failed to restore cache:', error.message);
}
}
// Restore templates if requested and available
if (options.restoreTemplates && backup.templates) {
try {
const templatesDir = path.join(targetPath, '.re-shell', 'templates');
await this.restoreDirectory(templatesDir, backup.templates);
console.log('Templates restoration completed');
}
catch (error) {
console.warn('Failed to restore templates:', error.message);
}
}
// Restore files if requested and available
if (options.restoreFiles && backup.files) {
try {
await this.restoreFiles(targetPath, backup.files, options.force);
console.log('Files restoration completed');
}
catch (error) {
console.warn('Failed to restore files:', error.message);
}
}
}
// Delete backup
async deleteBackup(id) {
if (!this.index.backups[id]) {
throw new error_handler_1.ValidationError(`Backup '${id}' not found`);
}
const backupPath = path.join(this.backupDir, `${id}.json`);
await fs.remove(backupPath);
delete this.index.backups[id];
await this.saveIndex();
}
// Export backup to file
async exportBackup(id, outputPath) {
const backup = await this.getBackup(id);
if (!backup) {
throw new error_handler_1.ValidationError(`Backup '${id}' not found`);
}
await fs.ensureDir(path.dirname(outputPath));
await fs.writeJson(outputPath, backup, { spaces: 2 });
}
// Import backup from file
async importBackup(filePath) {
if (!(await fs.pathExists(filePath))) {
throw new error_handler_1.ValidationError(`Backup file not found: ${filePath}`);
}
const backup = await fs.readJson(filePath);
// Validate backup structure
if (!backup.metadata || !backup.workspace) {
throw new error_handler_1.ValidationError('Invalid backup file format');
}
// Generate new ID if needed
const backupId = backup.metadata.id || this.generateBackupId();
backup.metadata.id = backupId;
// Save backup
const backupPath = path.join(this.backupDir, `${backupId}.json`);
await fs.writeJson(backupPath, backup, { spaces: 2 });
// Update index
this.index.backups[backupId] = backup.metadata;
await this.saveIndex();
return backupId;
}
// Cleanup old backups
async cleanupBackups(options = {}) {
const backups = await this.listBackups();
const toDelete = [];
let freedSpace = 0;
// Keep count limit
if (options.keepCount && backups.length > options.keepCount) {
const excess = backups.slice(options.keepCount);
toDelete.push(...excess.map(b => b.id));
}
// Keep days limit
if (options.keepDays) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - options.keepDays);
const oldBackups = backups.filter(b => new Date(b.timestamp) < cutoffDate && !toDelete.includes(b.id));
toDelete.push(...oldBackups.map(b => b.id));
}
// Calculate freed space
for (const id of toDelete) {
const backup = this.index.backups[id];
if (backup) {
freedSpace += backup.size;
}
}
// Delete backups if not dry run
if (!options.dryRun) {
for (const id of toDelete) {
await this.deleteBackup(id);
}
}
return {
deletedCount: toDelete.length,
freedSpace
};
}
// Get backup statistics
getBackupStatistics() {
const backups = Object.values(this.index.backups);
const totalSize = backups.reduce((sum, b) => sum + b.size, 0);
const sortedByDate = backups.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
return {
totalBackups: backups.length,
totalSize,
oldestBackup: sortedByDate[0]?.name,
newestBackup: sortedByDate[sortedByDate.length - 1]?.name,
averageSize: backups.length > 0 ? totalSize / backups.length : 0
};
}
// Private helper methods
generateBackupId() {
return crypto.randomBytes(8).toString('hex');
}
createDefaultIndex() {
return {
version: '1.0.0',
backups: {},
metadata: {
created: new Date().toISOString(),
lastModified: new Date().toISOString(),
totalBackups: 0,
totalSize: 0
}
};
}
async loadIndex() {
try {
if (await fs.pathExists(this.indexPath)) {
this.index = await fs.readJson(this.indexPath);
}
else {
await this.saveIndex();
}
}
catch (error) {
this.index = this.createDefaultIndex();
await this.saveIndex();
}
}
async saveIndex() {
this.index.metadata.lastModified = new Date().toISOString();
this.index.metadata.totalBackups = Object.keys(this.index.backups).length;
this.index.metadata.totalSize = Object.values(this.index.backups)
.reduce((sum, b) => sum + b.size, 0);
await fs.writeJson(this.indexPath, this.index, { spaces: 2 });
}
async saveWorkspaceDefinition(filePath, definition) {
const yaml = await Promise.resolve().then(() => __importStar(require('yaml')));
const content = yaml.stringify(definition);
await fs.writeFile(filePath, content, 'utf8');
}
async backupDirectory(dirPath) {
const result = {};
try {
const files = await fs.readdir(dirPath, { recursive: true });
for (const file of files) {
const fileName = typeof file === 'string' ? file : file.toString();
const fullPath = path.join(dirPath, fileName);
const stat = await fs.stat(fullPath);
if (stat.isFile()) {
try {
if (fileName.endsWith('.json')) {
result[fileName] = await fs.readJson(fullPath);
}
else {
result[fileName] = await fs.readFile(fullPath, 'utf8');
}
}
catch (error) {
console.warn(`Failed to backup file ${fileName}:`, error.message);
}
}
}
}
catch (error) {
console.warn(`Failed to backup directory ${dirPath}:`, error.message);
}
return result;
}
async restoreDirectory(dirPath, content) {
await fs.ensureDir(dirPath);
for (const [fileName, fileContent] of Object.entries(content)) {
const fullPath = path.join(dirPath, fileName);
await fs.ensureDir(path.dirname(fullPath));
try {
if (typeof fileContent === 'object') {
await fs.writeJson(fullPath, fileContent, { spaces: 2 });
}
else {
await fs.writeFile(fullPath, fileContent, 'utf8');
}
}
catch (error) {
console.warn(`Failed to restore file ${fileName}:`, error.message);
}
}
}
async backupFiles(patterns) {
const result = {};
for (const pattern of patterns) {
try {
const files = glob_1.glob.sync(pattern, {
cwd: this.rootPath,
absolute: false
});
for (const file of files) {
const fullPath = path.join(this.rootPath, file);
try {
const content = await fs.readFile(fullPath, 'utf8');
result[file] = content;
}
catch (error) {
console.warn(`Failed to backup file ${file}:`, error.message);
}
}
}
catch (error) {
console.warn(`Failed to process pattern ${pattern}:`, error.message);
}
}
return result;
}
async restoreFiles(targetPath, files, force = false) {
for (const [fileName, content] of Object.entries(files)) {
const fullPath = path.join(targetPath, fileName);
if (!force && await fs.pathExists(fullPath)) {
console.warn(`Skipping existing file: ${fileName} (use --force to overwrite)`);
continue;
}
await fs.ensureDir(path.dirname(fullPath));
try {
await fs.writeFile(fullPath, content, 'utf8');
}
catch (error) {
console.warn(`Failed to restore file ${fileName}:`, error.message);
}
}
}
}
exports.WorkspaceBackupManager = WorkspaceBackupManager;
// Utility functions
async function createWorkspaceBackupManager(rootPath) {
const manager = new WorkspaceBackupManager(rootPath);
await manager.init();
return manager;
}
// Quick backup function
async function createQuickBackup(workspaceFile, name) {
const manager = await createWorkspaceBackupManager();
return await manager.createBackup(workspaceFile, {
name: name || `quick-backup-${new Date().toISOString().split('T')[0]}`,
includeState: true,
includeTemplates: true
});
}
async function compareBackups(manager, id1, id2) {
const backup1 = await manager.getBackup(id1);
const backup2 = await manager.getBackup(id2);
if (!backup1 || !backup2) {
throw new error_handler_1.ValidationError('One or both backups not found');
}
const result = {
added: [],
removed: [],
modified: [],
unchanged: []
};
// Compare workspace definitions
const ws1Keys = new Set(Object.keys(backup1.workspace.workspaces || {}));
const ws2Keys = new Set(Object.keys(backup2.workspace.workspaces || {}));
for (const key of ws1Keys) {
if (!ws2Keys.has(key)) {
result.removed.push(key);
}
else {
const ws1 = JSON.stringify(backup1.workspace.workspaces[key]);
const ws2 = JSON.stringify(backup2.workspace.workspaces[key]);
if (ws1 !== ws2) {
result.modified.push(key);
}
else {
result.unchanged.push(key);
}
}
}
for (const key of ws2Keys) {
if (!ws1Keys.has(key)) {
result.added.push(key);
}
}
return result;
}