Compare commits

..

6 Commits

Author SHA1 Message Date
b320af0b61 1.16.10
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-08 09:46:34 +00:00
49e1ee1f39 fix(format): Improve concurrency control in caching and rollback modules, refine gitignore custom section handling, and enhance Prettier file processing. 2025-08-08 09:46:34 +00:00
cef31cf1ff 1.16.9
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-08 06:50:58 +00:00
74ecdde1ac fix(format): Improve concurrency control in cache and rollback modules, refine gitignore custom section handling, and enhance Prettier file processing 2025-08-08 06:50:58 +00:00
74a8229e43 1.16.8
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-08 06:25:40 +00:00
859cbc733d fix(format): Improve concurrency control in cache and rollback management with mutex locking and refine formatting details 2025-08-08 06:25:40 +00:00
44 changed files with 1037 additions and 782 deletions

4
.gitignore vendored
View File

@@ -16,4 +16,6 @@ node_modules/
dist/
dist_*/
#------# custom
#------# custom
.serena
test-output.json

View File

@@ -1,6 +1,31 @@
# Changelog
## 2025-08-08 - 1.16.10 - fix(format)
Improve concurrency control in caching and rollback modules, refine gitignore custom section handling, and enhance Prettier file processing.
- Added mutex locking in ChangeCache and RollbackManager to prevent race conditions during manifest updates
- Updated gitignore logic to detect and preserve custom sections
- Enhanced Prettier batching and file formatting for better performance
## 2025-08-08 - 1.16.9 - fix(format)
Improve concurrency control in cache and rollback modules, refine gitignore custom section handling, and enhance Prettier file processing
- Added mutex locking in ChangeCache and RollbackManager to prevent race conditions during manifest updates
- Updated gitignore logic to detect and preserve existing custom sections from various markers
- Simplified Prettier formatter to process files sequentially, skip files without extensions, and log detailed status
- Minor refactoring in base formatter and tsconfig file updates for improved reliability
## 2025-08-08 - 1.16.8 - fix(format)
Improve concurrency control in cache and rollback management with mutex locking and refine formatting details
- Added 'withMutex' functions in ChangeCache and RollbackManager to synchronize file I/O operations
- Introduced static mutex maps to prevent race conditions during manifest updates
- Fixed minor formatting issues in commit info and package.json
## 2025-08-08 - 1.16.7 - fix(core)
Improve formatting, logging, and rollback integrity in core modules
- Add .claude/settings.local.json with defined permissions for allowed commands
@@ -9,6 +34,7 @@ Improve formatting, logging, and rollback integrity in core modules
- Enhance logging messages and overall code clarity in CLI and commit modules
## 2025-08-08 - 1.16.6 - fix(changecache)
Improve cache manifest validation and atomic file writes; add local settings and overrides
- Add manifest structure validation and default fallback in getManifest
@@ -18,6 +44,7 @@ Improve cache manifest validation and atomic file writes; add local settings and
- Add an empty assets/overrides.json file for future overrides
## 2025-08-08 - 1.16.5 - fix(prettier)
Improve file selection in Prettier formatter, remove legacy package overrides, and update CI template indentation
- Added .claude/settings.local.json with updated permission settings for local commands
@@ -26,12 +53,14 @@ Improve file selection in Prettier formatter, remove legacy package overrides, a
- Refined Prettier formatter logic by defining include directories, root config files, and filtering duplicates instead of manual exclusion
## 2025-08-08 - 1.16.4 - fix(prettier)
Improve file exclusion in the Prettier formatter to skip unnecessary files and directories.
- Added exclusion patterns for node_modules, .git, dist, .nogit, coverage, .nyc_output, vendor, bower_components, jspm_packages, and minified files.
- Optimized filtering logic to ensure only valid files are processed.
## 2025-08-08 - 1.16.3 - fix(changecache/prettier)
Skip directories during file processing to prevent errors in changecache and prettier formatting
- Removed unnecessary await on synchronous file reads in changecache
@@ -39,18 +68,21 @@ Skip directories during file processing to prevent errors in changecache and pre
- Filtered out directories in prettier formatter to avoid processing non-files
## 2025-08-07 - 1.16.2 - fix(format)
Fix format command confirmation prompt to correctly check user response
- Fixed bug where format command always showed "cancelled" even when user confirmed
- Changed response check from `response.proceed` to `response.value` for SmartInteract compatibility
## 2025-08-04 - 1.16.1 - fix(package/config)
Move smartdiff dependency to runtime and add local bash permissions settings
- Moved '@push.rocks/smartdiff' from devDependencies to dependencies in package.json
- Added .claude/settings.local.json with allowed bash commands (grep, mkdir, find, ls)
## 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
@@ -61,12 +93,14 @@ Enhance format module with rollback, diff reporting, and improved parallel execu
- 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
- Bumped @git.zone/tsdoc from ^1.4.5 to ^1.5.0
- Bumped @types/node from ^22.15.17 to ^22.15.18
## 2025-05-13 - 1.15.4 - fix(package.json)
Update dependency versions: bump @git.zone/tsdoc, @push.rocks/lik, @push.rocks/smartlog, and @types/node to their latest releases
- Upgrade @git.zone/tsdoc from ^1.4.4 to ^1.4.5
@@ -75,6 +109,7 @@ Update dependency versions: bump @git.zone/tsdoc, @push.rocks/lik, @push.rocks/s
- Upgrade @types/node from ^22.14.1 to ^22.15.17
## 2025-04-15 - 1.15.3 - fix(deps)
update dependency versions and improve website template variable handling
- Bumped @git.zone/tsbuild from ^2.2.1 to ^2.3.2 and @types/node to ^22.14.1
@@ -82,56 +117,65 @@ update dependency versions and improve website template variable handling
- Refactored website template update to correctly supply variables with added logging
## 2025-04-15 - 1.15.2 - fix(website_update)
Await supplyVariables call in website update template
- Changed website template update to properly await the supplyVariables method
- Ensured asynchronous consistency in updating website template variables
## 2025-04-15 - 1.15.1 - fix(cli)
Refresh internal CLI tooling and configuration for consistency.
## 2025-04-15 - 1.15.0 - feat(config/template)
Add assetbrokerUrl and legalUrl fields to module config and update website template to supply these values
- Added assetbrokerUrl and legalUrl properties in ts/classes.gitzoneconfig.ts
- Updated ts/mod_format/format.templates.ts to pass assetbrokerUrl and legalUrl to website template
## 2025-04-15 - 1.14.1 - fix(package.json)
Add packageManager field to specify pnpm version for consistent package management
- Inserted packageManager property in package.json with pnpm version info to ensure reproducible dependency installs
## 2025-04-15 - 1.14.0 - feat(tsconfig_update)
Add runafter directive to trigger gitzone format after tsconfig update
- Added runafter configuration in assets/templates/tsconfig_update/.smartscaf.yml to automate formatting task
## 2025-03-07 - 1.13.1 - fix(cli)
Improve commit message logging
- Updated logging to display recommended next commit details.
- Enabled interactive prompt for choosing commit type and scope.
## 2025-02-28 - 1.13.0 - feat(templates)
Updated and added new TypeScript template files for npm projects
- Added new paths.ts and plugins.ts template files for npm projects.
- Removed outdated some.plugins.ts template file.
## 2025-02-25 - 1.12.8 - fix(metadata)
Updated package and npmextra json description and keywords for enhanced development workflow clarity
- Updated the description in package.json to focus on project setup and management.
- Aligned the keywords in both package.json and npmextra.json to include more relevant terms such as gitzone utilities, template management, and CI/CD.
## 2025-02-25 - 1.12.7 - fix(meta)
Fix issues in project metadata and configuration.
- Updated package metadata to ensure accurate project description and licensing.
- Ensured npm access level configuration consistency within npmextra.json.
## 2025-02-25 - 1.12.7 - fix(ci)
Updated dependencies and added CI/CD workflows.
- Updated several dependencies in package.json for compatibility and security.
@@ -140,6 +184,7 @@ Updated dependencies and added CI/CD workflows.
- Ensured consistent formatting with Prettier and TypeScript configurations.
## 2025-01-29 - 1.12.6 - fix(project)
Minor fixes and cleanup
- Removed outdated pages/ directory entry in .gitignore.
@@ -148,6 +193,7 @@ Minor fixes and cleanup
- Fixed formatting issues across various TypeScript files.
## 2025-01-29 - 1.12.5 - fix(cli)
Initial implementation of CLI utility with project management features
- Integration of various plugins for logging, command-line interactions, and project management.
@@ -155,34 +201,40 @@ Initial implementation of CLI utility with project management features
- Implement commands for packaging, versioning, and deprecating npm packages.
## 2025-01-29 - 1.12.2 - fix(format)
Add overrides for peek-readable in package.json formatting
- Added a URL correction in the packageJson repository information.
- Introduced support for pnpm overrides by including an `overrides.json` file.
## 2025-01-18 - 1.12.1 - fix(dependencies)
Update various package dependencies and Dockerfile base image
- Updated Dockerfile base image from 'alpinenpmci' to 'alpine_npmci'.
- Upgraded @git.zone/tsbuild, @git.zone/tsrun, @git.zone/tsdoc, and other dependencies to their latest versions.
## 2025-01-17 - 1.12.0 - feat(build)
Update TypeScript configuration to support emit decorator metadata
- Added emitDecoratorMetadata to the tsconfig.json template in assets/templates/tsconfig_update.
## 2025-01-08 - 1.11.0 - feat(cli)
Add Docker command for cleaning up Docker system and extend deprecation command for multiple registries
- Added a new command 'docker' to handle Docker system cleanup operations.
- Improved the 'deprecate' command to support deprecating packages across multiple npm registry URLs.
## 2025-01-01 - 1.10.10 - fix(templates)
Corrected typo in template file comment
- Fixed repeated comment in the template file for services under 'assets/templates/service/ts/some.plugins.ts'.
## 2025-01-01 - 1.10.9 - fix(templates)
Correct template file paths and organization for service projects
- Moved 'some.classes.some.ts' to 'classes.some.ts'
@@ -190,60 +242,70 @@ Correct template file paths and organization for service projects
- Resolved incorrect import paths in service templates
## 2025-01-01 - 1.10.8 - fix(assets/templates)
Update CI template configurations to use module.githost
- Replaced occurrences of {{git.host}} with {{module.githost}} in CI workflow files
- Updated package dependencies for service template
## 2024-12-26 - 1.10.7 - fix(assets)
Correct URLs in templates and fix TypeScript declaration
- Updated incorrect URLs in Dockerfile templates to 'host.today'.
- Fixed type declaration for 'TemplateResult' in header.ts file.
## 2024-12-08 - 1.10.6 - fix(ci)
Corrected Docker image URL in CI templates
- Updated Docker image URL from 'code.foss.global/hosttoday' to 'code.foss.global/host.today' in default_nottags.yaml and default_tags.yaml.
- Adjusted gitignore template to include a custom section delineation.
## 2024-12-02 - 1.10.5 - fix(assets)
Update .gitignore template to remove pages directory
- Removed 'pages/' from the ignored directories in the .gitignore template.
## 2024-11-05 - 1.10.4 - fix(mod_format)
Correct file extension for TypeScript path configuration
- Fixed the TypeScript configuration to use correct file extensions for module subdirectories.
## 2024-10-27 - 1.10.3 - fix(mod_format)
Reorder TypeScript formatting steps in mod_format module
- Moved TypeScript configuration formatting earlier in the sequence for better logical consistency.
## 2024-10-27 - 1.10.2 - fix(format)
Add logging for tsconfig.json formatting
- Added an info log message for tsconfig.json formatting in format.tsconfig.ts.
## 2024-10-27 - 1.10.1 - fix(format)
Fixed async issue in tsconfig module lookup and corrected property access
## 2024-10-27 - 1.10.0 - feat(mod_format)
Add support for tsconfig.json formatting
- Added a new script to format tsconfig.json.
- Updated package.json to include `@git.zone/tspublish` as a dependency.
## 2024-10-23 - 1.9.126 - fix(format)
Remove redundant package.json property checks
- Removed property checks for `main`, `typings`, and `browserslist` from format.packagejson.ts
- This change streamlines the formatting process by removing unnecessary exits
## 2024-09-29 - 1.9.125 - fix(cli)
Fix package version configuration and formatting issues
- Updated metadata fields in package.json (repository URL, bugs URL, and homepage).
@@ -251,15 +313,17 @@ Fix package version configuration and formatting issues
- Added missing Prettier default TypeScript and Markdown configurations.
## 2024-09-27 - 1.9.124 - fix(cli)
Ensured proper existence and initialization of readme files
- Ensured readme.md and readme.hints.md files are created and initialized if they do not exist.
## 2024-09-27 - 1.9.123 - fix(core)
No changes detected
## 2024-09-27 - 1.9.123 - fix(core)
Update dependencies and improve build configurations
- Updated several dependencies in package.json for better compatibility
@@ -270,88 +334,111 @@ Update dependencies and improve build configurations
- Provided initial structure for readme and readme hints
## 2024-06-24 - 1.9.122 - fix(mod_commit)
Update package.json dependencies: @git.zone/tsdoc and @push.rocks/smartpromise to latest versions.
- - Updated @git.zone/tsdoc to ^1.3.12
- - Updated @push.rocks/smartfile to ^11.0.21
## 2024-06-23 - 1.9.121 - fix(mod_commit)
Fix changelog template rendering by removing extra new line when no version details are provided.
- Update package.json dependencies: @git.zone/tsdoc and @push.rocks/smartpromise to latest versions.
## 2024-06-23 - 1.9.120 - fix(mod_commit)
Handle edge case for empty version details in changelog formatting
- Added check for the length of the recommendedNextVersionDetails array
- Ensure no extra newline in changelog if there are no version details
## 2024-06-23 - 1.9.119 - fix(dependencies)
Update @git.zone/tsdoc to v1.3.8
- Updated @git.zone/tsdoc from v1.3.7 to v1.3.8 in package.json
## 2024-06-23 - 1.9.118 - fix(dependencies)
Update @git.zone/tsdoc to version 1.3.7
- Bump @git.zone/tsdoc from 1.3.6 to 1.3.7 in both package.json and pnpm-lock.yaml
## 2024-06-23 - 1.9.117 - fix(dependencies)
Update @git.zone/tsdoc dependency to v1.3.6
- Updated @git.zone/tsdoc version from 1.3.5 to 1.3.6 in package.json
- Updated pnpm-lock.yaml to reflect the new version of @git.zone/tsdoc
## 2024-06-23 - 1.9.116 - fix(dependencies)
Update @git.zone/tsdoc to version 1.3.5
- Updated the @git.zone/tsdoc dependency in package.json and pnpm-lock.yaml from version 1.3.4 to 1.3.5
- Removed the outdated changelog.md file.
## 2024-06-23 - 1.9.114 - fix(format)
Fixed formatting issues across multiple TypeScript files.
## 2024-06-23 - 1.9.113 - fix(mod_commit)
Remove extra new lines in changelog.
## 2024-06-23 - 1.9.112 - fix(core)
Update changelog formatting and remove outdated entries.
## 2024-06-23 - 1.9.111 - fix(changelog)
Remove outdated changelog entries and update formatting.
## 2024-06-23 - 1.9.110 - fix(dependencies)
Update @git.zone/tsdoc to version 1.3.4.
## 2024-06-23 - 1.9.109 - fix(changelog)
Remove outdated entries and adjust formatting in changelog.
## 2024-06-23 - 1.9.108 - fix(dependencies)
Update @git.zone/tsdoc dependency to version 1.3.2.
## 2024-06-23 - 1.9.107 - fix(changelog)
Remove placeholder entries and adjust formatting in changelog.
## 2024-06-23 - 1.9.106 - fix(dependencies)
Updated @git.zone/tsdoc from version 1.3.0 to 1.3.1.
## 2024-06-23 - 1.9.105 - fix(dependencies)
Updated @git.zone/tsdoc dependency from 1.2.2 to 1.3.0 in package.json and pnpm-lock.yaml.
## 2024-06-23 - 1.9.104 - fix(changelog)
Remove placeholder entries and adjust formatting in changelog.
## 2024-06-23 - 1.9.103 - fix(changelog)
Fix changelog to remove placeholder entries and adjust formatting.
## 2024-06-23 - 1.9.102 - fix(logging)
Optimize logger instantiation and configuration.
## 2024-06-23 - 1.9.101 - fix(metadata)
Ensure accurate project metadata in package.json.
## 2024-06-23 - 1.9.100 - fix(dependencies)
Updated @git.zone/tsdoc dependency version to ^1.2.2 in package.json and pnpm-lock.yaml.
## 2024-06-23 - 1.9.99 - fix(mod_commit)
Fix variable reassignment issue in changelog writing step.

