Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad7f2742ff | |||
| b234ecc12a | |||
| 278df40ba7 | |||
| 6f0928e7c7 | |||
| 26effadcc9 | |||
| c38e94bcf3 | |||
| b9b51f29d1 | |||
| a3ad48368d | |||
| c10b764c0a | |||
| 7686504e4e | |||
| d96b220703 | |||
| 06f2de3230 | |||
| cc3128f07b | |||
| 358d677e72 | |||
| f421c5851d | |||
| a420157287 | |||
| 0e27d54ad2 | |||
| 738fbaa64f | |||
| fe7a9d93d1 | |||
| 9a4c8795d4 | |||
| faee6a1698 | |||
| 9a1044783d | |||
| b16eb75d81 | |||
| 261f7ee6b2 | |||
| fd7a73398c | |||
| f43f88a3cb | |||
| 4c86ad62fb | |||
| 4214a1fdf1 | |||
| 1c33735799 | |||
| 274405e364 | |||
| bf858c8650 | |||
| b257c82bd6 | |||
| 5a1f6d8c76 | |||
| d44ad6e4e4 | |||
| 142adfd396 | |||
| b55e75d169 | |||
| d0d922e53b | |||
| eda67395fe | |||
| 470e87eb79 | |||
| 3358a0eacc | |||
| b65fac6257 | |||
| 4ab59609e6 | |||
| 32f106291f | |||
| b8aa5d61f6 | |||
| 71759c276e | |||
| 7938f12d43 | |||
| 3722258d69 | |||
| 68859d0e97 | |||
| ecadbc7a86 | |||
| 0243bc5ec7 | |||
| 92e618104f | |||
| c089c1f80d | |||
| 10a394c7d8 | |||
| 5980308bb8 | |||
| 398e36bdf7 | |||
| 1e78517547 | |||
| 55700ad87e | |||
| 773df5268b | |||
| b51fa88283 | |||
| cb9f717d54 | |||
| 70be11894c | |||
| 89ab63b153 |
@@ -1,13 +1,12 @@
|
|||||||
{
|
{
|
||||||
"szci": {
|
"@ship.zone/szci": {
|
||||||
"npmGlobalTools": [],
|
"npmGlobalTools": []
|
||||||
"npmAccessLevel": "private",
|
|
||||||
"npmRegistryUrl": "verdaccio.lossless.one"
|
|
||||||
},
|
},
|
||||||
"tsdoc": {
|
"@git.zone/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"
|
"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"
|
||||||
},
|
},
|
||||||
"@git.zone/cli": {
|
"@git.zone/cli": {
|
||||||
|
"schemaVersion": 2,
|
||||||
"projectType": "npm",
|
"projectType": "npm",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "gitlab.com",
|
"githost": "gitlab.com",
|
||||||
@@ -35,12 +34,39 @@
|
|||||||
"CI/CD"
|
"CI/CD"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"commit": {
|
||||||
|
"confirmation": "prompt",
|
||||||
|
"steps": ["analyze", "changelog", "commit"]
|
||||||
|
},
|
||||||
"release": {
|
"release": {
|
||||||
"registries": [
|
"confirmation": "prompt",
|
||||||
"https://verdaccio.lossless.digital",
|
"preflight": {
|
||||||
"https://registry.npmjs.org"
|
"requireCleanTree": true,
|
||||||
],
|
"test": false,
|
||||||
"accessLevel": "public"
|
"build": true
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"git": {
|
||||||
|
"enabled": true,
|
||||||
|
"remote": "origin",
|
||||||
|
"pushBranch": true,
|
||||||
|
"pushTags": true
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"enabled": true,
|
||||||
|
"registries": [
|
||||||
|
"https://verdaccio.lossless.digital",
|
||||||
|
"https://registry.npmjs.org"
|
||||||
|
],
|
||||||
|
"accessLevel": "public",
|
||||||
|
"alreadyPublished": "success"
|
||||||
|
},
|
||||||
|
"docker": {
|
||||||
|
"enabled": false,
|
||||||
|
"engine": "tsdocker",
|
||||||
|
"patterns": []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,10 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
dist_*/
|
dist_*/
|
||||||
|
|
||||||
|
# rust
|
||||||
|
rust/target/
|
||||||
|
dist_rust/
|
||||||
|
|
||||||
# AI
|
# AI
|
||||||
.claude/
|
.claude/
|
||||||
.serena/
|
.serena/
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
|
||||||
"lib": ["ES2022", "DOM"],
|
"lib": ["ES2022", "DOM"],
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"checkJs": true
|
"checkJs": true
|
||||||
|
|||||||
@@ -12,15 +12,17 @@ fileName: package.json
|
|||||||
"author": "{{module.author}}",
|
"author": "{{module.author}}",
|
||||||
"license": "{{module.license}}",
|
"license": "{{module.license}}",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/ --web)",
|
"test": "(tstest test/ --verbose --logfile --timeout 60)",
|
||||||
"build": "(tsbuild --web --allowimplicitany)",
|
"build": "(tsbuild tsfolders --allowimplicitany)",
|
||||||
"buildDocs": "(tsdoc)"
|
"buildDocs": "(tsdoc)"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^3.1.2",
|
"@git.zone/tsbuild": "^4.1.2",
|
||||||
"@git.zone/tsrun": "^2.0.0",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@git.zone/tstest": "^3.1.3",
|
"@git.zone/tstest": "^3.1.8",
|
||||||
"@types/node": "^24.10.1"
|
"@types/node": "^25.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"dependencies": {
|
||||||
|
"@push.rocks/smartpath": "^6.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import * as plugins from './{{module.name}}.plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
export let demoExport = 'Hi there! :) This is an exported string';
|
export let demoExport = 'Hi there! :) This is an exported string';
|
||||||
|
|||||||
@@ -9,6 +9,13 @@
|
|||||||
"npmPackagename": "{{module.npmPackagename}}",
|
"npmPackagename": "{{module.npmPackagename}}",
|
||||||
"license": "{{module.license}}",
|
"license": "{{module.license}}",
|
||||||
"projectDomain": "{{module.projectDomain}}"
|
"projectDomain": "{{module.projectDomain}}"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"targets": {
|
||||||
|
"npm": {
|
||||||
|
"registries": ["{{npmPrivateRegistry}}"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@ship.zone/szci": {
|
"@ship.zone/szci": {
|
||||||
@@ -18,7 +25,6 @@
|
|||||||
},
|
},
|
||||||
"dockerBuildargEnvMap": {
|
"dockerBuildargEnvMap": {
|
||||||
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
|
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
|
||||||
},
|
}
|
||||||
"npmRegistryUrl": "{{npmPrivateRegistry}}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -1,3 +1,6 @@
|
|||||||
|
---
|
||||||
|
fileName: .smartconfig.json
|
||||||
|
---
|
||||||
{
|
{
|
||||||
"@git.zone/cli": {
|
"@git.zone/cli": {
|
||||||
"projectType": "{{projectType}}",
|
"projectType": "{{projectType}}",
|
||||||
@@ -11,7 +14,11 @@
|
|||||||
"projectDomain": "{{module.projectDomain}}"
|
"projectDomain": "{{module.projectDomain}}"
|
||||||
},
|
},
|
||||||
"release": {
|
"release": {
|
||||||
"accessLevel": "{{module.npmAccessLevel}}"
|
"targets": {
|
||||||
|
"npm": {
|
||||||
|
"accessLevel": "{{module.npmAccessLevel}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@ship.zone/szci": {
|
"@ship.zone/szci": {
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"json.schemas": [
|
"json.schemas": [
|
||||||
{
|
{
|
||||||
"fileMatch": ["/npmextra.json"],
|
"fileMatch": ["/.smartconfig.json"],
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -9,6 +9,13 @@
|
|||||||
"npmPackagename": "{{module.npmPackagename}}",
|
"npmPackagename": "{{module.npmPackagename}}",
|
||||||
"license": "{{module.license}}",
|
"license": "{{module.license}}",
|
||||||
"projectDomain": "{{module.projectDomain}}"
|
"projectDomain": "{{module.projectDomain}}"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"targets": {
|
||||||
|
"npm": {
|
||||||
|
"registries": ["{{private.npmRegistryUrl}}"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@ship.zone/szci": {
|
"@ship.zone/szci": {
|
||||||
@@ -18,7 +25,6 @@
|
|||||||
},
|
},
|
||||||
"dockerBuildargEnvMap": {
|
"dockerBuildargEnvMap": {
|
||||||
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
|
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
|
||||||
},
|
}
|
||||||
"npmRegistryUrl": "{{private.npmRegistryUrl}}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+189
@@ -1,5 +1,194 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Pending
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-05-14 - 2.19.1
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- migrate legacy release arrays during config fixes and validate release config shape (config)
|
||||||
|
- Automatically converts legacy release registry arrays into release.targets.npm.registries during smartconfig migration and config fix runs.
|
||||||
|
- Re-runs doctor checks after applying known migrations so resolved issues do not require the external fixer.
|
||||||
|
- Reports an explicit validation error when release config is not an object.
|
||||||
|
- Updates config fix prompts and help text to use generic configuration repair wording.
|
||||||
|
|
||||||
|
## 2026-05-13 - 2.19.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Delegate Docker release targets to `tsdocker`.
|
||||||
|
- Replace Docker image template publishing with `tsdocker push` execution.
|
||||||
|
- Move Docker registry behavior to `@git.zone/tsdocker` config and validate removed image config as invalid.
|
||||||
|
|
||||||
|
## 2026-05-10 - 2.18.1
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Run `gitzone config fix` opencode handoff with inherited terminal I/O.
|
||||||
|
|
||||||
|
## 2026-05-10 - 2.18.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add `gitzone config fix` to invoke opencode for configuration repair.
|
||||||
|
|
||||||
|
## 2026-05-10 - 2.17.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add guided project, CLI, release, and doctor flows to `gitzone config`.
|
||||||
|
|
||||||
|
## 2026-05-10 - 2.16.1
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Prevent startup update checks from crashing when installed package metadata is incomplete.
|
||||||
|
|
||||||
|
## 2026-05-10 - 2.16.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add `gitzone tools` for managing the global `@git.zone` toolchain from the main CLI.
|
||||||
|
|
||||||
|
## 2026-05-10 - 2.15.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Split `gitzone commit` and `gitzone release` into commit workflow and target-based release configuration.
|
||||||
|
- Add pending changelog handling so commits accumulate unreleased notes and releases move them into version sections.
|
||||||
|
- Add first-class release targets for git, npm, and Docker publishing.
|
||||||
|
|
||||||
|
## 2026-04-30 - 2.14.3 - fix(test)
|
||||||
|
move test workspace into .nogit and add bundled fixture project files
|
||||||
|
|
||||||
|
- updates package scripts to clone and run the sandbox test repository from .nogit/test
|
||||||
|
- adds a complete test fixture project under test/ for CLI, template, and documentation-related test scenarios
|
||||||
|
|
||||||
|
## 2026-04-30 - 2.14.2 - fix(package)
|
||||||
|
correct package entry point extension and align test scripts with pnpm
|
||||||
|
|
||||||
|
- Change the package main field from dist_ts/index.ts to dist_ts/index.js for the built CLI entry point.
|
||||||
|
- Update test and testBuild scripts to use pnpm run instead of npm run for consistent package manager usage.
|
||||||
|
|
||||||
|
## 2026-04-16 - 2.14.1 - fix(repo)
|
||||||
|
no changes to commit
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-04-16 - 2.14.0 - feat(cli)
|
||||||
|
add machine-readable CLI help, recommendation, and configuration flows
|
||||||
|
|
||||||
|
- introduces shared CLI mode handling for human, plain, and JSON output with configurable interactivity and update checks
|
||||||
|
- adds read-only JSON support for `commit recommend`, `format plan`, and command help output
|
||||||
|
- expands `config` and `services` commands with non-interactive config inspection and service enablement flows
|
||||||
|
- updates format and smartconfig handling to respect non-interactive execution and fail clearly when required metadata is missing
|
||||||
|
|
||||||
|
## 2026-04-16 - 2.13.16 - fix(mod_format)
|
||||||
|
stop package.json formatter from modifying buildDocs and dependency entries
|
||||||
|
|
||||||
|
- removes automatic buildDocs script injection from the package.json formatter
|
||||||
|
- removes dependency include/exclude and latest-version update logic from package.json formatting
|
||||||
|
- drops the unused smartnpm plugin import after removing registry lookups
|
||||||
|
|
||||||
|
## 2026-03-24 - 2.13.15 - fix(repo)
|
||||||
|
no changes to commit
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-03-24 - 2.13.14 - fix(mod_format)
|
||||||
|
move smartconfig file renaming into the formatter orchestrator
|
||||||
|
|
||||||
|
- Renames smartconfig.json or npmextra.json to .smartconfig.json before formatters run
|
||||||
|
- Simplifies the smartconfig formatter to only read and modify .smartconfig.json
|
||||||
|
- Removes create/delete change planning for config renames and applies only content updates within the formatter
|
||||||
|
|
||||||
|
## 2026-03-24 - 2.13.13 - fix(vscode-template)
|
||||||
|
update VS Code schema matching to use .smartconfig.json
|
||||||
|
|
||||||
|
- Changes the VS Code template settings so the JSON schema applies to /.smartconfig.json instead of /npmextra.json.
|
||||||
|
|
||||||
|
## 2026-03-24 - 2.13.12 - fix(mod_format)
|
||||||
|
render format templates through smartscaf before comparing generated files
|
||||||
|
|
||||||
|
- adds smartscaf-based in-memory template rendering so supplied variables are applied before detecting changes
|
||||||
|
- supports release.accessLevel as a fallback when selecting public vs private CI templates
|
||||||
|
- matches rendered output by template or destination path to handle renamed files from template frontmatter
|
||||||
|
|
||||||
|
## 2026-03-24 - 2.13.10 - fix(config)
|
||||||
|
migrate configuration handling from npmextra to smartconfig
|
||||||
|
|
||||||
|
- replace @push.rocks/npmextra with @push.rocks/smartconfig across config, commit, format, and service modules
|
||||||
|
- switch managed project config file references from npmextra.json to smartconfig.json
|
||||||
|
- update formatting and package metadata checks to include smartconfig.json
|
||||||
|
- extend the gitignore template with Rust build output directories
|
||||||
|
|
||||||
|
## 2026-03-11 - 2.13.9 - fix(deps,readme)
|
||||||
|
bump dependencies and update README to prefer pnpm and document semantic commit flags
|
||||||
|
|
||||||
|
- Dev dependency updates: @git.zone/tsbuild -> ^4.3.0, @git.zone/tstest -> ^3.3.2, @types/node -> ^25.4.0
|
||||||
|
- Dependency upgrades: @git.zone/tsdoc -> ^2.0.0, @git.zone/tspublish -> ^1.11.2, @push.rocks/lik -> ^6.3.1, @push.rocks/smartfs -> ^1.5.0, @push.rocks/smartlog -> ^3.2.1, @push.rocks/smartstream -> ^3.4.0, prettier -> ^3.8.1 (and other minor/patch bumps)
|
||||||
|
- README changes: prefer pnpm for global install, clarify format command dry-run behavior and --write flag, add and document gitzone commit flags (-y/--yes, -p/--push, -t/--test, -b/--build, -r/--release) and AI-powered commit workflow
|
||||||
|
- No source code changes; this is a documentation and dependency refresh, recommend a patch release
|
||||||
|
|
||||||
|
## 2026-03-05 - 2.13.8 - fix(dependencies)
|
||||||
|
move runtime tooling packages from devDependencies to dependencies
|
||||||
|
|
||||||
|
- Removed @push.rocks/smartdelay, @push.rocks/smartinteract, @push.rocks/smartnetwork, and @push.rocks/smartshell from devDependencies and added them to dependencies
|
||||||
|
- No package version numbers were changed; this ensures the moved packages are installed for consumers at runtime
|
||||||
|
|
||||||
|
## 2026-03-05 - 2.13.7 - fix(deps)
|
||||||
|
bump devDependencies: @git.zone/tsbuild to ^4.1.4 and @push.rocks/smartshell to ^3.3.7
|
||||||
|
|
||||||
|
- Updated @git.zone/tsbuild from ^4.1.2 to ^4.1.4 (patch)
|
||||||
|
- Updated @push.rocks/smartshell from ^3.3.0 to ^3.3.7 (patch)
|
||||||
|
|
||||||
|
## 2026-02-01 - 2.13.6 - fix(templates/npm)
|
||||||
|
use tsbuild tsfolders instead of --web flag in npm template build script
|
||||||
|
|
||||||
|
- Changed build script in assets/templates/npm/.package.json from "(tsbuild --web --allowimplicitany)" to "(tsbuild tsfolders --allowimplicitany)"
|
||||||
|
- Replaces --web flag with explicit tsfolders argument to correctly target project folders during build
|
||||||
|
|
||||||
|
## 2026-02-01 - 2.13.5 - fix(templates/npm)
|
||||||
|
update npm template: tweak test script, bump devDependencies, add smartpath dependency, and fix ts import path
|
||||||
|
|
||||||
|
- test script updated: '(tstest test/ --web)' -> '(tstest test/ --verbose --logfile --timeout 60)'
|
||||||
|
- devDependencies bumped: @git.zone/tsbuild ^3.1.2 -> ^4.1.2, @git.zone/tsrun ^2.0.0 -> ^2.0.1, @git.zone/tstest ^3.1.3 -> ^3.1.8, @types/node ^24.10.1 -> ^25.2.0
|
||||||
|
- dependencies: added @push.rocks/smartpath ^6.0.0
|
||||||
|
- TypeScript template import fixed: './{{module.name}}.plugins.js' -> './plugins.js'
|
||||||
|
|
||||||
|
## 2025-12-18 - 2.13.3 - fix(tsconfig)
|
||||||
|
remove experimentalDecorators and useDefineForClassFields from TypeScript configuration files
|
||||||
|
|
||||||
|
- Removed "experimentalDecorators": true from assets/templates/multienv/deno.json and tsconfig.json
|
||||||
|
- Removed "useDefineForClassFields": false from tsconfig.json
|
||||||
|
- This change alters TypeScript/Deno compiler behavior: decorator support and legacy class-field initialization semantics may be affected; code relying on those may need updates
|
||||||
|
|
||||||
|
## 2025-12-16 - 2.13.2 - fix(deps)
|
||||||
|
bump @git.zone/tspublish to ^1.11.0
|
||||||
|
|
||||||
|
- Updated dependency @git.zone/tspublish from ^1.10.3 to ^1.11.0 in package.json
|
||||||
|
|
||||||
|
## 2025-12-16 - 2.13.1 - fix(npmextra)
|
||||||
|
merge old npmextra keys into new keys during migration, preserving existing new values
|
||||||
|
|
||||||
|
- Changed migration logic to merge data when both old and new keys exist instead of skipping the merge.
|
||||||
|
- Merge preserves existing new-key values (old values do not overwrite new ones) and still deletes the old key after migration.
|
||||||
|
- Applied the fix in both ts/mod_format/format.npmextra.ts and ts/mod_format/formatters/npmextra.formatter.ts.
|
||||||
|
- Adds a console log for successful migrations; behavior for single-key rename remains unchanged.
|
||||||
|
|
||||||
|
## 2025-12-16 - 2.13.0 - feat(tests)
|
||||||
|
feat(tests): add sandbox test fixture, CI and editor configs; bump deps
|
||||||
|
|
||||||
|
- Added comprehensive test/ fixture (sandbox-npmts) including package.json, npmextra.json, readme, GitLab CI (.gitlab-ci.yml), .npmrc, VSCode launch/settings and qenv.yml
|
||||||
|
- Added test sources and helper files under test/test and test/ts (browser and node tests, commitinfo data, simple library code)
|
||||||
|
- Updated dependencies in package.json: @git.zone/tsdoc -> ^1.11.4, @push.rocks/smartfs -> ^1.3.1
|
||||||
|
|
||||||
|
## 2025-12-15 - 2.12.2 - fix(cli)
|
||||||
|
noop: no changes
|
||||||
|
|
||||||
|
- No source or documentation changes detected in the diff; nothing to release.
|
||||||
|
- Package version remains unchanged at 2.12.1.
|
||||||
|
|
||||||
## 2025-12-15 - 2.12.1 - fix(cli)
|
## 2025-12-15 - 2.12.1 - fix(cli)
|
||||||
No changes detected — no version bump required
|
No changes detected — no version bump required
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2015 Task Venture Capital GmbH (hello@lossless.com)
|
Copyright (c) 2015 Task Venture Capital GmbH (hello@task.vc)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -16,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|||||||
+32
-38
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/cli",
|
"name": "@git.zone/cli",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "2.12.1",
|
"version": "2.19.1",
|
||||||
"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.",
|
"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",
|
"main": "dist_ts/index.js",
|
||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -11,21 +11,21 @@
|
|||||||
"gzone": "./cli.js"
|
"gzone": "./cli.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(npm run clean && npm run prepareTest && npm run testCli && npm run testFormat && npm run testCommit && npm run testDeprecate && npm run testVersion && npm run testReadme && npm run testUpdate && npm run testTemplateNpm && npm run testTemplateLit) && rm -rf test",
|
"test": "(pnpm run clean && pnpm run prepareTest && pnpm run testCli && pnpm run testFormat && pnpm run testCommit && pnpm run testDeprecate && pnpm run testVersion && pnpm run testReadme && pnpm run testUpdate && pnpm run testTemplateNpm && pnpm run testTemplateLit) && rm -rf .nogit/test",
|
||||||
"build": "tsbuild tsfolders",
|
"build": "tsbuild tsfolders",
|
||||||
"clean": "(rm -rf test/)",
|
"clean": "(rm -rf .nogit/test/)",
|
||||||
"prepareTest": "(git clone https://gitlab.com/sandboxzone/sandbox-npmts.git test/)",
|
"prepareTest": "(mkdir -p .nogit && git clone https://gitlab.com/sandboxzone/sandbox-npmts.git .nogit/test/)",
|
||||||
"testBuild": "npm run build && rm -r dist/",
|
"testBuild": "pnpm run build && rm -r dist/",
|
||||||
"testCli": "(cd test && node ../cli.ts.js)",
|
"testCli": "(cd .nogit/test && node ../../cli.ts.js)",
|
||||||
"testCommit": "(cd test && node ../cli.ts.js commit)",
|
"testCommit": "(cd .nogit/test && node ../../cli.ts.js commit)",
|
||||||
"testDeprecate": "(cd test && node ../cli.ts.js deprecate)",
|
"testDeprecate": "(cd .nogit/test && node ../../cli.ts.js deprecate)",
|
||||||
"testOpen": "(cd test && node ../cli.ts.js open ci)",
|
"testOpen": "(cd .nogit/test && node ../../cli.ts.js open ci)",
|
||||||
"testReadme": "(cd test && node ../cli.ts.js readme)",
|
"testReadme": "(cd .nogit/test && node ../../cli.ts.js readme)",
|
||||||
"testFormat": "(cd test && node ../cli.ts.js format)",
|
"testFormat": "(cd .nogit/test && node ../../cli.ts.js format)",
|
||||||
"testTemplateNpm": "(rm -rf test/testtemplate_npm/ && mkdir test/testtemplate_npm && cd test/testtemplate_npm && node ../../cli.ts.js template npm)",
|
"testTemplateNpm": "(rm -rf .nogit/test/testtemplate_npm/ && mkdir -p .nogit/test/testtemplate_npm && cd .nogit/test/testtemplate_npm && node ../../../cli.ts.js template npm)",
|
||||||
"testTemplateLit": "(rm -rf test/testtemplate_lit/ && mkdir test/testtemplate_lit && cd test/testtemplate_lit && node ../../cli.ts.js template lit)",
|
"testTemplateLit": "(rm -rf .nogit/test/testtemplate_lit/ && mkdir -p .nogit/test/testtemplate_lit && cd .nogit/test/testtemplate_lit && node ../../../cli.ts.js template lit)",
|
||||||
"testUpdate": "(cd test && node ../cli.ts.js update)",
|
"testUpdate": "(cd .nogit/test && node ../../cli.ts.js update)",
|
||||||
"testVersion": "(cd test && node ../cli.ts.js -v)",
|
"testVersion": "(cd .nogit/test && node ../../cli.ts.js -v)",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -57,46 +57,40 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://gitlab.com/gitzone/private/gitzone#readme",
|
"homepage": "https://gitlab.com/gitzone/private/gitzone#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.0.2",
|
"@git.zone/tsbuild": "^4.3.0",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@git.zone/tstest": "^3.1.3",
|
"@git.zone/tstest": "^3.3.2",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@types/node": "^25.4.0"
|
||||||
"@push.rocks/smartinteract": "^2.0.16",
|
|
||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
|
||||||
"@push.rocks/smartshell": "^3.3.0",
|
|
||||||
"@types/node": "^25.0.2"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git.zone/tsdoc": "^1.11.3",
|
"@git.zone/tsdoc": "^2.0.0",
|
||||||
"@git.zone/tspublish": "^1.10.3",
|
"@git.zone/tspublish": "^1.11.2",
|
||||||
"@push.rocks/commitinfo": "^1.0.12",
|
"@push.rocks/commitinfo": "^1.0.12",
|
||||||
"@push.rocks/early": "^4.0.4",
|
"@push.rocks/early": "^4.0.4",
|
||||||
"@push.rocks/gulp-function": "^3.0.7",
|
|
||||||
"@push.rocks/lik": "^6.2.2",
|
|
||||||
"@push.rocks/npmextra": "^5.3.3",
|
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/smartcli": "^4.0.19",
|
"@push.rocks/smartcli": "^4.0.20",
|
||||||
|
"@push.rocks/smartconfig": "^6.0.1",
|
||||||
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"@push.rocks/smartdiff": "^1.1.0",
|
"@push.rocks/smartdiff": "^1.1.0",
|
||||||
"@push.rocks/smartfile": "^13.1.2",
|
"@push.rocks/smartfile": "^13.1.2",
|
||||||
"@push.rocks/smartfs": "^1.2.0",
|
"@push.rocks/smartfs": "^1.5.0",
|
||||||
"@push.rocks/smartgulp": "^3.0.4",
|
"@push.rocks/smartinteract": "^2.0.16",
|
||||||
"@push.rocks/smartjson": "^6.0.0",
|
"@push.rocks/smartjson": "^6.0.0",
|
||||||
"@push.rocks/smartlegal": "^1.0.27",
|
"@push.rocks/smartlegal": "^1.0.27",
|
||||||
"@push.rocks/smartlog": "^3.1.10",
|
"@push.rocks/smartlog": "^3.2.1",
|
||||||
"@push.rocks/smartlog-destination-local": "^9.0.2",
|
"@push.rocks/smartlog-destination-local": "^9.0.2",
|
||||||
"@push.rocks/smartmustache": "^3.0.2",
|
"@push.rocks/smartmustache": "^3.0.2",
|
||||||
|
"@push.rocks/smartnetwork": "^4.4.0",
|
||||||
"@push.rocks/smartnpm": "^2.0.6",
|
"@push.rocks/smartnpm": "^2.0.6",
|
||||||
"@push.rocks/smartobject": "^1.0.12",
|
"@push.rocks/smartobject": "^1.0.12",
|
||||||
"@push.rocks/smartopen": "^2.0.0",
|
"@push.rocks/smartopen": "^2.0.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartscaf": "^4.0.19",
|
"@push.rocks/smartscaf": "^4.0.21",
|
||||||
"@push.rocks/smartstream": "^3.2.5",
|
"@push.rocks/smartshell": "^3.5.0",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@push.rocks/smartupdate": "^2.0.6",
|
"@push.rocks/smartupdate": "^2.0.6",
|
||||||
"@types/through2": "^2.0.41",
|
"prettier": "^3.8.1"
|
||||||
"prettier": "^3.7.4",
|
|
||||||
"through2": "^4.0.2"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
@@ -107,7 +101,7 @@
|
|||||||
"dist_ts_web/**/*",
|
"dist_ts_web/**/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
"cli.js",
|
"cli.js",
|
||||||
"npmextra.json",
|
".smartconfig.json",
|
||||||
"readme.md"
|
"readme.md"
|
||||||
],
|
],
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
|||||||
Generated
+874
-1608
File diff suppressed because it is too large
Load Diff
+49
-48
@@ -23,10 +23,10 @@ Gitzone CLI (`@git.zone/cli`) is a comprehensive toolbelt for streamlining local
|
|||||||
|
|
||||||
### Configuration Management
|
### Configuration Management
|
||||||
|
|
||||||
- Uses `npmextra.json` for all tool configuration
|
- Uses `.smartconfig.json` for tool configuration
|
||||||
- Configuration stored under `gitzone` key in npmextra
|
- CLI settings live under the `@git.zone/cli` namespace
|
||||||
- No separate `.gitzonerc` file - everything in npmextra.json
|
- Agent and non-interactive defaults now belong under `@git.zone/cli.cli`
|
||||||
- Project type and module metadata also stored in npmextra
|
- Project type, module metadata, release settings, commit defaults, and format settings live in the same file
|
||||||
|
|
||||||
### Format Module (`mod_format`) - SIGNIFICANTLY ENHANCED
|
### Format Module (`mod_format`) - SIGNIFICANTLY ENHANCED
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ The format module is responsible for project standardization:
|
|||||||
2. **copy** - File copying with glob patterns (fully implemented)
|
2. **copy** - File copying with glob patterns (fully implemented)
|
||||||
3. **gitignore** - Creates/updates .gitignore from templates
|
3. **gitignore** - Creates/updates .gitignore from templates
|
||||||
4. **license** - Checks dependency licenses for compatibility
|
4. **license** - Checks dependency licenses for compatibility
|
||||||
5. **npmextra** - Manages project metadata and configuration
|
5. **smartconfig** - Manages project metadata and configuration
|
||||||
6. **packagejson** - Formats and updates package.json
|
6. **packagejson** - Formats and updates package.json
|
||||||
7. **prettier** - Applies code formatting with batching
|
7. **prettier** - Applies code formatting with batching
|
||||||
8. **readme** - Ensures readme files exist
|
8. **readme** - Ensures readme files exist
|
||||||
@@ -84,33 +84,31 @@ The format module is responsible for project standardization:
|
|||||||
|
|
||||||
1. **Plan → Action Workflow**: Shows changes before applying them
|
1. **Plan → Action Workflow**: Shows changes before applying them
|
||||||
2. **Rollback Mechanism**: Full backup and restore on failures
|
2. **Rollback Mechanism**: Full backup and restore on failures
|
||||||
3. **Enhanced Configuration**: Granular control via npmextra.json
|
3. **Enhanced Configuration**: Granular control via `.smartconfig.json`
|
||||||
4. **Better Error Handling**: Detailed errors with recovery options
|
4. **Better Error Handling**: Detailed errors with recovery options
|
||||||
5. **Performance Optimizations**: Parallel execution and caching
|
5. **Performance Optimizations**: Parallel execution and caching
|
||||||
6. **Reporting**: Diff views, statistics, verbose logging
|
6. **Reporting**: Diff views, statistics, verbose logging
|
||||||
7. **Architecture**: Clean separation of concerns with new classes
|
7. **Architecture**: Clean separation of concerns with new classes
|
||||||
8. **Unified Version Bumping**: Self-managed version updates eliminating npm warning pollution in deno.json
|
8. **Split Commit/Release Workflows**: `commit` creates source commits; `release` owns versioning, tags, and artifact publishing
|
||||||
|
|
||||||
### Version Bumping Refactor (Latest)
|
### Commit/Release Workflow Refactor (Latest)
|
||||||
|
|
||||||
The commit module's version bumping has been refactored to eliminate npm command dependencies:
|
The commit module no longer bumps versions, creates tags, or publishes packages. Release work now belongs to `gitzone release`:
|
||||||
|
|
||||||
**Changes:**
|
**Changes:**
|
||||||
|
|
||||||
- Removed `bumpNpmVersion()` - was causing npm warnings to pollute deno.json
|
- `gitzone commit` analyzes changes, updates `changelog.md` `Pending`, commits, and optionally pushes.
|
||||||
- Removed `syncVersionToDenoJson()` - no longer needed with unified approach
|
- `gitzone release` reads `Pending`, bumps versions, moves changelog entries into a version section, tags, pushes, and publishes configured artifacts.
|
||||||
- Removed separate `bumpDenoVersion()` - replaced by unified implementation
|
- Commit workflow steps are configured in `.smartconfig.json` under `@git.zone/cli.commit.steps`.
|
||||||
- Added `readCurrentVersion()` helper - reads from either package.json or deno.json
|
- Smartconfig schema versioning lives at `@git.zone/cli.schemaVersion`; run `gitzone config migrate <version>` for targeted migrations.
|
||||||
- Added `updateVersionFile()` helper - updates JSON files directly
|
- Release publishing is target-based under `@git.zone/cli.release.targets`.
|
||||||
- Unified `bumpProjectVersion()` - handles npm/deno/both with single clean code path
|
- NPM registries only live under `@git.zone/cli.release.targets.npm.registries`.
|
||||||
|
|
||||||
**Benefits:**
|
**Benefits:**
|
||||||
|
|
||||||
- No npm warning pollution in version fields
|
- Commit is safer and has no publishing side effects.
|
||||||
- Full control over version bumping process
|
- Multiple source commits can accumulate into one release via `Pending`.
|
||||||
- Simpler git history (no amending, no force-tagging)
|
- Per-artifact release results can distinguish published, already-published, skipped, and failed targets.
|
||||||
- Same code path for all project types
|
|
||||||
- Reuses existing `calculateNewVersion()` function
|
|
||||||
|
|
||||||
### Auto-Accept Flag for Commits
|
### Auto-Accept Flag for Commits
|
||||||
|
|
||||||
@@ -132,7 +130,7 @@ The commit module now supports `-y/--yes` flag for non-interactive commits:
|
|||||||
## Development Tips
|
## Development Tips
|
||||||
|
|
||||||
- Always check readme.plan.md for ongoing improvement plans
|
- Always check readme.plan.md for ongoing improvement plans
|
||||||
- Use npmextra.json for any new configuration options
|
- Use `.smartconfig.json` for any new configuration options
|
||||||
- Keep modules focused and single-purpose
|
- Keep modules focused and single-purpose
|
||||||
- Maintain the existing plugin pattern for dependencies
|
- Maintain the existing plugin pattern for dependencies
|
||||||
- Test format operations on sample projects before deploying
|
- Test format operations on sample projects before deploying
|
||||||
@@ -144,30 +142,18 @@ The commit module now supports `-y/--yes` flag for non-interactive commits:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"gitzone": {
|
"@git.zone/cli": {
|
||||||
|
"cli": {
|
||||||
|
"interactive": true,
|
||||||
|
"output": "human",
|
||||||
|
"checkUpdates": true
|
||||||
|
},
|
||||||
"format": {
|
"format": {
|
||||||
"interactive": true,
|
"interactive": true,
|
||||||
"parallel": true,
|
|
||||||
"showStats": true,
|
"showStats": true,
|
||||||
"cache": {
|
|
||||||
"enabled": true,
|
|
||||||
"clean": true
|
|
||||||
},
|
|
||||||
"rollback": {
|
|
||||||
"enabled": true,
|
|
||||||
"autoRollbackOnError": true,
|
|
||||||
"backupRetentionDays": 7
|
|
||||||
},
|
|
||||||
"modules": {
|
"modules": {
|
||||||
"skip": ["prettier"],
|
"skip": ["prettier"],
|
||||||
"only": [],
|
"only": []
|
||||||
"order": []
|
|
||||||
},
|
|
||||||
"licenses": {
|
|
||||||
"allowed": ["MIT", "Apache-2.0"],
|
|
||||||
"exceptions": {
|
|
||||||
"some-package": "GPL-3.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,6 +168,9 @@ The commit module now supports `-y/--yes` flag for non-interactive commits:
|
|||||||
# Interactive commit (default)
|
# Interactive commit (default)
|
||||||
gitzone commit
|
gitzone commit
|
||||||
|
|
||||||
|
# Read-only recommendation
|
||||||
|
gitzone commit recommend --json
|
||||||
|
|
||||||
# Auto-accept AI recommendations (no prompts)
|
# Auto-accept AI recommendations (no prompts)
|
||||||
gitzone commit -y
|
gitzone commit -y
|
||||||
gitzone commit --yes
|
gitzone commit --yes
|
||||||
@@ -201,11 +190,24 @@ gitzone commit --format
|
|||||||
# Basic format
|
# Basic format
|
||||||
gitzone format
|
gitzone format
|
||||||
|
|
||||||
|
# Read-only JSON plan
|
||||||
|
gitzone format plan --json
|
||||||
|
|
||||||
|
# CI-friendly check, exits non-zero when changes or validator errors remain
|
||||||
|
gitzone format check
|
||||||
|
|
||||||
# Dry run to preview changes
|
# Dry run to preview changes
|
||||||
gitzone format --dry-run
|
gitzone format --dry-run
|
||||||
|
|
||||||
# Non-interactive mode
|
# Limit formatter modules
|
||||||
gitzone format --yes
|
gitzone format --only prettier,packagejson
|
||||||
|
gitzone format --skip license
|
||||||
|
|
||||||
|
# Non-interactive apply
|
||||||
|
gitzone format --write --yes
|
||||||
|
|
||||||
|
# Deterministic format first, opencode for remaining issues
|
||||||
|
gitzone format fix
|
||||||
|
|
||||||
# Plan only (no execution)
|
# Plan only (no execution)
|
||||||
gitzone format --plan-only
|
gitzone format --plan-only
|
||||||
@@ -222,11 +224,10 @@ gitzone format --verbose
|
|||||||
# Detailed diff views
|
# Detailed diff views
|
||||||
gitzone format --detailed
|
gitzone format --detailed
|
||||||
|
|
||||||
# Rollback operations
|
# Inspect config for agents and scripts
|
||||||
gitzone format --rollback
|
gitzone config show --json
|
||||||
gitzone format --rollback <operation-id>
|
gitzone config set cli.output json
|
||||||
gitzone format --list-backups
|
gitzone config get release.targets.npm.accessLevel
|
||||||
gitzone format --clean-backups
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Issues (Now Resolved)
|
## Common Issues (Now Resolved)
|
||||||
@@ -298,7 +299,7 @@ All sync methods must become async. Functions that were previously synchronous (
|
|||||||
**Affected Modules:**
|
**Affected Modules:**
|
||||||
|
|
||||||
- ts/mod_format/\* (largest area - 15+ files)
|
- ts/mod_format/\* (largest area - 15+ files)
|
||||||
- ts/mod_commit/\* (version bumping)
|
- ts/mod_commit/\* and ts/mod_release/\* (commit/release workflows)
|
||||||
- ts/mod_services/\* (configuration management)
|
- ts/mod_services/\* (configuration management)
|
||||||
- ts/mod_meta/\* (meta repository management)
|
- ts/mod_meta/\* (meta repository management)
|
||||||
- ts/mod_standard/\* (template listing)
|
- ts/mod_standard/\* (template listing)
|
||||||
|
|||||||
@@ -1,539 +1,448 @@
|
|||||||
# @git.zone/cli 🚀
|
# @git.zone/cli 🚀
|
||||||
|
|
||||||
**The ultimate CLI toolbelt for modern TypeScript development workflows**
|
`@git.zone/cli` is the development workflow CLI behind the `gitzone` and `gzone` commands. It helps TypeScript-heavy teams keep projects tidy, create semantic source commits, manage local Docker-backed services, scaffold new modules, and release software through explicit, target-based release configuration.
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/@git.zone/cli)
|
It is opinionated where that saves time: source commits and releases are separate, changelog entries flow through a standard `Pending` section, project config lives in `.smartconfig.json`, and release targets make side effects visible before they happen.
|
||||||
[](https://opensource.org/licenses/MIT)
|
|
||||||
|
|
||||||
## 🎯 What is gitzone?
|
|
||||||
|
|
||||||
gitzone is a powerful command-line interface that supercharges your development workflow with automated project management, intelligent code formatting, seamless version control, and development service orchestration. Whether you're bootstrapping a new TypeScript project, maintaining code quality, managing complex multi-repository setups, or spinning up local development databases, gitzone has got you covered.
|
|
||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
|
|
||||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||||
|
|
||||||
## 🏃♂️ Quick Start
|
## Install
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install globally via npm
|
|
||||||
npm install -g @git.zone/cli
|
|
||||||
|
|
||||||
# Or with pnpm (recommended)
|
|
||||||
pnpm add -g @git.zone/cli
|
pnpm add -g @git.zone/cli
|
||||||
```
|
```
|
||||||
|
|
||||||
Once installed, you can use either `gitzone` or the shorter `gzone` command from anywhere in your terminal.
|
After installation, both binaries point to the same CLI:
|
||||||
|
|
||||||
### Your First Commands
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a new TypeScript npm package
|
gitzone --help
|
||||||
gitzone template npm
|
gzone --help
|
||||||
|
```
|
||||||
|
|
||||||
# Format your entire codebase
|
## The Big Idea
|
||||||
|
|
||||||
|
`gitzone commit` handles source history.
|
||||||
|
|
||||||
|
`gitzone release` handles release transactions.
|
||||||
|
|
||||||
|
That split is intentional. A commit should not unexpectedly publish npm packages, push Docker images, or trigger remote release pipelines. A release should clearly show which targets it will publish to.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Preview project standardization work
|
||||||
gitzone format
|
gitzone format
|
||||||
|
|
||||||
# Start local MongoDB and MinIO services
|
# Apply formatting changes
|
||||||
gitzone services start
|
gitzone format --write
|
||||||
|
|
||||||
# Create a semantic commit with AI-powered suggestions
|
# Create a semantic source commit
|
||||||
gitzone commit
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛠️ Core Features
|
|
||||||
|
|
||||||
### 🐳 Development Services Management
|
|
||||||
|
|
||||||
Effortlessly manage local MongoDB and MinIO (S3-compatible) services for your development environment:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gitzone services [command]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Available commands:**
|
|
||||||
|
|
||||||
- **`start [service]`** - Start services (mongo|s3|all)
|
|
||||||
- **`stop [service]`** - Stop services (mongo|s3|all)
|
|
||||||
- **`restart [service]`** - Restart services
|
|
||||||
- **`status`** - Show current service status
|
|
||||||
- **`config`** - Display configuration details
|
|
||||||
- **`compass`** - Get MongoDB Compass connection string with network IP
|
|
||||||
- **`logs [service] [lines]`** - View service logs
|
|
||||||
- **`remove`** - Remove containers (preserves data)
|
|
||||||
- **`clean`** - Remove containers AND data (⚠️ destructive)
|
|
||||||
|
|
||||||
**Key features:**
|
|
||||||
|
|
||||||
- 🎲 **Smart port assignment** - Automatically assigns random ports (20000-30000) to avoid conflicts
|
|
||||||
- 📦 **Project isolation** - Each project gets its own containers with unique names
|
|
||||||
- 💾 **Data persistence** - Data stored in `.nogit/` directories survives container restarts
|
|
||||||
- 🔗 **MongoDB Compass support** - Instantly get connection strings for GUI access
|
|
||||||
- 🌐 **Network IP detection** - Automatically detects your local network IP for remote connections
|
|
||||||
- ⚙️ **Auto-configuration** - Creates `.nogit/env.json` with smart defaults
|
|
||||||
|
|
||||||
**Example workflow:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start all services for your project
|
|
||||||
gitzone services start
|
|
||||||
|
|
||||||
# Check what's running
|
|
||||||
gitzone services status
|
|
||||||
|
|
||||||
# Get MongoDB Compass connection string
|
|
||||||
gitzone services compass
|
|
||||||
# Output: mongodb://defaultadmin:defaultpass@192.168.1.100:27018/myproject?authSource=admin
|
|
||||||
|
|
||||||
# View MongoDB logs
|
|
||||||
gitzone services logs mongo 50
|
|
||||||
|
|
||||||
# Stop services when done
|
|
||||||
gitzone services stop
|
|
||||||
```
|
|
||||||
|
|
||||||
The services are configured via `.nogit/env.json` which is automatically created with secure defaults and random ports for each project.
|
|
||||||
|
|
||||||
### 📦 Project Templates
|
|
||||||
|
|
||||||
Instantly scaffold production-ready projects with best practices built-in:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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
|
|
||||||
- **`wcc`** - Web Component Collection for reusable UI components
|
|
||||||
|
|
||||||
Each template comes pre-configured with:
|
|
||||||
|
|
||||||
- ✅ TypeScript with modern configurations
|
|
||||||
- ✅ Automated testing setup with `@git.zone/tstest`
|
|
||||||
- ✅ CI/CD pipelines (GitLab/GitHub)
|
|
||||||
- ✅ Code formatting and linting
|
|
||||||
- ✅ Documentation structure
|
|
||||||
|
|
||||||
### 🎨 Intelligent Code Formatting
|
|
||||||
|
|
||||||
The most powerful feature of gitzone - automatically format and standardize your entire codebase:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Preview changes without applying them
|
|
||||||
gitzone format --dry-run
|
|
||||||
|
|
||||||
# Format with automatic approval
|
|
||||||
gitzone format --yes
|
|
||||||
|
|
||||||
# Save formatting plan for later execution
|
|
||||||
gitzone format --save-plan format-plan.json
|
|
||||||
|
|
||||||
# Execute a saved plan
|
|
||||||
gitzone format --from-plan format-plan.json
|
|
||||||
|
|
||||||
# Enable verbose output for debugging
|
|
||||||
gitzone format --verbose
|
|
||||||
```
|
|
||||||
|
|
||||||
**Format features:**
|
|
||||||
|
|
||||||
- 🔄 **Smart caching** - Only processes changed files
|
|
||||||
- 🛡️ **Rollback support** - Undo formatting changes if needed
|
|
||||||
- 📊 **Detailed reporting** - See exactly what changed
|
|
||||||
- ⚡ **Parallel execution** - Format multiple files simultaneously
|
|
||||||
- 🎯 **Module-specific formatting** - Target specific formatters
|
|
||||||
|
|
||||||
**Rollback capabilities:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# List all available backups
|
|
||||||
gitzone format --list-backups
|
|
||||||
|
|
||||||
# Rollback to the last operation
|
|
||||||
gitzone format --rollback
|
|
||||||
|
|
||||||
# Rollback to a specific operation
|
|
||||||
gitzone format --rollback [operation-id]
|
|
||||||
|
|
||||||
# Clean old backups
|
|
||||||
gitzone format --clean-backups
|
|
||||||
```
|
|
||||||
|
|
||||||
**Formatters included:**
|
|
||||||
|
|
||||||
- **Prettier** - JavaScript/TypeScript code formatting
|
|
||||||
- **License** - Ensure proper licensing
|
|
||||||
- **Package.json** - Standardize package configurations
|
|
||||||
- **Tsconfig** - TypeScript configuration optimization
|
|
||||||
- **Readme** - Documentation formatting
|
|
||||||
- **Gitignore** - Repository ignore rules
|
|
||||||
- **Templates** - Project template updates
|
|
||||||
- **Npmextra** - Extended npm configurations
|
|
||||||
- **Cleanup** - Removes obsolete files (yarn.lock, package-lock.json, tslint.json, etc.)
|
|
||||||
|
|
||||||
### 🔀 Semantic Commits & Versioning
|
|
||||||
|
|
||||||
Create standardized commits with AI-powered suggestions that automatically handle versioning:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Interactive commit with AI recommendations
|
|
||||||
gitzone commit
|
gitzone commit
|
||||||
|
|
||||||
# Auto-accept AI recommendations
|
# Preview the configured release transaction
|
||||||
|
gitzone release --plan
|
||||||
|
|
||||||
|
# Release pending changelog entries to configured targets
|
||||||
|
gitzone release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `commit` | Analyze changes and create one semantic source commit |
|
||||||
|
| `release` | Turn pending changelog entries into a versioned release and publish targets |
|
||||||
|
| `format` | Plan or apply project formatting and standardization |
|
||||||
|
| `config` | Inspect, update, and migrate `.smartconfig.json` |
|
||||||
|
| `services` | Manage local MongoDB, MinIO, and Elasticsearch containers |
|
||||||
|
| `tools` | Manage the global `@git.zone` toolchain |
|
||||||
|
| `template` | Scaffold projects from built-in templates |
|
||||||
|
| `meta` | Manage multi-repository workspaces |
|
||||||
|
| `open` | Open repository assets like CI pages |
|
||||||
|
| `docker` | Run Docker maintenance tasks |
|
||||||
|
| `deprecate` | Deprecate npm packages across registries |
|
||||||
|
| `start` | Prepare an existing project for local work |
|
||||||
|
| `helpers` | Run small helper utilities |
|
||||||
|
|
||||||
|
Global flags include `--help`, `--json`, `--plain`, `--agent`, `--no-interactive`, and `--no-check-updates`.
|
||||||
|
|
||||||
|
## Toolchain Management
|
||||||
|
|
||||||
|
`gitzone tools` replaces the former `gtools` command from `@git.zone/tools`. It manages globally installed `@git.zone` development tools through pnpm.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check installed @git.zone tools and update outdated packages
|
||||||
|
gitzone tools update
|
||||||
|
|
||||||
|
# Update without prompts
|
||||||
|
gitzone tools update -y
|
||||||
|
|
||||||
|
# Install missing managed @git.zone tools
|
||||||
|
gitzone tools install
|
||||||
|
```
|
||||||
|
|
||||||
|
`gitzone tools update` checks `@git.zone/cli` first. If the CLI itself needs an update, it updates `@git.zone/cli` and asks you to rerun the command before updating the rest of the toolchain.
|
||||||
|
|
||||||
|
## Commit Workflow
|
||||||
|
|
||||||
|
`gitzone commit` creates one semantic source commit. It does not bump versions, create tags, publish packages, or push Docker images.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive semantic commit
|
||||||
|
gitzone commit
|
||||||
|
|
||||||
|
# Read-only AI recommendation
|
||||||
|
gitzone commit recommend --json
|
||||||
|
|
||||||
|
# Auto-accept safe recommendations
|
||||||
gitzone commit -y
|
gitzone commit -y
|
||||||
|
|
||||||
# Auto-accept and push
|
# Auto-accept, test, build, and push
|
||||||
gitzone commit -y -p
|
gitzone commit -ytbp
|
||||||
|
|
||||||
|
# Show the resolved workflow without mutating anything
|
||||||
|
gitzone commit --plan
|
||||||
```
|
```
|
||||||
|
|
||||||
Features:
|
The commit flow:
|
||||||
|
|
||||||
- 🤖 **AI-powered analysis** - Analyzes your changes and suggests commit type, scope, and message
|
1. Analyze the working tree.
|
||||||
- 📝 Interactive commit message builder with smart defaults
|
2. Suggest commit type, scope, and message.
|
||||||
- 🏷️ Automatic version bumping (major/minor/patch)
|
3. Write a human-readable entry into `changelog.md` under `## Pending`.
|
||||||
- 📜 Changelog generation
|
4. Stage and create one semantic source commit.
|
||||||
- 🚀 Optional auto-push to origin
|
5. Optionally run formatting, tests, build, and push based on flags or config.
|
||||||
- 🎯 Conventional commit compliance
|
|
||||||
|
|
||||||
The commit wizard guides you through:
|
Commit flags:
|
||||||
|
|
||||||
1. **Type selection** (fix/feat/BREAKING CHANGE) with AI recommendation
|
| Flag | Meaning |
|
||||||
2. **Scope definition** (component/module affected)
|
| --- | --- |
|
||||||
3. **Description crafting**
|
| `-y`, `--yes` | Auto-accept safe recommendations |
|
||||||
4. **Version bump determination**
|
| `-t`, `--test` | Add test step |
|
||||||
|
| `-b`, `--build` | Add build step |
|
||||||
|
| `-p`, `--push` | Push after the source commit |
|
||||||
|
| `-f`, `--format` | Run `gitzone format --write` before commit |
|
||||||
|
| `--plan` | Show resolved workflow only |
|
||||||
|
|
||||||
### 🏗️ Meta Repository Management
|
`-r` is intentionally not part of commit anymore. Use `gitzone release`.
|
||||||
|
|
||||||
Manage multiple related repositories as a cohesive unit:
|
## Release Workflow
|
||||||
|
|
||||||
|
`gitzone release` performs the release core once, then publishes to configured targets.
|
||||||
|
|
||||||
|
The release core is not configurable plumbing. It always follows the same professional release transaction:
|
||||||
|
|
||||||
|
1. Run configured preflight checks.
|
||||||
|
2. Read `changelog.md` `## Pending` entries.
|
||||||
|
3. Infer or accept a semver bump.
|
||||||
|
4. Update version files and baked commit info.
|
||||||
|
5. Move pending changelog entries into the new version section.
|
||||||
|
6. Create the local release commit.
|
||||||
|
7. Create the local release tag.
|
||||||
|
|
||||||
|
Targets decide what happens after that:
|
||||||
|
|
||||||
|
| Target | What it does |
|
||||||
|
| --- | --- |
|
||||||
|
| `git` | Pushes the release commit and tags, often triggering remote CI release builds |
|
||||||
|
| `npm` | Publishes the package to configured npm registries |
|
||||||
|
| `docker` | Delegates container builds and pushes to `tsdocker` |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Initialize a meta repository
|
# Preview the resolved release plan
|
||||||
gitzone meta init
|
gitzone release --plan
|
||||||
|
|
||||||
# Add a sub-project
|
# Release to configured targets
|
||||||
gitzone meta add [name] [git-url]
|
gitzone release
|
||||||
|
|
||||||
# Update all sub-projects
|
# Release only to npm
|
||||||
gitzone meta update
|
gitzone release --target npm
|
||||||
|
|
||||||
# Remove a sub-project
|
# Release only to git and Docker
|
||||||
gitzone meta remove [name]
|
gitzone release --target git,docker
|
||||||
|
|
||||||
|
# Skip package/container publishing and keep only git target
|
||||||
|
gitzone release --no-publish
|
||||||
|
|
||||||
|
# Override inferred semver level
|
||||||
|
gitzone release --minor
|
||||||
```
|
```
|
||||||
|
|
||||||
Perfect for:
|
Release flags:
|
||||||
|
|
||||||
- Monorepo management
|
| Flag | Meaning |
|
||||||
- Multi-package projects
|
| --- | --- |
|
||||||
- Coordinated deployments
|
| `-y`, `--yes` | Run without interactive confirmation |
|
||||||
- Synchronized versioning
|
| `-t`, `--test` | Enable preflight tests |
|
||||||
|
| `-b`, `--build` | Enable preflight build |
|
||||||
|
| `-p`, `--push` | Enable the `git` target |
|
||||||
|
| `--target <csv>` | Use only selected targets, e.g. `git,npm` |
|
||||||
|
| `--npm` | Enable the `npm` target |
|
||||||
|
| `--docker` | Enable the `docker` target |
|
||||||
|
| `--no-publish` | Keep release core and `git` target only |
|
||||||
|
| `--no-build` | Disable preflight build for this run |
|
||||||
|
| `--major`, `--minor`, `--patch` | Override inferred semver level |
|
||||||
|
| `--plan` | Show resolved workflow only |
|
||||||
|
|
||||||
### 🐳 Docker Management
|
## Standard Changelog
|
||||||
|
|
||||||
Streamline your Docker workflow:
|
The changelog is convention-based and intentionally not configured.
|
||||||
|
|
||||||
```bash
|
`gitzone commit` appends entries to:
|
||||||
# Clean up all Docker resources
|
|
||||||
gitzone docker prune
|
```markdown
|
||||||
|
## Pending
|
||||||
```
|
```
|
||||||
|
|
||||||
This command removes:
|
`gitzone release` moves those pending entries into a dated version section:
|
||||||
|
|
||||||
- Stopped containers
|
```markdown
|
||||||
- Unused images
|
## 2026-05-10 - 2.15.0
|
||||||
- Dangling volumes
|
|
||||||
- Unused networks
|
|
||||||
|
|
||||||
### 🔗 Quick CI/CD Access
|
|
||||||
|
|
||||||
Jump directly to your CI/CD configurations:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Open CI/CD settings
|
|
||||||
gitzone open ci
|
|
||||||
|
|
||||||
# Open pipelines view
|
|
||||||
gitzone open pipelines
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Works with GitLab repositories to provide instant access to your deployment configurations.
|
The standard buckets are `Breaking Changes`, `Features`, `Fixes`, `Documentation`, and `Maintenance`.
|
||||||
|
|
||||||
### 📝 Package Deprecation
|
## Configuration
|
||||||
|
|
||||||
Smoothly transition users from old to new packages:
|
CLI workflow config lives under `@git.zone/cli` in `.smartconfig.json`. Docker build and registry behavior lives under `@git.zone/tsdocker` and is used by the Docker release target.
|
||||||
|
|
||||||
```bash
|
|
||||||
gitzone deprecate
|
|
||||||
```
|
|
||||||
|
|
||||||
Interactive wizard for:
|
|
||||||
|
|
||||||
- Setting deprecation notices
|
|
||||||
- Guiding users to replacements
|
|
||||||
- Updating registry metadata
|
|
||||||
- Coordinating migration paths
|
|
||||||
|
|
||||||
### 🚦 Project Initialization
|
|
||||||
|
|
||||||
Prepare existing projects for development:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gitzone start
|
|
||||||
```
|
|
||||||
|
|
||||||
Automatically:
|
|
||||||
|
|
||||||
- Checks out master branch
|
|
||||||
- Pulls latest changes
|
|
||||||
- Installs dependencies
|
|
||||||
- Sets up development environment
|
|
||||||
|
|
||||||
### 🔧 Helper Utilities
|
|
||||||
|
|
||||||
Quick utilities for common tasks:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate a unique short ID
|
|
||||||
gitzone helpers shortid
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Configuration
|
|
||||||
|
|
||||||
### npmextra.json Configuration
|
|
||||||
|
|
||||||
Customize gitzone behavior through `npmextra.json`:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"gitzone": {
|
"@git.zone/cli": {
|
||||||
"format": {
|
"schemaVersion": 2,
|
||||||
"interactive": true,
|
"projectType": "npm",
|
||||||
"showDiffs": false,
|
"commit": {
|
||||||
"autoApprove": false,
|
"confirmation": "prompt",
|
||||||
"parallel": true,
|
"steps": ["analyze", "test", "build", "changelog", "commit", "push"]
|
||||||
"rollback": {
|
},
|
||||||
"enabled": true,
|
"release": {
|
||||||
"autoRollbackOnError": true,
|
"confirmation": "prompt",
|
||||||
"backupRetentionDays": 7
|
"preflight": {
|
||||||
|
"requireCleanTree": true,
|
||||||
|
"test": false,
|
||||||
|
"build": true
|
||||||
},
|
},
|
||||||
"modules": {
|
"targets": {
|
||||||
"skip": ["prettier"],
|
"git": {
|
||||||
"only": [],
|
"enabled": true,
|
||||||
"order": []
|
"remote": "origin",
|
||||||
},
|
"pushBranch": true,
|
||||||
"cache": {
|
"pushTags": true
|
||||||
"enabled": true,
|
},
|
||||||
"clean": true
|
"npm": {
|
||||||
|
"enabled": true,
|
||||||
|
"registries": ["https://registry.npmjs.org"],
|
||||||
|
"accessLevel": "public",
|
||||||
|
"alreadyPublished": "success"
|
||||||
|
},
|
||||||
|
"docker": {
|
||||||
|
"enabled": true,
|
||||||
|
"engine": "tsdocker",
|
||||||
|
"patterns": [],
|
||||||
|
"cached": true,
|
||||||
|
"parallel": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"@git.zone/tsdocker": {
|
||||||
|
"registries": ["registry.gitlab.com"],
|
||||||
|
"registryRepoMap": {
|
||||||
|
"registry.gitlab.com": "myorg/myproject"
|
||||||
|
},
|
||||||
|
"platforms": ["linux/amd64", "linux/arm64"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
NPM registries belong only here:
|
||||||
|
|
||||||
- `CI` - Detect CI environment for automated workflows
|
```text
|
||||||
- `DEBUG` - Enable debug output
|
@git.zone/cli.release.targets.npm.registries
|
||||||
- `GITZONE_FORMAT_PARALLEL` - Control parallel formatting
|
```
|
||||||
|
|
||||||
## 🏆 Best Practices
|
Docker registries belong only here and should be registry hosts without `http://` or `https://`:
|
||||||
|
|
||||||
### For New Projects
|
```text
|
||||||
|
@git.zone/tsdocker.registries
|
||||||
|
```
|
||||||
|
|
||||||
1. Start with a template: `gitzone template npm`
|
Useful config commands:
|
||||||
2. Set up local services: `gitzone services start`
|
|
||||||
3. Customize the generated structure
|
|
||||||
4. Run initial format: `gitzone format`
|
|
||||||
5. 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. Set up services: `gitzone services start`
|
|
||||||
5. Commit changes: `gitzone commit`
|
|
||||||
|
|
||||||
### For Teams
|
|
||||||
|
|
||||||
1. Document format preferences in `npmextra.json`
|
|
||||||
2. Share `.nogit/env.json` template for consistent service setup
|
|
||||||
3. Use `--save-plan` for reviewable format changes
|
|
||||||
4. Enable rollback for safety
|
|
||||||
5. Standardize commit conventions
|
|
||||||
|
|
||||||
## 🎯 Common Workflows
|
|
||||||
|
|
||||||
### Full-Stack Development Cycle
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Start fresh
|
# Show current @git.zone/cli config
|
||||||
gitzone start
|
gitzone config show --json
|
||||||
|
|
||||||
# 2. Spin up databases and services
|
# Configure project basics, CLI behavior, and release targets interactively
|
||||||
gitzone services start
|
gitzone config project
|
||||||
|
gitzone config cli
|
||||||
|
gitzone config release
|
||||||
|
|
||||||
# 3. Make changes
|
# Validate schema, legacy keys, release targets, registries, and npm auth
|
||||||
# ... your development work ...
|
gitzone config doctor
|
||||||
|
|
||||||
# 4. Check service logs if needed
|
# Use opencode to repair configuration issues found by doctor
|
||||||
gitzone services logs mongo
|
gitzone config fix
|
||||||
|
|
||||||
# 5. Format code
|
# Read the npm release target registries
|
||||||
|
gitzone config get release.targets.npm.registries
|
||||||
|
|
||||||
|
# Add an npm release target registry
|
||||||
|
gitzone config add https://registry.npmjs.org
|
||||||
|
|
||||||
|
# Set npm target access level
|
||||||
|
gitzone config access public
|
||||||
|
|
||||||
|
# Run schema migration to v2
|
||||||
|
gitzone config migrate 2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
`gitzone format` is dry-run by default. That makes it safe to run in any repo.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Preview changes
|
||||||
gitzone format
|
gitzone format
|
||||||
|
|
||||||
# 6. Commit with semantic versioning
|
# Emit a machine-readable plan
|
||||||
gitzone commit
|
gitzone format plan --json
|
||||||
|
|
||||||
# 7. Stop services when done
|
# Fail when formatting changes or validator errors remain
|
||||||
gitzone services stop
|
gitzone format check
|
||||||
|
|
||||||
|
# Run a subset of formatters
|
||||||
|
gitzone format --only prettier,packagejson
|
||||||
|
|
||||||
|
# Apply changes
|
||||||
|
gitzone format --write
|
||||||
|
|
||||||
|
# Apply without prompt
|
||||||
|
gitzone format --write --yes
|
||||||
|
|
||||||
|
# Apply deterministic fixes, then use opencode for remaining issues
|
||||||
|
gitzone format fix
|
||||||
```
|
```
|
||||||
|
|
||||||
### Multi-Repository Management
|
Formatters include cleanup, smartconfig normalization, dependency license checks, package metadata normalization, template updates, `.gitignore`, TypeScript config, Prettier, README existence checks, and configured copy operations.
|
||||||
|
|
||||||
|
`gitzone format fix` intentionally lives outside the default format path. Normal format runs stay deterministic; the fix command uses opencode only after deterministic formatters have done what they can.
|
||||||
|
|
||||||
|
## Development Services
|
||||||
|
|
||||||
|
`gitzone services` manages local Docker-backed services for development projects.
|
||||||
|
|
||||||
|
Supported services:
|
||||||
|
|
||||||
|
| Service | Aliases |
|
||||||
|
| --- | --- |
|
||||||
|
| MongoDB | `mongo`, `mongodb` |
|
||||||
|
| MinIO | `minio`, `s3` |
|
||||||
|
| Elasticsearch | `elasticsearch`, `es` |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Set up meta repository
|
# Start configured services
|
||||||
gitzone meta init
|
|
||||||
|
|
||||||
# 2. Add all related projects
|
|
||||||
gitzone meta add frontend https://github.com/org/frontend.git
|
|
||||||
gitzone meta add backend https://github.com/org/backend.git
|
|
||||||
gitzone meta add shared https://github.com/org/shared.git
|
|
||||||
|
|
||||||
# 3. Synchronize updates
|
|
||||||
gitzone meta update
|
|
||||||
```
|
|
||||||
|
|
||||||
### Safe Formatting with Rollback
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Preview changes
|
|
||||||
gitzone format --dry-run
|
|
||||||
|
|
||||||
# 2. Save plan for review
|
|
||||||
gitzone format --save-plan format-changes.json
|
|
||||||
|
|
||||||
# 3. Apply formatting
|
|
||||||
gitzone format --from-plan format-changes.json
|
|
||||||
|
|
||||||
# 4. If something goes wrong, rollback
|
|
||||||
gitzone format --rollback
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database-Driven Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Start MongoDB and MinIO
|
|
||||||
gitzone services start
|
gitzone services start
|
||||||
|
|
||||||
# 2. Get connection string for your app
|
# Enable specific services non-interactively
|
||||||
gitzone services config
|
gitzone services set mongodb,minio
|
||||||
|
|
||||||
# 3. Connect with MongoDB Compass
|
# Check status
|
||||||
gitzone services compass
|
|
||||||
|
|
||||||
# 4. Monitor services
|
|
||||||
gitzone services status
|
gitzone services status
|
||||||
|
|
||||||
# 5. Clean everything when done
|
# Print MongoDB Compass connection string
|
||||||
gitzone services clean # ⚠️ Warning: deletes data
|
gitzone services compass
|
||||||
|
|
||||||
|
# Show logs
|
||||||
|
gitzone services logs mongo 50
|
||||||
|
|
||||||
|
# Stop containers but keep data
|
||||||
|
gitzone services stop
|
||||||
|
|
||||||
|
# Remove containers and data
|
||||||
|
gitzone services clean
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔌 Integrations
|
Service config is stored in `.nogit/env.json`. Data is stored below `.nogit/`, so it stays out of Git.
|
||||||
|
|
||||||
### CI/CD Platforms
|
## Templates
|
||||||
|
|
||||||
- **GitLab CI** - Full pipeline support with templates
|
Start new projects with built-in scaffolds:
|
||||||
- **GitHub Actions** - Automated workflows
|
|
||||||
- **Docker** - Container-based deployments
|
|
||||||
|
|
||||||
### Development Tools
|
```bash
|
||||||
|
gitzone template npm
|
||||||
|
gitzone template service
|
||||||
|
gitzone template website
|
||||||
|
gitzone template wcc
|
||||||
|
```
|
||||||
|
|
||||||
- **TypeScript** - First-class support
|
Templates are rendered through SmartScaf and then can be normalized with `gitzone format`.
|
||||||
- **Prettier** - Code formatting
|
|
||||||
- **npm/pnpm** - Package management
|
|
||||||
- **MongoDB** - Local database service
|
|
||||||
- **MinIO** - S3-compatible object storage
|
|
||||||
- **MongoDB Compass** - Database GUI integration
|
|
||||||
|
|
||||||
### Version Control
|
## Meta Repositories
|
||||||
|
|
||||||
- **Git** - Deep integration
|
Use `gitzone meta` when one workspace coordinates multiple repositories.
|
||||||
- **Semantic Versioning** - Automatic version bumping
|
|
||||||
- **Conventional Commits** - Standardized commit messages
|
|
||||||
|
|
||||||
## 💡 Pro Tips
|
```bash
|
||||||
|
gitzone meta init
|
||||||
|
gitzone meta add frontend https://example.com/org/frontend.git
|
||||||
|
gitzone meta update
|
||||||
|
gitzone meta remove frontend
|
||||||
|
```
|
||||||
|
|
||||||
1. **Use aliases**: Add `alias gz='gitzone'` to your shell profile
|
## Other Utilities
|
||||||
2. **Combine commands**: `gitzone format --yes && gitzone commit`
|
|
||||||
3. **Leverage templates**: Start projects right with proven structures
|
|
||||||
4. **Enable caching**: Dramatically speeds up formatting operations
|
|
||||||
5. **Save format plans**: Review changes before applying in production
|
|
||||||
6. **Port management**: Let services auto-assign ports to avoid conflicts
|
|
||||||
7. **Use MongoDB Compass**: `gitzone services compass` for visual DB management
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
```bash
|
||||||
|
# Docker cleanup
|
||||||
|
gitzone docker prune
|
||||||
|
|
||||||
### Format Command Shows "Cancelled"
|
# Open GitLab CI settings or pipelines for the current repo
|
||||||
|
gitzone open ci
|
||||||
|
gitzone open pipelines
|
||||||
|
|
||||||
If the format command shows cancelled even after confirming:
|
# Deprecate an old npm package interactively
|
||||||
|
gitzone deprecate
|
||||||
|
|
||||||
- Check your `npmextra.json` configuration
|
# Prepare a project for local work
|
||||||
- Try with `--yes` flag to skip confirmation
|
gitzone start
|
||||||
- Use `--verbose` for detailed output
|
|
||||||
|
|
||||||
### Docker Commands Fail
|
# Generate a short unique ID
|
||||||
|
gitzone helpers shortid
|
||||||
|
```
|
||||||
|
|
||||||
Ensure Docker daemon is running:
|
## Troubleshooting
|
||||||
|
|
||||||
|
Format only previews changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gitzone format --write
|
||||||
|
```
|
||||||
|
|
||||||
|
Release says there is nothing to release:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make sure commits have populated the Pending changelog section
|
||||||
|
gitzone commit
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker services fail to start:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker info
|
docker info
|
||||||
|
gitzone services status
|
||||||
|
gitzone services reconfigure
|
||||||
```
|
```
|
||||||
|
|
||||||
### Services Won't Start
|
Config looks outdated:
|
||||||
|
|
||||||
Check for port conflicts:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Services auto-assign ports, but you can check the config
|
gitzone config migrate 2
|
||||||
cat .nogit/env.json
|
gitzone config show --json
|
||||||
|
|
||||||
# Verify Docker is running
|
|
||||||
docker ps
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Template Creation Issues
|
|
||||||
|
|
||||||
Verify npm/pnpm is properly configured:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm config get registry
|
|
||||||
```
|
|
||||||
|
|
||||||
### MongoDB Connection Issues
|
|
||||||
|
|
||||||
- Ensure services are running: `gitzone services status`
|
|
||||||
- Check firewall settings for the assigned ports
|
|
||||||
- Use `gitzone services compass` for the correct connection string
|
|
||||||
|
|
||||||
## 📈 Performance
|
|
||||||
|
|
||||||
gitzone is optimized for speed:
|
|
||||||
|
|
||||||
- **Parallel processing** for format operations
|
|
||||||
- **Smart caching** to avoid redundant work
|
|
||||||
- **Incremental updates** for meta repositories
|
|
||||||
- **Minimal dependencies** for fast installation
|
|
||||||
- **Isolated services** prevent resource conflicts
|
|
||||||
- **Auto port assignment** eliminates manual configuration
|
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/cli',
|
name: '@git.zone/cli',
|
||||||
version: '2.12.1',
|
version: '2.19.1',
|
||||||
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.'
|
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.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,11 +38,11 @@ export class GitzoneConfig {
|
|||||||
public data: IGitzoneConfigData;
|
public data: IGitzoneConfigData;
|
||||||
|
|
||||||
public async readConfigFromCwd() {
|
public async readConfigFromCwd() {
|
||||||
const npmextraInstance = new plugins.npmextra.Npmextra(paths.cwd);
|
const smartconfigInstance = new plugins.smartconfig.Smartconfig(paths.cwd);
|
||||||
this.data = npmextraInstance.dataFor<IGitzoneConfigData>('@git.zone/cli', {});
|
this.data = smartconfigInstance.dataFor<IGitzoneConfigData>('@git.zone/cli', {});
|
||||||
|
|
||||||
// Read szci config for backward compatibility
|
// Read szci config for backward compatibility
|
||||||
const szciConfig = npmextraInstance.dataFor<any>('@ship.zone/szci', {});
|
const szciConfig = smartconfigInstance.dataFor<any>('@ship.zone/szci', {});
|
||||||
|
|
||||||
// Prefer accessLevel from @git.zone/cli.release, fallback to @ship.zone/szci.npmAccessLevel
|
// Prefer accessLevel from @git.zone/cli.release, fallback to @ship.zone/szci.npmAccessLevel
|
||||||
const accessLevel =
|
const accessLevel =
|
||||||
|
|||||||
+127
-146
@@ -1,156 +1,137 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from "./plugins.js";
|
||||||
import * as paths from './paths.js';
|
import * as paths from "./paths.js";
|
||||||
import { GitzoneConfig } from './classes.gitzoneconfig.js';
|
import {
|
||||||
|
getProcessUserArgv,
|
||||||
|
getRawCliMode,
|
||||||
|
parseCliArgv,
|
||||||
|
} from "./helpers.climode.js";
|
||||||
|
import { commitinfo } from "./00_commitinfo_data.js";
|
||||||
|
|
||||||
const gitzoneSmartcli = new plugins.smartcli.Smartcli();
|
const runParsedCommand = async (argvArg: any): Promise<void> => {
|
||||||
|
const command = argvArg._?.[0];
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case undefined:
|
||||||
|
case "help": {
|
||||||
|
const modStandard = await import("./mod_standard/index.js");
|
||||||
|
await modStandard.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "commit": {
|
||||||
|
const modCommit = await import("./mod_commit/index.js");
|
||||||
|
await modCommit.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "release": {
|
||||||
|
const modRelease = await import("./mod_release/index.js");
|
||||||
|
await modRelease.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "deprecate": {
|
||||||
|
const modDeprecate = await import("./mod_deprecate/index.js");
|
||||||
|
await modDeprecate.run();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "docker": {
|
||||||
|
const modDocker = await import("./mod_docker/index.js");
|
||||||
|
await modDocker.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "format": {
|
||||||
|
const modFormat = await import("./mod_format/index.js");
|
||||||
|
await modFormat.run({
|
||||||
|
...argvArg,
|
||||||
|
write: argvArg.write || argvArg.w,
|
||||||
|
dryRun: argvArg["dry-run"],
|
||||||
|
yes: argvArg.yes || argvArg.y,
|
||||||
|
planOnly: argvArg["plan-only"] || argvArg.planOnly,
|
||||||
|
savePlan: argvArg["save-plan"] || argvArg.savePlan,
|
||||||
|
fromPlan: argvArg["from-plan"] || argvArg.fromPlan,
|
||||||
|
detailed: argvArg.detailed,
|
||||||
|
interactive: argvArg.interactive !== false,
|
||||||
|
verbose: argvArg.verbose,
|
||||||
|
diff: argvArg.diff,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "meta": {
|
||||||
|
const modMeta = await import("./mod_meta/index.js");
|
||||||
|
await modMeta.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "open": {
|
||||||
|
const modOpen = await import("./mod_open/index.js");
|
||||||
|
await modOpen.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "template": {
|
||||||
|
const modTemplate = await import("./mod_template/index.js");
|
||||||
|
await modTemplate.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "start": {
|
||||||
|
const modStart = await import("./mod_start/index.js");
|
||||||
|
await modStart.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "helpers": {
|
||||||
|
const modHelpers = await import("./mod_helpers/index.js");
|
||||||
|
await modHelpers.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "tools": {
|
||||||
|
const modTools = await import("./mod_tools/index.js");
|
||||||
|
await modTools.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "config": {
|
||||||
|
const modConfig = await import("./mod_config/index.js");
|
||||||
|
await modConfig.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "services": {
|
||||||
|
const modServices = await import("./mod_services/index.js");
|
||||||
|
await modServices.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const modStandard = await import("./mod_standard/index.js");
|
||||||
|
await modStandard.run(argvArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export let run = async () => {
|
export let run = async () => {
|
||||||
const done = plugins.smartpromise.defer();
|
const rawCliMode = await getRawCliMode();
|
||||||
|
|
||||||
// get packageInfo
|
// get packageInfo
|
||||||
const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||||
|
const projectInfoVersion = (projectInfo.npm as any)?.version;
|
||||||
|
const packageVersion =
|
||||||
|
typeof projectInfoVersion === "string" && projectInfoVersion.length > 0
|
||||||
|
? projectInfoVersion
|
||||||
|
: commitinfo.version;
|
||||||
|
|
||||||
// check for updates
|
// check for updates
|
||||||
const smartupdateInstance = new plugins.smartupdate.SmartUpdate();
|
if (rawCliMode.checkUpdates) {
|
||||||
await smartupdateInstance.check(
|
const smartupdateInstance = new plugins.smartupdate.SmartUpdate();
|
||||||
'gitzone',
|
try {
|
||||||
projectInfo.npm.version,
|
await smartupdateInstance.check(
|
||||||
'http://gitzone.gitlab.io/gitzone/changelog.html',
|
"gitzone",
|
||||||
);
|
packageVersion,
|
||||||
console.log('---------------------------------------------');
|
"http://gitzone.gitlab.io/gitzone/changelog.html",
|
||||||
gitzoneSmartcli.addVersion(projectInfo.npm.version);
|
);
|
||||||
|
} catch {
|
||||||
// ======> Standard task <======
|
// Update checks must never block actual CLI commands.
|
||||||
|
|
||||||
/**
|
|
||||||
* standard task
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.standardCommand().subscribe(async (argvArg) => {
|
|
||||||
const modStandard = await import('./mod_standard/index.js');
|
|
||||||
await modStandard.run();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ======> Specific tasks <======
|
|
||||||
|
|
||||||
/**
|
|
||||||
* commit something
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand('commit').subscribe(async (argvArg) => {
|
|
||||||
const modCommit = await import('./mod_commit/index.js');
|
|
||||||
await modCommit.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* deprecate a package on npm
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand('deprecate').subscribe(async (argvArg) => {
|
|
||||||
const modDeprecate = await import('./mod_deprecate/index.js');
|
|
||||||
await modDeprecate.run();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* docker
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand('docker').subscribe(async (argvArg) => {
|
|
||||||
const modDocker = await import('./mod_docker/index.js');
|
|
||||||
await modDocker.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update all files that comply with the gitzone standard
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand('format').subscribe(async (argvArg) => {
|
|
||||||
const config = GitzoneConfig.fromCwd();
|
|
||||||
const modFormat = await import('./mod_format/index.js');
|
|
||||||
|
|
||||||
// Handle rollback commands
|
|
||||||
if (argvArg.rollback) {
|
|
||||||
await modFormat.handleRollback(argvArg.rollback);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (argvArg['list-backups']) {
|
if (rawCliMode.output === "human") {
|
||||||
await modFormat.handleListBackups();
|
console.log("---------------------------------------------");
|
||||||
return;
|
}
|
||||||
}
|
const argvArg = parseCliArgv(getProcessUserArgv());
|
||||||
|
if (argvArg.v || argvArg.version) {
|
||||||
if (argvArg['clean-backups']) {
|
console.log(packageVersion);
|
||||||
await modFormat.handleCleanBackups();
|
return;
|
||||||
return;
|
}
|
||||||
}
|
await runParsedCommand(argvArg);
|
||||||
|
|
||||||
// Handle format with options
|
|
||||||
// Default is dry-mode, use --write/-w to apply changes
|
|
||||||
await modFormat.run({
|
|
||||||
write: argvArg.write || argvArg.w,
|
|
||||||
dryRun: argvArg['dry-run'],
|
|
||||||
yes: argvArg.yes,
|
|
||||||
planOnly: argvArg['plan-only'],
|
|
||||||
savePlan: argvArg['save-plan'],
|
|
||||||
fromPlan: argvArg['from-plan'],
|
|
||||||
detailed: argvArg.detailed,
|
|
||||||
interactive: argvArg.interactive !== false,
|
|
||||||
parallel: argvArg.parallel !== false,
|
|
||||||
verbose: argvArg.verbose,
|
|
||||||
diff: argvArg.diff,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* run meta commands
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand('meta').subscribe(async (argvArg) => {
|
|
||||||
const config = GitzoneConfig.fromCwd();
|
|
||||||
const modMeta = await import('./mod_meta/index.js');
|
|
||||||
modMeta.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* open assets
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand('open').subscribe(async (argvArg) => {
|
|
||||||
const modOpen = await import('./mod_open/index.js');
|
|
||||||
modOpen.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add a readme to a project
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand('template').subscribe(async (argvArg) => {
|
|
||||||
const modTemplate = await import('./mod_template/index.js');
|
|
||||||
modTemplate.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* start working on a project
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand('start').subscribe(async (argvArg) => {
|
|
||||||
const modTemplate = await import('./mod_start/index.js');
|
|
||||||
modTemplate.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
gitzoneSmartcli.addCommand('helpers').subscribe(async (argvArg) => {
|
|
||||||
const modHelpers = await import('./mod_helpers/index.js');
|
|
||||||
modHelpers.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* manage release configuration
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand('config').subscribe(async (argvArg) => {
|
|
||||||
const modConfig = await import('./mod_config/index.js');
|
|
||||||
await modConfig.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* manage development services (MongoDB, S3/MinIO)
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand('services').subscribe(async (argvArg) => {
|
|
||||||
const modServices = await import('./mod_services/index.js');
|
|
||||||
await modServices.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
// start parsing of the cli
|
|
||||||
gitzoneSmartcli.startParse();
|
|
||||||
return await done.promise;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import * as plugins from "./plugins.js";
|
||||||
|
|
||||||
|
export type TChangelogBucket =
|
||||||
|
| "Breaking Changes"
|
||||||
|
| "Features"
|
||||||
|
| "Fixes"
|
||||||
|
| "Documentation"
|
||||||
|
| "Maintenance";
|
||||||
|
|
||||||
|
export interface IChangelogEntry {
|
||||||
|
type: string;
|
||||||
|
scope: string;
|
||||||
|
message: string;
|
||||||
|
details?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPendingChangelog {
|
||||||
|
block: string;
|
||||||
|
isEmpty: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucketForCommitType = (commitType: string): TChangelogBucket => {
|
||||||
|
switch (commitType) {
|
||||||
|
case "BREAKING CHANGE":
|
||||||
|
return "Breaking Changes";
|
||||||
|
case "feat":
|
||||||
|
return "Features";
|
||||||
|
case "fix":
|
||||||
|
return "Fixes";
|
||||||
|
case "docs":
|
||||||
|
return "Documentation";
|
||||||
|
default:
|
||||||
|
return "Maintenance";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const readChangelog = async (filePath: string): Promise<string> => {
|
||||||
|
if (!(await plugins.smartfs.file(filePath).exists())) {
|
||||||
|
return "# Changelog\n\n";
|
||||||
|
}
|
||||||
|
return (await plugins.smartfs.file(filePath).encoding("utf8").read()) as string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeChangelog = async (filePath: string, content: string): Promise<void> => {
|
||||||
|
await plugins.smartfs.file(filePath).encoding("utf8").write(content.endsWith("\n") ? content : `${content}\n`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findPendingSection = (
|
||||||
|
content: string,
|
||||||
|
sectionName: string,
|
||||||
|
): { start: number; bodyStart: number; end: number } | null => {
|
||||||
|
const headingRegex = new RegExp(`^##\\s+${sectionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
|
||||||
|
const match = headingRegex.exec(content);
|
||||||
|
if (!match || match.index === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyStart = match.index + match[0].length;
|
||||||
|
const rest = content.slice(bodyStart);
|
||||||
|
const nextHeadingMatch = /^##\s+/m.exec(rest);
|
||||||
|
const end = nextHeadingMatch ? bodyStart + nextHeadingMatch.index : content.length;
|
||||||
|
return { start: match.index, bodyStart, end };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ensurePendingSection = async (
|
||||||
|
filePath: string,
|
||||||
|
sectionName = "Pending",
|
||||||
|
): Promise<string> => {
|
||||||
|
let content = await readChangelog(filePath);
|
||||||
|
if (findPendingSection(content, sectionName)) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingSection = `## ${sectionName}\n\n`;
|
||||||
|
const titleMatch = /^#\s+.+$/m.exec(content);
|
||||||
|
if (titleMatch && titleMatch.index !== undefined) {
|
||||||
|
const insertAt = titleMatch.index + titleMatch[0].length;
|
||||||
|
content = `${content.slice(0, insertAt)}\n\n${pendingSection}${content.slice(insertAt).replace(/^\n+/, "")}`;
|
||||||
|
} else {
|
||||||
|
content = `# Changelog\n\n${pendingSection}${content}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeChangelog(filePath, content);
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appendPendingChangelogEntry = async (
|
||||||
|
filePath: string,
|
||||||
|
sectionName: string,
|
||||||
|
entry: IChangelogEntry,
|
||||||
|
): Promise<void> => {
|
||||||
|
let content = await ensurePendingSection(filePath, sectionName);
|
||||||
|
const pendingSection = findPendingSection(content, sectionName)!;
|
||||||
|
let pendingBody = content.slice(pendingSection.bodyStart, pendingSection.end);
|
||||||
|
const bucket = bucketForCommitType(entry.type);
|
||||||
|
const bucketHeading = `### ${bucket}`;
|
||||||
|
|
||||||
|
const entryLines = [`- ${entry.message}${entry.scope ? ` (${entry.scope})` : ""}`];
|
||||||
|
for (const detail of entry.details || []) {
|
||||||
|
entryLines.push(` - ${detail}`);
|
||||||
|
}
|
||||||
|
const renderedEntry = entryLines.join("\n");
|
||||||
|
|
||||||
|
const bucketRegex = new RegExp(`^###\\s+${bucket.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
|
||||||
|
const bucketMatch = bucketRegex.exec(pendingBody);
|
||||||
|
if (!bucketMatch || bucketMatch.index === undefined) {
|
||||||
|
pendingBody = `${pendingBody.trimEnd()}\n\n${bucketHeading}\n\n${renderedEntry}\n`;
|
||||||
|
} else {
|
||||||
|
const bucketBodyStart = bucketMatch.index + bucketMatch[0].length;
|
||||||
|
const afterBucket = pendingBody.slice(bucketBodyStart);
|
||||||
|
const nextBucketMatch = /^###\s+/m.exec(afterBucket);
|
||||||
|
const insertAt = nextBucketMatch ? bucketBodyStart + nextBucketMatch.index : pendingBody.length;
|
||||||
|
const beforeInsert = pendingBody.slice(0, insertAt).trimEnd();
|
||||||
|
const afterInsert = pendingBody.slice(insertAt).replace(/^\n+/, "");
|
||||||
|
pendingBody = `${beforeInsert}\n${renderedEntry}\n\n${afterInsert}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = `${content.slice(0, pendingSection.bodyStart)}\n${pendingBody.trim()}\n\n${content.slice(pendingSection.end).replace(/^\n+/, "")}`;
|
||||||
|
await writeChangelog(filePath, content);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readPendingChangelog = async (
|
||||||
|
filePath: string,
|
||||||
|
sectionName = "Pending",
|
||||||
|
): Promise<IPendingChangelog> => {
|
||||||
|
const content = await ensurePendingSection(filePath, sectionName);
|
||||||
|
const pendingSection = findPendingSection(content, sectionName)!;
|
||||||
|
const block = content.slice(pendingSection.bodyStart, pendingSection.end).trim();
|
||||||
|
return {
|
||||||
|
block,
|
||||||
|
isEmpty: block.length === 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inferVersionTypeFromPending = (pendingBlock: string): "patch" | "minor" | "major" => {
|
||||||
|
if (/^###\s+Breaking Changes\s*$/m.test(pendingBlock)) {
|
||||||
|
return "major";
|
||||||
|
}
|
||||||
|
if (/^###\s+Features\s*$/m.test(pendingBlock)) {
|
||||||
|
return "minor";
|
||||||
|
}
|
||||||
|
return "patch";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const movePendingToVersion = async (
|
||||||
|
filePath: string,
|
||||||
|
sectionName: string,
|
||||||
|
versionHeading: string,
|
||||||
|
version: string,
|
||||||
|
dateString: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
let content = await ensurePendingSection(filePath, sectionName);
|
||||||
|
const pendingSection = findPendingSection(content, sectionName)!;
|
||||||
|
const pendingBlock = content.slice(pendingSection.bodyStart, pendingSection.end).trim();
|
||||||
|
if (!pendingBlock) {
|
||||||
|
throw new Error("No pending changelog entries. Nothing to release.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedHeading = versionHeading
|
||||||
|
.replaceAll("{{version}}", version)
|
||||||
|
.replaceAll("{{date}}", dateString);
|
||||||
|
const nextContent = content.slice(pendingSection.end).replace(/^\n+/, "");
|
||||||
|
content = `${content.slice(0, pendingSection.bodyStart)}\n\n${renderedHeading}\n\n${pendingBlock}\n\n${nextContent}`;
|
||||||
|
await writeChangelog(filePath, content);
|
||||||
|
};
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
import { getCliConfigValue } from "./helpers.smartconfig.js";
|
||||||
|
|
||||||
|
export type TCliOutputMode = "human" | "plain" | "json";
|
||||||
|
|
||||||
|
export interface ICliMode {
|
||||||
|
output: TCliOutputMode;
|
||||||
|
interactive: boolean;
|
||||||
|
json: boolean;
|
||||||
|
plain: boolean;
|
||||||
|
quiet: boolean;
|
||||||
|
yes: boolean;
|
||||||
|
help: boolean;
|
||||||
|
agent: boolean;
|
||||||
|
checkUpdates: boolean;
|
||||||
|
isTty: boolean;
|
||||||
|
command?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICliConfigSettings {
|
||||||
|
interactive?: boolean;
|
||||||
|
output?: TCliOutputMode;
|
||||||
|
checkUpdates?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TArgSource = Record<string, any> & { _?: string[] };
|
||||||
|
|
||||||
|
const camelCase = (value: string): string => {
|
||||||
|
return value.replace(/-([a-z])/g, (_match, group: string) =>
|
||||||
|
group.toUpperCase(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getArgValue = (argvArg: TArgSource, key: string): any => {
|
||||||
|
const keyVariants = [key, camelCase(key), key.replace(/-/g, "")];
|
||||||
|
for (const keyVariant of keyVariants) {
|
||||||
|
if (argvArg[keyVariant] !== undefined) {
|
||||||
|
return argvArg[keyVariant];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseRawArgv = (argv: string[]): TArgSource => {
|
||||||
|
const parsedArgv: TArgSource = { _: [] };
|
||||||
|
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const currentArg = argv[i];
|
||||||
|
|
||||||
|
if (currentArg.startsWith("--no-")) {
|
||||||
|
const key = currentArg.slice(5);
|
||||||
|
parsedArgv[key] = false;
|
||||||
|
parsedArgv[camelCase(key)] = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentArg.startsWith("--")) {
|
||||||
|
const withoutPrefix = currentArg.slice(2);
|
||||||
|
const [rawKey, inlineValue] = withoutPrefix.split("=", 2);
|
||||||
|
if (inlineValue !== undefined) {
|
||||||
|
parsedArgv[rawKey] = inlineValue;
|
||||||
|
parsedArgv[camelCase(rawKey)] = inlineValue;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextArg = argv[i + 1];
|
||||||
|
if (nextArg && !nextArg.startsWith("-")) {
|
||||||
|
parsedArgv[rawKey] = nextArg;
|
||||||
|
parsedArgv[camelCase(rawKey)] = nextArg;
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
parsedArgv[rawKey] = true;
|
||||||
|
parsedArgv[camelCase(rawKey)] = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentArg.startsWith("-") && currentArg.length > 1) {
|
||||||
|
for (const shortFlag of currentArg.slice(1).split("")) {
|
||||||
|
parsedArgv[shortFlag] = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedArgv._ = parsedArgv._ || [];
|
||||||
|
parsedArgv._.push(currentArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedArgv;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseCliArgv = parseRawArgv;
|
||||||
|
|
||||||
|
export const getProcessUserArgv = (): string[] => {
|
||||||
|
const rawArgv = process.argv;
|
||||||
|
const argv0Base = (rawArgv[0] || "").split(/[\\/]/).pop()?.toLowerCase();
|
||||||
|
const runtimeNames = new Set([
|
||||||
|
"node",
|
||||||
|
"node.exe",
|
||||||
|
"nodejs",
|
||||||
|
"nodejs.exe",
|
||||||
|
"bun",
|
||||||
|
"bun.exe",
|
||||||
|
"deno",
|
||||||
|
"deno.exe",
|
||||||
|
"tsx",
|
||||||
|
"tsx.exe",
|
||||||
|
"ts-node",
|
||||||
|
"ts-node.exe",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!runtimeNames.has(argv0Base || "")) {
|
||||||
|
return rawArgv.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstUserArg = rawArgv[1] || "";
|
||||||
|
const firstUserArgLooksLikeScript =
|
||||||
|
firstUserArg.includes("/") ||
|
||||||
|
firstUserArg.endsWith(".js") ||
|
||||||
|
firstUserArg.endsWith(".ts") ||
|
||||||
|
firstUserArg.endsWith(".mjs") ||
|
||||||
|
firstUserArg.endsWith(".cjs");
|
||||||
|
|
||||||
|
return rawArgv.slice(firstUserArgLooksLikeScript ? 2 : 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeOutputMode = (value: unknown): TCliOutputMode | undefined => {
|
||||||
|
if (value === "human" || value === "plain" || value === "json") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveCliMode = (
|
||||||
|
argvArg: TArgSource,
|
||||||
|
cliConfig: ICliConfigSettings,
|
||||||
|
): ICliMode => {
|
||||||
|
const isTty = Boolean(process.stdout?.isTTY && process.stdin?.isTTY);
|
||||||
|
const agentMode = Boolean(getArgValue(argvArg, "agent"));
|
||||||
|
const outputOverride = normalizeOutputMode(getArgValue(argvArg, "output"));
|
||||||
|
|
||||||
|
let output: TCliOutputMode =
|
||||||
|
normalizeOutputMode(cliConfig.output) || (isTty ? "human" : "plain");
|
||||||
|
if (agentMode || getArgValue(argvArg, "json")) {
|
||||||
|
output = "json";
|
||||||
|
} else if (getArgValue(argvArg, "plain")) {
|
||||||
|
output = "plain";
|
||||||
|
} else if (outputOverride) {
|
||||||
|
output = outputOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
const interactiveSetting = getArgValue(argvArg, "interactive");
|
||||||
|
let interactive = cliConfig.interactive ?? isTty;
|
||||||
|
if (interactiveSetting === true) {
|
||||||
|
interactive = true;
|
||||||
|
} else if (interactiveSetting === false) {
|
||||||
|
interactive = false;
|
||||||
|
}
|
||||||
|
if (!isTty || output !== "human" || agentMode) {
|
||||||
|
interactive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkUpdatesSetting = getArgValue(argvArg, "check-updates");
|
||||||
|
let checkUpdates = cliConfig.checkUpdates ?? output === "human";
|
||||||
|
if (checkUpdatesSetting === true) {
|
||||||
|
checkUpdates = true;
|
||||||
|
} else if (checkUpdatesSetting === false) {
|
||||||
|
checkUpdates = false;
|
||||||
|
}
|
||||||
|
if (output !== "human" || agentMode) {
|
||||||
|
checkUpdates = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
output,
|
||||||
|
interactive,
|
||||||
|
json: output === "json",
|
||||||
|
plain: output === "plain",
|
||||||
|
quiet: Boolean(
|
||||||
|
getArgValue(argvArg, "quiet") ||
|
||||||
|
getArgValue(argvArg, "q") ||
|
||||||
|
output === "json",
|
||||||
|
),
|
||||||
|
yes: Boolean(getArgValue(argvArg, "yes") || getArgValue(argvArg, "y")),
|
||||||
|
help: Boolean(
|
||||||
|
getArgValue(argvArg, "help") ||
|
||||||
|
getArgValue(argvArg, "h") ||
|
||||||
|
argvArg._?.[0] === "help",
|
||||||
|
),
|
||||||
|
agent: agentMode,
|
||||||
|
checkUpdates,
|
||||||
|
isTty,
|
||||||
|
command: argvArg._?.[0],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCliModeConfig = async (): Promise<ICliConfigSettings> => {
|
||||||
|
return await getCliConfigValue<ICliConfigSettings>("cli", {});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCliMode = async (
|
||||||
|
argvArg: TArgSource = {},
|
||||||
|
): Promise<ICliMode> => {
|
||||||
|
const cliConfig = await getCliModeConfig();
|
||||||
|
return resolveCliMode(argvArg, cliConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRawCliMode = async (): Promise<ICliMode> => {
|
||||||
|
const cliConfig = await getCliModeConfig();
|
||||||
|
const rawArgv = parseRawArgv(getProcessUserArgv());
|
||||||
|
return resolveCliMode(rawArgv, cliConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const printJson = (data: unknown): void => {
|
||||||
|
console.log(JSON.stringify(data, null, 2));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runWithSuppressedOutput = async <T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
): Promise<T> => {
|
||||||
|
const originalConsole = {
|
||||||
|
log: console.log,
|
||||||
|
info: console.info,
|
||||||
|
warn: console.warn,
|
||||||
|
error: console.error,
|
||||||
|
};
|
||||||
|
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
||||||
|
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
||||||
|
const noop = () => undefined;
|
||||||
|
|
||||||
|
console.log = noop;
|
||||||
|
console.info = noop;
|
||||||
|
console.warn = noop;
|
||||||
|
console.error = noop;
|
||||||
|
process.stdout.write = (() => true) as typeof process.stdout.write;
|
||||||
|
process.stderr.write = (() => true) as typeof process.stderr.write;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
console.log = originalConsole.log;
|
||||||
|
console.info = originalConsole.info;
|
||||||
|
console.warn = originalConsole.warn;
|
||||||
|
console.error = originalConsole.error;
|
||||||
|
process.stdout.write = originalStdoutWrite;
|
||||||
|
process.stderr.write = originalStderrWrite;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import * as plugins from "./plugins.js";
|
||||||
|
import { rename, writeFile } from "fs/promises";
|
||||||
|
|
||||||
|
export const CLI_NAMESPACE = "@git.zone/cli";
|
||||||
|
|
||||||
|
const isPlainObject = (value: unknown): value is Record<string, any> => {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSmartconfigPath = (cwd: string = process.cwd()): string => {
|
||||||
|
return plugins.path.join(cwd, ".smartconfig.json");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readSmartconfigFile = async (
|
||||||
|
cwd: string = process.cwd(),
|
||||||
|
): Promise<Record<string, any>> => {
|
||||||
|
const smartconfigPath = getSmartconfigPath(cwd);
|
||||||
|
if (!(await plugins.smartfs.file(smartconfigPath).exists())) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = (await plugins.smartfs
|
||||||
|
.file(smartconfigPath)
|
||||||
|
.encoding("utf8")
|
||||||
|
.read()) as string;
|
||||||
|
if (content.trim() === "") {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return JSON.parse(content);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const writeSmartconfigFile = async (
|
||||||
|
data: Record<string, any>,
|
||||||
|
cwd: string = process.cwd(),
|
||||||
|
): Promise<void> => {
|
||||||
|
const smartconfigPath = getSmartconfigPath(cwd);
|
||||||
|
const tempPath = `${smartconfigPath}.tmp-${Date.now()}`;
|
||||||
|
const content = JSON.stringify(data, null, 2);
|
||||||
|
await writeFile(tempPath, content, "utf8");
|
||||||
|
await rename(tempPath, smartconfigPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeCliConfigPath = (configPath: string): string => {
|
||||||
|
const trimmedPath = configPath.trim();
|
||||||
|
if (!trimmedPath || trimmedPath === CLI_NAMESPACE) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmedPath.startsWith(`${CLI_NAMESPACE}.`)) {
|
||||||
|
return trimmedPath.slice(`${CLI_NAMESPACE}.`.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmedPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCliConfigPathSegments = (configPath: string): string[] => {
|
||||||
|
const normalizedPath = normalizeCliConfigPath(configPath);
|
||||||
|
if (!normalizedPath) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedPath
|
||||||
|
.split(".")
|
||||||
|
.map((segment) => segment.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCliNamespaceConfig = (
|
||||||
|
smartconfigData: Record<string, any>,
|
||||||
|
): Record<string, any> => {
|
||||||
|
const cliConfig = smartconfigData[CLI_NAMESPACE];
|
||||||
|
if (isPlainObject(cliConfig)) {
|
||||||
|
return cliConfig;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCliConfigValueFromData = (
|
||||||
|
smartconfigData: Record<string, any>,
|
||||||
|
configPath: string,
|
||||||
|
): any => {
|
||||||
|
const segments = getCliConfigPathSegments(configPath);
|
||||||
|
let currentValue: any = getCliNamespaceConfig(smartconfigData);
|
||||||
|
|
||||||
|
for (const segment of segments) {
|
||||||
|
if (!isPlainObject(currentValue) && !Array.isArray(currentValue)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
currentValue = (currentValue as any)?.[segment];
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCliConfigValue = async <T>(
|
||||||
|
configPath: string,
|
||||||
|
defaultValue: T,
|
||||||
|
cwd: string = process.cwd(),
|
||||||
|
): Promise<T> => {
|
||||||
|
const smartconfigData = await readSmartconfigFile(cwd);
|
||||||
|
const configValue = getCliConfigValueFromData(smartconfigData, configPath);
|
||||||
|
|
||||||
|
if (configValue === undefined) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(defaultValue) && isPlainObject(configValue)) {
|
||||||
|
return {
|
||||||
|
...defaultValue,
|
||||||
|
...configValue,
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return configValue as T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setCliConfigValueInData = (
|
||||||
|
smartconfigData: Record<string, any>,
|
||||||
|
configPath: string,
|
||||||
|
value: any,
|
||||||
|
): Record<string, any> => {
|
||||||
|
const segments = getCliConfigPathSegments(configPath);
|
||||||
|
|
||||||
|
if (!isPlainObject(smartconfigData[CLI_NAMESPACE])) {
|
||||||
|
smartconfigData[CLI_NAMESPACE] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segments.length === 0) {
|
||||||
|
smartconfigData[CLI_NAMESPACE] = value;
|
||||||
|
return smartconfigData;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentValue = smartconfigData[CLI_NAMESPACE];
|
||||||
|
for (const segment of segments.slice(0, -1)) {
|
||||||
|
if (!isPlainObject(currentValue[segment])) {
|
||||||
|
currentValue[segment] = {};
|
||||||
|
}
|
||||||
|
currentValue = currentValue[segment];
|
||||||
|
}
|
||||||
|
|
||||||
|
currentValue[segments[segments.length - 1]] = value;
|
||||||
|
return smartconfigData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const unsetCliConfigValueInData = (
|
||||||
|
smartconfigData: Record<string, any>,
|
||||||
|
configPath: string,
|
||||||
|
): boolean => {
|
||||||
|
const segments = getCliConfigPathSegments(configPath);
|
||||||
|
if (segments.length === 0) {
|
||||||
|
if (smartconfigData[CLI_NAMESPACE] !== undefined) {
|
||||||
|
delete smartconfigData[CLI_NAMESPACE];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentSegments = segments.slice(0, -1);
|
||||||
|
let currentValue: any = getCliNamespaceConfig(smartconfigData);
|
||||||
|
const objectPath: Array<Record<string, any>> = [currentValue];
|
||||||
|
|
||||||
|
for (const segment of parentSegments) {
|
||||||
|
if (!isPlainObject(currentValue[segment])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
currentValue = currentValue[segment];
|
||||||
|
objectPath.push(currentValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSegment = segments[segments.length - 1];
|
||||||
|
if (!(lastSegment in currentValue)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete currentValue[lastSegment];
|
||||||
|
|
||||||
|
for (let i = objectPath.length - 1; i >= 1; i--) {
|
||||||
|
if (Object.keys(objectPath[i]).length > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentObject = objectPath[i - 1];
|
||||||
|
const parentKey = parentSegments[i - 1];
|
||||||
|
delete parentObject[parentKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(getCliNamespaceConfig(smartconfigData)).length === 0) {
|
||||||
|
delete smartconfigData[CLI_NAMESPACE];
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
export const CURRENT_GITZONE_CLI_SCHEMA_VERSION = 2;
|
||||||
|
|
||||||
|
export interface ISmartconfigMigrationResult {
|
||||||
|
migrated: boolean;
|
||||||
|
fromVersion: number;
|
||||||
|
toVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLI_NAMESPACE = "@git.zone/cli";
|
||||||
|
|
||||||
|
const isPlainObject = (value: unknown): value is Record<string, any> => {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureObject = (parent: Record<string, any>, key: string): Record<string, any> => {
|
||||||
|
if (!isPlainObject(parent[key])) {
|
||||||
|
parent[key] = {};
|
||||||
|
}
|
||||||
|
return parent[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeRegistryList = (registries: unknown[]): string[] => {
|
||||||
|
const result: string[] = [];
|
||||||
|
for (const registry of registries) {
|
||||||
|
if (typeof registry !== "string" || !registry.trim()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const normalizedRegistry = normalizeRegistryUrl(registry);
|
||||||
|
if (!result.includes(normalizedRegistry)) {
|
||||||
|
result.push(normalizedRegistry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrateLegacyReleaseArray = (smartconfigJson: Record<string, any>): boolean => {
|
||||||
|
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
|
||||||
|
if (!Array.isArray(cliConfig.release)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registries = normalizeRegistryList(cliConfig.release);
|
||||||
|
cliConfig.release = {
|
||||||
|
targets: {
|
||||||
|
npm: {
|
||||||
|
enabled: registries.length > 0,
|
||||||
|
registries,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrateNamespaceKeys = (smartconfigJson: Record<string, any>): boolean => {
|
||||||
|
let migrated = false;
|
||||||
|
const migrations = [
|
||||||
|
{ oldKey: "gitzone", newKey: CLI_NAMESPACE },
|
||||||
|
{ oldKey: "tsdoc", newKey: "@git.zone/tsdoc" },
|
||||||
|
{ oldKey: "npmdocker", newKey: "@git.zone/tsdocker" },
|
||||||
|
{ oldKey: "npmci", newKey: "@ship.zone/szci" },
|
||||||
|
{ oldKey: "szci", newKey: "@ship.zone/szci" },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { oldKey, newKey } of migrations) {
|
||||||
|
if (!isPlainObject(smartconfigJson[oldKey])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isPlainObject(smartconfigJson[newKey])) {
|
||||||
|
smartconfigJson[newKey] = smartconfigJson[oldKey];
|
||||||
|
} else {
|
||||||
|
smartconfigJson[newKey] = {
|
||||||
|
...smartconfigJson[oldKey],
|
||||||
|
...smartconfigJson[newKey],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
delete smartconfigJson[oldKey];
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrated;
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrateToV2 = (smartconfigJson: Record<string, any>): boolean => {
|
||||||
|
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
|
||||||
|
let migrated = migrateLegacyReleaseArray(smartconfigJson);
|
||||||
|
const releaseConfig = ensureObject(cliConfig, "release");
|
||||||
|
|
||||||
|
const targets = ensureObject(releaseConfig, "targets");
|
||||||
|
const shipzoneConfig = smartconfigJson["@ship.zone/szci"];
|
||||||
|
|
||||||
|
if (isPlainObject(releaseConfig.git) && !isPlainObject(targets.git)) {
|
||||||
|
targets.git = releaseConfig.git;
|
||||||
|
delete releaseConfig.git;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(releaseConfig.npm) && !isPlainObject(targets.npm)) {
|
||||||
|
targets.npm = releaseConfig.npm;
|
||||||
|
delete releaseConfig.npm;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(releaseConfig.docker)) {
|
||||||
|
targets.docker = isPlainObject(targets.docker)
|
||||||
|
? { ...releaseConfig.docker, ...targets.docker }
|
||||||
|
: releaseConfig.docker;
|
||||||
|
delete releaseConfig.docker;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(releaseConfig.registries)) {
|
||||||
|
const npmTarget = ensureObject(targets, "npm");
|
||||||
|
if (!Array.isArray(npmTarget.registries)) {
|
||||||
|
npmTarget.registries = releaseConfig.registries;
|
||||||
|
}
|
||||||
|
delete releaseConfig.registries;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releaseConfig.accessLevel) {
|
||||||
|
const npmTarget = ensureObject(targets, "npm");
|
||||||
|
if (!npmTarget.accessLevel) {
|
||||||
|
npmTarget.accessLevel = releaseConfig.accessLevel;
|
||||||
|
}
|
||||||
|
delete releaseConfig.accessLevel;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(shipzoneConfig)) {
|
||||||
|
if (shipzoneConfig.npmAccessLevel) {
|
||||||
|
const npmTarget = ensureObject(targets, "npm");
|
||||||
|
if (!npmTarget.accessLevel) {
|
||||||
|
npmTarget.accessLevel = shipzoneConfig.npmAccessLevel;
|
||||||
|
}
|
||||||
|
delete shipzoneConfig.npmAccessLevel;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shipzoneConfig.npmRegistryUrl) {
|
||||||
|
const npmTarget = ensureObject(targets, "npm");
|
||||||
|
const registry = normalizeRegistryUrl(shipzoneConfig.npmRegistryUrl);
|
||||||
|
const registries = Array.isArray(npmTarget.registries) ? npmTarget.registries : [];
|
||||||
|
if (!registries.includes(registry)) {
|
||||||
|
registries.push(registry);
|
||||||
|
}
|
||||||
|
npmTarget.registries = registries;
|
||||||
|
delete shipzoneConfig.npmRegistryUrl;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(releaseConfig.steps)) {
|
||||||
|
const steps = releaseConfig.steps as string[];
|
||||||
|
const preflight = ensureObject(releaseConfig, "preflight");
|
||||||
|
if (steps.includes("test") && preflight.test === undefined) {
|
||||||
|
preflight.test = true;
|
||||||
|
}
|
||||||
|
if (steps.includes("build") && preflight.build === undefined) {
|
||||||
|
preflight.build = true;
|
||||||
|
}
|
||||||
|
if (steps.includes("push")) {
|
||||||
|
const gitTarget = ensureObject(targets, "git");
|
||||||
|
if (gitTarget.enabled === undefined) {
|
||||||
|
gitTarget.enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (steps.includes("publishNpm")) {
|
||||||
|
const npmTarget = ensureObject(targets, "npm");
|
||||||
|
if (npmTarget.enabled === undefined) {
|
||||||
|
npmTarget.enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (steps.includes("publishDocker")) {
|
||||||
|
const dockerTarget = ensureObject(targets, "docker");
|
||||||
|
if (dockerTarget.enabled === undefined) {
|
||||||
|
dockerTarget.enabled = true;
|
||||||
|
}
|
||||||
|
dockerTarget.engine = "tsdocker";
|
||||||
|
}
|
||||||
|
delete releaseConfig.steps;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(targets.docker)) {
|
||||||
|
if (targets.docker.images) {
|
||||||
|
delete targets.docker.images;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
if (targets.docker.engine !== "tsdocker") {
|
||||||
|
targets.docker.engine = "tsdocker";
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(targets.docker.patterns)) {
|
||||||
|
targets.docker.patterns = [];
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releaseConfig.changelog) {
|
||||||
|
delete releaseConfig.changelog;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
cliConfig.schemaVersion = 2;
|
||||||
|
return migrated || true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeRegistryUrl = (url: string): string => {
|
||||||
|
let normalizedUrl = url.trim();
|
||||||
|
if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
|
||||||
|
normalizedUrl = `https://${normalizedUrl}`;
|
||||||
|
}
|
||||||
|
return normalizedUrl.endsWith("/") ? normalizedUrl.slice(0, -1) : normalizedUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const migrateSmartconfigData = (
|
||||||
|
smartconfigJson: Record<string, any>,
|
||||||
|
targetVersion = CURRENT_GITZONE_CLI_SCHEMA_VERSION,
|
||||||
|
): ISmartconfigMigrationResult => {
|
||||||
|
let migrated = false;
|
||||||
|
migrated = migrateNamespaceKeys(smartconfigJson) || migrated;
|
||||||
|
|
||||||
|
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
|
||||||
|
const fromVersion = typeof cliConfig.schemaVersion === "number" ? cliConfig.schemaVersion : 1;
|
||||||
|
let currentVersion = fromVersion;
|
||||||
|
|
||||||
|
if (targetVersion >= 2) {
|
||||||
|
migrated = migrateLegacyReleaseArray(smartconfigJson) || migrated;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentVersion < 2 && targetVersion >= 2) {
|
||||||
|
migrated = migrateToV2(smartconfigJson) || migrated;
|
||||||
|
currentVersion = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetVersion === CURRENT_GITZONE_CLI_SCHEMA_VERSION && cliConfig.schemaVersion !== targetVersion) {
|
||||||
|
cliConfig.schemaVersion = targetVersion;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
migrated,
|
||||||
|
fromVersion,
|
||||||
|
toVersion: Math.min(targetVersion, CURRENT_GITZONE_CLI_SCHEMA_VERSION),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
import { getCliConfigValue } from "./helpers.smartconfig.js";
|
||||||
|
|
||||||
|
export type TConfirmationMode = "prompt" | "auto" | "plan";
|
||||||
|
|
||||||
|
export type TCommitStep =
|
||||||
|
| "format"
|
||||||
|
| "analyze"
|
||||||
|
| "test"
|
||||||
|
| "build"
|
||||||
|
| "changelog"
|
||||||
|
| "commit"
|
||||||
|
| "push";
|
||||||
|
|
||||||
|
export type TReleaseTarget = "git" | "npm" | "docker";
|
||||||
|
|
||||||
|
export interface ICommitWorkflowConfig {
|
||||||
|
confirmation?: TConfirmationMode;
|
||||||
|
staging?: "all";
|
||||||
|
steps?: TCommitStep[];
|
||||||
|
alwaysTest?: boolean;
|
||||||
|
alwaysBuild?: boolean;
|
||||||
|
analyze?: {
|
||||||
|
provider?: "ai";
|
||||||
|
requireConfirmationFor?: string[];
|
||||||
|
};
|
||||||
|
test?: {
|
||||||
|
command?: string;
|
||||||
|
};
|
||||||
|
build?: {
|
||||||
|
command?: string;
|
||||||
|
verifyCleanTree?: boolean;
|
||||||
|
};
|
||||||
|
push?: {
|
||||||
|
remote?: string;
|
||||||
|
followTags?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReleaseGitTargetConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
remote?: string;
|
||||||
|
pushBranch?: boolean;
|
||||||
|
pushTags?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReleaseNpmTargetConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
registries?: string[];
|
||||||
|
accessLevel?: "public" | "private";
|
||||||
|
alreadyPublished?: "success" | "error";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReleaseDockerTargetConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
engine?: "tsdocker";
|
||||||
|
patterns?: string[];
|
||||||
|
cached?: boolean;
|
||||||
|
parallel?: boolean | number;
|
||||||
|
context?: string;
|
||||||
|
noBuild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReleaseWorkflowConfig {
|
||||||
|
confirmation?: TConfirmationMode;
|
||||||
|
version?: {
|
||||||
|
strategy?: "semver";
|
||||||
|
source?: "pendingChangelog" | "manual";
|
||||||
|
};
|
||||||
|
preflight?: {
|
||||||
|
requireCleanTree?: boolean;
|
||||||
|
test?: boolean;
|
||||||
|
build?: boolean;
|
||||||
|
testCommand?: string;
|
||||||
|
buildCommand?: string;
|
||||||
|
};
|
||||||
|
targets?: {
|
||||||
|
git?: IReleaseGitTargetConfig;
|
||||||
|
npm?: IReleaseNpmTargetConfig;
|
||||||
|
docker?: IReleaseDockerTargetConfig;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResolvedCommitWorkflow {
|
||||||
|
confirmation: TConfirmationMode;
|
||||||
|
steps: TCommitStep[];
|
||||||
|
staging: "all";
|
||||||
|
testCommand: string;
|
||||||
|
buildCommand: string;
|
||||||
|
changelogFile: "changelog.md";
|
||||||
|
changelogSection: "Pending";
|
||||||
|
pushRemote: string;
|
||||||
|
pushFollowTags: boolean;
|
||||||
|
releaseFlagRequested: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResolvedReleaseWorkflow {
|
||||||
|
confirmation: TConfirmationMode;
|
||||||
|
plan: string[];
|
||||||
|
targets: TReleaseTarget[];
|
||||||
|
requireCleanTree: boolean;
|
||||||
|
runTests: boolean;
|
||||||
|
runBuild: boolean;
|
||||||
|
testCommand: string;
|
||||||
|
buildCommand: string;
|
||||||
|
changelogFile: "changelog.md";
|
||||||
|
changelogPendingSection: "Pending";
|
||||||
|
changelogVersionHeading: "## {{date}} - {{version}}";
|
||||||
|
gitEnabled: boolean;
|
||||||
|
gitRemote: string;
|
||||||
|
pushBranch: boolean;
|
||||||
|
pushTags: boolean;
|
||||||
|
npmEnabled: boolean;
|
||||||
|
npmRegistries: string[];
|
||||||
|
npmAccessLevel: "public" | "private";
|
||||||
|
npmAlreadyPublished: "success" | "error";
|
||||||
|
dockerEnabled: boolean;
|
||||||
|
dockerEngine: "tsdocker";
|
||||||
|
dockerPatterns: string[];
|
||||||
|
dockerCached: boolean;
|
||||||
|
dockerParallel: boolean | number;
|
||||||
|
dockerContext?: string;
|
||||||
|
dockerNoBuild: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICliWorkflowConfig {
|
||||||
|
commit?: ICommitWorkflowConfig;
|
||||||
|
release?: IReleaseWorkflowConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitFlagToStep: Record<string, TCommitStep | undefined> = {
|
||||||
|
f: "format",
|
||||||
|
t: "test",
|
||||||
|
b: "build",
|
||||||
|
p: "push",
|
||||||
|
};
|
||||||
|
|
||||||
|
const unique = <T>(items: T[]): T[] => {
|
||||||
|
const result: T[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
if (!result.includes(item)) {
|
||||||
|
result.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeConfirmation = (
|
||||||
|
value: unknown,
|
||||||
|
fallback: TConfirmationMode,
|
||||||
|
): TConfirmationMode => {
|
||||||
|
if (value === "prompt" || value === "auto" || value === "plan") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeRegistryUrl = (url: string): string => {
|
||||||
|
let normalizedUrl = url.trim();
|
||||||
|
if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
|
||||||
|
normalizedUrl = `https://${normalizedUrl}`;
|
||||||
|
}
|
||||||
|
return normalizedUrl.endsWith("/") ? normalizedUrl.slice(0, -1) : normalizedUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDisabled = (argvArg: any, ...keys: string[]): boolean => {
|
||||||
|
return keys.some((key) => argvArg[key] === false || argvArg[`no-${key}`] || argvArg[`no${key[0].toUpperCase()}${key.slice(1)}`]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const readCliWorkflowConfig = async (): Promise<ICliWorkflowConfig> => {
|
||||||
|
return await getCliConfigValue<ICliWorkflowConfig>("", {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrderedArgsAfterCommand = (commandName: string): string[] => {
|
||||||
|
const rawArgs = process.argv.slice(2);
|
||||||
|
const commandIndex = rawArgs.indexOf(commandName);
|
||||||
|
if (commandIndex === -1) {
|
||||||
|
return rawArgs;
|
||||||
|
}
|
||||||
|
return rawArgs.slice(commandIndex + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrderedShortFlags = (commandName: string): string[] => {
|
||||||
|
const orderedFlags: string[] = [];
|
||||||
|
for (const arg of getOrderedArgsAfterCommand(commandName)) {
|
||||||
|
if (arg === "--") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (arg.startsWith("--")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg.startsWith("-") && arg.length > 1) {
|
||||||
|
orderedFlags.push(...arg.slice(1).split(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return orderedFlags;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasExplicitCommitWorkflowFlags = (argvArg: any): boolean => {
|
||||||
|
return Boolean(
|
||||||
|
argvArg.f ||
|
||||||
|
argvArg.format ||
|
||||||
|
argvArg.t ||
|
||||||
|
argvArg.test ||
|
||||||
|
argvArg.b ||
|
||||||
|
argvArg.build ||
|
||||||
|
argvArg.p ||
|
||||||
|
argvArg.push,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeCommitSteps = (rawSteps: TCommitStep[]): TCommitStep[] => {
|
||||||
|
const steps = unique(rawSteps.filter(Boolean));
|
||||||
|
const pushRequested = steps.includes("push");
|
||||||
|
const prePushSteps = steps.filter((step) => step !== "push");
|
||||||
|
|
||||||
|
if (!prePushSteps.includes("analyze")) {
|
||||||
|
prePushSteps.unshift("analyze");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prePushSteps.includes("changelog")) {
|
||||||
|
const commitIndex = prePushSteps.indexOf("commit");
|
||||||
|
if (commitIndex === -1) {
|
||||||
|
prePushSteps.push("changelog");
|
||||||
|
} else {
|
||||||
|
prePushSteps.splice(commitIndex, 0, "changelog");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prePushSteps.includes("commit")) {
|
||||||
|
prePushSteps.push("commit");
|
||||||
|
}
|
||||||
|
|
||||||
|
const analyzeIndex = prePushSteps.indexOf("analyze");
|
||||||
|
const commitIndex = prePushSteps.indexOf("commit");
|
||||||
|
if (analyzeIndex > commitIndex) {
|
||||||
|
throw new Error("Commit workflow requires analyze before commit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const changelogIndex = prePushSteps.indexOf("changelog");
|
||||||
|
if (changelogIndex === -1 || changelogIndex > commitIndex) {
|
||||||
|
throw new Error("Commit workflow requires changelog before commit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pushRequested ? [...prePushSteps, "push"] : prePushSteps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTargetOverride = (argvArg: any): TReleaseTarget[] | undefined => {
|
||||||
|
const validTargets: TReleaseTarget[] = ["git", "npm", "docker"];
|
||||||
|
const rawTargets = argvArg.target || argvArg.targets;
|
||||||
|
if (typeof rawTargets === "string") {
|
||||||
|
return rawTargets
|
||||||
|
.split(",")
|
||||||
|
.map((target) => target.trim())
|
||||||
|
.filter((target): target is TReleaseTarget => validTargets.includes(target as TReleaseTarget));
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets: TReleaseTarget[] = [];
|
||||||
|
if (argvArg.git || argvArg.p || argvArg.push) targets.push("git");
|
||||||
|
if (argvArg.npm) targets.push("npm");
|
||||||
|
if (argvArg.docker) targets.push("docker");
|
||||||
|
return targets.length > 0 ? targets : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildReleasePlan = (options: {
|
||||||
|
requireCleanTree: boolean;
|
||||||
|
runTests: boolean;
|
||||||
|
runBuild: boolean;
|
||||||
|
targets: TReleaseTarget[];
|
||||||
|
}): string[] => {
|
||||||
|
const plan: string[] = [];
|
||||||
|
if (options.requireCleanTree) plan.push("preflight.cleanTree");
|
||||||
|
if (options.runTests) plan.push("preflight.test");
|
||||||
|
plan.push("core.version", "core.changelog", "core.commit", "core.tag");
|
||||||
|
if (options.runBuild) plan.push("core.build");
|
||||||
|
for (const target of options.targets) {
|
||||||
|
plan.push(`target.${target}`);
|
||||||
|
}
|
||||||
|
return plan;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveCommitWorkflow = async (argvArg: any): Promise<IResolvedCommitWorkflow> => {
|
||||||
|
const cliConfig = await readCliWorkflowConfig();
|
||||||
|
const commitConfig = cliConfig.commit || {};
|
||||||
|
const releaseFlagRequested = Boolean(argvArg.r || argvArg.release);
|
||||||
|
|
||||||
|
let confirmation = normalizeConfirmation(commitConfig.confirmation, "prompt");
|
||||||
|
if (argvArg.plan) {
|
||||||
|
confirmation = "plan";
|
||||||
|
} else if (argvArg.y || argvArg.yes) {
|
||||||
|
confirmation = "auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
let rawSteps: TCommitStep[];
|
||||||
|
if (hasExplicitCommitWorkflowFlags(argvArg)) {
|
||||||
|
const orderedFlags = getOrderedShortFlags("commit");
|
||||||
|
rawSteps = ["analyze"];
|
||||||
|
for (const shortFlag of orderedFlags) {
|
||||||
|
const step = commitFlagToStep[shortFlag];
|
||||||
|
if (step) {
|
||||||
|
rawSteps.push(step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (argvArg.format && !rawSteps.includes("format")) rawSteps.push("format");
|
||||||
|
if (argvArg.test && !rawSteps.includes("test")) rawSteps.push("test");
|
||||||
|
if (argvArg.build && !rawSteps.includes("build")) rawSteps.push("build");
|
||||||
|
if (argvArg.push && !rawSteps.includes("push")) rawSteps.push("push");
|
||||||
|
rawSteps.push("changelog");
|
||||||
|
rawSteps.push("commit");
|
||||||
|
} else if (Array.isArray(commitConfig.steps) && commitConfig.steps.length > 0) {
|
||||||
|
rawSteps = commitConfig.steps;
|
||||||
|
} else {
|
||||||
|
rawSteps = ["analyze"];
|
||||||
|
if (commitConfig.alwaysTest) rawSteps.push("test");
|
||||||
|
if (commitConfig.alwaysBuild) rawSteps.push("build");
|
||||||
|
rawSteps.push("changelog");
|
||||||
|
rawSteps.push("commit");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
confirmation,
|
||||||
|
steps: normalizeCommitSteps(rawSteps),
|
||||||
|
staging: commitConfig.staging || "all",
|
||||||
|
testCommand: commitConfig.test?.command || "pnpm test",
|
||||||
|
buildCommand: commitConfig.build?.command || "pnpm build",
|
||||||
|
changelogFile: "changelog.md",
|
||||||
|
changelogSection: "Pending",
|
||||||
|
pushRemote: commitConfig.push?.remote || "origin",
|
||||||
|
pushFollowTags: commitConfig.push?.followTags || false,
|
||||||
|
releaseFlagRequested,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedReleaseWorkflow> => {
|
||||||
|
const cliConfig = await readCliWorkflowConfig();
|
||||||
|
const releaseConfig = cliConfig.release || {};
|
||||||
|
const targetConfig = releaseConfig.targets || {};
|
||||||
|
const gitConfig = targetConfig.git || {};
|
||||||
|
const npmConfig = targetConfig.npm || {};
|
||||||
|
const dockerConfig = targetConfig.docker || {};
|
||||||
|
const npmRegistries = (npmConfig.registries || []).map(normalizeRegistryUrl);
|
||||||
|
const npmEnabled = npmConfig.enabled ?? npmRegistries.length > 0;
|
||||||
|
const gitEnabled = gitConfig.enabled ?? true;
|
||||||
|
const dockerEnabled = dockerConfig.enabled ?? false;
|
||||||
|
|
||||||
|
let confirmation = normalizeConfirmation(releaseConfig.confirmation, "prompt");
|
||||||
|
if (argvArg.plan) {
|
||||||
|
confirmation = "plan";
|
||||||
|
} else if (argvArg.y || argvArg.yes) {
|
||||||
|
confirmation = "auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
let requireCleanTree = releaseConfig.preflight?.requireCleanTree ?? true;
|
||||||
|
let runTests = releaseConfig.preflight?.test ?? false;
|
||||||
|
let runBuild = releaseConfig.preflight?.build ?? true;
|
||||||
|
if (argvArg.t || argvArg.test) runTests = true;
|
||||||
|
if (argvArg.b || argvArg.build) runBuild = true;
|
||||||
|
if (isDisabled(argvArg, "test")) runTests = false;
|
||||||
|
if (isDisabled(argvArg, "build")) runBuild = false;
|
||||||
|
if (isDisabled(argvArg, "preflight")) requireCleanTree = false;
|
||||||
|
|
||||||
|
const configuredTargets: TReleaseTarget[] = [];
|
||||||
|
if (gitEnabled) configuredTargets.push("git");
|
||||||
|
if (npmEnabled) configuredTargets.push("npm");
|
||||||
|
if (dockerEnabled) configuredTargets.push("docker");
|
||||||
|
let targets = getTargetOverride(argvArg) || configuredTargets;
|
||||||
|
if (isDisabled(argvArg, "git", "push")) {
|
||||||
|
targets = targets.filter((target) => target !== "git");
|
||||||
|
}
|
||||||
|
if (isDisabled(argvArg, "publish")) {
|
||||||
|
targets = targets.filter((target) => target === "git");
|
||||||
|
}
|
||||||
|
targets = unique(targets);
|
||||||
|
|
||||||
|
return {
|
||||||
|
confirmation,
|
||||||
|
plan: buildReleasePlan({ requireCleanTree, runTests, runBuild, targets }),
|
||||||
|
targets,
|
||||||
|
requireCleanTree,
|
||||||
|
runTests,
|
||||||
|
runBuild,
|
||||||
|
testCommand: releaseConfig.preflight?.testCommand || "pnpm test",
|
||||||
|
buildCommand: releaseConfig.preflight?.buildCommand || "pnpm build",
|
||||||
|
changelogFile: "changelog.md",
|
||||||
|
changelogPendingSection: "Pending",
|
||||||
|
changelogVersionHeading: "## {{date}} - {{version}}",
|
||||||
|
gitEnabled,
|
||||||
|
gitRemote: gitConfig.remote || "origin",
|
||||||
|
pushBranch: gitConfig.pushBranch ?? true,
|
||||||
|
pushTags: gitConfig.pushTags ?? true,
|
||||||
|
npmEnabled,
|
||||||
|
npmRegistries,
|
||||||
|
npmAccessLevel: npmConfig.accessLevel || "public",
|
||||||
|
npmAlreadyPublished: npmConfig.alreadyPublished || "success",
|
||||||
|
dockerEnabled,
|
||||||
|
dockerEngine: "tsdocker",
|
||||||
|
dockerPatterns: Array.isArray(dockerConfig.patterns) ? dockerConfig.patterns : [],
|
||||||
|
dockerCached: dockerConfig.cached ?? false,
|
||||||
|
dockerParallel: dockerConfig.parallel ?? false,
|
||||||
|
dockerContext: typeof dockerConfig.context === "string" && dockerConfig.context.trim()
|
||||||
|
? dockerConfig.context.trim()
|
||||||
|
: undefined,
|
||||||
|
dockerNoBuild: dockerConfig.noBuild ?? false,
|
||||||
|
};
|
||||||
|
};
|
||||||
+340
-315
@@ -1,346 +1,371 @@
|
|||||||
// this file contains code to create commits in a consistent way
|
// this file contains code to create commits in a consistent way
|
||||||
|
|
||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from "./mod.plugins.js";
|
||||||
import * as paths from '../paths.js';
|
import * as paths from "../paths.js";
|
||||||
import { logger } from '../gitzone.logging.js';
|
import { logger } from "../gitzone.logging.js";
|
||||||
import * as helpers from './mod.helpers.js';
|
import * as ui from "./mod.ui.js";
|
||||||
import * as ui from './mod.ui.js';
|
import type { ICliMode } from "../helpers.climode.js";
|
||||||
import { ReleaseConfig } from '../mod_config/classes.releaseconfig.js';
|
import { getCliMode, printJson, runWithSuppressedOutput } from "../helpers.climode.js";
|
||||||
|
import { appendPendingChangelogEntry } from "../helpers.changelog.js";
|
||||||
|
import { resolveCommitWorkflow, type IResolvedCommitWorkflow } from "../helpers.workflow.js";
|
||||||
|
|
||||||
export const run = async (argvArg: any) => {
|
export const run = async (argvArg: any) => {
|
||||||
// Read commit config from npmextra.json
|
const mode = await getCliMode(argvArg);
|
||||||
const npmextraConfig = new plugins.npmextra.Npmextra();
|
const subcommand = argvArg._?.[1];
|
||||||
const gitzoneConfig = npmextraConfig.dataFor<{
|
|
||||||
commit?: {
|
|
||||||
alwaysTest?: boolean;
|
|
||||||
alwaysBuild?: boolean;
|
|
||||||
};
|
|
||||||
}>('@git.zone/cli', {});
|
|
||||||
const commitConfig = gitzoneConfig.commit || {};
|
|
||||||
|
|
||||||
// Check flags and merge with config options
|
if (mode.help || subcommand === "help") {
|
||||||
const wantsRelease = !!(argvArg.r || argvArg.release);
|
showHelp(mode);
|
||||||
const wantsTest = !!(argvArg.t || argvArg.test || commitConfig.alwaysTest);
|
return;
|
||||||
const wantsBuild = !!(argvArg.b || argvArg.build || commitConfig.alwaysBuild);
|
|
||||||
let releaseConfig: ReleaseConfig | null = null;
|
|
||||||
|
|
||||||
if (wantsRelease) {
|
|
||||||
releaseConfig = await ReleaseConfig.fromCwd();
|
|
||||||
if (!releaseConfig.hasRegistries()) {
|
|
||||||
logger.log('error', 'No release registries configured.');
|
|
||||||
console.log('');
|
|
||||||
console.log(' Run `gitzone config add <registry-url>` to add registries.');
|
|
||||||
console.log('');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print execution plan at the start
|
if (subcommand === "recommend") {
|
||||||
ui.printExecutionPlan({
|
await handleRecommend(mode);
|
||||||
autoAccept: !!(argvArg.y || argvArg.yes),
|
return;
|
||||||
push: !!(argvArg.p || argvArg.push),
|
|
||||||
test: wantsTest,
|
|
||||||
build: wantsBuild,
|
|
||||||
release: wantsRelease,
|
|
||||||
format: !!argvArg.format,
|
|
||||||
registries: releaseConfig?.getRegistries(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (argvArg.format) {
|
|
||||||
const formatMod = await import('../mod_format/index.js');
|
|
||||||
await formatMod.run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run tests early to fail fast before analysis
|
if (mode.json) {
|
||||||
if (wantsTest) {
|
printJson({
|
||||||
ui.printHeader('🧪 Running tests...');
|
ok: false,
|
||||||
const smartshellForTest = new plugins.smartshell.Smartshell({
|
error:
|
||||||
executor: 'bash',
|
"JSON output is only supported for the read-only recommendation flow. Use `gitzone commit recommend --json`.",
|
||||||
sourceFilePaths: [],
|
|
||||||
});
|
});
|
||||||
const testResult = await smartshellForTest.exec('pnpm test');
|
return;
|
||||||
if (testResult.exitCode !== 0) {
|
|
||||||
logger.log('error', 'Tests failed. Aborting commit.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
logger.log('success', 'All tests passed.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.printHeader('🔍 Analyzing repository changes...');
|
const workflow = await resolveCommitWorkflow(argvArg);
|
||||||
|
if (workflow.releaseFlagRequested) {
|
||||||
const aidoc = new plugins.tsdoc.AiDoc();
|
logger.log(
|
||||||
await aidoc.start();
|
"warn",
|
||||||
|
"`gitzone commit -r` is deprecated and no longer releases. Use `gitzone release` after committing.",
|
||||||
const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd);
|
);
|
||||||
|
}
|
||||||
await aidoc.stop();
|
|
||||||
|
printCommitExecutionPlan(workflow);
|
||||||
ui.printRecommendation({
|
if (workflow.confirmation === "plan") {
|
||||||
recommendedNextVersion: nextCommitObject.recommendedNextVersion,
|
return;
|
||||||
recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel,
|
|
||||||
recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope,
|
|
||||||
recommendedNextVersionMessage: nextCommitObject.recommendedNextVersionMessage,
|
|
||||||
});
|
|
||||||
|
|
||||||
let answerBucket: plugins.smartinteract.AnswerBucket;
|
|
||||||
|
|
||||||
// Check if -y/--yes flag is set AND version is not a breaking change
|
|
||||||
// Breaking changes (major version bumps) always require manual confirmation
|
|
||||||
const isBreakingChange = nextCommitObject.recommendedNextVersionLevel === 'BREAKING CHANGE';
|
|
||||||
const canAutoAccept = (argvArg.y || argvArg.yes) && !isBreakingChange;
|
|
||||||
|
|
||||||
if (canAutoAccept) {
|
|
||||||
// Auto-mode: create AnswerBucket programmatically
|
|
||||||
logger.log('info', '✓ Auto-accepting AI recommendations (--yes flag)');
|
|
||||||
|
|
||||||
answerBucket = new plugins.smartinteract.AnswerBucket();
|
|
||||||
answerBucket.addAnswer({
|
|
||||||
name: 'commitType',
|
|
||||||
value: nextCommitObject.recommendedNextVersionLevel,
|
|
||||||
});
|
|
||||||
answerBucket.addAnswer({
|
|
||||||
name: 'commitScope',
|
|
||||||
value: nextCommitObject.recommendedNextVersionScope,
|
|
||||||
});
|
|
||||||
answerBucket.addAnswer({
|
|
||||||
name: 'commitDescription',
|
|
||||||
value: nextCommitObject.recommendedNextVersionMessage,
|
|
||||||
});
|
|
||||||
answerBucket.addAnswer({
|
|
||||||
name: 'pushToOrigin',
|
|
||||||
value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided
|
|
||||||
});
|
|
||||||
answerBucket.addAnswer({
|
|
||||||
name: 'createRelease',
|
|
||||||
value: wantsRelease,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Warn if --yes was provided but we're requiring confirmation due to breaking change
|
|
||||||
if (isBreakingChange && (argvArg.y || argvArg.yes)) {
|
|
||||||
logger.log('warn', '⚠️ BREAKING CHANGE detected - manual confirmation required');
|
|
||||||
}
|
|
||||||
// Interactive mode: prompt user for input
|
|
||||||
const commitInteract = new plugins.smartinteract.SmartInteract();
|
|
||||||
commitInteract.addQuestions([
|
|
||||||
{
|
|
||||||
type: 'list',
|
|
||||||
name: `commitType`,
|
|
||||||
message: `Choose TYPE of the commit:`,
|
|
||||||
choices: [`fix`, `feat`, `BREAKING CHANGE`],
|
|
||||||
default: nextCommitObject.recommendedNextVersionLevel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
name: `commitScope`,
|
|
||||||
message: `What is the SCOPE of the commit:`,
|
|
||||||
default: nextCommitObject.recommendedNextVersionScope,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: `input`,
|
|
||||||
name: `commitDescription`,
|
|
||||||
message: `What is the DESCRIPTION of the commit?`,
|
|
||||||
default: nextCommitObject.recommendedNextVersionMessage,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: `pushToOrigin`,
|
|
||||||
message: `Do you want to push this version now?`,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: `createRelease`,
|
|
||||||
message: `Do you want to publish to npm registries?`,
|
|
||||||
default: wantsRelease,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
answerBucket = await commitInteract.runQueue();
|
|
||||||
}
|
}
|
||||||
const commitString = createCommitStringFromAnswerBucket(answerBucket);
|
|
||||||
const commitVersionType = (() => {
|
|
||||||
switch (answerBucket.getAnswerFor('commitType')) {
|
|
||||||
case 'fix':
|
|
||||||
return 'patch';
|
|
||||||
case 'feat':
|
|
||||||
return 'minor';
|
|
||||||
case 'BREAKING CHANGE':
|
|
||||||
return 'major';
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
ui.printHeader('✨ Creating Semantic Commit');
|
|
||||||
ui.printCommitMessage(commitString);
|
|
||||||
const smartshellInstance = new plugins.smartshell.Smartshell({
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
executor: 'bash',
|
executor: "bash",
|
||||||
sourceFilePaths: [],
|
sourceFilePaths: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load release config if user wants to release (interactively selected)
|
let nextCommitObject: any;
|
||||||
if (answerBucket.getAnswerFor('createRelease') && !releaseConfig) {
|
let answerBucket: plugins.smartinteract.AnswerBucket | undefined;
|
||||||
releaseConfig = await ReleaseConfig.fromCwd();
|
|
||||||
if (!releaseConfig.hasRegistries()) {
|
for (const step of workflow.steps) {
|
||||||
logger.log('error', 'No release registries configured.');
|
switch (step) {
|
||||||
console.log('');
|
case "format":
|
||||||
console.log(' Run `gitzone config add <registry-url>` to add registries.');
|
await runFormatStep();
|
||||||
console.log('');
|
break;
|
||||||
process.exit(1);
|
case "test":
|
||||||
|
await runCommandStep(smartshellInstance, "Running tests", workflow.testCommand);
|
||||||
|
break;
|
||||||
|
case "build":
|
||||||
|
await runCommandStep(smartshellInstance, "Running build", workflow.buildCommand);
|
||||||
|
break;
|
||||||
|
case "analyze":
|
||||||
|
nextCommitObject = await runAnalyzeStep();
|
||||||
|
answerBucket = await buildAnswerBucket(nextCommitObject, workflow, mode, argvArg);
|
||||||
|
break;
|
||||||
|
case "changelog":
|
||||||
|
assertAnalysisComplete(answerBucket, nextCommitObject);
|
||||||
|
await runChangelogStep(workflow, answerBucket!, nextCommitObject);
|
||||||
|
break;
|
||||||
|
case "commit":
|
||||||
|
assertAnalysisComplete(answerBucket, nextCommitObject);
|
||||||
|
await runCommitStep(smartshellInstance, answerBucket!);
|
||||||
|
break;
|
||||||
|
case "push":
|
||||||
|
await runPushStep(smartshellInstance, workflow);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine total steps based on options
|
const commitShaResult = await smartshellInstance.exec("git rev-parse --short HEAD");
|
||||||
// Note: test runs early (like format) so not counted in numbered steps
|
const currentBranch = await detectCurrentBranch(smartshellInstance);
|
||||||
const willPush = answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true');
|
|
||||||
const willRelease = answerBucket.getAnswerFor('createRelease') && releaseConfig?.hasRegistries();
|
|
||||||
let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version
|
|
||||||
if (wantsBuild) totalSteps += 2; // build step + verification step
|
|
||||||
if (willPush) totalSteps++;
|
|
||||||
if (willRelease) totalSteps++;
|
|
||||||
let currentStep = 0;
|
|
||||||
|
|
||||||
// Step 1: Baking commitinfo
|
|
||||||
currentStep++;
|
|
||||||
ui.printStep(currentStep, totalSteps, '🔧 Baking commit info into code', 'in-progress');
|
|
||||||
const commitInfo = new plugins.commitinfo.CommitInfo(
|
|
||||||
paths.cwd,
|
|
||||||
commitVersionType,
|
|
||||||
);
|
|
||||||
await commitInfo.writeIntoPotentialDirs();
|
|
||||||
ui.printStep(currentStep, totalSteps, '🔧 Baking commit info into code', 'done');
|
|
||||||
|
|
||||||
// Step 2: Writing changelog
|
|
||||||
currentStep++;
|
|
||||||
ui.printStep(currentStep, totalSteps, '📄 Generating changelog.md', 'in-progress');
|
|
||||||
let changelog = nextCommitObject.changelog;
|
|
||||||
changelog = changelog.replaceAll(
|
|
||||||
'{{nextVersion}}',
|
|
||||||
(await commitInfo.getNextPlannedVersion()).versionString,
|
|
||||||
);
|
|
||||||
changelog = changelog.replaceAll(
|
|
||||||
'{{nextVersionScope}}',
|
|
||||||
`${await answerBucket.getAnswerFor('commitType')}(${await answerBucket.getAnswerFor('commitScope')})`,
|
|
||||||
);
|
|
||||||
changelog = changelog.replaceAll(
|
|
||||||
'{{nextVersionMessage}}',
|
|
||||||
nextCommitObject.recommendedNextVersionMessage,
|
|
||||||
);
|
|
||||||
if (nextCommitObject.recommendedNextVersionDetails?.length > 0) {
|
|
||||||
changelog = changelog.replaceAll(
|
|
||||||
'{{nextVersionDetails}}',
|
|
||||||
'- ' + nextCommitObject.recommendedNextVersionDetails.join('\n- '),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
changelog = changelog.replaceAll('\n{{nextVersionDetails}}', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
await plugins.smartfs
|
|
||||||
.file(plugins.path.join(paths.cwd, `changelog.md`))
|
|
||||||
.encoding('utf8')
|
|
||||||
.write(changelog);
|
|
||||||
ui.printStep(currentStep, totalSteps, '📄 Generating changelog.md', 'done');
|
|
||||||
|
|
||||||
// Step 3: Staging files
|
|
||||||
currentStep++;
|
|
||||||
ui.printStep(currentStep, totalSteps, '📦 Staging files', 'in-progress');
|
|
||||||
await smartshellInstance.exec(`git add -A`);
|
|
||||||
ui.printStep(currentStep, totalSteps, '📦 Staging files', 'done');
|
|
||||||
|
|
||||||
// Step 4: Creating commit
|
|
||||||
currentStep++;
|
|
||||||
ui.printStep(currentStep, totalSteps, '💾 Creating git commit', 'in-progress');
|
|
||||||
await smartshellInstance.exec(`git commit -m "${commitString}"`);
|
|
||||||
ui.printStep(currentStep, totalSteps, '💾 Creating git commit', 'done');
|
|
||||||
|
|
||||||
// Step 5: Bumping version
|
|
||||||
currentStep++;
|
|
||||||
const projectType = await helpers.detectProjectType();
|
|
||||||
const newVersion = await helpers.bumpProjectVersion(projectType, commitVersionType, currentStep, totalSteps);
|
|
||||||
|
|
||||||
// Step 6: Run build (optional)
|
|
||||||
if (wantsBuild) {
|
|
||||||
currentStep++;
|
|
||||||
ui.printStep(currentStep, totalSteps, '🔨 Running build', 'in-progress');
|
|
||||||
const buildResult = await smartshellInstance.exec('pnpm build');
|
|
||||||
if (buildResult.exitCode !== 0) {
|
|
||||||
ui.printStep(currentStep, totalSteps, '🔨 Running build', 'error');
|
|
||||||
logger.log('error', 'Build failed. Aborting release.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
ui.printStep(currentStep, totalSteps, '🔨 Running build', 'done');
|
|
||||||
|
|
||||||
// Step 7: Verify no uncommitted changes
|
|
||||||
currentStep++;
|
|
||||||
ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'in-progress');
|
|
||||||
const statusResult = await smartshellInstance.exec('git status --porcelain');
|
|
||||||
if (statusResult.stdout.trim() !== '') {
|
|
||||||
ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'error');
|
|
||||||
logger.log('error', 'Build produced uncommitted changes. This usually means build output is not gitignored.');
|
|
||||||
logger.log('error', 'Uncommitted files:');
|
|
||||||
console.log(statusResult.stdout);
|
|
||||||
logger.log('error', 'Aborting release. Please ensure build artifacts are in .gitignore');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'done');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step: Push to remote (optional)
|
|
||||||
const currentBranch = await helpers.detectCurrentBranch();
|
|
||||||
if (willPush) {
|
|
||||||
currentStep++;
|
|
||||||
ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'in-progress');
|
|
||||||
await smartshellInstance.exec(`git push origin ${currentBranch} --follow-tags`);
|
|
||||||
ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'done');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 7: Publish to npm registries (optional)
|
|
||||||
let releasedRegistries: string[] = [];
|
|
||||||
if (willRelease && releaseConfig) {
|
|
||||||
currentStep++;
|
|
||||||
const registries = releaseConfig.getRegistries();
|
|
||||||
ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'in-progress');
|
|
||||||
|
|
||||||
const accessLevel = releaseConfig.getAccessLevel();
|
|
||||||
for (const registry of registries) {
|
|
||||||
try {
|
|
||||||
await smartshellInstance.exec(`npm publish --registry=${registry} --access=${accessLevel}`);
|
|
||||||
releasedRegistries.push(registry);
|
|
||||||
} catch (error) {
|
|
||||||
logger.log('error', `Failed to publish to ${registry}: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (releasedRegistries.length === registries.length) {
|
|
||||||
ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'done');
|
|
||||||
} else {
|
|
||||||
ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(''); // Add spacing before summary
|
|
||||||
|
|
||||||
// Get commit SHA for summary
|
|
||||||
const commitShaResult = await smartshellInstance.exec('git rev-parse --short HEAD');
|
|
||||||
const commitSha = commitShaResult.stdout.trim();
|
|
||||||
|
|
||||||
// Print final summary
|
|
||||||
ui.printSummary({
|
ui.printSummary({
|
||||||
projectType,
|
projectType: "source",
|
||||||
branch: currentBranch,
|
branch: currentBranch,
|
||||||
commitType: answerBucket.getAnswerFor('commitType'),
|
commitType: answerBucket!.getAnswerFor("commitType"),
|
||||||
commitScope: answerBucket.getAnswerFor('commitScope'),
|
commitScope: answerBucket!.getAnswerFor("commitScope"),
|
||||||
commitMessage: answerBucket.getAnswerFor('commitDescription'),
|
commitMessage: answerBucket!.getAnswerFor("commitDescription"),
|
||||||
newVersion: newVersion,
|
commitSha: commitShaResult.stdout.trim(),
|
||||||
commitSha: commitSha,
|
pushed: workflow.steps.includes("push"),
|
||||||
pushed: willPush,
|
|
||||||
released: releasedRegistries.length > 0,
|
|
||||||
releasedRegistries: releasedRegistries.length > 0 ? releasedRegistries : undefined,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function runFormatStep(): Promise<void> {
|
||||||
|
ui.printHeader("Formatting project files");
|
||||||
|
const formatMod = await import("../mod_format/index.js");
|
||||||
|
await formatMod.run({ write: true, yes: true, interactive: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommandStep(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
label: string,
|
||||||
|
command: string,
|
||||||
|
): Promise<void> {
|
||||||
|
ui.printHeader(label);
|
||||||
|
const result = await smartshellInstance.exec(command);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", `${label} failed. Aborting commit.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
logger.log("success", `${label} passed.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAnalyzeStep(): Promise<any> {
|
||||||
|
ui.printHeader("Analyzing repository changes");
|
||||||
|
const aidoc = new plugins.tsdoc.AiDoc();
|
||||||
|
await aidoc.start();
|
||||||
|
try {
|
||||||
|
const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd);
|
||||||
|
ui.printRecommendation({
|
||||||
|
recommendedNextVersion: nextCommitObject.recommendedNextVersion,
|
||||||
|
recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel,
|
||||||
|
recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope,
|
||||||
|
recommendedNextVersionMessage: nextCommitObject.recommendedNextVersionMessage,
|
||||||
|
});
|
||||||
|
return nextCommitObject;
|
||||||
|
} finally {
|
||||||
|
await aidoc.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildAnswerBucket(
|
||||||
|
nextCommitObject: any,
|
||||||
|
workflow: IResolvedCommitWorkflow,
|
||||||
|
mode: ICliMode,
|
||||||
|
argvArg: any,
|
||||||
|
): Promise<plugins.smartinteract.AnswerBucket> {
|
||||||
|
const isBreakingChange = nextCommitObject.recommendedNextVersionLevel === "BREAKING CHANGE";
|
||||||
|
const canAutoAccept = workflow.confirmation === "auto" && !isBreakingChange;
|
||||||
|
|
||||||
|
if (canAutoAccept) {
|
||||||
|
logger.log("info", "Auto-accepting AI recommendations");
|
||||||
|
return createAnswerBucket({
|
||||||
|
commitType: nextCommitObject.recommendedNextVersionLevel,
|
||||||
|
commitScope: nextCommitObject.recommendedNextVersionScope,
|
||||||
|
commitDescription: nextCommitObject.recommendedNextVersionMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBreakingChange && (workflow.confirmation === "auto" || argvArg.y || argvArg.yes)) {
|
||||||
|
logger.log("warn", "BREAKING CHANGE detected - manual confirmation required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mode.interactive) {
|
||||||
|
throw new Error("Commit confirmation requires an interactive terminal. Use `-y` or set commit.confirmation to `auto`.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitInteract = new plugins.smartinteract.SmartInteract();
|
||||||
|
commitInteract.addQuestions([
|
||||||
|
{
|
||||||
|
type: "list",
|
||||||
|
name: "commitType",
|
||||||
|
message: "Choose TYPE of the commit:",
|
||||||
|
choices: ["fix", "feat", "BREAKING CHANGE"],
|
||||||
|
default: nextCommitObject.recommendedNextVersionLevel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "commitScope",
|
||||||
|
message: "What is the SCOPE of the commit:",
|
||||||
|
default: nextCommitObject.recommendedNextVersionScope,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "commitDescription",
|
||||||
|
message: "What is the DESCRIPTION of the commit?",
|
||||||
|
default: nextCommitObject.recommendedNextVersionMessage,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
return await commitInteract.runQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAnswerBucket(answers: {
|
||||||
|
commitType: string;
|
||||||
|
commitScope: string;
|
||||||
|
commitDescription: string;
|
||||||
|
}): plugins.smartinteract.AnswerBucket {
|
||||||
|
const answerBucket = new plugins.smartinteract.AnswerBucket();
|
||||||
|
for (const [name, value] of Object.entries(answers)) {
|
||||||
|
answerBucket.addAnswer({ name, value });
|
||||||
|
}
|
||||||
|
return answerBucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runChangelogStep(
|
||||||
|
workflow: IResolvedCommitWorkflow,
|
||||||
|
answerBucket: plugins.smartinteract.AnswerBucket,
|
||||||
|
nextCommitObject: any,
|
||||||
|
): Promise<void> {
|
||||||
|
await appendPendingChangelogEntry(
|
||||||
|
plugins.path.join(paths.cwd, workflow.changelogFile),
|
||||||
|
workflow.changelogSection,
|
||||||
|
{
|
||||||
|
type: answerBucket.getAnswerFor("commitType"),
|
||||||
|
scope: answerBucket.getAnswerFor("commitScope"),
|
||||||
|
message: answerBucket.getAnswerFor("commitDescription"),
|
||||||
|
details: nextCommitObject.recommendedNextVersionDetails || [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
logger.log("success", `Updated ${workflow.changelogFile} pending section.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommitStep(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
answerBucket: plugins.smartinteract.AnswerBucket,
|
||||||
|
): Promise<void> {
|
||||||
|
ui.printHeader("Creating Semantic Commit");
|
||||||
|
const commitString = createCommitStringFromAnswerBucket(answerBucket);
|
||||||
|
ui.printCommitMessage(commitString);
|
||||||
|
await smartshellInstance.exec("git add -A");
|
||||||
|
const result = await smartshellInstance.exec(`git commit -m ${shellQuote(commitString)}`);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", "git commit failed.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPushStep(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
workflow: IResolvedCommitWorkflow,
|
||||||
|
): Promise<void> {
|
||||||
|
const currentBranch = await detectCurrentBranch(smartshellInstance);
|
||||||
|
const followTags = workflow.pushFollowTags ? " --follow-tags" : "";
|
||||||
|
const result = await smartshellInstance.exec(
|
||||||
|
`git push ${workflow.pushRemote} ${currentBranch}${followTags}`,
|
||||||
|
);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", "git push failed.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detectCurrentBranch(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
): Promise<string> {
|
||||||
|
const branchResult = await smartshellInstance.exec("git branch --show-current");
|
||||||
|
return branchResult.stdout.trim() || "master";
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertAnalysisComplete(
|
||||||
|
answerBucket: plugins.smartinteract.AnswerBucket | undefined,
|
||||||
|
nextCommitObject: any,
|
||||||
|
): void {
|
||||||
|
if (!answerBucket || !nextCommitObject) {
|
||||||
|
throw new Error("Commit workflow requires analyze before changelog and commit steps.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellQuote(value: string): string {
|
||||||
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printCommitExecutionPlan(workflow: IResolvedCommitWorkflow): void {
|
||||||
|
console.log("");
|
||||||
|
console.log("gitzone commit - resolved workflow");
|
||||||
|
console.log(`confirmation: ${workflow.confirmation}`);
|
||||||
|
console.log(`steps: ${workflow.steps.join(" -> ")}`);
|
||||||
|
console.log(`changelog: ${workflow.changelogFile}#${workflow.changelogSection}`);
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRecommend(mode: ICliMode): Promise<void> {
|
||||||
|
const recommendationBuilder = async () => {
|
||||||
|
const aidoc = new plugins.tsdoc.AiDoc();
|
||||||
|
await aidoc.start();
|
||||||
|
try {
|
||||||
|
return await aidoc.buildNextCommitObject(paths.cwd);
|
||||||
|
} finally {
|
||||||
|
await aidoc.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const recommendation = mode.json
|
||||||
|
? await runWithSuppressedOutput(recommendationBuilder)
|
||||||
|
: await recommendationBuilder();
|
||||||
|
|
||||||
|
if (mode.json) {
|
||||||
|
printJson(recommendation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.printRecommendation({
|
||||||
|
recommendedNextVersion: recommendation.recommendedNextVersion,
|
||||||
|
recommendedNextVersionLevel: recommendation.recommendedNextVersionLevel,
|
||||||
|
recommendedNextVersionScope: recommendation.recommendedNextVersionScope,
|
||||||
|
recommendedNextVersionMessage: recommendation.recommendedNextVersionMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Suggested commit: ${recommendation.recommendedNextVersionLevel}(${recommendation.recommendedNextVersionScope}): ${recommendation.recommendedNextVersionMessage}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const createCommitStringFromAnswerBucket = (
|
const createCommitStringFromAnswerBucket = (
|
||||||
answerBucket: plugins.smartinteract.AnswerBucket,
|
answerBucket: plugins.smartinteract.AnswerBucket,
|
||||||
) => {
|
) => {
|
||||||
const commitType = answerBucket.getAnswerFor('commitType');
|
const commitType = answerBucket.getAnswerFor("commitType");
|
||||||
const commitScope = answerBucket.getAnswerFor('commitScope');
|
const commitScope = answerBucket.getAnswerFor("commitScope");
|
||||||
const commitDescription = answerBucket.getAnswerFor('commitDescription');
|
const commitDescription = answerBucket.getAnswerFor("commitDescription");
|
||||||
return `${commitType}(${commitScope}): ${commitDescription}`;
|
return `${commitType}(${commitScope}): ${commitDescription}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function showHelp(mode?: ICliMode): void {
|
||||||
|
if (mode?.json) {
|
||||||
|
printJson({
|
||||||
|
command: "commit",
|
||||||
|
usage: "gitzone commit [recommend] [options]",
|
||||||
|
description: "Analyzes changes and creates one semantic source commit.",
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
name: "recommend",
|
||||||
|
description: "Generate a commit recommendation without mutating the repository",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
flags: [
|
||||||
|
{ flag: "-y, --yes", description: "Auto-accept safe AI recommendations" },
|
||||||
|
{ flag: "-p, --push", description: "Push to origin after committing" },
|
||||||
|
{ flag: "-t, --test", description: "Run tests as part of the commit workflow" },
|
||||||
|
{ flag: "-b, --build", description: "Run build as part of the commit workflow" },
|
||||||
|
{ flag: "-f, --format", description: "Run gitzone format before committing" },
|
||||||
|
{ flag: "--plan", description: "Show resolved workflow without mutating files" },
|
||||||
|
{ flag: "--json", description: "Emit JSON for `commit recommend` only" },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
"gitzone commit recommend --json",
|
||||||
|
"gitzone commit -y",
|
||||||
|
"gitzone commit -ytbp",
|
||||||
|
"gitzone release",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log("Usage: gitzone commit [recommend] [options]");
|
||||||
|
console.log("");
|
||||||
|
console.log("Creates one semantic source commit. It does not version, tag, or publish.");
|
||||||
|
console.log("");
|
||||||
|
console.log("Commands:");
|
||||||
|
console.log(" recommend Generate a commit recommendation without mutating the repository");
|
||||||
|
console.log("");
|
||||||
|
console.log("Flags:");
|
||||||
|
console.log(" -y, --yes Auto-accept safe AI recommendations");
|
||||||
|
console.log(" -p, --push Push after commit");
|
||||||
|
console.log(" -t, --test Run tests in the configured order");
|
||||||
|
console.log(" -b, --build Run build in the configured order");
|
||||||
|
console.log(" -f, --format Run gitzone format before committing");
|
||||||
|
console.log(" --plan Show resolved workflow without mutating files");
|
||||||
|
console.log(" --json Emit JSON for `commit recommend` only");
|
||||||
|
console.log("");
|
||||||
|
console.log("Examples:");
|
||||||
|
console.log(" gitzone commit recommend --json");
|
||||||
|
console.log(" gitzone commit -y");
|
||||||
|
console.log(" gitzone commit -ytbp");
|
||||||
|
console.log(" gitzone release");
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export async function detectProjectType(): Promise<ProjectType> {
|
|||||||
* @param versionType Type of version bump
|
* @param versionType Type of version bump
|
||||||
* @returns New version string
|
* @returns New version string
|
||||||
*/
|
*/
|
||||||
function calculateNewVersion(currentVersion: string, versionType: VersionType): string {
|
export function calculateNewVersion(currentVersion: string, versionType: VersionType): string {
|
||||||
const versionMatch = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/);
|
const versionMatch = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/);
|
||||||
|
|
||||||
if (!versionMatch) {
|
if (!versionMatch) {
|
||||||
@@ -95,7 +95,7 @@ function calculateNewVersion(currentVersion: string, versionType: VersionType):
|
|||||||
* @param projectType The project type to determine which file to read
|
* @param projectType The project type to determine which file to read
|
||||||
* @returns The current version string
|
* @returns The current version string
|
||||||
*/
|
*/
|
||||||
async function readCurrentVersion(projectType: ProjectType): Promise<string> {
|
export async function readCurrentVersion(projectType: ProjectType): Promise<string> {
|
||||||
if (projectType === 'npm' || projectType === 'both') {
|
if (projectType === 'npm' || projectType === 'both') {
|
||||||
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
||||||
const content = (await plugins.smartfs
|
const content = (await plugins.smartfs
|
||||||
@@ -128,7 +128,7 @@ async function readCurrentVersion(projectType: ProjectType): Promise<string> {
|
|||||||
* @param filePath Path to the JSON file
|
* @param filePath Path to the JSON file
|
||||||
* @param newVersion The new version to write
|
* @param newVersion The new version to write
|
||||||
*/
|
*/
|
||||||
async function updateVersionFile(filePath: string, newVersion: string): Promise<void> {
|
export async function updateVersionFile(filePath: string, newVersion: string): Promise<void> {
|
||||||
const content = (await plugins.smartfs
|
const content = (await plugins.smartfs
|
||||||
.file(filePath)
|
.file(filePath)
|
||||||
.encoding('utf8')
|
.encoding('utf8')
|
||||||
@@ -141,6 +141,30 @@ async function updateVersionFile(filePath: string, newVersion: string): Promise<
|
|||||||
.write(JSON.stringify(config, null, 2) + '\n');
|
.write(JSON.stringify(config, null, 2) + '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates project version files without creating commits or tags.
|
||||||
|
*/
|
||||||
|
export async function updateProjectVersionFiles(
|
||||||
|
projectType: ProjectType,
|
||||||
|
newVersion: string,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const filesToUpdate: string[] = [];
|
||||||
|
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
||||||
|
const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
|
||||||
|
|
||||||
|
if (projectType === 'npm' || projectType === 'both') {
|
||||||
|
await updateVersionFile(packageJsonPath, newVersion);
|
||||||
|
filesToUpdate.push('package.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectType === 'deno' || projectType === 'both') {
|
||||||
|
await updateVersionFile(denoJsonPath, newVersion);
|
||||||
|
filesToUpdate.push('deno.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
return filesToUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bumps the project version based on project type
|
* Bumps the project version based on project type
|
||||||
* Handles npm-only, deno-only, and dual projects with unified logic
|
* Handles npm-only, deno-only, and dual projects with unified logic
|
||||||
@@ -182,19 +206,7 @@ export async function bumpProjectVersion(
|
|||||||
logger.log('info', `Bumping version: ${currentVersion} → ${newVersion}`);
|
logger.log('info', `Bumping version: ${currentVersion} → ${newVersion}`);
|
||||||
|
|
||||||
// 3. Determine which files to update
|
// 3. Determine which files to update
|
||||||
const filesToUpdate: string[] = [];
|
const filesToUpdate = await updateProjectVersionFiles(projectType, newVersion);
|
||||||
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
|
||||||
const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
|
|
||||||
|
|
||||||
if (projectType === 'npm' || projectType === 'both') {
|
|
||||||
await updateVersionFile(packageJsonPath, newVersion);
|
|
||||||
filesToUpdate.push('package.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (projectType === 'deno' || projectType === 'both') {
|
|
||||||
await updateVersionFile(denoJsonPath, newVersion);
|
|
||||||
filesToUpdate.push('deno.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Stage all updated files
|
// 4. Stage all updated files
|
||||||
await smartshellInstance.exec(`git add ${filesToUpdate.join(' ')}`);
|
await smartshellInstance.exec(`git add ${filesToUpdate.join(' ')}`);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface ICommitSummary {
|
|||||||
commitType: string;
|
commitType: string;
|
||||||
commitScope: string;
|
commitScope: string;
|
||||||
commitMessage: string;
|
commitMessage: string;
|
||||||
newVersion: string;
|
newVersion?: string;
|
||||||
commitSha?: string;
|
commitSha?: string;
|
||||||
pushed: boolean;
|
pushed: boolean;
|
||||||
repoUrl?: string;
|
repoUrl?: string;
|
||||||
@@ -197,9 +197,14 @@ export function printSummary(summary: ICommitSummary): void {
|
|||||||
`Branch: 🌿 ${summary.branch}`,
|
`Branch: 🌿 ${summary.branch}`,
|
||||||
`Commit Type: ${getCommitTypeEmoji(summary.commitType)}`,
|
`Commit Type: ${getCommitTypeEmoji(summary.commitType)}`,
|
||||||
`Scope: 📍 ${summary.commitScope}`,
|
`Scope: 📍 ${summary.commitScope}`,
|
||||||
`New Version: 🏷️ v${summary.newVersion}`,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (summary.newVersion) {
|
||||||
|
lines.push(`New Version: 🏷️ v${summary.newVersion}`);
|
||||||
|
} else {
|
||||||
|
lines.push(`Version: ⊘ Not bumped`);
|
||||||
|
}
|
||||||
|
|
||||||
if (summary.commitSha) {
|
if (summary.commitSha) {
|
||||||
lines.push(`Commit SHA: 📌 ${summary.commitSha}`);
|
lines.push(`Commit SHA: 📌 ${summary.commitSha}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import * as plugins from './mod.plugins.js';
|
|||||||
export interface ICommitConfig {
|
export interface ICommitConfig {
|
||||||
alwaysTest: boolean;
|
alwaysTest: boolean;
|
||||||
alwaysBuild: boolean;
|
alwaysBuild: boolean;
|
||||||
|
confirmation: 'prompt' | 'auto' | 'plan';
|
||||||
|
steps: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages commit configuration stored in npmextra.json
|
* Manages commit configuration stored in .smartconfig.json
|
||||||
* under @git.zone/cli.commit namespace
|
* under @git.zone/cli.commit namespace
|
||||||
*/
|
*/
|
||||||
export class CommitConfig {
|
export class CommitConfig {
|
||||||
@@ -15,7 +17,7 @@ export class CommitConfig {
|
|||||||
|
|
||||||
constructor(cwd: string = process.cwd()) {
|
constructor(cwd: string = process.cwd()) {
|
||||||
this.cwd = cwd;
|
this.cwd = cwd;
|
||||||
this.config = { alwaysTest: false, alwaysBuild: false };
|
this.config = { alwaysTest: false, alwaysBuild: false, confirmation: 'prompt', steps: ['analyze', 'changelog', 'commit'] };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,50 +30,62 @@ export class CommitConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load configuration from npmextra.json
|
* Load configuration from .smartconfig.json
|
||||||
*/
|
*/
|
||||||
public async load(): Promise<void> {
|
public async load(): Promise<void> {
|
||||||
const npmextraInstance = new plugins.npmextra.Npmextra(this.cwd);
|
const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
|
||||||
const gitzoneConfig = npmextraInstance.dataFor<any>('@git.zone/cli', {});
|
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
|
||||||
|
|
||||||
|
const alwaysTest = gitzoneConfig?.commit?.alwaysTest ?? false;
|
||||||
|
const alwaysBuild = gitzoneConfig?.commit?.alwaysBuild ?? false;
|
||||||
this.config = {
|
this.config = {
|
||||||
alwaysTest: gitzoneConfig?.commit?.alwaysTest ?? false,
|
alwaysTest,
|
||||||
alwaysBuild: gitzoneConfig?.commit?.alwaysBuild ?? false,
|
alwaysBuild,
|
||||||
|
confirmation: gitzoneConfig?.commit?.confirmation ?? 'prompt',
|
||||||
|
steps: gitzoneConfig?.commit?.steps || [
|
||||||
|
'analyze',
|
||||||
|
...(alwaysTest ? ['test'] : []),
|
||||||
|
...(alwaysBuild ? ['build'] : []),
|
||||||
|
'changelog',
|
||||||
|
'commit',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save configuration to npmextra.json
|
* Save configuration to .smartconfig.json
|
||||||
*/
|
*/
|
||||||
public async save(): Promise<void> {
|
public async save(): Promise<void> {
|
||||||
const npmextraPath = plugins.path.join(this.cwd, 'npmextra.json');
|
const smartconfigPath = plugins.path.join(this.cwd, '.smartconfig.json');
|
||||||
let npmextraData: any = {};
|
let smartconfigData: any = {};
|
||||||
|
|
||||||
// Read existing npmextra.json
|
// Read existing .smartconfig.json
|
||||||
if (await plugins.smartfs.file(npmextraPath).exists()) {
|
if (await plugins.smartfs.file(smartconfigPath).exists()) {
|
||||||
const content = await plugins.smartfs.file(npmextraPath).encoding('utf8').read();
|
const content = await plugins.smartfs.file(smartconfigPath).encoding('utf8').read();
|
||||||
npmextraData = JSON.parse(content as string);
|
smartconfigData = JSON.parse(content as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure @git.zone/cli namespace exists
|
// Ensure @git.zone/cli namespace exists
|
||||||
if (!npmextraData['@git.zone/cli']) {
|
if (!smartconfigData['@git.zone/cli']) {
|
||||||
npmextraData['@git.zone/cli'] = {};
|
smartconfigData['@git.zone/cli'] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure commit object exists
|
// Ensure commit object exists
|
||||||
if (!npmextraData['@git.zone/cli'].commit) {
|
if (!smartconfigData['@git.zone/cli'].commit) {
|
||||||
npmextraData['@git.zone/cli'].commit = {};
|
smartconfigData['@git.zone/cli'].commit = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update commit settings
|
// Update commit settings
|
||||||
npmextraData['@git.zone/cli'].commit.alwaysTest = this.config.alwaysTest;
|
smartconfigData['@git.zone/cli'].commit.alwaysTest = this.config.alwaysTest;
|
||||||
npmextraData['@git.zone/cli'].commit.alwaysBuild = this.config.alwaysBuild;
|
smartconfigData['@git.zone/cli'].commit.alwaysBuild = this.config.alwaysBuild;
|
||||||
|
smartconfigData['@git.zone/cli'].commit.confirmation = this.config.confirmation;
|
||||||
|
smartconfigData['@git.zone/cli'].commit.steps = this.config.steps;
|
||||||
|
|
||||||
// Write back to file
|
// Write back to file
|
||||||
await plugins.smartfs
|
await plugins.smartfs
|
||||||
.file(npmextraPath)
|
.file(smartconfigPath)
|
||||||
.encoding('utf8')
|
.encoding('utf8')
|
||||||
.write(JSON.stringify(npmextraData, null, 2));
|
.write(JSON.stringify(smartconfigData, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -101,4 +115,20 @@ export class CommitConfig {
|
|||||||
public setAlwaysBuild(value: boolean): void {
|
public setAlwaysBuild(value: boolean): void {
|
||||||
this.config.alwaysBuild = value;
|
this.config.alwaysBuild = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getConfirmation(): 'prompt' | 'auto' | 'plan' {
|
||||||
|
return this.config.confirmation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setConfirmation(value: 'prompt' | 'auto' | 'plan'): void {
|
||||||
|
this.config.confirmation = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSteps(): string[] {
|
||||||
|
return [...this.config.steps];
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSteps(steps: string[]): void {
|
||||||
|
this.config.steps = [...steps];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export interface IReleaseConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages release configuration stored in npmextra.json
|
* Manages release configuration stored in .smartconfig.json
|
||||||
* under @git.zone/cli.release namespace
|
* under @git.zone/cli.release namespace
|
||||||
*/
|
*/
|
||||||
export class ReleaseConfig {
|
export class ReleaseConfig {
|
||||||
@@ -30,53 +30,60 @@ export class ReleaseConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load configuration from npmextra.json
|
* Load configuration from .smartconfig.json
|
||||||
*/
|
*/
|
||||||
public async load(): Promise<void> {
|
public async load(): Promise<void> {
|
||||||
const npmextraInstance = new plugins.npmextra.Npmextra(this.cwd);
|
const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
|
||||||
const gitzoneConfig = npmextraInstance.dataFor<any>('@git.zone/cli', {});
|
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
|
||||||
|
const npmTarget = gitzoneConfig?.release?.targets?.npm || {};
|
||||||
// Also check szci for backward compatibility
|
|
||||||
const szciConfig = npmextraInstance.dataFor<any>('@ship.zone/szci', {});
|
|
||||||
|
|
||||||
this.config = {
|
this.config = {
|
||||||
registries: gitzoneConfig?.release?.registries || [],
|
registries: npmTarget.registries || [],
|
||||||
accessLevel: gitzoneConfig?.release?.accessLevel || szciConfig?.npmAccessLevel || 'public',
|
accessLevel: npmTarget.accessLevel || 'public',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save configuration to npmextra.json
|
* Save configuration to .smartconfig.json
|
||||||
*/
|
*/
|
||||||
public async save(): Promise<void> {
|
public async save(): Promise<void> {
|
||||||
const npmextraPath = plugins.path.join(this.cwd, 'npmextra.json');
|
const smartconfigPath = plugins.path.join(this.cwd, '.smartconfig.json');
|
||||||
let npmextraData: any = {};
|
let smartconfigData: any = {};
|
||||||
|
|
||||||
// Read existing npmextra.json
|
// Read existing .smartconfig.json
|
||||||
if (await plugins.smartfs.file(npmextraPath).exists()) {
|
if (await plugins.smartfs.file(smartconfigPath).exists()) {
|
||||||
const content = await plugins.smartfs.file(npmextraPath).encoding('utf8').read();
|
const content = await plugins.smartfs.file(smartconfigPath).encoding('utf8').read();
|
||||||
npmextraData = JSON.parse(content as string);
|
smartconfigData = JSON.parse(content as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure @git.zone/cli namespace exists
|
// Ensure @git.zone/cli namespace exists
|
||||||
if (!npmextraData['@git.zone/cli']) {
|
if (!smartconfigData['@git.zone/cli']) {
|
||||||
npmextraData['@git.zone/cli'] = {};
|
smartconfigData['@git.zone/cli'] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure release object exists
|
// Ensure release object exists
|
||||||
if (!npmextraData['@git.zone/cli'].release) {
|
if (!smartconfigData['@git.zone/cli'].release) {
|
||||||
npmextraData['@git.zone/cli'].release = {};
|
smartconfigData['@git.zone/cli'].release = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!smartconfigData['@git.zone/cli'].release.targets) {
|
||||||
|
smartconfigData['@git.zone/cli'].release.targets = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!smartconfigData['@git.zone/cli'].release.targets.npm) {
|
||||||
|
smartconfigData['@git.zone/cli'].release.targets.npm = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update registries and accessLevel
|
// Update registries and accessLevel
|
||||||
npmextraData['@git.zone/cli'].release.registries = this.config.registries;
|
smartconfigData['@git.zone/cli'].release.targets.npm.enabled = this.config.registries.length > 0;
|
||||||
npmextraData['@git.zone/cli'].release.accessLevel = this.config.accessLevel;
|
smartconfigData['@git.zone/cli'].release.targets.npm.registries = this.config.registries;
|
||||||
|
smartconfigData['@git.zone/cli'].release.targets.npm.accessLevel = this.config.accessLevel;
|
||||||
|
|
||||||
// Write back to file
|
// Write back to file
|
||||||
await plugins.smartfs
|
await plugins.smartfs
|
||||||
.file(npmextraPath)
|
.file(smartconfigPath)
|
||||||
.encoding('utf8')
|
.encoding('utf8')
|
||||||
.write(JSON.stringify(npmextraData, null, 2));
|
.write(JSON.stringify(smartconfigData, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1900
-199
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,17 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from './mod.plugins.js';
|
||||||
import { FormatContext } from './classes.formatcontext.js';
|
import { FormatContext } from './classes.formatcontext.js';
|
||||||
import type { IPlannedChange, ICheckResult } from './interfaces.format.js';
|
import type {
|
||||||
|
IPlannedChange,
|
||||||
|
ICheckResult,
|
||||||
|
IFormatWarning,
|
||||||
|
} from './interfaces.format.js';
|
||||||
import { Project } from '../classes.project.js';
|
import { Project } from '../classes.project.js';
|
||||||
|
import { FormatStats } from './classes.formatstats.js';
|
||||||
|
|
||||||
export abstract class BaseFormatter {
|
export abstract class BaseFormatter {
|
||||||
protected context: FormatContext;
|
protected context: FormatContext;
|
||||||
protected project: Project;
|
protected project: Project;
|
||||||
protected stats: any; // Will be FormatStats from context
|
protected stats: FormatStats;
|
||||||
|
|
||||||
constructor(context: FormatContext, project: Project) {
|
constructor(context: FormatContext, project: Project) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
@@ -18,6 +23,14 @@ export abstract class BaseFormatter {
|
|||||||
abstract analyze(): Promise<IPlannedChange[]>;
|
abstract analyze(): Promise<IPlannedChange[]>;
|
||||||
abstract applyChange(change: IPlannedChange): Promise<void>;
|
abstract applyChange(change: IPlannedChange): Promise<void>;
|
||||||
|
|
||||||
|
get runsWithoutChanges(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(): Promise<IFormatWarning[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
async execute(changes: IPlannedChange[]): Promise<void> {
|
async execute(changes: IPlannedChange[]): Promise<void> {
|
||||||
const startTime = this.stats.moduleStartTime(this.name);
|
const startTime = this.stats.moduleStartTime(this.name);
|
||||||
this.stats.startModule(this.name);
|
this.stats.startModule(this.name);
|
||||||
@@ -36,9 +49,6 @@ export abstract class BaseFormatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.postExecute();
|
await this.postExecute();
|
||||||
} catch (error) {
|
|
||||||
// Don't rollback here - let the FormatPlanner handle it
|
|
||||||
throw error;
|
|
||||||
} finally {
|
} finally {
|
||||||
this.stats.endModule(this.name, startTime);
|
this.stats.endModule(this.name, startTime);
|
||||||
}
|
}
|
||||||
@@ -53,13 +63,10 @@ export abstract class BaseFormatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async modifyFile(filepath: string, content: string): Promise<void> {
|
protected async modifyFile(filepath: string, content: string): Promise<void> {
|
||||||
// Validate filepath before writing
|
|
||||||
if (!filepath || filepath.trim() === '') {
|
if (!filepath || filepath.trim() === '') {
|
||||||
throw new Error(`Invalid empty filepath in modifyFile`);
|
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;
|
let normalizedPath = filepath;
|
||||||
if (!plugins.path.parse(filepath).dir) {
|
if (!plugins.path.parse(filepath).dir) {
|
||||||
normalizedPath = './' + filepath;
|
normalizedPath = './' + filepath;
|
||||||
@@ -69,44 +76,46 @@ export abstract class BaseFormatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async createFile(filepath: string, content: string): Promise<void> {
|
protected async createFile(filepath: string, content: string): Promise<void> {
|
||||||
await plugins.smartfs.file(filepath).encoding('utf8').write(content);
|
let normalizedPath = filepath;
|
||||||
|
if (!plugins.path.parse(filepath).dir) {
|
||||||
|
normalizedPath = './' + filepath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure parent directory exists
|
||||||
|
const dir = plugins.path.dirname(normalizedPath);
|
||||||
|
if (dir && dir !== '.') {
|
||||||
|
await plugins.smartfs.directory(dir).recursive().create();
|
||||||
|
}
|
||||||
|
|
||||||
|
await plugins.smartfs.file(normalizedPath).encoding('utf8').write(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async deleteFile(filepath: string): Promise<void> {
|
protected async deleteFile(filepath: string): Promise<void> {
|
||||||
await plugins.smartfs.file(filepath).delete();
|
await plugins.smartfs.file(filepath).delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async shouldProcessFile(filepath: string): Promise<boolean> {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for diffs without applying changes
|
* Check for diffs without applying changes
|
||||||
* Returns information about what would change
|
|
||||||
*/
|
*/
|
||||||
async check(): Promise<ICheckResult> {
|
async check(): Promise<ICheckResult> {
|
||||||
const changes = await this.analyze();
|
const changes = await this.analyze();
|
||||||
const diffs: ICheckResult['diffs'] = [];
|
const diffs: ICheckResult['diffs'] = [];
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
// Skip generic changes that don't have actual content
|
|
||||||
if (change.path === '<various files>') {
|
if (change.path === '<various files>') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (change.type === 'modify' || change.type === 'create') {
|
if (change.type === 'modify' || change.type === 'create') {
|
||||||
// Read current content if file exists
|
|
||||||
let currentContent: string | undefined;
|
let currentContent: string | undefined;
|
||||||
try {
|
try {
|
||||||
currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
|
currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
|
||||||
} catch {
|
} catch {
|
||||||
// File doesn't exist yet
|
|
||||||
currentContent = undefined;
|
currentContent = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newContent = change.content;
|
const newContent = change.content;
|
||||||
|
|
||||||
// Check if there's an actual diff
|
|
||||||
if (currentContent !== newContent && newContent !== undefined) {
|
if (currentContent !== newContent && newContent !== undefined) {
|
||||||
diffs.push({
|
diffs.push({
|
||||||
path: change.path,
|
path: change.path,
|
||||||
@@ -116,7 +125,6 @@ export abstract class BaseFormatter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (change.type === 'delete') {
|
} else if (change.type === 'delete') {
|
||||||
// Check if file exists before marking for deletion
|
|
||||||
try {
|
try {
|
||||||
const currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
|
const currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
|
||||||
diffs.push({
|
diffs.push({
|
||||||
@@ -137,9 +145,6 @@ export abstract class BaseFormatter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a single diff using smartdiff
|
|
||||||
*/
|
|
||||||
displayDiff(diff: ICheckResult['diffs'][0]): void {
|
displayDiff(diff: ICheckResult['diffs'][0]): void {
|
||||||
console.log(`\n--- ${diff.path}`);
|
console.log(`\n--- ${diff.path}`);
|
||||||
if (diff.before && diff.after) {
|
if (diff.before && diff.after) {
|
||||||
@@ -150,7 +155,6 @@ export abstract class BaseFormatter {
|
|||||||
}));
|
}));
|
||||||
} else if (diff.after && !diff.before) {
|
} else if (diff.after && !diff.before) {
|
||||||
console.log(' (new file)');
|
console.log(' (new file)');
|
||||||
// Show first few lines of new content
|
|
||||||
const lines = diff.after.split('\n').slice(0, 10);
|
const lines = diff.after.split('\n').slice(0, 10);
|
||||||
lines.forEach(line => console.log(` + ${line}`));
|
lines.forEach(line => console.log(` + ${line}`));
|
||||||
if (diff.after.split('\n').length > 10) {
|
if (diff.after.split('\n').length > 10) {
|
||||||
@@ -161,9 +165,6 @@ export abstract class BaseFormatter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Display all diffs from a check result
|
|
||||||
*/
|
|
||||||
displayAllDiffs(result: ICheckResult): void {
|
displayAllDiffs(result: ICheckResult): void {
|
||||||
if (!result.hasDiff) {
|
if (!result.hasDiff) {
|
||||||
console.log(' No changes detected');
|
console.log(' No changes detected');
|
||||||
|
|||||||
@@ -1,235 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
|
|
||||||
export interface IFileCache {
|
|
||||||
path: string;
|
|
||||||
checksum: string;
|
|
||||||
modified: number;
|
|
||||||
size: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICacheManifest {
|
|
||||||
version: string;
|
|
||||||
lastFormat: number;
|
|
||||||
files: IFileCache[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ChangeCache {
|
|
||||||
private cacheDir: string;
|
|
||||||
private manifestPath: string;
|
|
||||||
private cacheVersion = '1.0.0';
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.cacheDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-cache');
|
|
||||||
this.manifestPath = plugins.path.join(this.cacheDir, 'manifest.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
|
||||||
await plugins.smartfs.directory(this.cacheDir).recursive().create();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getManifest(): Promise<ICacheManifest> {
|
|
||||||
const defaultManifest: ICacheManifest = {
|
|
||||||
version: this.cacheVersion,
|
|
||||||
lastFormat: 0,
|
|
||||||
files: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const exists = await plugins.smartfs.file(this.manifestPath).exists();
|
|
||||||
if (!exists) {
|
|
||||||
return defaultManifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const content = (await plugins.smartfs
|
|
||||||
.file(this.manifestPath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.read()) as string;
|
|
||||||
const manifest = JSON.parse(content);
|
|
||||||
|
|
||||||
// Validate the manifest structure
|
|
||||||
if (this.isValidManifest(manifest)) {
|
|
||||||
return manifest;
|
|
||||||
} else {
|
|
||||||
console.warn('Invalid manifest structure, returning default manifest');
|
|
||||||
return defaultManifest;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
`Failed to read cache manifest: ${error.message}, returning default manifest`,
|
|
||||||
);
|
|
||||||
// Try to delete the corrupted file
|
|
||||||
try {
|
|
||||||
await plugins.smartfs.file(this.manifestPath).delete();
|
|
||||||
} catch (removeError) {
|
|
||||||
// Ignore removal errors
|
|
||||||
}
|
|
||||||
return defaultManifest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveManifest(manifest: ICacheManifest): Promise<void> {
|
|
||||||
// Validate before saving
|
|
||||||
if (!this.isValidManifest(manifest)) {
|
|
||||||
throw new Error('Invalid manifest structure, cannot save');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure directory exists
|
|
||||||
await plugins.smartfs.directory(this.cacheDir).recursive().create();
|
|
||||||
|
|
||||||
// Write directly with proper JSON stringification
|
|
||||||
const jsonContent = JSON.stringify(manifest, null, 2);
|
|
||||||
await plugins.smartfs
|
|
||||||
.file(this.manifestPath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.write(jsonContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
async hasFileChanged(filePath: string): Promise<boolean> {
|
|
||||||
const absolutePath = plugins.path.isAbsolute(filePath)
|
|
||||||
? filePath
|
|
||||||
: plugins.path.join(paths.cwd, filePath);
|
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
const exists = await plugins.smartfs.file(absolutePath).exists();
|
|
||||||
if (!exists) {
|
|
||||||
return true; // File doesn't exist, so it's "changed" (will be created)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current file stats
|
|
||||||
const stats = await plugins.smartfs.file(absolutePath).stat();
|
|
||||||
|
|
||||||
// Skip directories
|
|
||||||
if (stats.isDirectory) {
|
|
||||||
return false; // Directories are not processed
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = (await plugins.smartfs
|
|
||||||
.file(absolutePath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.read()) as string;
|
|
||||||
const currentChecksum = this.calculateChecksum(content);
|
|
||||||
|
|
||||||
// Get cached info
|
|
||||||
const manifest = await this.getManifest();
|
|
||||||
const cachedFile = manifest.files.find((f) => f.path === filePath);
|
|
||||||
|
|
||||||
if (!cachedFile) {
|
|
||||||
return true; // Not in cache, so it's changed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare checksums
|
|
||||||
return (
|
|
||||||
cachedFile.checksum !== currentChecksum ||
|
|
||||||
cachedFile.size !== stats.size ||
|
|
||||||
cachedFile.modified !== stats.mtime.getTime()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateFileCache(filePath: string): Promise<void> {
|
|
||||||
const absolutePath = plugins.path.isAbsolute(filePath)
|
|
||||||
? filePath
|
|
||||||
: plugins.path.join(paths.cwd, filePath);
|
|
||||||
|
|
||||||
// Get current file stats
|
|
||||||
const stats = await plugins.smartfs.file(absolutePath).stat();
|
|
||||||
|
|
||||||
// Skip directories
|
|
||||||
if (stats.isDirectory) {
|
|
||||||
return; // Don't cache directories
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = (await plugins.smartfs
|
|
||||||
.file(absolutePath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.read()) as string;
|
|
||||||
const checksum = this.calculateChecksum(content);
|
|
||||||
|
|
||||||
// Update manifest
|
|
||||||
const manifest = await this.getManifest();
|
|
||||||
const existingIndex = manifest.files.findIndex((f) => f.path === filePath);
|
|
||||||
|
|
||||||
const cacheEntry: IFileCache = {
|
|
||||||
path: filePath,
|
|
||||||
checksum,
|
|
||||||
modified: stats.mtime.getTime(),
|
|
||||||
size: stats.size,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
|
||||||
manifest.files[existingIndex] = cacheEntry;
|
|
||||||
} else {
|
|
||||||
manifest.files.push(cacheEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest.lastFormat = Date.now();
|
|
||||||
await this.saveManifest(manifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getChangedFiles(filePaths: string[]): Promise<string[]> {
|
|
||||||
const changedFiles: string[] = [];
|
|
||||||
|
|
||||||
for (const filePath of filePaths) {
|
|
||||||
if (await this.hasFileChanged(filePath)) {
|
|
||||||
changedFiles.push(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return changedFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
async clean(): Promise<void> {
|
|
||||||
const manifest = await this.getManifest();
|
|
||||||
const validFiles: IFileCache[] = [];
|
|
||||||
|
|
||||||
// Remove entries for files that no longer exist
|
|
||||||
for (const file of manifest.files) {
|
|
||||||
const absolutePath = plugins.path.isAbsolute(file.path)
|
|
||||||
? file.path
|
|
||||||
: plugins.path.join(paths.cwd, file.path);
|
|
||||||
|
|
||||||
if (await plugins.smartfs.file(absolutePath).exists()) {
|
|
||||||
validFiles.push(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest.files = validFiles;
|
|
||||||
await this.saveManifest(manifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
private calculateChecksum(content: string | Buffer): string {
|
|
||||||
return plugins.crypto.createHash('sha256').update(content).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
private isValidManifest(manifest: any): manifest is ICacheManifest {
|
|
||||||
// Check if manifest has the required structure
|
|
||||||
if (!manifest || typeof manifest !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check required fields
|
|
||||||
if (
|
|
||||||
typeof manifest.version !== 'string' ||
|
|
||||||
typeof manifest.lastFormat !== 'number' ||
|
|
||||||
!Array.isArray(manifest.files)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check each file entry
|
|
||||||
for (const file of manifest.files) {
|
|
||||||
if (
|
|
||||||
!file ||
|
|
||||||
typeof file !== 'object' ||
|
|
||||||
typeof file.path !== 'string' ||
|
|
||||||
typeof file.checksum !== 'string' ||
|
|
||||||
typeof file.modified !== 'number' ||
|
|
||||||
typeof file.size !== 'number'
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import { BaseFormatter } from './classes.baseformatter.js';
|
|
||||||
|
|
||||||
export interface IModuleDependency {
|
|
||||||
module: string;
|
|
||||||
dependencies: Set<string>;
|
|
||||||
dependents: Set<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DependencyAnalyzer {
|
|
||||||
private moduleDependencies: Map<string, IModuleDependency> = new Map();
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.initializeDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
private initializeDependencies(): void {
|
|
||||||
// Define dependencies between format modules
|
|
||||||
const dependencies = {
|
|
||||||
cleanup: [], // No dependencies
|
|
||||||
npmextra: [], // No dependencies
|
|
||||||
license: ['npmextra'], // Depends on npmextra for config
|
|
||||||
packagejson: ['npmextra'], // Depends on npmextra for config
|
|
||||||
templates: ['npmextra', 'packagejson'], // Depends on both
|
|
||||||
gitignore: ['templates'], // Depends on templates
|
|
||||||
tsconfig: ['packagejson'], // Depends on package.json
|
|
||||||
prettier: [
|
|
||||||
'cleanup',
|
|
||||||
'npmextra',
|
|
||||||
'packagejson',
|
|
||||||
'templates',
|
|
||||||
'gitignore',
|
|
||||||
'tsconfig',
|
|
||||||
], // Runs after most others
|
|
||||||
readme: ['npmextra', 'packagejson'], // Depends on project metadata
|
|
||||||
copy: ['npmextra'], // Depends on config
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize all modules
|
|
||||||
for (const [module, deps] of Object.entries(dependencies)) {
|
|
||||||
this.moduleDependencies.set(module, {
|
|
||||||
module,
|
|
||||||
dependencies: new Set(deps),
|
|
||||||
dependents: new Set(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build reverse dependencies (dependents)
|
|
||||||
for (const [module, deps] of Object.entries(dependencies)) {
|
|
||||||
for (const dep of deps) {
|
|
||||||
const depModule = this.moduleDependencies.get(dep);
|
|
||||||
if (depModule) {
|
|
||||||
depModule.dependents.add(module);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getExecutionGroups(modules: BaseFormatter[]): BaseFormatter[][] {
|
|
||||||
const modulesMap = new Map(modules.map((m) => [m.name, m]));
|
|
||||||
const executed = new Set<string>();
|
|
||||||
const groups: BaseFormatter[][] = [];
|
|
||||||
|
|
||||||
while (executed.size < modules.length) {
|
|
||||||
const currentGroup: BaseFormatter[] = [];
|
|
||||||
|
|
||||||
for (const module of modules) {
|
|
||||||
if (executed.has(module.name)) continue;
|
|
||||||
|
|
||||||
const dependency = this.moduleDependencies.get(module.name);
|
|
||||||
if (!dependency) {
|
|
||||||
// Unknown module, execute in isolation
|
|
||||||
currentGroup.push(module);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if all dependencies have been executed
|
|
||||||
const allDepsExecuted = Array.from(dependency.dependencies).every(
|
|
||||||
(dep) => executed.has(dep) || !modulesMap.has(dep),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (allDepsExecuted) {
|
|
||||||
currentGroup.push(module);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentGroup.length === 0) {
|
|
||||||
// Circular dependency or error - execute remaining modules
|
|
||||||
for (const module of modules) {
|
|
||||||
if (!executed.has(module.name)) {
|
|
||||||
currentGroup.push(module);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentGroup.forEach((m) => executed.add(m.name));
|
|
||||||
groups.push(currentGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
canRunInParallel(module1: string, module2: string): boolean {
|
|
||||||
const dep1 = this.moduleDependencies.get(module1);
|
|
||||||
const dep2 = this.moduleDependencies.get(module2);
|
|
||||||
|
|
||||||
if (!dep1 || !dep2) return false;
|
|
||||||
|
|
||||||
// Check if module1 depends on module2 or vice versa
|
|
||||||
return (
|
|
||||||
!dep1.dependencies.has(module2) &&
|
|
||||||
!dep2.dependencies.has(module1) &&
|
|
||||||
!dep1.dependents.has(module2) &&
|
|
||||||
!dep2.dependents.has(module1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,31 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from "./mod.plugins.js";
|
||||||
import { FormatStats } from './classes.formatstats.js';
|
import { FormatStats } from "./classes.formatstats.js";
|
||||||
|
|
||||||
|
interface IFormatContextOptions {
|
||||||
|
interactive?: boolean;
|
||||||
|
jsonOutput?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class FormatContext {
|
export class FormatContext {
|
||||||
private formatStats: FormatStats;
|
private formatStats: FormatStats;
|
||||||
|
private interactive: boolean;
|
||||||
|
private jsonOutput: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor(options: IFormatContextOptions = {}) {
|
||||||
this.formatStats = new FormatStats();
|
this.formatStats = new FormatStats();
|
||||||
|
this.interactive = options.interactive ?? true;
|
||||||
|
this.jsonOutput = options.jsonOutput ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFormatStats(): FormatStats {
|
getFormatStats(): FormatStats {
|
||||||
return this.formatStats;
|
return this.formatStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isInteractive(): boolean {
|
||||||
|
return this.interactive;
|
||||||
|
}
|
||||||
|
|
||||||
|
isJsonOutput(): boolean {
|
||||||
|
return this.jsonOutput;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from './mod.plugins.js';
|
||||||
import { FormatContext } from './classes.formatcontext.js';
|
import { FormatContext } from './classes.formatcontext.js';
|
||||||
import { BaseFormatter } from './classes.baseformatter.js';
|
import { BaseFormatter } from './classes.baseformatter.js';
|
||||||
import type { IFormatPlan, IPlannedChange } from './interfaces.format.js';
|
import type {
|
||||||
|
IFormatPlan,
|
||||||
|
IPlannedChange,
|
||||||
|
IFormatWarning,
|
||||||
|
} from './interfaces.format.js';
|
||||||
|
import { getModuleIcon } from './interfaces.format.js';
|
||||||
import { logger } from '../gitzone.logging.js';
|
import { logger } from '../gitzone.logging.js';
|
||||||
import { DependencyAnalyzer } from './classes.dependency-analyzer.js';
|
|
||||||
import { DiffReporter } from './classes.diffreporter.js';
|
import { DiffReporter } from './classes.diffreporter.js';
|
||||||
|
|
||||||
export class FormatPlanner {
|
export class FormatPlanner {
|
||||||
private plannedChanges: Map<string, IPlannedChange[]> = new Map();
|
private plannedChanges: Map<string, IPlannedChange[]> = new Map();
|
||||||
private dependencyAnalyzer = new DependencyAnalyzer();
|
|
||||||
private diffReporter = new DiffReporter();
|
private diffReporter = new DiffReporter();
|
||||||
|
|
||||||
async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> {
|
async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> {
|
||||||
@@ -18,7 +21,6 @@ export class FormatPlanner {
|
|||||||
filesAdded: 0,
|
filesAdded: 0,
|
||||||
filesModified: 0,
|
filesModified: 0,
|
||||||
filesRemoved: 0,
|
filesRemoved: 0,
|
||||||
estimatedTime: 0,
|
|
||||||
},
|
},
|
||||||
changes: [],
|
changes: [],
|
||||||
warnings: [],
|
warnings: [],
|
||||||
@@ -32,7 +34,6 @@ export class FormatPlanner {
|
|||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
plan.changes.push(change);
|
plan.changes.push(change);
|
||||||
|
|
||||||
// Update summary
|
|
||||||
switch (change.type) {
|
switch (change.type) {
|
||||||
case 'create':
|
case 'create':
|
||||||
plan.summary.filesAdded++;
|
plan.summary.filesAdded++;
|
||||||
@@ -45,20 +46,25 @@ export class FormatPlanner {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const warnings = await module.validate();
|
||||||
|
plan.warnings.push(...warnings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
plan.warnings.push({
|
plan.warnings.push({
|
||||||
level: 'error',
|
level: 'error',
|
||||||
message: `Failed to analyze module ${module.name}: ${error.message}`,
|
message: `Failed to analyze module ${module.name}: ${errorMessage}`,
|
||||||
module: module.name,
|
module: module.name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plan.warnings.push(...this.detectConflictingChanges(plan.changes));
|
||||||
|
|
||||||
plan.summary.totalFiles =
|
plan.summary.totalFiles =
|
||||||
plan.summary.filesAdded +
|
plan.summary.filesAdded +
|
||||||
plan.summary.filesModified +
|
plan.summary.filesModified +
|
||||||
plan.summary.filesRemoved;
|
plan.summary.filesRemoved;
|
||||||
plan.summary.estimatedTime = plan.summary.totalFiles * 100; // 100ms per file estimate
|
|
||||||
|
|
||||||
return plan;
|
return plan;
|
||||||
}
|
}
|
||||||
@@ -67,27 +73,21 @@ export class FormatPlanner {
|
|||||||
plan: IFormatPlan,
|
plan: IFormatPlan,
|
||||||
modules: BaseFormatter[],
|
modules: BaseFormatter[],
|
||||||
context: FormatContext,
|
context: FormatContext,
|
||||||
parallel: boolean = false,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
const changesByModule = this.groupChangesByModule(plan.changes);
|
||||||
|
|
||||||
try {
|
for (const module of modules) {
|
||||||
// Always use sequential execution to avoid race conditions
|
const changes = changesByModule.get(module.name) || [];
|
||||||
for (const module of modules) {
|
|
||||||
const changes = this.plannedChanges.get(module.name) || [];
|
|
||||||
|
|
||||||
if (changes.length > 0) {
|
if (changes.length > 0 || module.runsWithoutChanges) {
|
||||||
logger.log('info', `Executing ${module.name} formatter...`);
|
logger.log('info', `Executing ${module.name} formatter...`);
|
||||||
await module.execute(changes);
|
await module.execute(changes);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const endTime = Date.now();
|
|
||||||
const duration = endTime - startTime;
|
|
||||||
logger.log('info', `Format operations completed in ${duration}ms`);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.log('info', `Format operations completed in ${duration}ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async displayPlan(
|
async displayPlan(
|
||||||
@@ -103,7 +103,6 @@ export class FormatPlanner {
|
|||||||
console.log('');
|
console.log('');
|
||||||
console.log('Changes by module:');
|
console.log('Changes by module:');
|
||||||
|
|
||||||
// Group changes by module
|
|
||||||
const changesByModule = new Map<string, IPlannedChange[]>();
|
const changesByModule = new Map<string, IPlannedChange[]>();
|
||||||
for (const change of plan.changes) {
|
for (const change of plan.changes) {
|
||||||
const moduleChanges = changesByModule.get(change.module) || [];
|
const moduleChanges = changesByModule.get(change.module) || [];
|
||||||
@@ -113,14 +112,13 @@ export class FormatPlanner {
|
|||||||
|
|
||||||
for (const [module, changes] of changesByModule) {
|
for (const [module, changes] of changesByModule) {
|
||||||
console.log(
|
console.log(
|
||||||
`\n${this.getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`,
|
`\n${getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
const icon = this.getChangeIcon(change.type);
|
const icon = this.getChangeIcon(change.type);
|
||||||
console.log(` ${icon} ${change.path} - ${change.description}`);
|
console.log(` ${icon} ${change.path} - ${change.description}`);
|
||||||
|
|
||||||
// Show diff for modified files if detailed view is requested
|
|
||||||
if (detailed && change.type === 'modify') {
|
if (detailed && change.type === 'modify') {
|
||||||
const diff = await this.diffReporter.generateDiffForChange(change);
|
const diff = await this.diffReporter.generateDiffForChange(change);
|
||||||
if (diff) {
|
if (diff) {
|
||||||
@@ -141,22 +139,6 @@ export class FormatPlanner {
|
|||||||
console.log('\n' + '━'.repeat(50));
|
console.log('\n' + '━'.repeat(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
private getModuleIcon(module: string): string {
|
|
||||||
const icons: Record<string, string> = {
|
|
||||||
packagejson: '📦',
|
|
||||||
license: '📝',
|
|
||||||
tsconfig: '🔧',
|
|
||||||
cleanup: '🚮',
|
|
||||||
gitignore: '🔒',
|
|
||||||
prettier: '✨',
|
|
||||||
readme: '📖',
|
|
||||||
templates: '📄',
|
|
||||||
npmextra: '⚙️',
|
|
||||||
copy: '📋',
|
|
||||||
};
|
|
||||||
return icons[module] || '📁';
|
|
||||||
}
|
|
||||||
|
|
||||||
private getChangeIcon(type: 'create' | 'modify' | 'delete'): string {
|
private getChangeIcon(type: 'create' | 'modify' | 'delete'): string {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'create':
|
case 'create':
|
||||||
@@ -167,4 +149,55 @@ export class FormatPlanner {
|
|||||||
return '❌';
|
return '❌';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private groupChangesByModule(
|
||||||
|
changes: IPlannedChange[],
|
||||||
|
): Map<string, IPlannedChange[]> {
|
||||||
|
const changesByModule = new Map<string, IPlannedChange[]>();
|
||||||
|
for (const change of changes) {
|
||||||
|
const moduleChanges = changesByModule.get(change.module) || [];
|
||||||
|
moduleChanges.push(change);
|
||||||
|
changesByModule.set(change.module, moduleChanges);
|
||||||
|
}
|
||||||
|
return changesByModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
private detectConflictingChanges(
|
||||||
|
changes: IPlannedChange[],
|
||||||
|
): IFormatWarning[] {
|
||||||
|
const warnings: IFormatWarning[] = [];
|
||||||
|
const changesByPath = new Map<string, IPlannedChange[]>();
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
if (!change.path || change.path === '<various files>') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathChanges = changesByPath.get(change.path) || [];
|
||||||
|
pathChanges.push(change);
|
||||||
|
changesByPath.set(change.path, pathChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [path, pathChanges] of changesByPath) {
|
||||||
|
const modules = [...new Set(pathChanges.map((change) => change.module))];
|
||||||
|
if (modules.length < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDelete = pathChanges.some((change) => change.type === 'delete');
|
||||||
|
const plannedContents = pathChanges
|
||||||
|
.map((change) => change.content)
|
||||||
|
.filter((content): content is string => content !== undefined);
|
||||||
|
const uniqueContents = new Set(plannedContents);
|
||||||
|
const level = hasDelete || uniqueContents.size > 1 ? 'warning' : 'info';
|
||||||
|
|
||||||
|
warnings.push({
|
||||||
|
level,
|
||||||
|
module: 'planner',
|
||||||
|
message: `Multiple formatters plan changes for ${path}: ${modules.join(', ')}. They will run in formatter order.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from './mod.plugins.js';
|
||||||
import { logger } from '../gitzone.logging.js';
|
import { logger } from '../gitzone.logging.js';
|
||||||
|
import { getModuleIcon } from './interfaces.format.js';
|
||||||
|
|
||||||
export interface IModuleStats {
|
export interface IModuleStats {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -23,8 +24,6 @@ export interface IFormatStats {
|
|||||||
totalModified: number;
|
totalModified: number;
|
||||||
totalDeleted: number;
|
totalDeleted: number;
|
||||||
totalErrors: number;
|
totalErrors: number;
|
||||||
cacheHits: number;
|
|
||||||
cacheMisses: number;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,8 +42,6 @@ export class FormatStats {
|
|||||||
totalModified: 0,
|
totalModified: 0,
|
||||||
totalDeleted: 0,
|
totalDeleted: 0,
|
||||||
totalErrors: 0,
|
totalErrors: 0,
|
||||||
cacheHits: 0,
|
|
||||||
cacheMisses: 0,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -107,14 +104,6 @@ export class FormatStats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recordCacheHit(): void {
|
|
||||||
this.stats.overallStats.cacheHits++;
|
|
||||||
}
|
|
||||||
|
|
||||||
recordCacheMiss(): void {
|
|
||||||
this.stats.overallStats.cacheMisses++;
|
|
||||||
}
|
|
||||||
|
|
||||||
finish(): void {
|
finish(): void {
|
||||||
this.stats.endTime = Date.now();
|
this.stats.endTime = Date.now();
|
||||||
this.stats.totalExecutionTime = this.stats.endTime - this.stats.startTime;
|
this.stats.totalExecutionTime = this.stats.endTime - this.stats.startTime;
|
||||||
@@ -135,20 +124,6 @@ export class FormatStats {
|
|||||||
console.log(` • Deleted: ${this.stats.overallStats.totalDeleted}`);
|
console.log(` • Deleted: ${this.stats.overallStats.totalDeleted}`);
|
||||||
console.log(` Errors: ${this.stats.overallStats.totalErrors}`);
|
console.log(` Errors: ${this.stats.overallStats.totalErrors}`);
|
||||||
|
|
||||||
if (
|
|
||||||
this.stats.overallStats.cacheHits > 0 ||
|
|
||||||
this.stats.overallStats.cacheMisses > 0
|
|
||||||
) {
|
|
||||||
const cacheHitRate =
|
|
||||||
(this.stats.overallStats.cacheHits /
|
|
||||||
(this.stats.overallStats.cacheHits +
|
|
||||||
this.stats.overallStats.cacheMisses)) *
|
|
||||||
100;
|
|
||||||
console.log(` Cache Hit Rate: ${cacheHitRate.toFixed(1)}%`);
|
|
||||||
console.log(` • Hits: ${this.stats.overallStats.cacheHits}`);
|
|
||||||
console.log(` • Misses: ${this.stats.overallStats.cacheMisses}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Module stats
|
// Module stats
|
||||||
console.log('\nModule Breakdown:');
|
console.log('\nModule Breakdown:');
|
||||||
console.log('─'.repeat(50));
|
console.log('─'.repeat(50));
|
||||||
@@ -159,7 +134,7 @@ export class FormatStats {
|
|||||||
|
|
||||||
for (const moduleStats of sortedModules) {
|
for (const moduleStats of sortedModules) {
|
||||||
console.log(
|
console.log(
|
||||||
`\n${this.getModuleIcon(moduleStats.name)} ${moduleStats.name}:`,
|
`\n${getModuleIcon(moduleStats.name)} ${moduleStats.name}:`,
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
` Execution Time: ${this.formatDuration(moduleStats.executionTime)}`,
|
` Execution Time: ${this.formatDuration(moduleStats.executionTime)}`,
|
||||||
@@ -211,19 +186,4 @@ export class FormatStats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getModuleIcon(module: string): string {
|
|
||||||
const icons: Record<string, string> = {
|
|
||||||
packagejson: '📦',
|
|
||||||
license: '📝',
|
|
||||||
tsconfig: '🔧',
|
|
||||||
cleanup: '🚮',
|
|
||||||
gitignore: '🔒',
|
|
||||||
prettier: '✨',
|
|
||||||
readme: '📖',
|
|
||||||
templates: '📄',
|
|
||||||
npmextra: '⚙️',
|
|
||||||
copy: '📋',
|
|
||||||
};
|
|
||||||
return icons[module] || '📁';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,340 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
import type { IFormatOperation } from './interfaces.format.js';
|
|
||||||
|
|
||||||
export class RollbackManager {
|
|
||||||
private backupDir: string;
|
|
||||||
private manifestPath: string;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.backupDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-backups');
|
|
||||||
this.manifestPath = plugins.path.join(this.backupDir, 'manifest.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
async createOperation(): Promise<IFormatOperation> {
|
|
||||||
await this.ensureBackupDir();
|
|
||||||
|
|
||||||
const operation: IFormatOperation = {
|
|
||||||
id: this.generateOperationId(),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
files: [],
|
|
||||||
status: 'pending',
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.updateManifest(operation);
|
|
||||||
return operation;
|
|
||||||
}
|
|
||||||
|
|
||||||
async backupFile(filepath: string, operationId: string): Promise<void> {
|
|
||||||
const operation = await this.getOperation(operationId);
|
|
||||||
if (!operation) {
|
|
||||||
throw new Error(`Operation ${operationId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const absolutePath = plugins.path.isAbsolute(filepath)
|
|
||||||
? filepath
|
|
||||||
: plugins.path.join(paths.cwd, filepath);
|
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
const exists = await plugins.smartfs.file(absolutePath).exists();
|
|
||||||
if (!exists) {
|
|
||||||
// File doesn't exist yet (will be created), so we skip backup
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read file content and metadata
|
|
||||||
const content = (await plugins.smartfs
|
|
||||||
.file(absolutePath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.read()) as string;
|
|
||||||
const stats = await plugins.smartfs.file(absolutePath).stat();
|
|
||||||
const checksum = this.calculateChecksum(content);
|
|
||||||
|
|
||||||
// Create backup
|
|
||||||
const backupPath = this.getBackupPath(operationId, filepath);
|
|
||||||
await plugins.smartfs
|
|
||||||
.directory(plugins.path.dirname(backupPath))
|
|
||||||
.recursive()
|
|
||||||
.create();
|
|
||||||
await plugins.smartfs.file(backupPath).encoding('utf8').write(content);
|
|
||||||
|
|
||||||
// Update operation
|
|
||||||
operation.files.push({
|
|
||||||
path: filepath,
|
|
||||||
originalContent: content,
|
|
||||||
checksum,
|
|
||||||
permissions: stats.mode.toString(8),
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.updateManifest(operation);
|
|
||||||
}
|
|
||||||
|
|
||||||
async rollback(operationId: string): Promise<void> {
|
|
||||||
const operation = await this.getOperation(operationId);
|
|
||||||
if (!operation) {
|
|
||||||
// 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
|
|
||||||
: plugins.path.join(paths.cwd, file.path);
|
|
||||||
|
|
||||||
// Verify backup integrity
|
|
||||||
const backupPath = this.getBackupPath(operationId, file.path);
|
|
||||||
const backupContent = await plugins.smartfs
|
|
||||||
.file(backupPath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.read();
|
|
||||||
const backupChecksum = this.calculateChecksum(backupContent);
|
|
||||||
|
|
||||||
if (backupChecksum !== file.checksum) {
|
|
||||||
throw new Error(`Backup integrity check failed for ${file.path}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore file
|
|
||||||
await plugins.smartfs
|
|
||||||
.file(absolutePath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.write(file.originalContent);
|
|
||||||
|
|
||||||
// Restore permissions
|
|
||||||
const mode = parseInt(file.permissions, 8);
|
|
||||||
// Note: Permissions restoration may not work on all platforms
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update operation status
|
|
||||||
operation.status = 'rolled-back';
|
|
||||||
await this.updateManifest(operation);
|
|
||||||
}
|
|
||||||
|
|
||||||
async markComplete(operationId: string): Promise<void> {
|
|
||||||
const operation = await this.getOperation(operationId);
|
|
||||||
if (!operation) {
|
|
||||||
throw new Error(`Operation ${operationId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
operation.status = 'completed';
|
|
||||||
await this.updateManifest(operation);
|
|
||||||
}
|
|
||||||
|
|
||||||
async cleanOldBackups(retentionDays: number): Promise<void> {
|
|
||||||
const manifest = await this.getManifest();
|
|
||||||
const cutoffTime = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
const operationsToDelete = manifest.operations.filter(
|
|
||||||
(op) => op.timestamp < cutoffTime && op.status === 'completed',
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const operation of operationsToDelete) {
|
|
||||||
// Remove backup files
|
|
||||||
const operationDir = plugins.path.join(
|
|
||||||
this.backupDir,
|
|
||||||
'operations',
|
|
||||||
operation.id,
|
|
||||||
);
|
|
||||||
await plugins.smartfs.directory(operationDir).recursive().delete();
|
|
||||||
|
|
||||||
// Remove from manifest
|
|
||||||
manifest.operations = manifest.operations.filter(
|
|
||||||
(op) => op.id !== operation.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.saveManifest(manifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyBackup(operationId: string): Promise<boolean> {
|
|
||||||
const operation = await this.getOperation(operationId);
|
|
||||||
if (!operation) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const file of operation.files) {
|
|
||||||
const backupPath = this.getBackupPath(operationId, file.path);
|
|
||||||
const exists = await plugins.smartfs.file(backupPath).exists();
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await plugins.smartfs
|
|
||||||
.file(backupPath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.read();
|
|
||||||
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.smartfs.directory(this.backupDir).recursive().create();
|
|
||||||
await plugins.smartfs
|
|
||||||
.directory(plugins.path.join(this.backupDir, 'operations'))
|
|
||||||
.recursive()
|
|
||||||
.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateOperationId(): string {
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
||||||
const random = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `${timestamp}-${random}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getBackupPath(operationId: string, filepath: string): string {
|
|
||||||
const filename = plugins.path.basename(filepath);
|
|
||||||
const dir = plugins.path.dirname(filepath);
|
|
||||||
const safeDir = dir.replace(/[/\\]/g, '__');
|
|
||||||
return plugins.path.join(
|
|
||||||
this.backupDir,
|
|
||||||
'operations',
|
|
||||||
operationId,
|
|
||||||
'files',
|
|
||||||
safeDir,
|
|
||||||
`${filename}.backup`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private calculateChecksum(content: string | Buffer): string {
|
|
||||||
return plugins.crypto.createHash('sha256').update(content).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getManifest(): Promise<{ operations: IFormatOperation[] }> {
|
|
||||||
const defaultManifest = { operations: [] };
|
|
||||||
|
|
||||||
const exists = await plugins.smartfs.file(this.manifestPath).exists();
|
|
||||||
if (!exists) {
|
|
||||||
return defaultManifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const content = (await plugins.smartfs
|
|
||||||
.file(this.manifestPath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.read()) as string;
|
|
||||||
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',
|
|
||||||
);
|
|
||||||
return defaultManifest;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
`Failed to read rollback manifest: ${error.message}, returning default manifest`,
|
|
||||||
);
|
|
||||||
// Try to delete the corrupted file
|
|
||||||
try {
|
|
||||||
await plugins.smartfs.file(this.manifestPath).delete();
|
|
||||||
} catch (removeError) {
|
|
||||||
// Ignore removal errors
|
|
||||||
}
|
|
||||||
return defaultManifest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async saveManifest(manifest: {
|
|
||||||
operations: IFormatOperation[];
|
|
||||||
}): Promise<void> {
|
|
||||||
// Validate before saving
|
|
||||||
if (!this.isValidManifest(manifest)) {
|
|
||||||
throw new Error('Invalid rollback manifest structure, cannot save');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure directory exists
|
|
||||||
await this.ensureBackupDir();
|
|
||||||
|
|
||||||
// Write directly with proper JSON stringification
|
|
||||||
const jsonContent = JSON.stringify(manifest, null, 2);
|
|
||||||
await plugins.smartfs
|
|
||||||
.file(this.manifestPath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.write(jsonContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getOperation(
|
|
||||||
operationId: string,
|
|
||||||
): Promise<IFormatOperation | null> {
|
|
||||||
const manifest = await this.getManifest();
|
|
||||||
return manifest.operations.find((op) => op.id === operationId) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateManifest(operation: IFormatOperation): Promise<void> {
|
|
||||||
const manifest = await this.getManifest();
|
|
||||||
const existingIndex = manifest.operations.findIndex(
|
|
||||||
(op) => op.id === operation.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
|
||||||
manifest.operations[existingIndex] = operation;
|
|
||||||
} else {
|
|
||||||
manifest.operations.push(operation);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.saveManifest(manifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
) {
|
|
||||||
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'
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
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',
|
|
||||||
];
|
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
for (const relativeFilePath of filesToDelete) {
|
|
||||||
const fileExists = await plugins.smartfs.file(relativeFilePath).exists();
|
|
||||||
if (fileExists) {
|
|
||||||
logger.log('info', `Found ${relativeFilePath}! Removing it!`);
|
|
||||||
await plugins.smartfs
|
|
||||||
.file(plugins.path.join(paths.cwd, relativeFilePath))
|
|
||||||
.delete();
|
|
||||||
} else {
|
|
||||||
logger.log('info', `Project is free of ${relativeFilePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import type { Project } from '../classes.project.js';
|
|
||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
const gitzoneConfig = await projectArg.gitzoneConfig;
|
|
||||||
|
|
||||||
// Get copy configuration from npmextra.json
|
|
||||||
const npmextraConfig = new plugins.npmextra.Npmextra();
|
|
||||||
const copyConfig = npmextraConfig.dataFor<any>('gitzone.format.copy', {
|
|
||||||
patterns: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!copyConfig.patterns || copyConfig.patterns.length === 0) {
|
|
||||||
logger.log('info', 'No copy patterns configured in npmextra.json');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const pattern of copyConfig.patterns) {
|
|
||||||
if (!pattern.from || !pattern.to) {
|
|
||||||
logger.log('warn', 'Invalid copy pattern - missing "from" or "to" field');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Handle glob patterns
|
|
||||||
const entries = await plugins.smartfs
|
|
||||||
.directory('.')
|
|
||||||
.recursive()
|
|
||||||
.filter(pattern.from)
|
|
||||||
.list();
|
|
||||||
const files = entries.map((entry) => entry.path);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const sourcePath = file;
|
|
||||||
let destPath = pattern.to;
|
|
||||||
|
|
||||||
// If destination is a directory, preserve filename
|
|
||||||
if (pattern.to.endsWith('/')) {
|
|
||||||
const filename = plugins.path.basename(file);
|
|
||||||
destPath = plugins.path.join(pattern.to, filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle template variables in destination path
|
|
||||||
if (pattern.preservePath) {
|
|
||||||
const relativePath = plugins.path.relative(
|
|
||||||
plugins.path.dirname(pattern.from.replace(/\*/g, '')),
|
|
||||||
file,
|
|
||||||
);
|
|
||||||
destPath = plugins.path.join(pattern.to, relativePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure destination directory exists
|
|
||||||
await plugins.smartfs
|
|
||||||
.directory(plugins.path.dirname(destPath))
|
|
||||||
.recursive()
|
|
||||||
.create();
|
|
||||||
|
|
||||||
// Copy file
|
|
||||||
await plugins.smartfs.file(sourcePath).copy(destPath);
|
|
||||||
logger.log('info', `Copied ${sourcePath} to ${destPath}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.log(
|
|
||||||
'error',
|
|
||||||
`Failed to copy pattern ${pattern.from}: ${error.message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example npmextra.json configuration:
|
|
||||||
* {
|
|
||||||
* "gitzone": {
|
|
||||||
* "format": {
|
|
||||||
* "copy": {
|
|
||||||
* "patterns": [
|
|
||||||
* {
|
|
||||||
* "from": "src/assets/*",
|
|
||||||
* "to": "dist/assets/",
|
|
||||||
* "preservePath": true
|
|
||||||
* },
|
|
||||||
* {
|
|
||||||
* "from": "config/*.json",
|
|
||||||
* "to": "dist/"
|
|
||||||
* }
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
|
|
||||||
import { Project } from '../classes.project.js';
|
|
||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
|
||||||
const gitignorePath = plugins.path.join(paths.cwd, './.gitignore');
|
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
const gitignoreExists = await plugins.smartfs.file(gitignorePath).exists();
|
|
||||||
let customContent = '';
|
|
||||||
|
|
||||||
if (gitignoreExists) {
|
|
||||||
// lets get the existing gitignore file
|
|
||||||
const existingGitIgnoreString = (await plugins.smartfs
|
|
||||||
.file(gitignorePath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.read()) as string;
|
|
||||||
|
|
||||||
// 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 = (await plugins.smartfs
|
|
||||||
.file(gitignorePath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.read()) as string;
|
|
||||||
// The template already ends with "#------# custom", so just append the content
|
|
||||||
const finalContent =
|
|
||||||
newGitignoreContent.trimEnd() + '\n' + customContent + '\n';
|
|
||||||
await plugins.smartfs
|
|
||||||
.file(gitignorePath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.write(finalContent);
|
|
||||||
logger.log('info', 'Updated .gitignore while preserving custom section!');
|
|
||||||
} else {
|
|
||||||
logger.log('info', 'Added a .gitignore!');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
import { Project } from '../classes.project.js';
|
|
||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
|
||||||
|
|
||||||
const incompatibleLicenses: string[] = ['AGPL', 'GPL', 'SSPL'];
|
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
const nodeModulesInstalled = await plugins.smartfs
|
|
||||||
.directory(plugins.path.join(paths.cwd, 'node_modules'))
|
|
||||||
.exists();
|
|
||||||
if (!nodeModulesInstalled) {
|
|
||||||
logger.log('warn', 'No node_modules found. Skipping license check');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
|
|
||||||
const licenseCheckResult = await licenseChecker.excludeLicenseWithinPath(
|
|
||||||
paths.cwd,
|
|
||||||
incompatibleLicenses,
|
|
||||||
);
|
|
||||||
if (licenseCheckResult.failingModules.length === 0) {
|
|
||||||
logger.log('info', 'Success -> licenses passed!');
|
|
||||||
} 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}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
import * as gulpFunction from '@push.rocks/gulp-function';
|
|
||||||
import { Project } from '../classes.project.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrates npmextra.json from old namespace keys to new package-scoped keys
|
|
||||||
*/
|
|
||||||
const migrateNamespaceKeys = (npmextraJson: any): boolean => {
|
|
||||||
let migrated = false;
|
|
||||||
const migrations = [
|
|
||||||
{ oldKey: 'gitzone', newKey: '@git.zone/cli' },
|
|
||||||
{ oldKey: 'tsdoc', newKey: '@git.zone/tsdoc' },
|
|
||||||
{ oldKey: 'npmdocker', newKey: '@git.zone/tsdocker' },
|
|
||||||
{ oldKey: 'npmci', newKey: '@ship.zone/szci' },
|
|
||||||
{ oldKey: 'szci', newKey: '@ship.zone/szci' },
|
|
||||||
];
|
|
||||||
for (const { oldKey, newKey } of migrations) {
|
|
||||||
if (npmextraJson[oldKey] && !npmextraJson[newKey]) {
|
|
||||||
npmextraJson[newKey] = npmextraJson[oldKey];
|
|
||||||
delete npmextraJson[oldKey];
|
|
||||||
migrated = true;
|
|
||||||
console.log(`Migrated npmextra.json: ${oldKey} -> ${newKey}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return migrated;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrates npmAccessLevel from @ship.zone/szci to @git.zone/cli.release.accessLevel
|
|
||||||
* This is a one-time migration for projects using the old location
|
|
||||||
*/
|
|
||||||
const migrateAccessLevel = (npmextraJson: any): boolean => {
|
|
||||||
const szciConfig = npmextraJson['@ship.zone/szci'];
|
|
||||||
|
|
||||||
// Check if szci has npmAccessLevel that needs to be migrated
|
|
||||||
if (!szciConfig?.npmAccessLevel) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we already have the new location
|
|
||||||
const gitzoneConfig = npmextraJson['@git.zone/cli'] || {};
|
|
||||||
if (gitzoneConfig?.release?.accessLevel) {
|
|
||||||
// Already migrated, just remove from szci
|
|
||||||
delete szciConfig.npmAccessLevel;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure @git.zone/cli and release exist
|
|
||||||
if (!npmextraJson['@git.zone/cli']) {
|
|
||||||
npmextraJson['@git.zone/cli'] = {};
|
|
||||||
}
|
|
||||||
if (!npmextraJson['@git.zone/cli'].release) {
|
|
||||||
npmextraJson['@git.zone/cli'].release = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate the value
|
|
||||||
npmextraJson['@git.zone/cli'].release.accessLevel = szciConfig.npmAccessLevel;
|
|
||||||
delete szciConfig.npmAccessLevel;
|
|
||||||
|
|
||||||
console.log(`Migrated npmAccessLevel to @git.zone/cli.release.accessLevel`);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* runs the npmextra file checking
|
|
||||||
*/
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
const formatSmartstream = new plugins.smartstream.StreamWrapper([
|
|
||||||
plugins.smartgulp.src([`npmextra.json`]),
|
|
||||||
gulpFunction.forEach(async (fileArg: plugins.smartfile.SmartFile) => {
|
|
||||||
const fileString = fileArg.contents.toString();
|
|
||||||
const npmextraJson = JSON.parse(fileString);
|
|
||||||
|
|
||||||
// Migrate old namespace keys to new package-scoped keys
|
|
||||||
migrateNamespaceKeys(npmextraJson);
|
|
||||||
|
|
||||||
// Migrate npmAccessLevel from szci to @git.zone/cli.release.accessLevel
|
|
||||||
migrateAccessLevel(npmextraJson);
|
|
||||||
|
|
||||||
if (!npmextraJson['@git.zone/cli']) {
|
|
||||||
npmextraJson['@git.zone/cli'] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const expectedRepoInformation: string[] = [
|
|
||||||
'projectType',
|
|
||||||
'module.githost',
|
|
||||||
'module.gitscope',
|
|
||||||
'module.gitrepo',
|
|
||||||
'module.description',
|
|
||||||
'module.npmPackagename',
|
|
||||||
'module.license',
|
|
||||||
];
|
|
||||||
|
|
||||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
|
||||||
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
|
||||||
if (
|
|
||||||
!plugins.smartobject.smartGet(
|
|
||||||
npmextraJson['@git.zone/cli'],
|
|
||||||
expectedRepoInformationItem,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
interactInstance.addQuestions([
|
|
||||||
{
|
|
||||||
message: `What is the value of ${expectedRepoInformationItem}`,
|
|
||||||
name: expectedRepoInformationItem,
|
|
||||||
type: 'input',
|
|
||||||
default: 'undefined variable',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const answerbucket = await interactInstance.runQueue();
|
|
||||||
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
|
||||||
const cliProvidedValue = answerbucket.getAnswerFor(
|
|
||||||
expectedRepoInformationItem,
|
|
||||||
);
|
|
||||||
if (cliProvidedValue) {
|
|
||||||
plugins.smartobject.smartAdd(
|
|
||||||
npmextraJson['@git.zone/cli'],
|
|
||||||
expectedRepoInformationItem,
|
|
||||||
cliProvidedValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete obsolete
|
|
||||||
// tbd
|
|
||||||
|
|
||||||
if (!npmextraJson['@ship.zone/szci']) {
|
|
||||||
npmextraJson['@ship.zone/szci'] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
fileArg.setContentsFromString(JSON.stringify(npmextraJson, null, 2));
|
|
||||||
}),
|
|
||||||
plugins.smartgulp.replace(),
|
|
||||||
]);
|
|
||||||
await formatSmartstream.run().catch((error) => {
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
import * as gulpFunction from '@push.rocks/gulp-function';
|
|
||||||
import { Project } from '../classes.project.js';
|
|
||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ensures a certain dependency
|
|
||||||
*/
|
|
||||||
const ensureDependency = async (
|
|
||||||
packageJsonObjectArg: any,
|
|
||||||
position: 'dep' | 'devDep' | 'everywhere',
|
|
||||||
constraint: 'exclude' | 'include' | 'latest',
|
|
||||||
dependencyArg: string,
|
|
||||||
) => {
|
|
||||||
const [packageName, version] = dependencyArg.includes('@')
|
|
||||||
? dependencyArg.split('@').filter(Boolean)
|
|
||||||
: [dependencyArg, 'latest'];
|
|
||||||
|
|
||||||
const targetSections: string[] = [];
|
|
||||||
|
|
||||||
switch (position) {
|
|
||||||
case 'dep':
|
|
||||||
targetSections.push('dependencies');
|
|
||||||
break;
|
|
||||||
case 'devDep':
|
|
||||||
targetSections.push('devDependencies');
|
|
||||||
break;
|
|
||||||
case 'everywhere':
|
|
||||||
targetSections.push('dependencies', 'devDependencies');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const section of targetSections) {
|
|
||||||
if (!packageJsonObjectArg[section]) {
|
|
||||||
packageJsonObjectArg[section] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (constraint) {
|
|
||||||
case 'exclude':
|
|
||||||
delete packageJsonObjectArg[section][packageName];
|
|
||||||
break;
|
|
||||||
case 'include':
|
|
||||||
if (!packageJsonObjectArg[section][packageName]) {
|
|
||||||
packageJsonObjectArg[section][packageName] =
|
|
||||||
version === 'latest' ? '^1.0.0' : version;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'latest':
|
|
||||||
// Fetch latest version from npm
|
|
||||||
try {
|
|
||||||
const registry = new plugins.smartnpm.NpmRegistry();
|
|
||||||
const packageInfo = await registry.getPackageInfo(packageName);
|
|
||||||
const latestVersion = packageInfo['dist-tags'].latest;
|
|
||||||
packageJsonObjectArg[section][packageName] = `^${latestVersion}`;
|
|
||||||
} catch (error) {
|
|
||||||
logger.log(
|
|
||||||
'warn',
|
|
||||||
`Could not fetch latest version for ${packageName}, using existing or default`,
|
|
||||||
);
|
|
||||||
if (!packageJsonObjectArg[section][packageName]) {
|
|
||||||
packageJsonObjectArg[section][packageName] =
|
|
||||||
version === 'latest' ? '^1.0.0' : version;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
const formatStreamWrapper = new plugins.smartstream.StreamWrapper([
|
|
||||||
plugins.smartgulp.src([`package.json`]),
|
|
||||||
gulpFunction.forEach(async (fileArg: plugins.smartfile.SmartFile) => {
|
|
||||||
const npmextraConfig = new plugins.npmextra.Npmextra(paths.cwd);
|
|
||||||
const gitzoneData: any = npmextraConfig.dataFor('@git.zone/cli', {});
|
|
||||||
const fileString = fileArg.contents.toString();
|
|
||||||
const packageJson = JSON.parse(fileString);
|
|
||||||
|
|
||||||
// metadata
|
|
||||||
packageJson.repository = {
|
|
||||||
type: 'git',
|
|
||||||
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}.git`,
|
|
||||||
};
|
|
||||||
((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`));
|
|
||||||
|
|
||||||
// Check for module type
|
|
||||||
if (!packageJson.type) {
|
|
||||||
logger.log('info', `setting packageJson.type to "module"`);
|
|
||||||
packageJson.type = 'module';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for private or public
|
|
||||||
if (packageJson.private !== undefined) {
|
|
||||||
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!',
|
|
||||||
);
|
|
||||||
packageJson.private = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for license
|
|
||||||
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!',
|
|
||||||
);
|
|
||||||
packageJson.license = 'UNLICENSED';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for build script
|
|
||||||
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!',
|
|
||||||
);
|
|
||||||
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.',
|
|
||||||
);
|
|
||||||
packageJson.scripts.buildDocs = `tsdoc`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for files
|
|
||||||
packageJson.files = [
|
|
||||||
'ts/**/*',
|
|
||||||
'ts_web/**/*',
|
|
||||||
'dist/**/*',
|
|
||||||
'dist_*/**/*',
|
|
||||||
'dist_ts/**/*',
|
|
||||||
'dist_ts_web/**/*',
|
|
||||||
'assets/**/*',
|
|
||||||
'cli.js',
|
|
||||||
'npmextra.json',
|
|
||||||
'readme.md',
|
|
||||||
];
|
|
||||||
|
|
||||||
// check for dependencies
|
|
||||||
// Note: @push.rocks/tapbundle is deprecated - use @git.zone/tstest/tapbundle instead
|
|
||||||
await ensureDependency(
|
|
||||||
packageJson,
|
|
||||||
'devDep',
|
|
||||||
'exclude',
|
|
||||||
'@push.rocks/tapbundle',
|
|
||||||
);
|
|
||||||
await ensureDependency(
|
|
||||||
packageJson,
|
|
||||||
'devDep',
|
|
||||||
'latest',
|
|
||||||
'@git.zone/tstest',
|
|
||||||
);
|
|
||||||
await ensureDependency(
|
|
||||||
packageJson,
|
|
||||||
'devDep',
|
|
||||||
'latest',
|
|
||||||
'@git.zone/tsbuild',
|
|
||||||
);
|
|
||||||
|
|
||||||
// set overrides
|
|
||||||
const overridesContent = (await plugins.smartfs
|
|
||||||
.file(plugins.path.join(paths.assetsDir, 'overrides.json'))
|
|
||||||
.encoding('utf8')
|
|
||||||
.read()) as string;
|
|
||||||
const overrides = JSON.parse(overridesContent);
|
|
||||||
packageJson.pnpm = packageJson.pnpm || {};
|
|
||||||
packageJson.pnpm.overrides = overrides;
|
|
||||||
|
|
||||||
// exclude
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
fileArg.setContentsFromString(JSON.stringify(packageJson, null, 2));
|
|
||||||
}),
|
|
||||||
plugins.smartgulp.replace(),
|
|
||||||
]);
|
|
||||||
await formatStreamWrapper.run().catch((error) => {
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import prettier from 'prettier';
|
|
||||||
import { Project } from '../classes.project.js';
|
|
||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
|
||||||
|
|
||||||
const prettierDefaultTypeScriptConfig: prettier.Options = {
|
|
||||||
printWidth: 100,
|
|
||||||
parser: 'typescript',
|
|
||||||
singleQuote: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const prettierDefaultMarkdownConfig: prettier.Options = {
|
|
||||||
singleQuote: true,
|
|
||||||
printWidth: 100,
|
|
||||||
parser: 'markdown',
|
|
||||||
};
|
|
||||||
|
|
||||||
const filesToFormat = [
|
|
||||||
`ts/**/*.ts`,
|
|
||||||
`test/**/*.ts`,
|
|
||||||
`readme.md`,
|
|
||||||
`docs/**/*.md`,
|
|
||||||
];
|
|
||||||
|
|
||||||
const choosePrettierConfig = (fileArg: plugins.smartfile.SmartFile) => {
|
|
||||||
switch (fileArg.parsedPath.ext) {
|
|
||||||
case '.ts':
|
|
||||||
return prettierDefaultTypeScriptConfig;
|
|
||||||
case '.md':
|
|
||||||
return prettierDefaultMarkdownConfig;
|
|
||||||
default:
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const prettierTypeScriptPipestop = plugins.through2.obj(
|
|
||||||
async (fileArg: plugins.smartfile.SmartFile, enc, cb) => {
|
|
||||||
const fileString = fileArg.contentBuffer.toString();
|
|
||||||
const chosenConfig = choosePrettierConfig(fileArg);
|
|
||||||
const filePasses = await prettier.check(fileString, chosenConfig);
|
|
||||||
if (filePasses) {
|
|
||||||
logger.log('info', `OK! -> ${fileArg.path} passes!`);
|
|
||||||
cb(null);
|
|
||||||
} else {
|
|
||||||
logger.log('info', `${fileArg.path} is being reformated!`);
|
|
||||||
const formatedFileString = await prettier.format(
|
|
||||||
fileString,
|
|
||||||
chosenConfig,
|
|
||||||
);
|
|
||||||
fileArg.setContentsFromString(formatedFileString);
|
|
||||||
cb(null, fileArg);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
const formatStreamWrapper = new plugins.smartstream.StreamWrapper([
|
|
||||||
plugins.smartgulp.src(filesToFormat),
|
|
||||||
prettierTypeScriptPipestop,
|
|
||||||
plugins.smartgulp.replace(),
|
|
||||||
]);
|
|
||||||
await formatStreamWrapper.run().catch((error) => {
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
|
|
||||||
export const run = async () => {
|
|
||||||
const readmePath = plugins.path.join(paths.cwd, 'readme.md');
|
|
||||||
const readmeHintsPath = plugins.path.join(paths.cwd, 'readme.hints.md');
|
|
||||||
|
|
||||||
// Check and initialize readme.md if it doesn't exist
|
|
||||||
const readmeExists = await plugins.smartfs.file(readmePath).exists();
|
|
||||||
if (!readmeExists) {
|
|
||||||
await plugins.smartfs.file(readmePath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.write('# Project Readme\n\nThis is the initial readme file.');
|
|
||||||
console.log('Initialized readme.md');
|
|
||||||
} else {
|
|
||||||
console.log('readme.md already exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check and initialize readme.hints.md if it doesn't exist
|
|
||||||
const readmeHintsExists = await plugins.smartfs.file(readmeHintsPath).exists();
|
|
||||||
if (!readmeHintsExists) {
|
|
||||||
await plugins.smartfs.file(readmeHintsPath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.write('# Project Readme Hints\n\nThis is the initial readme hints file.');
|
|
||||||
console.log('Initialized readme.hints.md');
|
|
||||||
} else {
|
|
||||||
console.log('readme.hints.md already exists');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
|
||||||
import { Project } from '../classes.project.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* takes care of updating files from templates
|
|
||||||
*/
|
|
||||||
export const run = async (project: Project) => {
|
|
||||||
const templateModule = await import('../mod_template/index.js');
|
|
||||||
|
|
||||||
// update vscode
|
|
||||||
const vscodeTemplate = await templateModule.getTemplate('vscode');
|
|
||||||
await vscodeTemplate.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', `Updated vscode template!`);
|
|
||||||
|
|
||||||
// update gitlab ci and Dockerfile
|
|
||||||
switch (project.gitzoneConfig.data.projectType) {
|
|
||||||
case 'npm':
|
|
||||||
case 'wcc':
|
|
||||||
if (project.gitzoneConfig.data.npmciOptions.npmAccessLevel === 'public') {
|
|
||||||
const ciTemplateDefault =
|
|
||||||
await templateModule.getTemplate('ci_default');
|
|
||||||
ciTemplateDefault.writeToDisk(paths.cwd);
|
|
||||||
} else {
|
|
||||||
const ciTemplateDefault =
|
|
||||||
await templateModule.getTemplate('ci_default_private');
|
|
||||||
ciTemplateDefault.writeToDisk(paths.cwd);
|
|
||||||
}
|
|
||||||
logger.log('info', 'Updated .gitlabci.yml!');
|
|
||||||
break;
|
|
||||||
case 'service':
|
|
||||||
case 'website':
|
|
||||||
const ciTemplateDocker = await templateModule.getTemplate('ci_docker');
|
|
||||||
await ciTemplateDocker.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', 'Updated CI/CD config files!');
|
|
||||||
|
|
||||||
// lets care about docker
|
|
||||||
const dockerTemplate =
|
|
||||||
await templateModule.getTemplate('dockerfile_service');
|
|
||||||
dockerTemplate.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', 'Updated Dockerfile!');
|
|
||||||
|
|
||||||
// lets care about cli
|
|
||||||
const cliTemplate = await templateModule.getTemplate('cli');
|
|
||||||
await cliTemplate.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', 'Updated cli.ts.js and cli.js!');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update html
|
|
||||||
if (project.gitzoneConfig.data.projectType === 'website') {
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
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');
|
|
||||||
await websiteUpdateTemplate.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', `Updated html for element template!`);
|
|
||||||
} else if (project.gitzoneConfig.data.projectType === 'wcc') {
|
|
||||||
const wccUpdateTemplate = await templateModule.getTemplate('wcc_update');
|
|
||||||
await wccUpdateTemplate.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', `Updated html for wcc template!`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
|
||||||
import { Project } from '../classes.project.js';
|
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
// lets care about tsconfig.json
|
|
||||||
logger.log('info', 'Formatting tsconfig.json...');
|
|
||||||
const factory = plugins.smartfile.SmartFileFactory.nodeFs();
|
|
||||||
const tsconfigSmartfile = await factory.fromFilePath(
|
|
||||||
plugins.path.join(paths.cwd, 'tsconfig.json'),
|
|
||||||
);
|
|
||||||
const tsconfigObject = JSON.parse(tsconfigSmartfile.parseContentAsString());
|
|
||||||
tsconfigObject.compilerOptions = tsconfigObject.compilerOptions || {};
|
|
||||||
tsconfigObject.compilerOptions.baseUrl = '.';
|
|
||||||
tsconfigObject.compilerOptions.paths = {};
|
|
||||||
const tsPublishMod = await import('@git.zone/tspublish');
|
|
||||||
const tsPublishInstance = new tsPublishMod.TsPublish();
|
|
||||||
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`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
await tsconfigSmartfile.editContentAsString(async () => {
|
|
||||||
return JSON.stringify(tsconfigObject, null, 2);
|
|
||||||
});
|
|
||||||
await tsconfigSmartfile.write();
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import type { IPlannedChange } from '../interfaces.format.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
import * as plugins from '../mod.plugins.js';
|
import * as plugins from '../mod.plugins.js';
|
||||||
import * as cleanupFormatter from '../format.cleanup.js';
|
|
||||||
|
|
||||||
export class CleanupFormatter extends BaseFormatter {
|
export class CleanupFormatter extends BaseFormatter {
|
||||||
get name(): string {
|
get name(): string {
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ export class CopyFormatter extends BaseFormatter {
|
|||||||
async analyze(): Promise<IPlannedChange[]> {
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
const changes: IPlannedChange[] = [];
|
const changes: IPlannedChange[] = [];
|
||||||
|
|
||||||
// Get copy configuration from npmextra.json
|
// Get copy configuration from .smartconfig.json
|
||||||
const npmextraConfig = new plugins.npmextra.Npmextra();
|
const smartconfigInstance = new plugins.smartconfig.Smartconfig();
|
||||||
const copyConfig = npmextraConfig.dataFor<{ patterns: ICopyPattern[] }>(
|
const copyConfig = smartconfigInstance.dataFor<{ patterns: ICopyPattern[] }>(
|
||||||
'gitzone.format.copy',
|
'gitzone.format.copy',
|
||||||
{ patterns: [] },
|
{ patterns: [] },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!copyConfig.patterns || copyConfig.patterns.length === 0) {
|
if (!copyConfig.patterns || copyConfig.patterns.length === 0) {
|
||||||
logVerbose('No copy patterns configured in npmextra.json');
|
logVerbose('No copy patterns configured in .smartconfig.json');
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,10 +103,6 @@ export class CopyFormatter extends BaseFormatter {
|
|||||||
async applyChange(change: IPlannedChange): Promise<void> {
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
if (!change.content) return;
|
if (!change.content) return;
|
||||||
|
|
||||||
// Ensure destination directory exists
|
|
||||||
const destDir = plugins.path.dirname(change.path);
|
|
||||||
await plugins.smartfs.directory(destDir).recursive().create();
|
|
||||||
|
|
||||||
if (change.type === 'create') {
|
if (change.type === 'create') {
|
||||||
await this.createFile(change.path, change.content);
|
await this.createFile(change.path, change.content);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,42 +1,39 @@
|
|||||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import type { IPlannedChange } from '../interfaces.format.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
import * as plugins from '../mod.plugins.js';
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import * as paths from '../../paths.js';
|
||||||
import { logger } from '../../gitzone.logging.js';
|
import { logger } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
// Standard gitignore template content (without front-matter)
|
|
||||||
const GITIGNORE_TEMPLATE = `.nogit/
|
|
||||||
|
|
||||||
# artifacts
|
|
||||||
coverage/
|
|
||||||
public/
|
|
||||||
|
|
||||||
# installs
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# caches
|
|
||||||
.yarn/
|
|
||||||
.cache/
|
|
||||||
.rpt2_cache
|
|
||||||
|
|
||||||
# builds
|
|
||||||
dist/
|
|
||||||
dist_*/
|
|
||||||
|
|
||||||
# AI
|
|
||||||
.claude/
|
|
||||||
.serena/
|
|
||||||
|
|
||||||
#------# custom`;
|
|
||||||
|
|
||||||
export class GitignoreFormatter extends BaseFormatter {
|
export class GitignoreFormatter extends BaseFormatter {
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return 'gitignore';
|
return 'gitignore';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the standard gitignore template from the asset file,
|
||||||
|
* stripping the YAML frontmatter.
|
||||||
|
*/
|
||||||
|
private async getStandardTemplate(): Promise<string> {
|
||||||
|
const templatePath = plugins.path.join(paths.templatesDir, 'gitignore', '_gitignore');
|
||||||
|
const raw = (await plugins.smartfs
|
||||||
|
.file(templatePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
|
// Strip YAML frontmatter (---\n...\n---)
|
||||||
|
const frontmatterEnd = raw.indexOf('---', 3);
|
||||||
|
if (frontmatterEnd !== -1) {
|
||||||
|
return raw.slice(frontmatterEnd + 3).trimStart();
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
async analyze(): Promise<IPlannedChange[]> {
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
const changes: IPlannedChange[] = [];
|
const changes: IPlannedChange[] = [];
|
||||||
const gitignorePath = '.gitignore';
|
const gitignorePath = '.gitignore';
|
||||||
|
|
||||||
|
const standardTemplate = await this.getStandardTemplate();
|
||||||
|
|
||||||
// Check if file exists and extract custom content
|
// Check if file exists and extract custom content
|
||||||
let customContent = '';
|
let customContent = '';
|
||||||
const exists = await plugins.smartfs.file(gitignorePath).exists();
|
const exists = await plugins.smartfs.file(gitignorePath).exists();
|
||||||
@@ -59,11 +56,11 @@ export class GitignoreFormatter extends BaseFormatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compute new content
|
// Compute new content
|
||||||
let newContent = GITIGNORE_TEMPLATE;
|
let newContent = standardTemplate;
|
||||||
if (customContent) {
|
if (customContent) {
|
||||||
newContent = GITIGNORE_TEMPLATE + '\n' + customContent + '\n';
|
newContent = standardTemplate + '\n' + customContent + '\n';
|
||||||
} else {
|
} else {
|
||||||
newContent = GITIGNORE_TEMPLATE + '\n';
|
newContent = standardTemplate + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read current content to compare
|
// Read current content to compare
|
||||||
@@ -75,7 +72,6 @@ export class GitignoreFormatter extends BaseFormatter {
|
|||||||
.read()) as string;
|
.read()) as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine change type
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
changes.push({
|
changes.push({
|
||||||
type: 'create',
|
type: 'create',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import type { IPlannedChange } from '../interfaces.format.js';
|
import type { IFormatWarning, IPlannedChange } from '../interfaces.format.js';
|
||||||
import * as plugins from '../mod.plugins.js';
|
import * as plugins from '../mod.plugins.js';
|
||||||
import * as paths from '../../paths.js';
|
import * as paths from '../../paths.js';
|
||||||
import { logger } from '../../gitzone.logging.js';
|
import { logger } from '../../gitzone.logging.js';
|
||||||
@@ -11,6 +11,10 @@ export class LicenseFormatter extends BaseFormatter {
|
|||||||
return 'license';
|
return 'license';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get runsWithoutChanges(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async analyze(): Promise<IPlannedChange[]> {
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
// License formatter only checks for incompatible licenses
|
// License formatter only checks for incompatible licenses
|
||||||
// It does not modify any files, so return empty array
|
// It does not modify any files, so return empty array
|
||||||
@@ -18,29 +22,34 @@ export class LicenseFormatter extends BaseFormatter {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async validate(): Promise<IFormatWarning[]> {
|
||||||
|
const result = await this.checkLicenses();
|
||||||
|
if (!result || result.failingModules.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
level: 'error',
|
||||||
|
module: this.name,
|
||||||
|
message: `License check failed for ${result.failingModules.length} module(s): ${result.failingModules
|
||||||
|
.map((failedModule) => `${failedModule.name} (${failedModule.license})`)
|
||||||
|
.join(', ')}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
async execute(changes: IPlannedChange[]): Promise<void> {
|
async execute(changes: IPlannedChange[]): Promise<void> {
|
||||||
const startTime = this.stats.moduleStartTime(this.name);
|
const startTime = this.stats.moduleStartTime(this.name);
|
||||||
this.stats.startModule(this.name);
|
this.stats.startModule(this.name);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if node_modules exists
|
const licenseCheckResult = await this.checkLicenses();
|
||||||
const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
|
if (!licenseCheckResult) {
|
||||||
const nodeModulesExists = await plugins.smartfs
|
|
||||||
.directory(nodeModulesPath)
|
|
||||||
.exists();
|
|
||||||
|
|
||||||
if (!nodeModulesExists) {
|
|
||||||
logger.log('warn', 'No node_modules found. Skipping license check');
|
logger.log('warn', 'No node_modules found. Skipping license check');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run license check
|
|
||||||
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
|
|
||||||
const licenseCheckResult = await licenseChecker.excludeLicenseWithinPath(
|
|
||||||
paths.cwd,
|
|
||||||
INCOMPATIBLE_LICENSES,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (licenseCheckResult.failingModules.length === 0) {
|
if (licenseCheckResult.failingModules.length === 0) {
|
||||||
logger.log('info', 'License check passed - no incompatible licenses found');
|
logger.log('info', 'License check passed - no incompatible licenses found');
|
||||||
} else {
|
} else {
|
||||||
@@ -59,4 +68,23 @@ export class LicenseFormatter extends BaseFormatter {
|
|||||||
async applyChange(change: IPlannedChange): Promise<void> {
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
// No file changes for license formatter
|
// No file changes for license formatter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async checkLicenses(): Promise<{
|
||||||
|
failingModules: Array<{ name: string; license: string }>;
|
||||||
|
} | undefined> {
|
||||||
|
const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
|
||||||
|
const nodeModulesExists = await plugins.smartfs
|
||||||
|
.directory(nodeModulesPath)
|
||||||
|
.exists();
|
||||||
|
|
||||||
|
if (!nodeModulesExists) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
|
||||||
|
return await licenseChecker.excludeLicenseWithinPath(
|
||||||
|
paths.cwd,
|
||||||
|
INCOMPATIBLE_LICENSES,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
|
||||||
import type { IPlannedChange } from '../interfaces.format.js';
|
|
||||||
import * as plugins from '../mod.plugins.js';
|
|
||||||
import { logger, logVerbose } from '../../gitzone.logging.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrates npmextra.json from old namespace keys to new package-scoped keys
|
|
||||||
*/
|
|
||||||
const migrateNamespaceKeys = (npmextraJson: any): boolean => {
|
|
||||||
let migrated = false;
|
|
||||||
const migrations = [
|
|
||||||
{ oldKey: 'gitzone', newKey: '@git.zone/cli' },
|
|
||||||
{ oldKey: 'tsdoc', newKey: '@git.zone/tsdoc' },
|
|
||||||
{ oldKey: 'npmdocker', newKey: '@git.zone/tsdocker' },
|
|
||||||
{ oldKey: 'npmci', newKey: '@ship.zone/szci' },
|
|
||||||
{ oldKey: 'szci', newKey: '@ship.zone/szci' },
|
|
||||||
];
|
|
||||||
for (const { oldKey, newKey } of migrations) {
|
|
||||||
if (npmextraJson[oldKey] && !npmextraJson[newKey]) {
|
|
||||||
npmextraJson[newKey] = npmextraJson[oldKey];
|
|
||||||
delete npmextraJson[oldKey];
|
|
||||||
migrated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return migrated;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrates npmAccessLevel from @ship.zone/szci to @git.zone/cli.release.accessLevel
|
|
||||||
*/
|
|
||||||
const migrateAccessLevel = (npmextraJson: any): boolean => {
|
|
||||||
const szciConfig = npmextraJson['@ship.zone/szci'];
|
|
||||||
|
|
||||||
if (!szciConfig?.npmAccessLevel) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gitzoneConfig = npmextraJson['@git.zone/cli'] || {};
|
|
||||||
if (gitzoneConfig?.release?.accessLevel) {
|
|
||||||
delete szciConfig.npmAccessLevel;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!npmextraJson['@git.zone/cli']) {
|
|
||||||
npmextraJson['@git.zone/cli'] = {};
|
|
||||||
}
|
|
||||||
if (!npmextraJson['@git.zone/cli'].release) {
|
|
||||||
npmextraJson['@git.zone/cli'].release = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
npmextraJson['@git.zone/cli'].release.accessLevel = szciConfig.npmAccessLevel;
|
|
||||||
delete szciConfig.npmAccessLevel;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class NpmextraFormatter extends BaseFormatter {
|
|
||||||
get name(): string {
|
|
||||||
return 'npmextra';
|
|
||||||
}
|
|
||||||
|
|
||||||
async analyze(): Promise<IPlannedChange[]> {
|
|
||||||
const changes: IPlannedChange[] = [];
|
|
||||||
const npmextraPath = 'npmextra.json';
|
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
const exists = await plugins.smartfs.file(npmextraPath).exists();
|
|
||||||
if (!exists) {
|
|
||||||
logVerbose('npmextra.json does not exist, skipping');
|
|
||||||
return changes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read current content
|
|
||||||
const currentContent = (await plugins.smartfs
|
|
||||||
.file(npmextraPath)
|
|
||||||
.encoding('utf8')
|
|
||||||
.read()) as string;
|
|
||||||
|
|
||||||
// Parse and compute new content
|
|
||||||
const npmextraJson = JSON.parse(currentContent);
|
|
||||||
|
|
||||||
// Apply migrations (these are automatic, non-interactive)
|
|
||||||
migrateNamespaceKeys(npmextraJson);
|
|
||||||
migrateAccessLevel(npmextraJson);
|
|
||||||
|
|
||||||
// Ensure namespaces exist
|
|
||||||
if (!npmextraJson['@git.zone/cli']) {
|
|
||||||
npmextraJson['@git.zone/cli'] = {};
|
|
||||||
}
|
|
||||||
if (!npmextraJson['@ship.zone/szci']) {
|
|
||||||
npmextraJson['@ship.zone/szci'] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const newContent = JSON.stringify(npmextraJson, null, 2);
|
|
||||||
|
|
||||||
// Only add change if content differs
|
|
||||||
if (newContent !== currentContent) {
|
|
||||||
changes.push({
|
|
||||||
type: 'modify',
|
|
||||||
path: npmextraPath,
|
|
||||||
module: this.name,
|
|
||||||
description: 'Migrate and format npmextra.json',
|
|
||||||
content: newContent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return changes;
|
|
||||||
}
|
|
||||||
|
|
||||||
async applyChange(change: IPlannedChange): Promise<void> {
|
|
||||||
if (change.type !== 'modify' || !change.content) return;
|
|
||||||
|
|
||||||
// Parse the content to check for missing required fields
|
|
||||||
const npmextraJson = JSON.parse(change.content);
|
|
||||||
|
|
||||||
// Check for missing required module information
|
|
||||||
const expectedRepoInformation: string[] = [
|
|
||||||
'projectType',
|
|
||||||
'module.githost',
|
|
||||||
'module.gitscope',
|
|
||||||
'module.gitrepo',
|
|
||||||
'module.description',
|
|
||||||
'module.npmPackagename',
|
|
||||||
'module.license',
|
|
||||||
];
|
|
||||||
|
|
||||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
|
||||||
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
|
||||||
if (
|
|
||||||
!plugins.smartobject.smartGet(
|
|
||||||
npmextraJson['@git.zone/cli'],
|
|
||||||
expectedRepoInformationItem,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
interactInstance.addQuestions([
|
|
||||||
{
|
|
||||||
message: `What is the value of ${expectedRepoInformationItem}`,
|
|
||||||
name: expectedRepoInformationItem,
|
|
||||||
type: 'input',
|
|
||||||
default: 'undefined variable',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const answerbucket = await interactInstance.runQueue();
|
|
||||||
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
|
||||||
const cliProvidedValue = answerbucket.getAnswerFor(
|
|
||||||
expectedRepoInformationItem,
|
|
||||||
);
|
|
||||||
if (cliProvidedValue) {
|
|
||||||
plugins.smartobject.smartAdd(
|
|
||||||
npmextraJson['@git.zone/cli'],
|
|
||||||
expectedRepoInformationItem,
|
|
||||||
cliProvidedValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the final content
|
|
||||||
const finalContent = JSON.stringify(npmextraJson, null, 2);
|
|
||||||
await this.modifyFile(change.path, finalContent);
|
|
||||||
logger.log('info', 'Updated npmextra.json');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,77 +4,6 @@ import * as plugins from '../mod.plugins.js';
|
|||||||
import * as paths from '../../paths.js';
|
import * as paths from '../../paths.js';
|
||||||
import { logger, logVerbose } from '../../gitzone.logging.js';
|
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensures a certain dependency exists or is excluded
|
|
||||||
*/
|
|
||||||
const ensureDependency = async (
|
|
||||||
packageJsonObject: any,
|
|
||||||
position: 'dep' | 'devDep' | 'everywhere',
|
|
||||||
constraint: 'exclude' | 'include' | 'latest',
|
|
||||||
dependencyArg: string,
|
|
||||||
): Promise<void> => {
|
|
||||||
// Parse package name and version, handling scoped packages like @scope/name@version
|
|
||||||
const isScoped = dependencyArg.startsWith('@');
|
|
||||||
const lastAtIndex = dependencyArg.lastIndexOf('@');
|
|
||||||
|
|
||||||
// For scoped packages, the version @ must come after the /
|
|
||||||
// For unscoped packages, any @ indicates a version
|
|
||||||
const hasVersion = isScoped
|
|
||||||
? lastAtIndex > dependencyArg.indexOf('/')
|
|
||||||
: lastAtIndex >= 0;
|
|
||||||
|
|
||||||
const packageName = hasVersion ? dependencyArg.slice(0, lastAtIndex) : dependencyArg;
|
|
||||||
const version = hasVersion ? dependencyArg.slice(lastAtIndex + 1) : 'latest';
|
|
||||||
|
|
||||||
const targetSections: string[] = [];
|
|
||||||
|
|
||||||
switch (position) {
|
|
||||||
case 'dep':
|
|
||||||
targetSections.push('dependencies');
|
|
||||||
break;
|
|
||||||
case 'devDep':
|
|
||||||
targetSections.push('devDependencies');
|
|
||||||
break;
|
|
||||||
case 'everywhere':
|
|
||||||
targetSections.push('dependencies', 'devDependencies');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const section of targetSections) {
|
|
||||||
if (!packageJsonObject[section]) {
|
|
||||||
packageJsonObject[section] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (constraint) {
|
|
||||||
case 'exclude':
|
|
||||||
delete packageJsonObject[section][packageName];
|
|
||||||
break;
|
|
||||||
case 'include':
|
|
||||||
if (!packageJsonObject[section][packageName]) {
|
|
||||||
packageJsonObject[section][packageName] =
|
|
||||||
version === 'latest' ? '^1.0.0' : version;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'latest':
|
|
||||||
try {
|
|
||||||
const registry = new plugins.smartnpm.NpmRegistry();
|
|
||||||
const packageInfo = await registry.getPackageInfo(packageName);
|
|
||||||
const latestVersion = packageInfo['dist-tags'].latest;
|
|
||||||
packageJsonObject[section][packageName] = `^${latestVersion}`;
|
|
||||||
} catch (error) {
|
|
||||||
logVerbose(
|
|
||||||
`Could not fetch latest version for ${packageName}, using existing or default`,
|
|
||||||
);
|
|
||||||
if (!packageJsonObject[section][packageName]) {
|
|
||||||
packageJsonObject[section][packageName] =
|
|
||||||
version === 'latest' ? '^1.0.0' : version;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export class PackageJsonFormatter extends BaseFormatter {
|
export class PackageJsonFormatter extends BaseFormatter {
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return 'packagejson';
|
return 'packagejson';
|
||||||
@@ -100,9 +29,9 @@ export class PackageJsonFormatter extends BaseFormatter {
|
|||||||
// Parse and compute new content
|
// Parse and compute new content
|
||||||
const packageJson = JSON.parse(currentContent);
|
const packageJson = JSON.parse(currentContent);
|
||||||
|
|
||||||
// Get gitzone config from npmextra
|
// Get gitzone config from smartconfig
|
||||||
const npmextraConfig = new plugins.npmextra.Npmextra(paths.cwd);
|
const smartconfigInstance = new plugins.smartconfig.Smartconfig(paths.cwd);
|
||||||
const gitzoneData: any = npmextraConfig.dataFor('@git.zone/cli', {});
|
const gitzoneData: any = smartconfigInstance.dataFor('@git.zone/cli', {});
|
||||||
|
|
||||||
// Set metadata from gitzone config
|
// Set metadata from gitzone config
|
||||||
if (gitzoneData.module) {
|
if (gitzoneData.module) {
|
||||||
@@ -141,11 +70,6 @@ export class PackageJsonFormatter extends BaseFormatter {
|
|||||||
packageJson.scripts.build = `echo "Not needed for now"`;
|
packageJson.scripts.build = `echo "Not needed for now"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure buildDocs script exists
|
|
||||||
if (!packageJson.scripts.buildDocs) {
|
|
||||||
packageJson.scripts.buildDocs = `tsdoc`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set files array
|
// Set files array
|
||||||
packageJson.files = [
|
packageJson.files = [
|
||||||
'ts/**/*',
|
'ts/**/*',
|
||||||
@@ -156,25 +80,10 @@ export class PackageJsonFormatter extends BaseFormatter {
|
|||||||
'dist_ts_web/**/*',
|
'dist_ts_web/**/*',
|
||||||
'assets/**/*',
|
'assets/**/*',
|
||||||
'cli.js',
|
'cli.js',
|
||||||
'npmextra.json',
|
'.smartconfig.json',
|
||||||
'readme.md',
|
'readme.md',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Handle dependencies
|
|
||||||
await ensureDependency(
|
|
||||||
packageJson,
|
|
||||||
'devDep',
|
|
||||||
'exclude',
|
|
||||||
'@push.rocks/tapbundle',
|
|
||||||
);
|
|
||||||
await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tstest');
|
|
||||||
await ensureDependency(
|
|
||||||
packageJson,
|
|
||||||
'devDep',
|
|
||||||
'latest',
|
|
||||||
'@git.zone/tsbuild',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set pnpm overrides from assets
|
// Set pnpm overrides from assets
|
||||||
try {
|
try {
|
||||||
const overridesContent = (await plugins.smartfs
|
const overridesContent = (await plugins.smartfs
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
const rootConfigFiles = [
|
const rootConfigFiles = [
|
||||||
'package.json',
|
'package.json',
|
||||||
'tsconfig.json',
|
'tsconfig.json',
|
||||||
'npmextra.json',
|
'.smartconfig.json',
|
||||||
'.prettierrc',
|
'.prettierrc',
|
||||||
'.prettierrc.json',
|
'.prettierrc.json',
|
||||||
'.prettierrc.js',
|
'.prettierrc.js',
|
||||||
@@ -56,7 +56,8 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
);
|
);
|
||||||
allFiles.push(...filteredFiles);
|
allFiles.push(...filteredFiles);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logVerbose(`Skipping directory ${dir}: ${error.message}`);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logVerbose(`Skipping directory ${dir}: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,19 +73,17 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
|
const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
|
||||||
allFiles.push(...rootLevelFiles);
|
allFiles.push(...rootLevelFiles);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logVerbose(`Skipping pattern ${pattern}: ${error.message}`);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logVerbose(`Skipping pattern ${pattern}: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove duplicates
|
// Remove duplicates
|
||||||
const uniqueFiles = [...new Set(allFiles)];
|
const uniqueFiles = [...new Set(allFiles)];
|
||||||
|
|
||||||
// Get all files that match the pattern
|
|
||||||
const files = uniqueFiles;
|
|
||||||
|
|
||||||
// Ensure we only process actual files (not directories)
|
// Ensure we only process actual files (not directories)
|
||||||
const validFiles: string[] = [];
|
const validFiles: string[] = [];
|
||||||
for (const file of files) {
|
for (const file of uniqueFiles) {
|
||||||
try {
|
try {
|
||||||
const stats = await plugins.smartfs.file(file).stat();
|
const stats = await plugins.smartfs.file(file).stat();
|
||||||
if (!stats.isDirectory) {
|
if (!stats.isDirectory) {
|
||||||
@@ -92,27 +91,46 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Skip files that can't be accessed
|
// Skip files that can't be accessed
|
||||||
logVerbose(`Skipping ${file} - cannot access: ${error.message}`);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logVerbose(`Skipping ${file} - cannot access: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check which files need formatting
|
const prettier = await import('prettier');
|
||||||
|
const prettierConfig = await this.getPrettierConfig();
|
||||||
|
|
||||||
for (const file of validFiles) {
|
for (const file of validFiles) {
|
||||||
// Skip files that haven't changed
|
try {
|
||||||
if (!(await this.shouldProcessFile(file))) {
|
const fileExt = plugins.path.extname(file).toLowerCase();
|
||||||
logVerbose(`Skipping ${file} - no changes detected`);
|
if (!fileExt) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
changes.push({
|
const content = (await plugins.smartfs
|
||||||
type: 'modify',
|
.file(file)
|
||||||
path: file,
|
.encoding('utf8')
|
||||||
module: this.name,
|
.read()) as string;
|
||||||
description: 'Format with Prettier',
|
const formatted = await prettier.format(content, {
|
||||||
});
|
filepath: file,
|
||||||
|
...prettierConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (formatted !== content) {
|
||||||
|
changes.push({
|
||||||
|
type: 'modify',
|
||||||
|
path: file,
|
||||||
|
module: this.name,
|
||||||
|
description: 'Format with Prettier',
|
||||||
|
content: formatted,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logVerbose(`Skipping Prettier analysis for ${file}: ${errorMessage}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('info', `Found ${changes.length} files to format with Prettier`);
|
logger.log('info', `Found ${changes.length} files needing Prettier`);
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,9 +155,10 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
this.stats.recordFileOperation(this.name, change.type, true);
|
this.stats.recordFileOperation(this.name, change.type, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.stats.recordFileOperation(this.name, change.type, false);
|
this.stats.recordFileOperation(this.name, change.type, false);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
logger.log(
|
logger.log(
|
||||||
'error',
|
'error',
|
||||||
`Failed to format ${change.path}: ${error.message}`,
|
`Failed to format ${change.path}: ${errorMessage}`,
|
||||||
);
|
);
|
||||||
// Don't throw - continue with other files
|
// Don't throw - continue with other files
|
||||||
}
|
}
|
||||||
@@ -202,28 +221,32 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
logVerbose(`No formatting changes for ${change.path}`);
|
logVerbose(`No formatting changes for ${change.path}`);
|
||||||
}
|
}
|
||||||
} catch (prettierError) {
|
} catch (prettierError) {
|
||||||
|
const prettierErrorMessage = prettierError instanceof Error
|
||||||
|
? prettierError.message
|
||||||
|
: String(prettierError);
|
||||||
// Check if it's a parser error
|
// Check if it's a parser error
|
||||||
if (
|
if (prettierErrorMessage.includes('No parser could be inferred')) {
|
||||||
prettierError.message &&
|
logVerbose(`Skipping ${change.path} - ${prettierErrorMessage}`);
|
||||||
prettierError.message.includes('No parser could be inferred')
|
|
||||||
) {
|
|
||||||
logVerbose(`Skipping ${change.path} - ${prettierError.message}`);
|
|
||||||
return; // Skip this file silently
|
return; // Skip this file silently
|
||||||
}
|
}
|
||||||
throw prettierError;
|
throw prettierError;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
const errorStack = error instanceof Error ? error.stack : undefined;
|
||||||
// Log the full error stack for debugging mkdir issues
|
// Log the full error stack for debugging mkdir issues
|
||||||
if (error.message && error.message.includes('mkdir')) {
|
if (errorMessage.includes('mkdir')) {
|
||||||
logger.log(
|
logger.log(
|
||||||
'error',
|
'error',
|
||||||
`Failed to format ${change.path}: ${error.message}`,
|
`Failed to format ${change.path}: ${errorMessage}`,
|
||||||
);
|
);
|
||||||
logger.log('error', `Error stack: ${error.stack}`);
|
if (errorStack) {
|
||||||
|
logger.log('error', `Error stack: ${errorStack}`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.log(
|
logger.log(
|
||||||
'error',
|
'error',
|
||||||
`Failed to format ${change.path}: ${error.message}`,
|
`Failed to format ${change.path}: ${errorMessage}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@@ -232,7 +255,7 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
|
|
||||||
private async getPrettierConfig(): Promise<any> {
|
private async getPrettierConfig(): Promise<any> {
|
||||||
// Try to load prettier config from the project
|
// Try to load prettier config from the project
|
||||||
const prettierConfig = new plugins.npmextra.Npmextra();
|
const prettierConfig = new plugins.smartconfig.Smartconfig();
|
||||||
return prettierConfig.dataFor('prettier', {
|
return prettierConfig.dataFor('prettier', {
|
||||||
// Default prettier config
|
// Default prettier config
|
||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
@@ -244,52 +267,7 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Override check() to compute diffs on-the-fly by running prettier
|
|
||||||
*/
|
|
||||||
async check(): Promise<ICheckResult> {
|
async check(): Promise<ICheckResult> {
|
||||||
const changes = await this.analyze();
|
return await super.check();
|
||||||
const diffs: ICheckResult['diffs'] = [];
|
|
||||||
|
|
||||||
for (const change of changes) {
|
|
||||||
if (change.type !== 'modify') continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Read current content
|
|
||||||
const currentContent = (await plugins.smartfs
|
|
||||||
.file(change.path)
|
|
||||||
.encoding('utf8')
|
|
||||||
.read()) as string;
|
|
||||||
|
|
||||||
// Skip files without extension (prettier can't infer parser)
|
|
||||||
const fileExt = plugins.path.extname(change.path).toLowerCase();
|
|
||||||
if (!fileExt) continue;
|
|
||||||
|
|
||||||
// Format with prettier to get what it would produce
|
|
||||||
const prettier = await import('prettier');
|
|
||||||
const formatted = await prettier.format(currentContent, {
|
|
||||||
filepath: change.path,
|
|
||||||
...(await this.getPrettierConfig()),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only add to diffs if content differs
|
|
||||||
if (formatted !== currentContent) {
|
|
||||||
diffs.push({
|
|
||||||
path: change.path,
|
|
||||||
type: 'modify',
|
|
||||||
before: currentContent,
|
|
||||||
after: formatted,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Skip files that can't be processed
|
|
||||||
logVerbose(`Skipping diff for ${change.path}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasDiff: diffs.length > 0,
|
|
||||||
diffs,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { BaseFormatter } from "../classes.baseformatter.js";
|
||||||
|
import type { IPlannedChange } from "../interfaces.format.js";
|
||||||
|
import * as plugins from "../mod.plugins.js";
|
||||||
|
import { logger, logVerbose } from "../../gitzone.logging.js";
|
||||||
|
import { migrateSmartconfigData } from "../../helpers.smartconfigmigrations.js";
|
||||||
|
|
||||||
|
const CONFIG_FILE = ".smartconfig.json";
|
||||||
|
|
||||||
|
export class SmartconfigFormatter extends BaseFormatter {
|
||||||
|
get name(): string {
|
||||||
|
return "smartconfig";
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
|
const changes: IPlannedChange[] = [];
|
||||||
|
|
||||||
|
// File rename (npmextra.json/smartconfig.json → .smartconfig.json)
|
||||||
|
// is handled by the orchestrator before analysis.
|
||||||
|
// This formatter only operates on .smartconfig.json.
|
||||||
|
const exists = await plugins.smartfs.file(CONFIG_FILE).exists();
|
||||||
|
if (!exists) {
|
||||||
|
logVerbose(".smartconfig.json does not exist, skipping");
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentContent = (await plugins.smartfs
|
||||||
|
.file(CONFIG_FILE)
|
||||||
|
.encoding("utf8")
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
|
const smartconfigJson = JSON.parse(currentContent);
|
||||||
|
|
||||||
|
migrateSmartconfigData(smartconfigJson);
|
||||||
|
|
||||||
|
// Ensure namespaces exist
|
||||||
|
if (!smartconfigJson["@git.zone/cli"]) {
|
||||||
|
smartconfigJson["@git.zone/cli"] = {};
|
||||||
|
}
|
||||||
|
if (!smartconfigJson["@ship.zone/szci"]) {
|
||||||
|
smartconfigJson["@ship.zone/szci"] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContent = JSON.stringify(smartconfigJson, null, 2);
|
||||||
|
|
||||||
|
if (newContent !== currentContent) {
|
||||||
|
changes.push({
|
||||||
|
type: "modify",
|
||||||
|
path: CONFIG_FILE,
|
||||||
|
module: this.name,
|
||||||
|
description: "Migrate and format .smartconfig.json",
|
||||||
|
content: newContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
|
if (change.type !== "modify" || !change.content) return;
|
||||||
|
|
||||||
|
const smartconfigJson = JSON.parse(change.content);
|
||||||
|
|
||||||
|
// Check for missing required module information
|
||||||
|
const expectedRepoInformation: string[] = [
|
||||||
|
"projectType",
|
||||||
|
"module.githost",
|
||||||
|
"module.gitscope",
|
||||||
|
"module.gitrepo",
|
||||||
|
"module.description",
|
||||||
|
"module.npmPackagename",
|
||||||
|
"module.license",
|
||||||
|
];
|
||||||
|
|
||||||
|
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||||
|
const missingRepoInformation = expectedRepoInformation.filter(
|
||||||
|
(expectedRepoInformationItem) => {
|
||||||
|
return !plugins.smartobject.smartGet(
|
||||||
|
smartconfigJson["@git.zone/cli"],
|
||||||
|
expectedRepoInformationItem,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (missingRepoInformation.length > 0 && !this.context.isInteractive()) {
|
||||||
|
throw new Error(
|
||||||
|
`Missing required .smartconfig.json fields: ${missingRepoInformation.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
||||||
|
if (
|
||||||
|
!plugins.smartobject.smartGet(
|
||||||
|
smartconfigJson["@git.zone/cli"],
|
||||||
|
expectedRepoInformationItem,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
interactInstance.addQuestions([
|
||||||
|
{
|
||||||
|
message: `What is the value of ${expectedRepoInformationItem}`,
|
||||||
|
name: expectedRepoInformationItem,
|
||||||
|
type: "input",
|
||||||
|
default: "undefined variable",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const answerbucket = await interactInstance.runQueue();
|
||||||
|
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
||||||
|
const cliProvidedValue = answerbucket.getAnswerFor(
|
||||||
|
expectedRepoInformationItem,
|
||||||
|
);
|
||||||
|
if (cliProvidedValue) {
|
||||||
|
plugins.smartobject.smartAdd(
|
||||||
|
smartconfigJson["@git.zone/cli"],
|
||||||
|
expectedRepoInformationItem,
|
||||||
|
cliProvidedValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalContent = JSON.stringify(smartconfigJson, null, 2);
|
||||||
|
await this.modifyFile(change.path, finalContent);
|
||||||
|
logger.log("info", "Updated .smartconfig.json");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,32 @@ export class TemplatesFormatter extends BaseFormatter {
|
|||||||
return 'templates';
|
return 'templates';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a template directory through smartscaf and return a map of path → content.
|
||||||
|
*/
|
||||||
|
private async renderTemplate(templateName: string): Promise<Map<string, string>> {
|
||||||
|
const templateDir = plugins.path.join(paths.templatesDir, templateName);
|
||||||
|
|
||||||
|
const scafTemplate = new plugins.smartscaf.ScafTemplate(templateDir);
|
||||||
|
await scafTemplate.readTemplateFromDir();
|
||||||
|
|
||||||
|
const gitzoneData = this.project.gitzoneConfig?.data;
|
||||||
|
if (gitzoneData) {
|
||||||
|
await scafTemplate.supplyVariables({
|
||||||
|
module: gitzoneData.module,
|
||||||
|
projectType: gitzoneData.projectType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedFiles = await scafTemplate.renderToMemory();
|
||||||
|
|
||||||
|
const fileMap = new Map<string, string>();
|
||||||
|
for (const file of renderedFiles) {
|
||||||
|
fileMap.set(file.path, file.contents.toString());
|
||||||
|
}
|
||||||
|
return fileMap;
|
||||||
|
}
|
||||||
|
|
||||||
async analyze(): Promise<IPlannedChange[]> {
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
const changes: IPlannedChange[] = [];
|
const changes: IPlannedChange[] = [];
|
||||||
const project = this.project;
|
const project = this.project;
|
||||||
@@ -25,7 +51,8 @@ export class TemplatesFormatter extends BaseFormatter {
|
|||||||
switch (projectType) {
|
switch (projectType) {
|
||||||
case 'npm':
|
case 'npm':
|
||||||
case 'wcc':
|
case 'wcc':
|
||||||
const accessLevel = project.gitzoneConfig?.data?.npmciOptions?.npmAccessLevel;
|
const accessLevel = (project.gitzoneConfig?.data as any)?.release?.accessLevel
|
||||||
|
|| project.gitzoneConfig?.data?.npmciOptions?.npmAccessLevel;
|
||||||
const ciTemplate = accessLevel === 'public' ? 'ci_default' : 'ci_default_private';
|
const ciTemplate = accessLevel === 'public' ? 'ci_default' : 'ci_default_private';
|
||||||
const ciChanges = await this.analyzeTemplate(ciTemplate, [
|
const ciChanges = await this.analyzeTemplate(ciTemplate, [
|
||||||
{ templatePath: '.gitea/workflows/default_nottags.yaml', destPath: '.gitea/workflows/default_nottags.yaml' },
|
{ templatePath: '.gitea/workflows/default_nottags.yaml', destPath: '.gitea/workflows/default_nottags.yaml' },
|
||||||
@@ -62,9 +89,6 @@ export class TemplatesFormatter extends BaseFormatter {
|
|||||||
{ templatePath: 'html/index.html', destPath: 'html/index.html' },
|
{ templatePath: 'html/index.html', destPath: 'html/index.html' },
|
||||||
]);
|
]);
|
||||||
changes.push(...websiteChanges);
|
changes.push(...websiteChanges);
|
||||||
} else if (projectType === 'service') {
|
|
||||||
const serviceChanges = await this.analyzeTemplate('service_update', []);
|
|
||||||
changes.push(...serviceChanges);
|
|
||||||
} else if (projectType === 'wcc') {
|
} else if (projectType === 'wcc') {
|
||||||
const wccChanges = await this.analyzeTemplate('wcc_update', [
|
const wccChanges = await this.analyzeTemplate('wcc_update', [
|
||||||
{ templatePath: 'html/index.html', destPath: 'html/index.html' },
|
{ templatePath: 'html/index.html', destPath: 'html/index.html' },
|
||||||
@@ -83,53 +107,47 @@ export class TemplatesFormatter extends BaseFormatter {
|
|||||||
const changes: IPlannedChange[] = [];
|
const changes: IPlannedChange[] = [];
|
||||||
const templateDir = plugins.path.join(paths.templatesDir, templateName);
|
const templateDir = plugins.path.join(paths.templatesDir, templateName);
|
||||||
|
|
||||||
// Check if template exists
|
|
||||||
const templateExists = await plugins.smartfs.directory(templateDir).exists();
|
const templateExists = await plugins.smartfs.directory(templateDir).exists();
|
||||||
if (!templateExists) {
|
if (!templateExists) {
|
||||||
logVerbose(`Template ${templateName} not found`);
|
logVerbose(`Template ${templateName} not found`);
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of files) {
|
let renderedFiles: Map<string, string>;
|
||||||
const templateFilePath = plugins.path.join(templateDir, file.templatePath);
|
try {
|
||||||
const destFilePath = file.destPath;
|
renderedFiles = await this.renderTemplate(templateName);
|
||||||
|
} catch (error) {
|
||||||
|
logVerbose(`Failed to render template ${templateName}: ${error.message}`);
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if template file exists
|
for (const file of files) {
|
||||||
const fileExists = await plugins.smartfs.file(templateFilePath).exists();
|
// Look up by templatePath first, then destPath (frontmatter may rename files)
|
||||||
if (!fileExists) {
|
const processedContent = renderedFiles.get(file.templatePath)
|
||||||
logVerbose(`Template file ${templateFilePath} not found`);
|
|| renderedFiles.get(file.destPath);
|
||||||
|
|
||||||
|
if (!processedContent) {
|
||||||
|
logVerbose(`Template file ${file.templatePath} not found in rendered output`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const destExists = await plugins.smartfs.file(file.destPath).exists();
|
||||||
// Read template content
|
let currentContent = '';
|
||||||
const templateContent = (await plugins.smartfs
|
if (destExists) {
|
||||||
.file(templateFilePath)
|
currentContent = (await plugins.smartfs
|
||||||
|
.file(file.destPath)
|
||||||
.encoding('utf8')
|
.encoding('utf8')
|
||||||
.read()) as string;
|
.read()) as string;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if destination file exists
|
if (processedContent !== currentContent) {
|
||||||
const destExists = await plugins.smartfs.file(destFilePath).exists();
|
changes.push({
|
||||||
let currentContent = '';
|
type: destExists ? 'modify' : 'create',
|
||||||
if (destExists) {
|
path: file.destPath,
|
||||||
currentContent = (await plugins.smartfs
|
module: this.name,
|
||||||
.file(destFilePath)
|
description: `Apply template ${templateName}/${file.templatePath}`,
|
||||||
.encoding('utf8')
|
content: processedContent,
|
||||||
.read()) as string;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Only add change if content differs
|
|
||||||
if (templateContent !== currentContent) {
|
|
||||||
changes.push({
|
|
||||||
type: destExists ? 'modify' : 'create',
|
|
||||||
path: destFilePath,
|
|
||||||
module: this.name,
|
|
||||||
description: `Apply template ${templateName}/${file.templatePath}`,
|
|
||||||
content: templateContent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logVerbose(`Failed to read template ${templateFilePath}: ${error.message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,12 +157,6 @@ export class TemplatesFormatter extends BaseFormatter {
|
|||||||
async applyChange(change: IPlannedChange): Promise<void> {
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
if (!change.content) return;
|
if (!change.content) return;
|
||||||
|
|
||||||
// Ensure destination directory exists
|
|
||||||
const destDir = plugins.path.dirname(change.path);
|
|
||||||
if (destDir && destDir !== '.') {
|
|
||||||
await plugins.smartfs.directory(destDir).recursive().create();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (change.type === 'create') {
|
if (change.type === 'create') {
|
||||||
await this.createFile(change.path, change.content);
|
await this.createFile(change.path, change.content);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -30,9 +30,10 @@ export class TsconfigFormatter extends BaseFormatter {
|
|||||||
const tsconfigObject = JSON.parse(currentContent);
|
const tsconfigObject = JSON.parse(currentContent);
|
||||||
tsconfigObject.compilerOptions = tsconfigObject.compilerOptions || {};
|
tsconfigObject.compilerOptions = tsconfigObject.compilerOptions || {};
|
||||||
tsconfigObject.compilerOptions.baseUrl = '.';
|
tsconfigObject.compilerOptions.baseUrl = '.';
|
||||||
tsconfigObject.compilerOptions.paths = {};
|
const existingPaths = tsconfigObject.compilerOptions.paths || {};
|
||||||
|
|
||||||
// Get module paths from tspublish
|
// Get module paths from tspublish, merging with existing custom paths
|
||||||
|
const tspublishPaths: Record<string, string[]> = {};
|
||||||
try {
|
try {
|
||||||
const tsPublishMod = await import('@git.zone/tspublish');
|
const tsPublishMod = await import('@git.zone/tspublish');
|
||||||
const tsPublishInstance = new tsPublishMod.TsPublish();
|
const tsPublishInstance = new tsPublishMod.TsPublish();
|
||||||
@@ -40,7 +41,7 @@ export class TsconfigFormatter extends BaseFormatter {
|
|||||||
|
|
||||||
for (const publishModule of Object.keys(publishModules)) {
|
for (const publishModule of Object.keys(publishModules)) {
|
||||||
const publishConfig = publishModules[publishModule];
|
const publishConfig = publishModules[publishModule];
|
||||||
tsconfigObject.compilerOptions.paths[`${publishConfig.name}`] = [
|
tspublishPaths[`${publishConfig.name}`] = [
|
||||||
`./${publishModule}/index.js`,
|
`./${publishModule}/index.js`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -48,6 +49,8 @@ export class TsconfigFormatter extends BaseFormatter {
|
|||||||
logVerbose(`Could not get tspublish modules: ${error.message}`);
|
logVerbose(`Could not get tspublish modules: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tsconfigObject.compilerOptions.paths = { ...existingPaths, ...tspublishPaths };
|
||||||
|
|
||||||
const newContent = JSON.stringify(tsconfigObject, null, 2);
|
const newContent = JSON.stringify(tsconfigObject, null, 2);
|
||||||
|
|
||||||
// Only add change if content differs
|
// Only add change if content differs
|
||||||
|
|||||||
+596
-165
@@ -1,120 +1,488 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from "./mod.plugins.js";
|
||||||
import { Project } from '../classes.project.js';
|
import { Project } from "../classes.project.js";
|
||||||
import { FormatContext } from './classes.formatcontext.js';
|
import { FormatContext } from "./classes.formatcontext.js";
|
||||||
import { FormatPlanner } from './classes.formatplanner.js';
|
import { FormatPlanner } from "./classes.formatplanner.js";
|
||||||
import { BaseFormatter } from './classes.baseformatter.js';
|
import { BaseFormatter } from "./classes.baseformatter.js";
|
||||||
import { logger, setVerboseMode } from '../gitzone.logging.js';
|
import { logger, setVerboseMode } from "../gitzone.logging.js";
|
||||||
|
import type { ICliMode } from "../helpers.climode.js";
|
||||||
|
import {
|
||||||
|
getCliMode,
|
||||||
|
printJson,
|
||||||
|
runWithSuppressedOutput,
|
||||||
|
} from "../helpers.climode.js";
|
||||||
|
import { getCliConfigValue } from "../helpers.smartconfig.js";
|
||||||
|
|
||||||
// Import wrapper classes for formatters
|
import { CleanupFormatter } from "./formatters/cleanup.formatter.js";
|
||||||
import { CleanupFormatter } from './formatters/cleanup.formatter.js';
|
import { SmartconfigFormatter } from "./formatters/smartconfig.formatter.js";
|
||||||
import { NpmextraFormatter } from './formatters/npmextra.formatter.js';
|
import { LicenseFormatter } from "./formatters/license.formatter.js";
|
||||||
import { LicenseFormatter } from './formatters/license.formatter.js';
|
import { PackageJsonFormatter } from "./formatters/packagejson.formatter.js";
|
||||||
import { PackageJsonFormatter } from './formatters/packagejson.formatter.js';
|
import { TemplatesFormatter } from "./formatters/templates.formatter.js";
|
||||||
import { TemplatesFormatter } from './formatters/templates.formatter.js';
|
import { GitignoreFormatter } from "./formatters/gitignore.formatter.js";
|
||||||
import { GitignoreFormatter } from './formatters/gitignore.formatter.js';
|
import { TsconfigFormatter } from "./formatters/tsconfig.formatter.js";
|
||||||
import { TsconfigFormatter } from './formatters/tsconfig.formatter.js';
|
import { PrettierFormatter } from "./formatters/prettier.formatter.js";
|
||||||
import { PrettierFormatter } from './formatters/prettier.formatter.js';
|
import { ReadmeFormatter } from "./formatters/readme.formatter.js";
|
||||||
import { ReadmeFormatter } from './formatters/readme.formatter.js';
|
import { CopyFormatter } from "./formatters/copy.formatter.js";
|
||||||
import { CopyFormatter } from './formatters/copy.formatter.js';
|
import type { ICheckResult, IFormatPlan } from "./interfaces.format.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename npmextra.json or smartconfig.json to .smartconfig.json
|
||||||
|
* before any formatter tries to read config.
|
||||||
|
*/
|
||||||
|
async function migrateConfigFile(allowWrite: boolean): Promise<void> {
|
||||||
|
const target = ".smartconfig.json";
|
||||||
|
const targetExists = await plugins.smartfs.file(target).exists();
|
||||||
|
if (targetExists) return;
|
||||||
|
|
||||||
|
for (const oldName of ["smartconfig.json", "npmextra.json"]) {
|
||||||
|
const exists = await plugins.smartfs.file(oldName).exists();
|
||||||
|
if (exists) {
|
||||||
|
if (!allowWrite) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const content = (await plugins.smartfs
|
||||||
|
.file(oldName)
|
||||||
|
.encoding("utf8")
|
||||||
|
.read()) as string;
|
||||||
|
await plugins.smartfs.file(`./${target}`).encoding("utf8").write(content);
|
||||||
|
await plugins.smartfs.file(oldName).delete();
|
||||||
|
logger.log("info", `Migrated ${oldName} to ${target}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared formatter class map used by both run() and runFormatter()
|
||||||
|
const formatterMap: Record<
|
||||||
|
string,
|
||||||
|
new (ctx: FormatContext, proj: Project) => BaseFormatter
|
||||||
|
> = {
|
||||||
|
cleanup: CleanupFormatter,
|
||||||
|
smartconfig: SmartconfigFormatter,
|
||||||
|
license: LicenseFormatter,
|
||||||
|
packagejson: PackageJsonFormatter,
|
||||||
|
templates: TemplatesFormatter,
|
||||||
|
gitignore: GitignoreFormatter,
|
||||||
|
tsconfig: TsconfigFormatter,
|
||||||
|
prettier: PrettierFormatter,
|
||||||
|
readme: ReadmeFormatter,
|
||||||
|
copy: CopyFormatter,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Formatters that don't require projectType to be set
|
||||||
|
const formattersNotRequiringProjectType = [
|
||||||
|
"smartconfig",
|
||||||
|
"prettier",
|
||||||
|
"cleanup",
|
||||||
|
"packagejson",
|
||||||
|
];
|
||||||
|
|
||||||
|
const getFormatConfig = async () => {
|
||||||
|
const rawFormatConfig = await getCliConfigValue<Record<string, any>>(
|
||||||
|
"format",
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
interactive: true,
|
||||||
|
showDiffs: false,
|
||||||
|
autoApprove: false,
|
||||||
|
showStats: true,
|
||||||
|
modules: {
|
||||||
|
skip: [],
|
||||||
|
only: [],
|
||||||
|
...(rawFormatConfig.modules || {}),
|
||||||
|
},
|
||||||
|
...rawFormatConfig,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeModuleList = (value: unknown): string[] => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.flatMap((item) => normalizeModuleList(item));
|
||||||
|
}
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
.split(",")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlanStatus = (plan: IFormatPlan) => {
|
||||||
|
const errorWarnings = plan.warnings.filter(
|
||||||
|
(warning) => warning.level === "error",
|
||||||
|
);
|
||||||
|
const hasChanges = plan.summary.totalFiles > 0;
|
||||||
|
const hasErrors = errorWarnings.length > 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: !hasChanges && !hasErrors,
|
||||||
|
hasChanges,
|
||||||
|
hasErrors,
|
||||||
|
errorCount: errorWarnings.length,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createActiveFormatters = async (options: {
|
||||||
|
interactive: boolean;
|
||||||
|
jsonOutput: boolean;
|
||||||
|
only?: string[];
|
||||||
|
skip?: string[];
|
||||||
|
}) => {
|
||||||
|
const project = await Project.fromCwd({ requireProjectType: false });
|
||||||
|
const context = new FormatContext(options);
|
||||||
|
const planner = new FormatPlanner();
|
||||||
|
|
||||||
|
const formatConfig = await getFormatConfig();
|
||||||
|
const formatters = Object.entries(formatterMap).map(
|
||||||
|
([, FormatterClass]) => new FormatterClass(context, project),
|
||||||
|
);
|
||||||
|
|
||||||
|
const onlyModules = options.only?.length
|
||||||
|
? options.only
|
||||||
|
: formatConfig.modules.only;
|
||||||
|
const skipModules = [
|
||||||
|
...formatConfig.modules.skip,
|
||||||
|
...(options.skip || []),
|
||||||
|
];
|
||||||
|
|
||||||
|
const activeFormatters = formatters.filter((formatter) => {
|
||||||
|
if (onlyModules.length > 0) {
|
||||||
|
return onlyModules.includes(formatter.name);
|
||||||
|
}
|
||||||
|
if (skipModules.includes(formatter.name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
context,
|
||||||
|
planner,
|
||||||
|
formatConfig,
|
||||||
|
activeFormatters,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildFormatPlan = async (options: {
|
||||||
|
fromPlan?: string;
|
||||||
|
interactive: boolean;
|
||||||
|
jsonOutput: boolean;
|
||||||
|
only?: string[];
|
||||||
|
skip?: string[];
|
||||||
|
}) => {
|
||||||
|
const { context, planner, formatConfig, activeFormatters } =
|
||||||
|
await createActiveFormatters({
|
||||||
|
interactive: options.interactive,
|
||||||
|
jsonOutput: options.jsonOutput,
|
||||||
|
only: options.only,
|
||||||
|
skip: options.skip,
|
||||||
|
});
|
||||||
|
|
||||||
|
const plan = options.fromPlan
|
||||||
|
? JSON.parse(
|
||||||
|
(await plugins.smartfs
|
||||||
|
.file(options.fromPlan)
|
||||||
|
.encoding("utf8")
|
||||||
|
.read()) as string,
|
||||||
|
)
|
||||||
|
: await planner.planFormat(activeFormatters);
|
||||||
|
|
||||||
|
return {
|
||||||
|
context,
|
||||||
|
planner,
|
||||||
|
formatConfig,
|
||||||
|
activeFormatters,
|
||||||
|
plan,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const serializePlan = (plan: any) => {
|
||||||
|
return {
|
||||||
|
summary: plan.summary,
|
||||||
|
warnings: plan.warnings,
|
||||||
|
changes: plan.changes.map((change: any) => ({
|
||||||
|
type: change.type,
|
||||||
|
path: change.path,
|
||||||
|
module: change.module,
|
||||||
|
description: change.description,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildFormatFixPrompt = (
|
||||||
|
plan: IFormatPlan,
|
||||||
|
extraInstructions: string,
|
||||||
|
): string => {
|
||||||
|
const promptParts = [
|
||||||
|
"Other /c-* commands can be found at ~/.config/opencode/commands/*",
|
||||||
|
"# gitzone format fix",
|
||||||
|
"",
|
||||||
|
`Working directory: ${process.cwd()}`,
|
||||||
|
"",
|
||||||
|
"Repair project formatting so `gitzone format check --json` passes.",
|
||||||
|
"",
|
||||||
|
"Rules:",
|
||||||
|
"- Read `.smartconfig.json`, `package.json`, `tsconfig.json`, and the current format plan before editing.",
|
||||||
|
"- Prefer deterministic gitzone standards, bundled assets, and existing project conventions.",
|
||||||
|
"- Keep changes focused on formatting, metadata normalization, templates, and config consistency.",
|
||||||
|
"- Do not commit, release, install dependencies, or modify unrelated files.",
|
||||||
|
"- Use pnpm commands only if commands are needed.",
|
||||||
|
"- Run `gitzone format --write --yes` after changes.",
|
||||||
|
"- Run `gitzone format check --json` after changes and keep fixing until it passes.",
|
||||||
|
"- Run `git diff --check` after changes to catch whitespace problems.",
|
||||||
|
"",
|
||||||
|
"Current format plan:",
|
||||||
|
JSON.stringify(serializePlan(plan), null, 2),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (extraInstructions) {
|
||||||
|
promptParts.push("", "Additional user instructions:", extraInstructions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promptParts.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormatFix = async (
|
||||||
|
options: Record<string, any>,
|
||||||
|
mode: ICliMode,
|
||||||
|
): Promise<void> => {
|
||||||
|
if (mode.json) {
|
||||||
|
printJson({
|
||||||
|
ok: false,
|
||||||
|
error:
|
||||||
|
"JSON output is not supported for `gitzone format fix`. Use `gitzone format check --json` for machine-readable diagnostics.",
|
||||||
|
});
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraInstructions = (options._?.slice(2).join(" ") || "").trim();
|
||||||
|
const force = Boolean(options.force);
|
||||||
|
const autoApprove = Boolean(options.yes || mode.yes);
|
||||||
|
const formatConfig = await getFormatConfig();
|
||||||
|
const interactive =
|
||||||
|
options.interactive ?? (mode.interactive && formatConfig.interactive);
|
||||||
|
const only = normalizeModuleList(options.only);
|
||||||
|
const skip = normalizeModuleList(options.skip);
|
||||||
|
|
||||||
|
const buildCurrentPlan = async () => {
|
||||||
|
return await buildFormatPlan({
|
||||||
|
interactive,
|
||||||
|
jsonOutput: false,
|
||||||
|
only,
|
||||||
|
skip,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.log("info", "Analyzing project for format fixes...");
|
||||||
|
let { plan } = await buildCurrentPlan();
|
||||||
|
let status = getPlanStatus(plan);
|
||||||
|
|
||||||
|
if (status.ok && !extraInstructions && !force) {
|
||||||
|
logger.log(
|
||||||
|
"success",
|
||||||
|
"Format check found no issues. Use `gitzone format fix --force` to run opencode anyway.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!autoApprove) {
|
||||||
|
if (!mode.interactive) {
|
||||||
|
throw new Error(
|
||||||
|
"Format fix requires an interactive terminal or `-y` to run non-interactively.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||||
|
`Run format fixes? (${plan.summary.totalFiles} planned change(s), ${status.errorCount} error warning(s))`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
logger.log("info", "Format fix cancelled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.hasChanges) {
|
||||||
|
logger.log("info", "Applying deterministic format changes first...");
|
||||||
|
await run({
|
||||||
|
_: ["format"],
|
||||||
|
write: true,
|
||||||
|
yes: true,
|
||||||
|
interactive: false,
|
||||||
|
verbose: options.verbose,
|
||||||
|
detailed: options.detailed,
|
||||||
|
only: options.only,
|
||||||
|
skip: options.skip,
|
||||||
|
});
|
||||||
|
|
||||||
|
({ plan } = await buildCurrentPlan());
|
||||||
|
status = getPlanStatus(plan);
|
||||||
|
if (status.ok && !extraInstructions && !force) {
|
||||||
|
logger.log("success", "Format fix completed successfully.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const opencodeArgs = [
|
||||||
|
"run",
|
||||||
|
"--title",
|
||||||
|
"gitzone format fix",
|
||||||
|
"--dir",
|
||||||
|
process.cwd(),
|
||||||
|
];
|
||||||
|
if (autoApprove) {
|
||||||
|
opencodeArgs.push("--dangerously-skip-permissions");
|
||||||
|
}
|
||||||
|
opencodeArgs.push(buildFormatFixPrompt(plan, extraInstructions));
|
||||||
|
|
||||||
|
logger.log("info", "Starting opencode format fix...");
|
||||||
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
|
executor: "bash",
|
||||||
|
sourceFilePaths: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
let result: plugins.smartshell.IExecResult;
|
||||||
|
try {
|
||||||
|
result = await smartshellInstance.execSpawn("opencode", opencodeArgs, {
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to run opencode: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", `opencode exited with code ${result.exitCode}`);
|
||||||
|
process.exitCode = result.exitCode || 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("info", "Running deterministic format pass after opencode...");
|
||||||
|
await run({
|
||||||
|
_: ["format"],
|
||||||
|
write: true,
|
||||||
|
yes: true,
|
||||||
|
interactive: false,
|
||||||
|
verbose: options.verbose,
|
||||||
|
detailed: options.detailed,
|
||||||
|
only: options.only,
|
||||||
|
skip: options.skip,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { planner: finalPlanner, plan: finalPlan } = await buildCurrentPlan();
|
||||||
|
await finalPlanner.displayPlan(finalPlan, options.detailed);
|
||||||
|
const finalStatus = getPlanStatus(finalPlan);
|
||||||
|
if (finalStatus.ok) {
|
||||||
|
logger.log("success", "Format fix completed successfully.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
"error",
|
||||||
|
`Format fix left ${finalPlan.summary.totalFiles} planned change(s) and ${finalStatus.errorCount} error warning(s).`,
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
};
|
||||||
|
|
||||||
export let run = async (
|
export let run = async (
|
||||||
options: {
|
options: {
|
||||||
write?: boolean; // Explicitly write changes (default: false, dry-mode)
|
write?: boolean;
|
||||||
dryRun?: boolean; // Deprecated, kept for compatibility
|
dryRun?: boolean; // Deprecated, kept for compatibility
|
||||||
yes?: boolean;
|
yes?: boolean;
|
||||||
planOnly?: boolean;
|
planOnly?: boolean;
|
||||||
savePlan?: string;
|
savePlan?: string;
|
||||||
fromPlan?: string;
|
fromPlan?: string;
|
||||||
detailed?: boolean;
|
detailed?: boolean;
|
||||||
interactive?: boolean;
|
interactive?: boolean;
|
||||||
parallel?: boolean;
|
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
diff?: boolean; // Show file diffs
|
diff?: boolean;
|
||||||
|
[key: string]: any;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<any> => {
|
): Promise<any> => {
|
||||||
// Set verbose mode if requested
|
const mode = await getCliMode(options as any);
|
||||||
|
const subcommand = (options as any)?._?.[1];
|
||||||
|
|
||||||
|
if (mode.help || subcommand === "help") {
|
||||||
|
showHelp(mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (options.verbose) {
|
if (options.verbose) {
|
||||||
setVerboseMode(true);
|
setVerboseMode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if we should write changes
|
if (subcommand === "fix") {
|
||||||
// Default is dry-mode (no writing) unless --write/-w is specified
|
await handleFormatFix(options, mode);
|
||||||
const shouldWrite = options.write ?? (options.dryRun === false);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const project = await Project.fromCwd({ requireProjectType: false });
|
const shouldWrite = options.write ?? options.dryRun === false;
|
||||||
const context = new FormatContext();
|
const treatAsPlan = subcommand === "plan";
|
||||||
// Cache system removed - no longer needed
|
const treatAsCheck = subcommand === "check" || Boolean(options.check);
|
||||||
const planner = new FormatPlanner();
|
|
||||||
|
|
||||||
// Get configuration from npmextra
|
if (treatAsCheck && shouldWrite) {
|
||||||
const npmextraConfig = new plugins.npmextra.Npmextra();
|
const error = "`gitzone format check` is read-only and cannot be combined with --write.";
|
||||||
const formatConfig = npmextraConfig.dataFor<any>('@git.zone/cli.format', {
|
if (mode.json) {
|
||||||
interactive: true,
|
printJson({ ok: false, error });
|
||||||
showDiffs: false,
|
} else {
|
||||||
autoApprove: false,
|
logger.log("error", error);
|
||||||
planTimeout: 30000,
|
}
|
||||||
rollback: {
|
process.exitCode = 1;
|
||||||
enabled: true,
|
return;
|
||||||
autoRollbackOnError: true,
|
}
|
||||||
backupRetentionDays: 7,
|
|
||||||
maxBackupSize: '100MB',
|
|
||||||
excludePatterns: ['node_modules/**', '.git/**'],
|
|
||||||
},
|
|
||||||
modules: {
|
|
||||||
skip: [],
|
|
||||||
only: [],
|
|
||||||
order: [],
|
|
||||||
},
|
|
||||||
parallel: true,
|
|
||||||
cache: {
|
|
||||||
enabled: true,
|
|
||||||
clean: true, // Clean invalid entries from cache
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cache cleaning removed - no longer using cache system
|
if (mode.json && shouldWrite) {
|
||||||
|
printJson({
|
||||||
|
ok: false,
|
||||||
|
error:
|
||||||
|
"JSON output is only supported for read-only format planning. Use `gitzone format plan --json` or omit `--json` when applying changes.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Override config with command options
|
// Migrate config file before anything reads it
|
||||||
const interactive = options.interactive ?? formatConfig.interactive;
|
await migrateConfigFile(shouldWrite);
|
||||||
const autoApprove = options.yes ?? formatConfig.autoApprove;
|
|
||||||
const parallel = options.parallel ?? formatConfig.parallel;
|
const formatConfig = await getFormatConfig();
|
||||||
|
const interactive =
|
||||||
|
options.interactive ?? (mode.interactive && formatConfig.interactive);
|
||||||
|
const autoApprove = options.yes ?? (mode.yes || formatConfig.autoApprove);
|
||||||
|
const only = normalizeModuleList(options.only);
|
||||||
|
const skip = normalizeModuleList(options.skip);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Initialize formatters
|
const planBuilder = async () => {
|
||||||
const formatters = [
|
return await buildFormatPlan({
|
||||||
new CleanupFormatter(context, project),
|
fromPlan: options.fromPlan,
|
||||||
new NpmextraFormatter(context, project),
|
interactive,
|
||||||
new LicenseFormatter(context, project),
|
jsonOutput: mode.json,
|
||||||
new PackageJsonFormatter(context, project),
|
only,
|
||||||
new TemplatesFormatter(context, project),
|
skip,
|
||||||
new GitignoreFormatter(context, project),
|
});
|
||||||
new TsconfigFormatter(context, project),
|
};
|
||||||
new PrettierFormatter(context, project),
|
|
||||||
new ReadmeFormatter(context, project),
|
|
||||||
new CopyFormatter(context, project),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Filter formatters based on configuration
|
if (!mode.json) {
|
||||||
const activeFormatters = formatters.filter((formatter) => {
|
logger.log("info", "Analyzing project for format operations...");
|
||||||
if (formatConfig.modules.only.length > 0) {
|
}
|
||||||
return formatConfig.modules.only.includes(formatter.name);
|
const { context, planner, activeFormatters, plan } = mode.json
|
||||||
}
|
? await runWithSuppressedOutput(planBuilder)
|
||||||
if (formatConfig.modules.skip.includes(formatter.name)) {
|
: await planBuilder();
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Plan phase
|
if (mode.json) {
|
||||||
logger.log('info', 'Analyzing project for format operations...');
|
const serializedPlan = serializePlan(plan);
|
||||||
let plan = options.fromPlan
|
if (treatAsCheck) {
|
||||||
? JSON.parse(
|
const status = getPlanStatus(plan);
|
||||||
(await plugins.smartfs
|
printJson({ ok: status.ok, ...serializedPlan });
|
||||||
.file(options.fromPlan)
|
if (!status.ok) {
|
||||||
.encoding('utf8')
|
process.exitCode = 1;
|
||||||
.read()) as string,
|
}
|
||||||
)
|
return;
|
||||||
: await planner.planFormat(activeFormatters);
|
}
|
||||||
|
printJson(serializedPlan);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Display plan
|
// Display plan
|
||||||
await planner.displayPlan(plan, options.detailed);
|
await planner.displayPlan(plan, options.detailed);
|
||||||
@@ -123,34 +491,49 @@ export let run = async (
|
|||||||
if (options.savePlan) {
|
if (options.savePlan) {
|
||||||
await plugins.smartfs
|
await plugins.smartfs
|
||||||
.file(options.savePlan)
|
.file(options.savePlan)
|
||||||
.encoding('utf8')
|
.encoding("utf8")
|
||||||
.write(JSON.stringify(plan, null, 2));
|
.write(JSON.stringify(plan, null, 2));
|
||||||
logger.log('info', `Plan saved to ${options.savePlan}`);
|
logger.log("info", `Plan saved to ${options.savePlan}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit if plan-only mode
|
if (options.planOnly || treatAsPlan) {
|
||||||
if (options.planOnly) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show diffs if requested (works in both dry-run and write modes)
|
if (treatAsCheck) {
|
||||||
if (options.diff) {
|
const status = getPlanStatus(plan);
|
||||||
logger.log('info', 'Showing file diffs:');
|
if (status.ok) {
|
||||||
console.log('');
|
logger.log("success", "Format check passed");
|
||||||
|
} else {
|
||||||
|
logger.log(
|
||||||
|
"error",
|
||||||
|
`Format check failed: ${plan.summary.totalFiles} planned change(s), ${status.errorCount} error warning(s)`,
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show diffs if explicitly requested or before interactive write confirmation
|
||||||
|
const showDiffs =
|
||||||
|
options.diff || (shouldWrite && interactive && !autoApprove);
|
||||||
|
if (showDiffs) {
|
||||||
|
logger.log("info", "Showing file diffs:");
|
||||||
|
console.log("");
|
||||||
|
|
||||||
for (const formatter of activeFormatters) {
|
for (const formatter of activeFormatters) {
|
||||||
const checkResult = await formatter.check();
|
const checkResult = await formatter.check();
|
||||||
if (checkResult.hasDiff) {
|
if (checkResult.hasDiff) {
|
||||||
logger.log('info', `[${formatter.name}]`);
|
logger.log("info", `[${formatter.name}]`);
|
||||||
formatter.displayAllDiffs(checkResult);
|
formatter.displayAllDiffs(checkResult);
|
||||||
console.log('');
|
console.log("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dry-run mode (default behavior)
|
// Dry-run mode (default behavior)
|
||||||
if (!shouldWrite) {
|
if (!shouldWrite) {
|
||||||
logger.log('info', 'Dry-run mode - use --write (-w) to apply changes');
|
logger.log("info", "Dry-run mode - use --write (-w) to apply changes");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,73 +541,44 @@ export let run = async (
|
|||||||
if (interactive && !autoApprove) {
|
if (interactive && !autoApprove) {
|
||||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||||
const response = await interactInstance.askQuestion({
|
const response = await interactInstance.askQuestion({
|
||||||
type: 'confirm',
|
type: "confirm",
|
||||||
name: 'proceed',
|
name: "proceed",
|
||||||
message: 'Proceed with formatting?',
|
message: "Proceed with formatting?",
|
||||||
default: true,
|
default: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!(response as any).value) {
|
if (!(response as any).value) {
|
||||||
logger.log('info', 'Format operation cancelled by user');
|
logger.log("info", "Format operation cancelled by user");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute phase
|
// Execute phase
|
||||||
logger.log(
|
logger.log("info", "Executing format operations...");
|
||||||
'info',
|
await planner.executePlan(plan, activeFormatters, context);
|
||||||
`Executing format operations${parallel ? ' in parallel' : ' sequentially'}...`,
|
|
||||||
);
|
|
||||||
await planner.executePlan(plan, activeFormatters, context, parallel);
|
|
||||||
|
|
||||||
// Finish statistics tracking
|
|
||||||
context.getFormatStats().finish();
|
context.getFormatStats().finish();
|
||||||
|
|
||||||
// Display statistics
|
const showStats = formatConfig.showStats ?? true;
|
||||||
const showStats = npmextraConfig.dataFor('gitzone.format.showStats', true);
|
|
||||||
if (showStats) {
|
if (showStats) {
|
||||||
context.getFormatStats().displayStats();
|
context.getFormatStats().displayStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save stats if requested
|
|
||||||
if (options.detailed) {
|
if (options.detailed) {
|
||||||
const statsPath = `.nogit/format-stats-${Date.now()}.json`;
|
const statsPath = `.nogit/format-stats-${Date.now()}.json`;
|
||||||
await context.getFormatStats().saveReport(statsPath);
|
await context.getFormatStats().saveReport(statsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('success', 'Format operations completed successfully!');
|
logger.log("success", "Format operations completed successfully!");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log('error', `Format operation failed: ${error.message}`);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.log("error", `Format operation failed: ${errorMessage}`);
|
||||||
// Rollback system has been removed for stability
|
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export CLI command handlers
|
|
||||||
export const handleRollback = async (operationId?: string): Promise<void> => {
|
|
||||||
logger.log('info', 'Rollback system has been disabled for stability');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const handleListBackups = async (): Promise<void> => {
|
|
||||||
logger.log('info', 'Backup system has been disabled for stability');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const handleCleanBackups = async (): Promise<void> => {
|
|
||||||
logger.log(
|
|
||||||
'info',
|
|
||||||
'Backup cleaning has been disabled - backup system removed',
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Import the ICheckResult type for external use
|
|
||||||
import type { ICheckResult } from './interfaces.format.js';
|
|
||||||
export type { ICheckResult };
|
export type { ICheckResult };
|
||||||
|
|
||||||
// Formatters that don't require projectType to be set
|
|
||||||
const formattersNotRequiringProjectType = ['npmextra', 'prettier', 'cleanup', 'packagejson'];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a single formatter by name (for use by other modules)
|
* Run a single formatter by name (for use by other modules)
|
||||||
*/
|
*/
|
||||||
@@ -232,28 +586,14 @@ export const runFormatter = async (
|
|||||||
formatterName: string,
|
formatterName: string,
|
||||||
options: {
|
options: {
|
||||||
silent?: boolean;
|
silent?: boolean;
|
||||||
checkOnly?: boolean; // Only check for diffs, don't apply
|
checkOnly?: boolean;
|
||||||
showDiff?: boolean; // Show the diff output
|
showDiff?: boolean;
|
||||||
} = {}
|
} = {},
|
||||||
): Promise<ICheckResult | void> => {
|
): Promise<ICheckResult | void> => {
|
||||||
// Determine if this formatter requires projectType
|
const requireProjectType =
|
||||||
const requireProjectType = !formattersNotRequiringProjectType.includes(formatterName);
|
!formattersNotRequiringProjectType.includes(formatterName);
|
||||||
const project = await Project.fromCwd({ requireProjectType });
|
const project = await Project.fromCwd({ requireProjectType });
|
||||||
const context = new FormatContext();
|
const context = new FormatContext({ interactive: true, jsonOutput: false });
|
||||||
|
|
||||||
// Map formatter names to classes
|
|
||||||
const formatterMap: Record<string, new (ctx: FormatContext, proj: Project) => BaseFormatter> = {
|
|
||||||
cleanup: CleanupFormatter,
|
|
||||||
npmextra: NpmextraFormatter,
|
|
||||||
license: LicenseFormatter,
|
|
||||||
packagejson: PackageJsonFormatter,
|
|
||||||
templates: TemplatesFormatter,
|
|
||||||
gitignore: GitignoreFormatter,
|
|
||||||
tsconfig: TsconfigFormatter,
|
|
||||||
prettier: PrettierFormatter,
|
|
||||||
readme: ReadmeFormatter,
|
|
||||||
copy: CopyFormatter,
|
|
||||||
};
|
|
||||||
|
|
||||||
const FormatterClass = formatterMap[formatterName];
|
const FormatterClass = formatterMap[formatterName];
|
||||||
if (!FormatterClass) {
|
if (!FormatterClass) {
|
||||||
@@ -262,7 +602,6 @@ export const runFormatter = async (
|
|||||||
|
|
||||||
const formatter = new FormatterClass(context, project);
|
const formatter = new FormatterClass(context, project);
|
||||||
|
|
||||||
// Check-only mode: just check for diffs and optionally display them
|
|
||||||
if (options.checkOnly) {
|
if (options.checkOnly) {
|
||||||
const result = await formatter.check();
|
const result = await formatter.check();
|
||||||
if (result.hasDiff && options.showDiff) {
|
if (result.hasDiff && options.showDiff) {
|
||||||
@@ -271,7 +610,6 @@ export const runFormatter = async (
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal mode: analyze and apply changes
|
|
||||||
const changes = await formatter.analyze();
|
const changes = await formatter.analyze();
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
@@ -279,6 +617,99 @@ export const runFormatter = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!options.silent) {
|
if (!options.silent) {
|
||||||
logger.log('success', `Formatter '${formatterName}' completed`);
|
logger.log("success", `Formatter '${formatterName}' completed`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function showHelp(mode?: ICliMode): void {
|
||||||
|
if (mode?.json) {
|
||||||
|
printJson({
|
||||||
|
command: "format",
|
||||||
|
usage: "gitzone format [plan|check|fix] [options]",
|
||||||
|
description:
|
||||||
|
"Plans formatting changes by default and applies them only with --write.",
|
||||||
|
flags: [
|
||||||
|
{ flag: "--write, -w", description: "Apply planned changes" },
|
||||||
|
{
|
||||||
|
flag: "--yes",
|
||||||
|
description: "Skip the interactive confirmation before writing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--plan-only",
|
||||||
|
description: "Show the plan without applying changes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--save-plan <file>",
|
||||||
|
description: "Write the format plan to a file",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--from-plan <file>",
|
||||||
|
description: "Load a previously saved plan",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--detailed",
|
||||||
|
description: "Show detailed diffs and save stats",
|
||||||
|
},
|
||||||
|
{ flag: "--verbose", description: "Enable verbose logging" },
|
||||||
|
{
|
||||||
|
flag: "--diff",
|
||||||
|
description: "Show per-file diffs before applying changes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--only <modules>",
|
||||||
|
description: "Run only the comma-separated formatter modules",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--skip <modules>",
|
||||||
|
description: "Skip the comma-separated formatter modules",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--force",
|
||||||
|
description: "Run `format fix` even when the deterministic plan is clean",
|
||||||
|
},
|
||||||
|
{ flag: "--json", description: "Emit a read-only format plan as JSON" },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
"gitzone format",
|
||||||
|
"gitzone format plan --json",
|
||||||
|
"gitzone format check",
|
||||||
|
"gitzone format --write --yes",
|
||||||
|
"gitzone format fix",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log("Usage: gitzone format [plan|check|fix] [options]");
|
||||||
|
console.log("");
|
||||||
|
console.log(
|
||||||
|
"Plans formatting changes by default and applies them only with --write.",
|
||||||
|
);
|
||||||
|
console.log("");
|
||||||
|
console.log("Flags:");
|
||||||
|
console.log(" --write, -w Apply planned changes");
|
||||||
|
console.log(
|
||||||
|
" --yes Skip the interactive confirmation before writing",
|
||||||
|
);
|
||||||
|
console.log(" --plan-only Show the plan without applying changes");
|
||||||
|
console.log(" --save-plan <file> Write the format plan to a file");
|
||||||
|
console.log(" --from-plan <file> Load a previously saved plan");
|
||||||
|
console.log(" --detailed Show detailed diffs and save stats");
|
||||||
|
console.log(" --verbose Enable verbose logging");
|
||||||
|
console.log(
|
||||||
|
" --diff Show per-file diffs before applying changes",
|
||||||
|
);
|
||||||
|
console.log(" --only <modules> Run only comma-separated formatter modules");
|
||||||
|
console.log(" --skip <modules> Skip comma-separated formatter modules");
|
||||||
|
console.log(" --force Run format fix even when the plan is clean");
|
||||||
|
console.log(" --json Emit a read-only format plan as JSON");
|
||||||
|
console.log("");
|
||||||
|
console.log("Examples:");
|
||||||
|
console.log(" gitzone format");
|
||||||
|
console.log(" gitzone format plan --json");
|
||||||
|
console.log(" gitzone format check");
|
||||||
|
console.log(" gitzone format --write --yes");
|
||||||
|
console.log(" gitzone format fix");
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
export type IFormatOperation = {
|
export type IFormatWarning = {
|
||||||
id: string;
|
level: 'info' | 'warning' | 'error';
|
||||||
timestamp: number;
|
message: string;
|
||||||
files: Array<{
|
module: string;
|
||||||
path: string;
|
|
||||||
originalContent: string;
|
|
||||||
checksum: string;
|
|
||||||
permissions: string;
|
|
||||||
}>;
|
|
||||||
status: 'pending' | 'in-progress' | 'completed' | 'failed' | 'rolled-back';
|
|
||||||
error?: Error;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IFormatPlan = {
|
export type IFormatPlan = {
|
||||||
@@ -17,21 +10,9 @@ export type IFormatPlan = {
|
|||||||
filesAdded: number;
|
filesAdded: number;
|
||||||
filesModified: number;
|
filesModified: number;
|
||||||
filesRemoved: number;
|
filesRemoved: number;
|
||||||
estimatedTime: number;
|
|
||||||
};
|
};
|
||||||
changes: Array<{
|
changes: IPlannedChange[];
|
||||||
type: 'create' | 'modify' | 'delete';
|
warnings: IFormatWarning[];
|
||||||
path: string;
|
|
||||||
module: string;
|
|
||||||
description: string;
|
|
||||||
diff?: string;
|
|
||||||
size?: number;
|
|
||||||
}>;
|
|
||||||
warnings: Array<{
|
|
||||||
level: 'info' | 'warning' | 'error';
|
|
||||||
message: string;
|
|
||||||
module: string;
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IPlannedChange = {
|
export type IPlannedChange = {
|
||||||
@@ -40,9 +21,6 @@ export type IPlannedChange = {
|
|||||||
module: string;
|
module: string;
|
||||||
description: string;
|
description: string;
|
||||||
content?: string; // New content for create/modify operations
|
content?: string; // New content for create/modify operations
|
||||||
originalContent?: string; // Original content for comparison
|
|
||||||
diff?: string;
|
|
||||||
size?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ICheckResult {
|
export interface ICheckResult {
|
||||||
@@ -54,3 +32,19 @@ export interface ICheckResult {
|
|||||||
after?: string;
|
after?: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getModuleIcon(module: string): string {
|
||||||
|
const icons: Record<string, string> = {
|
||||||
|
packagejson: '📦',
|
||||||
|
license: '📝',
|
||||||
|
tsconfig: '🔧',
|
||||||
|
cleanup: '🚮',
|
||||||
|
gitignore: '🔒',
|
||||||
|
prettier: '✨',
|
||||||
|
readme: '📖',
|
||||||
|
templates: '📄',
|
||||||
|
smartconfig: '⚙️',
|
||||||
|
copy: '📋',
|
||||||
|
};
|
||||||
|
return icons[module] || '📁';
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,31 +1,21 @@
|
|||||||
export * from '../plugins.js';
|
export * from '../plugins.js';
|
||||||
|
|
||||||
import * as crypto from 'crypto';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as lik from '@push.rocks/lik';
|
|
||||||
import * as smartfile from '@push.rocks/smartfile';
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
import * as smartgulp from '@push.rocks/smartgulp';
|
|
||||||
import * as smartinteract from '@push.rocks/smartinteract';
|
import * as smartinteract from '@push.rocks/smartinteract';
|
||||||
import * as smartlegal from '@push.rocks/smartlegal';
|
import * as smartlegal from '@push.rocks/smartlegal';
|
||||||
import * as smartobject from '@push.rocks/smartobject';
|
import * as smartobject from '@push.rocks/smartobject';
|
||||||
import * as smartnpm from '@push.rocks/smartnpm';
|
import * as smartconfig from '@push.rocks/smartconfig';
|
||||||
import * as smartstream from '@push.rocks/smartstream';
|
|
||||||
import * as through2 from 'through2';
|
|
||||||
import * as npmextra from '@push.rocks/npmextra';
|
|
||||||
import * as smartdiff from '@push.rocks/smartdiff';
|
import * as smartdiff from '@push.rocks/smartdiff';
|
||||||
|
import * as smartscaf from '@push.rocks/smartscaf';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
crypto,
|
|
||||||
path,
|
path,
|
||||||
lik,
|
|
||||||
smartfile,
|
smartfile,
|
||||||
smartgulp,
|
|
||||||
smartinteract,
|
smartinteract,
|
||||||
smartlegal,
|
smartlegal,
|
||||||
smartobject,
|
smartobject,
|
||||||
smartnpm,
|
smartconfig,
|
||||||
smartstream,
|
|
||||||
through2,
|
|
||||||
npmextra,
|
|
||||||
smartdiff,
|
smartdiff,
|
||||||
|
smartscaf,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
import * as plugins from "./mod.plugins.js";
|
||||||
|
import * as paths from "../paths.js";
|
||||||
|
import { logger } from "../gitzone.logging.js";
|
||||||
|
import type { ICliMode } from "../helpers.climode.js";
|
||||||
|
import { getCliMode, printJson } from "../helpers.climode.js";
|
||||||
|
import {
|
||||||
|
inferVersionTypeFromPending,
|
||||||
|
movePendingToVersion,
|
||||||
|
readPendingChangelog,
|
||||||
|
} from "../helpers.changelog.js";
|
||||||
|
import {
|
||||||
|
resolveReleaseWorkflow,
|
||||||
|
type IResolvedReleaseWorkflow,
|
||||||
|
} from "../helpers.workflow.js";
|
||||||
|
import * as commitHelpers from "../mod_commit/mod.helpers.js";
|
||||||
|
|
||||||
|
type TTargetStatus = "success" | "already-published" | "skipped" | "failed";
|
||||||
|
|
||||||
|
interface ITargetResult {
|
||||||
|
target: string;
|
||||||
|
status: TTargetStatus;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const run = async (argvArg: any) => {
|
||||||
|
const mode = await getCliMode(argvArg);
|
||||||
|
const subcommand = argvArg._?.[1];
|
||||||
|
|
||||||
|
if (mode.help || subcommand === "help") {
|
||||||
|
showHelp(mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode.json) {
|
||||||
|
printJson({
|
||||||
|
ok: false,
|
||||||
|
error: "JSON output is not supported for mutating release workflows yet. Use `gitzone release --plan` for a human-readable plan.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflow = await resolveReleaseWorkflow(argvArg);
|
||||||
|
printReleasePlan(workflow);
|
||||||
|
if (workflow.confirmation === "plan") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
|
executor: "bash",
|
||||||
|
sourceFilePaths: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const pending = await readPendingChangelog(
|
||||||
|
plugins.path.join(paths.cwd, workflow.changelogFile),
|
||||||
|
workflow.changelogPendingSection,
|
||||||
|
);
|
||||||
|
if (pending.isEmpty && !argvArg["allow-empty"] && !argvArg.allowEmpty) {
|
||||||
|
logger.log("error", "No pending changelog entries. Nothing to release.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionType = resolveVersionType(argvArg, pending.block);
|
||||||
|
const projectType = await commitHelpers.detectProjectType();
|
||||||
|
const currentVersion = await commitHelpers.readCurrentVersion(projectType);
|
||||||
|
const plannedVersion = commitHelpers.calculateNewVersion(currentVersion, versionType);
|
||||||
|
|
||||||
|
if (workflow.confirmation === "prompt") {
|
||||||
|
if (!mode.interactive) {
|
||||||
|
throw new Error("Release confirmation requires an interactive terminal. Use `-y` or set release.confirmation to `auto`.");
|
||||||
|
}
|
||||||
|
const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||||
|
`Release v${plannedVersion} (${versionType}) now?`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
logger.log("info", "Release cancelled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let newVersion = plannedVersion;
|
||||||
|
const gitResults: ITargetResult[] = [];
|
||||||
|
const npmResults: ITargetResult[] = [];
|
||||||
|
const dockerResults: ITargetResult[] = [];
|
||||||
|
|
||||||
|
if (workflow.requireCleanTree) {
|
||||||
|
await verifyCleanTree(smartshellInstance, "Working tree is not clean. Commit or stash changes before releasing.");
|
||||||
|
}
|
||||||
|
if (workflow.runTests) {
|
||||||
|
await runCommandStep(smartshellInstance, "Running tests", workflow.testCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
newVersion = await runVersionStep(projectType, versionType);
|
||||||
|
await runChangelogStep(workflow, newVersion);
|
||||||
|
await runReleaseCommitStep(smartshellInstance, newVersion);
|
||||||
|
await runTagStep(smartshellInstance, newVersion);
|
||||||
|
|
||||||
|
if (workflow.runBuild) {
|
||||||
|
await runCommandStep(smartshellInstance, "Running release build", workflow.buildCommand);
|
||||||
|
await verifyCleanTree(smartshellInstance, "Build produced uncommitted changes. Aborting release.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workflow.targets.includes("git")) {
|
||||||
|
gitResults.push(...(await runGitTarget(smartshellInstance, workflow)));
|
||||||
|
}
|
||||||
|
if (workflow.targets.includes("npm")) {
|
||||||
|
npmResults.push(...(await runNpmTarget(smartshellInstance, workflow)));
|
||||||
|
}
|
||||||
|
if (workflow.targets.includes("docker")) {
|
||||||
|
dockerResults.push(...(await runDockerTarget(smartshellInstance, workflow)));
|
||||||
|
}
|
||||||
|
|
||||||
|
printReleaseSummary(newVersion, gitResults, npmResults, dockerResults);
|
||||||
|
if ([...gitResults, ...npmResults, ...dockerResults].some((result) => result.status === "failed")) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveVersionType(argvArg: any, pendingBlock: string): commitHelpers.VersionType {
|
||||||
|
if (argvArg.major) return "major";
|
||||||
|
if (argvArg.minor) return "minor";
|
||||||
|
if (argvArg.patch) return "patch";
|
||||||
|
return inferVersionTypeFromPending(pendingBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommandStep(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
label: string,
|
||||||
|
command: string,
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(`\n${label}`);
|
||||||
|
const result = await smartshellInstance.exec(command);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", `${label} failed. Aborting release.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
logger.log("success", `${label} passed.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyCleanTree(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
errorMessage: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const statusResult = await smartshellInstance.exec("git status --porcelain");
|
||||||
|
if (statusResult.stdout.trim() !== "") {
|
||||||
|
logger.log("error", errorMessage);
|
||||||
|
console.log(statusResult.stdout);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runVersionStep(
|
||||||
|
projectType: commitHelpers.ProjectType,
|
||||||
|
versionType: commitHelpers.VersionType,
|
||||||
|
): Promise<string> {
|
||||||
|
const currentVersion = await commitHelpers.readCurrentVersion(projectType);
|
||||||
|
const newVersion = commitHelpers.calculateNewVersion(currentVersion, versionType);
|
||||||
|
logger.log("info", `Bumping version: ${currentVersion} -> ${newVersion}`);
|
||||||
|
|
||||||
|
const commitInfo = new plugins.commitinfo.CommitInfo(paths.cwd, versionType);
|
||||||
|
await commitInfo.writeIntoPotentialDirs();
|
||||||
|
await commitHelpers.updateProjectVersionFiles(projectType, newVersion);
|
||||||
|
return newVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runChangelogStep(
|
||||||
|
workflow: IResolvedReleaseWorkflow,
|
||||||
|
newVersion: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const dateString = new Date().toISOString().slice(0, 10);
|
||||||
|
await movePendingToVersion(
|
||||||
|
plugins.path.join(paths.cwd, workflow.changelogFile),
|
||||||
|
workflow.changelogPendingSection,
|
||||||
|
workflow.changelogVersionHeading,
|
||||||
|
newVersion,
|
||||||
|
dateString,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runReleaseCommitStep(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
newVersion: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await smartshellInstance.exec("git add -A");
|
||||||
|
const result = await smartshellInstance.exec(`git commit -m ${shellQuote(`v${newVersion}`)}`);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", "Release commit failed.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runTagStep(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
newVersion: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const result = await smartshellInstance.exec(`git tag v${newVersion} -m ${shellQuote(`v${newVersion}`)}`);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", "Release tag failed.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runGitTarget(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
workflow: IResolvedReleaseWorkflow,
|
||||||
|
): Promise<ITargetResult[]> {
|
||||||
|
const currentBranchResult = await smartshellInstance.exec("git branch --show-current");
|
||||||
|
const currentBranch = currentBranchResult.stdout.trim() || "master";
|
||||||
|
const commands: Array<{ target: string; command: string }> = [];
|
||||||
|
if (workflow.pushBranch) {
|
||||||
|
commands.push({
|
||||||
|
target: `${workflow.gitRemote}/${currentBranch}`,
|
||||||
|
command: `git push ${workflow.gitRemote} ${currentBranch}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (workflow.pushTags) {
|
||||||
|
commands.push({
|
||||||
|
target: `${workflow.gitRemote}/tags`,
|
||||||
|
command: `git push ${workflow.gitRemote} --tags`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: ITargetResult[] = [];
|
||||||
|
for (const { target, command } of commands) {
|
||||||
|
const result = await smartshellInstance.exec(command);
|
||||||
|
results.push({
|
||||||
|
target,
|
||||||
|
status: result.exitCode === 0 ? "success" : "failed",
|
||||||
|
message: result.exitCode === 0 ? undefined : "push failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runNpmTarget(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
workflow: IResolvedReleaseWorkflow,
|
||||||
|
): Promise<ITargetResult[]> {
|
||||||
|
if (!workflow.npmEnabled) {
|
||||||
|
return [{ target: "npm", status: "skipped", message: "disabled" }];
|
||||||
|
}
|
||||||
|
if (workflow.npmRegistries.length === 0) {
|
||||||
|
return [{ target: "npm", status: "failed", message: "no registries configured" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: ITargetResult[] = [];
|
||||||
|
for (const registry of workflow.npmRegistries) {
|
||||||
|
const command = `pnpm publish --registry=${registry} --access=${workflow.npmAccessLevel}`;
|
||||||
|
const result = await smartshellInstance.exec(command);
|
||||||
|
const output = `${result.stdout || ""}\n${(result as any).stderr || ""}\n${(result as any).combinedOutput || ""}`;
|
||||||
|
if (result.exitCode === 0) {
|
||||||
|
results.push({ target: registry, status: "success" });
|
||||||
|
} else if (isAlreadyPublishedOutput(output) && workflow.npmAlreadyPublished === "success") {
|
||||||
|
results.push({ target: registry, status: "already-published" });
|
||||||
|
} else {
|
||||||
|
results.push({ target: registry, status: "failed", message: firstMeaningfulLine(output) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runDockerTarget(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
workflow: IResolvedReleaseWorkflow,
|
||||||
|
): Promise<ITargetResult[]> {
|
||||||
|
if (!workflow.dockerEnabled) {
|
||||||
|
return [{ target: "docker", status: "skipped", message: "disabled" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = buildTsdockerPushCommand(workflow);
|
||||||
|
const result = await smartshellInstance.exec(command);
|
||||||
|
const output = `${result.stdout || ""}\n${(result as any).stderr || ""}\n${(result as any).combinedOutput || ""}`;
|
||||||
|
return [{
|
||||||
|
target: workflow.dockerPatterns.length > 0
|
||||||
|
? `tsdocker:${workflow.dockerPatterns.join(",")}`
|
||||||
|
: "tsdocker",
|
||||||
|
status: result.exitCode === 0 ? "success" : "failed",
|
||||||
|
message: result.exitCode === 0 ? undefined : firstMeaningfulLine(output),
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTsdockerPushCommand(workflow: IResolvedReleaseWorkflow): string {
|
||||||
|
const commandParts = ["tsdocker", "push"];
|
||||||
|
if (workflow.dockerNoBuild) {
|
||||||
|
commandParts.push("--no-build");
|
||||||
|
}
|
||||||
|
if (workflow.dockerCached) {
|
||||||
|
commandParts.push("--cached");
|
||||||
|
}
|
||||||
|
if (workflow.dockerParallel === true) {
|
||||||
|
commandParts.push("--parallel");
|
||||||
|
} else if (typeof workflow.dockerParallel === "number" && Number.isFinite(workflow.dockerParallel) && workflow.dockerParallel > 0) {
|
||||||
|
commandParts.push(`--parallel=${Math.floor(workflow.dockerParallel)}`);
|
||||||
|
}
|
||||||
|
if (workflow.dockerContext) {
|
||||||
|
commandParts.push(`--context=${shellQuote(workflow.dockerContext)}`);
|
||||||
|
}
|
||||||
|
for (const pattern of workflow.dockerPatterns) {
|
||||||
|
commandParts.push(shellQuote(pattern));
|
||||||
|
}
|
||||||
|
return commandParts.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAlreadyPublishedOutput(output: string): boolean {
|
||||||
|
return /previously published versions|cannot publish over|already exists/i.test(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstMeaningfulLine(output: string): string {
|
||||||
|
return output
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.find((line) => line.length > 0) || "command failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellQuote(value: string): string {
|
||||||
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printReleasePlan(workflow: IResolvedReleaseWorkflow): void {
|
||||||
|
console.log("");
|
||||||
|
console.log("gitzone release - resolved workflow");
|
||||||
|
console.log(`confirmation: ${workflow.confirmation}`);
|
||||||
|
console.log(`plan: ${workflow.plan.join(" -> ")}`);
|
||||||
|
console.log(`targets: ${workflow.targets.length > 0 ? workflow.targets.join(", ") : "none"}`);
|
||||||
|
console.log(`changelog: ${workflow.changelogFile}#${workflow.changelogPendingSection}`);
|
||||||
|
if (workflow.targets.includes("npm")) {
|
||||||
|
console.log(`npm registries: ${workflow.npmRegistries.length > 0 ? workflow.npmRegistries.join(", ") : "none"}`);
|
||||||
|
}
|
||||||
|
if (workflow.targets.includes("docker")) {
|
||||||
|
console.log(`docker engine: ${workflow.dockerEngine}`);
|
||||||
|
console.log(`docker patterns: ${workflow.dockerPatterns.length > 0 ? workflow.dockerPatterns.join(", ") : "all Dockerfiles"}`);
|
||||||
|
console.log(`docker options: ${formatDockerOptions(workflow)}`);
|
||||||
|
}
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDockerOptions(workflow: IResolvedReleaseWorkflow): string {
|
||||||
|
const options: string[] = [];
|
||||||
|
if (workflow.dockerCached) options.push("cached");
|
||||||
|
if (workflow.dockerParallel) options.push(`parallel=${workflow.dockerParallel === true ? "true" : workflow.dockerParallel}`);
|
||||||
|
if (workflow.dockerNoBuild) options.push("no-build");
|
||||||
|
if (workflow.dockerContext) options.push(`context=${workflow.dockerContext}`);
|
||||||
|
return options.length > 0 ? options.join(", ") : "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
function printReleaseSummary(
|
||||||
|
newVersion: string,
|
||||||
|
gitResults: ITargetResult[],
|
||||||
|
npmResults: ITargetResult[],
|
||||||
|
dockerResults: ITargetResult[],
|
||||||
|
): void {
|
||||||
|
console.log("");
|
||||||
|
console.log(`Release v${newVersion}`);
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
if (gitResults.length > 0) {
|
||||||
|
console.log("git:");
|
||||||
|
for (const result of gitResults) {
|
||||||
|
console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (npmResults.length > 0) {
|
||||||
|
console.log("npm:");
|
||||||
|
for (const result of npmResults) {
|
||||||
|
console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dockerResults.length > 0) {
|
||||||
|
console.log("docker:");
|
||||||
|
for (const result of dockerResults) {
|
||||||
|
console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showHelp(mode?: ICliMode): void {
|
||||||
|
if (mode?.json) {
|
||||||
|
printJson({
|
||||||
|
command: "release",
|
||||||
|
usage: "gitzone release [options]",
|
||||||
|
description: "Creates a versioned release from pending changelog entries and publishes configured artifacts.",
|
||||||
|
flags: [
|
||||||
|
{ flag: "-y, --yes", description: "Run without interactive confirmation" },
|
||||||
|
{ flag: "-t, --test", description: "Enable release preflight tests" },
|
||||||
|
{ flag: "-b, --build", description: "Enable release preflight build" },
|
||||||
|
{ flag: "-p, --push", description: "Enable the git release target" },
|
||||||
|
{ flag: "--target <names>", description: "Release only selected targets: git,npm,docker" },
|
||||||
|
{ flag: "--npm", description: "Enable the npm release target" },
|
||||||
|
{ flag: "--docker", description: "Enable the tsdocker release target" },
|
||||||
|
{ flag: "--no-publish", description: "Run release core and git target only" },
|
||||||
|
{ flag: "--plan", description: "Show resolved workflow without mutating files" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log("Usage: gitzone release [options]");
|
||||||
|
console.log("");
|
||||||
|
console.log("Creates a versioned release from changelog Pending entries.");
|
||||||
|
console.log("");
|
||||||
|
console.log("Flags:");
|
||||||
|
console.log(" -y, --yes Run without interactive confirmation");
|
||||||
|
console.log(" -t, --test Enable release preflight tests");
|
||||||
|
console.log(" -b, --build Enable release preflight build");
|
||||||
|
console.log(" -p, --push Enable the git release target");
|
||||||
|
console.log(" --target <names> Release only selected targets: git,npm,docker");
|
||||||
|
console.log(" --npm Enable the npm release target");
|
||||||
|
console.log(" --docker Enable the tsdocker release target");
|
||||||
|
console.log(" --no-publish Run release core and git target only");
|
||||||
|
console.log(" --major|--minor|--patch Override inferred semver level");
|
||||||
|
console.log(" --plan Show resolved workflow without mutating files");
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export * from "../plugins.js";
|
||||||
|
|
||||||
|
import * as commitinfo from "@push.rocks/commitinfo";
|
||||||
|
|
||||||
|
export { commitinfo };
|
||||||
@@ -26,11 +26,11 @@ export interface IGlobalRegistryData {
|
|||||||
|
|
||||||
export class GlobalRegistry {
|
export class GlobalRegistry {
|
||||||
private static instance: GlobalRegistry | null = null;
|
private static instance: GlobalRegistry | null = null;
|
||||||
private kvStore: plugins.npmextra.KeyValueStore<IGlobalRegistryData>;
|
private kvStore: plugins.smartconfig.KeyValueStore<IGlobalRegistryData>;
|
||||||
private docker: DockerContainer;
|
private docker: DockerContainer;
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.kvStore = new plugins.npmextra.KeyValueStore({
|
this.kvStore = new plugins.smartconfig.KeyValueStore({
|
||||||
typeArg: 'userHomeDir',
|
typeArg: 'userHomeDir',
|
||||||
identityArg: 'gitzone-services',
|
identityArg: 'gitzone-services',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class ServiceManager {
|
|||||||
await this.config.loadOrCreate();
|
await this.config.loadOrCreate();
|
||||||
logger.log('info', `📋 Project: ${this.config.getConfig().PROJECT_NAME}`);
|
logger.log('info', `📋 Project: ${this.config.getConfig().PROJECT_NAME}`);
|
||||||
|
|
||||||
// Load service selection from npmextra.json
|
// Load service selection from .smartconfig.json
|
||||||
await this.loadServiceConfiguration();
|
await this.loadServiceConfiguration();
|
||||||
|
|
||||||
// Validate and update ports if needed
|
// Validate and update ports if needed
|
||||||
@@ -39,11 +39,11 @@ export class ServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load service configuration from npmextra.json
|
* Load service configuration from .smartconfig.json
|
||||||
*/
|
*/
|
||||||
private async loadServiceConfiguration(): Promise<void> {
|
private async loadServiceConfiguration(): Promise<void> {
|
||||||
const npmextraConfig = new plugins.npmextra.Npmextra(process.cwd());
|
const smartconfigInstance = new plugins.smartconfig.Smartconfig(process.cwd());
|
||||||
const gitzoneConfig = npmextraConfig.dataFor<any>('@git.zone/cli', {});
|
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
|
||||||
|
|
||||||
// Check if services array exists
|
// Check if services array exists
|
||||||
if (!gitzoneConfig.services || !Array.isArray(gitzoneConfig.services) || gitzoneConfig.services.length === 0) {
|
if (!gitzoneConfig.services || !Array.isArray(gitzoneConfig.services) || gitzoneConfig.services.length === 0) {
|
||||||
@@ -63,7 +63,7 @@ export class ServiceManager {
|
|||||||
|
|
||||||
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
|
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
|
||||||
|
|
||||||
// Save to npmextra.json
|
// Save to .smartconfig.json
|
||||||
await this.saveServiceConfiguration(this.enabledServices);
|
await this.saveServiceConfiguration(this.enabledServices);
|
||||||
} else {
|
} else {
|
||||||
this.enabledServices = gitzoneConfig.services;
|
this.enabledServices = gitzoneConfig.services;
|
||||||
@@ -72,31 +72,31 @@ export class ServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save service configuration to npmextra.json
|
* Save service configuration to .smartconfig.json
|
||||||
*/
|
*/
|
||||||
private async saveServiceConfiguration(services: string[]): Promise<void> {
|
private async saveServiceConfiguration(services: string[]): Promise<void> {
|
||||||
const npmextraPath = plugins.path.join(process.cwd(), 'npmextra.json');
|
const smartconfigPath = plugins.path.join(process.cwd(), '.smartconfig.json');
|
||||||
let npmextraData: any = {};
|
let smartconfigData: any = {};
|
||||||
|
|
||||||
// Read existing npmextra.json if it exists
|
// Read existing .smartconfig.json if it exists
|
||||||
if (await plugins.smartfs.file(npmextraPath).exists()) {
|
if (await plugins.smartfs.file(smartconfigPath).exists()) {
|
||||||
const content = await plugins.smartfs.file(npmextraPath).encoding('utf8').read();
|
const content = await plugins.smartfs.file(smartconfigPath).encoding('utf8').read();
|
||||||
npmextraData = JSON.parse(content as string);
|
smartconfigData = JSON.parse(content as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update @git.zone/cli.services
|
// Update @git.zone/cli.services
|
||||||
if (!npmextraData['@git.zone/cli']) {
|
if (!smartconfigData['@git.zone/cli']) {
|
||||||
npmextraData['@git.zone/cli'] = {};
|
smartconfigData['@git.zone/cli'] = {};
|
||||||
}
|
}
|
||||||
npmextraData['@git.zone/cli'].services = services;
|
smartconfigData['@git.zone/cli'].services = services;
|
||||||
|
|
||||||
// Write back to npmextra.json
|
// Write back to .smartconfig.json
|
||||||
await plugins.smartfs
|
await plugins.smartfs
|
||||||
.file(npmextraPath)
|
.file(smartconfigPath)
|
||||||
.encoding('utf8')
|
.encoding('utf8')
|
||||||
.write(JSON.stringify(npmextraData, null, 2));
|
.write(JSON.stringify(smartconfigData, null, 2));
|
||||||
|
|
||||||
logger.log('ok', `✅ Saved service configuration to npmextra.json`);
|
logger.log('ok', `✅ Saved service configuration to .smartconfig.json`);
|
||||||
logger.log('info', `🔧 Enabled services: ${services.join(', ')}`);
|
logger.log('info', `🔧 Enabled services: ${services.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -904,7 +904,7 @@ export class ServiceManager {
|
|||||||
|
|
||||||
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
|
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
|
||||||
|
|
||||||
// Save to npmextra.json
|
// Save to .smartconfig.json
|
||||||
await this.saveServiceConfiguration(this.enabledServices);
|
await this.saveServiceConfiguration(this.enabledServices);
|
||||||
|
|
||||||
logger.log('ok', '✅ Service configuration updated');
|
logger.log('ok', '✅ Service configuration updated');
|
||||||
|
|||||||
+550
-183
@@ -1,12 +1,26 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from "./mod.plugins.js";
|
||||||
import * as helpers from './helpers.js';
|
import * as helpers from "./helpers.js";
|
||||||
import { ServiceManager } from './classes.servicemanager.js';
|
import { ServiceManager } from "./classes.servicemanager.js";
|
||||||
import { GlobalRegistry } from './classes.globalregistry.js';
|
import { GlobalRegistry } from "./classes.globalregistry.js";
|
||||||
import { logger } from '../gitzone.logging.js';
|
import { logger } from "../gitzone.logging.js";
|
||||||
|
import type { ICliMode } from "../helpers.climode.js";
|
||||||
|
import { getCliMode, printJson } from "../helpers.climode.js";
|
||||||
|
import {
|
||||||
|
getCliConfigValueFromData,
|
||||||
|
readSmartconfigFile,
|
||||||
|
setCliConfigValueInData,
|
||||||
|
writeSmartconfigFile,
|
||||||
|
} from "../helpers.smartconfig.js";
|
||||||
|
|
||||||
export const run = async (argvArg: any) => {
|
export const run = async (argvArg: any) => {
|
||||||
|
const mode = await getCliMode(argvArg);
|
||||||
const isGlobal = argvArg.g || argvArg.global;
|
const isGlobal = argvArg.g || argvArg.global;
|
||||||
const command = argvArg._[1] || 'help';
|
const command = argvArg._[1] || "help";
|
||||||
|
|
||||||
|
if (mode.help || command === "help") {
|
||||||
|
showHelp(mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle global commands first
|
// Handle global commands first
|
||||||
if (isGlobal) {
|
if (isGlobal) {
|
||||||
@@ -14,264 +28,597 @@ export const run = async (argvArg: any) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local project commands
|
const service = argvArg._[2] || "all";
|
||||||
const serviceManager = new ServiceManager();
|
|
||||||
await serviceManager.init();
|
|
||||||
|
|
||||||
const service = argvArg._[2] || 'all';
|
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'start':
|
case "config":
|
||||||
await handleStart(serviceManager, service);
|
if (service === "services" || argvArg._[2] === "services") {
|
||||||
break;
|
const serviceManager = new ServiceManager();
|
||||||
|
await serviceManager.init();
|
||||||
case 'stop':
|
|
||||||
await handleStop(serviceManager, service);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'restart':
|
|
||||||
await handleRestart(serviceManager, service);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'status':
|
|
||||||
await serviceManager.showStatus();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'config':
|
|
||||||
if (service === 'services' || argvArg._[2] === 'services') {
|
|
||||||
await handleConfigureServices(serviceManager);
|
await handleConfigureServices(serviceManager);
|
||||||
} else {
|
} else {
|
||||||
await serviceManager.showConfig();
|
await handleShowConfig(mode);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'compass':
|
case "set":
|
||||||
await serviceManager.showCompassConnection();
|
await handleSetServices(argvArg._[2], mode);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'logs':
|
case "enable":
|
||||||
const lines = parseInt(argvArg._[3]) || 20;
|
await handleEnableServices(argvArg._.slice(2), mode);
|
||||||
await serviceManager.showLogs(service, lines);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'remove':
|
case "disable":
|
||||||
await handleRemove(serviceManager);
|
await handleDisableServices(argvArg._.slice(2), mode);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'clean':
|
case "start":
|
||||||
await handleClean(serviceManager);
|
case "stop":
|
||||||
|
case "restart":
|
||||||
|
case "status":
|
||||||
|
case "compass":
|
||||||
|
case "logs":
|
||||||
|
case "remove":
|
||||||
|
case "clean":
|
||||||
|
case "reconfigure": {
|
||||||
|
const serviceManager = new ServiceManager();
|
||||||
|
await serviceManager.init();
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "start":
|
||||||
|
await handleStart(serviceManager, service);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "stop":
|
||||||
|
await handleStop(serviceManager, service);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "restart":
|
||||||
|
await handleRestart(serviceManager, service);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "status":
|
||||||
|
await serviceManager.showStatus();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "compass":
|
||||||
|
await serviceManager.showCompassConnection();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "logs": {
|
||||||
|
const lines = parseInt(argvArg._[3]) || 20;
|
||||||
|
await serviceManager.showLogs(service, lines);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "remove":
|
||||||
|
await handleRemove(serviceManager);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "clean":
|
||||||
|
await handleClean(serviceManager);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "reconfigure":
|
||||||
|
await serviceManager.reconfigure();
|
||||||
|
break;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'reconfigure':
|
|
||||||
await serviceManager.reconfigure();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'help':
|
|
||||||
default:
|
default:
|
||||||
showHelp();
|
showHelp(mode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const allowedServices = ["mongodb", "minio", "elasticsearch"];
|
||||||
|
|
||||||
|
const normalizeServiceName = (service: string): string => {
|
||||||
|
switch (service) {
|
||||||
|
case "mongo":
|
||||||
|
case "mongodb":
|
||||||
|
return "mongodb";
|
||||||
|
case "minio":
|
||||||
|
case "s3":
|
||||||
|
return "minio";
|
||||||
|
case "elastic":
|
||||||
|
case "elasticsearch":
|
||||||
|
case "es":
|
||||||
|
return "elasticsearch";
|
||||||
|
default:
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function readServicesConfig(): Promise<{
|
||||||
|
enabledServices: string[];
|
||||||
|
environment: Record<string, any> | null;
|
||||||
|
}> {
|
||||||
|
const smartconfigData = await readSmartconfigFile();
|
||||||
|
const enabledServices = getCliConfigValueFromData(
|
||||||
|
smartconfigData,
|
||||||
|
"services",
|
||||||
|
);
|
||||||
|
let environment: Record<string, any> | null = null;
|
||||||
|
const envPath = plugins.path.join(process.cwd(), ".nogit", "env.json");
|
||||||
|
if (await plugins.smartfs.file(envPath).exists()) {
|
||||||
|
const envContent = (await plugins.smartfs
|
||||||
|
.file(envPath)
|
||||||
|
.encoding("utf8")
|
||||||
|
.read()) as string;
|
||||||
|
environment = JSON.parse(envContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabledServices: Array.isArray(enabledServices) ? enabledServices : [],
|
||||||
|
environment,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateEnabledServices(services: string[]): Promise<void> {
|
||||||
|
const smartconfigData = await readSmartconfigFile();
|
||||||
|
setCliConfigValueInData(smartconfigData, "services", services);
|
||||||
|
await writeSmartconfigFile(smartconfigData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleShowConfig(mode: ICliMode) {
|
||||||
|
const configData = await readServicesConfig();
|
||||||
|
|
||||||
|
if (mode.json) {
|
||||||
|
printJson(configData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.printHeader("Current Services Configuration");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
`Enabled Services: ${configData.enabledServices.length > 0 ? configData.enabledServices.join(", ") : "none configured"}`,
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
if (!configData.environment) {
|
||||||
|
logger.log(
|
||||||
|
"note",
|
||||||
|
"No .nogit/env.json found yet. Start a service once to create runtime defaults.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = configData.environment;
|
||||||
|
logger.log("note", "MongoDB:");
|
||||||
|
logger.log("info", ` Host: ${env.MONGODB_HOST}:${env.MONGODB_PORT}`);
|
||||||
|
logger.log("info", ` Database: ${env.MONGODB_NAME}`);
|
||||||
|
logger.log("info", ` User: ${env.MONGODB_USER}`);
|
||||||
|
logger.log("info", ` Container: ${env.PROJECT_NAME}-mongodb`);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
` Data: ${plugins.path.join(process.cwd(), ".nogit", "mongodata")}`,
|
||||||
|
);
|
||||||
|
logger.log("info", ` Connection: ${env.MONGODB_URL}`);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
logger.log("note", "S3/MinIO:");
|
||||||
|
logger.log("info", ` Host: ${env.S3_HOST}`);
|
||||||
|
logger.log("info", ` API Port: ${env.S3_PORT}`);
|
||||||
|
logger.log("info", ` Console Port: ${env.S3_CONSOLE_PORT}`);
|
||||||
|
logger.log("info", ` Bucket: ${env.S3_BUCKET}`);
|
||||||
|
logger.log("info", ` Container: ${env.PROJECT_NAME}-minio`);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
` Data: ${plugins.path.join(process.cwd(), ".nogit", "miniodata")}`,
|
||||||
|
);
|
||||||
|
logger.log("info", ` Endpoint: ${env.S3_ENDPOINT}`);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
logger.log("note", "Elasticsearch:");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
` Host: ${env.ELASTICSEARCH_HOST}:${env.ELASTICSEARCH_PORT}`,
|
||||||
|
);
|
||||||
|
logger.log("info", ` User: ${env.ELASTICSEARCH_USER}`);
|
||||||
|
logger.log("info", ` Container: ${env.PROJECT_NAME}-elasticsearch`);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
` Data: ${plugins.path.join(process.cwd(), ".nogit", "esdata")}`,
|
||||||
|
);
|
||||||
|
logger.log("info", ` Connection: ${env.ELASTICSEARCH_URL}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSetServices(rawValue: string | undefined, mode: ICliMode) {
|
||||||
|
if (!rawValue) {
|
||||||
|
throw new Error("Specify a comma-separated list of services");
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedServices = rawValue
|
||||||
|
.split(",")
|
||||||
|
.map((service) => normalizeServiceName(service.trim()))
|
||||||
|
.filter(Boolean);
|
||||||
|
validateRequestedServices(requestedServices);
|
||||||
|
await updateEnabledServices(requestedServices);
|
||||||
|
|
||||||
|
if (mode.json) {
|
||||||
|
printJson({ ok: true, action: "set", enabledServices: requestedServices });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("ok", `Enabled services set to: ${requestedServices.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEnableServices(
|
||||||
|
requestedServices: string[],
|
||||||
|
mode: ICliMode,
|
||||||
|
) {
|
||||||
|
const normalizedServices = requestedServices.map((service) =>
|
||||||
|
normalizeServiceName(service),
|
||||||
|
);
|
||||||
|
validateRequestedServices(normalizedServices);
|
||||||
|
|
||||||
|
const configData = await readServicesConfig();
|
||||||
|
const nextServices = Array.from(
|
||||||
|
new Set([...configData.enabledServices, ...normalizedServices]),
|
||||||
|
);
|
||||||
|
await updateEnabledServices(nextServices);
|
||||||
|
|
||||||
|
if (mode.json) {
|
||||||
|
printJson({ ok: true, action: "enable", enabledServices: nextServices });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("ok", `Enabled services: ${nextServices.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDisableServices(
|
||||||
|
requestedServices: string[],
|
||||||
|
mode: ICliMode,
|
||||||
|
) {
|
||||||
|
const normalizedServices = requestedServices.map((service) =>
|
||||||
|
normalizeServiceName(service),
|
||||||
|
);
|
||||||
|
validateRequestedServices(normalizedServices);
|
||||||
|
|
||||||
|
const configData = await readServicesConfig();
|
||||||
|
const nextServices = configData.enabledServices.filter(
|
||||||
|
(service) => !normalizedServices.includes(service),
|
||||||
|
);
|
||||||
|
await updateEnabledServices(nextServices);
|
||||||
|
|
||||||
|
if (mode.json) {
|
||||||
|
printJson({ ok: true, action: "disable", enabledServices: nextServices });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("ok", `Enabled services: ${nextServices.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRequestedServices(services: string[]): void {
|
||||||
|
if (services.length === 0) {
|
||||||
|
throw new Error("Specify at least one service");
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidServices = services.filter(
|
||||||
|
(service) => !allowedServices.includes(service),
|
||||||
|
);
|
||||||
|
if (invalidServices.length > 0) {
|
||||||
|
throw new Error(`Unknown service(s): ${invalidServices.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleStart(serviceManager: ServiceManager, service: string) {
|
async function handleStart(serviceManager: ServiceManager, service: string) {
|
||||||
helpers.printHeader('Starting Services');
|
helpers.printHeader("Starting Services");
|
||||||
|
|
||||||
switch (service) {
|
switch (service) {
|
||||||
case 'mongo':
|
case "mongo":
|
||||||
case 'mongodb':
|
case "mongodb":
|
||||||
await serviceManager.startMongoDB();
|
await serviceManager.startMongoDB();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'minio':
|
case "minio":
|
||||||
case 's3':
|
case "s3":
|
||||||
await serviceManager.startMinIO();
|
await serviceManager.startMinIO();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'elasticsearch':
|
case "elasticsearch":
|
||||||
case 'es':
|
case "es":
|
||||||
await serviceManager.startElasticsearch();
|
await serviceManager.startElasticsearch();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'all':
|
case "all":
|
||||||
case '':
|
case "":
|
||||||
await serviceManager.startAll();
|
await serviceManager.startAll();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.log('error', `Unknown service: ${service}`);
|
logger.log("error", `Unknown service: ${service}`);
|
||||||
logger.log('note', 'Use: mongo, s3, elasticsearch, or all');
|
logger.log("note", "Use: mongo, s3, elasticsearch, or all");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleStop(serviceManager: ServiceManager, service: string) {
|
async function handleStop(serviceManager: ServiceManager, service: string) {
|
||||||
helpers.printHeader('Stopping Services');
|
helpers.printHeader("Stopping Services");
|
||||||
|
|
||||||
switch (service) {
|
switch (service) {
|
||||||
case 'mongo':
|
case "mongo":
|
||||||
case 'mongodb':
|
case "mongodb":
|
||||||
await serviceManager.stopMongoDB();
|
await serviceManager.stopMongoDB();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'minio':
|
case "minio":
|
||||||
case 's3':
|
case "s3":
|
||||||
await serviceManager.stopMinIO();
|
await serviceManager.stopMinIO();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'elasticsearch':
|
case "elasticsearch":
|
||||||
case 'es':
|
case "es":
|
||||||
await serviceManager.stopElasticsearch();
|
await serviceManager.stopElasticsearch();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'all':
|
case "all":
|
||||||
case '':
|
case "":
|
||||||
await serviceManager.stopAll();
|
await serviceManager.stopAll();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.log('error', `Unknown service: ${service}`);
|
logger.log("error", `Unknown service: ${service}`);
|
||||||
logger.log('note', 'Use: mongo, s3, elasticsearch, or all');
|
logger.log("note", "Use: mongo, s3, elasticsearch, or all");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRestart(serviceManager: ServiceManager, service: string) {
|
async function handleRestart(serviceManager: ServiceManager, service: string) {
|
||||||
helpers.printHeader('Restarting Services');
|
helpers.printHeader("Restarting Services");
|
||||||
|
|
||||||
switch (service) {
|
switch (service) {
|
||||||
case 'mongo':
|
case "mongo":
|
||||||
case 'mongodb':
|
case "mongodb":
|
||||||
await serviceManager.stopMongoDB();
|
await serviceManager.stopMongoDB();
|
||||||
await plugins.smartdelay.delayFor(2000);
|
await plugins.smartdelay.delayFor(2000);
|
||||||
await serviceManager.startMongoDB();
|
await serviceManager.startMongoDB();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'minio':
|
case "minio":
|
||||||
case 's3':
|
case "s3":
|
||||||
await serviceManager.stopMinIO();
|
await serviceManager.stopMinIO();
|
||||||
await plugins.smartdelay.delayFor(2000);
|
await plugins.smartdelay.delayFor(2000);
|
||||||
await serviceManager.startMinIO();
|
await serviceManager.startMinIO();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'elasticsearch':
|
case "elasticsearch":
|
||||||
case 'es':
|
case "es":
|
||||||
await serviceManager.stopElasticsearch();
|
await serviceManager.stopElasticsearch();
|
||||||
await plugins.smartdelay.delayFor(2000);
|
await plugins.smartdelay.delayFor(2000);
|
||||||
await serviceManager.startElasticsearch();
|
await serviceManager.startElasticsearch();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'all':
|
case "all":
|
||||||
case '':
|
case "":
|
||||||
await serviceManager.stopAll();
|
await serviceManager.stopAll();
|
||||||
await plugins.smartdelay.delayFor(2000);
|
await plugins.smartdelay.delayFor(2000);
|
||||||
await serviceManager.startAll();
|
await serviceManager.startAll();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.log('error', `Unknown service: ${service}`);
|
logger.log("error", `Unknown service: ${service}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemove(serviceManager: ServiceManager) {
|
async function handleRemove(serviceManager: ServiceManager) {
|
||||||
helpers.printHeader('Removing Containers');
|
helpers.printHeader("Removing Containers");
|
||||||
logger.log('note', '⚠️ This will remove containers but preserve data');
|
logger.log("note", "⚠️ This will remove containers but preserve data");
|
||||||
|
|
||||||
const shouldContinue = await plugins.smartinteract.SmartInteract.getCliConfirmation('Continue?', false);
|
const shouldContinue =
|
||||||
|
await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||||
|
"Continue?",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
if (shouldContinue) {
|
if (shouldContinue) {
|
||||||
await serviceManager.removeContainers();
|
await serviceManager.removeContainers();
|
||||||
} else {
|
} else {
|
||||||
logger.log('note', 'Cancelled');
|
logger.log("note", "Cancelled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleClean(serviceManager: ServiceManager) {
|
async function handleClean(serviceManager: ServiceManager) {
|
||||||
helpers.printHeader('Clean All');
|
helpers.printHeader("Clean All");
|
||||||
logger.log('error', '⚠️ WARNING: This will remove all containers and data!');
|
logger.log("error", "⚠️ WARNING: This will remove all containers and data!");
|
||||||
logger.log('error', 'This action cannot be undone!');
|
logger.log("error", "This action cannot be undone!");
|
||||||
|
|
||||||
const smartinteraction = new plugins.smartinteract.SmartInteract();
|
const smartinteraction = new plugins.smartinteract.SmartInteract();
|
||||||
const confirmAnswer = await smartinteraction.askQuestion({
|
const confirmAnswer = await smartinteraction.askQuestion({
|
||||||
name: 'confirm',
|
name: "confirm",
|
||||||
type: 'input',
|
type: "input",
|
||||||
message: 'Type "yes" to confirm:',
|
message: 'Type "yes" to confirm:',
|
||||||
default: 'no'
|
default: "no",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (confirmAnswer.value === 'yes') {
|
if (confirmAnswer.value === "yes") {
|
||||||
await serviceManager.removeContainers();
|
await serviceManager.removeContainers();
|
||||||
console.log();
|
console.log();
|
||||||
await serviceManager.cleanData();
|
await serviceManager.cleanData();
|
||||||
logger.log('ok', 'All cleaned ✓');
|
logger.log("ok", "All cleaned ✓");
|
||||||
} else {
|
} else {
|
||||||
logger.log('note', 'Cancelled');
|
logger.log("note", "Cancelled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleConfigureServices(serviceManager: ServiceManager) {
|
async function handleConfigureServices(serviceManager: ServiceManager) {
|
||||||
helpers.printHeader('Configure Services');
|
helpers.printHeader("Configure Services");
|
||||||
await serviceManager.configureServices();
|
await serviceManager.configureServices();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showHelp() {
|
export function showHelp(mode?: ICliMode) {
|
||||||
helpers.printHeader('GitZone Services Manager');
|
if (mode?.json) {
|
||||||
|
printJson({
|
||||||
|
command: "services",
|
||||||
|
usage: "gitzone services <command> [options]",
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
name: "config",
|
||||||
|
description:
|
||||||
|
"Show configured services and any existing runtime env.json data",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set <csv>",
|
||||||
|
description: "Set the enabled service list without prompts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "enable <service...>",
|
||||||
|
description: "Enable one or more services without prompts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disable <service...>",
|
||||||
|
description: "Disable one or more services without prompts",
|
||||||
|
},
|
||||||
|
{ name: "start [service]", description: "Start services" },
|
||||||
|
{ name: "stop [service]", description: "Stop services" },
|
||||||
|
{ name: "status", description: "Show service status" },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
"gitzone services config --json",
|
||||||
|
"gitzone services set mongodb,minio",
|
||||||
|
"gitzone services enable elasticsearch",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
logger.log('ok', 'Usage: gitzone services [command] [options]');
|
helpers.printHeader("GitZone Services Manager");
|
||||||
|
|
||||||
|
logger.log("ok", "Usage: gitzone services [command] [options]");
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
logger.log('note', 'Commands:');
|
logger.log("note", "Commands:");
|
||||||
logger.log('info', ' start [service] Start services (mongo|s3|elasticsearch|all)');
|
logger.log(
|
||||||
logger.log('info', ' stop [service] Stop services (mongo|s3|elasticsearch|all)');
|
"info",
|
||||||
logger.log('info', ' restart [service] Restart services (mongo|s3|elasticsearch|all)');
|
" start [service] Start services (mongo|s3|elasticsearch|all)",
|
||||||
logger.log('info', ' status Show service status');
|
);
|
||||||
logger.log('info', ' config Show current configuration');
|
logger.log(
|
||||||
logger.log('info', ' config services Configure which services are enabled');
|
"info",
|
||||||
logger.log('info', ' compass Show MongoDB Compass connection string');
|
" stop [service] Stop services (mongo|s3|elasticsearch|all)",
|
||||||
logger.log('info', ' logs [service] Show logs (mongo|s3|elasticsearch|all) [lines]');
|
);
|
||||||
logger.log('info', ' reconfigure Reassign ports and restart services');
|
logger.log(
|
||||||
logger.log('info', ' remove Remove all containers');
|
"info",
|
||||||
logger.log('info', ' clean Remove all containers and data ⚠️');
|
" restart [service] Restart services (mongo|s3|elasticsearch|all)",
|
||||||
logger.log('info', ' help Show this help message');
|
);
|
||||||
|
logger.log("info", " status Show service status");
|
||||||
|
logger.log("info", " config Show current configuration");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" config services Configure which services are enabled",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" set <csv> Set enabled services without prompts",
|
||||||
|
);
|
||||||
|
logger.log("info", " enable <svc...> Enable one or more services");
|
||||||
|
logger.log("info", " disable <svc...> Disable one or more services");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" compass Show MongoDB Compass connection string",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" logs [service] Show logs (mongo|s3|elasticsearch|all) [lines]",
|
||||||
|
);
|
||||||
|
logger.log("info", " reconfigure Reassign ports and restart services");
|
||||||
|
logger.log("info", " remove Remove all containers");
|
||||||
|
logger.log("info", " clean Remove all containers and data ⚠️");
|
||||||
|
logger.log("info", " help Show this help message");
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
logger.log('note', 'Available Services:');
|
logger.log("note", "Available Services:");
|
||||||
logger.log('info', ' • MongoDB (mongo) - Document database');
|
logger.log("info", " • MongoDB (mongo) - Document database");
|
||||||
logger.log('info', ' • MinIO (s3) - S3-compatible object storage');
|
logger.log("info", " • MinIO (s3) - S3-compatible object storage");
|
||||||
logger.log('info', ' • Elasticsearch (elasticsearch) - Search and analytics engine');
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" • Elasticsearch (elasticsearch) - Search and analytics engine",
|
||||||
|
);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
logger.log('note', 'Features:');
|
logger.log("note", "Features:");
|
||||||
logger.log('info', ' • Auto-creates .nogit/env.json with smart defaults');
|
logger.log("info", " • Auto-creates .nogit/env.json with smart defaults");
|
||||||
logger.log('info', ' • Random ports (20000-30000) for MongoDB/MinIO to avoid conflicts');
|
logger.log(
|
||||||
logger.log('info', ' • Elasticsearch uses standard port 9200');
|
"info",
|
||||||
logger.log('info', ' • Project-specific containers for multi-project support');
|
" • Random ports (20000-30000) for MongoDB/MinIO to avoid conflicts",
|
||||||
logger.log('info', ' • Preserves custom configuration values');
|
);
|
||||||
logger.log('info', ' • MongoDB Compass connection support');
|
logger.log("info", " • Elasticsearch uses standard port 9200");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" • Project-specific containers for multi-project support",
|
||||||
|
);
|
||||||
|
logger.log("info", " • Preserves custom configuration values");
|
||||||
|
logger.log("info", " • MongoDB Compass connection support");
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
logger.log('note', 'Examples:');
|
logger.log("note", "Examples:");
|
||||||
logger.log('info', ' gitzone services start # Start all services');
|
logger.log(
|
||||||
logger.log('info', ' gitzone services start mongo # Start only MongoDB');
|
"info",
|
||||||
logger.log('info', ' gitzone services start elasticsearch # Start only Elasticsearch');
|
" gitzone services start # Start all services",
|
||||||
logger.log('info', ' gitzone services stop # Stop all services');
|
);
|
||||||
logger.log('info', ' gitzone services status # Check service status');
|
logger.log(
|
||||||
logger.log('info', ' gitzone services config # Show configuration');
|
"info",
|
||||||
logger.log('info', ' gitzone services compass # Get MongoDB Compass connection');
|
" gitzone services start mongo # Start only MongoDB",
|
||||||
logger.log('info', ' gitzone services logs elasticsearch # Show Elasticsearch logs');
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services start elasticsearch # Start only Elasticsearch",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services stop # Stop all services",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services status # Check service status",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services config # Show configuration",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services config --json # Show configuration as JSON",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services set mongodb,minio # Configure services without prompts",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services compass # Get MongoDB Compass connection",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services logs elasticsearch # Show Elasticsearch logs",
|
||||||
|
);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
logger.log('note', 'Global Commands (-g/--global):');
|
logger.log("note", "Global Commands (-g/--global):");
|
||||||
logger.log('info', ' list -g List all registered projects');
|
logger.log("info", " list -g List all registered projects");
|
||||||
logger.log('info', ' status -g Show status across all projects');
|
logger.log("info", " status -g Show status across all projects");
|
||||||
logger.log('info', ' stop -g Stop all containers across all projects');
|
logger.log(
|
||||||
logger.log('info', ' cleanup -g Remove stale registry entries');
|
"info",
|
||||||
|
" stop -g Stop all containers across all projects",
|
||||||
|
);
|
||||||
|
logger.log("info", " cleanup -g Remove stale registry entries");
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
logger.log('note', 'Global Examples:');
|
logger.log("note", "Global Examples:");
|
||||||
logger.log('info', ' gitzone services list -g # List all registered projects');
|
logger.log(
|
||||||
logger.log('info', ' gitzone services status -g # Show global container status');
|
"info",
|
||||||
logger.log('info', ' gitzone services stop -g # Stop all (prompts for confirmation)');
|
" gitzone services list -g # List all registered projects",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services status -g # Show global container status",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services stop -g # Stop all (prompts for confirmation)",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Global Command Handlers ====================
|
// ==================== Global Command Handlers ====================
|
||||||
@@ -280,23 +627,23 @@ async function handleGlobalCommand(command: string) {
|
|||||||
const globalRegistry = GlobalRegistry.getInstance();
|
const globalRegistry = GlobalRegistry.getInstance();
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'list':
|
case "list":
|
||||||
await handleGlobalList(globalRegistry);
|
await handleGlobalList(globalRegistry);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'status':
|
case "status":
|
||||||
await handleGlobalStatus(globalRegistry);
|
await handleGlobalStatus(globalRegistry);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'stop':
|
case "stop":
|
||||||
await handleGlobalStop(globalRegistry);
|
await handleGlobalStop(globalRegistry);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'cleanup':
|
case "cleanup":
|
||||||
await handleGlobalCleanup(globalRegistry);
|
await handleGlobalCleanup(globalRegistry);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'help':
|
case "help":
|
||||||
default:
|
default:
|
||||||
showHelp();
|
showHelp();
|
||||||
break;
|
break;
|
||||||
@@ -304,13 +651,13 @@ async function handleGlobalCommand(command: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleGlobalList(globalRegistry: GlobalRegistry) {
|
async function handleGlobalList(globalRegistry: GlobalRegistry) {
|
||||||
helpers.printHeader('Registered Projects (Global)');
|
helpers.printHeader("Registered Projects (Global)");
|
||||||
|
|
||||||
const projects = await globalRegistry.getAllProjects();
|
const projects = await globalRegistry.getAllProjects();
|
||||||
const projectPaths = Object.keys(projects);
|
const projectPaths = Object.keys(projects);
|
||||||
|
|
||||||
if (projectPaths.length === 0) {
|
if (projectPaths.length === 0) {
|
||||||
logger.log('note', 'No projects registered');
|
logger.log("note", "No projects registered");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,20 +666,20 @@ async function handleGlobalList(globalRegistry: GlobalRegistry) {
|
|||||||
const lastActive = new Date(project.lastActive).toLocaleString();
|
const lastActive = new Date(project.lastActive).toLocaleString();
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
logger.log('ok', `📁 ${project.projectName}`);
|
logger.log("ok", `📁 ${project.projectName}`);
|
||||||
logger.log('info', ` Path: ${project.projectPath}`);
|
logger.log("info", ` Path: ${project.projectPath}`);
|
||||||
logger.log('info', ` Services: ${project.enabledServices.join(', ')}`);
|
logger.log("info", ` Services: ${project.enabledServices.join(", ")}`);
|
||||||
logger.log('info', ` Last Active: ${lastActive}`);
|
logger.log("info", ` Last Active: ${lastActive}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleGlobalStatus(globalRegistry: GlobalRegistry) {
|
async function handleGlobalStatus(globalRegistry: GlobalRegistry) {
|
||||||
helpers.printHeader('Global Service Status');
|
helpers.printHeader("Global Service Status");
|
||||||
|
|
||||||
const statuses = await globalRegistry.getGlobalStatus();
|
const statuses = await globalRegistry.getGlobalStatus();
|
||||||
|
|
||||||
if (statuses.length === 0) {
|
if (statuses.length === 0) {
|
||||||
logger.log('note', 'No projects registered');
|
logger.log("note", "No projects registered");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,28 +688,39 @@ async function handleGlobalStatus(globalRegistry: GlobalRegistry) {
|
|||||||
|
|
||||||
for (const project of statuses) {
|
for (const project of statuses) {
|
||||||
console.log();
|
console.log();
|
||||||
logger.log('ok', `📁 ${project.projectName}`);
|
logger.log("ok", `📁 ${project.projectName}`);
|
||||||
logger.log('info', ` Path: ${project.projectPath}`);
|
logger.log("info", ` Path: ${project.projectPath}`);
|
||||||
|
|
||||||
if (project.containers.length === 0) {
|
if (project.containers.length === 0) {
|
||||||
logger.log('note', ' No containers configured');
|
logger.log("note", " No containers configured");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const container of project.containers) {
|
for (const container of project.containers) {
|
||||||
totalContainers++;
|
totalContainers++;
|
||||||
const statusIcon = container.status === 'running' ? '🟢' : container.status === 'exited' ? '🟡' : '⚪';
|
const statusIcon =
|
||||||
if (container.status === 'running') runningCount++;
|
container.status === "running"
|
||||||
logger.log('info', ` ${statusIcon} ${container.name}: ${container.status}`);
|
? "🟢"
|
||||||
|
: container.status === "exited"
|
||||||
|
? "🟡"
|
||||||
|
: "⚪";
|
||||||
|
if (container.status === "running") runningCount++;
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
` ${statusIcon} ${container.name}: ${container.status}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
logger.log('note', `Summary: ${runningCount}/${totalContainers} containers running across ${statuses.length} project(s)`);
|
logger.log(
|
||||||
|
"note",
|
||||||
|
`Summary: ${runningCount}/${totalContainers} containers running across ${statuses.length} project(s)`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleGlobalStop(globalRegistry: GlobalRegistry) {
|
async function handleGlobalStop(globalRegistry: GlobalRegistry) {
|
||||||
helpers.printHeader('Stop All Containers (Global)');
|
helpers.printHeader("Stop All Containers (Global)");
|
||||||
|
|
||||||
const statuses = await globalRegistry.getGlobalStatus();
|
const statuses = await globalRegistry.getGlobalStatus();
|
||||||
|
|
||||||
@@ -370,64 +728,73 @@ async function handleGlobalStop(globalRegistry: GlobalRegistry) {
|
|||||||
let runningCount = 0;
|
let runningCount = 0;
|
||||||
for (const project of statuses) {
|
for (const project of statuses) {
|
||||||
for (const container of project.containers) {
|
for (const container of project.containers) {
|
||||||
if (container.status === 'running') runningCount++;
|
if (container.status === "running") runningCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runningCount === 0) {
|
if (runningCount === 0) {
|
||||||
logger.log('note', 'No running containers found');
|
logger.log("note", "No running containers found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('note', `Found ${runningCount} running container(s) across ${statuses.length} project(s)`);
|
logger.log(
|
||||||
|
"note",
|
||||||
|
`Found ${runningCount} running container(s) across ${statuses.length} project(s)`,
|
||||||
|
);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
// Show what will be stopped
|
// Show what will be stopped
|
||||||
for (const project of statuses) {
|
for (const project of statuses) {
|
||||||
const runningContainers = project.containers.filter(c => c.status === 'running');
|
const runningContainers = project.containers.filter(
|
||||||
|
(c) => c.status === "running",
|
||||||
|
);
|
||||||
if (runningContainers.length > 0) {
|
if (runningContainers.length > 0) {
|
||||||
logger.log('info', `${project.projectName}:`);
|
logger.log("info", `${project.projectName}:`);
|
||||||
for (const container of runningContainers) {
|
for (const container of runningContainers) {
|
||||||
logger.log('info', ` • ${container.name}`);
|
logger.log("info", ` • ${container.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
const shouldContinue = await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
const shouldContinue =
|
||||||
'Stop all containers?',
|
await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||||
false
|
"Stop all containers?",
|
||||||
);
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
if (!shouldContinue) {
|
if (!shouldContinue) {
|
||||||
logger.log('note', 'Cancelled');
|
logger.log("note", "Cancelled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('note', 'Stopping all containers...');
|
logger.log("note", "Stopping all containers...");
|
||||||
const result = await globalRegistry.stopAll();
|
const result = await globalRegistry.stopAll();
|
||||||
|
|
||||||
if (result.stopped.length > 0) {
|
if (result.stopped.length > 0) {
|
||||||
logger.log('ok', `Stopped: ${result.stopped.join(', ')}`);
|
logger.log("ok", `Stopped: ${result.stopped.join(", ")}`);
|
||||||
}
|
}
|
||||||
if (result.failed.length > 0) {
|
if (result.failed.length > 0) {
|
||||||
logger.log('error', `Failed to stop: ${result.failed.join(', ')}`);
|
logger.log("error", `Failed to stop: ${result.failed.join(", ")}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleGlobalCleanup(globalRegistry: GlobalRegistry) {
|
async function handleGlobalCleanup(globalRegistry: GlobalRegistry) {
|
||||||
helpers.printHeader('Cleanup Registry (Global)');
|
helpers.printHeader("Cleanup Registry (Global)");
|
||||||
|
|
||||||
logger.log('note', 'Checking for stale registry entries...');
|
logger.log("note", "Checking for stale registry entries...");
|
||||||
const removed = await globalRegistry.cleanup();
|
const removed = await globalRegistry.cleanup();
|
||||||
|
|
||||||
if (removed.length === 0) {
|
if (removed.length === 0) {
|
||||||
logger.log('ok', 'No stale entries found');
|
logger.log("ok", "No stale entries found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('ok', `Removed ${removed.length} stale entr${removed.length === 1 ? 'y' : 'ies'}:`);
|
logger.log(
|
||||||
|
"ok",
|
||||||
|
`Removed ${removed.length} stale entr${removed.length === 1 ? "y" : "ies"}:`,
|
||||||
|
);
|
||||||
for (const path of removed) {
|
for (const path of removed) {
|
||||||
logger.log('info', ` • ${path}`);
|
logger.log("info", ` • ${path}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+218
-58
@@ -1,91 +1,251 @@
|
|||||||
/* -----------------------------------------------
|
/* -----------------------------------------------
|
||||||
* executes as standard task
|
* executes as standard task
|
||||||
* ----------------------------------------------- */
|
* ----------------------------------------------- */
|
||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from "./mod.plugins.js";
|
||||||
import * as paths from '../paths.js';
|
import * as paths from "../paths.js";
|
||||||
|
import type { ICliMode } from "../helpers.climode.js";
|
||||||
|
import { getCliMode, printJson } from "../helpers.climode.js";
|
||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
import { logger } from "../gitzone.logging.js";
|
||||||
|
|
||||||
export let run = async () => {
|
type ICommandHelpSummary = {
|
||||||
console.log('');
|
name: string;
|
||||||
console.log('╭─────────────────────────────────────────────────────────────╮');
|
description: string;
|
||||||
console.log('│ gitzone - Development Workflow CLI │');
|
};
|
||||||
console.log('╰─────────────────────────────────────────────────────────────╯');
|
|
||||||
console.log('');
|
const commandSummaries: ICommandHelpSummary[] = [
|
||||||
|
{
|
||||||
|
name: "commit",
|
||||||
|
description:
|
||||||
|
"Analyze changes and create semantic source commits",
|
||||||
|
},
|
||||||
|
{ name: "release", description: "Create versioned releases from pending changelog entries" },
|
||||||
|
{ name: "format", description: "Plan or apply project formatting changes" },
|
||||||
|
{ name: "config", description: "Read and change .smartconfig.json settings" },
|
||||||
|
{ name: "services", description: "Manage or configure development services" },
|
||||||
|
{ name: "tools", description: "Manage the global @git.zone toolchain" },
|
||||||
|
{ name: "template", description: "Create a project from a template" },
|
||||||
|
{ name: "open", description: "Open project assets and CI pages" },
|
||||||
|
{ name: "docker", description: "Run Docker-related maintenance tasks" },
|
||||||
|
{
|
||||||
|
name: "deprecate",
|
||||||
|
description: "Deprecate npm packages across registries",
|
||||||
|
},
|
||||||
|
{ name: "meta", description: "Run meta-repository commands" },
|
||||||
|
{ name: "start", description: "Prepare a project for local work" },
|
||||||
|
{ name: "helpers", description: "Run helper utilities" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export let run = async (argvArg: any = {}) => {
|
||||||
|
const mode = await getCliMode(argvArg);
|
||||||
|
const requestedCommandHelp =
|
||||||
|
argvArg._?.[0] === "help" ? argvArg._?.[1] : undefined;
|
||||||
|
|
||||||
|
if (mode.help || requestedCommandHelp) {
|
||||||
|
await showHelp(mode, requestedCommandHelp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mode.interactive) {
|
||||||
|
await showHelp(mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log(
|
||||||
|
"╭─────────────────────────────────────────────────────────────╮",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"│ gitzone - Development Workflow CLI │",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"╰─────────────────────────────────────────────────────────────╯",
|
||||||
|
);
|
||||||
|
console.log("");
|
||||||
|
|
||||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||||
const response = await interactInstance.askQuestion({
|
const response = await interactInstance.askQuestion({
|
||||||
type: 'list',
|
type: "list",
|
||||||
name: 'action',
|
name: "action",
|
||||||
message: 'What would you like to do?',
|
message: "What would you like to do?",
|
||||||
default: 'commit',
|
default: "commit",
|
||||||
choices: [
|
choices: [
|
||||||
{ name: 'Commit changes (semantic versioning)', value: 'commit' },
|
{ name: "Commit changes", value: "commit" },
|
||||||
{ name: 'Format project files', value: 'format' },
|
{ name: "Release pending changes", value: "release" },
|
||||||
{ name: 'Configure release settings', value: 'config' },
|
{ name: "Format project files", value: "format" },
|
||||||
{ name: 'Create from template', value: 'template' },
|
{ name: "Configure release settings", value: "config" },
|
||||||
{ name: 'Manage dev services (MongoDB, S3)', value: 'services' },
|
{ name: "Create from template", value: "template" },
|
||||||
{ name: 'Open project assets', value: 'open' },
|
{ name: "Manage dev services (MongoDB, S3)", value: "services" },
|
||||||
{ name: 'Show help', value: 'help' },
|
{ name: "Manage global @git.zone tools", value: "tools" },
|
||||||
|
{ name: "Open project assets", value: "open" },
|
||||||
|
{ name: "Show help", value: "help" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const action = (response as any).value;
|
const action = (response as any).value;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'commit': {
|
case "commit": {
|
||||||
const modCommit = await import('../mod_commit/index.js');
|
const modCommit = await import("../mod_commit/index.js");
|
||||||
await modCommit.run({ _: ['commit'] });
|
await modCommit.run({ _: ["commit"] });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'format': {
|
case "release": {
|
||||||
const modFormat = await import('../mod_format/index.js');
|
const modRelease = await import("../mod_release/index.js");
|
||||||
|
await modRelease.run({ _: ["release"] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "format": {
|
||||||
|
const modFormat = await import("../mod_format/index.js");
|
||||||
await modFormat.run({ interactive: true });
|
await modFormat.run({ interactive: true });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'config': {
|
case "config": {
|
||||||
const modConfig = await import('../mod_config/index.js');
|
const modConfig = await import("../mod_config/index.js");
|
||||||
await modConfig.run({ _: ['config'] });
|
await modConfig.run({ _: ["config"] });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'template': {
|
case "template": {
|
||||||
const modTemplate = await import('../mod_template/index.js');
|
const modTemplate = await import("../mod_template/index.js");
|
||||||
await modTemplate.run({ _: ['template'] });
|
await modTemplate.run({ _: ["template"] });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'services': {
|
case "services": {
|
||||||
const modServices = await import('../mod_services/index.js');
|
const modServices = await import("../mod_services/index.js");
|
||||||
await modServices.run({ _: ['services'] });
|
await modServices.run({ _: ["services"] });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'open': {
|
case "tools": {
|
||||||
const modOpen = await import('../mod_open/index.js');
|
const modTools = await import("../mod_tools/index.js");
|
||||||
await modOpen.run({ _: ['open'] });
|
await modTools.run({ _: ["tools"] });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'help':
|
case "open": {
|
||||||
showHelp();
|
const modOpen = await import("../mod_open/index.js");
|
||||||
|
await modOpen.run({ _: ["open"] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "help":
|
||||||
|
await showHelp(mode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function showHelp(): void {
|
export async function showHelp(
|
||||||
console.log('');
|
mode: ICliMode,
|
||||||
console.log('Usage: gitzone <command> [options]');
|
commandName?: string,
|
||||||
console.log('');
|
): Promise<void> {
|
||||||
console.log('Commands:');
|
if (commandName) {
|
||||||
console.log(' commit Create a semantic commit with versioning');
|
const handled = await showCommandHelp(commandName, mode);
|
||||||
console.log(' format Format and standardize project files');
|
if (handled) {
|
||||||
console.log(' config Manage release registry configuration');
|
return;
|
||||||
console.log(' template Create a new project from template');
|
}
|
||||||
console.log(' services Manage dev services (MongoDB, S3/MinIO)');
|
}
|
||||||
console.log(' open Open project assets (GitLab, npm, etc.)');
|
|
||||||
console.log(' docker Docker-related operations');
|
if (mode.json) {
|
||||||
console.log(' deprecate Deprecate a package on npm');
|
printJson({
|
||||||
console.log(' meta Run meta commands');
|
name: "gitzone",
|
||||||
console.log(' start Start working on a project');
|
usage: "gitzone <command> [options]",
|
||||||
console.log(' helpers Run helper utilities');
|
commands: commandSummaries,
|
||||||
console.log('');
|
globalFlags: [
|
||||||
console.log('Run gitzone <command> --help for more information on a command.');
|
{ flag: "--help, -h", description: "Show help output" },
|
||||||
console.log('');
|
{
|
||||||
|
flag: "--json",
|
||||||
|
description: "Emit machine-readable JSON when supported",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--plain",
|
||||||
|
description: "Use plain text output when supported",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--agent",
|
||||||
|
description: "Prefer non-interactive machine-friendly output",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--no-interactive",
|
||||||
|
description: "Disable prompts and interactive menus",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--no-check-updates",
|
||||||
|
description: "Skip the update check banner",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log("Usage: gitzone <command> [options]");
|
||||||
|
console.log("");
|
||||||
|
console.log("Commands:");
|
||||||
|
for (const commandSummary of commandSummaries) {
|
||||||
|
console.log(
|
||||||
|
` ${commandSummary.name.padEnd(11)} ${commandSummary.description}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log("");
|
||||||
|
console.log("Global flags:");
|
||||||
|
console.log(" --help, -h Show help output");
|
||||||
|
console.log(
|
||||||
|
" --json Emit machine-readable JSON when supported",
|
||||||
|
);
|
||||||
|
console.log(" --plain Use plain text output when supported");
|
||||||
|
console.log(
|
||||||
|
" --agent Prefer non-interactive machine-friendly output",
|
||||||
|
);
|
||||||
|
console.log(" --no-interactive Disable prompts and interactive menus");
|
||||||
|
console.log(" --no-check-updates Skip the update check banner");
|
||||||
|
console.log("");
|
||||||
|
console.log("Examples:");
|
||||||
|
console.log(" gitzone help commit");
|
||||||
|
console.log(" gitzone config show --json");
|
||||||
|
console.log(" gitzone commit recommend --json");
|
||||||
|
console.log(" gitzone release --plan");
|
||||||
|
console.log(" gitzone format plan --json");
|
||||||
|
console.log(" gitzone format check");
|
||||||
|
console.log(" gitzone services set mongodb,minio");
|
||||||
|
console.log(" gitzone tools update");
|
||||||
|
console.log("");
|
||||||
|
console.log("Run gitzone <command> --help for command-specific usage.");
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showCommandHelp(
|
||||||
|
commandName: string,
|
||||||
|
mode: ICliMode,
|
||||||
|
): Promise<boolean> {
|
||||||
|
switch (commandName) {
|
||||||
|
case "commit": {
|
||||||
|
const modCommit = await import("../mod_commit/index.js");
|
||||||
|
modCommit.showHelp(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case "release": {
|
||||||
|
const modRelease = await import("../mod_release/index.js");
|
||||||
|
modRelease.showHelp(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case "config": {
|
||||||
|
const modConfig = await import("../mod_config/index.js");
|
||||||
|
modConfig.showHelp(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case "format": {
|
||||||
|
const modFormat = await import("../mod_format/index.js");
|
||||||
|
modFormat.showHelp(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case "services": {
|
||||||
|
const modServices = await import("../mod_services/index.js");
|
||||||
|
modServices.showHelp(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case "tools": {
|
||||||
|
const modTools = await import("../mod_tools/index.js");
|
||||||
|
modTools.showHelp(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import * as plugins from "./mod.plugins.js";
|
||||||
|
|
||||||
|
export interface IInstalledPackage {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPackageUpdateInfo {
|
||||||
|
name: string;
|
||||||
|
currentVersion: string;
|
||||||
|
latestVersion: string;
|
||||||
|
needsUpdate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPackageManagerInfo {
|
||||||
|
available: boolean;
|
||||||
|
currentVersion: string;
|
||||||
|
latestVersion: string | null;
|
||||||
|
needsUpdate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PackageManagerUtil {
|
||||||
|
private shell = new plugins.smartshell.Smartshell({
|
||||||
|
executor: "bash",
|
||||||
|
});
|
||||||
|
|
||||||
|
public async detectPnpm(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await this.shell.execSilent("pnpm --version 2>/dev/null");
|
||||||
|
return result.exitCode === 0 && Boolean(result.stdout.trim());
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPnpmVersionInfo(): Promise<IPackageManagerInfo> {
|
||||||
|
const available = await this.detectPnpm();
|
||||||
|
if (!available) {
|
||||||
|
return {
|
||||||
|
available: false,
|
||||||
|
currentVersion: "unknown",
|
||||||
|
latestVersion: null,
|
||||||
|
needsUpdate: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentVersion = await this.getCurrentPnpmVersion();
|
||||||
|
const latestVersion = await this.getLatestVersion("pnpm", ["https://registry.npmjs.org"]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
available: true,
|
||||||
|
currentVersion,
|
||||||
|
latestVersion,
|
||||||
|
needsUpdate: latestVersion ? this.isNewerVersion(currentVersion, latestVersion) : false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getInstalledPackages(): Promise<IInstalledPackage[]> {
|
||||||
|
const packages: IInstalledPackage[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.shell.execSilent("pnpm list -g --depth=0 --json 2>/dev/null || true");
|
||||||
|
const output = result.stdout.trim();
|
||||||
|
if (!output) {
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(output);
|
||||||
|
const dataArray = Array.isArray(data) ? data : [data];
|
||||||
|
for (const item of dataArray) {
|
||||||
|
const dependencies = item.dependencies || {};
|
||||||
|
for (const [name, info] of Object.entries(dependencies)) {
|
||||||
|
if (!name.startsWith("@git.zone/")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
packages.push({
|
||||||
|
name,
|
||||||
|
version: (info as any).version || "unknown",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLatestVersion(
|
||||||
|
packageName: string,
|
||||||
|
registries = ["https://verdaccio.lossless.digital", "https://registry.npmjs.org"],
|
||||||
|
): Promise<string | null> {
|
||||||
|
for (const registry of registries) {
|
||||||
|
const latest = await this.getLatestVersionFromRegistry(registry, packageName);
|
||||||
|
if (latest) {
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async installLatest(packageName: string): Promise<boolean> {
|
||||||
|
const packageSpecifier = `${packageName}@latest`;
|
||||||
|
console.log(` Installing ${packageSpecifier} via pnpm...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.shell.exec(`pnpm add -g ${shellQuote(packageSpecifier)}`);
|
||||||
|
return result.exitCode === 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isNewerVersion(current: string, latest: string): boolean {
|
||||||
|
const currentParts = normalizeSemver(current);
|
||||||
|
const latestParts = normalizeSemver(latest);
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
||||||
|
const currentPart = currentParts[i] || 0;
|
||||||
|
const latestPart = latestParts[i] || 0;
|
||||||
|
if (latestPart > currentPart) return true;
|
||||||
|
if (latestPart < currentPart) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getCurrentPnpmVersion(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const result = await this.shell.execSilent("pnpm --version 2>/dev/null");
|
||||||
|
const versionMatch = result.stdout.trim().match(/(\d+\.\d+\.\d+)/);
|
||||||
|
return versionMatch?.[1] || "unknown";
|
||||||
|
} catch {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getLatestVersionFromRegistry(
|
||||||
|
registry: string,
|
||||||
|
packageName: string,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const encodedName = packageName.replace("/", "%2f");
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 8000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${registry}/${encodedName}`, {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: {
|
||||||
|
accept: "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const latest = (data as any)["dist-tags"]?.latest;
|
||||||
|
return typeof latest === "string" && latest.length > 0 ? latest : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSemver(version: string): number[] {
|
||||||
|
return version
|
||||||
|
.replace(/^[^\d]*/, "")
|
||||||
|
.split(".")
|
||||||
|
.map((part) => parseInt(part, 10) || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellQuote(value: string): string {
|
||||||
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
import * as plugins from "./mod.plugins.js";
|
||||||
|
import { commitinfo } from "../00_commitinfo_data.js";
|
||||||
|
import type { ICliMode } from "../helpers.climode.js";
|
||||||
|
import { getCliMode, printJson } from "../helpers.climode.js";
|
||||||
|
import {
|
||||||
|
PackageManagerUtil,
|
||||||
|
type IInstalledPackage,
|
||||||
|
type IPackageUpdateInfo,
|
||||||
|
} from "./classes.packagemanager.js";
|
||||||
|
|
||||||
|
export const GITZONE_PACKAGES = [
|
||||||
|
"@git.zone/cli",
|
||||||
|
"@git.zone/tsdoc",
|
||||||
|
"@git.zone/tsbuild",
|
||||||
|
"@git.zone/tstest",
|
||||||
|
"@git.zone/tspublish",
|
||||||
|
"@git.zone/tsbundle",
|
||||||
|
"@git.zone/tsdocker",
|
||||||
|
"@git.zone/tsview",
|
||||||
|
"@git.zone/tswatch",
|
||||||
|
"@git.zone/tsrust",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const run = async (argvArg: any = {}): Promise<void> => {
|
||||||
|
const mode = await getCliMode(argvArg);
|
||||||
|
const command = argvArg._?.[1] || "help";
|
||||||
|
|
||||||
|
if (mode.help || command === "help") {
|
||||||
|
showHelp(mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "update":
|
||||||
|
await runUpdate(argvArg, mode);
|
||||||
|
break;
|
||||||
|
case "install":
|
||||||
|
await runInstall(argvArg, mode);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
showHelp(mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function runUpdate(argvArg: any, mode: ICliMode): Promise<void> {
|
||||||
|
const verbose = Boolean(argvArg.v || argvArg.verbose);
|
||||||
|
const pmUtil = new PackageManagerUtil();
|
||||||
|
|
||||||
|
console.log("Scanning for installed @git.zone packages...\n");
|
||||||
|
|
||||||
|
const pnpmInfo = await pmUtil.getPnpmVersionInfo();
|
||||||
|
if (!pnpmInfo.available) {
|
||||||
|
console.log("pnpm is required for gitzone tools update, but it was not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Package manager:\n");
|
||||||
|
console.log(" Name Current Latest Status");
|
||||||
|
console.log(" ----------------------------------------------");
|
||||||
|
const latestPnpm = (pnpmInfo.latestVersion || "unknown").padEnd(12);
|
||||||
|
const pnpmStatus = pnpmInfo.latestVersion === null
|
||||||
|
? "? Version unknown"
|
||||||
|
: pnpmInfo.needsUpdate
|
||||||
|
? "Update available"
|
||||||
|
: "Up to date";
|
||||||
|
console.log(` ${"pnpm".padEnd(9)}${pnpmInfo.currentVersion.padEnd(12)}${latestPnpm}${pnpmStatus}`);
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
console.log("Using pnpm as the supported global package manager.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
const selfUpdated = await handleSelfUpdate(pmUtil, mode);
|
||||||
|
if (selfUpdated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const installedPackages = await pmUtil.getInstalledPackages();
|
||||||
|
const packageInfos = await getPackageUpdateInfos(pmUtil, installedPackages);
|
||||||
|
|
||||||
|
if (packageInfos.length === 0) {
|
||||||
|
console.log("No managed @git.zone packages found installed globally.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Installed @git.zone packages:\n");
|
||||||
|
console.log(" Package Current Latest Status");
|
||||||
|
console.log(" ------------------------------------------------------------");
|
||||||
|
for (const packageInfo of packageInfos) {
|
||||||
|
const status = packageInfo.latestVersion === "unknown"
|
||||||
|
? "? Version unknown"
|
||||||
|
: packageInfo.needsUpdate
|
||||||
|
? "Update available"
|
||||||
|
: "Up to date";
|
||||||
|
console.log(
|
||||||
|
` ${packageInfo.name.padEnd(28)}${packageInfo.currentVersion.padEnd(12)}${packageInfo.latestVersion.padEnd(12)}${status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
await printMissingPackages(pmUtil, installedPackages);
|
||||||
|
|
||||||
|
const packagesToUpdate = packageInfos.filter((packageInfo) => packageInfo.needsUpdate);
|
||||||
|
if (packagesToUpdate.length === 0) {
|
||||||
|
console.log("All managed packages are up to date.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${packagesToUpdate.length} package(s) with available updates.\n`);
|
||||||
|
|
||||||
|
if (!mode.yes && !mode.interactive) {
|
||||||
|
console.log("Run gitzone tools update -y to update without prompts.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shouldUpdate = mode.yes;
|
||||||
|
if (!shouldUpdate) {
|
||||||
|
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||||
|
const answer = await interactInstance.askQuestion({
|
||||||
|
type: "confirm",
|
||||||
|
name: "confirmUpdate",
|
||||||
|
message: "Do you want to update these packages?",
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
|
shouldUpdate = answer.value === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldUpdate) {
|
||||||
|
console.log("Update cancelled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await installPackages(pmUtil, packagesToUpdate.map((packageInfo) => packageInfo.name), "updated");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runInstall(argvArg: any, mode: ICliMode): Promise<void> {
|
||||||
|
const verbose = Boolean(argvArg.v || argvArg.verbose);
|
||||||
|
const pmUtil = new PackageManagerUtil();
|
||||||
|
|
||||||
|
console.log("Scanning for missing @git.zone packages...\n");
|
||||||
|
|
||||||
|
const pnpmAvailable = await pmUtil.detectPnpm();
|
||||||
|
if (!pnpmAvailable) {
|
||||||
|
console.log("pnpm is required for gitzone tools install, but it was not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
console.log("Using pnpm as the supported global package manager.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
const installedPackages = await pmUtil.getInstalledPackages();
|
||||||
|
const installedNames = new Set(installedPackages.map((packageInfo) => packageInfo.name));
|
||||||
|
const missingPackages = GITZONE_PACKAGES.filter((packageName) => !installedNames.has(packageName));
|
||||||
|
|
||||||
|
if (missingPackages.length === 0) {
|
||||||
|
console.log("All managed @git.zone packages are already installed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${missingPackages.length} missing package(s).\n`);
|
||||||
|
|
||||||
|
if (!mode.yes && !mode.interactive) {
|
||||||
|
await printPackageListWithLatest(pmUtil, missingPackages);
|
||||||
|
console.log("Run gitzone tools install -y to install all missing packages without prompts.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedPackages = missingPackages;
|
||||||
|
if (!mode.yes) {
|
||||||
|
const choicesWithVersions: Array<{ name: string; value: string }> = [];
|
||||||
|
for (const packageName of missingPackages) {
|
||||||
|
const latest = await pmUtil.getLatestVersion(packageName);
|
||||||
|
choicesWithVersions.push({
|
||||||
|
name: `${packageName}${latest ? `@${latest}` : ""}`,
|
||||||
|
value: packageName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||||
|
const answer = await interactInstance.askQuestion({
|
||||||
|
type: "checkbox",
|
||||||
|
name: "packages",
|
||||||
|
message: "Select packages to install:",
|
||||||
|
default: missingPackages,
|
||||||
|
choices: choicesWithVersions,
|
||||||
|
});
|
||||||
|
|
||||||
|
selectedPackages = answer.value as string[];
|
||||||
|
if (selectedPackages.length === 0) {
|
||||||
|
console.log("No packages selected. Nothing to install.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await installPackages(pmUtil, selectedPackages, "installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSelfUpdate(
|
||||||
|
pmUtil: PackageManagerUtil,
|
||||||
|
mode: ICliMode,
|
||||||
|
): Promise<boolean> {
|
||||||
|
console.log("Checking for gitzone self-update...\n");
|
||||||
|
const currentVersion = commitinfo.version;
|
||||||
|
const latestVersion = await pmUtil.getLatestVersion("@git.zone/cli");
|
||||||
|
|
||||||
|
if (!latestVersion || !pmUtil.isNewerVersion(currentVersion, latestVersion)) {
|
||||||
|
console.log(` @git.zone/cli ${currentVersion} Up to date\n`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` @git.zone/cli ${currentVersion} -> ${latestVersion} Update available\n`);
|
||||||
|
|
||||||
|
if (!mode.yes && !mode.interactive) {
|
||||||
|
console.log("Run gitzone tools update -y to update gitzone first.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shouldUpdate = mode.yes;
|
||||||
|
if (!shouldUpdate) {
|
||||||
|
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||||
|
const answer = await interactInstance.askQuestion({
|
||||||
|
type: "confirm",
|
||||||
|
name: "confirmSelfUpdate",
|
||||||
|
message: "Do you want to update gitzone itself first?",
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
|
shouldUpdate = answer.value === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldUpdate) {
|
||||||
|
console.log("Skipping gitzone self-update.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await pmUtil.installLatest("@git.zone/cli");
|
||||||
|
if (!success) {
|
||||||
|
console.log("\ngitzone self-update failed. Continuing with the current version.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\ngitzone has been updated. Re-run gitzone tools update to check remaining packages.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPackageUpdateInfos(
|
||||||
|
pmUtil: PackageManagerUtil,
|
||||||
|
installedPackages: IInstalledPackage[],
|
||||||
|
): Promise<IPackageUpdateInfo[]> {
|
||||||
|
const packageInfos: IPackageUpdateInfo[] = [];
|
||||||
|
for (const installedPackage of installedPackages) {
|
||||||
|
if (!GITZONE_PACKAGES.includes(installedPackage.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const latestVersion = await pmUtil.getLatestVersion(installedPackage.name);
|
||||||
|
packageInfos.push({
|
||||||
|
name: installedPackage.name,
|
||||||
|
currentVersion: installedPackage.version,
|
||||||
|
latestVersion: latestVersion || "unknown",
|
||||||
|
needsUpdate: latestVersion
|
||||||
|
? pmUtil.isNewerVersion(installedPackage.version, latestVersion)
|
||||||
|
: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return packageInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function printMissingPackages(
|
||||||
|
pmUtil: PackageManagerUtil,
|
||||||
|
installedPackages: IInstalledPackage[],
|
||||||
|
): Promise<void> {
|
||||||
|
const installedNames = new Set(installedPackages.map((packageInfo) => packageInfo.name));
|
||||||
|
const missingPackages = GITZONE_PACKAGES.filter((packageName) => !installedNames.has(packageName));
|
||||||
|
if (missingPackages.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Not installed (managed @git.zone packages):\n");
|
||||||
|
await printPackageListWithLatest(pmUtil, missingPackages);
|
||||||
|
console.log("Run gitzone tools install to install missing packages.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function printPackageListWithLatest(
|
||||||
|
pmUtil: PackageManagerUtil,
|
||||||
|
packageNames: string[],
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(" Package Latest");
|
||||||
|
console.log(" ----------------------------------------");
|
||||||
|
for (const packageName of packageNames) {
|
||||||
|
const latest = await pmUtil.getLatestVersion(packageName);
|
||||||
|
console.log(` ${packageName.padEnd(28)} ${latest || "unknown"}`);
|
||||||
|
}
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installPackages(
|
||||||
|
pmUtil: PackageManagerUtil,
|
||||||
|
packageNames: string[],
|
||||||
|
action: "installed" | "updated",
|
||||||
|
): Promise<void> {
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
for (const packageName of packageNames) {
|
||||||
|
const success = await pmUtil.installLatest(packageName);
|
||||||
|
if (success) {
|
||||||
|
console.log(` ${packageName} ${action} successfully`);
|
||||||
|
successCount++;
|
||||||
|
} else {
|
||||||
|
console.log(` ${packageName} failed`);
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
if (failCount === 0) {
|
||||||
|
console.log(`All ${successCount} package(s) ${action} successfully.`);
|
||||||
|
} else {
|
||||||
|
console.log(`${successCount} package(s) ${action}, ${failCount} failed.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showHelp(mode?: ICliMode): void {
|
||||||
|
if (mode?.json) {
|
||||||
|
printJson({
|
||||||
|
name: "gitzone tools",
|
||||||
|
usage: "gitzone tools <command> [options]",
|
||||||
|
commands: [
|
||||||
|
{ name: "update", description: "Check and update globally installed @git.zone packages" },
|
||||||
|
{ name: "install", description: "Install missing managed @git.zone packages" },
|
||||||
|
],
|
||||||
|
flags: [
|
||||||
|
{ flag: "-y, --yes", description: "Run without confirmation prompts" },
|
||||||
|
{ flag: "-v, --verbose", description: "Show package manager diagnostics" },
|
||||||
|
],
|
||||||
|
packageManager: "pnpm",
|
||||||
|
managedPackages: GITZONE_PACKAGES,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log("Usage: gitzone tools <command> [options]");
|
||||||
|
console.log("");
|
||||||
|
console.log("Commands:");
|
||||||
|
console.log(" update Check and update globally installed @git.zone packages");
|
||||||
|
console.log(" install Install missing managed @git.zone packages");
|
||||||
|
console.log("");
|
||||||
|
console.log("Options:");
|
||||||
|
console.log(" -y, --yes Run without confirmation prompts");
|
||||||
|
console.log(" -v, --verbose Show package manager diagnostics");
|
||||||
|
console.log("");
|
||||||
|
console.log("Examples:");
|
||||||
|
console.log(" gitzone tools update");
|
||||||
|
console.log(" gitzone tools update -y");
|
||||||
|
console.log(" gitzone tools install");
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "../plugins.js";
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import * as smartlog from '@push.rocks/smartlog';
|
import * as smartlog from '@push.rocks/smartlog';
|
||||||
import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
|
import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
|
||||||
import * as npmextra from '@push.rocks/npmextra';
|
import * as smartconfig from '@push.rocks/smartconfig';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as projectinfo from '@push.rocks/projectinfo';
|
import * as projectinfo from '@push.rocks/projectinfo';
|
||||||
import * as smartcli from '@push.rocks/smartcli';
|
import * as smartcli from '@push.rocks/smartcli';
|
||||||
@@ -20,7 +20,7 @@ export const smartfs = new SmartFs(new SmartFsProviderNode());
|
|||||||
export {
|
export {
|
||||||
smartlog,
|
smartlog,
|
||||||
smartlogDestinationLocal,
|
smartlogDestinationLocal,
|
||||||
npmextra,
|
smartconfig,
|
||||||
path,
|
path,
|
||||||
projectinfo,
|
projectinfo,
|
||||||
smartcli,
|
smartcli,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
|
||||||
"useDefineForClassFields": false,
|
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
|
|||||||
Reference in New Issue
Block a user