diff --git a/assets/overrides.json b/assets/overrides.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/assets/overrides.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/changelog.md b/changelog.md index bdab3a2..3c30beb 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-08-08 - 1.16.6 - fix(changecache) +Improve cache manifest validation and atomic file writes; add local settings and overrides + +- Add manifest structure validation and default fallback in getManifest +- Implement atomic write in saveManifest using a temporary file and rename strategy +- Enhance error handling and cleanup for corrupted manifest files +- Introduce new .claude/settings.local.json for project-specific permission configuration +- Add an empty assets/overrides.json file for future overrides + ## 2025-08-08 - 1.16.5 - fix(prettier) Improve file selection in Prettier formatter, remove legacy package overrides, and update CI template indentation diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index a1c7b6a..8cc3486 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/cli', - version: '1.16.5', + version: '1.16.6', description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.' } diff --git a/ts/mod_format/classes.changecache.ts b/ts/mod_format/classes.changecache.ts index f368787..038c0db 100644 --- a/ts/mod_format/classes.changecache.ts +++ b/ts/mod_format/classes.changecache.ts @@ -29,21 +29,67 @@ export class ChangeCache { } async getManifest(): Promise { + const defaultManifest: ICacheManifest = { + version: this.cacheVersion, + lastFormat: 0, + files: [] + }; + const exists = await plugins.smartfile.fs.fileExists(this.manifestPath); if (!exists) { - return { - version: this.cacheVersion, - lastFormat: 0, - files: [] - }; + return defaultManifest; } - const content = plugins.smartfile.fs.toStringSync(this.manifestPath); - return JSON.parse(content); + 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 { - await plugins.smartfile.memory.toFs(JSON.stringify(manifest, null, 2), this.manifestPath); + // 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 { @@ -95,7 +141,7 @@ export class ChangeCache { return; // Don't cache directories } - const content = await plugins.smartfile.fs.toStringSync(absolutePath); + const content = plugins.smartfile.fs.toStringSync(absolutePath); const checksum = this.calculateChecksum(content); // Update manifest @@ -153,4 +199,31 @@ export class ChangeCache { 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; + } } \ No newline at end of file