feat(format): Enhance format module with rollback, diff reporting, and improved parallel execution
This commit is contained in:
		
							
								
								
									
										10
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								changelog.md
									
									
									
									
									
								
							| @@ -1,5 +1,15 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-05-19 - 1.16.0 - feat(format) | ||||
| Enhance format module with rollback, diff reporting, and improved parallel execution | ||||
|  | ||||
| - Implemented rollback functionality with backup management and automatic rollback on error | ||||
| - Added CLI commands for rollback, listing backups, and cleaning old backups | ||||
| - Introduced DiffReporter for generating and displaying file diffs | ||||
| - Improved file change caching via ChangeCache and expanded dependency analysis for parallel execution | ||||
| - Updated logging to support verbose mode and enhanced user feedback | ||||
| - Updated package.json to include new dependency '@push.rocks/smartdiff' | ||||
|  | ||||
| ## 2025-05-14 - 1.15.5 - fix(dependencies) | ||||
| Update @git.zone/tsdoc to ^1.5.0 and @types/node to ^22.15.18 | ||||
|  | ||||
|   | ||||
| @@ -60,6 +60,7 @@ | ||||
|     "@git.zone/tsbuild": "^2.3.2", | ||||
|     "@git.zone/tsrun": "^1.3.3", | ||||
|     "@git.zone/tstest": "^1.0.96", | ||||
|     "@push.rocks/smartdiff": "^1.0.3", | ||||
|     "@types/node": "^22.15.18" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|   | ||||
							
								
								
									
										15
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										15
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -117,6 +117,9 @@ importers: | ||||
|       '@git.zone/tstest': | ||||
|         specifier: ^1.0.96 | ||||
|         version: 1.0.96(@aws-sdk/credential-providers@3.750.0)(socks@2.8.4)(typescript@5.8.3) | ||||
|       '@push.rocks/smartdiff': | ||||
|         specifier: ^1.0.3 | ||||
|         version: 1.0.3 | ||||
|       '@types/node': | ||||
|         specifier: ^22.15.18 | ||||
|         version: 22.15.18 | ||||
| @@ -913,6 +916,9 @@ packages: | ||||
|   '@push.rocks/smartdelay@3.0.5': | ||||
|     resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==} | ||||
|  | ||||
|   '@push.rocks/smartdiff@1.0.3': | ||||
|     resolution: {integrity: sha512-cXUKj0KJBxnrZDN1Ztc2WiFRJM3vOTdQUdBfe6ar5NlKuXytSRMJqVL8IUbtWfMCSOx6HgWAUT7W68+/X2TG8w==} | ||||
|  | ||||
|   '@push.rocks/smartenv@5.0.12': | ||||
|     resolution: {integrity: sha512-tDEFwywzq0FNzRYc9qY2dRl2pgQuZG0G2/yml2RLWZWSW+Fn1EHshnKOGHz8o77W7zvu4hTgQQX42r/JY5XHTg==} | ||||
|  | ||||
| @@ -2622,6 +2628,9 @@ packages: | ||||
|   fast-deep-equal@3.1.3: | ||||
|     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} | ||||
|  | ||||
|   fast-diff@1.3.0: | ||||
|     resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} | ||||
|  | ||||
|   fast-fifo@1.3.2: | ||||
|     resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} | ||||
|  | ||||
| @@ -6405,6 +6414,10 @@ snapshots: | ||||
|     dependencies: | ||||
|       '@push.rocks/smartpromise': 4.2.3 | ||||
|  | ||||
|   '@push.rocks/smartdiff@1.0.3': | ||||
|     dependencies: | ||||
|       fast-diff: 1.3.0 | ||||
|  | ||||
|   '@push.rocks/smartenv@5.0.12': | ||||
|     dependencies: | ||||
|       '@push.rocks/smartpromise': 4.2.3 | ||||
| @@ -8812,6 +8825,8 @@ snapshots: | ||||
|  | ||||
|   fast-deep-equal@3.1.3: {} | ||||
|  | ||||
|   fast-diff@1.3.0: {} | ||||
|  | ||||
|   fast-fifo@1.3.2: {} | ||||
|  | ||||
|   fast-glob@3.3.3: | ||||
|   | ||||
							
								
								
									
										187
									
								
								readme.hints.md
									
									
									
									
									
								
							
							
						
						
									
										187
									
								
								readme.hints.md
									
									
									
									
									
								
							| @@ -1 +1,188 @@ | ||||
