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'); } // Ensure directory exists await plugins.smartfile.fs.ensureDir(this.cacheDir); // Write directly with proper JSON stringification const jsonContent = JSON.stringify(manifest, null, 2); await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath); } 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; } }