@iyulab/oops
Version:
Core SDK for Oops - Safe text file editing with automatic backup
269 lines • 9.63 kB
JavaScript
;
/**
* 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