UNPKG

@iyulab/oops

Version:

Core SDK for Oops - Safe text file editing with automatic backup

269 lines 9.63 kB
"use strict"; /** * Simple Backup System - Core Purpose Implementation * * Purpose: Safe text file editing with automatic backup and simple undo * Features: One backup per file, atomic operations, simple workflow */ 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.SimpleBackup = void 0; const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const file_system_1 = require("./file-system"); class SimpleBackup { workspacePath; backupDir; stateFile; constructor(workspacePath) { this.workspacePath = workspacePath || path.join(process.cwd(), '.oops'); this.backupDir = path.join(this.workspacePath, 'backups'); this.stateFile = path.join(this.workspacePath, 'simple-state.json'); } /** * Start tracking a file with automatic backup * Equivalent to: oops <file> */ async startTracking(filePath) { // Validate file exists if (!(await file_system_1.FileSystem.exists(filePath))) { throw new Error(`File not found: ${filePath}`); } // Check if already tracked if (await this.isTracked(filePath)) { return; // Already tracking, no-op } // Ensure workspace and backup directories exist await this.ensureDirectories(); // Create backup const backupPath = await this.createBackup(filePath); // Add to tracking state await this.addToState(filePath, backupPath); } /** * Check if a file is currently being tracked */ async isTracked(filePath) { try { const state = await this.loadState(); return state.files.some((f) => f.filePath === path.resolve(filePath)); } catch { return false; } } /** * Check if backup exists for a file */ async hasBackup(filePath) { try { const state = await this.loadState(); const fileInfo = state.files.find((f) => f.filePath === path.resolve(filePath)); return fileInfo ? await file_system_1.FileSystem.exists(fileInfo.backupPath) : false; } catch { return false; } } /** * Get backup content for a file */ async getBackupContent(filePath) { const state = await this.loadState(); const fileInfo = state.files.find((f) => f.filePath === path.resolve(filePath)); if (!fileInfo) { throw new Error(`File is not being tracked: ${filePath}`); } return await file_system_1.FileSystem.readFile(fileInfo.backupPath); } /** * Check if file has changes compared to backup */ async hasChanges(filePath) { if (!(await this.isTracked(filePath))) { return false; } try { const currentContent = await file_system_1.FileSystem.readFile(filePath); const backupContent = await this.getBackupContent(filePath); return currentContent !== backupContent; } catch { return true; // If we can't read, assume changes } } /** * Keep changes and stop tracking * Equivalent to: oops keep <file> */ async keep(filePath) { if (!(await this.isTracked(filePath))) { throw new Error(`File is not being tracked: ${filePath}`); } // Remove from tracking state and cleanup backup await this.removeFromState(filePath); } /** * Undo changes and restore from backup * Equivalent to: oops undo <file> */ async undo(filePath) { if (!(await this.isTracked(filePath))) { throw new Error(`File is not being tracked: ${filePath}`); } // Restore from backup const backupContent = await this.getBackupContent(filePath); await file_system_1.FileSystem.writeFile(filePath, backupContent); // Remove from tracking state and cleanup backup await this.removeFromState(filePath); } /** * Get status of all tracked files * Equivalent to: oops status */ async getStatus() { try { const state = await this.loadState(); const trackedFiles = []; for (const fileInfo of state.files) { try { const hasChanges = await this.hasChanges(fileInfo.filePath); trackedFiles.push({ filePath: fileInfo.filePath, hasChanges, backupPath: fileInfo.backupPath, trackedAt: new Date(fileInfo.trackedAt), }); } catch { // Skip files that can't be read } } return { trackedFiles, totalFiles: trackedFiles.length, }; } catch { return { trackedFiles: [], totalFiles: 0, }; } } /** * Get diff between backup and current file * Equivalent to: oops diff <file> */ async getDiff(filePath) { if (!(await this.isTracked(filePath))) { throw new Error(`File is not being tracked: ${filePath}`); } const currentContent = await file_system_1.FileSystem.readFile(filePath); const backupContent = await this.getBackupContent(filePath); return this.generateSimpleDiff(backupContent, currentContent, filePath); } // Private helper methods async ensureDirectories() { await file_system_1.FileSystem.mkdir(this.workspacePath); await file_system_1.FileSystem.mkdir(this.backupDir); } async createBackup(filePath) { const fileName = path.basename(filePath); const timestamp = Date.now(); const backupPath = path.join(this.backupDir, `${fileName}.${timestamp}.bak`); await file_system_1.FileSystem.copyFile(filePath, backupPath); return backupPath; } async addToState(filePath, backupPath) { const state = await this.loadState(); state.files.push({ filePath: path.resolve(filePath), backupPath, trackedAt: new Date().toISOString(), }); await this.saveState(state); } async removeFromState(filePath) { const state = await this.loadState(); const fileInfo = state.files.find((f) => f.filePath === path.resolve(filePath)); if (fileInfo) { // Remove backup file try { await fs.unlink(fileInfo.backupPath); } catch { // Ignore if backup file doesn't exist } // Remove from state state.files = state.files.filter((f) => f.filePath !== path.resolve(filePath)); await this.saveState(state); } } async loadState() { try { const content = await file_system_1.FileSystem.readFile(this.stateFile); return JSON.parse(content); } catch { return { files: [] }; } } async saveState(state) { await file_system_1.FileSystem.writeFile(this.stateFile, JSON.stringify(state, null, 2)); } generateSimpleDiff(originalContent, currentContent, filePath) { const originalLines = originalContent.split('\n'); const currentLines = currentContent.split('\n'); let diff = `--- ${filePath} (backup)\n`; diff += `+++ ${filePath} (current)\n`; const maxLines = Math.max(originalLines.length, currentLines.length); for (let i = 0; i < maxLines; i++) { const originalLine = originalLines[i]; const currentLine = currentLines[i]; if (originalLine !== currentLine) { if (originalLine !== undefined) { diff += `-${originalLine}\n`; } if (currentLine !== undefined) { diff += `+${currentLine}\n`; } } } return diff; } } exports.SimpleBackup = SimpleBackup; //# sourceMappingURL=simple-backup.js.map