View File

@@ -36,4 +36,4 @@
"tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
}
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@git.zone/cli",
"private": false,
"version": "1.16.7",
"version": "1.16.10",
"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.",
"main": "dist_ts/index.ts",
"typings": "dist_ts/index.d.ts",

View File

@@ -1,10 +1,11 @@
# Gitzone CLI - Development Hints
* the cli of the git.zone project.
- 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
@@ -14,12 +15,14 @@ Gitzone CLI (`@git.zone/cli`) is a comprehensive toolbelt for streamlining local
## 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
@@ -30,6 +33,7 @@ Gitzone CLI (`@git.zone/cli`) is a comprehensive toolbelt for streamlining local
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
@@ -42,6 +46,7 @@ The format module is responsible for project standardization:
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
@@ -182,7 +187,7 @@ gitzone format --clean-backups
## API Changes
- smartfile API updated to use fs.* and memory.* namespaces
- 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
- Type imports use `import type` for proper verbatim module syntax

View File

@@ -47,12 +47,14 @@ gitzone template [template-name]
```
**Available templates:**
- **`npm`** - TypeScript npm package with testing, CI/CD, and full tooling
- **`service`** - Microservice architecture with Docker support
- **`website`** - Modern web application with LitElement and service workers
- **`website`** - Modern web application with LitElement and service workers
- **`wcc`** - Web Component Collection for reusable UI components
Each template comes pre-configured with:
- ✅ TypeScript with modern configurations
- ✅ Automated testing setup
- ✅ CI/CD pipelines (GitLab/GitHub)
@@ -81,6 +83,7 @@ gitzone format --verbose
```
**Format features:**
- 🔄 **Smart caching** - Only processes changed files
- 🛡️ **Rollback support** - Undo formatting changes if needed
- 📊 **Detailed reporting** - See exactly what changed
@@ -88,6 +91,7 @@ gitzone format --verbose
- 🎯 **Module-specific formatting** - Target specific formatters
**Rollback capabilities:**
```bash
# List all available backups
gitzone format --list-backups
@@ -103,6 +107,7 @@ gitzone format --clean-backups
```
**Formatters included:**
- **Prettier** - JavaScript/TypeScript code formatting
- **License** - Ensure proper licensing
- **Package.json** - Standardize package configurations
@@ -121,6 +126,7 @@ gitzone commit
```
Features:
- 📝 Interactive commit message builder
- 🏷️ Automatic version bumping (major/minor/patch)
- 📜 Changelog generation
@@ -128,6 +134,7 @@ Features:
- 🎯 Conventional commit compliance
The commit wizard guides you through:
1. **Type selection** (feat/fix/docs/style/refactor/perf/test/chore)
2. **Scope definition** (component/module affected)
3. **Description crafting**
@@ -153,6 +160,7 @@ gitzone meta remove [name]
```
Perfect for:
- Monorepo management
- Multi-package projects
- Coordinated deployments
@@ -168,6 +176,7 @@ gitzone docker prune
```
This command removes:
- Stopped containers
- Unused images
- Dangling volumes
@@ -196,6 +205,7 @@ gitzone deprecate
```
Interactive wizard for:
- Setting deprecation notices
- Guiding users to replacements
- Updating registry metadata
@@ -210,6 +220,7 @@ gitzone start
```
Automatically:
- Checks out master branch
- Pulls latest changes
- Installs dependencies
@@ -266,18 +277,21 @@ Customize gitzone behavior through `npmextra.json`:
## 🏆 Best Practices
### For New Projects
1. Start with a template: `gitzone template npm`
2. Customize the generated structure
3. Run initial format: `gitzone format`
4. Set up CI/CD: `gitzone open ci`
### For Existing Projects
1. Initialize: `gitzone start`
2. Format codebase: `gitzone format --dry-run` (preview first!)
3. Apply formatting: `gitzone format --yes`
4. Commit changes: `gitzone commit`
### For Teams
1. Document format preferences in `npmextra.json`
2. Use `--save-plan` for reviewable format changes
3. Enable rollback for safety
@@ -286,6 +300,7 @@ Customize gitzone behavior through `npmextra.json`:
## 🎯 Common Workflows
### Clean Development Cycle
```bash
# 1. Start fresh
gitzone start
@@ -304,6 +319,7 @@ gitzone commit
```
### Multi-Repository Management
```bash
# 1. Set up meta repository
gitzone meta init
@@ -318,6 +334,7 @@ gitzone meta update
```
### Safe Formatting with Rollback
```bash
# 1. Preview changes
gitzone format --dry-run
@@ -335,17 +352,20 @@ gitzone format --rollback
## 🔌 Integrations
### CI/CD Platforms
- **GitLab CI** - Full pipeline support with templates
- **GitHub Actions** - Automated workflows
- **Docker** - Container-based deployments
### Development Tools
- **TypeScript** - First-class support
- **Prettier** - Code formatting
- **ESLint** - Linting (via format modules)
- **npm/pnpm** - Package management
### Version Control
- **Git** - Deep integration
- **Semantic Versioning** - Automatic version bumping
- **Conventional Commits** - Standardized commit messages
@@ -361,19 +381,25 @@ gitzone format --rollback
## 🐛 Troubleshooting
### Format Command Shows "Cancelled"
If the format command shows cancelled even after confirming:
- Check your `npmextra.json` configuration
- Try with `--yes` flag to skip confirmation
- Use `--verbose` for detailed output
### Docker Commands Fail
Ensure Docker daemon is running:
```bash
docker info
```
### Template Creation Issues
Verify npm/pnpm is properly configured:
```bash
npm config get registry
```
@@ -381,6 +407,7 @@ npm config get registry
## 📈 Performance
gitzone is optimized for speed:
- **Parallel processing** for format operations
- **Smart caching** to avoid redundant work
- **Incremental updates** for meta repositories
@@ -388,7 +415,7 @@ gitzone is optimized for speed:
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
@@ -403,4 +430,4 @@ Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@@ -3,11 +3,13 @@
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
@@ -15,12 +17,14 @@ This plan outlines improvements for the gitzone format module to enhance its fun
- [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)
@@ -28,6 +32,7 @@ This plan outlines improvements for the gitzone format module to enhance its fun
- [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)
@@ -37,12 +42,14 @@ This plan outlines improvements for the gitzone format module to enhance its fun
## 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
@@ -51,30 +58,35 @@ This plan outlines improvements for the gitzone format module to enhance its fun
## 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
@@ -167,4 +179,4 @@ This plan outlines improvements for the gitzone format module to enhance its fun
- ✅ Faster execution through parallel processing and caching
- ✅ Enhanced user control through configuration
- ✅ Better visibility through reporting and statistics
- ✅ Improved maintainability through better architecture
- ✅ Improved maintainability through better architecture

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/cli',
version: '1.16.7',
version: '1.16.10',
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.'
}

View File

@@ -2,32 +2,29 @@ import * as plugins from './mod.plugins.js';
import { FormatContext } from './classes.formatcontext.js';
import type { IPlannedChange } from './interfaces.format.js';
import { Project } from '../classes.project.js';
import { ChangeCache } from './classes.changecache.js';
export abstract class BaseFormatter {
protected context: FormatContext;
protected project: Project;
protected cache: ChangeCache;
protected stats: any; // Will be FormatStats from context
constructor(context: FormatContext, project: Project) {
this.context = context;
this.project = project;
this.cache = context.getChangeCache();
this.stats = context.getFormatStats();
}
abstract get name(): string;
abstract analyze(): Promise<IPlannedChange[]>;
abstract applyChange(change: IPlannedChange): Promise<void>;
async execute(changes: IPlannedChange[]): Promise<void> {
const startTime = this.stats.moduleStartTime(this.name);
this.stats.startModule(this.name);
try {
await this.preExecute();
for (const change of changes) {
try {
await this.applyChange(change);
@@ -37,57 +34,49 @@ export abstract class BaseFormatter {
throw error;
}
}
await this.postExecute();
} catch (error) {
await this.context.rollbackOperation();
// Don't rollback here - let the FormatPlanner handle it
throw error;
} finally {
this.stats.endModule(this.name, startTime);
}
}
protected async preExecute(): Promise<void> {
// Override in subclasses if needed
}
protected async postExecute(): Promise<void> {
// Override in subclasses if needed
}
protected async modifyFile(filepath: string, content: string): Promise<void> {
await this.context.trackFileChange(filepath);
await plugins.smartfile.memory.toFs(content, filepath);
await this.cache.updateFileCache(filepath);
// Validate filepath before writing
if (!filepath || filepath.trim() === '') {
throw new Error(`Invalid empty filepath in modifyFile`);
}
// Ensure we have a proper path with directory component
// If the path has no directory component (e.g., "package.json"), prepend "./"
let normalizedPath = filepath;
if (!plugins.path.parse(filepath).dir) {
normalizedPath = './' + filepath;
}
await plugins.smartfile.memory.toFs(content, normalizedPath);
}
protected async createFile(filepath: string, content: string): Promise<void> {
await plugins.smartfile.memory.toFs(content, filepath);
await this.cache.updateFileCache(filepath);
}
protected async deleteFile(filepath: string): Promise<void> {
await this.context.trackFileChange(filepath);
await plugins.smartfile.fs.remove(filepath);
}
protected async shouldProcessFile(filepath: string): Promise<boolean> {
const config = new plugins.npmextra.Npmextra();
const useCache = config.dataFor('gitzone.format.cache.enabled', true);
if (!useCache) {
return true; // Process all files if cache is disabled
}
const hasChanged = await this.cache.hasFileChanged(filepath);
// Record cache statistics
if (hasChanged) {
this.stats.recordCacheMiss();
} else {
this.stats.recordCacheHit();
}
return hasChanged;
return true;
}
}
}

