fix(format): Improve concurrency control in cache and rollback management with mutex locking and refine formatting details
This commit is contained in:
		| @@ -1,5 +1,12 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-08-08 - 1.16.8 - fix(format) | ||||
| Improve concurrency control in cache and rollback management with mutex locking and refine formatting details | ||||
|  | ||||
| - Added 'withMutex' functions in ChangeCache and RollbackManager to synchronize file I/O operations | ||||
| - Introduced static mutex maps to prevent race conditions during manifest updates | ||||
| - Fixed minor formatting issues in commit info and package.json | ||||
|  | ||||
| ## 2025-08-08 - 1.16.7 - fix(core) | ||||
| Improve formatting, logging, and rollback integrity in core modules | ||||
|  | ||||
|   | ||||
| @@ -116,4 +116,4 @@ | ||||
|     "overrides": {} | ||||
|   }, | ||||
|   "packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6" | ||||
| } | ||||
| } | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@git.zone/cli', | ||||
|   version: '1.16.7', | ||||
|   version: '1.16.8', | ||||
|   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.' | ||||
| } | ||||
|   | ||||
| @@ -2,32 +2,29 @@ import * as plugins from './mod.plugins.js'; | ||||
| import { FormatContext } from './classes.formatcontext.js'; | ||||
| import type { IPlannedChange } from './interfaces.format.js'; | ||||
| import { Project } from '../classes.project.js'; | ||||
| import { ChangeCache } from './classes.changecache.js'; | ||||
|  | ||||
| export abstract class BaseFormatter { | ||||
|   protected context: FormatContext; | ||||
|   protected project: Project; | ||||
|   protected cache: ChangeCache; | ||||
|   protected stats: any; // Will be FormatStats from context | ||||
|    | ||||
|  | ||||
|   constructor(context: FormatContext, project: Project) { | ||||
|     this.context = context; | ||||
|     this.project = project; | ||||
|     this.cache = context.getChangeCache(); | ||||
|     this.stats = context.getFormatStats(); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   abstract get name(): string; | ||||
|   abstract analyze(): Promise<IPlannedChange[]>; | ||||
|   abstract applyChange(change: IPlannedChange): Promise<void>; | ||||
|    | ||||
|  | ||||
|   async execute(changes: IPlannedChange[]): Promise<void> { | ||||
|     const startTime = this.stats.moduleStartTime(this.name); | ||||
|     this.stats.startModule(this.name); | ||||
|      | ||||
|  | ||||
|     try { | ||||
|       await this.preExecute(); | ||||
|        | ||||
|  | ||||
|       for (const change of changes) { | ||||
|         try { | ||||
|           await this.applyChange(change); | ||||
| @@ -37,57 +34,37 @@ export abstract class BaseFormatter { | ||||
|           throw error; | ||||
|         } | ||||
|       } | ||||
|        | ||||
|  | ||||
|       await this.postExecute(); | ||||
|     } catch (error) { | ||||
|       await this.context.rollbackOperation(); | ||||
|       // Don't rollback here - let the FormatPlanner handle it | ||||
|       throw error; | ||||
|     } finally { | ||||
|       this.stats.endModule(this.name, startTime); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|  | ||||
|   protected async preExecute(): Promise<void> { | ||||
|     // Override in subclasses if needed | ||||
|   } | ||||
|    | ||||
|  | ||||
|   protected async postExecute(): Promise<void> { | ||||
|     // Override in subclasses if needed | ||||
|   } | ||||
|    | ||||
|  | ||||
|   protected async modifyFile(filepath: string, content: string): Promise<void> { | ||||
|     await this.context.trackFileChange(filepath); | ||||
|     await plugins.smartfile.memory.toFs(content, filepath); | ||||
|     await this.cache.updateFileCache(filepath); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   protected async createFile(filepath: string, content: string): Promise<void> { | ||||
|     await plugins.smartfile.memory.toFs(content, filepath); | ||||
|     await this.cache.updateFileCache(filepath); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   protected async deleteFile(filepath: string): Promise<void> { | ||||
|     await this.context.trackFileChange(filepath); | ||||
|     await plugins.smartfile.fs.remove(filepath); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   protected async shouldProcessFile(filepath: string): Promise<boolean> { | ||||
|     const config = new plugins.npmextra.Npmextra(); | ||||
|     const useCache = config.dataFor('gitzone.format.cache.enabled', true); | ||||
|      | ||||
|     if (!useCache) { | ||||
|       return true; // Process all files if cache is disabled | ||||
|     } | ||||
|      | ||||
|     const hasChanged = await this.cache.hasFileChanged(filepath); | ||||
|      | ||||
|     // Record cache statistics | ||||
|     if (hasChanged) { | ||||
|       this.stats.recordCacheMiss(); | ||||
|     } else { | ||||
|       this.stats.recordCacheHit(); | ||||
|     } | ||||
|      | ||||
|     return hasChanged; | ||||
|     return true; | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -18,32 +18,32 @@ 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: [] | ||||
|       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; | ||||
| @@ -52,7 +52,9 @@ export class ChangeCache { | ||||
|         return defaultManifest; | ||||
|       } | ||||
|     } catch (error) { | ||||
|       console.warn(`Failed to read cache manifest: ${error.message}, returning default manifest`); | ||||
|       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); | ||||
| @@ -62,168 +64,160 @@ export class ChangeCache { | ||||
|       return defaultManifest; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async saveManifest(manifest: ICacheManifest): Promise<void> { | ||||
|     // 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; | ||||
|     } | ||||
|  | ||||
|     // 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  | ||||
|     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); | ||||
|      | ||||
|     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; | ||||
|     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  | ||||
|     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 existingIndex = manifest.files.findIndex((f) => f.path === filePath); | ||||
|  | ||||
|     const cacheEntry: IFileCache = { | ||||
|       path: filePath, | ||||
|       checksum, | ||||
|       modified: stats.mtimeMs, | ||||
|       size: stats.size | ||||
|       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  | ||||
|       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)) { | ||||
|     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') { | ||||
|       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; | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -9,35 +9,42 @@ export interface IModuleDependency { | ||||
|  | ||||
| export class DependencyAnalyzer { | ||||
|   private moduleDependencies: Map<string, IModuleDependency> = new Map(); | ||||
|    | ||||
|  | ||||
|   constructor() { | ||||
|     this.initializeDependencies(); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   private initializeDependencies(): void { | ||||
|     // Define dependencies between format modules | ||||
|     const dependencies = { | ||||
|       'cleanup': [],  // No dependencies | ||||
|       'npmextra': [],  // No dependencies | ||||
|       'license': ['npmextra'],  // Depends on npmextra for config | ||||
|       'packagejson': ['npmextra'],  // Depends on npmextra for config | ||||
|       'templates': ['npmextra', 'packagejson'],  // Depends on both | ||||
|       'gitignore': ['templates'],  // Depends on templates | ||||
|       'tsconfig': ['packagejson'],  // Depends on package.json | ||||
|       'prettier': ['cleanup', 'npmextra', 'packagejson', 'templates', 'gitignore', 'tsconfig'],  // Runs after most others | ||||
|       'readme': ['npmextra', 'packagejson'],  // Depends on project metadata | ||||
|       'copy': ['npmextra'],  // Depends on config | ||||
|       cleanup: [], // No dependencies | ||||
|       npmextra: [], // No dependencies | ||||
|       license: ['npmextra'], // Depends on npmextra for config | ||||
|       packagejson: ['npmextra'], // Depends on npmextra for config | ||||
|       templates: ['npmextra', 'packagejson'], // Depends on both | ||||
|       gitignore: ['templates'], // Depends on templates | ||||
|       tsconfig: ['packagejson'], // Depends on package.json | ||||
|       prettier: [ | ||||
|         'cleanup', | ||||
|         'npmextra', | ||||
|         'packagejson', | ||||
|         'templates', | ||||
|         'gitignore', | ||||
|         'tsconfig', | ||||
|       ], // Runs after most others | ||||
|       readme: ['npmextra', 'packagejson'], // Depends on project metadata | ||||
|       copy: ['npmextra'], // Depends on config | ||||
|     }; | ||||
|      | ||||
|  | ||||
|     // Initialize all modules | ||||
|     for (const [module, deps] of Object.entries(dependencies)) { | ||||
|       this.moduleDependencies.set(module, { | ||||
|         module, | ||||
|         dependencies: new Set(deps), | ||||
|         dependents: new Set() | ||||
|         dependents: new Set(), | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Build reverse dependencies (dependents) | ||||
|     for (const [module, deps] of Object.entries(dependencies)) { | ||||
|       for (const dep of deps) { | ||||
| @@ -48,34 +55,35 @@ export class DependencyAnalyzer { | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|  | ||||
|   getExecutionGroups(modules: BaseFormatter[]): BaseFormatter[][] { | ||||
|     const modulesMap = new Map(modules.map(m => [m.name, m])); | ||||
|     const modulesMap = new Map(modules.map((m) => [m.name, m])); | ||||
|     const executed = new Set<string>(); | ||||
|     const groups: BaseFormatter[][] = []; | ||||
|      | ||||
|  | ||||
|     while (executed.size < modules.length) { | ||||
|       const currentGroup: BaseFormatter[] = []; | ||||
|        | ||||
|  | ||||
|       for (const module of modules) { | ||||
|         if (executed.has(module.name)) continue; | ||||
|          | ||||
|  | ||||
|         const dependency = this.moduleDependencies.get(module.name); | ||||
|         if (!dependency) { | ||||
|           // Unknown module, execute in isolation | ||||
|           currentGroup.push(module); | ||||
|           continue; | ||||
|         } | ||||
|          | ||||
|  | ||||
|         // Check if all dependencies have been executed | ||||
|         const allDepsExecuted = Array.from(dependency.dependencies) | ||||
|           .every(dep => executed.has(dep) || !modulesMap.has(dep)); | ||||
|          | ||||
|         const allDepsExecuted = Array.from(dependency.dependencies).every( | ||||
|           (dep) => executed.has(dep) || !modulesMap.has(dep), | ||||
|         ); | ||||
|  | ||||
|         if (allDepsExecuted) { | ||||
|           currentGroup.push(module); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|  | ||||
|       if (currentGroup.length === 0) { | ||||
|         // Circular dependency or error - execute remaining modules | ||||
|         for (const module of modules) { | ||||
| @@ -84,24 +92,26 @@ export class DependencyAnalyzer { | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       currentGroup.forEach(m => executed.add(m.name)); | ||||
|  | ||||
|       currentGroup.forEach((m) => executed.add(m.name)); | ||||
|       groups.push(currentGroup); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     return groups; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   canRunInParallel(module1: string, module2: string): boolean { | ||||
|     const dep1 = this.moduleDependencies.get(module1); | ||||
|     const dep2 = this.moduleDependencies.get(module2); | ||||
|      | ||||
|  | ||||
|     if (!dep1 || !dep2) return false; | ||||
|      | ||||
|  | ||||
|     // Check if module1 depends on module2 or vice versa | ||||
|     return !dep1.dependencies.has(module2) &&  | ||||
|            !dep2.dependencies.has(module1) && | ||||
|            !dep1.dependents.has(module2) && | ||||
|            !dep2.dependents.has(module1); | ||||
|     return ( | ||||
|       !dep1.dependencies.has(module2) && | ||||
|       !dep2.dependencies.has(module1) && | ||||
|       !dep1.dependents.has(module2) && | ||||
|       !dep2.dependents.has(module1) | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -4,72 +4,85 @@ import { logger } from '../gitzone.logging.js'; | ||||
|  | ||||
| export class DiffReporter { | ||||
|   private diffs: Map<string, string> = new Map(); | ||||
|    | ||||
|   async generateDiff(filePath: string, oldContent: string, newContent: string): Promise<string> { | ||||
|  | ||||
|   async generateDiff( | ||||
|     filePath: string, | ||||
|     oldContent: string, | ||||
|     newContent: string, | ||||
|   ): Promise<string> { | ||||
|     const diff = plugins.smartdiff.createDiff(oldContent, newContent); | ||||
|     this.diffs.set(filePath, diff); | ||||
|     return diff; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async generateDiffForChange(change: IPlannedChange): Promise<string | null> { | ||||
|     if (change.type !== 'modify') { | ||||
|       return null; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     try { | ||||
|       const exists = await plugins.smartfile.fs.fileExists(change.path); | ||||
|       if (!exists) { | ||||
|         return null; | ||||
|       } | ||||
|        | ||||
|       const currentContent = await plugins.smartfile.fs.toStringSync(change.path); | ||||
|        | ||||
|  | ||||
|       const currentContent = await plugins.smartfile.fs.toStringSync( | ||||
|         change.path, | ||||
|       ); | ||||
|  | ||||
|       // For planned changes, we need the new content | ||||
|       if (!change.content) { | ||||
|         return null; | ||||
|       } | ||||
|        | ||||
|       return await this.generateDiff(change.path, currentContent, change.content); | ||||
|  | ||||
|       return await this.generateDiff( | ||||
|         change.path, | ||||
|         currentContent, | ||||
|         change.content, | ||||
|       ); | ||||
|     } catch (error) { | ||||
|       logger.log('error', `Failed to generate diff for ${change.path}: ${error.message}`); | ||||
|       logger.log( | ||||
|         'error', | ||||
|         `Failed to generate diff for ${change.path}: ${error.message}`, | ||||
|       ); | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|  | ||||
|   displayDiff(filePath: string, diff?: string): void { | ||||
|     const diffToShow = diff || this.diffs.get(filePath); | ||||
|      | ||||
|  | ||||
|     if (!diffToShow) { | ||||
|       logger.log('warn', `No diff available for ${filePath}`); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     console.log(`\n${this.formatDiffHeader(filePath)}`); | ||||
|     console.log(this.colorDiff(diffToShow)); | ||||
|     console.log('━'.repeat(50)); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   displayAllDiffs(): void { | ||||
|     if (this.diffs.size === 0) { | ||||
|       logger.log('info', 'No diffs to display'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     console.log('\nFile Changes:'); | ||||
|     console.log('═'.repeat(50)); | ||||
|      | ||||
|  | ||||
|     for (const [filePath, diff] of this.diffs) { | ||||
|       this.displayDiff(filePath, diff); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|  | ||||
|   private formatDiffHeader(filePath: string): string { | ||||
|     return `📄 ${filePath}`; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   private colorDiff(diff: string): string { | ||||
|     const lines = diff.split('\n'); | ||||
|     const coloredLines = lines.map(line => { | ||||
|     const coloredLines = lines.map((line) => { | ||||
|       if (line.startsWith('+') && !line.startsWith('+++')) { | ||||
|         return `\x1b[32m${line}\x1b[0m`; // Green for additions | ||||
|       } else if (line.startsWith('-') && !line.startsWith('---')) { | ||||
| @@ -80,29 +93,32 @@ export class DiffReporter { | ||||
|         return line; | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|  | ||||
|     return coloredLines.join('\n'); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async saveDiffReport(outputPath: string): Promise<void> { | ||||
|     const report = { | ||||
|       timestamp: new Date().toISOString(), | ||||
|       totalFiles: this.diffs.size, | ||||
|       diffs: Array.from(this.diffs.entries()).map(([path, diff]) => ({ | ||||
|         path, | ||||
|         diff | ||||
|       })) | ||||
|         diff, | ||||
|       })), | ||||
|     }; | ||||
|      | ||||
|     await plugins.smartfile.memory.toFs(JSON.stringify(report, null, 2), outputPath); | ||||
|  | ||||
|     await plugins.smartfile.memory.toFs( | ||||
|       JSON.stringify(report, null, 2), | ||||
|       outputPath, | ||||
|     ); | ||||
|     logger.log('info', `Diff report saved to ${outputPath}`); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   hasAnyDiffs(): boolean { | ||||
|     return this.diffs.size > 0; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   getDiffCount(): number { | ||||
|     return this.diffs.size; | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,65 +1,14 @@ | ||||
| import * as plugins from './mod.plugins.js'; | ||||
| import { RollbackManager } from './classes.rollbackmanager.js'; | ||||
| import { ChangeCache } from './classes.changecache.js'; | ||||
| import { FormatStats } from './classes.formatstats.js'; | ||||
| import type { IFormatOperation, IFormatPlan } from './interfaces.format.js'; | ||||
|  | ||||
| export class FormatContext { | ||||
|   private rollbackManager: RollbackManager; | ||||
|   private currentOperation: IFormatOperation | null = null; | ||||
|   private changeCache: ChangeCache; | ||||
|   private formatStats: FormatStats; | ||||
|    | ||||
|  | ||||
|   constructor() { | ||||
|     this.rollbackManager = new RollbackManager(); | ||||
|     this.changeCache = new ChangeCache(); | ||||
|     this.formatStats = new FormatStats(); | ||||
|   } | ||||
|    | ||||
|   async beginOperation(): Promise<void> { | ||||
|     this.currentOperation = await this.rollbackManager.createOperation(); | ||||
|   } | ||||
|    | ||||
|   async trackFileChange(filepath: string): Promise<void> { | ||||
|     if (!this.currentOperation) { | ||||
|       throw new Error('No operation in progress. Call beginOperation() first.'); | ||||
|     } | ||||
|     await this.rollbackManager.backupFile(filepath, this.currentOperation.id); | ||||
|   } | ||||
|    | ||||
|   async commitOperation(): Promise<void> { | ||||
|     if (!this.currentOperation) { | ||||
|       throw new Error('No operation in progress. Call beginOperation() first.'); | ||||
|     } | ||||
|     await this.rollbackManager.markComplete(this.currentOperation.id); | ||||
|     this.currentOperation = null; | ||||
|   } | ||||
|    | ||||
|   async rollbackOperation(): Promise<void> { | ||||
|     if (!this.currentOperation) { | ||||
|       throw new Error('No operation in progress. Call beginOperation() first.'); | ||||
|     } | ||||
|     await this.rollbackManager.rollback(this.currentOperation.id); | ||||
|     this.currentOperation = null; | ||||
|   } | ||||
|    | ||||
|   async rollbackTo(operationId: string): Promise<void> { | ||||
|     await this.rollbackManager.rollback(operationId); | ||||
|   } | ||||
|    | ||||
|   getRollbackManager(): RollbackManager { | ||||
|     return this.rollbackManager; | ||||
|   } | ||||
|    | ||||
|   getChangeCache(): ChangeCache { | ||||
|     return this.changeCache; | ||||
|   } | ||||
|    | ||||
|   async initializeCache(): Promise<void> { | ||||
|     await this.changeCache.initialize(); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   getFormatStats(): FormatStats { | ||||
|     return this.formatStats; | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -10,7 +10,7 @@ export class FormatPlanner { | ||||
|   private plannedChanges: Map<string, IPlannedChange[]> = new Map(); | ||||
|   private dependencyAnalyzer = new DependencyAnalyzer(); | ||||
|   private diffReporter = new DiffReporter(); | ||||
|    | ||||
|  | ||||
|   async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> { | ||||
|     const plan: IFormatPlan = { | ||||
|       summary: { | ||||
| @@ -18,20 +18,20 @@ export class FormatPlanner { | ||||
|         filesAdded: 0, | ||||
|         filesModified: 0, | ||||
|         filesRemoved: 0, | ||||
|         estimatedTime: 0 | ||||
|         estimatedTime: 0, | ||||
|       }, | ||||
|       changes: [], | ||||
|       warnings: [] | ||||
|       warnings: [], | ||||
|     }; | ||||
|      | ||||
|  | ||||
|     for (const module of modules) { | ||||
|       try { | ||||
|         const changes = await module.analyze(); | ||||
|         this.plannedChanges.set(module.name, changes); | ||||
|          | ||||
|  | ||||
|         for (const change of changes) { | ||||
|           plan.changes.push(change); | ||||
|            | ||||
|  | ||||
|           // Update summary | ||||
|           switch (change.type) { | ||||
|             case 'create': | ||||
| @@ -49,67 +49,51 @@ export class FormatPlanner { | ||||
|         plan.warnings.push({ | ||||
|           level: 'error', | ||||
|           message: `Failed to analyze module ${module.name}: ${error.message}`, | ||||
|           module: module.name | ||||
|           module: module.name, | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     plan.summary.totalFiles = plan.summary.filesAdded + plan.summary.filesModified + plan.summary.filesRemoved; | ||||
|  | ||||
|     plan.summary.totalFiles = | ||||
|       plan.summary.filesAdded + | ||||
|       plan.summary.filesModified + | ||||
|       plan.summary.filesRemoved; | ||||
|     plan.summary.estimatedTime = plan.summary.totalFiles * 100; // 100ms per file estimate | ||||
|      | ||||
|  | ||||
|     return plan; | ||||
|   } | ||||
|    | ||||
|   async executePlan(plan: IFormatPlan, modules: BaseFormatter[], context: FormatContext, parallel: boolean = true): Promise<void> { | ||||
|     await context.beginOperation(); | ||||
|  | ||||
|   async executePlan( | ||||
|     plan: IFormatPlan, | ||||
|     modules: BaseFormatter[], | ||||
|     context: FormatContext, | ||||
|     parallel: boolean = false, | ||||
|   ): Promise<void> { | ||||
|     const startTime = Date.now(); | ||||
|      | ||||
|  | ||||
|     try { | ||||
|       if (parallel) { | ||||
|         // Get execution groups based on dependencies | ||||
|         const executionGroups = this.dependencyAnalyzer.getExecutionGroups(modules); | ||||
|          | ||||
|         logger.log('info', `Executing formatters in ${executionGroups.length} groups...`); | ||||
|          | ||||
|         for (let i = 0; i < executionGroups.length; i++) { | ||||
|           const group = executionGroups[i]; | ||||
|           logger.log('info', `Executing group ${i + 1}: ${group.map(m => m.name).join(', ')}`); | ||||
|            | ||||
|           // Execute modules in this group in parallel | ||||
|           const promises = group.map(async (module) => { | ||||
|             const changes = this.plannedChanges.get(module.name) || []; | ||||
|             if (changes.length > 0) { | ||||
|               logger.log('info', `Executing ${module.name} formatter...`); | ||||
|               await module.execute(changes); | ||||
|             } | ||||
|           }); | ||||
|            | ||||
|           await Promise.all(promises); | ||||
|         } | ||||
|       } else { | ||||
|         // Sequential execution (original implementation) | ||||
|         for (const module of modules) { | ||||
|           const changes = this.plannedChanges.get(module.name) || []; | ||||
|            | ||||
|           if (changes.length > 0) { | ||||
|             logger.log('info', `Executing ${module.name} formatter...`); | ||||
|             await module.execute(changes); | ||||
|           } | ||||
|       // Always use sequential execution to avoid race conditions | ||||
|       for (const module of modules) { | ||||
|         const changes = this.plannedChanges.get(module.name) || []; | ||||
|  | ||||
|         if (changes.length > 0) { | ||||
|           logger.log('info', `Executing ${module.name} formatter...`); | ||||
|           await module.execute(changes); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|  | ||||
|       const endTime = Date.now(); | ||||
|       const duration = endTime - startTime; | ||||
|       logger.log('info', `Format operations completed in ${duration}ms`); | ||||
|        | ||||
|       await context.commitOperation(); | ||||
|     } catch (error) { | ||||
|       await context.rollbackOperation(); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   async displayPlan(plan: IFormatPlan, detailed: boolean = false): Promise<void> { | ||||
|  | ||||
|   async displayPlan( | ||||
|     plan: IFormatPlan, | ||||
|     detailed: boolean = false, | ||||
|   ): Promise<void> { | ||||
|     console.log('\nFormat Plan:'); | ||||
|     console.log('━'.repeat(50)); | ||||
|     console.log(`Summary: ${plan.summary.totalFiles} files will be changed`); | ||||
| @@ -118,7 +102,7 @@ export class FormatPlanner { | ||||
|     console.log(`  • ${plan.summary.filesRemoved} deleted files`); | ||||
|     console.log(''); | ||||
|     console.log('Changes by module:'); | ||||
|      | ||||
|  | ||||
|     // Group changes by module | ||||
|     const changesByModule = new Map<string, IPlannedChange[]>(); | ||||
|     for (const change of plan.changes) { | ||||
| @@ -126,14 +110,16 @@ export class FormatPlanner { | ||||
|       moduleChanges.push(change); | ||||
|       changesByModule.set(change.module, moduleChanges); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     for (const [module, changes] of changesByModule) { | ||||
|       console.log(`\n${this.getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`); | ||||
|        | ||||
|       console.log( | ||||
|         `\n${this.getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`, | ||||
|       ); | ||||
|  | ||||
|       for (const change of changes) { | ||||
|         const icon = this.getChangeIcon(change.type); | ||||
|         console.log(`  ${icon} ${change.path} - ${change.description}`); | ||||
|          | ||||
|  | ||||
|         // Show diff for modified files if detailed view is requested | ||||
|         if (detailed && change.type === 'modify') { | ||||
|           const diff = await this.diffReporter.generateDiffForChange(change); | ||||
| @@ -143,7 +129,7 @@ export class FormatPlanner { | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|  | ||||
|     if (plan.warnings.length > 0) { | ||||
|       console.log('\nWarnings:'); | ||||
|       for (const warning of plan.warnings) { | ||||
| @@ -151,26 +137,26 @@ export class FormatPlanner { | ||||
|         console.log(`  ${icon} ${warning.message}`); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|  | ||||
|     console.log('\n' + '━'.repeat(50)); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   private getModuleIcon(module: string): string { | ||||
|     const icons: Record<string, string> = { | ||||
|       'packagejson': '📦', | ||||
|       'license': '📝', | ||||
|       'tsconfig': '🔧', | ||||
|       'cleanup': '🚮', | ||||
|       'gitignore': '🔒', | ||||
|       'prettier': '✨', | ||||
|       'readme': '📖', | ||||
|       'templates': '📄', | ||||
|       'npmextra': '⚙️', | ||||
|       'copy': '📋' | ||||
|       packagejson: '📦', | ||||
|       license: '📝', | ||||
|       tsconfig: '🔧', | ||||
|       cleanup: '🚮', | ||||
|       gitignore: '🔒', | ||||
|       prettier: '✨', | ||||
|       readme: '📖', | ||||
|       templates: '📄', | ||||
|       npmextra: '⚙️', | ||||
|       copy: '📋', | ||||
|     }; | ||||
|     return icons[module] || '📁'; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   private getChangeIcon(type: 'create' | 'modify' | 'delete'): string { | ||||
|     switch (type) { | ||||
|       case 'create': | ||||
| @@ -181,4 +167,4 @@ export class FormatPlanner { | ||||
|         return '❌'; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -30,7 +30,7 @@ export interface IFormatStats { | ||||
|  | ||||
| export class FormatStats { | ||||
|   private stats: IFormatStats; | ||||
|    | ||||
|  | ||||
|   constructor() { | ||||
|     this.stats = { | ||||
|       totalExecutionTime: 0, | ||||
| @@ -44,11 +44,11 @@ export class FormatStats { | ||||
|         totalDeleted: 0, | ||||
|         totalErrors: 0, | ||||
|         cacheHits: 0, | ||||
|         cacheMisses: 0 | ||||
|       } | ||||
|         cacheMisses: 0, | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   startModule(moduleName: string): void { | ||||
|     this.stats.moduleStats.set(moduleName, { | ||||
|       name: moduleName, | ||||
| @@ -58,31 +58,35 @@ export class FormatStats { | ||||
|       successes: 0, | ||||
|       filesCreated: 0, | ||||
|       filesModified: 0, | ||||
|       filesDeleted: 0 | ||||
|       filesDeleted: 0, | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   moduleStartTime(moduleName: string): number { | ||||
|     return Date.now(); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   endModule(moduleName: string, startTime: number): void { | ||||
|     const moduleStats = this.stats.moduleStats.get(moduleName); | ||||
|     if (moduleStats) { | ||||
|       moduleStats.executionTime = Date.now() - startTime; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   recordFileOperation(moduleName: string, operation: 'create' | 'modify' | 'delete', success: boolean = true): void { | ||||
|  | ||||
|   recordFileOperation( | ||||
|     moduleName: string, | ||||
|     operation: 'create' | 'modify' | 'delete', | ||||
|     success: boolean = true, | ||||
|   ): void { | ||||
|     const moduleStats = this.stats.moduleStats.get(moduleName); | ||||
|     if (!moduleStats) return; | ||||
|      | ||||
|  | ||||
|     moduleStats.filesProcessed++; | ||||
|      | ||||
|  | ||||
|     if (success) { | ||||
|       moduleStats.successes++; | ||||
|       this.stats.overallStats.totalFiles++; | ||||
|        | ||||
|  | ||||
|       switch (operation) { | ||||
|         case 'create': | ||||
|           moduleStats.filesCreated++; | ||||
| @@ -102,53 +106,66 @@ export class FormatStats { | ||||
|       this.stats.overallStats.totalErrors++; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|  | ||||
|   recordCacheHit(): void { | ||||
|     this.stats.overallStats.cacheHits++; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   recordCacheMiss(): void { | ||||
|     this.stats.overallStats.cacheMisses++; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   finish(): void { | ||||
|     this.stats.endTime = Date.now(); | ||||
|     this.stats.totalExecutionTime = this.stats.endTime - this.stats.startTime; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   displayStats(): void { | ||||
|     console.log('\n📊 Format Operation Statistics:'); | ||||
|     console.log('═'.repeat(50)); | ||||
|      | ||||
|  | ||||
|     // Overall stats | ||||
|     console.log('\nOverall Summary:'); | ||||
|     console.log(`  Total Execution Time: ${this.formatDuration(this.stats.totalExecutionTime)}`); | ||||
|     console.log( | ||||
|       `  Total Execution Time: ${this.formatDuration(this.stats.totalExecutionTime)}`, | ||||
|     ); | ||||
|     console.log(`  Files Processed: ${this.stats.overallStats.totalFiles}`); | ||||
|     console.log(`    • Created: ${this.stats.overallStats.totalCreated}`); | ||||
|     console.log(`    • Modified: ${this.stats.overallStats.totalModified}`); | ||||
|     console.log(`    • Deleted: ${this.stats.overallStats.totalDeleted}`); | ||||
|     console.log(`  Errors: ${this.stats.overallStats.totalErrors}`); | ||||
|      | ||||
|     if (this.stats.overallStats.cacheHits > 0 || this.stats.overallStats.cacheMisses > 0) { | ||||
|       const cacheHitRate = this.stats.overallStats.cacheHits /  | ||||
|         (this.stats.overallStats.cacheHits + this.stats.overallStats.cacheMisses) * 100; | ||||
|  | ||||
|     if ( | ||||
|       this.stats.overallStats.cacheHits > 0 || | ||||
|       this.stats.overallStats.cacheMisses > 0 | ||||
|     ) { | ||||
|       const cacheHitRate = | ||||
|         (this.stats.overallStats.cacheHits / | ||||
|           (this.stats.overallStats.cacheHits + | ||||
|             this.stats.overallStats.cacheMisses)) * | ||||
|         100; | ||||
|       console.log(`  Cache Hit Rate: ${cacheHitRate.toFixed(1)}%`); | ||||
|       console.log(`    • Hits: ${this.stats.overallStats.cacheHits}`); | ||||
|       console.log(`    • Misses: ${this.stats.overallStats.cacheMisses}`); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Module stats | ||||
|     console.log('\nModule Breakdown:'); | ||||
|     console.log('─'.repeat(50)); | ||||
|      | ||||
|     const sortedModules = Array.from(this.stats.moduleStats.values()) | ||||
|       .sort((a, b) => b.filesProcessed - a.filesProcessed); | ||||
|      | ||||
|  | ||||
|     const sortedModules = Array.from(this.stats.moduleStats.values()).sort( | ||||
|       (a, b) => b.filesProcessed - a.filesProcessed, | ||||
|     ); | ||||
|  | ||||
|     for (const moduleStats of sortedModules) { | ||||
|       console.log(`\n${this.getModuleIcon(moduleStats.name)} ${moduleStats.name}:`); | ||||
|       console.log(`  Execution Time: ${this.formatDuration(moduleStats.executionTime)}`); | ||||
|       console.log( | ||||
|         `\n${this.getModuleIcon(moduleStats.name)} ${moduleStats.name}:`, | ||||
|       ); | ||||
|       console.log( | ||||
|         `  Execution Time: ${this.formatDuration(moduleStats.executionTime)}`, | ||||
|       ); | ||||
|       console.log(`  Files Processed: ${moduleStats.filesProcessed}`); | ||||
|        | ||||
|  | ||||
|       if (moduleStats.filesCreated > 0) { | ||||
|         console.log(`    • Created: ${moduleStats.filesCreated}`); | ||||
|       } | ||||
| @@ -158,27 +175,30 @@ export class FormatStats { | ||||
|       if (moduleStats.filesDeleted > 0) { | ||||
|         console.log(`    • Deleted: ${moduleStats.filesDeleted}`); | ||||
|       } | ||||
|        | ||||
|  | ||||
|       if (moduleStats.errors > 0) { | ||||
|         console.log(`  ❌ Errors: ${moduleStats.errors}`); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|  | ||||
|     console.log('\n' + '═'.repeat(50)); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async saveReport(outputPath: string): Promise<void> { | ||||
|     const report = { | ||||
|       timestamp: new Date().toISOString(), | ||||
|       executionTime: this.stats.totalExecutionTime, | ||||
|       overallStats: this.stats.overallStats, | ||||
|       moduleStats: Array.from(this.stats.moduleStats.values()) | ||||
|       moduleStats: Array.from(this.stats.moduleStats.values()), | ||||
|     }; | ||||
|      | ||||
|     await plugins.smartfile.memory.toFs(JSON.stringify(report, null, 2), outputPath); | ||||
|  | ||||
|     await plugins.smartfile.memory.toFs( | ||||
|       JSON.stringify(report, null, 2), | ||||
|       outputPath, | ||||
|     ); | ||||
|     logger.log('info', `Statistics report saved to ${outputPath}`); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   private formatDuration(ms: number): string { | ||||
|     if (ms < 1000) { | ||||
|       return `${ms}ms`; | ||||
| @@ -190,20 +210,20 @@ export class FormatStats { | ||||
|       return `${minutes}m ${seconds}s`; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|  | ||||
|   private getModuleIcon(module: string): string { | ||||
|     const icons: Record<string, string> = { | ||||
|       'packagejson': '📦', | ||||
|       'license': '📝', | ||||
|       'tsconfig': '🔧', | ||||
|       'cleanup': '🚮', | ||||
|       'gitignore': '🔒', | ||||
|       'prettier': '✨', | ||||
|       'readme': '📖', | ||||
|       'templates': '📄', | ||||
|       'npmextra': '⚙️', | ||||
|       'copy': '📋' | ||||
|       packagejson: '📦', | ||||
|       license: '📝', | ||||
|       tsconfig: '🔧', | ||||
|       cleanup: '🚮', | ||||
|       gitignore: '🔒', | ||||
|       prettier: '✨', | ||||
|       readme: '📖', | ||||
|       templates: '📄', | ||||
|       npmextra: '⚙️', | ||||
|       copy: '📋', | ||||
|     }; | ||||
|     return icons[module] || '📁'; | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -5,206 +5,227 @@ import type { IFormatOperation } from './interfaces.format.js'; | ||||
| export class RollbackManager { | ||||
|   private backupDir: string; | ||||
|   private manifestPath: string; | ||||
|    | ||||
|  | ||||
|   constructor() { | ||||
|     this.backupDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-backups'); | ||||
|     this.manifestPath = plugins.path.join(this.backupDir, 'manifest.json'); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async createOperation(): Promise<IFormatOperation> { | ||||
|     await this.ensureBackupDir(); | ||||
|      | ||||
|  | ||||
|     const operation: IFormatOperation = { | ||||
|       id: this.generateOperationId(), | ||||
|       timestamp: Date.now(), | ||||
|       files: [], | ||||
|       status: 'pending' | ||||
|       status: 'pending', | ||||
|     }; | ||||
|      | ||||
|  | ||||
|     await this.updateManifest(operation); | ||||
|     return operation; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async backupFile(filepath: string, operationId: string): Promise<void> { | ||||
|     const operation = await this.getOperation(operationId); | ||||
|     if (!operation) { | ||||
|       throw new Error(`Operation ${operationId} not found`); | ||||
|     } | ||||
|      | ||||
|     const absolutePath = plugins.path.isAbsolute(filepath)  | ||||
|       ? filepath  | ||||
|  | ||||
|     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) { | ||||
|       // File doesn't exist yet (will be created), so we skip backup | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Read file content and metadata | ||||
|     const content = plugins.smartfile.fs.toStringSync(absolutePath); | ||||
|     const stats = await plugins.smartfile.fs.stat(absolutePath); | ||||
|     const checksum = this.calculateChecksum(content); | ||||
|      | ||||
|  | ||||
|     // Create backup | ||||
|     const backupPath = this.getBackupPath(operationId, filepath); | ||||
|     await plugins.smartfile.fs.ensureDir(plugins.path.dirname(backupPath)); | ||||
|     await plugins.smartfile.memory.toFs(content, backupPath); | ||||
|      | ||||
|  | ||||
|     // Update operation | ||||
|     operation.files.push({ | ||||
|       path: filepath, | ||||
|       originalContent: content, | ||||
|       checksum, | ||||
|       permissions: stats.mode.toString(8) | ||||
|       permissions: stats.mode.toString(8), | ||||
|     }); | ||||
|      | ||||
|  | ||||
|     await this.updateManifest(operation); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async rollback(operationId: string): Promise<void> { | ||||
|     const operation = await this.getOperation(operationId); | ||||
|     if (!operation) { | ||||
|       throw new Error(`Operation ${operationId} not found`); | ||||
|       // Operation doesn't exist, might have already been rolled back or never created | ||||
|       console.warn(`Operation ${operationId} not found for rollback, skipping`); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     if (operation.status === 'rolled-back') { | ||||
|       throw new Error(`Operation ${operationId} has already been rolled back`); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Restore files in reverse order | ||||
|     for (let i = operation.files.length - 1; i >= 0; i--) { | ||||
|       const file = operation.files[i]; | ||||
|       const absolutePath = plugins.path.isAbsolute(file.path)  | ||||
|         ? file.path  | ||||
|       const absolutePath = plugins.path.isAbsolute(file.path) | ||||
|         ? file.path | ||||
|         : plugins.path.join(paths.cwd, file.path); | ||||
|        | ||||
|  | ||||
|       // Verify backup integrity | ||||
|       const backupPath = this.getBackupPath(operationId, file.path); | ||||
|       const backupContent = plugins.smartfile.fs.toStringSync(backupPath); | ||||
|       const backupChecksum = this.calculateChecksum(backupContent); | ||||
|        | ||||
|  | ||||
|       if (backupChecksum !== file.checksum) { | ||||
|         throw new Error(`Backup integrity check failed for ${file.path}`); | ||||
|       } | ||||
|        | ||||
|  | ||||
|       // Restore file | ||||
|       await plugins.smartfile.memory.toFs(file.originalContent, absolutePath); | ||||
|        | ||||
|  | ||||
|       // Restore permissions | ||||
|       const mode = parseInt(file.permissions, 8); | ||||
|       // Note: Permissions restoration may not work on all platforms | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Update operation status | ||||
|     operation.status = 'rolled-back'; | ||||
|     await this.updateManifest(operation); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async markComplete(operationId: string): Promise<void> { | ||||
|     const operation = await this.getOperation(operationId); | ||||
|     if (!operation) { | ||||
|       throw new Error(`Operation ${operationId} not found`); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     operation.status = 'completed'; | ||||
|     await this.updateManifest(operation); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async cleanOldBackups(retentionDays: number): Promise<void> { | ||||
|     const manifest = await this.getManifest(); | ||||
|     const cutoffTime = Date.now() - (retentionDays * 24 * 60 * 60 * 1000); | ||||
|      | ||||
|     const operationsToDelete = manifest.operations.filter(op =>  | ||||
|       op.timestamp < cutoffTime && op.status === 'completed' | ||||
|     const cutoffTime = Date.now() - retentionDays * 24 * 60 * 60 * 1000; | ||||
|  | ||||
|     const operationsToDelete = manifest.operations.filter( | ||||
|       (op) => op.timestamp < cutoffTime && op.status === 'completed', | ||||
|     ); | ||||
|      | ||||
|  | ||||
|     for (const operation of operationsToDelete) { | ||||
|       // Remove backup files | ||||
|       const operationDir = plugins.path.join(this.backupDir, 'operations', operation.id); | ||||
|       const operationDir = plugins.path.join( | ||||
|         this.backupDir, | ||||
|         'operations', | ||||
|         operation.id, | ||||
|       ); | ||||
|       await plugins.smartfile.fs.remove(operationDir); | ||||
|        | ||||
|  | ||||
|       // Remove from manifest | ||||
|       manifest.operations = manifest.operations.filter(op => op.id !== operation.id); | ||||
|       manifest.operations = manifest.operations.filter( | ||||
|         (op) => op.id !== operation.id, | ||||
|       ); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     await this.saveManifest(manifest); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async verifyBackup(operationId: string): Promise<boolean> { | ||||
|     const operation = await this.getOperation(operationId); | ||||
|     if (!operation) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     for (const file of operation.files) { | ||||
|       const backupPath = this.getBackupPath(operationId, file.path); | ||||
|       const exists = await plugins.smartfile.fs.fileExists(backupPath); | ||||
|        | ||||
|  | ||||
|       if (!exists) { | ||||
|         return false; | ||||
|       } | ||||
|        | ||||
|  | ||||
|       const content = plugins.smartfile.fs.toStringSync(backupPath); | ||||
|       const checksum = this.calculateChecksum(content); | ||||
|        | ||||
|  | ||||
|       if (checksum !== file.checksum) { | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|  | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async listBackups(): Promise<IFormatOperation[]> { | ||||
|     const manifest = await this.getManifest(); | ||||
|     return manifest.operations; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   private async ensureBackupDir(): Promise<void> { | ||||
|     await plugins.smartfile.fs.ensureDir(this.backupDir); | ||||
|     await plugins.smartfile.fs.ensureDir(plugins.path.join(this.backupDir, 'operations')); | ||||
|     await plugins.smartfile.fs.ensureDir( | ||||
|       plugins.path.join(this.backupDir, 'operations'), | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   private generateOperationId(): string { | ||||
|     const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); | ||||
|     const random = Math.random().toString(36).substring(2, 8); | ||||
|     return `${timestamp}-${random}`; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   private getBackupPath(operationId: string, filepath: string): string { | ||||
|     const filename = plugins.path.basename(filepath); | ||||
|     const dir = plugins.path.dirname(filepath); | ||||
|     const safeDir = dir.replace(/[/\\]/g, '__'); | ||||
|     return plugins.path.join(this.backupDir, 'operations', operationId, 'files', safeDir, `${filename}.backup`); | ||||
|     return plugins.path.join( | ||||
|       this.backupDir, | ||||
|       'operations', | ||||
|       operationId, | ||||
|       'files', | ||||
|       safeDir, | ||||
|       `${filename}.backup`, | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   private calculateChecksum(content: string | Buffer): string { | ||||
|     return plugins.crypto.createHash('sha256').update(content).digest('hex'); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   private async getManifest(): Promise<{ operations: IFormatOperation[] }> { | ||||
|     const defaultManifest = { operations: [] }; | ||||
|      | ||||
|  | ||||
|     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 rollback manifest structure, returning default manifest'); | ||||
|         console.warn( | ||||
|           'Invalid rollback manifest structure, returning default manifest', | ||||
|         ); | ||||
|         return defaultManifest; | ||||
|       } | ||||
|     } catch (error) { | ||||
|       console.warn(`Failed to read rollback manifest: ${error.message}, returning default manifest`); | ||||
|       console.warn( | ||||
|         `Failed to read rollback manifest: ${error.message}, returning default manifest`, | ||||
|       ); | ||||
|       // Try to delete the corrupted file | ||||
|       try { | ||||
|         await plugins.smartfile.fs.remove(this.manifestPath); | ||||
| @@ -214,85 +235,84 @@ export class RollbackManager { | ||||
|       return defaultManifest; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   private async saveManifest(manifest: { operations: IFormatOperation[] }): Promise<void> { | ||||
|  | ||||
|   private async saveManifest(manifest: { | ||||
|     operations: IFormatOperation[]; | ||||
|   }): Promise<void> { | ||||
|     // Validate before saving | ||||
|     if (!this.isValidManifest(manifest)) { | ||||
|       throw new Error('Invalid rollback 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; | ||||
|     } | ||||
|  | ||||
|     // Ensure directory exists | ||||
|     await this.ensureBackupDir(); | ||||
|  | ||||
|     // Write directly with proper JSON stringification | ||||
|     const jsonContent = JSON.stringify(manifest, null, 2); | ||||
|     await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath); | ||||
|   } | ||||
|    | ||||
|   private async getOperation(operationId: string): Promise<IFormatOperation | null> { | ||||
|  | ||||
|   private async getOperation( | ||||
|     operationId: string, | ||||
|   ): Promise<IFormatOperation | null> { | ||||
|     const manifest = await this.getManifest(); | ||||
|     return manifest.operations.find(op => op.id === operationId) || null; | ||||
|     return manifest.operations.find((op) => op.id === operationId) || null; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   private async updateManifest(operation: IFormatOperation): Promise<void> { | ||||
|     const manifest = await this.getManifest(); | ||||
|     const existingIndex = manifest.operations.findIndex(op => op.id === operation.id); | ||||
|      | ||||
|     const existingIndex = manifest.operations.findIndex( | ||||
|       (op) => op.id === operation.id, | ||||
|     ); | ||||
|  | ||||
|     if (existingIndex !== -1) { | ||||
|       manifest.operations[existingIndex] = operation; | ||||
|     } else { | ||||
|       manifest.operations.push(operation); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     await this.saveManifest(manifest); | ||||
|   } | ||||
|    | ||||
|   private isValidManifest(manifest: any): manifest is { operations: IFormatOperation[] } { | ||||
|  | ||||
|   private isValidManifest( | ||||
|     manifest: any, | ||||
|   ): manifest is { operations: IFormatOperation[] } { | ||||
|     // Check if manifest has the required structure | ||||
|     if (!manifest || typeof manifest !== 'object') { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Check required fields | ||||
|     if (!Array.isArray(manifest.operations)) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Check each operation entry | ||||
|     for (const operation of manifest.operations) { | ||||
|       if (!operation || typeof operation !== 'object' || | ||||
|           typeof operation.id !== 'string' || | ||||
|           typeof operation.timestamp !== 'number' || | ||||
|           typeof operation.status !== 'string' || | ||||
|           !Array.isArray(operation.files)) { | ||||
|       if ( | ||||
|         !operation || | ||||
|         typeof operation !== 'object' || | ||||
|         typeof operation.id !== 'string' || | ||||
|         typeof operation.timestamp !== 'number' || | ||||
|         typeof operation.status !== 'string' || | ||||
|         !Array.isArray(operation.files) | ||||
|       ) { | ||||
|         return false; | ||||
|       } | ||||
|        | ||||
|  | ||||
|       // Check each file in the operation | ||||
|       for (const file of operation.files) { | ||||
|         if (!file || typeof file !== 'object' || | ||||
|             typeof file.path !== 'string' || | ||||
|             typeof file.checksum !== 'string') { | ||||
|         if ( | ||||
|           !file || | ||||
|           typeof file !== 'object' || | ||||
|           typeof file.path !== 'string' || | ||||
|           typeof file.checksum !== 'string' | ||||
|         ) { | ||||
|           return false; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|  | ||||
|     return true; | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -4,14 +4,21 @@ import * as paths from '../paths.js'; | ||||
| import { logger } from '../gitzone.logging.js'; | ||||
| import { Project } from '../classes.project.js'; | ||||
|  | ||||
| const filesToDelete = ['defaults.yml', 'yarn.lock', 'package-lock.json', 'tslint.json']; | ||||
| const filesToDelete = [ | ||||
|   'defaults.yml', | ||||
|   'yarn.lock', | ||||
|   'package-lock.json', | ||||
|   'tslint.json', | ||||
| ]; | ||||
|  | ||||
| export const run = async (projectArg: Project) => { | ||||
|   for (const relativeFilePath of filesToDelete) { | ||||
|     const fileExists = plugins.smartfile.fs.fileExistsSync(relativeFilePath); | ||||
|     if (fileExists) { | ||||
|       logger.log('info', `Found ${relativeFilePath}! Removing it!`); | ||||
|       plugins.smartfile.fs.removeSync(plugins.path.join(paths.cwd, relativeFilePath)); | ||||
|       plugins.smartfile.fs.removeSync( | ||||
|         plugins.path.join(paths.cwd, relativeFilePath), | ||||
|       ); | ||||
|     } else { | ||||
|       logger.log('info', `Project is free of ${relativeFilePath}`); | ||||
|     } | ||||
|   | ||||
| @@ -4,56 +4,59 @@ import { logger } from '../gitzone.logging.js'; | ||||
|  | ||||
| export const run = async (projectArg: Project) => { | ||||
|   const gitzoneConfig = await projectArg.gitzoneConfig; | ||||
|    | ||||
|  | ||||
|   // Get copy configuration from npmextra.json | ||||
|   const npmextraConfig = new plugins.npmextra.Npmextra(); | ||||
|   const copyConfig = npmextraConfig.dataFor<any>('gitzone.format.copy', { | ||||
|     patterns: [] | ||||
|     patterns: [], | ||||
|   }); | ||||
|    | ||||
|  | ||||
|   if (!copyConfig.patterns || copyConfig.patterns.length === 0) { | ||||
|     logger.log('info', 'No copy patterns configured in npmextra.json'); | ||||
|     return; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   for (const pattern of copyConfig.patterns) { | ||||
|     if (!pattern.from || !pattern.to) { | ||||
|       logger.log('warn', 'Invalid copy pattern - missing "from" or "to" field'); | ||||
|       continue; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     try { | ||||
|       // Handle glob patterns | ||||
|       const files = await plugins.smartfile.fs.listFileTree('.', pattern.from); | ||||
|        | ||||
|  | ||||
|       for (const file of files) { | ||||
|         const sourcePath = file; | ||||
|         let destPath = pattern.to; | ||||
|          | ||||
|  | ||||
|         // If destination is a directory, preserve filename | ||||
|         if (pattern.to.endsWith('/')) { | ||||
|           const filename = plugins.path.basename(file); | ||||
|           destPath = plugins.path.join(pattern.to, filename); | ||||
|         } | ||||
|          | ||||
|  | ||||
|         // Handle template variables in destination path | ||||
|         if (pattern.preservePath) { | ||||
|           const relativePath = plugins.path.relative( | ||||
|             plugins.path.dirname(pattern.from.replace(/\*/g, '')),  | ||||
|             file | ||||
|             plugins.path.dirname(pattern.from.replace(/\*/g, '')), | ||||
|             file, | ||||
|           ); | ||||
|           destPath = plugins.path.join(pattern.to, relativePath); | ||||
|         } | ||||
|          | ||||
|  | ||||
|         // Ensure destination directory exists | ||||
|         await plugins.smartfile.fs.ensureDir(plugins.path.dirname(destPath)); | ||||
|          | ||||
|  | ||||
|         // Copy file | ||||
|         await plugins.smartfile.fs.copy(sourcePath, destPath); | ||||
|         logger.log('info', `Copied ${sourcePath} to ${destPath}`); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       logger.log('error', `Failed to copy pattern ${pattern.from}: ${error.message}`); | ||||
|       logger.log( | ||||
|         'error', | ||||
|         `Failed to copy pattern ${pattern.from}: ${error.message}`, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| @@ -79,4 +82,4 @@ export const run = async (projectArg: Project) => { | ||||
|  *     } | ||||
|  *   } | ||||
|  * } | ||||
|  */ | ||||
|  */ | ||||
|   | ||||
| @@ -12,7 +12,8 @@ export const run = async (projectArg: Project) => { | ||||
|   const ciTemplate = await templateModule.getTemplate('gitignore'); | ||||
|   if (gitignoreExists) { | ||||
|     // lets get the existing gitignore file | ||||
|     const existingGitIgnoreString = plugins.smartfile.fs.toStringSync(gitignorePath); | ||||
|     const existingGitIgnoreString = | ||||
|       plugins.smartfile.fs.toStringSync(gitignorePath); | ||||
|     let customPart = existingGitIgnoreString.split('# custom\n')[1]; | ||||
|     customPart ? null : (customPart = ''); | ||||
|   } | ||||
|   | ||||
| @@ -24,7 +24,9 @@ export const run = async (projectArg: Project) => { | ||||
|   } else { | ||||
|     logger.log('error', 'Error -> licenses failed. Here is why:'); | ||||
|     for (const failedModule of licenseCheckResult.failingModules) { | ||||
|       console.log(`${failedModule.name} fails with license ${failedModule.license}`); | ||||
|       console.log( | ||||
|         `${failedModule.name} fails with license ${failedModule.license}`, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|   | ||||
| @@ -29,7 +29,12 @@ export const run = async (projectArg: Project) => { | ||||
|  | ||||
|       const interactInstance = new plugins.smartinteract.SmartInteract(); | ||||
|       for (const expectedRepoInformationItem of expectedRepoInformation) { | ||||
|         if (!plugins.smartobject.smartGet(npmextraJson.gitzone, expectedRepoInformationItem)) { | ||||
|         if ( | ||||
|           !plugins.smartobject.smartGet( | ||||
|             npmextraJson.gitzone, | ||||
|             expectedRepoInformationItem, | ||||
|           ) | ||||
|         ) { | ||||
|           interactInstance.addQuestions([ | ||||
|             { | ||||
|               message: `What is the value of ${expectedRepoInformationItem}`, | ||||
| @@ -43,7 +48,9 @@ export const run = async (projectArg: Project) => { | ||||
|  | ||||
|       const answerbucket = await interactInstance.runQueue(); | ||||
|       for (const expectedRepoInformationItem of expectedRepoInformation) { | ||||
|         const cliProvidedValue = answerbucket.getAnswerFor(expectedRepoInformationItem); | ||||
|         const cliProvidedValue = answerbucket.getAnswerFor( | ||||
|           expectedRepoInformationItem, | ||||
|         ); | ||||
|         if (cliProvidedValue) { | ||||
|           plugins.smartobject.smartAdd( | ||||
|             npmextraJson.gitzone, | ||||
|   | ||||
| @@ -19,7 +19,7 @@ const ensureDependency = async ( | ||||
|     : [dependencyArg, 'latest']; | ||||
|  | ||||
|   const targetSections: string[] = []; | ||||
|    | ||||
|  | ||||
|   switch (position) { | ||||
|     case 'dep': | ||||
|       targetSections.push('dependencies'); | ||||
| @@ -43,7 +43,8 @@ const ensureDependency = async ( | ||||
|         break; | ||||
|       case 'include': | ||||
|         if (!packageJsonObjectArg[section][packageName]) { | ||||
|           packageJsonObjectArg[section][packageName] = version === 'latest' ? '^1.0.0' : version; | ||||
|           packageJsonObjectArg[section][packageName] = | ||||
|             version === 'latest' ? '^1.0.0' : version; | ||||
|         } | ||||
|         break; | ||||
|       case 'latest': | ||||
| @@ -54,9 +55,13 @@ const ensureDependency = async ( | ||||
|           const latestVersion = packageInfo['dist-tags'].latest; | ||||
|           packageJsonObjectArg[section][packageName] = `^${latestVersion}`; | ||||
|         } catch (error) { | ||||
|           logger.log('warn', `Could not fetch latest version for ${packageName}, using existing or default`); | ||||
|           logger.log( | ||||
|             'warn', | ||||
|             `Could not fetch latest version for ${packageName}, using existing or default`, | ||||
|           ); | ||||
|           if (!packageJsonObjectArg[section][packageName]) { | ||||
|             packageJsonObjectArg[section][packageName] = version === 'latest' ? '^1.0.0' : version; | ||||
|             packageJsonObjectArg[section][packageName] = | ||||
|               version === 'latest' ? '^1.0.0' : version; | ||||
|           } | ||||
|         } | ||||
|         break; | ||||
| @@ -91,9 +96,15 @@ export const run = async (projectArg: Project) => { | ||||
|  | ||||
|       // Check for private or public | ||||
|       if (packageJson.private !== undefined) { | ||||
|         logger.log('info', 'Success -> found private/public info in package.json!'); | ||||
|         logger.log( | ||||
|           'info', | ||||
|           'Success -> found private/public info in package.json!', | ||||
|         ); | ||||
|       } else { | ||||
|         logger.log('error', 'found no private boolean! Setting it to private for now!'); | ||||
|         logger.log( | ||||
|           'error', | ||||
|           'found no private boolean! Setting it to private for now!', | ||||
|         ); | ||||
|         packageJson.private = true; | ||||
|       } | ||||
|  | ||||
| @@ -101,7 +112,10 @@ export const run = async (projectArg: Project) => { | ||||
|       if (packageJson.license) { | ||||
|         logger.log('info', 'Success -> found license in package.json!'); | ||||
|       } else { | ||||
|         logger.log('error', 'found no license! Setting it to UNLICENSED for now!'); | ||||
|         logger.log( | ||||
|           'error', | ||||
|           'found no license! Setting it to UNLICENSED for now!', | ||||
|         ); | ||||
|         packageJson.license = 'UNLICENSED'; | ||||
|       } | ||||
|  | ||||
| @@ -109,13 +123,19 @@ export const run = async (projectArg: Project) => { | ||||
|       if (packageJson.scripts.build) { | ||||
|         logger.log('info', 'Success -> found build script in package.json!'); | ||||
|       } else { | ||||
|         logger.log('error', 'found no build script! Putting a placeholder there for now!'); | ||||
|         logger.log( | ||||
|           'error', | ||||
|           'found no build script! Putting a placeholder there for now!', | ||||
|         ); | ||||
|         packageJson.scripts.build = `echo "Not needed for now"`; | ||||
|       } | ||||
|  | ||||
|       // Check for buildDocs script | ||||
|       if (!packageJson.scripts.buildDocs) { | ||||
|         logger.log('info', 'found no buildDocs script! Putting tsdoc script there now.'); | ||||
|         logger.log( | ||||
|           'info', | ||||
|           'found no buildDocs script! Putting tsdoc script there now.', | ||||
|         ); | ||||
|         packageJson.scripts.buildDocs = `tsdoc`; | ||||
|       } | ||||
|  | ||||
| @@ -134,9 +154,24 @@ export const run = async (projectArg: Project) => { | ||||
|       ]; | ||||
|  | ||||
|       // check for dependencies | ||||
|       await ensureDependency(packageJson, 'devDep', 'latest', '@push.rocks/tapbundle'); | ||||
|       await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tstest'); | ||||
|       await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tsbuild'); | ||||
|       await ensureDependency( | ||||
|         packageJson, | ||||
|         'devDep', | ||||
|         'latest', | ||||
|         '@push.rocks/tapbundle', | ||||
|       ); | ||||
|       await ensureDependency( | ||||
|         packageJson, | ||||
|         'devDep', | ||||
|         'latest', | ||||
|         '@git.zone/tstest', | ||||
|       ); | ||||
|       await ensureDependency( | ||||
|         packageJson, | ||||
|         'devDep', | ||||
|         'latest', | ||||
|         '@git.zone/tsbuild', | ||||
|       ); | ||||
|  | ||||
|       // set overrides | ||||
|       const overrides = plugins.smartfile.fs.toObjectSync( | ||||
|   | ||||
| @@ -16,7 +16,12 @@ const prettierDefaultMarkdownConfig: prettier.Options = { | ||||
|   parser: 'markdown', | ||||
| }; | ||||
|  | ||||
| const filesToFormat = [`ts/**/*.ts`, `test/**/*.ts`, `readme.md`, `docs/**/*.md`]; | ||||
| const filesToFormat = [ | ||||
|   `ts/**/*.ts`, | ||||
|   `test/**/*.ts`, | ||||
|   `readme.md`, | ||||
|   `docs/**/*.md`, | ||||
| ]; | ||||
|  | ||||
| const choosePrettierConfig = (fileArg: plugins.smartfile.SmartFile) => { | ||||
|   switch (fileArg.parsedPath.ext) { | ||||
| @@ -39,7 +44,10 @@ const prettierTypeScriptPipestop = plugins.through2.obj( | ||||
|       cb(null); | ||||
|     } else { | ||||
|       logger.log('info', `${fileArg.path} is being reformated!`); | ||||
|       const formatedFileString = await prettier.format(fileString, chosenConfig); | ||||
|       const formatedFileString = await prettier.format( | ||||
|         fileString, | ||||
|         chosenConfig, | ||||
|       ); | ||||
|       fileArg.setContentsFromString(formatedFileString); | ||||
|       cb(null, fileArg); | ||||
|     } | ||||
|   | ||||
| @@ -18,7 +18,8 @@ export const run = async () => { | ||||
|   } | ||||
|  | ||||
|   // Check and initialize readme.hints.md if it doesn't exist | ||||
|   const readmeHintsExists = await plugins.smartfile.fs.fileExists(readmeHintsPath); | ||||
|   const readmeHintsExists = | ||||
|     await plugins.smartfile.fs.fileExists(readmeHintsPath); | ||||
|   if (!readmeHintsExists) { | ||||
|     await plugins.smartfile.fs.toFs( | ||||
|       '# Project Readme Hints\n\nThis is the initial readme hints file.', | ||||
|   | ||||
| @@ -26,10 +26,12 @@ export const run = async (project: Project) => { | ||||
|     case 'npm': | ||||
|     case 'wcc': | ||||
|       if (project.gitzoneConfig.data.npmciOptions.npmAccessLevel === 'public') { | ||||
|         const ciTemplateDefault = await templateModule.getTemplate('ci_default'); | ||||
|         const ciTemplateDefault = | ||||
|           await templateModule.getTemplate('ci_default'); | ||||
|         ciTemplateDefault.writeToDisk(paths.cwd); | ||||
|       } else { | ||||
|         const ciTemplateDefault = await templateModule.getTemplate('ci_default_private'); | ||||
|         const ciTemplateDefault = | ||||
|           await templateModule.getTemplate('ci_default_private'); | ||||
|         ciTemplateDefault.writeToDisk(paths.cwd); | ||||
|       } | ||||
|       logger.log('info', 'Updated .gitlabci.yml!'); | ||||
| @@ -41,7 +43,8 @@ export const run = async (project: Project) => { | ||||
|       logger.log('info', 'Updated CI/CD config files!'); | ||||
|  | ||||
|       // lets care about docker | ||||
|       const dockerTemplate = await templateModule.getTemplate('dockerfile_service'); | ||||
|       const dockerTemplate = | ||||
|         await templateModule.getTemplate('dockerfile_service'); | ||||
|       dockerTemplate.writeToDisk(paths.cwd); | ||||
|       logger.log('info', 'Updated Dockerfile!'); | ||||
|  | ||||
| @@ -56,17 +59,22 @@ export const run = async (project: Project) => { | ||||
|  | ||||
|   // update html | ||||
|   if (project.gitzoneConfig.data.projectType === 'website') { | ||||
|     const websiteUpdateTemplate = await templateModule.getTemplate('website_update'); | ||||
|     const variables ={ | ||||
|     const websiteUpdateTemplate = | ||||
|       await templateModule.getTemplate('website_update'); | ||||
|     const variables = { | ||||
|       assetbrokerUrl: project.gitzoneConfig.data.module.assetbrokerUrl, | ||||
|       legalUrl: project.gitzoneConfig.data.module.legalUrl, | ||||
|     }; | ||||
|     console.log('updating website template with variables\n', JSON.stringify(variables, null, 2)); | ||||
|     console.log( | ||||
|       'updating website template with variables\n', | ||||
|       JSON.stringify(variables, null, 2), | ||||
|     ); | ||||
|     websiteUpdateTemplate.supplyVariables(variables); | ||||
|     await websiteUpdateTemplate.writeToDisk(paths.cwd); | ||||
|     logger.log('info', `Updated html for website!`); | ||||
|   } else if (project.gitzoneConfig.data.projectType === 'service') { | ||||
|     const websiteUpdateTemplate = await templateModule.getTemplate('service_update'); | ||||
|     const websiteUpdateTemplate = | ||||
|       await templateModule.getTemplate('service_update'); | ||||
|     await websiteUpdateTemplate.writeToDisk(paths.cwd); | ||||
|     logger.log('info', `Updated html for element template!`); | ||||
|   } else if (project.gitzoneConfig.data.projectType === 'wcc') { | ||||
|   | ||||
| @@ -19,8 +19,12 @@ export const run = async (projectArg: Project) => { | ||||
|   const publishModules = await tsPublishInstance.getModuleSubDirs(paths.cwd); | ||||
|   for (const publishModule of Object.keys(publishModules)) { | ||||
|     const publishConfig = publishModules[publishModule]; | ||||
|     tsconfigObject.compilerOptions.paths[`${publishConfig.name}`] = [`./${publishModule}/index.js`]; | ||||
|     tsconfigObject.compilerOptions.paths[`${publishConfig.name}`] = [ | ||||
|       `./${publishModule}/index.js`, | ||||
|     ]; | ||||
|   } | ||||
|   tsconfigSmartfile.setContentsFromString(JSON.stringify(tsconfigObject, null, 2)); | ||||
|   tsconfigSmartfile.setContentsFromString( | ||||
|     JSON.stringify(tsconfigObject, null, 2), | ||||
|   ); | ||||
|   await tsconfigSmartfile.write(); | ||||
| }; | ||||
|   | ||||
| @@ -7,13 +7,18 @@ export class CleanupFormatter extends BaseFormatter { | ||||
|   get name(): string { | ||||
|     return 'cleanup'; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async analyze(): Promise<IPlannedChange[]> { | ||||
|     const changes: IPlannedChange[] = []; | ||||
|      | ||||
|  | ||||
|     // List of files to remove | ||||
|     const filesToRemove = ['yarn.lock', 'package-lock.json', 'tslint.json', 'defaults.yml']; | ||||
|      | ||||
|     const filesToRemove = [ | ||||
|       'yarn.lock', | ||||
|       'package-lock.json', | ||||
|       'tslint.json', | ||||
|       'defaults.yml', | ||||
|     ]; | ||||
|  | ||||
|     for (const file of filesToRemove) { | ||||
|       const exists = await plugins.smartfile.fs.fileExists(file); | ||||
|       if (exists) { | ||||
| @@ -21,14 +26,14 @@ export class CleanupFormatter extends BaseFormatter { | ||||
|           type: 'delete', | ||||
|           path: file, | ||||
|           module: this.name, | ||||
|           description: `Remove obsolete file` | ||||
|           description: `Remove obsolete file`, | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|  | ||||
|     return changes; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async applyChange(change: IPlannedChange): Promise<void> { | ||||
|     switch (change.type) { | ||||
|       case 'delete': | ||||
| @@ -36,4 +41,4 @@ export class CleanupFormatter extends BaseFormatter { | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -5,4 +5,4 @@ export class CopyFormatter extends LegacyFormatter { | ||||
|   constructor(context: any, project: any) { | ||||
|     super(context, project, 'copy', formatCopy); | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -5,4 +5,4 @@ export class GitignoreFormatter extends LegacyFormatter { | ||||
|   constructor(context: any, project: any) { | ||||
|     super(context, project, 'gitignore', formatGitignore); | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -7,30 +7,37 @@ import * as plugins from '../mod.plugins.js'; | ||||
| export class LegacyFormatter extends BaseFormatter { | ||||
|   private moduleName: string; | ||||
|   private formatModule: any; | ||||
|    | ||||
|   constructor(context: any, project: Project, moduleName: string, formatModule: any) { | ||||
|  | ||||
|   constructor( | ||||
|     context: any, | ||||
|     project: Project, | ||||
|     moduleName: string, | ||||
|     formatModule: any, | ||||
|   ) { | ||||
|     super(context, project); | ||||
|     this.moduleName = moduleName; | ||||
|     this.formatModule = formatModule; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   get name(): string { | ||||
|     return this.moduleName; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async analyze(): Promise<IPlannedChange[]> { | ||||
|     // For legacy modules, we can't easily predict changes | ||||
|     // So we'll return a generic change that indicates the module will run | ||||
|     return [{ | ||||
|       type: 'modify', | ||||
|       path: '<various files>', | ||||
|       module: this.name, | ||||
|       description: `Run ${this.name} formatter` | ||||
|     }]; | ||||
|     return [ | ||||
|       { | ||||
|         type: 'modify', | ||||
|         path: '<various files>', | ||||
|         module: this.name, | ||||
|         description: `Run ${this.name} formatter`, | ||||
|       }, | ||||
|     ]; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async applyChange(change: IPlannedChange): Promise<void> { | ||||
|     // Run the legacy format module | ||||
|     await this.formatModule.run(this.project); | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -5,4 +5,4 @@ export class LicenseFormatter extends LegacyFormatter { | ||||
|   constructor(context: any, project: any) { | ||||
|     super(context, project, 'license', formatLicense); | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -5,4 +5,4 @@ export class NpmextraFormatter extends LegacyFormatter { | ||||
|   constructor(context: any, project: any) { | ||||
|     super(context, project, 'npmextra', formatNpmextra); | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -5,4 +5,4 @@ export class PackageJsonFormatter extends LegacyFormatter { | ||||
|   constructor(context: any, project: any) { | ||||
|     super(context, project, 'packagejson', formatPackageJson); | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -7,21 +7,16 @@ export class PrettierFormatter extends BaseFormatter { | ||||
|   get name(): string { | ||||
|     return 'prettier'; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async analyze(): Promise<IPlannedChange[]> { | ||||
|     const changes: IPlannedChange[] = []; | ||||
|      | ||||
|  | ||||
|     // Define directories to format (TypeScript directories by default) | ||||
|     const includeDirs = [ | ||||
|       'ts', | ||||
|       'ts_*', | ||||
|       'test', | ||||
|       'tests' | ||||
|     ]; | ||||
|      | ||||
|     const includeDirs = ['ts', 'ts_*', 'test', 'tests']; | ||||
|  | ||||
|     // File extensions to format | ||||
|     const extensions = '{ts,tsx,js,jsx,json,md,css,scss,html,xml,yaml,yml}'; | ||||
|      | ||||
|  | ||||
|     // Also format root-level config files | ||||
|     const rootConfigFiles = [ | ||||
|       'package.json', | ||||
| @@ -36,33 +31,36 @@ export class PrettierFormatter extends BaseFormatter { | ||||
|       'CHANGELOG.md', | ||||
|       'license', | ||||
|       'LICENSE', | ||||
|       '*.md' | ||||
|       '*.md', | ||||
|     ]; | ||||
|      | ||||
|  | ||||
|     // Collect all files to format | ||||
|     const allFiles: string[] = []; | ||||
|      | ||||
|  | ||||
|     // Add files from TypeScript directories | ||||
|     for (const dir of includeDirs) { | ||||
|       const globPattern = `${dir}/**/*.${extensions}`; | ||||
|       const dirFiles = await plugins.smartfile.fs.listFileTree('.', globPattern); | ||||
|       const dirFiles = await plugins.smartfile.fs.listFileTree( | ||||
|         '.', | ||||
|         globPattern, | ||||
|       ); | ||||
|       allFiles.push(...dirFiles); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Add root config files | ||||
|     for (const pattern of rootConfigFiles) { | ||||
|       const rootFiles = await plugins.smartfile.fs.listFileTree('.', pattern); | ||||
|       // Only include files at root level (no slashes in path) | ||||
|       const rootLevelFiles = rootFiles.filter(f => !f.includes('/')); | ||||
|       const rootLevelFiles = rootFiles.filter((f) => !f.includes('/')); | ||||
|       allFiles.push(...rootLevelFiles); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Remove duplicates | ||||
|     const uniqueFiles = [...new Set(allFiles)]; | ||||
|      | ||||
|  | ||||
|     // Get all files that match the pattern | ||||
|     const files = uniqueFiles; | ||||
|      | ||||
|  | ||||
|     // Ensure we only process actual files (not directories) | ||||
|     const validFiles: string[] = []; | ||||
|     for (const file of files) { | ||||
| @@ -76,48 +74,52 @@ export class PrettierFormatter extends BaseFormatter { | ||||
|         logVerbose(`Skipping ${file} - cannot access: ${error.message}`); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Check which files need formatting | ||||
|     for (const file of validFiles) { | ||||
|       // Skip files that haven't changed | ||||
|       if (!await this.shouldProcessFile(file)) { | ||||
|       if (!(await this.shouldProcessFile(file))) { | ||||
|         logVerbose(`Skipping ${file} - no changes detected`); | ||||
|         continue; | ||||
|       } | ||||
|        | ||||
|  | ||||
|       changes.push({ | ||||
|         type: 'modify', | ||||
|         path: file, | ||||
|         module: this.name, | ||||
|         description: 'Format with Prettier' | ||||
|         description: 'Format with Prettier', | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     logger.log('info', `Found ${changes.length} files to format with Prettier`); | ||||
|     return changes; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async execute(changes: IPlannedChange[]): Promise<void> { | ||||
|     const startTime = this.stats.moduleStartTime(this.name); | ||||
|     this.stats.startModule(this.name); | ||||
|      | ||||
|  | ||||
|     try { | ||||
|       await this.preExecute(); | ||||
|        | ||||
|  | ||||
|       // Batch process files | ||||
|       const batchSize = 10; // Process 10 files at a time | ||||
|       const batches: IPlannedChange[][] = []; | ||||
|        | ||||
|  | ||||
|       for (let i = 0; i < changes.length; i += batchSize) { | ||||
|         batches.push(changes.slice(i, i + batchSize)); | ||||
|       } | ||||
|        | ||||
|       logVerbose(`Processing ${changes.length} files in ${batches.length} batches`); | ||||
|        | ||||
|  | ||||
|       logVerbose( | ||||
|         `Processing ${changes.length} files in ${batches.length} batches`, | ||||
|       ); | ||||
|  | ||||
|       for (let i = 0; i < batches.length; i++) { | ||||
|         const batch = batches[i]; | ||||
|         logVerbose(`Processing batch ${i + 1}/${batches.length} (${batch.length} files)`); | ||||
|          | ||||
|         logVerbose( | ||||
|           `Processing batch ${i + 1}/${batches.length} (${batch.length} files)`, | ||||
|         ); | ||||
|  | ||||
|         // Process batch in parallel | ||||
|         const promises = batch.map(async (change) => { | ||||
|           try { | ||||
| @@ -125,44 +127,45 @@ export class PrettierFormatter extends BaseFormatter { | ||||
|             this.stats.recordFileOperation(this.name, change.type, true); | ||||
|           } catch (error) { | ||||
|             this.stats.recordFileOperation(this.name, change.type, false); | ||||
|             logger.log('error', `Failed to format ${change.path}: ${error.message}`); | ||||
|             logger.log( | ||||
|               'error', | ||||
|               `Failed to format ${change.path}: ${error.message}`, | ||||
|             ); | ||||
|             // Don't throw - continue with other files | ||||
|           } | ||||
|         }); | ||||
|          | ||||
|  | ||||
|         await Promise.all(promises); | ||||
|       } | ||||
|        | ||||
|  | ||||
|       await this.postExecute(); | ||||
|     } catch (error) { | ||||
|       await this.context.rollbackOperation(); | ||||
|       // Rollback removed - no longer tracking operations | ||||
|       throw error; | ||||
|     } finally { | ||||
|       this.stats.endModule(this.name, startTime); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async applyChange(change: IPlannedChange): Promise<void> { | ||||
|     if (change.type !== 'modify') return; | ||||
|      | ||||
|  | ||||
|     try { | ||||
|       // Read current content | ||||
|       const content = plugins.smartfile.fs.toStringSync(change.path); | ||||
|        | ||||
|  | ||||
|       // Format with prettier | ||||
|       const prettier = await import('prettier'); | ||||
|       const formatted = await prettier.format(content, { | ||||
|         filepath: change.path, | ||||
|         ...(await this.getPrettierConfig()) | ||||
|         ...(await this.getPrettierConfig()), | ||||
|       }); | ||||
|        | ||||
|  | ||||
|       // Only write if content actually changed | ||||
|       if (formatted !== content) { | ||||
|         await this.modifyFile(change.path, formatted); | ||||
|         logVerbose(`Formatted ${change.path}`); | ||||
|       } else { | ||||
|         // Still update cache even if content didn't change | ||||
|         await this.cache.updateFileCache(change.path); | ||||
|         logVerbose(`No formatting changes for ${change.path}`); | ||||
|       } | ||||
|     } catch (error) { | ||||
| @@ -170,7 +173,7 @@ export class PrettierFormatter extends BaseFormatter { | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|  | ||||
|   private async getPrettierConfig(): Promise<any> { | ||||
|     // Try to load prettier config from the project | ||||
|     const prettierConfig = new plugins.npmextra.Npmextra(); | ||||
| @@ -181,7 +184,7 @@ export class PrettierFormatter extends BaseFormatter { | ||||
|       printWidth: 80, | ||||
|       tabWidth: 2, | ||||
|       semi: true, | ||||
|       arrowParens: 'always' | ||||
|       arrowParens: 'always', | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -6,17 +6,19 @@ export class ReadmeFormatter extends BaseFormatter { | ||||
|   get name(): string { | ||||
|     return 'readme'; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async analyze(): Promise<IPlannedChange[]> { | ||||
|     return [{ | ||||
|       type: 'modify', | ||||
|       path: 'readme.md', | ||||
|       module: this.name, | ||||
|       description: 'Ensure readme files exist' | ||||
|     }]; | ||||
|     return [ | ||||
|       { | ||||
|         type: 'modify', | ||||
|         path: 'readme.md', | ||||
|         module: this.name, | ||||
|         description: 'Ensure readme files exist', | ||||
|       }, | ||||
|     ]; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   async applyChange(change: IPlannedChange): Promise<void> { | ||||
|     await formatReadme.run(); | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -5,4 +5,4 @@ export class TemplatesFormatter extends LegacyFormatter { | ||||
|   constructor(context: any, project: any) { | ||||
|     super(context, project, 'templates', formatTemplates); | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -5,4 +5,4 @@ export class TsconfigFormatter extends LegacyFormatter { | ||||
|   constructor(context: any, project: any) { | ||||
|     super(context, project, 'tsconfig', formatTsconfig); | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -16,27 +16,29 @@ import { PrettierFormatter } from './formatters/prettier.formatter.js'; | ||||
| import { ReadmeFormatter } from './formatters/readme.formatter.js'; | ||||
| import { CopyFormatter } from './formatters/copy.formatter.js'; | ||||
|  | ||||
| export let run = async (options: { | ||||
|   dryRun?: boolean; | ||||
|   yes?: boolean; | ||||
|   planOnly?: boolean; | ||||
|   savePlan?: string; | ||||
|   fromPlan?: string; | ||||
|   detailed?: boolean; | ||||
|   interactive?: boolean; | ||||
|   parallel?: boolean; | ||||
|   verbose?: boolean; | ||||
| } = {}): Promise<any> => { | ||||
| export let run = async ( | ||||
|   options: { | ||||
|     dryRun?: boolean; | ||||
|     yes?: boolean; | ||||
|     planOnly?: boolean; | ||||
|     savePlan?: string; | ||||
|     fromPlan?: string; | ||||
|     detailed?: boolean; | ||||
|     interactive?: boolean; | ||||
|     parallel?: boolean; | ||||
|     verbose?: boolean; | ||||
|   } = {}, | ||||
| ): Promise<any> => { | ||||
|   // Set verbose mode if requested | ||||
|   if (options.verbose) { | ||||
|     setVerboseMode(true); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   const project = await Project.fromCwd(); | ||||
|   const context = new FormatContext(); | ||||
|   await context.initializeCache();  // Initialize the cache system | ||||
|   // Cache system removed - no longer needed | ||||
|   const planner = new FormatPlanner(); | ||||
|    | ||||
|  | ||||
|   // Get configuration from npmextra | ||||
|   const npmextraConfig = new plugins.npmextra.Npmextra(); | ||||
|   const formatConfig = npmextraConfig.dataFor<any>('gitzone.format', { | ||||
| @@ -49,30 +51,27 @@ export let run = async (options: { | ||||
|       autoRollbackOnError: true, | ||||
|       backupRetentionDays: 7, | ||||
|       maxBackupSize: '100MB', | ||||
|       excludePatterns: ['node_modules/**', '.git/**'] | ||||
|       excludePatterns: ['node_modules/**', '.git/**'], | ||||
|     }, | ||||
|     modules: { | ||||
|       skip: [], | ||||
|       only: [], | ||||
|       order: [] | ||||
|       order: [], | ||||
|     }, | ||||
|     parallel: true, | ||||
|     cache: { | ||||
|       enabled: true, | ||||
|       clean: true  // Clean invalid entries from cache | ||||
|     } | ||||
|       clean: true, // Clean invalid entries from cache | ||||
|     }, | ||||
|   }); | ||||
|    | ||||
|   // Clean cache if configured | ||||
|   if (formatConfig.cache.clean) { | ||||
|     await context.getChangeCache().clean(); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   // Cache cleaning removed - no longer using cache system | ||||
|  | ||||
|   // Override config with command options | ||||
|   const interactive = options.interactive ?? formatConfig.interactive; | ||||
|   const autoApprove = options.yes ?? formatConfig.autoApprove; | ||||
|   const parallel = options.parallel ?? formatConfig.parallel; | ||||
|    | ||||
|  | ||||
|   try { | ||||
|     // Initialize formatters | ||||
|     const formatters = [ | ||||
| @@ -87,9 +86,9 @@ export let run = async (options: { | ||||
|       new ReadmeFormatter(context, project), | ||||
|       new CopyFormatter(context, project), | ||||
|     ]; | ||||
|      | ||||
|  | ||||
|     // Filter formatters based on configuration | ||||
|     const activeFormatters = formatters.filter(formatter => { | ||||
|     const activeFormatters = formatters.filter((formatter) => { | ||||
|       if (formatConfig.modules.only.length > 0) { | ||||
|         return formatConfig.modules.only.includes(formatter.name); | ||||
|       } | ||||
| @@ -98,33 +97,36 @@ export let run = async (options: { | ||||
|       } | ||||
|       return true; | ||||
|     }); | ||||
|      | ||||
|  | ||||
|     // Plan phase | ||||
|     logger.log('info', 'Analyzing project for format operations...'); | ||||
|     let plan = options.fromPlan | ||||
|       ? JSON.parse(await plugins.smartfile.fs.toStringSync(options.fromPlan)) | ||||
|       : await planner.planFormat(activeFormatters); | ||||
|      | ||||
|  | ||||
|     // Display plan | ||||
|     await planner.displayPlan(plan, options.detailed); | ||||
|      | ||||
|  | ||||
|     // Save plan if requested | ||||
|     if (options.savePlan) { | ||||
|       await plugins.smartfile.memory.toFs(JSON.stringify(plan, null, 2), options.savePlan); | ||||
|       await plugins.smartfile.memory.toFs( | ||||
|         JSON.stringify(plan, null, 2), | ||||
|         options.savePlan, | ||||
|       ); | ||||
|       logger.log('info', `Plan saved to ${options.savePlan}`); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Exit if plan-only mode | ||||
|     if (options.planOnly) { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Dry-run mode | ||||
|     if (options.dryRun) { | ||||
|       logger.log('info', 'Dry-run mode - no changes will be made'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Interactive confirmation | ||||
|     if (interactive && !autoApprove) { | ||||
|       const interactInstance = new plugins.smartinteract.SmartInteract(); | ||||
| @@ -132,117 +134,56 @@ export let run = async (options: { | ||||
|         type: 'confirm', | ||||
|         name: 'proceed', | ||||
|         message: 'Proceed with formatting?', | ||||
|         default: true | ||||
|         default: true, | ||||
|       }); | ||||
|        | ||||
|  | ||||
|       if (!(response as any).value) { | ||||
|         logger.log('info', 'Format operation cancelled by user'); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Execute phase | ||||
|     logger.log('info', `Executing format operations${parallel ? ' in parallel' : ' sequentially'}...`); | ||||
|     logger.log( | ||||
|       'info', | ||||
|       `Executing format operations${parallel ? ' in parallel' : ' sequentially'}...`, | ||||
|     ); | ||||
|     await planner.executePlan(plan, activeFormatters, context, parallel); | ||||
|      | ||||
|  | ||||
|     // Finish statistics tracking | ||||
|     context.getFormatStats().finish(); | ||||
|      | ||||
|  | ||||
|     // Display statistics | ||||
|     const showStats = npmextraConfig.dataFor('gitzone.format.showStats', true); | ||||
|     if (showStats) { | ||||
|       context.getFormatStats().displayStats(); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Save stats if requested | ||||
|     if (options.detailed) { | ||||
|       const statsPath = `.nogit/format-stats-${Date.now()}.json`; | ||||
|       await context.getFormatStats().saveReport(statsPath); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     logger.log('success', 'Format operations completed successfully!'); | ||||
|      | ||||
|   } catch (error) { | ||||
|     logger.log('error', `Format operation failed: ${error.message}`); | ||||
|      | ||||
|     // Automatic rollback if enabled | ||||
|     if (formatConfig.rollback.enabled && formatConfig.rollback.autoRollbackOnError) { | ||||
|       logger.log('info', 'Attempting automatic rollback...'); | ||||
|       try { | ||||
|         await context.rollbackOperation(); | ||||
|         logger.log('success', 'Rollback completed successfully'); | ||||
|       } catch (rollbackError) { | ||||
|         logger.log('error', `Rollback failed: ${rollbackError.message}`); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Rollback system has been removed for stability | ||||
|  | ||||
|     throw error; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // Export CLI command handlers | ||||
| export const handleRollback = async (operationId?: string): Promise<void> => { | ||||
|   const context = new FormatContext(); | ||||
|   const rollbackManager = context.getRollbackManager(); | ||||
|    | ||||
|   if (!operationId) { | ||||
|     // Rollback to last operation | ||||
|     const backups = await rollbackManager.listBackups(); | ||||
|     const lastOperation = backups | ||||
|       .filter(op => op.status !== 'rolled-back') | ||||
|       .sort((a, b) => b.timestamp - a.timestamp)[0]; | ||||
|      | ||||
|     if (!lastOperation) { | ||||
|       logger.log('warn', 'No operations available for rollback'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     operationId = lastOperation.id; | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     await rollbackManager.rollback(operationId); | ||||
|     logger.log('success', `Successfully rolled back operation ${operationId}`); | ||||
|   } catch (error) { | ||||
|     logger.log('error', `Rollback failed: ${error.message}`); | ||||
|     throw error; | ||||
|   } | ||||
|   logger.log('info', 'Rollback system has been disabled for stability'); | ||||
| }; | ||||
|  | ||||
| export const handleListBackups = async (): Promise<void> => { | ||||
|   const context = new FormatContext(); | ||||
|   const rollbackManager = context.getRollbackManager(); | ||||
|   const backups = await rollbackManager.listBackups(); | ||||
|    | ||||
|   if (backups.length === 0) { | ||||
|     logger.log('info', 'No backup operations found'); | ||||
|     return; | ||||
|   } | ||||
|    | ||||
|   console.log('\nAvailable backups:'); | ||||
|   console.log('━'.repeat(50)); | ||||
|    | ||||
|   for (const backup of backups) { | ||||
|     const date = new Date(backup.timestamp).toLocaleString(); | ||||
|     const status = backup.status; | ||||
|     const filesCount = backup.files.length; | ||||
|      | ||||
|     console.log(`ID: ${backup.id}`); | ||||
|     console.log(`Date: ${date}`); | ||||
|     console.log(`Status: ${status}`); | ||||
|     console.log(`Files: ${filesCount}`); | ||||
|     console.log('─'.repeat(50)); | ||||
|   } | ||||
|   logger.log('info', 'Backup system has been disabled for stability'); | ||||
| }; | ||||
|  | ||||
| export const handleCleanBackups = async (): Promise<void> => { | ||||
|   const context = new FormatContext(); | ||||
|   const rollbackManager = context.getRollbackManager(); | ||||
|    | ||||
|   // Get retention days from config | ||||
|   const npmextraConfig = new plugins.npmextra.Npmextra(); | ||||
|   const retentionDays = npmextraConfig.dataFor<any>('gitzone.format.rollback.backupRetentionDays', 7); | ||||
|    | ||||
|   await rollbackManager.cleanOldBackups(retentionDays); | ||||
|   logger.log('success', `Cleaned backups older than ${retentionDays} days`); | ||||
| }; | ||||
|   logger.log('info', 'Backup cleaning has been disabled - backup system removed'); | ||||
| }; | ||||
|   | ||||
| @@ -9,7 +9,7 @@ export type IFormatOperation = { | ||||
|   }>; | ||||
|   status: 'pending' | 'in-progress' | 'completed' | 'failed' | 'rolled-back'; | ||||
|   error?: Error; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export type IFormatPlan = { | ||||
|   summary: { | ||||
| @@ -32,7 +32,7 @@ export type IFormatPlan = { | ||||
|     message: string; | ||||
|     module: string; | ||||
|   }>; | ||||
| } | ||||
| }; | ||||
|  | ||||
| export type IPlannedChange = { | ||||
|   type: 'create' | 'modify' | 'delete'; | ||||
| @@ -42,4 +42,4 @@ export type IPlannedChange = { | ||||
|   content?: string; // For create/modify operations | ||||
|   diff?: string; | ||||
|   size?: number; | ||||
| } | ||||
| }; | ||||
|   | ||||
| @@ -35,7 +35,10 @@ export class Meta { | ||||
|    * sorts the metaRepoData | ||||
|    */ | ||||
|   public async sortMetaRepoData() { | ||||
|     const stringifiedMetadata = plugins.smartjson.stringify(this.metaRepoData, []); | ||||
|     const stringifiedMetadata = plugins.smartjson.stringify( | ||||
|       this.metaRepoData, | ||||
|       [], | ||||
|     ); | ||||
|     this.metaRepoData = plugins.smartjson.parse(stringifiedMetadata); | ||||
|   } | ||||
|  | ||||
| @@ -45,11 +48,15 @@ export class Meta { | ||||
|   public async readDirectory() { | ||||
|     await this.syncToRemote(true); | ||||
|     logger.log('info', `reading directory`); | ||||
|     const metaFileExists = plugins.smartfile.fs.fileExistsSync(this.filePaths.metaJson); | ||||
|     const metaFileExists = plugins.smartfile.fs.fileExistsSync( | ||||
|       this.filePaths.metaJson, | ||||
|     ); | ||||
|     if (!metaFileExists) { | ||||
|       throw new Error(`meta file does not exist at ${this.filePaths.metaJson}`); | ||||
|     } | ||||
|     this.metaRepoData = plugins.smartfile.fs.toObjectSync(this.filePaths.metaJson); | ||||
|     this.metaRepoData = plugins.smartfile.fs.toObjectSync( | ||||
|       this.filePaths.metaJson, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -76,7 +83,10 @@ export class Meta { | ||||
|       this.filePaths.metaJson, | ||||
|     ); | ||||
|     // write .gitignore to disk | ||||
|     plugins.smartfile.memory.toFsSync(await this.generateGitignore(), this.filePaths.gitIgnore); | ||||
|     plugins.smartfile.memory.toFsSync( | ||||
|       await this.generateGitignore(), | ||||
|       this.filePaths.gitIgnore, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -84,13 +94,17 @@ export class Meta { | ||||
|    */ | ||||
|   public async syncToRemote(gitCleanArg = false) { | ||||
|     logger.log('info', `syncing from origin master`); | ||||
|     await this.smartshellInstance.exec(`cd ${this.cwd} && git pull origin master`); | ||||
|     await this.smartshellInstance.exec( | ||||
|       `cd ${this.cwd} && git pull origin master`, | ||||
|     ); | ||||
|     if (gitCleanArg) { | ||||
|       logger.log('info', `cleaning the repository from old directories`); | ||||
|       await this.smartshellInstance.exec(`cd ${this.cwd} && git clean -fd`); | ||||
|     } | ||||
|     logger.log('info', `syncing  to remote origin master`); | ||||
|     await this.smartshellInstance.exec(`cd ${this.cwd} && git push origin master`); | ||||
|     await this.smartshellInstance.exec( | ||||
|       `cd ${this.cwd} && git push origin master`, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -98,7 +112,9 @@ export class Meta { | ||||
|    */ | ||||
|   public async updateLocalRepos() { | ||||
|     await this.syncToRemote(); | ||||
|     const projects = plugins.smartfile.fs.toObjectSync(this.filePaths.metaJson).projects; | ||||
|     const projects = plugins.smartfile.fs.toObjectSync( | ||||
|       this.filePaths.metaJson, | ||||
|     ).projects; | ||||
|     const preExistingFolders = plugins.smartfile.fs.listFoldersSync(this.cwd); | ||||
|     for (const preExistingFolderArg of preExistingFolders) { | ||||
|       if ( | ||||
| @@ -107,14 +123,18 @@ export class Meta { | ||||
|           projectFolder.startsWith(preExistingFolderArg), | ||||
|         ) | ||||
|       ) { | ||||
|         const response = await plugins.smartinteraction.SmartInteract.getCliConfirmation( | ||||
|           `Do you want to delete superfluous directory >>${preExistingFolderArg}<< ?`, | ||||
|           true, | ||||
|         ); | ||||
|         const response = | ||||
|           await plugins.smartinteraction.SmartInteract.getCliConfirmation( | ||||
|             `Do you want to delete superfluous directory >>${preExistingFolderArg}<< ?`, | ||||
|             true, | ||||
|           ); | ||||
|         if (response) { | ||||
|           logger.log('warn', `Deleting >>${preExistingFolderArg}<<!`); | ||||
|         } else { | ||||
|           logger.log('warn', `Not deleting ${preExistingFolderArg} by request!`); | ||||
|           logger.log( | ||||
|             'warn', | ||||
|             `Not deleting ${preExistingFolderArg} by request!`, | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| @@ -160,7 +180,9 @@ export class Meta { | ||||
|    */ | ||||
|   public async initProject() { | ||||
|     await this.syncToRemote(true); | ||||
|     const fileExists = await plugins.smartfile.fs.fileExists(this.filePaths.metaJson); | ||||
|     const fileExists = await plugins.smartfile.fs.fileExists( | ||||
|       this.filePaths.metaJson, | ||||
|     ); | ||||
|     if (!fileExists) { | ||||
|       await plugins.smartfile.memory.toFs( | ||||
|         JSON.stringify({ | ||||
| @@ -168,7 +190,10 @@ export class Meta { | ||||
|         }), | ||||
|         this.filePaths.metaJson, | ||||
|       ); | ||||
|       logger.log(`success`, `created a new .meta.json in directory ${this.cwd}`); | ||||
|       logger.log( | ||||
|         `success`, | ||||
|         `created a new .meta.json in directory ${this.cwd}`, | ||||
|       ); | ||||
|       await plugins.smartfile.memory.toFs( | ||||
|         JSON.stringify({ | ||||
|           name: this.dirName, | ||||
| @@ -176,9 +201,15 @@ export class Meta { | ||||
|         }), | ||||
|         this.filePaths.packageJson, | ||||
|       ); | ||||
|       logger.log(`success`, `created a new package.json in directory ${this.cwd}`); | ||||
|       logger.log( | ||||
|         `success`, | ||||
|         `created a new package.json in directory ${this.cwd}`, | ||||
|       ); | ||||
|     } else { | ||||
|       logger.log(`error`, `directory ${this.cwd} already has a .metaJson file. Doing nothing.`); | ||||
|       logger.log( | ||||
|         `error`, | ||||
|         `directory ${this.cwd} already has a .metaJson file. Doing nothing.`, | ||||
|       ); | ||||
|     } | ||||
|     await this.smartshellInstance.exec( | ||||
|       `cd ${this.cwd} && git add -A && git commit -m "feat(project): init meta project for ${this.dirName}"`, | ||||
| @@ -195,7 +226,9 @@ export class Meta { | ||||
|     const existingProject = this.metaRepoData.projects[projectNameArg]; | ||||
|  | ||||
|     if (existingProject) { | ||||
|       throw new Error('Project already exists! Please remove it first before adding it again.'); | ||||
|       throw new Error( | ||||
|         'Project already exists! Please remove it first before adding it again.', | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     this.metaRepoData.projects[projectNameArg] = gitUrlArg; | ||||
| @@ -217,7 +250,10 @@ export class Meta { | ||||
|     const existingProject = this.metaRepoData.projects[projectNameArg]; | ||||
|  | ||||
|     if (!existingProject) { | ||||
|       logger.log('error', `Project ${projectNameArg} does not exist! So it cannot be removed`); | ||||
|       logger.log( | ||||
|         'error', | ||||
|         `Project ${projectNameArg} does not exist! So it cannot be removed`, | ||||
|       ); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
| @@ -228,7 +264,9 @@ export class Meta { | ||||
|     await this.writeToDisk(); | ||||
|  | ||||
|     logger.log('info', 'removing directory from cwd'); | ||||
|     await plugins.smartfile.fs.remove(plugins.path.join(paths.cwd, projectNameArg)); | ||||
|     await plugins.smartfile.fs.remove( | ||||
|       plugins.path.join(paths.cwd, projectNameArg), | ||||
|     ); | ||||
|     await this.updateLocalRepos(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -16,7 +16,9 @@ export let run = () => { | ||||
|     * create a new project with 'gitzone template [template]' | ||||
|       the following templates exist: ${(() => { | ||||
|         let projects = `\n`; | ||||
|         for (const template of plugins.smartfile.fs.listFoldersSync(paths.templatesDir)) { | ||||
|         for (const template of plugins.smartfile.fs.listFoldersSync( | ||||
|           paths.templatesDir, | ||||
|         )) { | ||||
|           projects += `       - ${template}\n`; | ||||
|         } | ||||
|         return projects; | ||||
|   | ||||
| @@ -15,7 +15,9 @@ export const run = async (argvArg: any) => { | ||||
|   }); | ||||
|  | ||||
|   await smartshellInstance.execStrict(`cd ${paths.cwd} && git checkout master`); | ||||
|   await smartshellInstance.execStrict(`cd ${paths.cwd} && git pull origin master`); | ||||
|   await smartshellInstance.execStrict( | ||||
|     `cd ${paths.cwd} && git pull origin master`, | ||||
|   ); | ||||
|   await smartshellInstance.execStrict(`cd ${paths.cwd} && npm ci`); | ||||
|  | ||||
|   await provideNoGitFiles(); | ||||
|   | ||||
| @@ -16,7 +16,9 @@ export const isTemplate = async (templateNameArg: string) => { | ||||
|  | ||||
| export const getTemplate = async (templateNameArg: string) => { | ||||
|   if (isTemplate(templateNameArg)) { | ||||
|     const localScafTemplate = new plugins.smartscaf.ScafTemplate(getTemplatePath(templateNameArg)); | ||||
|     const localScafTemplate = new plugins.smartscaf.ScafTemplate( | ||||
|       getTemplatePath(templateNameArg), | ||||
|     ); | ||||
|     await localScafTemplate.readTemplateFromDir(); | ||||
|     return localScafTemplate; | ||||
|   } else { | ||||
| @@ -32,7 +34,8 @@ export const run = async (argvArg: any) => { | ||||
|     const answerBucket = await smartinteract.askQuestion({ | ||||
|       type: 'list', | ||||
|       default: 'npm', | ||||
|       message: 'What template do you want to scaffold? (Only showing mpost common options)', | ||||
|       message: | ||||
|         'What template do you want to scaffold? (Only showing mpost common options)', | ||||
|       name: 'templateName', | ||||
|       choices: ['npm', 'service', 'wcc', 'website'], | ||||
|     }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user