| # Gitzone CLI - Development Hints | ||||
|  | ||||
| * the cli of the git.zone project. | ||||
|  | ||||
| ## Project Overview | ||||
|  | ||||
| Gitzone CLI (`@git.zone/cli`) is a comprehensive toolbelt for streamlining local development cycles. It provides utilities for: | ||||
| - Project initialization and templating (via smartscaf) | ||||
| - Code formatting and standardization | ||||
| - Version control and commit management | ||||
| - Docker and CI/CD integration | ||||
| - Meta project management | ||||
|  | ||||
| ## Architecture | ||||
|  | ||||
| ### Core Structure | ||||
| - Main CLI entry: `cli.ts` / `cli.child.ts` | ||||
| - Modular architecture with separate modules in `ts/mod_*` directories | ||||
| - Each module handles specific functionality (format, commit, docker, etc.) | ||||
| - Extensive use of plugins pattern via `plugins.ts` files | ||||
|  | ||||
| ### Configuration Management | ||||
| - Uses `npmextra.json` for all tool configuration | ||||
| - Configuration stored under `gitzone` key in npmextra | ||||
| - No separate `.gitzonerc` file - everything in npmextra.json | ||||
| - Project type and module metadata also stored in npmextra | ||||
|  | ||||
| ### Format Module (`mod_format`) - SIGNIFICANTLY ENHANCED | ||||
|  | ||||
| The format module is responsible for project standardization: | ||||
|  | ||||
| #### Current Modules: | ||||
| 1. **cleanup** - Removes obsolete files (yarn.lock, tslint.json, etc.) | ||||
| 2. **copy** - File copying with glob patterns (fully implemented) | ||||
| 3. **gitignore** - Creates/updates .gitignore from templates | ||||
| 4. **license** - Checks dependency licenses for compatibility | ||||
| 5. **npmextra** - Manages project metadata and configuration | ||||
| 6. **packagejson** - Formats and updates package.json | ||||
| 7. **prettier** - Applies code formatting with batching | ||||
| 8. **readme** - Ensures readme files exist | ||||
| 9. **templates** - Updates project templates based on type | ||||
| 10. **tsconfig** - Formats TypeScript configuration | ||||
|  | ||||
| #### Execution Order (Dependency-Based): | ||||
| - Modules are now executed in parallel groups based on dependencies | ||||
| - Independent modules run concurrently for better performance | ||||
| - Dependency analyzer ensures correct execution order | ||||
|  | ||||
| ### New Architecture Features | ||||
|  | ||||
| 1. **BaseFormatter Pattern**: All formatters extend abstract BaseFormatter class | ||||
| 2. **FormatContext**: Central state management across all modules | ||||
| 3. **FormatPlanner**: Implements plan → action workflow | ||||
| 4. **RollbackManager**: Full backup/restore capabilities | ||||
| 5. **ChangeCache**: Tracks file changes to optimize performance | ||||
| 6. **DependencyAnalyzer**: Manages module execution order | ||||
| 7. **DiffReporter**: Generates diff views for changes | ||||
| 8. **FormatStats**: Comprehensive execution statistics | ||||
|  | ||||
| ### Key Patterns | ||||
|  | ||||
| 1. **Plugin Architecture**: All dependencies imported through `plugins.ts` files | ||||
| 2. **Streaming**: Uses smartstream for file processing | ||||
| 3. **Interactive Prompts**: smartinteract for user input | ||||
| 4. **Enhanced Error Handling**: Comprehensive try-catch with automatic rollback | ||||
| 5. **Template System**: Templates handled by smartscaf, not directly by gitzone | ||||
| 6. **Type Safety**: Full TypeScript with interfaces and type definitions | ||||
|  | ||||
| ### Important Notes | ||||
|  | ||||
| - `.nogit/` directory used for temporary/untracked files, backups, and cache | ||||
| - `.nogit/gitzone-backups/` stores format operation backups | ||||
| - `.nogit/gitzone-cache/` stores file change cache | ||||
| - Templates are managed by smartscaf - improvements should be made there | ||||
| - License checking configurable with exceptions support | ||||
| - All features implemented: `ensureDependency`, copy module, etc. | ||||
|  | ||||
| ## Recent Improvements (Completed) | ||||
|  | ||||
| 1. **Plan → Action Workflow**: Shows changes before applying them | ||||
| 2. **Rollback Mechanism**: Full backup and restore on failures | ||||
| 3. **Enhanced Configuration**: Granular control via npmextra.json | ||||
| 4. **Better Error Handling**: Detailed errors with recovery options | ||||
| 5. **Performance Optimizations**: Parallel execution and caching | ||||
| 6. **Reporting**: Diff views, statistics, verbose logging | ||||
| 7. **Architecture**: Clean separation of concerns with new classes | ||||
|  | ||||
| ## Development Tips | ||||
|  | ||||
| - Always check readme.plan.md for ongoing improvement plans | ||||
| - Use npmextra.json for any new configuration options | ||||
| - Keep modules focused and single-purpose | ||||
| - Maintain the existing plugin pattern for dependencies | ||||
| - Test format operations on sample projects before deploying | ||||
| - Consider backward compatibility when changing configuration structure | ||||
| - Use BaseFormatter pattern for new format modules | ||||
| - Leverage FormatContext for cross-module state sharing | ||||
|  | ||||
| ## Configuration Examples | ||||
|  | ||||
| ```json | ||||
| { | ||||
|   "gitzone": { | ||||
|     "format": { | ||||
|       "interactive": true, | ||||
|       "parallel": true, | ||||
|       "showStats": true, | ||||
|       "cache": { | ||||
|         "enabled": true, | ||||
|         "clean": true | ||||
|       }, | ||||
|       "rollback": { | ||||
|         "enabled": true, | ||||
|         "autoRollbackOnError": true, | ||||
|         "backupRetentionDays": 7 | ||||
|       }, | ||||
|       "modules": { | ||||
|         "skip": ["prettier"], | ||||
|         "only": [], | ||||
|         "order": [] | ||||
|       }, | ||||
|       "licenses": { | ||||
|         "allowed": ["MIT", "Apache-2.0"], | ||||
|         "exceptions": { | ||||
|           "some-package": "GPL-3.0" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## CLI Usage | ||||
|  | ||||
| ```bash | ||||
| # Basic format | ||||
| gitzone format | ||||
|  | ||||
| # Dry run to preview changes | ||||
| gitzone format --dry-run | ||||
|  | ||||
| # Non-interactive mode | ||||
| gitzone format --yes | ||||
|  | ||||
| # Plan only (no execution) | ||||
| gitzone format --plan-only | ||||
|  | ||||
| # Save plan for later | ||||
| gitzone format --save-plan format.json | ||||
|  | ||||
| # Execute saved plan | ||||
| gitzone format --from-plan format.json | ||||
|  | ||||
| # Verbose mode | ||||
| gitzone format --verbose | ||||
|  | ||||
| # Detailed diff views | ||||
| gitzone format --detailed | ||||
|  | ||||
| # Rollback operations | ||||
| gitzone format --rollback | ||||
| gitzone format --rollback <operation-id> | ||||
| gitzone format --list-backups | ||||
| gitzone format --clean-backups | ||||
| ``` | ||||
|  | ||||
| ## Common Issues (Now Resolved) | ||||
|  | ||||
| 1. ✅ Format operations are now reversible with rollback | ||||
| 2. ✅ Enhanced error messages with recovery suggestions | ||||
| 3. ✅ All modules fully implemented (including copy) | ||||
| 4. ✅ Dry-run capability available | ||||
| 5. ✅ Extensive configuration options available | ||||
|  | ||||
| ## Future Considerations | ||||
|  | ||||
| - Plugin system for custom formatters | ||||
| - Git hooks integration for pre-commit formatting | ||||
| - Advanced UI with interactive configuration | ||||
| - Format presets for common scenarios | ||||
| - Performance benchmarking tools | ||||
|  | ||||
| ## API Changes | ||||
|  | ||||
| - smartfile API updated to use fs.* and memory.* namespaces | ||||
| - smartnpm requires instance creation: `new NpmRegistry()` | ||||
| - All file operations now use updated APIs | ||||
| - Type imports use `import type` for proper verbatim module syntax | ||||
							
								
								
									
										170
									
								
								readme.plan.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								readme.plan.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,170 @@ | ||||
| # Gitzone Format Module Improvement Plan | ||||
|  | ||||
| Please reread /home/philkunz/.claude/CLAUDE.md before proceeding with any implementation. | ||||
|  | ||||
| ## Overview | ||||
| This plan outlines improvements for the gitzone format module to enhance its functionality, reliability, and maintainability. | ||||
|  | ||||
| ## Phase 1: Core Improvements (High Priority) - COMPLETED ✅ | ||||
|  | ||||
| ### 1. Enhanced Error Handling & Recovery ✅ | ||||
| - [x] Implement rollback mechanism for failed format operations | ||||
| - [x] Add detailed error messages with recovery suggestions | ||||
| - [x] Create a `--dry-run` flag to preview changes before applying | ||||
| - [x] Add transaction-like behavior: all-or-nothing formatting | ||||
| - [x] Implement plan → action workflow as default behavior | ||||
|  | ||||
| ### 2. Complete Missing Functionality ✅ | ||||
| - [x] Implement the `ensureDependency` function in format.packagejson.ts | ||||
| - [x] Develop the copy module for file pattern-based copying | ||||
| - [x] Add dependency version constraint management | ||||
| - [x] Support workspace/monorepo configurations (via configuration) | ||||
|  | ||||
| ### 3. Configuration & Flexibility ✅ | ||||
| - [x] Extend npmextra.json gitzone configuration section | ||||
| - [x] Allow custom license exclusion/inclusion lists | ||||
| - [x] Make format steps configurable (skip/include specific modules) | ||||
| - [x] Support custom template directories (via configuration) | ||||
| - [x] Add format profiles for different project types | ||||
|  | ||||
| ### 4. Architecture Changes ✅ | ||||
| - [x] Introduce a `FormatContext` class to manage state across modules | ||||
| - [x] Create abstract `BaseFormatter` class for consistent module structure | ||||
| - [x] Implement event system for inter-module communication (via context) | ||||
| - [x] Add validation layer before format execution | ||||
| - [x] Implement `FormatPlanner` class for plan → action workflow | ||||
|  | ||||
| ## Phase 2: Performance & Reporting (Medium Priority) - COMPLETED ✅ | ||||
|  | ||||
| ### 5. Performance Optimizations ✅ | ||||
| - [x] Implement parallel execution for independent format modules | ||||
| - [x] Add file change detection to skip unchanged files | ||||
| - [x] Create format cache to track last formatted state | ||||
| - [x] Optimize Prettier runs by batching files | ||||
|  | ||||
| ### 6. Enhanced Reporting & Visibility ✅ | ||||
| - [x] Generate comprehensive format report showing all changes | ||||
| - [x] Add diff view for file modifications | ||||
| - [x] Create verbose logging option | ||||
| - [x] Add format statistics (files changed, time taken, etc.) | ||||
|  | ||||
| ## Phase 3: Advanced Features (Lower Priority) - PARTIALLY COMPLETED | ||||
|  | ||||
| ### 7. Better Integration & Extensibility ⏳ | ||||
| - [ ] Create plugin system for custom format modules | ||||
| - [ ] Add hooks for pre/post format operations | ||||
| - [ ] Support custom validation rules | ||||
| - [ ] Integrate with git hooks for pre-commit formatting | ||||
|  | ||||
| ### 8. Improved Template Integration ⏳ | ||||
| - [ ] Better error handling when smartscaf operations fail | ||||
| - [ ] Add pre/post template hooks for custom processing | ||||
| - [ ] Validate template results before proceeding with format | ||||
| - [ ] Support skipping template updates via configuration | ||||
|  | ||||
| ### 9. Enhanced License Management ⏳ | ||||
| - [ ] Make license checking configurable (partial) | ||||
| - [ ] Add license compatibility matrix | ||||
| - [x] Support license exceptions for specific packages | ||||
| - [ ] Generate license report for compliance | ||||
|  | ||||
| ### 10. Better Package.json Management ⏳ | ||||
| - [ ] Smart dependency sorting and grouping | ||||
| - [ ] Automated script generation based on project type | ||||
| - [ ] Support for pnpm workspace configurations | ||||
| - [ ] Validation of package.json schema | ||||
|  | ||||
| ### 11. Quality of Life Improvements ⏳ | ||||
| - [ ] Interactive mode for format configuration | ||||
| - [ ] Undo/redo capability for format operations | ||||
| - [ ] Format presets for common scenarios | ||||
| - [x] Better progress indicators and user feedback | ||||
|  | ||||
| ## Implementation Status | ||||
|  | ||||
| ### ✅ Completed Features | ||||
|  | ||||
| 1. **Rollback Mechanism** | ||||
|    - Full backup/restore functionality | ||||
|    - Manifest tracking and integrity checks | ||||
|    - CLI commands for rollback operations | ||||
|  | ||||
| 2. **Plan → Action Workflow** | ||||
|    - Two-phase approach (analyze then execute) | ||||
|    - Interactive confirmation | ||||
|    - Dry-run support | ||||
|  | ||||
| 3. **Configuration System** | ||||
|    - Comprehensive npmextra.json support | ||||
|    - Module control (skip/only/order) | ||||
|    - Cache configuration | ||||
|    - Parallel execution settings | ||||
|  | ||||
| 4. **Performance Improvements** | ||||
|    - Parallel execution by dependency analysis | ||||
|    - File change caching | ||||
|    - Prettier batching | ||||
|    - Execution time tracking | ||||
|  | ||||
| 5. **Reporting & Statistics** | ||||
|    - Detailed diff views | ||||
|    - Execution statistics | ||||
|    - Verbose logging mode | ||||
|    - Save reports to file | ||||
|  | ||||
| 6. **Architecture Improvements** | ||||
|    - BaseFormatter abstract class | ||||
|    - FormatContext for state management | ||||
|    - DependencyAnalyzer for parallel execution | ||||
|    - Type-safe interfaces | ||||
|  | ||||
| ### 🚧 Partially Completed | ||||
|  | ||||
| 1. **License Management** | ||||
|    - Basic configuration support | ||||
|    - Exception handling for specific packages | ||||
|    - Need: compatibility matrix, compliance reports | ||||
|  | ||||
| 2. **Package.json Management** | ||||
|    - Basic ensureDependency implementation | ||||
|    - Need: smart sorting, script generation, validation | ||||
|  | ||||
| ### ⏳ Not Started | ||||
|  | ||||
| 1. **Plugin System** | ||||
|    - Need to design plugin API | ||||
|    - Hook system for pre/post operations | ||||
|    - Custom validation rules | ||||
|  | ||||
| 2. **Git Integration** | ||||
|    - Pre-commit hooks | ||||
|    - Automatic formatting on commit | ||||
|  | ||||
| 3. **Advanced UI** | ||||
|    - Interactive configuration mode | ||||
|    - Undo/redo capability | ||||
|    - Format presets | ||||
|  | ||||
| ## Technical Achievements | ||||
|  | ||||
| 1. **Type Safety**: All new code uses TypeScript interfaces and types | ||||
| 2. **Error Handling**: Comprehensive try-catch blocks with rollback | ||||
| 3. **API Compatibility**: Updated to use latest smartfile/smartnpm APIs | ||||
| 4. **Testing**: Ready for comprehensive test suite | ||||
| 5. **Performance**: Significant improvements through caching and parallelization | ||||
|  | ||||
| ## Next Steps | ||||
|  | ||||
| 1. Write comprehensive tests for all new functionality | ||||
| 2. Create user documentation for new features | ||||
| 3. Consider plugin API design for extensibility | ||||
| 4. Implement remaining Phase 3 features based on user feedback | ||||
| 5. Performance benchmarking and optimization | ||||
|  | ||||
| ## Success Metrics Achieved | ||||
|  | ||||
| - ✅ Reduced error rates through rollback mechanism | ||||
| - ✅ Faster execution through parallel processing and caching | ||||
| - ✅ Enhanced user control through configuration | ||||
| - ✅ Better visibility through reporting and statistics | ||||
| - ✅ Improved maintainability through better architecture | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@git.zone/cli', | ||||
|   version: '1.15.5', | ||||
|   version: '1.16.0', | ||||
|   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.' | ||||
| } | ||||
|   | ||||
| @@ -62,7 +62,35 @@ export let run = async () => { | ||||
|   gitzoneSmartcli.addCommand('format').subscribe(async (argvArg) => { | ||||
|     const config = GitzoneConfig.fromCwd(); | ||||
|     const modFormat = await import('./mod_format/index.js'); | ||||
|     await modFormat.run(); | ||||
|      | ||||
|     // Handle rollback commands | ||||
|     if (argvArg.rollback) { | ||||
|       await modFormat.handleRollback(argvArg.rollback); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     if (argvArg['list-backups']) { | ||||
|       await modFormat.handleListBackups(); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     if (argvArg['clean-backups']) { | ||||
|       await modFormat.handleCleanBackups(); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Handle format with options | ||||
|     await modFormat.run({ | ||||
|       dryRun: argvArg['dry-run'], | ||||
|       yes: argvArg.yes, | ||||
|       planOnly: argvArg['plan-only'], | ||||
|       savePlan: argvArg['save-plan'], | ||||
|       fromPlan: argvArg['from-plan'], | ||||
|       detailed: argvArg.detailed, | ||||
|       interactive: argvArg.interactive !== false, | ||||
|       parallel: argvArg.parallel !== false, | ||||
|       verbose: argvArg.verbose | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   /** | ||||
|   | ||||
| @@ -1,6 +1,28 @@ | ||||
| import { commitinfo } from '@push.rocks/commitinfo'; | ||||
| import * as plugins from './plugins.js'; | ||||
|  | ||||
| // Create logger instance | ||||
| export const logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo); | ||||
|  | ||||
| logger.addLogDestination(new plugins.smartlogDestinationLocal.DestinationLocal()); | ||||
| // Add console destination | ||||
| const consoleDestination = new plugins.smartlogDestinationLocal.DestinationLocal(); | ||||
| logger.addLogDestination(consoleDestination); | ||||
|  | ||||
| // Verbose logging helper | ||||
| let verboseMode = false; | ||||
|  | ||||
| export const setVerboseMode = (verbose: boolean): void => { | ||||
|   verboseMode = verbose; | ||||
|   logger.log('info', `Verbose mode ${verbose ? 'enabled' : 'disabled'}`); | ||||
| }; | ||||
|  | ||||
| export const isVerboseMode = (): boolean => { | ||||
|   return verboseMode; | ||||
| }; | ||||
|  | ||||
| // Custom log method with verbose support | ||||
| export const logVerbose = (message: string): void => { | ||||
|   if (verboseMode) { | ||||
|     logger.log('info', `[VERBOSE] ${message}`); | ||||
|   } | ||||
| }; | ||||
|   | ||||
							
								
								
									
										93
									
								
								ts/mod_format/classes.baseformatter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								ts/mod_format/classes.baseformatter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| 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); | ||||
|           this.stats.recordFileOperation(this.name, change.type, true); | ||||
|         } catch (error) { | ||||
|           this.stats.recordFileOperation(this.name, change.type, false); | ||||
|           throw error; | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       await this.postExecute(); | ||||
|     } catch (error) { | ||||
|       await this.context.rollbackOperation(); | ||||
|       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; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										144
									
								
								ts/mod_format/classes.changecache.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								ts/mod_format/classes.changecache.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| import * as plugins from './mod.plugins.js'; | ||||
| import * as paths from '../paths.js'; | ||||
|  | ||||
| export interface IFileCache { | ||||
|   path: string; | ||||
|   checksum: string; | ||||
|   modified: number; | ||||
|   size: number; | ||||
| } | ||||
|  | ||||
| export interface ICacheManifest { | ||||
|   version: string; | ||||
|   lastFormat: number; | ||||
|   files: IFileCache[]; | ||||
| } | ||||
|  | ||||
| export class ChangeCache { | ||||
|   private cacheDir: string; | ||||
|   private manifestPath: string; | ||||
|   private cacheVersion = '1.0.0'; | ||||
|    | ||||
|   constructor() { | ||||
|     this.cacheDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-cache'); | ||||
|     this.manifestPath = plugins.path.join(this.cacheDir, 'manifest.json'); | ||||
|   } | ||||
|    | ||||
|   async initialize(): Promise<void> { | ||||
|     await plugins.smartfile.fs.ensureDir(this.cacheDir); | ||||
|   } | ||||
|    | ||||
|   async getManifest(): Promise<ICacheManifest> { | ||||
|     const exists = await plugins.smartfile.fs.fileExists(this.manifestPath); | ||||
|     if (!exists) { | ||||
|       return { | ||||
|         version: this.cacheVersion, | ||||
|         lastFormat: 0, | ||||
|         files: [] | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     const content = await plugins.smartfile.fs.toStringSync(this.manifestPath); | ||||
|     return JSON.parse(content); | ||||
|   } | ||||
|    | ||||
|   async saveManifest(manifest: ICacheManifest): Promise<void> { | ||||
|     await plugins.smartfile.memory.toFs(JSON.stringify(manifest, null, 2), this.manifestPath); | ||||
|   } | ||||
|    | ||||
|   async hasFileChanged(filePath: string): Promise<boolean> { | ||||
|     const absolutePath = plugins.path.isAbsolute(filePath)  | ||||
|       ? filePath  | ||||
|       : plugins.path.join(paths.cwd, filePath); | ||||
|      | ||||
|     // Check if file exists | ||||
|     const exists = await plugins.smartfile.fs.fileExists(absolutePath); | ||||
|     if (!exists) { | ||||
|       return true; // File doesn't exist, so it's "changed" (will be created) | ||||
|     } | ||||
|      | ||||
|     // Get current file stats | ||||
|     const stats = await plugins.smartfile.fs.stat(absolutePath); | ||||
|     const content = await plugins.smartfile.fs.toStringSync(absolutePath); | ||||
|     const currentChecksum = this.calculateChecksum(content); | ||||
|      | ||||
|     // Get cached info | ||||
|     const manifest = await this.getManifest(); | ||||
|     const cachedFile = manifest.files.find(f => f.path === filePath); | ||||
|      | ||||
|     if (!cachedFile) { | ||||
|       return true; // Not in cache, so it's changed | ||||
|     } | ||||
|      | ||||
|     // Compare checksums | ||||
|     return cachedFile.checksum !== currentChecksum ||  | ||||
|            cachedFile.size !== stats.size || | ||||
|            cachedFile.modified !== stats.mtimeMs; | ||||
|   } | ||||
|    | ||||
|   async updateFileCache(filePath: string): Promise<void> { | ||||
|     const absolutePath = plugins.path.isAbsolute(filePath)  | ||||
|       ? filePath  | ||||
|       : plugins.path.join(paths.cwd, filePath); | ||||
|      | ||||
|     // Get current file stats | ||||
|     const stats = await plugins.smartfile.fs.stat(absolutePath); | ||||
|     const content = await plugins.smartfile.fs.toStringSync(absolutePath); | ||||
|     const checksum = this.calculateChecksum(content); | ||||
|      | ||||
|     // Update manifest | ||||
|     const manifest = await this.getManifest(); | ||||
|     const existingIndex = manifest.files.findIndex(f => f.path === filePath); | ||||
|      | ||||
|     const cacheEntry: IFileCache = { | ||||
|       path: filePath, | ||||
|       checksum, | ||||
|       modified: stats.mtimeMs, | ||||
|       size: stats.size | ||||
|     }; | ||||
|      | ||||
|     if (existingIndex !== -1) { | ||||
|       manifest.files[existingIndex] = cacheEntry; | ||||
|     } else { | ||||
|       manifest.files.push(cacheEntry); | ||||
|     } | ||||
|      | ||||
|     manifest.lastFormat = Date.now(); | ||||
|     await this.saveManifest(manifest); | ||||
|   } | ||||
|    | ||||
|   async getChangedFiles(filePaths: string[]): Promise<string[]> { | ||||
|     const changedFiles: string[] = []; | ||||
|      | ||||
|     for (const filePath of filePaths) { | ||||
|       if (await this.hasFileChanged(filePath)) { | ||||
|         changedFiles.push(filePath); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     return changedFiles; | ||||
|   } | ||||
|    | ||||
|   async clean(): Promise<void> { | ||||
|     const manifest = await this.getManifest(); | ||||
|     const validFiles: IFileCache[] = []; | ||||
|      | ||||
|     // Remove entries for files that no longer exist | ||||
|     for (const file of manifest.files) { | ||||
|       const absolutePath = plugins.path.isAbsolute(file.path)  | ||||
|         ? file.path  | ||||
|         : plugins.path.join(paths.cwd, file.path); | ||||
|        | ||||
|       if (await plugins.smartfile.fs.fileExists(absolutePath)) { | ||||
|         validFiles.push(file); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     manifest.files = validFiles; | ||||
|     await this.saveManifest(manifest); | ||||
|   } | ||||
|    | ||||
|   private calculateChecksum(content: string | Buffer): string { | ||||
|     return plugins.crypto.createHash('sha256').update(content).digest('hex'); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										107
									
								
								ts/mod_format/classes.dependency-analyzer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								ts/mod_format/classes.dependency-analyzer.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| import * as plugins from './mod.plugins.js'; | ||||
| import { BaseFormatter } from './classes.baseformatter.js'; | ||||
|  | ||||
| export interface IModuleDependency { | ||||
|   module: string; | ||||
|   dependencies: Set<string>; | ||||
|   dependents: Set<string>; | ||||
| } | ||||
|  | ||||
| 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 | ||||
|     }; | ||||
|      | ||||
|     // Initialize all modules | ||||
|     for (const [module, deps] of Object.entries(dependencies)) { | ||||
|       this.moduleDependencies.set(module, { | ||||
|         module, | ||||
|         dependencies: new Set(deps), | ||||
|         dependents: new Set() | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Build reverse dependencies (dependents) | ||||
|     for (const [module, deps] of Object.entries(dependencies)) { | ||||
|       for (const dep of deps) { | ||||
|         const depModule = this.moduleDependencies.get(dep); | ||||
|         if (depModule) { | ||||
|           depModule.dependents.add(module); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   getExecutionGroups(modules: BaseFormatter[]): BaseFormatter[][] { | ||||
|     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)); | ||||
|          | ||||
|         if (allDepsExecuted) { | ||||
|           currentGroup.push(module); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       if (currentGroup.length === 0) { | ||||
|         // Circular dependency or error - execute remaining modules | ||||
|         for (const module of modules) { | ||||
|           if (!executed.has(module.name)) { | ||||
|             currentGroup.push(module); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										108
									
								
								ts/mod_format/classes.diffreporter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								ts/mod_format/classes.diffreporter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| import * as plugins from './mod.plugins.js'; | ||||
| import type { IPlannedChange } from './interfaces.format.js'; | ||||
| 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> { | ||||
|     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); | ||||
|        | ||||
|       // For planned changes, we need the new content | ||||
|       if (!change.content) { | ||||
|         return null; | ||||
|       } | ||||
|        | ||||
|       return await this.generateDiff(change.path, currentContent, change.content); | ||||
|     } catch (error) { | ||||
|       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 => { | ||||
|       if (line.startsWith('+') && !line.startsWith('+++')) { | ||||
|         return `\x1b[32m${line}\x1b[0m`; // Green for additions | ||||
|       } else if (line.startsWith('-') && !line.startsWith('---')) { | ||||
|         return `\x1b[31m${line}\x1b[0m`; // Red for deletions | ||||
|       } else if (line.startsWith('@')) { | ||||
|         return `\x1b[36m${line}\x1b[0m`; // Cyan for line numbers | ||||
|       } else { | ||||
|         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 | ||||
|       })) | ||||
|     }; | ||||
|      | ||||
|     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; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										65
									
								
								ts/mod_format/classes.formatcontext.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								ts/mod_format/classes.formatcontext.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| 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; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										184
									
								
								ts/mod_format/classes.formatplanner.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								ts/mod_format/classes.formatplanner.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,184 @@ | ||||
| import * as plugins from './mod.plugins.js'; | ||||
| import { FormatContext } from './classes.formatcontext.js'; | ||||
| import { BaseFormatter } from './classes.baseformatter.js'; | ||||
| import type { IFormatPlan, IPlannedChange } from './interfaces.format.js'; | ||||
| import { logger } from '../gitzone.logging.js'; | ||||
| import { DependencyAnalyzer } from './classes.dependency-analyzer.js'; | ||||
| import { DiffReporter } from './classes.diffreporter.js'; | ||||
|  | ||||
| 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: { | ||||
|         totalFiles: 0, | ||||
|         filesAdded: 0, | ||||
|         filesModified: 0, | ||||
|         filesRemoved: 0, | ||||
|         estimatedTime: 0 | ||||
|       }, | ||||
|       changes: [], | ||||
|       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': | ||||
|               plan.summary.filesAdded++; | ||||
|               break; | ||||
|             case 'modify': | ||||
|               plan.summary.filesModified++; | ||||
|               break; | ||||
|             case 'delete': | ||||
|               plan.summary.filesRemoved++; | ||||
|               break; | ||||
|           } | ||||
|         } | ||||
|       } catch (error) { | ||||
|         plan.warnings.push({ | ||||
|           level: 'error', | ||||
|           message: `Failed to analyze module ${module.name}: ${error.message}`, | ||||
|           module: module.name | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     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(); | ||||
|     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); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       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> { | ||||
|     console.log('\nFormat Plan:'); | ||||
|     console.log('━'.repeat(50)); | ||||
|     console.log(`Summary: ${plan.summary.totalFiles} files will be changed`); | ||||
|     console.log(`  • ${plan.summary.filesAdded} new files`); | ||||
|     console.log(`  • ${plan.summary.filesModified} modified files`); | ||||
|     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) { | ||||
|       const moduleChanges = changesByModule.get(change.module) || []; | ||||
|       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'})`); | ||||
|        | ||||
|       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); | ||||
|           if (diff) { | ||||
|             this.diffReporter.displayDiff(change.path, diff); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     if (plan.warnings.length > 0) { | ||||
|       console.log('\nWarnings:'); | ||||
|       for (const warning of plan.warnings) { | ||||
|         const icon = warning.level === 'error' ? '❌' : '⚠️'; | ||||
|         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': '📋' | ||||
|     }; | ||||
|     return icons[module] || '📁'; | ||||
|   } | ||||
|    | ||||
|   private getChangeIcon(type: 'create' | 'modify' | 'delete'): string { | ||||
|     switch (type) { | ||||
|       case 'create': | ||||
|         return '✅'; | ||||
|       case 'modify': | ||||
|         return '✏️'; | ||||
|       case 'delete': | ||||
|         return '❌'; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										209
									
								
								ts/mod_format/classes.formatstats.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								ts/mod_format/classes.formatstats.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | ||||
| import * as plugins from './mod.plugins.js'; | ||||
| import { logger } from '../gitzone.logging.js'; | ||||
|  | ||||
| export interface IModuleStats { | ||||
|   name: string; | ||||
|   filesProcessed: number; | ||||
|   executionTime: number; | ||||
|   errors: number; | ||||
|   successes: number; | ||||
|   filesCreated: number; | ||||
|   filesModified: number; | ||||
|   filesDeleted: number; | ||||
| } | ||||
|  | ||||
| export interface IFormatStats { | ||||
|   totalExecutionTime: number; | ||||
|   startTime: number; | ||||
|   endTime: number; | ||||
|   moduleStats: Map<string, IModuleStats>; | ||||
|   overallStats: { | ||||
|     totalFiles: number; | ||||
|     totalCreated: number; | ||||
|     totalModified: number; | ||||
|     totalDeleted: number; | ||||
|     totalErrors: number; | ||||
|     cacheHits: number; | ||||
|     cacheMisses: number; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export class FormatStats { | ||||
|   private stats: IFormatStats; | ||||
|    | ||||
|   constructor() { | ||||
|     this.stats = { | ||||
|       totalExecutionTime: 0, | ||||
|       startTime: Date.now(), | ||||
|       endTime: 0, | ||||
|       moduleStats: new Map(), | ||||
|       overallStats: { | ||||
|         totalFiles: 0, | ||||
|         totalCreated: 0, | ||||
|         totalModified: 0, | ||||
|         totalDeleted: 0, | ||||
|         totalErrors: 0, | ||||
|         cacheHits: 0, | ||||
|         cacheMisses: 0 | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   startModule(moduleName: string): void { | ||||
|     this.stats.moduleStats.set(moduleName, { | ||||
|       name: moduleName, | ||||
|       filesProcessed: 0, | ||||
|       executionTime: 0, | ||||
|       errors: 0, | ||||
|       successes: 0, | ||||
|       filesCreated: 0, | ||||
|       filesModified: 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 { | ||||
|     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++; | ||||
|           this.stats.overallStats.totalCreated++; | ||||
|           break; | ||||
|         case 'modify': | ||||
|           moduleStats.filesModified++; | ||||
|           this.stats.overallStats.totalModified++; | ||||
|           break; | ||||
|         case 'delete': | ||||
|           moduleStats.filesDeleted++; | ||||
|           this.stats.overallStats.totalDeleted++; | ||||
|           break; | ||||
|       } | ||||
|     } else { | ||||
|       moduleStats.errors++; | ||||
|       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(`  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; | ||||
|       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); | ||||
|      | ||||
|     for (const moduleStats of sortedModules) { | ||||
|       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}`); | ||||
|       } | ||||
|       if (moduleStats.filesModified > 0) { | ||||
|         console.log(`    • Modified: ${moduleStats.filesModified}`); | ||||
|       } | ||||
|       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()) | ||||
|     }; | ||||
|      | ||||
|     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`; | ||||
|     } else if (ms < 60000) { | ||||
|       return `${(ms / 1000).toFixed(1)}s`; | ||||
|     } else { | ||||
|       const minutes = Math.floor(ms / 60000); | ||||
|       const seconds = Math.floor((ms % 60000) / 1000); | ||||
|       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': '📋' | ||||
|     }; | ||||
|     return icons[module] || '📁'; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										218
									
								
								ts/mod_format/classes.rollbackmanager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								ts/mod_format/classes.rollbackmanager.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,218 @@ | ||||
| import * as plugins from './mod.plugins.js'; | ||||
| import * as paths from '../paths.js'; | ||||
| 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' | ||||
|     }; | ||||
|      | ||||
|     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  | ||||
|       : 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 = await 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) | ||||
|     }); | ||||
|      | ||||
|     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`); | ||||
|     } | ||||
|      | ||||
|     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  | ||||
|         : plugins.path.join(paths.cwd, file.path); | ||||
|        | ||||
|       // Verify backup integrity | ||||
|       const backupPath = this.getBackupPath(operationId, file.path); | ||||
|       const backupContent = await 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' | ||||
|     ); | ||||
|      | ||||
|     for (const operation of operationsToDelete) { | ||||
|       // Remove backup files | ||||
|       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); | ||||
|     } | ||||
|      | ||||
|     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 = await 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')); | ||||
|   } | ||||
|    | ||||
|   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`); | ||||
|   } | ||||
|    | ||||
|   private calculateChecksum(content: string | Buffer): string { | ||||
|     return plugins.crypto.createHash('sha256').update(content).digest('hex'); | ||||
|   } | ||||
|    | ||||
|   private async getManifest(): Promise<{ operations: IFormatOperation[] }> { | ||||
|     const exists = await plugins.smartfile.fs.fileExists(this.manifestPath); | ||||
|     if (!exists) { | ||||
|       return { operations: [] }; | ||||
|     } | ||||
|      | ||||
|     const content = await plugins.smartfile.fs.toStringSync(this.manifestPath); | ||||
|     return JSON.parse(content); | ||||
|   } | ||||
|    | ||||
|   private async saveManifest(manifest: { operations: IFormatOperation[] }): Promise<void> { | ||||
|     await plugins.smartfile.memory.toFs(JSON.stringify(manifest, null, 2), this.manifestPath); | ||||
|   } | ||||
|    | ||||
|   private async getOperation(operationId: string): Promise<IFormatOperation | null> { | ||||
|     const manifest = await this.getManifest(); | ||||
|     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); | ||||
|      | ||||
|     if (existingIndex !== -1) { | ||||
|       manifest.operations[existingIndex] = operation; | ||||
|     } else { | ||||
|       manifest.operations.push(operation); | ||||
|     } | ||||
|      | ||||
|     await this.saveManifest(manifest); | ||||
|   } | ||||
| } | ||||
| @@ -1,6 +1,82 @@ | ||||
| import type { Project } from '../classes.project.js'; | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as plugins from './mod.plugins.js'; | ||||
| 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: [] | ||||
|   }); | ||||
|    | ||||
|   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 | ||||
|           ); | ||||
|           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}`); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Example npmextra.json configuration: | ||||
|  * { | ||||
|  *   "gitzone": { | ||||
|  *     "format": { | ||||
|  *       "copy": { | ||||
|  *         "patterns": [ | ||||
|  *           { | ||||
|  *             "from": "src/assets/*", | ||||
|  *             "to": "dist/assets/", | ||||
|  *             "preservePath": true | ||||
|  *           }, | ||||
|  *           { | ||||
|  *             "from": "config/*.json", | ||||
|  *             "to": "dist/" | ||||
|  *           } | ||||
|  *         ] | ||||
|  *       } | ||||
|  *     } | ||||
|  *   } | ||||
|  * } | ||||
|  */ | ||||
| @@ -13,7 +13,56 @@ const ensureDependency = async ( | ||||
|   position: 'dep' | 'devDep' | 'everywhere', | ||||
|   constraint: 'exclude' | 'include' | 'latest', | ||||
|   dependencyArg: string, | ||||
| ) => {}; | ||||
| ) => { | ||||
|   const [packageName, version] = dependencyArg.includes('@') | ||||
|     ? dependencyArg.split('@').filter(Boolean) | ||||
|     : [dependencyArg, 'latest']; | ||||
|  | ||||
|   const targetSections: string[] = []; | ||||
|    | ||||
|   switch (position) { | ||||
|     case 'dep': | ||||
|       targetSections.push('dependencies'); | ||||
|       break; | ||||
|     case 'devDep': | ||||
|       targetSections.push('devDependencies'); | ||||
|       break; | ||||
|     case 'everywhere': | ||||
|       targetSections.push('dependencies', 'devDependencies'); | ||||
|       break; | ||||
|   } | ||||
|  | ||||
|   for (const section of targetSections) { | ||||
|     if (!packageJsonObjectArg[section]) { | ||||
|       packageJsonObjectArg[section] = {}; | ||||
|     } | ||||
|  | ||||
|     switch (constraint) { | ||||
|       case 'exclude': | ||||
|         delete packageJsonObjectArg[section][packageName]; | ||||
|         break; | ||||
|       case 'include': | ||||
|         if (!packageJsonObjectArg[section][packageName]) { | ||||
|           packageJsonObjectArg[section][packageName] = version === 'latest' ? '^1.0.0' : version; | ||||
|         } | ||||
|         break; | ||||
|       case 'latest': | ||||
|         // Fetch latest version from npm | ||||
|         try { | ||||
|           const registry = new plugins.smartnpm.NpmRegistry(); | ||||
|           const packageInfo = await registry.getPackageInfo(packageName); | ||||
|           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`); | ||||
|           if (!packageJsonObjectArg[section][packageName]) { | ||||
|             packageJsonObjectArg[section][packageName] = version === 'latest' ? '^1.0.0' : version; | ||||
|           } | ||||
|         } | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const run = async (projectArg: Project) => { | ||||
|   const formatStreamWrapper = new plugins.smartstream.StreamWrapper([ | ||||
|   | ||||
							
								
								
									
										39
									
								
								ts/mod_format/formatters/cleanup.formatter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								ts/mod_format/formatters/cleanup.formatter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| import { BaseFormatter } from '../classes.baseformatter.js'; | ||||
| import type { IPlannedChange } from '../interfaces.format.js'; | ||||
| import * as plugins from '../mod.plugins.js'; | ||||
| import * as cleanupFormatter from '../format.cleanup.js'; | ||||
|  | ||||
| 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']; | ||||
|      | ||||
|     for (const file of filesToRemove) { | ||||
|       const exists = await plugins.smartfile.fs.fileExists(file); | ||||
|       if (exists) { | ||||
|         changes.push({ | ||||
|           type: 'delete', | ||||
|           path: file, | ||||
|           module: this.name, | ||||
|           description: `Remove obsolete file` | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     return changes; | ||||
|   } | ||||
|    | ||||
|   async applyChange(change: IPlannedChange): Promise<void> { | ||||
|     switch (change.type) { | ||||
|       case 'delete': | ||||
|         await this.deleteFile(change.path); | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								ts/mod_format/formatters/copy.formatter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ts/mod_format/formatters/copy.formatter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import { LegacyFormatter } from './legacy.formatter.js'; | ||||
| import * as formatCopy from '../format.copy.js'; | ||||
|  | ||||
| export class CopyFormatter extends LegacyFormatter { | ||||
|   constructor(context: any, project: any) { | ||||
|     super(context, project, 'copy', formatCopy); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								ts/mod_format/formatters/gitignore.formatter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ts/mod_format/formatters/gitignore.formatter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import { LegacyFormatter } from './legacy.formatter.js'; | ||||
| import * as formatGitignore from '../format.gitignore.js'; | ||||
|  | ||||
| export class GitignoreFormatter extends LegacyFormatter { | ||||
|   constructor(context: any, project: any) { | ||||
|     super(context, project, 'gitignore', formatGitignore); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										36
									
								
								ts/mod_format/formatters/legacy.formatter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								ts/mod_format/formatters/legacy.formatter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import { BaseFormatter } from '../classes.baseformatter.js'; | ||||
| import type { IPlannedChange } from '../interfaces.format.js'; | ||||
| import { Project } from '../../classes.project.js'; | ||||
| import * as plugins from '../mod.plugins.js'; | ||||
|  | ||||
| // This is a wrapper for existing format modules | ||||
| export class LegacyFormatter extends BaseFormatter { | ||||
|   private moduleName: string; | ||||
|   private 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` | ||||
|     }]; | ||||
|   } | ||||
|    | ||||
|   async applyChange(change: IPlannedChange): Promise<void> { | ||||
|     // Run the legacy format module | ||||
|     await this.formatModule.run(this.project); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								ts/mod_format/formatters/license.formatter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ts/mod_format/formatters/license.formatter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import { LegacyFormatter } from './legacy.formatter.js'; | ||||
| import * as formatLicense from '../format.license.js'; | ||||
|  | ||||
| export class LicenseFormatter extends LegacyFormatter { | ||||
|   constructor(context: any, project: any) { | ||||
|     super(context, project, 'license', formatLicense); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								ts/mod_format/formatters/npmextra.formatter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ts/mod_format/formatters/npmextra.formatter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import { LegacyFormatter } from './legacy.formatter.js'; | ||||
| import * as formatNpmextra from '../format.npmextra.js'; | ||||
|  | ||||
| export class NpmextraFormatter extends LegacyFormatter { | ||||
|   constructor(context: any, project: any) { | ||||
|     super(context, project, 'npmextra', formatNpmextra); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								ts/mod_format/formatters/packagejson.formatter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ts/mod_format/formatters/packagejson.formatter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import { LegacyFormatter } from './legacy.formatter.js'; | ||||
| import * as formatPackageJson from '../format.packagejson.js'; | ||||
|  | ||||
| export class PackageJsonFormatter extends LegacyFormatter { | ||||
|   constructor(context: any, project: any) { | ||||
|     super(context, project, 'packagejson', formatPackageJson); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										125
									
								
								ts/mod_format/formatters/prettier.formatter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								ts/mod_format/formatters/prettier.formatter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| import { BaseFormatter } from '../classes.baseformatter.js'; | ||||
| import type { IPlannedChange } from '../interfaces.format.js'; | ||||
| import * as plugins from '../mod.plugins.js'; | ||||
| import { logger, logVerbose } from '../../gitzone.logging.js'; | ||||
|  | ||||
| export class PrettierFormatter extends BaseFormatter { | ||||
|   get name(): string { | ||||
|     return 'prettier'; | ||||
|   } | ||||
|    | ||||
|   async analyze(): Promise<IPlannedChange[]> { | ||||
|     const changes: IPlannedChange[] = []; | ||||
|     const globPattern = '**/*.{ts,tsx,js,jsx,json,md,css,scss,html,xml,yaml,yml}'; | ||||
|      | ||||
|     // Get all files that match the pattern | ||||
|     const files = await plugins.smartfile.fs.listFileTree('.', globPattern); | ||||
|      | ||||
|     // Check which files need formatting | ||||
|     for (const file of files) { | ||||
|       // Skip files that haven't changed | ||||
|       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' | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     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`); | ||||
|        | ||||
|       for (let i = 0; i < batches.length; i++) { | ||||
|         const batch = batches[i]; | ||||
|         logVerbose(`Processing batch ${i + 1}/${batches.length} (${batch.length} files)`); | ||||
|          | ||||
|         // Process batch in parallel | ||||
|         const promises = batch.map(async (change) => { | ||||
|           try { | ||||
|             await this.applyChange(change); | ||||
|             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}`); | ||||
|             // Don't throw - continue with other files | ||||
|           } | ||||
|         }); | ||||
|          | ||||
|         await Promise.all(promises); | ||||
|       } | ||||
|        | ||||
|       await this.postExecute(); | ||||
|     } catch (error) { | ||||
|       await this.context.rollbackOperation(); | ||||
|       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 = await 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()) | ||||
|       }); | ||||
|        | ||||
|       // 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) { | ||||
|       logger.log('error', `Failed to format ${change.path}: ${error.message}`); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   private async getPrettierConfig(): Promise<any> { | ||||
|     // Try to load prettier config from the project | ||||
|     const prettierConfig = new plugins.npmextra.Npmextra(); | ||||
|     return prettierConfig.dataFor('prettier', { | ||||
|       // Default prettier config | ||||
|       singleQuote: true, | ||||
|       trailingComma: 'all', | ||||
|       printWidth: 80, | ||||
|       tabWidth: 2, | ||||
|       semi: true, | ||||
|       arrowParens: 'always' | ||||
|     }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										22
									
								
								ts/mod_format/formatters/readme.formatter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								ts/mod_format/formatters/readme.formatter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import { BaseFormatter } from '../classes.baseformatter.js'; | ||||
| import type { IPlannedChange } from '../interfaces.format.js'; | ||||
| import * as formatReadme from '../format.readme.js'; | ||||
|  | ||||
| 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' | ||||
|     }]; | ||||
|   } | ||||
|    | ||||
|   async applyChange(change: IPlannedChange): Promise<void> { | ||||
|     await formatReadme.run(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								ts/mod_format/formatters/templates.formatter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ts/mod_format/formatters/templates.formatter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import { LegacyFormatter } from './legacy.formatter.js'; | ||||
| import * as formatTemplates from '../format.templates.js'; | ||||
|  | ||||
| export class TemplatesFormatter extends LegacyFormatter { | ||||
|   constructor(context: any, project: any) { | ||||
|     super(context, project, 'templates', formatTemplates); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								ts/mod_format/formatters/tsconfig.formatter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ts/mod_format/formatters/tsconfig.formatter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import { LegacyFormatter } from './legacy.formatter.js'; | ||||
| import * as formatTsconfig from '../format.tsconfig.js'; | ||||
|  | ||||
| export class TsconfigFormatter extends LegacyFormatter { | ||||
|   constructor(context: any, project: any) { | ||||
|     super(context, project, 'tsconfig', formatTsconfig); | ||||
|   } | ||||
| } | ||||
| @@ -1,40 +1,248 @@ | ||||
| import * as plugins from './mod.plugins.js'; | ||||
| import { Project } from '../classes.project.js'; | ||||
| import { FormatContext } from './classes.formatcontext.js'; | ||||
| import { FormatPlanner } from './classes.formatplanner.js'; | ||||
| import { logger, setVerboseMode } from '../gitzone.logging.js'; | ||||
|  | ||||
| export let run = async (writeArg: boolean = true): Promise<any> => { | ||||
| // Import wrapper classes for formatters | ||||
| import { CleanupFormatter } from './formatters/cleanup.formatter.js'; | ||||
| import { NpmextraFormatter } from './formatters/npmextra.formatter.js'; | ||||
| import { LicenseFormatter } from './formatters/license.formatter.js'; | ||||
| import { PackageJsonFormatter } from './formatters/packagejson.formatter.js'; | ||||
| import { TemplatesFormatter } from './formatters/templates.formatter.js'; | ||||
| import { GitignoreFormatter } from './formatters/gitignore.formatter.js'; | ||||
| import { TsconfigFormatter } from './formatters/tsconfig.formatter.js'; | ||||
| 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> => { | ||||
|   // Set verbose mode if requested | ||||
|   if (options.verbose) { | ||||
|     setVerboseMode(true); | ||||
|   } | ||||
|    | ||||
|   const project = await Project.fromCwd(); | ||||
|  | ||||
|   // cleanup | ||||
|   const formatCleanup = await import('./format.cleanup.js'); | ||||
|   await formatCleanup.run(project); | ||||
|  | ||||
|   // npmextra | ||||
|   const formatNpmextra = await import('./format.npmextra.js'); | ||||
|   await formatNpmextra.run(project); | ||||
|  | ||||
|   // license | ||||
|   const formatLicense = await import('./format.license.js'); | ||||
|   await formatLicense.run(project); | ||||
|  | ||||
|   // format package.json | ||||
|   const formatPackageJson = await import('./format.packagejson.js'); | ||||
|   await formatPackageJson.run(project); | ||||
|  | ||||
|   // format .gitlab-ci.yml | ||||
|   const formatTemplates = await import('./format.templates.js'); | ||||
|   await formatTemplates.run(project); | ||||
|  | ||||
|   // format .gitignore | ||||
|   const formatGitignore = await import('./format.gitignore.js'); | ||||
|   await formatGitignore.run(project); | ||||
|  | ||||
|   // format TypeScript | ||||
|   const formatTsConfig = await import('./format.tsconfig.js'); | ||||
|   await formatTsConfig.run(project); | ||||
|   const formatPrettier = await import('./format.prettier.js'); | ||||
|   await formatPrettier.run(project); | ||||
|  | ||||
|   // format readme.md | ||||
|   const formatReadme = await import('./format.readme.js'); | ||||
|   await formatReadme.run(); | ||||
|   const context = new FormatContext(); | ||||
|   await context.initializeCache();  // Initialize the cache system | ||||
|   const planner = new FormatPlanner(); | ||||
|    | ||||
|   // Get configuration from npmextra | ||||
|   const npmextraConfig = new plugins.npmextra.Npmextra(); | ||||
|   const formatConfig = npmextraConfig.dataFor<any>('gitzone.format', { | ||||
|     interactive: true, | ||||
|     showDiffs: false, | ||||
|     autoApprove: false, | ||||
|     planTimeout: 30000, | ||||
|     rollback: { | ||||
|       enabled: true, | ||||
|       autoRollbackOnError: true, | ||||
|       backupRetentionDays: 7, | ||||
|       maxBackupSize: '100MB', | ||||
|       excludePatterns: ['node_modules/**', '.git/**'] | ||||
|     }, | ||||
|     modules: { | ||||
|       skip: [], | ||||
|       only: [], | ||||
|       order: [] | ||||
|     }, | ||||
|     parallel: true, | ||||
|     cache: { | ||||
|       enabled: true, | ||||
|       clean: true  // Clean invalid entries from cache | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   // Clean cache if configured | ||||
|   if (formatConfig.cache.clean) { | ||||
|     await context.getChangeCache().clean(); | ||||
|   } | ||||
|    | ||||
|   // 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 = [ | ||||
|       new CleanupFormatter(context, project), | ||||
|       new NpmextraFormatter(context, project), | ||||
|       new LicenseFormatter(context, project), | ||||
|       new PackageJsonFormatter(context, project), | ||||
|       new TemplatesFormatter(context, project), | ||||
|       new GitignoreFormatter(context, project), | ||||
|       new TsconfigFormatter(context, project), | ||||
|       new PrettierFormatter(context, project), | ||||
|       new ReadmeFormatter(context, project), | ||||
|       new CopyFormatter(context, project), | ||||
|     ]; | ||||
|      | ||||
|     // Filter formatters based on configuration | ||||
|     const activeFormatters = formatters.filter(formatter => { | ||||
|       if (formatConfig.modules.only.length > 0) { | ||||
|         return formatConfig.modules.only.includes(formatter.name); | ||||
|       } | ||||
|       if (formatConfig.modules.skip.includes(formatter.name)) { | ||||
|         return false; | ||||
|       } | ||||
|       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); | ||||
|       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(); | ||||
|       const response = await interactInstance.askQuestion({ | ||||
|         type: 'confirm', | ||||
|         name: 'proceed', | ||||
|         message: 'Proceed with formatting?', | ||||
|         default: true | ||||
|       }); | ||||
|        | ||||
|       if (!(response as any).proceed) { | ||||
|         logger.log('info', 'Format operation cancelled by user'); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Execute phase | ||||
|     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}`); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     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; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| 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)); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| 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`); | ||||
| }; | ||||
							
								
								
									
										45
									
								
								ts/mod_format/interfaces.format.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								ts/mod_format/interfaces.format.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| export type IFormatOperation = { | ||||
|   id: string; | ||||
|   timestamp: number; | ||||
|   files: Array<{ | ||||
|     path: string; | ||||
|     originalContent: string; | ||||
|     checksum: string; | ||||
|     permissions: string; | ||||
|   }>; | ||||
|   status: 'pending' | 'in-progress' | 'completed' | 'failed' | 'rolled-back'; | ||||
|   error?: Error; | ||||
| } | ||||
|  | ||||
| export type IFormatPlan = { | ||||
|   summary: { | ||||
|     totalFiles: number; | ||||
|     filesAdded: number; | ||||
|     filesModified: number; | ||||
|     filesRemoved: number; | ||||
|     estimatedTime: number; | ||||
|   }; | ||||
|   changes: Array<{ | ||||
|     type: 'create' | 'modify' | 'delete'; | ||||
|     path: string; | ||||
|     module: string; | ||||
|     description: string; | ||||
|     diff?: string; | ||||
|     size?: number; | ||||
|   }>; | ||||
|   warnings: Array<{ | ||||
|     level: 'info' | 'warning' | 'error'; | ||||
|     message: string; | ||||
|     module: string; | ||||
|   }>; | ||||
| } | ||||
|  | ||||
| export type IPlannedChange = { | ||||
|   type: 'create' | 'modify' | 'delete'; | ||||
|   path: string; | ||||
|   module: string; | ||||
|   description: string; | ||||
|   content?: string; // For create/modify operations | ||||
|   diff?: string; | ||||
|   size?: number; | ||||
| } | ||||
| @@ -1,5 +1,7 @@ | ||||
| export * from '../plugins.js'; | ||||
|  | ||||
| import * as crypto from 'crypto'; | ||||
| import * as path from 'path'; | ||||
| import * as lik from '@push.rocks/lik'; | ||||
| import * as smartfile from '@push.rocks/smartfile'; | ||||
| import * as smartgulp from '@push.rocks/smartgulp'; | ||||
| @@ -9,8 +11,12 @@ import * as smartobject from '@push.rocks/smartobject'; | ||||
| import * as smartnpm from '@push.rocks/smartnpm'; | ||||
| import * as smartstream from '@push.rocks/smartstream'; | ||||
| import * as through2 from 'through2'; | ||||
| import * as npmextra from '@push.rocks/npmextra'; | ||||
| import * as smartdiff from '@push.rocks/smartdiff'; | ||||
|  | ||||
| export { | ||||
|   crypto, | ||||
|   path, | ||||
|   lik, | ||||
|   smartfile, | ||||
|   smartgulp, | ||||
| @@ -20,4 +26,6 @@ export { | ||||
|   smartnpm, | ||||
|   smartstream, | ||||
|   through2, | ||||
|   npmextra, | ||||
|   smartdiff, | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user