View File

@@ -18,32 +18,32 @@ export class ChangeCache {
private cacheDir: string;
private manifestPath: string;
private cacheVersion = '1.0.0';
constructor() {
this.cacheDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-cache');
this.manifestPath = plugins.path.join(this.cacheDir, 'manifest.json');
}
async initialize(): Promise<void> {
await plugins.smartfile.fs.ensureDir(this.cacheDir);
}
async getManifest(): Promise<ICacheManifest> {
const defaultManifest: ICacheManifest = {
version: this.cacheVersion,
lastFormat: 0,
files: []
files: [],
};
const exists = await plugins.smartfile.fs.fileExists(this.manifestPath);
if (!exists) {
return defaultManifest;
}
try {
const content = plugins.smartfile.fs.toStringSync(this.manifestPath);
const manifest = JSON.parse(content);
// Validate the manifest structure
if (this.isValidManifest(manifest)) {
return manifest;
@@ -52,7 +52,9 @@ export class ChangeCache {
return defaultManifest;
}
} catch (error) {
console.warn(`Failed to read cache manifest: ${error.message}, returning default manifest`);
console.warn(
`Failed to read cache manifest: ${error.message}, returning default manifest`,
);
// Try to delete the corrupted file
try {
await plugins.smartfile.fs.remove(this.manifestPath);
@@ -62,168 +64,160 @@ export class ChangeCache {
return defaultManifest;
}
}
async saveManifest(manifest: ICacheManifest): Promise<void> {
// Validate before saving
if (!this.isValidManifest(manifest)) {
throw new Error('Invalid manifest structure, cannot save');
}
// Use atomic write: write to temp file, then move it
const tempPath = `${this.manifestPath}.tmp`;
try {
// Write to temporary file
const jsonContent = JSON.stringify(manifest, null, 2);
await plugins.smartfile.memory.toFs(jsonContent, tempPath);
// Move temp file to actual manifest (atomic-like operation)
// Since smartfile doesn't have rename, we copy and delete
await plugins.smartfile.fs.copy(tempPath, this.manifestPath);
await plugins.smartfile.fs.remove(tempPath);
} catch (error) {
// Clean up temp file if it exists
try {
await plugins.smartfile.fs.remove(tempPath);
} catch (removeError) {
// Ignore removal errors
}
throw error;
}
// Ensure directory exists
await plugins.smartfile.fs.ensureDir(this.cacheDir);
// Write directly with proper JSON stringification
const jsonContent = JSON.stringify(manifest, null, 2);
await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath);
}
async hasFileChanged(filePath: string): Promise<boolean> {
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
: plugins.path.join(paths.cwd, filePath);
// Check if file exists
const exists = await plugins.smartfile.fs.fileExists(absolutePath);
if (!exists) {
return true; // File doesn't exist, so it's "changed" (will be created)
}
// Get current file stats
const stats = await plugins.smartfile.fs.stat(absolutePath);
// Skip directories
if (stats.isDirectory()) {
return false; // Directories are not processed
}
const content = plugins.smartfile.fs.toStringSync(absolutePath);
const currentChecksum = this.calculateChecksum(content);
// Get cached info
const manifest = await this.getManifest();
const cachedFile = manifest.files.find(f => f.path === filePath);
const cachedFile = manifest.files.find((f) => f.path === filePath);
if (!cachedFile) {
return true; // Not in cache, so it's changed
}
// Compare checksums
return cachedFile.checksum !== currentChecksum ||
cachedFile.size !== stats.size ||
cachedFile.modified !== stats.mtimeMs;
return (
cachedFile.checksum !== currentChecksum ||
cachedFile.size !== stats.size ||
cachedFile.modified !== stats.mtimeMs
);
}
async updateFileCache(filePath: string): Promise<void> {
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
: plugins.path.join(paths.cwd, filePath);
// Get current file stats
const stats = await plugins.smartfile.fs.stat(absolutePath);
// Skip directories
if (stats.isDirectory()) {
return; // Don't cache directories
}
const content = plugins.smartfile.fs.toStringSync(absolutePath);
const checksum = this.calculateChecksum(content);
// Update manifest
const manifest = await this.getManifest();
const existingIndex = manifest.files.findIndex(f => f.path === filePath);
const existingIndex = manifest.files.findIndex((f) => f.path === filePath);
const cacheEntry: IFileCache = {
path: filePath,
checksum,
modified: stats.mtimeMs,
size: stats.size
size: stats.size,
};
if (existingIndex !== -1) {
manifest.files[existingIndex] = cacheEntry;
} else {
manifest.files.push(cacheEntry);
}
manifest.lastFormat = Date.now();
await this.saveManifest(manifest);
}
async getChangedFiles(filePaths: string[]): Promise<string[]> {
const changedFiles: string[] = [];
for (const filePath of filePaths) {
if (await this.hasFileChanged(filePath)) {
changedFiles.push(filePath);
}
}
return changedFiles;
}
async clean(): Promise<void> {
const manifest = await this.getManifest();
const validFiles: IFileCache[] = [];
// Remove entries for files that no longer exist
for (const file of manifest.files) {
const absolutePath = plugins.path.isAbsolute(file.path)
? file.path
const absolutePath = plugins.path.isAbsolute(file.path)
? file.path
: plugins.path.join(paths.cwd, file.path);
if (await plugins.smartfile.fs.fileExists(absolutePath)) {
validFiles.push(file);
}
}
manifest.files = validFiles;
await this.saveManifest(manifest);
}
private calculateChecksum(content: string | Buffer): string {
return plugins.crypto.createHash('sha256').update(content).digest('hex');
}
private isValidManifest(manifest: any): manifest is ICacheManifest {
// Check if manifest has the required structure
if (!manifest || typeof manifest !== 'object') {
return false;
}
// Check required fields
if (typeof manifest.version !== 'string' ||
typeof manifest.lastFormat !== 'number' ||
!Array.isArray(manifest.files)) {
if (
typeof manifest.version !== 'string' ||
typeof manifest.lastFormat !== 'number' ||
!Array.isArray(manifest.files)
) {
return false;
}
// Check each file entry
for (const file of manifest.files) {
if (!file || typeof file !== 'object' ||
typeof file.path !== 'string' ||
typeof file.checksum !== 'string' ||
typeof file.modified !== 'number' ||
typeof file.size !== 'number') {
if (
!file ||
typeof file !== 'object' ||
typeof file.path !== 'string' ||
typeof file.checksum !== 'string' ||
typeof file.modified !== 'number' ||
typeof file.size !== 'number'
) {
return false;
}
}
return true;
}
}
}

View File

@@ -9,35 +9,42 @@ export interface IModuleDependency {
export class DependencyAnalyzer {
private moduleDependencies: Map<string, IModuleDependency> = new Map();
constructor() {
this.initializeDependencies();
}
private initializeDependencies(): void {
// Define dependencies between format modules
const dependencies = {
'cleanup': [], // No dependencies
'npmextra': [], // No dependencies
'license': ['npmextra'], // Depends on npmextra for config
'packagejson': ['npmextra'], // Depends on npmextra for config
'templates': ['npmextra', 'packagejson'], // Depends on both
'gitignore': ['templates'], // Depends on templates
'tsconfig': ['packagejson'], // Depends on package.json
'prettier': ['cleanup', 'npmextra', 'packagejson', 'templates', 'gitignore', 'tsconfig'], // Runs after most others
'readme': ['npmextra', 'packagejson'], // Depends on project metadata
'copy': ['npmextra'], // Depends on config
cleanup: [], // No dependencies
npmextra: [], // No dependencies
license: ['npmextra'], // Depends on npmextra for config
packagejson: ['npmextra'], // Depends on npmextra for config
templates: ['npmextra', 'packagejson'], // Depends on both
gitignore: ['templates'], // Depends on templates
tsconfig: ['packagejson'], // Depends on package.json
prettier: [
'cleanup',
'npmextra',
'packagejson',
'templates',
'gitignore',
'tsconfig',
], // Runs after most others
readme: ['npmextra', 'packagejson'], // Depends on project metadata
copy: ['npmextra'], // Depends on config
};
// Initialize all modules
for (const [module, deps] of Object.entries(dependencies)) {
this.moduleDependencies.set(module, {
module,
dependencies: new Set(deps),
dependents: new Set()
dependents: new Set(),
});
}
// Build reverse dependencies (dependents)
for (const [module, deps] of Object.entries(dependencies)) {
for (const dep of deps) {
@@ -48,34 +55,35 @@ export class DependencyAnalyzer {
}
}
}
getExecutionGroups(modules: BaseFormatter[]): BaseFormatter[][] {
const modulesMap = new Map(modules.map(m => [m.name, m]));
const modulesMap = new Map(modules.map((m) => [m.name, m]));
const executed = new Set<string>();
const groups: BaseFormatter[][] = [];
while (executed.size < modules.length) {
const currentGroup: BaseFormatter[] = [];
for (const module of modules) {
if (executed.has(module.name)) continue;
const dependency = this.moduleDependencies.get(module.name);
if (!dependency) {
// Unknown module, execute in isolation
currentGroup.push(module);
continue;
}
// Check if all dependencies have been executed
const allDepsExecuted = Array.from(dependency.dependencies)
.every(dep => executed.has(dep) || !modulesMap.has(dep));
const allDepsExecuted = Array.from(dependency.dependencies).every(
(dep) => executed.has(dep) || !modulesMap.has(dep),
);
if (allDepsExecuted) {
currentGroup.push(module);
}
}
if (currentGroup.length === 0) {
// Circular dependency or error - execute remaining modules
for (const module of modules) {
@@ -84,24 +92,26 @@ export class DependencyAnalyzer {
}
}
}
currentGroup.forEach(m => executed.add(m.name));
currentGroup.forEach((m) => executed.add(m.name));
groups.push(currentGroup);
}
return groups;
}
canRunInParallel(module1: string, module2: string): boolean {
const dep1 = this.moduleDependencies.get(module1);
const dep2 = this.moduleDependencies.get(module2);
if (!dep1 || !dep2) return false;
// Check if module1 depends on module2 or vice versa
return !dep1.dependencies.has(module2) &&
!dep2.dependencies.has(module1) &&
!dep1.dependents.has(module2) &&
!dep2.dependents.has(module1);
return (
!dep1.dependencies.has(module2) &&
!dep2.dependencies.has(module1) &&
!dep1.dependents.has(module2) &&
!dep2.dependents.has(module1)
);
}
}
}

View File

@@ -4,72 +4,85 @@ import { logger } from '../gitzone.logging.js';
export class DiffReporter {
private diffs: Map<string, string> = new Map();
async generateDiff(filePath: string, oldContent: string, newContent: string): Promise<string> {
async generateDiff(
filePath: string,
oldContent: string,
newContent: string,
): Promise<string> {
const diff = plugins.smartdiff.createDiff(oldContent, newContent);
this.diffs.set(filePath, diff);
return diff;
}
async generateDiffForChange(change: IPlannedChange): Promise<string | null> {
if (change.type !== 'modify') {
return null;
}
try {
const exists = await plugins.smartfile.fs.fileExists(change.path);
if (!exists) {
return null;
}
const currentContent = await plugins.smartfile.fs.toStringSync(change.path);
const currentContent = await plugins.smartfile.fs.toStringSync(
change.path,
);
// For planned changes, we need the new content
if (!change.content) {
return null;
}
return await this.generateDiff(change.path, currentContent, change.content);
return await this.generateDiff(
change.path,
currentContent,
change.content,
);
} catch (error) {
logger.log('error', `Failed to generate diff for ${change.path}: ${error.message}`);
logger.log(
'error',
`Failed to generate diff for ${change.path}: ${error.message}`,
);
return null;
}
}
displayDiff(filePath: string, diff?: string): void {
const diffToShow = diff || this.diffs.get(filePath);
if (!diffToShow) {
logger.log('warn', `No diff available for ${filePath}`);
return;
}
console.log(`\n${this.formatDiffHeader(filePath)}`);
console.log(this.colorDiff(diffToShow));
console.log('━'.repeat(50));
}
displayAllDiffs(): void {
if (this.diffs.size === 0) {
logger.log('info', 'No diffs to display');
return;
}
console.log('\nFile Changes:');
console.log('═'.repeat(50));
for (const [filePath, diff] of this.diffs) {
this.displayDiff(filePath, diff);
}
}
private formatDiffHeader(filePath: string): string {
return `📄 ${filePath}`;
}
private colorDiff(diff: string): string {
const lines = diff.split('\n');
const coloredLines = lines.map(line => {
const coloredLines = lines.map((line) => {
if (line.startsWith('+') && !line.startsWith('+++')) {
return `\x1b[32m${line}\x1b[0m`; // Green for additions
} else if (line.startsWith('-') && !line.startsWith('---')) {
@@ -80,29 +93,32 @@ export class DiffReporter {
return line;
}
});
return coloredLines.join('\n');
}
async saveDiffReport(outputPath: string): Promise<void> {
const report = {
timestamp: new Date().toISOString(),
totalFiles: this.diffs.size,
diffs: Array.from(this.diffs.entries()).map(([path, diff]) => ({
path,
diff
}))
diff,
})),
};
await plugins.smartfile.memory.toFs(JSON.stringify(report, null, 2), outputPath);
await plugins.smartfile.memory.toFs(
JSON.stringify(report, null, 2),
outputPath,
);
logger.log('info', `Diff report saved to ${outputPath}`);
}
hasAnyDiffs(): boolean {
return this.diffs.size > 0;
}
getDiffCount(): number {
return this.diffs.size;
}
}
}

View File

@@ -1,65 +1,14 @@
import * as plugins from './mod.plugins.js';
import { RollbackManager } from './classes.rollbackmanager.js';
import { ChangeCache } from './classes.changecache.js';
import { FormatStats } from './classes.formatstats.js';
import type { IFormatOperation, IFormatPlan } from './interfaces.format.js';
export class FormatContext {
private rollbackManager: RollbackManager;
private currentOperation: IFormatOperation | null = null;
private changeCache: ChangeCache;
private formatStats: FormatStats;
constructor() {
this.rollbackManager = new RollbackManager();
this.changeCache = new ChangeCache();
this.formatStats = new FormatStats();
}
async beginOperation(): Promise<void> {
this.currentOperation = await this.rollbackManager.createOperation();
}
async trackFileChange(filepath: string): Promise<void> {
if (!this.currentOperation) {
throw new Error('No operation in progress. Call beginOperation() first.');
}
await this.rollbackManager.backupFile(filepath, this.currentOperation.id);
}
async commitOperation(): Promise<void> {
if (!this.currentOperation) {
throw new Error('No operation in progress. Call beginOperation() first.');
}
await this.rollbackManager.markComplete(this.currentOperation.id);
this.currentOperation = null;
}
async rollbackOperation(): Promise<void> {
if (!this.currentOperation) {
throw new Error('No operation in progress. Call beginOperation() first.');
}
await this.rollbackManager.rollback(this.currentOperation.id);
this.currentOperation = null;
}
async rollbackTo(operationId: string): Promise<void> {
await this.rollbackManager.rollback(operationId);
}
getRollbackManager(): RollbackManager {
return this.rollbackManager;
}
getChangeCache(): ChangeCache {
return this.changeCache;
}
async initializeCache(): Promise<void> {
await this.changeCache.initialize();
}
getFormatStats(): FormatStats {
return this.formatStats;
}
}
}

View File

@@ -10,7 +10,7 @@ export class FormatPlanner {
private plannedChanges: Map<string, IPlannedChange[]> = new Map();
private dependencyAnalyzer = new DependencyAnalyzer();
private diffReporter = new DiffReporter();
async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> {
const plan: IFormatPlan = {
summary: {
@@ -18,20 +18,20 @@ export class FormatPlanner {
filesAdded: 0,
filesModified: 0,
filesRemoved: 0,
estimatedTime: 0
estimatedTime: 0,
},
changes: [],
warnings: []
warnings: [],
};
for (const module of modules) {
try {
const changes = await module.analyze();
this.plannedChanges.set(module.name, changes);
for (const change of changes) {
plan.changes.push(change);
// Update summary
switch (change.type) {
case 'create':
@@ -49,67 +49,51 @@ export class FormatPlanner {
plan.warnings.push({
level: 'error',
message: `Failed to analyze module ${module.name}: ${error.message}`,
module: module.name
module: module.name,
});
}
}
plan.summary.totalFiles = plan.summary.filesAdded + plan.summary.filesModified + plan.summary.filesRemoved;
plan.summary.totalFiles =
plan.summary.filesAdded +
plan.summary.filesModified +
plan.summary.filesRemoved;
plan.summary.estimatedTime = plan.summary.totalFiles * 100; // 100ms per file estimate
return plan;
}
async executePlan(plan: IFormatPlan, modules: BaseFormatter[], context: FormatContext, parallel: boolean = true): Promise<void> {
await context.beginOperation();
async executePlan(
plan: IFormatPlan,
modules: BaseFormatter[],
context: FormatContext,
parallel: boolean = false,
): Promise<void> {
const startTime = Date.now();
try {
if (parallel) {
// Get execution groups based on dependencies
const executionGroups = this.dependencyAnalyzer.getExecutionGroups(modules);
logger.log('info', `Executing formatters in ${executionGroups.length} groups...`);
for (let i = 0; i < executionGroups.length; i++) {
const group = executionGroups[i];
logger.log('info', `Executing group ${i + 1}: ${group.map(m => m.name).join(', ')}`);
// Execute modules in this group in parallel
const promises = group.map(async (module) => {
const changes = this.plannedChanges.get(module.name) || [];
if (changes.length > 0) {
logger.log('info', `Executing ${module.name} formatter...`);
await module.execute(changes);
}
});
await Promise.all(promises);
}
} else {
// Sequential execution (original implementation)
for (const module of modules) {
const changes = this.plannedChanges.get(module.name) || [];
if (changes.length > 0) {
logger.log('info', `Executing ${module.name} formatter...`);
await module.execute(changes);
}
// Always use sequential execution to avoid race conditions
for (const module of modules) {
const changes = this.plannedChanges.get(module.name) || [];
if (changes.length > 0) {
logger.log('info', `Executing ${module.name} formatter...`);
await module.execute(changes);
}
}
const endTime = Date.now();
const duration = endTime - startTime;
logger.log('info', `Format operations completed in ${duration}ms`);
await context.commitOperation();
} catch (error) {
await context.rollbackOperation();
throw error;
}
}
async displayPlan(plan: IFormatPlan, detailed: boolean = false): Promise<void> {
async displayPlan(
plan: IFormatPlan,
detailed: boolean = false,
): Promise<void> {
console.log('\nFormat Plan:');
console.log('━'.repeat(50));
console.log(`Summary: ${plan.summary.totalFiles} files will be changed`);
@@ -118,7 +102,7 @@ export class FormatPlanner {
console.log(`${plan.summary.filesRemoved} deleted files`);
console.log('');
console.log('Changes by module:');
// Group changes by module
const changesByModule = new Map<string, IPlannedChange[]>();
for (const change of plan.changes) {
@@ -126,14 +110,16 @@ export class FormatPlanner {
moduleChanges.push(change);
changesByModule.set(change.module, moduleChanges);
}
for (const [module, changes] of changesByModule) {
console.log(`\n${this.getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`);
console.log(
`\n${this.getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`,
);
for (const change of changes) {
const icon = this.getChangeIcon(change.type);
console.log(` ${icon} ${change.path} - ${change.description}`);
// Show diff for modified files if detailed view is requested
if (detailed && change.type === 'modify') {
const diff = await this.diffReporter.generateDiffForChange(change);
@@ -143,7 +129,7 @@ export class FormatPlanner {
}
}
}
if (plan.warnings.length > 0) {
console.log('\nWarnings:');
for (const warning of plan.warnings) {
@@ -151,26 +137,26 @@ export class FormatPlanner {
console.log(` ${icon} ${warning.message}`);
}
}
console.log('\n' + '━'.repeat(50));
}
private getModuleIcon(module: string): string {
const icons: Record<string, string> = {
'packagejson': '📦',
'license': '📝',
'tsconfig': '🔧',
'cleanup': '🚮',
'gitignore': '🔒',
'prettier': '✨',
'readme': '📖',
'templates': '📄',
'npmextra': '⚙️',
'copy': '📋'
packagejson: '📦',
license: '📝',
tsconfig: '🔧',
cleanup: '🚮',
gitignore: '🔒',
prettier: '✨',
readme: '📖',
templates: '📄',
npmextra: '⚙️',
copy: '📋',
};
return icons[module] || '📁';
}
private getChangeIcon(type: 'create' | 'modify' | 'delete'): string {
switch (type) {
case 'create':
@@ -181,4 +167,4 @@ export class FormatPlanner {
return '❌';
}
}
}
}

View File

@@ -30,7 +30,7 @@ export interface IFormatStats {
export class FormatStats {
private stats: IFormatStats;
constructor() {
this.stats = {
totalExecutionTime: 0,
@@ -44,11 +44,11 @@ export class FormatStats {
totalDeleted: 0,
totalErrors: 0,
cacheHits: 0,
cacheMisses: 0
}
cacheMisses: 0,
},
};
}
startModule(moduleName: string): void {
this.stats.moduleStats.set(moduleName, {
name: moduleName,
@@ -58,31 +58,35 @@ export class FormatStats {
successes: 0,
filesCreated: 0,
filesModified: 0,
filesDeleted: 0
filesDeleted: 0,
});
}
moduleStartTime(moduleName: string): number {
return Date.now();
}
endModule(moduleName: string, startTime: number): void {
const moduleStats = this.stats.moduleStats.get(moduleName);
if (moduleStats) {
moduleStats.executionTime = Date.now() - startTime;
}
}
recordFileOperation(moduleName: string, operation: 'create' | 'modify' | 'delete', success: boolean = true): void {
recordFileOperation(
moduleName: string,
operation: 'create' | 'modify' | 'delete',
success: boolean = true,
): void {
const moduleStats = this.stats.moduleStats.get(moduleName);
if (!moduleStats) return;
moduleStats.filesProcessed++;
if (success) {
moduleStats.successes++;
this.stats.overallStats.totalFiles++;
switch (operation) {
case 'create':
moduleStats.filesCreated++;
@@ -102,53 +106,66 @@ export class FormatStats {
this.stats.overallStats.totalErrors++;
}
}
recordCacheHit(): void {
this.stats.overallStats.cacheHits++;
}
recordCacheMiss(): void {
this.stats.overallStats.cacheMisses++;
}
finish(): void {
this.stats.endTime = Date.now();
this.stats.totalExecutionTime = this.stats.endTime - this.stats.startTime;
}
displayStats(): void {
console.log('\n📊 Format Operation Statistics:');
console.log('═'.repeat(50));
// Overall stats
console.log('\nOverall Summary:');
console.log(` Total Execution Time: ${this.formatDuration(this.stats.totalExecutionTime)}`);
console.log(
` Total Execution Time: ${this.formatDuration(this.stats.totalExecutionTime)}`,
);
console.log(` Files Processed: ${this.stats.overallStats.totalFiles}`);
console.log(` • Created: ${this.stats.overallStats.totalCreated}`);
console.log(` • Modified: ${this.stats.overallStats.totalModified}`);
console.log(` • Deleted: ${this.stats.overallStats.totalDeleted}`);
console.log(` Errors: ${this.stats.overallStats.totalErrors}`);
if (this.stats.overallStats.cacheHits > 0 || this.stats.overallStats.cacheMisses > 0) {
const cacheHitRate = this.stats.overallStats.cacheHits /
(this.stats.overallStats.cacheHits + this.stats.overallStats.cacheMisses) * 100;
if (
this.stats.overallStats.cacheHits > 0 ||
this.stats.overallStats.cacheMisses > 0
) {
const cacheHitRate =
(this.stats.overallStats.cacheHits /
(this.stats.overallStats.cacheHits +
this.stats.overallStats.cacheMisses)) *
100;
console.log(` Cache Hit Rate: ${cacheHitRate.toFixed(1)}%`);
console.log(` • Hits: ${this.stats.overallStats.cacheHits}`);
console.log(` • Misses: ${this.stats.overallStats.cacheMisses}`);
}
// Module stats
console.log('\nModule Breakdown:');
console.log('─'.repeat(50));
const sortedModules = Array.from(this.stats.moduleStats.values())
.sort((a, b) => b.filesProcessed - a.filesProcessed);
const sortedModules = Array.from(this.stats.moduleStats.values()).sort(
(a, b) => b.filesProcessed - a.filesProcessed,
);
for (const moduleStats of sortedModules) {
console.log(`\n${this.getModuleIcon(moduleStats.name)} ${moduleStats.name}:`);
console.log(` Execution Time: ${this.formatDuration(moduleStats.executionTime)}`);
console.log(
`\n${this.getModuleIcon(moduleStats.name)} ${moduleStats.name}:`,
);
console.log(
` Execution Time: ${this.formatDuration(moduleStats.executionTime)}`,
);
console.log(` Files Processed: ${moduleStats.filesProcessed}`);
if (moduleStats.filesCreated > 0) {
console.log(` • Created: ${moduleStats.filesCreated}`);
}
@@ -158,27 +175,30 @@ export class FormatStats {
if (moduleStats.filesDeleted > 0) {
console.log(` • Deleted: ${moduleStats.filesDeleted}`);
}
if (moduleStats.errors > 0) {
console.log(` ❌ Errors: ${moduleStats.errors}`);
}
}
console.log('\n' + '═'.repeat(50));
}
async saveReport(outputPath: string): Promise<void> {
const report = {
timestamp: new Date().toISOString(),
executionTime: this.stats.totalExecutionTime,
overallStats: this.stats.overallStats,
moduleStats: Array.from(this.stats.moduleStats.values())
moduleStats: Array.from(this.stats.moduleStats.values()),
};
await plugins.smartfile.memory.toFs(JSON.stringify(report, null, 2), outputPath);
await plugins.smartfile.memory.toFs(
JSON.stringify(report, null, 2),
outputPath,
);
logger.log('info', `Statistics report saved to ${outputPath}`);
}
private formatDuration(ms: number): string {
if (ms < 1000) {
return `${ms}ms`;
@@ -190,20 +210,20 @@ export class FormatStats {
return `${minutes}m ${seconds}s`;
}
}
private getModuleIcon(module: string): string {
const icons: Record<string, string> = {
'packagejson': '📦',
'license': '📝',
'tsconfig': '🔧',
'cleanup': '🚮',
'gitignore': '🔒',
'prettier': '✨',
'readme': '📖',
'templates': '📄',
'npmextra': '⚙️',
'copy': '📋'
packagejson: '📦',
license: '📝',
tsconfig: '🔧',
cleanup: '🚮',
gitignore: '🔒',
prettier: '✨',
readme: '📖',
templates: '📄',
npmextra: '⚙️',
copy: '📋',
};
return icons[module] || '📁';
}
}
}

View File

@@ -5,206 +5,227 @@ import type { IFormatOperation } from './interfaces.format.js';
export class RollbackManager {
private backupDir: string;
private manifestPath: string;
constructor() {
this.backupDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-backups');
this.manifestPath = plugins.path.join(this.backupDir, 'manifest.json');
}
async createOperation(): Promise<IFormatOperation> {
await this.ensureBackupDir();
const operation: IFormatOperation = {
id: this.generateOperationId(),
timestamp: Date.now(),
files: [],
status: 'pending'
status: 'pending',
};
await this.updateManifest(operation);
return operation;
}
async backupFile(filepath: string, operationId: string): Promise<void> {
const operation = await this.getOperation(operationId);
if (!operation) {
throw new Error(`Operation ${operationId} not found`);
}
const absolutePath = plugins.path.isAbsolute(filepath)
? filepath
const absolutePath = plugins.path.isAbsolute(filepath)
? filepath
: plugins.path.join(paths.cwd, filepath);
// Check if file exists
const exists = await plugins.smartfile.fs.fileExists(absolutePath);
if (!exists) {
// File doesn't exist yet (will be created), so we skip backup
return;
}
// Read file content and metadata
const content = plugins.smartfile.fs.toStringSync(absolutePath);
const stats = await plugins.smartfile.fs.stat(absolutePath);
const checksum = this.calculateChecksum(content);
// Create backup
const backupPath = this.getBackupPath(operationId, filepath);
await plugins.smartfile.fs.ensureDir(plugins.path.dirname(backupPath));
await plugins.smartfile.memory.toFs(content, backupPath);
// Update operation
operation.files.push({
path: filepath,
originalContent: content,
checksum,
permissions: stats.mode.toString(8)
permissions: stats.mode.toString(8),
});
await this.updateManifest(operation);
}
async rollback(operationId: string): Promise<void> {
const operation = await this.getOperation(operationId);
if (!operation) {
throw new Error(`Operation ${operationId} not found`);
// Operation doesn't exist, might have already been rolled back or never created
console.warn(`Operation ${operationId} not found for rollback, skipping`);
return;
}
if (operation.status === 'rolled-back') {
throw new Error(`Operation ${operationId} has already been rolled back`);
}
// Restore files in reverse order
for (let i = operation.files.length - 1; i >= 0; i--) {
const file = operation.files[i];
const absolutePath = plugins.path.isAbsolute(file.path)
? file.path
const absolutePath = plugins.path.isAbsolute(file.path)
? file.path
: plugins.path.join(paths.cwd, file.path);
// Verify backup integrity
const backupPath = this.getBackupPath(operationId, file.path);
const backupContent = plugins.smartfile.fs.toStringSync(backupPath);
const backupChecksum = this.calculateChecksum(backupContent);
if (backupChecksum !== file.checksum) {
throw new Error(`Backup integrity check failed for ${file.path}`);
}
// Restore file
await plugins.smartfile.memory.toFs(file.originalContent, absolutePath);
// Restore permissions
const mode = parseInt(file.permissions, 8);
// Note: Permissions restoration may not work on all platforms
}
// Update operation status
operation.status = 'rolled-back';
await this.updateManifest(operation);
}
async markComplete(operationId: string): Promise<void> {
const operation = await this.getOperation(operationId);
if (!operation) {
throw new Error(`Operation ${operationId} not found`);
}
operation.status = 'completed';
await this.updateManifest(operation);
}
async cleanOldBackups(retentionDays: number): Promise<void> {
const manifest = await this.getManifest();
const cutoffTime = Date.now() - (retentionDays * 24 * 60 * 60 * 1000);
const operationsToDelete = manifest.operations.filter(op =>
op.timestamp < cutoffTime && op.status === 'completed'
const cutoffTime = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
const operationsToDelete = manifest.operations.filter(
(op) => op.timestamp < cutoffTime && op.status === 'completed',
);
for (const operation of operationsToDelete) {
// Remove backup files
const operationDir = plugins.path.join(this.backupDir, 'operations', operation.id);
const operationDir = plugins.path.join(
this.backupDir,
'operations',
operation.id,
);
await plugins.smartfile.fs.remove(operationDir);
// Remove from manifest
manifest.operations = manifest.operations.filter(op => op.id !== operation.id);
manifest.operations = manifest.operations.filter(
(op) => op.id !== operation.id,
);
}
await this.saveManifest(manifest);
}
async verifyBackup(operationId: string): Promise<boolean> {
const operation = await this.getOperation(operationId);
if (!operation) {
return false;
}
for (const file of operation.files) {
const backupPath = this.getBackupPath(operationId, file.path);
const exists = await plugins.smartfile.fs.fileExists(backupPath);
if (!exists) {
return false;
}
const content = plugins.smartfile.fs.toStringSync(backupPath);
const checksum = this.calculateChecksum(content);
if (checksum !== file.checksum) {
return false;
}
}
return true;
}
async listBackups(): Promise<IFormatOperation[]> {
const manifest = await this.getManifest();
return manifest.operations;
}
private async ensureBackupDir(): Promise<void> {
await plugins.smartfile.fs.ensureDir(this.backupDir);
await plugins.smartfile.fs.ensureDir(plugins.path.join(this.backupDir, 'operations'));
await plugins.smartfile.fs.ensureDir(
plugins.path.join(this.backupDir, 'operations'),
);
}
private generateOperationId(): string {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const random = Math.random().toString(36).substring(2, 8);
return `${timestamp}-${random}`;
}
private getBackupPath(operationId: string, filepath: string): string {
const filename = plugins.path.basename(filepath);
const dir = plugins.path.dirname(filepath);
const safeDir = dir.replace(/[/\\]/g, '__');
return plugins.path.join(this.backupDir, 'operations', operationId, 'files', safeDir, `${filename}.backup`);
return plugins.path.join(
this.backupDir,
'operations',
operationId,
'files',
safeDir,
`${filename}.backup`,
);
}
private calculateChecksum(content: string | Buffer): string {
return plugins.crypto.createHash('sha256').update(content).digest('hex');
}
private async getManifest(): Promise<{ operations: IFormatOperation[] }> {
const defaultManifest = { operations: [] };
const exists = await plugins.smartfile.fs.fileExists(this.manifestPath);
if (!exists) {
return defaultManifest;
}
try {
const content = plugins.smartfile.fs.toStringSync(this.manifestPath);
const manifest = JSON.parse(content);
// Validate the manifest structure
if (this.isValidManifest(manifest)) {
return manifest;
} else {
console.warn('Invalid rollback manifest structure, returning default manifest');
console.warn(
'Invalid rollback manifest structure, returning default manifest',
);
return defaultManifest;
}
} catch (error) {
console.warn(`Failed to read rollback manifest: ${error.message}, returning default manifest`);
console.warn(
`Failed to read rollback manifest: ${error.message}, returning default manifest`,
);
// Try to delete the corrupted file
try {
await plugins.smartfile.fs.remove(this.manifestPath);
@@ -214,85 +235,84 @@ export class RollbackManager {
return defaultManifest;
}
}
private async saveManifest(manifest: { operations: IFormatOperation[] }): Promise<void> {
private async saveManifest(manifest: {
operations: IFormatOperation[];
}): Promise<void> {
// Validate before saving
if (!this.isValidManifest(manifest)) {
throw new Error('Invalid rollback manifest structure, cannot save');
}
// Use atomic write: write to temp file, then move it
const tempPath = `${this.manifestPath}.tmp`;
try {
// Write to temporary file
const jsonContent = JSON.stringify(manifest, null, 2);
await plugins.smartfile.memory.toFs(jsonContent, tempPath);
// Move temp file to actual manifest (atomic-like operation)
// Since smartfile doesn't have rename, we copy and delete
await plugins.smartfile.fs.copy(tempPath, this.manifestPath);
await plugins.smartfile.fs.remove(tempPath);
} catch (error) {
// Clean up temp file if it exists
try {
await plugins.smartfile.fs.remove(tempPath);
} catch (removeError) {
// Ignore removal errors
}
throw error;
}
// Ensure directory exists
await this.ensureBackupDir();
// Write directly with proper JSON stringification
const jsonContent = JSON.stringify(manifest, null, 2);
await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath);
}
private async getOperation(operationId: string): Promise<IFormatOperation | null> {
private async getOperation(
operationId: string,
): Promise<IFormatOperation | null> {
const manifest = await this.getManifest();
return manifest.operations.find(op => op.id === operationId) || null;
return manifest.operations.find((op) => op.id === operationId) || null;
}
private async updateManifest(operation: IFormatOperation): Promise<void> {
const manifest = await this.getManifest();
const existingIndex = manifest.operations.findIndex(op => op.id === operation.id);
const existingIndex = manifest.operations.findIndex(
(op) => op.id === operation.id,
);
if (existingIndex !== -1) {
manifest.operations[existingIndex] = operation;
} else {
manifest.operations.push(operation);
}
await this.saveManifest(manifest);
}
private isValidManifest(manifest: any): manifest is { operations: IFormatOperation[] } {
private isValidManifest(
manifest: any,
): manifest is { operations: IFormatOperation[] } {
// Check if manifest has the required structure
if (!manifest || typeof manifest !== 'object') {
return false;
}
// Check required fields
if (!Array.isArray(manifest.operations)) {
return false;
}
// Check each operation entry
for (const operation of manifest.operations) {
if (!operation || typeof operation !== 'object' ||
typeof operation.id !== 'string' ||
typeof operation.timestamp !== 'number' ||
typeof operation.status !== 'string' ||
!Array.isArray(operation.files)) {
if (
!operation ||
typeof operation !== 'object' ||
typeof operation.id !== 'string' ||
typeof operation.timestamp !== 'number' ||
typeof operation.status !== 'string' ||
!Array.isArray(operation.files)
) {
return false;
}
// Check each file in the operation
for (const file of operation.files) {
if (!file || typeof file !== 'object' ||
typeof file.path !== 'string' ||
typeof file.checksum !== 'string') {
if (
!file ||
typeof file !== 'object' ||
typeof file.path !== 'string' ||
typeof file.checksum !== 'string'
) {
return false;
}
}
}
return true;
}
}
}

View File

@@ -4,14 +4,21 @@ import * as paths from '../paths.js';
import { logger } from '../gitzone.logging.js';
import { Project } from '../classes.project.js';
const filesToDelete = ['defaults.yml', 'yarn.lock', 'package-lock.json', 'tslint.json'];
const filesToDelete = [
'defaults.yml',
'yarn.lock',
'package-lock.json',
'tslint.json',
];
export const run = async (projectArg: Project) => {
for (const relativeFilePath of filesToDelete) {
const fileExists = plugins.smartfile.fs.fileExistsSync(relativeFilePath);
if (fileExists) {
logger.log('info', `Found ${relativeFilePath}! Removing it!`);
plugins.smartfile.fs.removeSync(plugins.path.join(paths.cwd, relativeFilePath));
plugins.smartfile.fs.removeSync(
plugins.path.join(paths.cwd, relativeFilePath),
);
} else {
logger.log('info', `Project is free of ${relativeFilePath}`);
}

View File

@@ -4,56 +4,59 @@ import { logger } from '../gitzone.logging.js';
export const run = async (projectArg: Project) => {
const gitzoneConfig = await projectArg.gitzoneConfig;
// Get copy configuration from npmextra.json
const npmextraConfig = new plugins.npmextra.Npmextra();
const copyConfig = npmextraConfig.dataFor<any>('gitzone.format.copy', {
patterns: []
patterns: [],
});
if (!copyConfig.patterns || copyConfig.patterns.length === 0) {
logger.log('info', 'No copy patterns configured in npmextra.json');
return;
}
for (const pattern of copyConfig.patterns) {
if (!pattern.from || !pattern.to) {
logger.log('warn', 'Invalid copy pattern - missing "from" or "to" field');
continue;
}
try {
// Handle glob patterns
const files = await plugins.smartfile.fs.listFileTree('.', pattern.from);
for (const file of files) {
const sourcePath = file;
let destPath = pattern.to;
// If destination is a directory, preserve filename
if (pattern.to.endsWith('/')) {
const filename = plugins.path.basename(file);
destPath = plugins.path.join(pattern.to, filename);
}
// Handle template variables in destination path
if (pattern.preservePath) {
const relativePath = plugins.path.relative(
plugins.path.dirname(pattern.from.replace(/\*/g, '')),
file
plugins.path.dirname(pattern.from.replace(/\*/g, '')),
file,
);
destPath = plugins.path.join(pattern.to, relativePath);
}
// Ensure destination directory exists
await plugins.smartfile.fs.ensureDir(plugins.path.dirname(destPath));
// Copy file
await plugins.smartfile.fs.copy(sourcePath, destPath);
logger.log('info', `Copied ${sourcePath} to ${destPath}`);
}
} catch (error) {
logger.log('error', `Failed to copy pattern ${pattern.from}: ${error.message}`);
logger.log(
'error',
`Failed to copy pattern ${pattern.from}: ${error.message}`,
);
}
}
};
@@ -79,4 +82,4 @@ export const run = async (projectArg: Project) => {
* }
* }
* }
*/
*/

View File

@@ -8,14 +8,40 @@ const gitignorePath = plugins.path.join(paths.cwd, './.gitignore');
export const run = async (projectArg: Project) => {
const gitignoreExists = await plugins.smartfile.fs.fileExists(gitignorePath);
const templateModule = await import('../mod_template/index.js');
const ciTemplate = await templateModule.getTemplate('gitignore');
let customContent = '';
if (gitignoreExists) {
// lets get the existing gitignore file
const existingGitIgnoreString = plugins.smartfile.fs.toStringSync(gitignorePath);
let customPart = existingGitIgnoreString.split('# custom\n')[1];
customPart ? null : (customPart = '');
const existingGitIgnoreString =
plugins.smartfile.fs.toStringSync(gitignorePath);
// Check for different custom section markers
const customMarkers = ['#------# custom', '# custom'];
for (const marker of customMarkers) {
const splitResult = existingGitIgnoreString.split(marker);
if (splitResult.length > 1) {
// Get everything after the marker (excluding the marker itself)
customContent = splitResult[1].trim();
break;
}
}
}
// Write the template
const templateModule = await import('../mod_template/index.js');
const ciTemplate = await templateModule.getTemplate('gitignore');
await ciTemplate.writeToDisk(paths.cwd);
// Append the custom content if it exists
if (customContent) {
const newGitignoreContent =
plugins.smartfile.fs.toStringSync(gitignorePath);
// The template already ends with "#------# custom", so just append the content
const finalContent =
newGitignoreContent.trimEnd() + '\n' + customContent + '\n';
await plugins.smartfile.fs.toFs(finalContent, gitignorePath);
logger.log('info', 'Updated .gitignore while preserving custom section!');
} else {
logger.log('info', 'Added a .gitignore!');
}
ciTemplate.writeToDisk(paths.cwd);
logger.log('info', 'Added a .gitignore!');
};

View File

@@ -24,7 +24,9 @@ export const run = async (projectArg: Project) => {
} else {
logger.log('error', 'Error -> licenses failed. Here is why:');
for (const failedModule of licenseCheckResult.failingModules) {
console.log(`${failedModule.name} fails with license ${failedModule.license}`);
console.log(
`${failedModule.name} fails with license ${failedModule.license}`,
);
}
}
};

View File

@@ -29,7 +29,12 @@ export const run = async (projectArg: Project) => {
const interactInstance = new plugins.smartinteract.SmartInteract();
for (const expectedRepoInformationItem of expectedRepoInformation) {
if (!plugins.smartobject.smartGet(npmextraJson.gitzone, expectedRepoInformationItem)) {
if (
!plugins.smartobject.smartGet(
npmextraJson.gitzone,
expectedRepoInformationItem,
)
) {
interactInstance.addQuestions([
{
message: `What is the value of ${expectedRepoInformationItem}`,
@@ -43,7 +48,9 @@ export const run = async (projectArg: Project) => {
const answerbucket = await interactInstance.runQueue();
for (const expectedRepoInformationItem of expectedRepoInformation) {
const cliProvidedValue = answerbucket.getAnswerFor(expectedRepoInformationItem);
const cliProvidedValue = answerbucket.getAnswerFor(
expectedRepoInformationItem,
);
if (cliProvidedValue) {
plugins.smartobject.smartAdd(
npmextraJson.gitzone,

View File

@@ -19,7 +19,7 @@ const ensureDependency = async (
: [dependencyArg, 'latest'];
const targetSections: string[] = [];
switch (position) {
case 'dep':
targetSections.push('dependencies');
@@ -43,7 +43,8 @@ const ensureDependency = async (
break;
case 'include':
if (!packageJsonObjectArg[section][packageName]) {
packageJsonObjectArg[section][packageName] = version === 'latest' ? '^1.0.0' : version;
packageJsonObjectArg[section][packageName] =
version === 'latest' ? '^1.0.0' : version;
}
break;
case 'latest':
@@ -54,9 +55,13 @@ const ensureDependency = async (
const latestVersion = packageInfo['dist-tags'].latest;
packageJsonObjectArg[section][packageName] = `^${latestVersion}`;
} catch (error) {
logger.log('warn', `Could not fetch latest version for ${packageName}, using existing or default`);
logger.log(
'warn',
`Could not fetch latest version for ${packageName}, using existing or default`,
);
if (!packageJsonObjectArg[section][packageName]) {
packageJsonObjectArg[section][packageName] = version === 'latest' ? '^1.0.0' : version;
packageJsonObjectArg[section][packageName] =
version === 'latest' ? '^1.0.0' : version;
}
}
break;
@@ -78,10 +83,10 @@ export const run = async (projectArg: Project) => {
type: 'git',
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}.git`,
};
(packageJson.bugs = {
((packageJson.bugs = {
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}/issues`,
}),
(packageJson.homepage = `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}#readme`);
(packageJson.homepage = `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}#readme`));
// Check for module type
if (!packageJson.type) {
@@ -91,9 +96,15 @@ export const run = async (projectArg: Project) => {
// Check for private or public
if (packageJson.private !== undefined) {
logger.log('info', 'Success -> found private/public info in package.json!');
logger.log(
'info',
'Success -> found private/public info in package.json!',
);
} else {
logger.log('error', 'found no private boolean! Setting it to private for now!');
logger.log(
'error',
'found no private boolean! Setting it to private for now!',
);
packageJson.private = true;
}
@@ -101,7 +112,10 @@ export const run = async (projectArg: Project) => {
if (packageJson.license) {
logger.log('info', 'Success -> found license in package.json!');
} else {
logger.log('error', 'found no license! Setting it to UNLICENSED for now!');
logger.log(
'error',
'found no license! Setting it to UNLICENSED for now!',
);
packageJson.license = 'UNLICENSED';
}
@@ -109,13 +123,19 @@ export const run = async (projectArg: Project) => {
if (packageJson.scripts.build) {
logger.log('info', 'Success -> found build script in package.json!');
} else {
logger.log('error', 'found no build script! Putting a placeholder there for now!');
logger.log(
'error',
'found no build script! Putting a placeholder there for now!',
);
packageJson.scripts.build = `echo "Not needed for now"`;
}
// Check for buildDocs script
if (!packageJson.scripts.buildDocs) {
logger.log('info', 'found no buildDocs script! Putting tsdoc script there now.');
logger.log(
'info',
'found no buildDocs script! Putting tsdoc script there now.',
);
packageJson.scripts.buildDocs = `tsdoc`;
}
@@ -134,9 +154,24 @@ export const run = async (projectArg: Project) => {
];
// check for dependencies
await ensureDependency(packageJson, 'devDep', 'latest', '@push.rocks/tapbundle');
await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tstest');
await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tsbuild');
await ensureDependency(
packageJson,
'devDep',
'latest',
'@push.rocks/tapbundle',
);
await ensureDependency(
packageJson,
'devDep',
'latest',
'@git.zone/tstest',
);
await ensureDependency(
packageJson,
'devDep',
'latest',
'@git.zone/tsbuild',
);
// set overrides
const overrides = plugins.smartfile.fs.toObjectSync(

View File

@@ -16,7 +16,12 @@ const prettierDefaultMarkdownConfig: prettier.Options = {
parser: 'markdown',
};
const filesToFormat = [`ts/**/*.ts`, `test/**/*.ts`, `readme.md`, `docs/**/*.md`];
const filesToFormat = [
`ts/**/*.ts`,
`test/**/*.ts`,
`readme.md`,
`docs/**/*.md`,
];
const choosePrettierConfig = (fileArg: plugins.smartfile.SmartFile) => {
switch (fileArg.parsedPath.ext) {
@@ -39,7 +44,10 @@ const prettierTypeScriptPipestop = plugins.through2.obj(
cb(null);
} else {
logger.log('info', `${fileArg.path} is being reformated!`);
const formatedFileString = await prettier.format(fileString, chosenConfig);
const formatedFileString = await prettier.format(
fileString,
chosenConfig,
);
fileArg.setContentsFromString(formatedFileString);
cb(null, fileArg);
}

View File

@@ -18,7 +18,8 @@ export const run = async () => {
}
// Check and initialize readme.hints.md if it doesn't exist
const readmeHintsExists = await plugins.smartfile.fs.fileExists(readmeHintsPath);
const readmeHintsExists =
await plugins.smartfile.fs.fileExists(readmeHintsPath);
if (!readmeHintsExists) {
await plugins.smartfile.fs.toFs(
'# Project Readme Hints\n\nThis is the initial readme hints file.',

View File

@@ -26,10 +26,12 @@ export const run = async (project: Project) => {
case 'npm':
case 'wcc':
if (project.gitzoneConfig.data.npmciOptions.npmAccessLevel === 'public') {
const ciTemplateDefault = await templateModule.getTemplate('ci_default');
const ciTemplateDefault =
await templateModule.getTemplate('ci_default');
ciTemplateDefault.writeToDisk(paths.cwd);
} else {
const ciTemplateDefault = await templateModule.getTemplate('ci_default_private');
const ciTemplateDefault =
await templateModule.getTemplate('ci_default_private');
ciTemplateDefault.writeToDisk(paths.cwd);
}
logger.log('info', 'Updated .gitlabci.yml!');
@@ -41,7 +43,8 @@ export const run = async (project: Project) => {
logger.log('info', 'Updated CI/CD config files!');
// lets care about docker
const dockerTemplate = await templateModule.getTemplate('dockerfile_service');
const dockerTemplate =
await templateModule.getTemplate('dockerfile_service');
dockerTemplate.writeToDisk(paths.cwd);
logger.log('info', 'Updated Dockerfile!');
@@ -56,17 +59,22 @@ export const run = async (project: Project) => {
// update html
if (project.gitzoneConfig.data.projectType === 'website') {
const websiteUpdateTemplate = await templateModule.getTemplate('website_update');
const variables ={
const websiteUpdateTemplate =
await templateModule.getTemplate('website_update');
const variables = {
assetbrokerUrl: project.gitzoneConfig.data.module.assetbrokerUrl,
legalUrl: project.gitzoneConfig.data.module.legalUrl,
};
console.log('updating website template with variables\n', JSON.stringify(variables, null, 2));
console.log(
'updating website template with variables\n',
JSON.stringify(variables, null, 2),
);
websiteUpdateTemplate.supplyVariables(variables);
await websiteUpdateTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated html for website!`);
} else if (project.gitzoneConfig.data.projectType === 'service') {
const websiteUpdateTemplate = await templateModule.getTemplate('service_update');
const websiteUpdateTemplate =
await templateModule.getTemplate('service_update');
await websiteUpdateTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated html for element template!`);
} else if (project.gitzoneConfig.data.projectType === 'wcc') {

View File

@@ -19,8 +19,12 @@ export const run = async (projectArg: Project) => {
const publishModules = await tsPublishInstance.getModuleSubDirs(paths.cwd);
for (const publishModule of Object.keys(publishModules)) {
const publishConfig = publishModules[publishModule];
tsconfigObject.compilerOptions.paths[`${publishConfig.name}`] = [`./${publishModule}/index.js`];
tsconfigObject.compilerOptions.paths[`${publishConfig.name}`] = [
`./${publishModule}/index.js`,
];
}
tsconfigSmartfile.setContentsFromString(JSON.stringify(tsconfigObject, null, 2));
tsconfigSmartfile.setContentsFromString(
JSON.stringify(tsconfigObject, null, 2),
);
await tsconfigSmartfile.write();
};

View File

@@ -7,13 +7,18 @@ export class CleanupFormatter extends BaseFormatter {
get name(): string {
return 'cleanup';
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
// List of files to remove
const filesToRemove = ['yarn.lock', 'package-lock.json', 'tslint.json', 'defaults.yml'];
const filesToRemove = [
'yarn.lock',
'package-lock.json',
'tslint.json',
'defaults.yml',
];
for (const file of filesToRemove) {
const exists = await plugins.smartfile.fs.fileExists(file);
if (exists) {
@@ -21,14 +26,14 @@ export class CleanupFormatter extends BaseFormatter {
type: 'delete',
path: file,
module: this.name,
description: `Remove obsolete file`
description: `Remove obsolete file`,
});
}
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
switch (change.type) {
case 'delete':
@@ -36,4 +41,4 @@ export class CleanupFormatter extends BaseFormatter {
break;
}
}
}
}

View File

@@ -5,4 +5,4 @@ export class CopyFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'copy', formatCopy);
}
}
}

View File

@@ -5,4 +5,4 @@ export class GitignoreFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'gitignore', formatGitignore);
}
}
}

View File

@@ -7,30 +7,37 @@ import * as plugins from '../mod.plugins.js';
export class LegacyFormatter extends BaseFormatter {
private moduleName: string;
private formatModule: any;
constructor(context: any, project: Project, moduleName: string, formatModule: any) {
constructor(
context: any,
project: Project,
moduleName: string,
formatModule: any,
) {
super(context, project);
this.moduleName = moduleName;
this.formatModule = formatModule;
}
get name(): string {
return this.moduleName;
}
async analyze(): Promise<IPlannedChange[]> {
// For legacy modules, we can't easily predict changes
// So we'll return a generic change that indicates the module will run
return [{
type: 'modify',
path: '<various files>',
module: this.name,
description: `Run ${this.name} formatter`
}];
return [
{
type: 'modify',
path: '<various files>',
module: this.name,
description: `Run ${this.name} formatter`,
},
];
}
async applyChange(change: IPlannedChange): Promise<void> {
// Run the legacy format module
await this.formatModule.run(this.project);
}
}
}

View File

@@ -5,4 +5,4 @@ export class LicenseFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'license', formatLicense);
}
}
}

View File

@@ -5,4 +5,4 @@ export class NpmextraFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'npmextra', formatNpmextra);
}
}
}

View File

@@ -5,4 +5,4 @@ export class PackageJsonFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'packagejson', formatPackageJson);
}
}
}

View File

@@ -7,21 +7,16 @@ export class PrettierFormatter extends BaseFormatter {
get name(): string {
return 'prettier';
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
// Define directories to format (TypeScript directories by default)
const includeDirs = [
'ts',
'ts_*',
'test',
'tests'
];
const includeDirs = ['ts', 'ts_*', 'test', 'tests'];
// File extensions to format
const extensions = '{ts,tsx,js,jsx,json,md,css,scss,html,xml,yaml,yml}';
// Also format root-level config files
const rootConfigFiles = [
'package.json',
@@ -34,35 +29,39 @@ export class PrettierFormatter extends BaseFormatter {
'README.md',
'changelog.md',
'CHANGELOG.md',
'license',
'LICENSE',
'*.md'
// Skip files without extensions as prettier can't infer parser
// 'license',
// 'LICENSE',
'*.md',
];
// Collect all files to format
const allFiles: string[] = [];
// Add files from TypeScript directories
for (const dir of includeDirs) {
const globPattern = `${dir}/**/*.${extensions}`;
const dirFiles = await plugins.smartfile.fs.listFileTree('.', globPattern);
const dirFiles = await plugins.smartfile.fs.listFileTree(
'.',
globPattern,
);
allFiles.push(...dirFiles);
}
// Add root config files
for (const pattern of rootConfigFiles) {
const rootFiles = await plugins.smartfile.fs.listFileTree('.', pattern);
// Only include files at root level (no slashes in path)
const rootLevelFiles = rootFiles.filter(f => !f.includes('/'));
const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
allFiles.push(...rootLevelFiles);
}
// Remove duplicates
const uniqueFiles = [...new Set(allFiles)];
// Get all files that match the pattern
const files = uniqueFiles;
// Ensure we only process actual files (not directories)
const validFiles: string[] = [];
for (const file of files) {
@@ -76,101 +75,138 @@ export class PrettierFormatter extends BaseFormatter {
logVerbose(`Skipping ${file} - cannot access: ${error.message}`);
}
}
// Check which files need formatting
for (const file of validFiles) {
// Skip files that haven't changed
if (!await this.shouldProcessFile(file)) {
if (!(await this.shouldProcessFile(file))) {
logVerbose(`Skipping ${file} - no changes detected`);
continue;
}
changes.push({
type: 'modify',
path: file,
module: this.name,
description: 'Format with Prettier'
description: 'Format with Prettier',
});
}
logger.log('info', `Found ${changes.length} files to format with Prettier`);
return changes;
}
async execute(changes: IPlannedChange[]): Promise<void> {
const startTime = this.stats.moduleStartTime(this.name);
this.stats.startModule(this.name);
try {
await this.preExecute();
// Batch process files
const batchSize = 10; // Process 10 files at a time
const batches: IPlannedChange[][] = [];
for (let i = 0; i < changes.length; i += batchSize) {
batches.push(changes.slice(i, i + batchSize));
logVerbose(`Processing ${changes.length} files sequentially`);
// Process files sequentially to avoid prettier cache/state issues
for (let i = 0; i < changes.length; i++) {
const change = changes[i];
logVerbose(
`Processing file ${i + 1}/${changes.length}: ${change.path}`,
);
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
}
}
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();
// Rollback removed - no longer tracking operations
throw error;
} finally {
this.stats.endModule(this.name, startTime);
}
}
async applyChange(change: IPlannedChange): Promise<void> {
if (change.type !== 'modify') return;
try {
// Validate the path before processing
if (!change.path || change.path.trim() === '') {
logger.log(
'error',
`Invalid empty path in change: ${JSON.stringify(change)}`,
);
throw new Error('Invalid empty path');
}
// Read current content
const content = plugins.smartfile.fs.toStringSync(change.path);
// Format with prettier
const prettier = await import('prettier');
const formatted = await prettier.format(content, {
filepath: change.path,
...(await this.getPrettierConfig())
});
// 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}`);
// Skip files that prettier can't parse without explicit parser
const fileExt = plugins.path.extname(change.path).toLowerCase();
if (!fileExt || fileExt === '') {
// Files without extensions need explicit parser
logVerbose(
`Skipping ${change.path} - no file extension for parser inference`,
);
return;
}
try {
const formatted = await prettier.format(content, {
filepath: change.path,
...(await this.getPrettierConfig()),
});
// Only write if content actually changed
if (formatted !== content) {
// Debug: log the path being written
logVerbose(`Writing formatted content to: ${change.path}`);
await this.modifyFile(change.path, formatted);
logVerbose(`Formatted ${change.path}`);
} else {
logVerbose(`No formatting changes for ${change.path}`);
}
} catch (prettierError) {
// Check if it's a parser error
if (
prettierError.message &&
prettierError.message.includes('No parser could be inferred')
) {
logVerbose(`Skipping ${change.path} - ${prettierError.message}`);
return; // Skip this file silently
}
throw prettierError;
}
} catch (error) {
logger.log('error', `Failed to format ${change.path}: ${error.message}`);
// Log the full error stack for debugging mkdir issues
if (error.message && error.message.includes('mkdir')) {
logger.log(
'error',
`Failed to format ${change.path}: ${error.message}`,
);
logger.log('error', `Error stack: ${error.stack}`);
} else {
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();
@@ -181,7 +217,7 @@ export class PrettierFormatter extends BaseFormatter {
printWidth: 80,
tabWidth: 2,
semi: true,
arrowParens: 'always'
arrowParens: 'always',
});
}
}
}

View File

@@ -6,17 +6,19 @@ export class ReadmeFormatter extends BaseFormatter {
get name(): string {
return 'readme';
}
async analyze(): Promise<IPlannedChange[]> {
return [{
type: 'modify',
path: 'readme.md',
module: this.name,
description: 'Ensure readme files exist'
}];
return [
{
type: 'modify',
path: 'readme.md',
module: this.name,
description: 'Ensure readme files exist',
},
];
}
async applyChange(change: IPlannedChange): Promise<void> {
await formatReadme.run();
}
}
}

View File

@@ -5,4 +5,4 @@ export class TemplatesFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'templates', formatTemplates);
}
}
}

View File

@@ -5,4 +5,4 @@ export class TsconfigFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'tsconfig', formatTsconfig);
}
}
}

View File

@@ -16,27 +16,29 @@ import { PrettierFormatter } from './formatters/prettier.formatter.js';
import { ReadmeFormatter } from './formatters/readme.formatter.js';
import { CopyFormatter } from './formatters/copy.formatter.js';
export let run = async (options: {
dryRun?: boolean;
yes?: boolean;
planOnly?: boolean;
savePlan?: string;
fromPlan?: string;
detailed?: boolean;
interactive?: boolean;
parallel?: boolean;
verbose?: boolean;
} = {}): Promise<any> => {
export let run = async (
options: {
dryRun?: boolean;
yes?: boolean;
planOnly?: boolean;
savePlan?: string;
fromPlan?: string;
detailed?: boolean;
interactive?: boolean;
parallel?: boolean;
verbose?: boolean;
} = {},
): Promise<any> => {
// Set verbose mode if requested
if (options.verbose) {
setVerboseMode(true);
}
const project = await Project.fromCwd();
const context = new FormatContext();
await context.initializeCache(); // Initialize the cache system
// Cache system removed - no longer needed
const planner = new FormatPlanner();
// Get configuration from npmextra
const npmextraConfig = new plugins.npmextra.Npmextra();
const formatConfig = npmextraConfig.dataFor<any>('gitzone.format', {
@@ -49,30 +51,27 @@ export let run = async (options: {
autoRollbackOnError: true,
backupRetentionDays: 7,
maxBackupSize: '100MB',
excludePatterns: ['node_modules/**', '.git/**']
excludePatterns: ['node_modules/**', '.git/**'],
},
modules: {
skip: [],
only: [],
order: []
order: [],
},
parallel: true,
cache: {
enabled: true,
clean: true // Clean invalid entries from cache
}
clean: true, // Clean invalid entries from cache
},
});
// Clean cache if configured
if (formatConfig.cache.clean) {
await context.getChangeCache().clean();
}
// Cache cleaning removed - no longer using cache system
// Override config with command options
const interactive = options.interactive ?? formatConfig.interactive;
const autoApprove = options.yes ?? formatConfig.autoApprove;
const parallel = options.parallel ?? formatConfig.parallel;
try {
// Initialize formatters
const formatters = [
@@ -87,9 +86,9 @@ export let run = async (options: {
new ReadmeFormatter(context, project),
new CopyFormatter(context, project),
];
// Filter formatters based on configuration
const activeFormatters = formatters.filter(formatter => {
const activeFormatters = formatters.filter((formatter) => {
if (formatConfig.modules.only.length > 0) {
return formatConfig.modules.only.includes(formatter.name);
}
@@ -98,33 +97,36 @@ export let run = async (options: {
}
return true;
});
// Plan phase
logger.log('info', 'Analyzing project for format operations...');
let plan = options.fromPlan
? JSON.parse(await plugins.smartfile.fs.toStringSync(options.fromPlan))
: await planner.planFormat(activeFormatters);
// Display plan
await planner.displayPlan(plan, options.detailed);
// Save plan if requested
if (options.savePlan) {
await plugins.smartfile.memory.toFs(JSON.stringify(plan, null, 2), options.savePlan);
await plugins.smartfile.memory.toFs(
JSON.stringify(plan, null, 2),
options.savePlan,
);
logger.log('info', `Plan saved to ${options.savePlan}`);
}
// Exit if plan-only mode
if (options.planOnly) {
return;
}
// Dry-run mode
if (options.dryRun) {
logger.log('info', 'Dry-run mode - no changes will be made');
return;
}
// Interactive confirmation
if (interactive && !autoApprove) {
const interactInstance = new plugins.smartinteract.SmartInteract();
@@ -132,117 +134,59 @@ export let run = async (options: {
type: 'confirm',
name: 'proceed',
message: 'Proceed with formatting?',
default: true
default: true,
});
if (!(response as any).value) {
logger.log('info', 'Format operation cancelled by user');
return;
}
}
// Execute phase
logger.log('info', `Executing format operations${parallel ? ' in parallel' : ' sequentially'}...`);
logger.log(
'info',
`Executing format operations${parallel ? ' in parallel' : ' sequentially'}...`,
);
await planner.executePlan(plan, activeFormatters, context, parallel);
// Finish statistics tracking
context.getFormatStats().finish();
// Display statistics
const showStats = npmextraConfig.dataFor('gitzone.format.showStats', true);
if (showStats) {
context.getFormatStats().displayStats();
}
// Save stats if requested
if (options.detailed) {
const statsPath = `.nogit/format-stats-${Date.now()}.json`;
await context.getFormatStats().saveReport(statsPath);
}
logger.log('success', 'Format operations completed successfully!');
} catch (error) {
logger.log('error', `Format operation failed: ${error.message}`);
// Automatic rollback if enabled
if (formatConfig.rollback.enabled && formatConfig.rollback.autoRollbackOnError) {
logger.log('info', 'Attempting automatic rollback...');
try {
await context.rollbackOperation();
logger.log('success', 'Rollback completed successfully');
} catch (rollbackError) {
logger.log('error', `Rollback failed: ${rollbackError.message}`);
}
}
// Rollback system has been removed for stability
throw error;
}
};
// Export CLI command handlers
export const handleRollback = async (operationId?: string): Promise<void> => {
const context = new FormatContext();
const rollbackManager = context.getRollbackManager();
if (!operationId) {
// Rollback to last operation
const backups = await rollbackManager.listBackups();
const lastOperation = backups
.filter(op => op.status !== 'rolled-back')
.sort((a, b) => b.timestamp - a.timestamp)[0];
if (!lastOperation) {
logger.log('warn', 'No operations available for rollback');
return;
}
operationId = lastOperation.id;
}
try {
await rollbackManager.rollback(operationId);
logger.log('success', `Successfully rolled back operation ${operationId}`);
} catch (error) {
logger.log('error', `Rollback failed: ${error.message}`);
throw error;
}
logger.log('info', 'Rollback system has been disabled for stability');
};
export const handleListBackups = async (): Promise<void> => {
const context = new FormatContext();
const rollbackManager = context.getRollbackManager();
const backups = await rollbackManager.listBackups();
if (backups.length === 0) {
logger.log('info', 'No backup operations found');
return;
}
console.log('\nAvailable backups:');
console.log('━'.repeat(50));
for (const backup of backups) {
const date = new Date(backup.timestamp).toLocaleString();
const status = backup.status;
const filesCount = backup.files.length;
console.log(`ID: ${backup.id}`);
console.log(`Date: ${date}`);
console.log(`Status: ${status}`);
console.log(`Files: ${filesCount}`);
console.log('─'.repeat(50));
}
logger.log('info', 'Backup system has been disabled for stability');
};
export const handleCleanBackups = async (): Promise<void> => {
const context = new FormatContext();
const rollbackManager = context.getRollbackManager();
// Get retention days from config
const npmextraConfig = new plugins.npmextra.Npmextra();
const retentionDays = npmextraConfig.dataFor<any>('gitzone.format.rollback.backupRetentionDays', 7);
await rollbackManager.cleanOldBackups(retentionDays);
logger.log('success', `Cleaned backups older than ${retentionDays} days`);
};
logger.log(
'info',
'Backup cleaning has been disabled - backup system removed',
);
};

View File

@@ -9,7 +9,7 @@ export type IFormatOperation = {
}>;
status: 'pending' | 'in-progress' | 'completed' | 'failed' | 'rolled-back';
error?: Error;
}
};
export type IFormatPlan = {
summary: {
@@ -32,7 +32,7 @@ export type IFormatPlan = {
message: string;
module: string;
}>;
}
};
export type IPlannedChange = {
type: 'create' | 'modify' | 'delete';
@@ -42,4 +42,4 @@ export type IPlannedChange = {
content?: string; // For create/modify operations
diff?: string;
size?: number;
}
};

View File

@@ -35,7 +35,10 @@ export class Meta {
* sorts the metaRepoData
*/
public async sortMetaRepoData() {
const stringifiedMetadata = plugins.smartjson.stringify(this.metaRepoData, []);
const stringifiedMetadata = plugins.smartjson.stringify(
this.metaRepoData,
[],
);
this.metaRepoData = plugins.smartjson.parse(stringifiedMetadata);
}
@@ -45,11 +48,15 @@ export class Meta {
public async readDirectory() {
await this.syncToRemote(true);
logger.log('info', `reading directory`);
const metaFileExists = plugins.smartfile.fs.fileExistsSync(this.filePaths.metaJson);
const metaFileExists = plugins.smartfile.fs.fileExistsSync(
this.filePaths.metaJson,
);
if (!metaFileExists) {
throw new Error(`meta file does not exist at ${this.filePaths.metaJson}`);
}
this.metaRepoData = plugins.smartfile.fs.toObjectSync(this.filePaths.metaJson);
this.metaRepoData = plugins.smartfile.fs.toObjectSync(
this.filePaths.metaJson,
);
}
/**
@@ -76,7 +83,10 @@ export class Meta {
this.filePaths.metaJson,
);
// write .gitignore to disk
plugins.smartfile.memory.toFsSync(await this.generateGitignore(), this.filePaths.gitIgnore);
plugins.smartfile.memory.toFsSync(
await this.generateGitignore(),
this.filePaths.gitIgnore,
);
}
/**
@@ -84,13 +94,17 @@ export class Meta {
*/
public async syncToRemote(gitCleanArg = false) {
logger.log('info', `syncing from origin master`);
await this.smartshellInstance.exec(`cd ${this.cwd} && git pull origin master`);
await this.smartshellInstance.exec(
`cd ${this.cwd} && git pull origin master`,
);
if (gitCleanArg) {
logger.log('info', `cleaning the repository from old directories`);
await this.smartshellInstance.exec(`cd ${this.cwd} && git clean -fd`);
}
logger.log('info', `syncing to remote origin master`);
await this.smartshellInstance.exec(`cd ${this.cwd} && git push origin master`);
await this.smartshellInstance.exec(
`cd ${this.cwd} && git push origin master`,
);
}
/**
@@ -98,7 +112,9 @@ export class Meta {
*/
public async updateLocalRepos() {
await this.syncToRemote();
const projects = plugins.smartfile.fs.toObjectSync(this.filePaths.metaJson).projects;
const projects = plugins.smartfile.fs.toObjectSync(
this.filePaths.metaJson,
).projects;
const preExistingFolders = plugins.smartfile.fs.listFoldersSync(this.cwd);
for (const preExistingFolderArg of preExistingFolders) {
if (
@@ -107,14 +123,18 @@ export class Meta {
projectFolder.startsWith(preExistingFolderArg),
)
) {
const response = await plugins.smartinteraction.SmartInteract.getCliConfirmation(
`Do you want to delete superfluous directory >>${preExistingFolderArg}<< ?`,
true,
);
const response =
await plugins.smartinteraction.SmartInteract.getCliConfirmation(
`Do you want to delete superfluous directory >>${preExistingFolderArg}<< ?`,
true,
);
if (response) {
logger.log('warn', `Deleting >>${preExistingFolderArg}<<!`);
} else {
logger.log('warn', `Not deleting ${preExistingFolderArg} by request!`);
logger.log(
'warn',
`Not deleting ${preExistingFolderArg} by request!`,
);
}
}
}
@@ -160,7 +180,9 @@ export class Meta {
*/
public async initProject() {
await this.syncToRemote(true);
const fileExists = await plugins.smartfile.fs.fileExists(this.filePaths.metaJson);
const fileExists = await plugins.smartfile.fs.fileExists(
this.filePaths.metaJson,
);
if (!fileExists) {
await plugins.smartfile.memory.toFs(
JSON.stringify({
@@ -168,7 +190,10 @@ export class Meta {
}),
this.filePaths.metaJson,
);
logger.log(`success`, `created a new .meta.json in directory ${this.cwd}`);
logger.log(
`success`,
`created a new .meta.json in directory ${this.cwd}`,
);
await plugins.smartfile.memory.toFs(
JSON.stringify({
name: this.dirName,
@@ -176,9 +201,15 @@ export class Meta {
}),
this.filePaths.packageJson,
);
logger.log(`success`, `created a new package.json in directory ${this.cwd}`);
logger.log(
`success`,
`created a new package.json in directory ${this.cwd}`,
);
} else {
logger.log(`error`, `directory ${this.cwd} already has a .metaJson file. Doing nothing.`);
logger.log(
`error`,
`directory ${this.cwd} already has a .metaJson file. Doing nothing.`,
);
}
await this.smartshellInstance.exec(
`cd ${this.cwd} && git add -A && git commit -m "feat(project): init meta project for ${this.dirName}"`,
@@ -195,7 +226,9 @@ export class Meta {
const existingProject = this.metaRepoData.projects[projectNameArg];
if (existingProject) {
throw new Error('Project already exists! Please remove it first before adding it again.');
throw new Error(
'Project already exists! Please remove it first before adding it again.',
);
}
this.metaRepoData.projects[projectNameArg] = gitUrlArg;
@@ -217,7 +250,10 @@ export class Meta {
const existingProject = this.metaRepoData.projects[projectNameArg];
if (!existingProject) {
logger.log('error', `Project ${projectNameArg} does not exist! So it cannot be removed`);
logger.log(
'error',
`Project ${projectNameArg} does not exist! So it cannot be removed`,
);
return;
}
@@ -228,7 +264,9 @@ export class Meta {
await this.writeToDisk();
logger.log('info', 'removing directory from cwd');
await plugins.smartfile.fs.remove(plugins.path.join(paths.cwd, projectNameArg));
await plugins.smartfile.fs.remove(
plugins.path.join(paths.cwd, projectNameArg),
);
await this.updateLocalRepos();
}
}

View File

@@ -16,7 +16,9 @@ export let run = () => {
* create a new project with 'gitzone template [template]'
the following templates exist: ${(() => {
let projects = `\n`;
for (const template of plugins.smartfile.fs.listFoldersSync(paths.templatesDir)) {
for (const template of plugins.smartfile.fs.listFoldersSync(
paths.templatesDir,
)) {
projects += ` - ${template}\n`;
}
return projects;

View File

@@ -15,7 +15,9 @@ export const run = async (argvArg: any) => {
});
await smartshellInstance.execStrict(`cd ${paths.cwd} && git checkout master`);
await smartshellInstance.execStrict(`cd ${paths.cwd} && git pull origin master`);
await smartshellInstance.execStrict(
`cd ${paths.cwd} && git pull origin master`,
);
await smartshellInstance.execStrict(`cd ${paths.cwd} && npm ci`);
await provideNoGitFiles();

View File

@@ -16,7 +16,9 @@ export const isTemplate = async (templateNameArg: string) => {
export const getTemplate = async (templateNameArg: string) => {
if (isTemplate(templateNameArg)) {
const localScafTemplate = new plugins.smartscaf.ScafTemplate(getTemplatePath(templateNameArg));
const localScafTemplate = new plugins.smartscaf.ScafTemplate(
getTemplatePath(templateNameArg),
);
await localScafTemplate.readTemplateFromDir();
return localScafTemplate;
} else {
@@ -32,7 +34,8 @@ export const run = async (argvArg: any) => {
const answerBucket = await smartinteract.askQuestion({
type: 'list',
default: 'npm',
message: 'What template do you want to scaffold? (Only showing mpost common options)',
message:
'What template do you want to scaffold? (Only showing mpost common options)',
name: 'templateName',
choices: ['npm', 'service', 'wcc', 'website'],
});

View File

@@ -10,7 +10,5 @@
"baseUrl": ".",
"paths": {}
},
"exclude": [
"dist_*/**/*.d.ts"
]
}
"exclude": ["dist_*/**/*.d.ts"]
}