import * as plugins from './mod.plugins.js'; import * as paths from '../paths.js'; export interface IFileCache { path: string; checksum: string; modified: number; size: number; } export interface ICacheManifest { version: string; lastFormat: number; files: IFileCache[]; } export class ChangeCache { private cacheDir: string; private manifestPath: string; private cacheVersion = '1.0.0'; constructor() { this.cacheDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-cache'); this.manifestPath = plugins.path.join(this.cacheDir, 'manifest.json'); } async initialize(): Promise { await plugins.smartfile.fs.ensureDir(this.cacheDir); } async getManifest(): Promise { const defaultManifest: ICacheManifest = { version: this.cacheVersion, lastFormat: 0, files: [] }; const exists = await plugins.smartfile.fs.fileExists(this.manifestPath); if (!exists) { return defaultManifest; } try { const content = plugins.smartfile.fs.toStringSync(this.manifestPath); const manifest = JSON.parse(content); // Validate the manifest structure if (this.isValidManifest(manifest)) { return manifest; } else { console.warn('Invalid manifest structure, returning default manifest'); return defaultManifest; } } catch (error) { console.warn(`Failed to read cache manifest: ${error.message}, returning default manifest`); // Try to delete the corrupted file try { await plugins.smartfile.fs.remove(this.manifestPath); } catch (removeError) { // Ignore removal errors } return defaultManifest; } } async saveManifest(manifest: ICacheManifest): Promise { // Validate before saving if (!this.isValidManifest(manifest)) { throw new Error('Invalid manifest structure, cannot save'); } // Use atomic write: write to temp file, then move it const tempPath = `${this.manifestPath}.tmp`; try { // Write to temporary file const jsonContent = JSON.stringify(manifest, null, 2); await plugins.smartfile.memory.toFs(jsonContent, tempPath); // Move temp file to actual manifest (atomic-like operation) // Since smartfile doesn't have rename, we copy and delete await plugins.smartfile.fs.copy(tempPath, this.manifestPath); await plugins.smartfile.fs.remove(tempPath); } catch (error) { // Clean up temp file if it exists try { await plugins.smartfile.fs.remove(tempPath); } catch (removeError) { // Ignore removal errors } throw error; } } async hasFileChanged(filePath: string): Promise { const absolutePath = plugins.path.isAbsolute(filePath) ? filePath : plugins.path.join(paths.cwd, filePath); // Check if file exists const exists = await plugins.smartfile.fs.fileExists(absolutePath); if (!exists) { return true; // File doesn't exist, so it's "changed" (will be created) } // Get current file stats const stats = await plugins.smartfile.fs.stat(absolutePath); // Skip directories if (stats.isDirectory()) { return false; // Directories are not processed } const content = plugins.smartfile.fs.toStringSync(absolutePath); const currentChecksum = this.calculateChecksum(content); // Get cached info const manifest = await this.getManifest(); const cachedFile = manifest.files.find(f => f.path === filePath); if (!cachedFile) { return true; // Not in cache, so it's changed } // Compare checksums return cachedFile.checksum !== currentChecksum || cachedFile.size !== stats.size || cachedFile.modified !== stats.mtimeMs; } async updateFileCache(filePath: string): Promise { const absolutePath = plugins.path.isAbsolute(filePath) ? filePath : plugins.path.join(paths.cwd, filePath); // Get current file stats const stats = await plugins.smartfile.fs.stat(absolutePath); // Skip directories if (stats.isDirectory()) { return; // Don't cache directories } const content = plugins.smartfile.fs.toStringSync(absolutePath); const checksum = this.calculateChecksum(content); // Update manifest const manifest = await this.getManifest(); const existingIndex = manifest.files.findIndex(f => f.path === filePath); const cacheEntry: IFileCache = { path: filePath, checksum, modified: stats.mtimeMs, size: stats.size }; if (existingIndex !== -1) { manifest.files[existingIndex] = cacheEntry; } else { manifest.files.push(cacheEntry); } manifest.lastFormat = Date.now(); await this.saveManifest(manifest); } async getChangedFiles(filePaths: string[]): Promise { const changedFiles: string[] = []; for (const filePath of filePaths) { if (await this.hasFileChanged(filePath)) { changedFiles.push(filePath); } } return changedFiles; } async clean(): Promise { const manifest = await this.getManifest(); const validFiles: IFileCache[] = []; // Remove entries for files that no longer exist for (const file of manifest.files) { const absolutePath = plugins.path.isAbsolute(file.path) ? file.path : plugins.path.join(paths.cwd, file.path); if (await plugins.smartfile.fs.fileExists(absolutePath)) { validFiles.push(file); } } manifest.files = validFiles; await this.saveManifest(manifest); } private calculateChecksum(content: string | Buffer): string { return plugins.crypto.createHash('sha256').update(content).digest('hex'); } private isValidManifest(manifest: any): manifest is ICacheManifest { // Check if manifest has the required structure if (!manifest || typeof manifest !== 'object') { return false; } // Check required fields if (typeof manifest.version !== 'string' || typeof manifest.lastFormat !== 'number' || !Array.isArray(manifest.files)) { return false; } // Check each file entry for (const file of manifest.files) { if (!file || typeof file !== 'object' || typeof file.path !== 'string' || typeof file.checksum !== 'string' || typeof file.modified !== 'number' || typeof file.size !== 'number') { return false; } } return true; } }