224 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			224 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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<void> {
 | 
						|
    await plugins.smartfile.fs.ensureDir(this.cacheDir);
 | 
						|
  }
 | 
						|
 | 
						|
  async getManifest(): Promise<ICacheManifest> {
 | 
						|
    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<void> {
 | 
						|
    // 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<boolean> {
 | 
						|
    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<void> {
 | 
						|
    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<string[]> {
 | 
						|
    const changedFiles: string[] = [];
 | 
						|
 | 
						|
    for (const filePath of filePaths) {
 | 
						|
      if (await this.hasFileChanged(filePath)) {
 | 
						|
        changedFiles.push(filePath);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return changedFiles;
 | 
						|
  }
 | 
						|
 | 
						|
  async clean(): Promise<void> {
 | 
						|
    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;
 | 
						|
  }
 | 
						|
}
 |