Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a1044783d | |||
| b16eb75d81 | |||
| 261f7ee6b2 | |||
| fd7a73398c | |||
| f43f88a3cb | |||
| 4c86ad62fb | |||
| 4214a1fdf1 | |||
| 1c33735799 | |||
| 274405e364 | |||
| bf858c8650 | |||
| b257c82bd6 | |||
| 5a1f6d8c76 | |||
| d44ad6e4e4 | |||
| 142adfd396 | |||
| b55e75d169 | |||
| d0d922e53b | |||
| eda67395fe | |||
| 470e87eb79 |
@@ -19,6 +19,10 @@ node_modules/
|
||||
dist/
|
||||
dist_*/
|
||||
|
||||
# rust
|
||||
rust/target/
|
||||
dist_rust/
|
||||
|
||||
# AI
|
||||
.claude/
|
||||
.serena/
|
||||
|
||||
+3
@@ -1,3 +1,6 @@
|
||||
---
|
||||
fileName: .smartconfig.json
|
||||
---
|
||||
{
|
||||
"@git.zone/cli": {
|
||||
"projectType": "{{projectType}}",
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/npmextra.json"],
|
||||
"fileMatch": ["/.smartconfig.json"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
+5
-11
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@git.zone/cli",
|
||||
"private": false,
|
||||
"version": "2.13.9",
|
||||
"version": "2.14.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.",
|
||||
"main": "dist_ts/index.ts",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
@@ -67,16 +67,13 @@
|
||||
"@git.zone/tspublish": "^1.11.2",
|
||||
"@push.rocks/commitinfo": "^1.0.12",
|
||||
"@push.rocks/early": "^4.0.4",
|
||||
"@push.rocks/gulp-function": "^3.0.7",
|
||||
"@push.rocks/lik": "^6.3.1",
|
||||
"@push.rocks/npmextra": "^5.3.3",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@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/smartfile": "^13.1.2",
|
||||
"@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/smartlegal": "^1.0.27",
|
||||
@@ -89,14 +86,11 @@
|
||||
"@push.rocks/smartopen": "^2.0.0",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartscaf": "^4.0.19",
|
||||
"@push.rocks/smartscaf": "^4.0.21",
|
||||
"@push.rocks/smartshell": "^3.3.7",
|
||||
"@push.rocks/smartstream": "^3.4.0",
|
||||
"@push.rocks/smartunique": "^3.0.9",
|
||||
"@push.rocks/smartupdate": "^2.0.6",
|
||||
"@types/through2": "^2.0.41",
|
||||
"prettier": "^3.8.1",
|
||||
"through2": "^4.0.2"
|
||||
"prettier": "^3.8.1"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -107,7 +101,7 @@
|
||||
"dist_ts_web/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
".smartconfig.json",
|
||||
"readme.md"
|
||||
],
|
||||
"browserslist": [
|
||||
|
||||
Generated
+28
-63
@@ -20,21 +20,15 @@ importers:
|
||||
'@push.rocks/early':
|
||||
specifier: ^4.0.4
|
||||
version: 4.0.4
|
||||
'@push.rocks/gulp-function':
|
||||
specifier: ^3.0.7
|
||||
version: 3.0.7
|
||||
'@push.rocks/lik':
|
||||
specifier: ^6.3.1
|
||||
version: 6.3.1
|
||||
'@push.rocks/npmextra':
|
||||
specifier: ^5.3.3
|
||||
version: 5.3.3
|
||||
'@push.rocks/projectinfo':
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
'@push.rocks/smartcli':
|
||||
specifier: ^4.0.20
|
||||
version: 4.0.20
|
||||
'@push.rocks/smartconfig':
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
'@push.rocks/smartdelay':
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
@@ -47,9 +41,6 @@ importers:
|
||||
'@push.rocks/smartfs':
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0
|
||||
'@push.rocks/smartgulp':
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4
|
||||
'@push.rocks/smartinteract':
|
||||
specifier: ^2.0.16
|
||||
version: 2.0.16
|
||||
@@ -87,29 +78,20 @@ importers:
|
||||
specifier: ^4.2.3
|
||||
version: 4.2.3
|
||||
'@push.rocks/smartscaf':
|
||||
specifier: ^4.0.19
|
||||
version: 4.0.19
|
||||
specifier: ^4.0.21
|
||||
version: 4.0.21
|
||||
'@push.rocks/smartshell':
|
||||
specifier: ^3.3.7
|
||||
version: 3.3.7
|
||||
'@push.rocks/smartstream':
|
||||
specifier: ^3.4.0
|
||||
version: 3.4.0
|
||||
'@push.rocks/smartunique':
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9
|
||||
'@push.rocks/smartupdate':
|
||||
specifier: ^2.0.6
|
||||
version: 2.0.6
|
||||
'@types/through2':
|
||||
specifier: ^2.0.41
|
||||
version: 2.0.41
|
||||
prettier:
|
||||
specifier: ^3.8.1
|
||||
version: 3.8.1
|
||||
through2:
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2
|
||||
devDependencies:
|
||||
'@git.zone/tsbuild':
|
||||
specifier: ^4.3.0
|
||||
@@ -1018,9 +1000,6 @@ packages:
|
||||
'@push.rocks/early@4.0.4':
|
||||
resolution: {integrity: sha512-ak6/vqZ1PlFV08fSFQ6UwiBrr+K6IsfieZWWzT7eex1Ls6GvWEi8wZ3REFDPJq/qckNLWSgEy0EsqzRtltkaCA==}
|
||||
|
||||
'@push.rocks/gulp-function@3.0.7':
|
||||
resolution: {integrity: sha512-jmGrCItaDU0vEWNWzGQxKJmSc7c66YS5qNJ5TRPOfmSAZekcucDcGoI0XBOvirX+/sb7SZWsK6/3Qo3wweNnUg==}
|
||||
|
||||
'@push.rocks/isounique@1.0.5':
|
||||
resolution: {integrity: sha512-Z0BVqZZOCif1THTbIKWMgg0wxCzt9CyBtBBqQJiZ+jJ0KlQFrQHNHrPt81/LXe/L4x0cxWsn0bpL6W5DNSvNLw==}
|
||||
|
||||
@@ -1072,6 +1051,9 @@ packages:
|
||||
'@push.rocks/smartclickhouse@2.2.0':
|
||||
resolution: {integrity: sha512-eTzKiREIPSzL1kPkVyD6vEbn+WV/DvQqDjP67VlhNlQGbRcemnJG/eLrUUR1ytmdIqnsZGEK6UYBgyj5nhzLNQ==}
|
||||
|
||||
'@push.rocks/smartconfig@6.0.1':
|
||||
resolution: {integrity: sha512-nuUbiOTy7WbDliZV2mG1VRaeJYygtBlLNVs3LmLoPmBzbZwwUMq+HFVS19BhXxmQnGZ5+JXW05dZXKfMqEDZnw==}
|
||||
|
||||
'@push.rocks/smartcrypto@2.0.4':
|
||||
resolution: {integrity: sha512-1+/5bsjyataf5uUkUNnnVXGRAt+gHVk1KDzozjTqgqJxHvQk1d9fVDohL6CxUhUucTPtu5VR5xNBiV8YCDuGyw==}
|
||||
|
||||
@@ -1096,9 +1078,6 @@ packages:
|
||||
'@push.rocks/smarterror@2.0.1':
|
||||
resolution: {integrity: sha512-iCcH1D8tlDJgMFsaJ6lhdOTKhbU0KoprNv9MRP9o7691QOx4JEDXiHtr/lNtxVo8BUtdb9CF6kazaknO9KuORA==}
|
||||
|
||||
'@push.rocks/smartevent@2.0.5':
|
||||
resolution: {integrity: sha512-aU1hEoiMv8qDs+b3ln6e6GseyqM8sSqkGxhNTteLM6ve5dmTofnAdQ/tXshYNUUg2kPqi4ohcuf1/iACwjXNHw==}
|
||||
|
||||
'@push.rocks/smartexit@1.0.23':
|
||||
resolution: {integrity: sha512-WmwKYcwbHBByoABhHHB+PAjr5475AtD/xBh1mDcqPrFsOOUOZq3BBUdpq25wI3ccu/SZB5IwaimiVzadls6HkA==}
|
||||
|
||||
@@ -1132,9 +1111,6 @@ packages:
|
||||
'@push.rocks/smartguard@3.1.0':
|
||||
resolution: {integrity: sha512-J23q84f1O+TwFGmd4lrO9XLHUh2DaLXo9PN/9VmTWYzTkQDv5JehmifXVI0esophXcCIfbdIu6hbt7/aHlDF4A==}
|
||||
|
||||
'@push.rocks/smartgulp@3.0.4':
|
||||
resolution: {integrity: sha512-RjMxhmELaG+OBeqQQ3xCgrBpslbpWiP523stCJ393B3Xg2E7mkzsOU4x7weJWyUw/G5SqotnaYXGj94WH8Yqtg==}
|
||||
|
||||
'@push.rocks/smarthash@3.2.6':
|
||||
resolution: {integrity: sha512-Mq/WNX0Tjjes3X1gHd/ZBwOOKSrAG/Z3Xoc0OcCm3P20WKpniihkMpsnlE7wGjvpHLi/ZRe/XkB3KC3d5r9X4g==}
|
||||
|
||||
@@ -1240,8 +1216,8 @@ packages:
|
||||
'@push.rocks/smarts3@5.3.0':
|
||||
resolution: {integrity: sha512-6bo55ovCDEylbTxwPFZYDrZrz2babQEUmxHIexmVcP2j+6LYRHDbGYnWoyKdtqniqDFZ04pFkOoZ85hUzU5xCw==}
|
||||
|
||||
'@push.rocks/smartscaf@4.0.19':
|
||||
resolution: {integrity: sha512-J9z2rWtFhT55kDZfWGpghnwy4aybmJoatzq5icfbZMXohS2KALuj1N17iXZ9L+43F1AwpUeZ/KnokNsh7yHZ5w==}
|
||||
'@push.rocks/smartscaf@4.0.21':
|
||||
resolution: {integrity: sha512-vt+O/dp/uSTdfK2CtkgMSJenYwfZLMTi9IM9uHfkKd8DaI4UvqiFCO6/smkv08zWQlLBVlUD2UWVd7RM9h1BJg==}
|
||||
|
||||
'@push.rocks/smartserve@2.0.1':
|
||||
resolution: {integrity: sha512-YQb2qexfCzCqOlLWBBXKMg6xG4zahCPAxomz/KEKAwHtW6wMTtuHKSTSkRTQ0vl9jssLMAmRz2OyafiL9XGJXQ==}
|
||||
@@ -3761,9 +3737,6 @@ packages:
|
||||
threads@1.7.0:
|
||||
resolution: {integrity: sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==}
|
||||
|
||||
through2@3.0.2:
|
||||
resolution: {integrity: sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==}
|
||||
|
||||
through2@4.0.2:
|
||||
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
||||
|
||||
@@ -5514,12 +5487,6 @@ snapshots:
|
||||
'@push.rocks/consolecolor': 2.0.3
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
|
||||
'@push.rocks/gulp-function@3.0.7':
|
||||
dependencies:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@types/through2': 2.0.41
|
||||
through2: 3.0.2
|
||||
|
||||
'@push.rocks/isounique@1.0.5': {}
|
||||
|
||||
'@push.rocks/levelcache@3.2.0':
|
||||
@@ -5752,6 +5719,23 @@ snapshots:
|
||||
'@push.rocks/smarturl': 3.1.0
|
||||
'@push.rocks/webrequest': 4.0.5
|
||||
|
||||
'@push.rocks/smartconfig@6.0.1':
|
||||
dependencies:
|
||||
'@push.rocks/qenv': 6.1.3
|
||||
'@push.rocks/smartfile': 11.2.7
|
||||
'@push.rocks/smartjson': 5.2.0
|
||||
'@push.rocks/smartlog': 3.2.1
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/taskbuffer': 3.5.0
|
||||
'@tsclass/tsclass': 9.3.0
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- react
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
'@push.rocks/smartcrypto@2.0.4':
|
||||
dependencies:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
@@ -5824,11 +5808,6 @@ snapshots:
|
||||
clean-stack: 1.3.0
|
||||
make-error-cause: 2.3.0
|
||||
|
||||
'@push.rocks/smartevent@2.0.5':
|
||||
dependencies:
|
||||
'@pushrocks/smartpromise': 3.1.10
|
||||
'@pushrocks/smartrx': 2.0.27
|
||||
|
||||
'@push.rocks/smartexit@1.0.23':
|
||||
dependencies:
|
||||
'@push.rocks/lik': 6.3.1
|
||||
@@ -5931,15 +5910,6 @@ snapshots:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
|
||||
'@push.rocks/smartgulp@3.0.4':
|
||||
dependencies:
|
||||
'@push.rocks/smartevent': 2.0.5
|
||||
'@push.rocks/smartfile': 11.2.7
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartstream': 3.4.0
|
||||
'@types/through2': 2.0.41
|
||||
through2: 4.0.2
|
||||
|
||||
'@push.rocks/smarthash@3.2.6':
|
||||
dependencies:
|
||||
'@push.rocks/smartenv': 5.0.13
|
||||
@@ -6254,7 +6224,7 @@ snapshots:
|
||||
'@push.rocks/smartrust': 1.3.1
|
||||
'@tsclass/tsclass': 9.3.0
|
||||
|
||||
'@push.rocks/smartscaf@4.0.19':
|
||||
'@push.rocks/smartscaf@4.0.21':
|
||||
dependencies:
|
||||
'@push.rocks/lik': 6.3.1
|
||||
'@push.rocks/smartfile': 11.2.7
|
||||
@@ -9359,11 +9329,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
through2@3.0.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
through2@4.0.2:
|
||||
dependencies:
|
||||
readable-stream: 3.6.2
|
||||
|
||||
+25
-32
@@ -23,10 +23,10 @@ Gitzone CLI (`@git.zone/cli`) is a comprehensive toolbelt for streamlining local
|
||||
|
||||
### Configuration Management
|
||||
|
||||
- Uses `npmextra.json` for all tool configuration
|
||||
- Configuration stored under `gitzone` key in npmextra
|
||||
- No separate `.gitzonerc` file - everything in npmextra.json
|
||||
- Project type and module metadata also stored in npmextra
|
||||
- Uses `.smartconfig.json` for tool configuration
|
||||
- CLI settings live under the `@git.zone/cli` namespace
|
||||
- Agent and non-interactive defaults now belong under `@git.zone/cli.cli`
|
||||
- Project type, module metadata, release settings, commit defaults, and format settings live in the same file
|
||||
|
||||
### Format Module (`mod_format`) - SIGNIFICANTLY ENHANCED
|
||||
|
||||
@@ -84,7 +84,7 @@ The format module is responsible for project standardization:
|
||||
|
||||
1. **Plan → Action Workflow**: Shows changes before applying them
|
||||
2. **Rollback Mechanism**: Full backup and restore on failures
|
||||
3. **Enhanced Configuration**: Granular control via npmextra.json
|
||||
3. **Enhanced Configuration**: Granular control via `.smartconfig.json`
|
||||
4. **Better Error Handling**: Detailed errors with recovery options
|
||||
5. **Performance Optimizations**: Parallel execution and caching
|
||||
6. **Reporting**: Diff views, statistics, verbose logging
|
||||
@@ -132,7 +132,7 @@ The commit module now supports `-y/--yes` flag for non-interactive commits:
|
||||
## Development Tips
|
||||
|
||||
- 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
|
||||
- Maintain the existing plugin pattern for dependencies
|
||||
- Test format operations on sample projects before deploying
|
||||
@@ -144,30 +144,18 @@ The commit module now supports `-y/--yes` flag for non-interactive commits:
|
||||
|
||||
```json
|
||||
{
|
||||
"gitzone": {
|
||||
"@git.zone/cli": {
|
||||
"cli": {
|
||||
"interactive": true,
|
||||
"output": "human",
|
||||
"checkUpdates": true
|
||||
},
|
||||
"format": {
|
||||
"interactive": true,
|
||||
"parallel": true,
|
||||
"showStats": true,
|
||||
"cache": {
|
||||
"enabled": true,
|
||||
"clean": true
|
||||
},
|
||||
"rollback": {
|
||||
"enabled": true,
|
||||
"autoRollbackOnError": true,
|
||||
"backupRetentionDays": 7
|
||||
},
|
||||
"modules": {
|
||||
"skip": ["prettier"],
|
||||
"only": [],
|
||||
"order": []
|
||||
},
|
||||
"licenses": {
|
||||
"allowed": ["MIT", "Apache-2.0"],
|
||||
"exceptions": {
|
||||
"some-package": "GPL-3.0"
|
||||
}
|
||||
"only": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,6 +170,9 @@ The commit module now supports `-y/--yes` flag for non-interactive commits:
|
||||
# Interactive commit (default)
|
||||
gitzone commit
|
||||
|
||||
# Read-only recommendation
|
||||
gitzone commit recommend --json
|
||||
|
||||
# Auto-accept AI recommendations (no prompts)
|
||||
gitzone commit -y
|
||||
gitzone commit --yes
|
||||
@@ -201,11 +192,14 @@ gitzone commit --format
|
||||
# Basic format
|
||||
gitzone format
|
||||
|
||||
# Read-only JSON plan
|
||||
gitzone format plan --json
|
||||
|
||||
# Dry run to preview changes
|
||||
gitzone format --dry-run
|
||||
|
||||
# Non-interactive mode
|
||||
gitzone format --yes
|
||||
# Non-interactive apply
|
||||
gitzone format --write --yes
|
||||
|
||||
# Plan only (no execution)
|
||||
gitzone format --plan-only
|
||||
@@ -222,11 +216,10 @@ gitzone format --verbose
|
||||
# Detailed diff views
|
||||
gitzone format --detailed
|
||||
|
||||
# Rollback operations
|
||||
gitzone format --rollback
|
||||
gitzone format --rollback <operation-id>
|
||||
gitzone format --list-backups
|
||||
gitzone format --clean-backups
|
||||
# Inspect config for agents and scripts
|
||||
gitzone config show --json
|
||||
gitzone config set cli.output json
|
||||
gitzone config get release.accessLevel
|
||||
```
|
||||
|
||||
## Common Issues (Now Resolved)
|
||||
|
||||
@@ -56,6 +56,9 @@ Create standardized commits with AI-powered suggestions that automatically handl
|
||||
# Interactive commit with AI recommendations
|
||||
gitzone commit
|
||||
|
||||
# Read-only recommendation for agents and scripts
|
||||
gitzone commit recommend --json
|
||||
|
||||
# Auto-accept AI recommendations (skipped for BREAKING CHANGEs)
|
||||
gitzone commit -y
|
||||
|
||||
@@ -65,14 +68,15 @@ gitzone commit -ypbr
|
||||
|
||||
**Flags:**
|
||||
|
||||
| Flag | Long Form | Description |
|
||||
|------|-----------|-------------|
|
||||
| `-y` | `--yes` | Auto-accept AI recommendations |
|
||||
| `-p` | `--push` | Push to remote after commit |
|
||||
| `-t` | `--test` | Run tests before committing |
|
||||
| `-b` | `--build` | Build after commit, verify clean tree |
|
||||
| `-r` | `--release` | Publish to configured npm registries |
|
||||
| | `--format` | Run format before committing |
|
||||
| Flag | Long Form | Description |
|
||||
| ---- | ----------- | ---------------------------------------- |
|
||||
| `-y` | `--yes` | Auto-accept AI recommendations |
|
||||
| `-p` | `--push` | Push to remote after commit |
|
||||
| `-t` | `--test` | Run tests before committing |
|
||||
| `-b` | `--build` | Build after commit, verify clean tree |
|
||||
| `-r` | `--release` | Publish to configured npm registries |
|
||||
| | `--format` | Run format before committing |
|
||||
| | `--json` | Emit JSON for `gitzone commit recommend` |
|
||||
|
||||
**Workflow steps:**
|
||||
|
||||
@@ -94,6 +98,9 @@ Automatically format and standardize your entire codebase. **Dry-run by default*
|
||||
# Preview what would change (default behavior)
|
||||
gitzone format
|
||||
|
||||
# Emit a machine-readable plan
|
||||
gitzone format plan --json
|
||||
|
||||
# Apply changes
|
||||
gitzone format --write
|
||||
|
||||
@@ -109,22 +116,22 @@ gitzone format --verbose
|
||||
|
||||
**Flags:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--write` / `-w` | Apply changes (default is dry-run) |
|
||||
| `--yes` | Auto-approve without interactive confirmation |
|
||||
| `--plan-only` | Only show what would be done |
|
||||
| `--save-plan <file>` | Save the format plan to a file |
|
||||
| `--from-plan <file>` | Load and execute a saved plan |
|
||||
| `--detailed` | Show detailed stats and save report |
|
||||
| `--parallel` / `--no-parallel` | Toggle parallel execution |
|
||||
| `--verbose` | Enable verbose logging |
|
||||
| `--diff` | Show file diffs |
|
||||
| Flag | Description |
|
||||
| -------------------- | --------------------------------------------- |
|
||||
| `--write` / `-w` | Apply changes (default is dry-run) |
|
||||
| `--yes` | Auto-approve without interactive confirmation |
|
||||
| `--plan-only` | Only show what would be done |
|
||||
| `--save-plan <file>` | Save the format plan to a file |
|
||||
| `--from-plan <file>` | Load and execute a saved plan |
|
||||
| `--detailed` | Show detailed stats and save report |
|
||||
| `--verbose` | Enable verbose logging |
|
||||
| `--diff` | Show file diffs |
|
||||
| `--json` | Emit a read-only format plan as JSON |
|
||||
|
||||
**Formatters (executed in order):**
|
||||
|
||||
1. 🧹 **Cleanup** — removes obsolete files (yarn.lock, package-lock.json, tslint.json, etc.)
|
||||
2. ⚙️ **Npmextra** — formats and standardizes `npmextra.json`
|
||||
2. ⚙️ **Smartconfig** — formats and standardizes `.smartconfig.json`
|
||||
3. 📜 **License** — ensures proper licensing and checks dependency licenses
|
||||
4. 📦 **Package.json** — standardizes package configuration
|
||||
5. 📋 **Templates** — applies project template updates
|
||||
@@ -144,18 +151,21 @@ gitzone services [command]
|
||||
|
||||
**Commands:**
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `start [service]` | Start services (`mongo`\|`s3`\|`elasticsearch`\|`all`) |
|
||||
| `stop [service]` | Stop services |
|
||||
| `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 (default: 20 lines) |
|
||||
| `reconfigure` | Reassign ports and restart all services |
|
||||
| `remove` | Remove containers (preserves data) |
|
||||
| `clean` | Remove containers AND data (⚠️ destructive) |
|
||||
| Command | Description |
|
||||
| ------------------------ | ------------------------------------------------------ |
|
||||
| `start [service]` | Start services (`mongo`\|`s3`\|`elasticsearch`\|`all`) |
|
||||
| `stop [service]` | Stop services |
|
||||
| `restart [service]` | Restart services |
|
||||
| `status` | Show current service status |
|
||||
| `config` | Display configuration details |
|
||||
| `set <csv>` | Set enabled services without prompts |
|
||||
| `enable <service...>` | Enable one or more services |
|
||||
| `disable <service...>` | Disable one or more services |
|
||||
| `compass` | Get MongoDB Compass connection string with network IP |
|
||||
| `logs [service] [lines]` | View service logs (default: 20 lines) |
|
||||
| `reconfigure` | Reassign ports and restart all services |
|
||||
| `remove` | Remove containers (preserves data) |
|
||||
| `clean` | Remove containers AND data (⚠️ destructive) |
|
||||
|
||||
**Service aliases:**
|
||||
|
||||
@@ -195,6 +205,9 @@ gitzone services cleanup -g
|
||||
# Start all services for your project
|
||||
gitzone services start
|
||||
|
||||
# Configure enabled services without prompts
|
||||
gitzone services set mongodb,minio
|
||||
|
||||
# Check what's running
|
||||
gitzone services status
|
||||
|
||||
@@ -217,18 +230,21 @@ Manage release registries and commit settings:
|
||||
gitzone config [subcommand]
|
||||
```
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `show` | Display current release config (registries, access level) |
|
||||
| `add [url]` | Add a registry URL (default: `https://registry.npmjs.org`) |
|
||||
| `remove [url]` | Remove a registry URL (interactive selection if no URL) |
|
||||
| `clear` | Clear all registries (with confirmation) |
|
||||
| `access [public\|private]` | Set npm access level for publishing |
|
||||
| `commit alwaysTest [true\|false]` | Always run tests before commit |
|
||||
| `commit alwaysBuild [true\|false]` | Always build after commit |
|
||||
| `services` | Configure which services are enabled |
|
||||
| Command | Description |
|
||||
| ---------------------------------- | ---------------------------------------------------------- |
|
||||
| `show` | Display current release config (registries, access level) |
|
||||
| `get <path>` | Read a single value from `@git.zone/cli` |
|
||||
| `set <path> <value>` | Write a single value to `@git.zone/cli` |
|
||||
| `unset <path>` | Remove a single value from `@git.zone/cli` |
|
||||
| `add [url]` | Add a registry URL (default: `https://registry.npmjs.org`) |
|
||||
| `remove [url]` | Remove a registry URL (interactive selection if no URL) |
|
||||
| `clear` | Clear all registries (with confirmation) |
|
||||
| `access [public\|private]` | Set npm access level for publishing |
|
||||
| `commit alwaysTest [true\|false]` | Always run tests before commit |
|
||||
| `commit alwaysBuild [true\|false]` | Always build after commit |
|
||||
| `services` | Configure which services are enabled |
|
||||
|
||||
Configuration is stored in `npmextra.json` under the `@git.zone/cli` key.
|
||||
Configuration is stored in `.smartconfig.json` under the `@git.zone/cli` key.
|
||||
|
||||
### 📦 Project Templates
|
||||
|
||||
@@ -323,44 +339,33 @@ gitzone helpers shortid
|
||||
|
||||
## 📋 Configuration
|
||||
|
||||
### npmextra.json
|
||||
### .smartconfig.json
|
||||
|
||||
Customize gitzone behavior through `npmextra.json`:
|
||||
Customize gitzone behavior through `.smartconfig.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"@git.zone/cli": {
|
||||
"projectType": "npm",
|
||||
"cli": {
|
||||
"interactive": true,
|
||||
"output": "human",
|
||||
"checkUpdates": true
|
||||
},
|
||||
"release": {
|
||||
"registries": [
|
||||
"https://registry.npmjs.org"
|
||||
],
|
||||
"registries": ["https://registry.npmjs.org"],
|
||||
"accessLevel": "public"
|
||||
},
|
||||
"commit": {
|
||||
"alwaysTest": false,
|
||||
"alwaysBuild": false
|
||||
}
|
||||
},
|
||||
"gitzone": {
|
||||
},
|
||||
"format": {
|
||||
"interactive": true,
|
||||
"parallel": true,
|
||||
"showStats": true,
|
||||
"cache": {
|
||||
"enabled": true,
|
||||
"clean": true
|
||||
},
|
||||
"modules": {
|
||||
"skip": ["prettier"],
|
||||
"only": [],
|
||||
"order": []
|
||||
},
|
||||
"licenses": {
|
||||
"allowed": ["MIT", "Apache-2.0"],
|
||||
"exceptions": {
|
||||
"some-package": "GPL-3.0"
|
||||
}
|
||||
"only": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -408,6 +413,23 @@ gitzone services stop
|
||||
gitzone commit -ytbpr
|
||||
```
|
||||
|
||||
### Agent-Friendly Inspection
|
||||
|
||||
```bash
|
||||
# Top-level machine-readable help
|
||||
gitzone help config --json
|
||||
|
||||
# Read-only commit recommendation
|
||||
gitzone commit recommend --json
|
||||
|
||||
# Read-only format plan
|
||||
gitzone format plan --json
|
||||
|
||||
# Read or change config without prompts
|
||||
gitzone config get release.accessLevel
|
||||
gitzone config set cli.interactive false
|
||||
```
|
||||
|
||||
### Multi-Repository Management
|
||||
|
||||
```bash
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/cli',
|
||||
version: '2.13.9',
|
||||
version: '2.14.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.'
|
||||
}
|
||||
|
||||
@@ -38,11 +38,11 @@ export class GitzoneConfig {
|
||||
public data: IGitzoneConfigData;
|
||||
|
||||
public async readConfigFromCwd() {
|
||||
const npmextraInstance = new plugins.npmextra.Npmextra(paths.cwd);
|
||||
this.data = npmextraInstance.dataFor<IGitzoneConfigData>('@git.zone/cli', {});
|
||||
const smartconfigInstance = new plugins.smartconfig.Smartconfig(paths.cwd);
|
||||
this.data = smartconfigInstance.dataFor<IGitzoneConfigData>('@git.zone/cli', {});
|
||||
|
||||
// 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
|
||||
const accessLevel =
|
||||
|
||||
+50
-55
@@ -1,23 +1,29 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
import { GitzoneConfig } from './classes.gitzoneconfig.js';
|
||||
import * as plugins from "./plugins.js";
|
||||
import * as paths from "./paths.js";
|
||||
import { GitzoneConfig } from "./classes.gitzoneconfig.js";
|
||||
import { getRawCliMode } from "./helpers.climode.js";
|
||||
|
||||
const gitzoneSmartcli = new plugins.smartcli.Smartcli();
|
||||
|
||||
export let run = async () => {
|
||||
const done = plugins.smartpromise.defer();
|
||||
const rawCliMode = await getRawCliMode();
|
||||
|
||||
// get packageInfo
|
||||
const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||
|
||||
// check for updates
|
||||
const smartupdateInstance = new plugins.smartupdate.SmartUpdate();
|
||||
await smartupdateInstance.check(
|
||||
'gitzone',
|
||||
projectInfo.npm.version,
|
||||
'http://gitzone.gitlab.io/gitzone/changelog.html',
|
||||
);
|
||||
console.log('---------------------------------------------');
|
||||
if (rawCliMode.checkUpdates) {
|
||||
const smartupdateInstance = new plugins.smartupdate.SmartUpdate();
|
||||
await smartupdateInstance.check(
|
||||
"gitzone",
|
||||
projectInfo.npm.version,
|
||||
"http://gitzone.gitlab.io/gitzone/changelog.html",
|
||||
);
|
||||
}
|
||||
if (rawCliMode.output === "human") {
|
||||
console.log("---------------------------------------------");
|
||||
}
|
||||
gitzoneSmartcli.addVersion(projectInfo.npm.version);
|
||||
|
||||
// ======> Standard task <======
|
||||
@@ -26,8 +32,13 @@ export let run = async () => {
|
||||
* standard task
|
||||
*/
|
||||
gitzoneSmartcli.standardCommand().subscribe(async (argvArg) => {
|
||||
const modStandard = await import('./mod_standard/index.js');
|
||||
await modStandard.run();
|
||||
const modStandard = await import("./mod_standard/index.js");
|
||||
await modStandard.run(argvArg);
|
||||
});
|
||||
|
||||
gitzoneSmartcli.addCommand("help").subscribe(async (argvArg) => {
|
||||
const modStandard = await import("./mod_standard/index.js");
|
||||
await modStandard.run(argvArg);
|
||||
});
|
||||
|
||||
// ======> Specific tasks <======
|
||||
@@ -35,62 +46,46 @@ export let run = async () => {
|
||||
/**
|
||||
* commit something
|
||||
*/
|
||||
gitzoneSmartcli.addCommand('commit').subscribe(async (argvArg) => {
|
||||
const modCommit = await import('./mod_commit/index.js');
|
||||
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');
|
||||
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');
|
||||
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) => {
|
||||
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']) {
|
||||
await modFormat.handleListBackups();
|
||||
return;
|
||||
}
|
||||
|
||||
if (argvArg['clean-backups']) {
|
||||
await modFormat.handleCleanBackups();
|
||||
return;
|
||||
}
|
||||
const modFormat = await import("./mod_format/index.js");
|
||||
|
||||
// Handle format with options
|
||||
// Default is dry-mode, use --write/-w to apply changes
|
||||
await modFormat.run({
|
||||
...argvArg,
|
||||
write: argvArg.write || argvArg.w,
|
||||
dryRun: argvArg['dry-run'],
|
||||
dryRun: argvArg["dry-run"],
|
||||
yes: argvArg.yes,
|
||||
planOnly: argvArg['plan-only'],
|
||||
savePlan: argvArg['save-plan'],
|
||||
fromPlan: argvArg['from-plan'],
|
||||
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,
|
||||
});
|
||||
@@ -99,54 +94,54 @@ export let run = async () => {
|
||||
/**
|
||||
* run meta commands
|
||||
*/
|
||||
gitzoneSmartcli.addCommand('meta').subscribe(async (argvArg) => {
|
||||
gitzoneSmartcli.addCommand("meta").subscribe(async (argvArg) => {
|
||||
const config = GitzoneConfig.fromCwd();
|
||||
const modMeta = await import('./mod_meta/index.js');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
gitzoneSmartcli.addCommand("services").subscribe(async (argvArg) => {
|
||||
const modServices = await import("./mod_services/index.js");
|
||||
await modServices.run(argvArg);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
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;
|
||||
};
|
||||
|
||||
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(process.argv.slice(2));
|
||||
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;
|
||||
};
|
||||
+328
-101
@@ -1,21 +1,49 @@
|
||||
// this file contains code to create commits in a consistent way
|
||||
|
||||
import * as plugins from './mod.plugins.js';
|
||||
import * as paths from '../paths.js';
|
||||
import { logger } from '../gitzone.logging.js';
|
||||
import * as helpers from './mod.helpers.js';
|
||||
import * as ui from './mod.ui.js';
|
||||
import { ReleaseConfig } from '../mod_config/classes.releaseconfig.js';
|
||||
import * as plugins from "./mod.plugins.js";
|
||||
import * as paths from "../paths.js";
|
||||
import { logger } from "../gitzone.logging.js";
|
||||
import * as helpers from "./mod.helpers.js";
|
||||
import * as ui from "./mod.ui.js";
|
||||
import { ReleaseConfig } from "../mod_config/classes.releaseconfig.js";
|
||||
import type { ICliMode } from "../helpers.climode.js";
|
||||
import {
|
||||
getCliMode,
|
||||
printJson,
|
||||
runWithSuppressedOutput,
|
||||
} from "../helpers.climode.js";
|
||||
|
||||
export const run = async (argvArg: any) => {
|
||||
// Read commit config from npmextra.json
|
||||
const npmextraConfig = new plugins.npmextra.Npmextra();
|
||||
const gitzoneConfig = npmextraConfig.dataFor<{
|
||||
const mode = await getCliMode(argvArg);
|
||||
const subcommand = argvArg._?.[1];
|
||||
|
||||
if (mode.help || subcommand === "help") {
|
||||
showHelp(mode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === "recommend") {
|
||||
await handleRecommend(mode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode.json) {
|
||||
printJson({
|
||||
ok: false,
|
||||
error:
|
||||
"JSON output is only supported for the read-only recommendation flow. Use `gitzone commit recommend --json`.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Read commit config from .smartconfig.json
|
||||
const smartconfigInstance = new plugins.smartconfig.Smartconfig();
|
||||
const gitzoneConfig = smartconfigInstance.dataFor<{
|
||||
commit?: {
|
||||
alwaysTest?: boolean;
|
||||
alwaysBuild?: boolean;
|
||||
};
|
||||
}>('@git.zone/cli', {});
|
||||
}>("@git.zone/cli", {});
|
||||
const commitConfig = gitzoneConfig.commit || {};
|
||||
|
||||
// Check flags and merge with config options
|
||||
@@ -27,10 +55,12 @@ export const run = async (argvArg: any) => {
|
||||
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('');
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -47,26 +77,26 @@ export const run = async (argvArg: any) => {
|
||||
});
|
||||
|
||||
if (argvArg.format) {
|
||||
const formatMod = await import('../mod_format/index.js');
|
||||
const formatMod = await import("../mod_format/index.js");
|
||||
await formatMod.run();
|
||||
}
|
||||
|
||||
// Run tests early to fail fast before analysis
|
||||
if (wantsTest) {
|
||||
ui.printHeader('🧪 Running tests...');
|
||||
ui.printHeader("🧪 Running tests...");
|
||||
const smartshellForTest = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
executor: "bash",
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
const testResult = await smartshellForTest.exec('pnpm test');
|
||||
const testResult = await smartshellForTest.exec("pnpm test");
|
||||
if (testResult.exitCode !== 0) {
|
||||
logger.log('error', 'Tests failed. Aborting commit.');
|
||||
logger.log("error", "Tests failed. Aborting commit.");
|
||||
process.exit(1);
|
||||
}
|
||||
logger.log('success', 'All tests passed.');
|
||||
logger.log("success", "All tests passed.");
|
||||
}
|
||||
|
||||
ui.printHeader('🔍 Analyzing repository changes...');
|
||||
ui.printHeader("🔍 Analyzing repository changes...");
|
||||
|
||||
const aidoc = new plugins.tsdoc.AiDoc();
|
||||
await aidoc.start();
|
||||
@@ -79,58 +109,63 @@ export const run = async (argvArg: any) => {
|
||||
recommendedNextVersion: nextCommitObject.recommendedNextVersion,
|
||||
recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel,
|
||||
recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope,
|
||||
recommendedNextVersionMessage: nextCommitObject.recommendedNextVersionMessage,
|
||||
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 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)');
|
||||
logger.log("info", "✓ Auto-accepting AI recommendations (--yes flag)");
|
||||
|
||||
answerBucket = new plugins.smartinteract.AnswerBucket();
|
||||
answerBucket.addAnswer({
|
||||
name: 'commitType',
|
||||
name: "commitType",
|
||||
value: nextCommitObject.recommendedNextVersionLevel,
|
||||
});
|
||||
answerBucket.addAnswer({
|
||||
name: 'commitScope',
|
||||
name: "commitScope",
|
||||
value: nextCommitObject.recommendedNextVersionScope,
|
||||
});
|
||||
answerBucket.addAnswer({
|
||||
name: 'commitDescription',
|
||||
name: "commitDescription",
|
||||
value: nextCommitObject.recommendedNextVersionMessage,
|
||||
});
|
||||
answerBucket.addAnswer({
|
||||
name: 'pushToOrigin',
|
||||
name: "pushToOrigin",
|
||||
value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided
|
||||
});
|
||||
answerBucket.addAnswer({
|
||||
name: 'createRelease',
|
||||
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');
|
||||
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',
|
||||
type: "list",
|
||||
name: `commitType`,
|
||||
message: `Choose TYPE of the commit:`,
|
||||
choices: [`fix`, `feat`, `BREAKING CHANGE`],
|
||||
default: nextCommitObject.recommendedNextVersionLevel,
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
type: "input",
|
||||
name: `commitScope`,
|
||||
message: `What is the SCOPE of the commit:`,
|
||||
default: nextCommitObject.recommendedNextVersionScope,
|
||||
@@ -142,13 +177,13 @@ export const run = async (argvArg: any) => {
|
||||
default: nextCommitObject.recommendedNextVersionMessage,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
type: "confirm",
|
||||
name: `pushToOrigin`,
|
||||
message: `Do you want to push this version now?`,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
type: "confirm",
|
||||
name: `createRelease`,
|
||||
message: `Do you want to publish to npm registries?`,
|
||||
default: wantsRelease,
|
||||
@@ -157,40 +192,50 @@ export const run = async (argvArg: any) => {
|
||||
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';
|
||||
}
|
||||
})();
|
||||
const commitType = answerBucket.getAnswerFor("commitType");
|
||||
let commitVersionType: helpers.VersionType;
|
||||
switch (commitType) {
|
||||
case "fix":
|
||||
commitVersionType = "patch";
|
||||
break;
|
||||
case "feat":
|
||||
commitVersionType = "minor";
|
||||
break;
|
||||
case "BREAKING CHANGE":
|
||||
commitVersionType = "major";
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported commit type: ${commitType}`);
|
||||
}
|
||||
|
||||
ui.printHeader('✨ Creating Semantic Commit');
|
||||
ui.printHeader("✨ Creating Semantic Commit");
|
||||
ui.printCommitMessage(commitString);
|
||||
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
executor: "bash",
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
// Load release config if user wants to release (interactively selected)
|
||||
if (answerBucket.getAnswerFor('createRelease') && !releaseConfig) {
|
||||
if (answerBucket.getAnswerFor("createRelease") && !releaseConfig) {
|
||||
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('');
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine total steps based on options
|
||||
// Note: test runs early (like format) so not counted in numbered steps
|
||||
const willPush = answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true');
|
||||
const willRelease = answerBucket.getAnswerFor('createRelease') && releaseConfig?.hasRegistries();
|
||||
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++;
|
||||
@@ -199,96 +244,156 @@ export const run = async (argvArg: any) => {
|
||||
|
||||
// Step 1: Baking commitinfo
|
||||
currentStep++;
|
||||
ui.printStep(currentStep, totalSteps, '🔧 Baking commit info into code', 'in-progress');
|
||||
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');
|
||||
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;
|
||||
ui.printStep(
|
||||
currentStep,
|
||||
totalSteps,
|
||||
"📄 Generating changelog.md",
|
||||
"in-progress",
|
||||
);
|
||||
let changelog = nextCommitObject.changelog || "# Changelog\n";
|
||||
changelog = changelog.replaceAll(
|
||||
'{{nextVersion}}',
|
||||
"{{nextVersion}}",
|
||||
(await commitInfo.getNextPlannedVersion()).versionString,
|
||||
);
|
||||
changelog = changelog.replaceAll(
|
||||
'{{nextVersionScope}}',
|
||||
`${await answerBucket.getAnswerFor('commitType')}(${await answerBucket.getAnswerFor('commitScope')})`,
|
||||
"{{nextVersionScope}}",
|
||||
`${await answerBucket.getAnswerFor("commitType")}(${await answerBucket.getAnswerFor("commitScope")})`,
|
||||
);
|
||||
changelog = changelog.replaceAll(
|
||||
'{{nextVersionMessage}}',
|
||||
"{{nextVersionMessage}}",
|
||||
nextCommitObject.recommendedNextVersionMessage,
|
||||
);
|
||||
if (nextCommitObject.recommendedNextVersionDetails?.length > 0) {
|
||||
changelog = changelog.replaceAll(
|
||||
'{{nextVersionDetails}}',
|
||||
'- ' + nextCommitObject.recommendedNextVersionDetails.join('\n- '),
|
||||
"{{nextVersionDetails}}",
|
||||
"- " + nextCommitObject.recommendedNextVersionDetails.join("\n- "),
|
||||
);
|
||||
} else {
|
||||
changelog = changelog.replaceAll('\n{{nextVersionDetails}}', '');
|
||||
changelog = changelog.replaceAll("\n{{nextVersionDetails}}", "");
|
||||
}
|
||||
|
||||
await plugins.smartfs
|
||||
.file(plugins.path.join(paths.cwd, `changelog.md`))
|
||||
.encoding('utf8')
|
||||
.encoding("utf8")
|
||||
.write(changelog);
|
||||
ui.printStep(currentStep, totalSteps, '📄 Generating changelog.md', 'done');
|
||||
ui.printStep(currentStep, totalSteps, "📄 Generating changelog.md", "done");
|
||||
|
||||
// Step 3: Staging files
|
||||
currentStep++;
|
||||
ui.printStep(currentStep, totalSteps, '📦 Staging files', 'in-progress');
|
||||
ui.printStep(currentStep, totalSteps, "📦 Staging files", "in-progress");
|
||||
await smartshellInstance.exec(`git add -A`);
|
||||
ui.printStep(currentStep, totalSteps, '📦 Staging files', 'done');
|
||||
ui.printStep(currentStep, totalSteps, "📦 Staging files", "done");
|
||||
|
||||
// Step 4: Creating commit
|
||||
currentStep++;
|
||||
ui.printStep(currentStep, totalSteps, '💾 Creating git commit', 'in-progress');
|
||||
ui.printStep(
|
||||
currentStep,
|
||||
totalSteps,
|
||||
"💾 Creating git commit",
|
||||
"in-progress",
|
||||
);
|
||||
await smartshellInstance.exec(`git commit -m "${commitString}"`);
|
||||
ui.printStep(currentStep, totalSteps, '💾 Creating git commit', 'done');
|
||||
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);
|
||||
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');
|
||||
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.');
|
||||
ui.printStep(currentStep, totalSteps, "🔨 Running build", "error");
|
||||
logger.log("error", "Build failed. Aborting release.");
|
||||
process.exit(1);
|
||||
}
|
||||
ui.printStep(currentStep, totalSteps, '🔨 Running build', 'done');
|
||||
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:');
|
||||
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');
|
||||
logger.log(
|
||||
"error",
|
||||
"Aborting release. Please ensure build artifacts are in .gitignore",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'done');
|
||||
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');
|
||||
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)
|
||||
@@ -296,51 +401,173 @@ export const run = async (argvArg: any) => {
|
||||
if (willRelease && releaseConfig) {
|
||||
currentStep++;
|
||||
const registries = releaseConfig.getRegistries();
|
||||
ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'in-progress');
|
||||
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}`);
|
||||
await smartshellInstance.exec(
|
||||
`npm publish --registry=${registry} --access=${accessLevel}`,
|
||||
);
|
||||
releasedRegistries.push(registry);
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to publish to ${registry}: ${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');
|
||||
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');
|
||||
ui.printStep(
|
||||
currentStep,
|
||||
totalSteps,
|
||||
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
|
||||
"error",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(''); // Add spacing before summary
|
||||
console.log(""); // Add spacing before summary
|
||||
|
||||
// Get commit SHA for summary
|
||||
const commitShaResult = await smartshellInstance.exec('git rev-parse --short HEAD');
|
||||
const commitShaResult = await smartshellInstance.exec(
|
||||
"git rev-parse --short HEAD",
|
||||
);
|
||||
const commitSha = commitShaResult.stdout.trim();
|
||||
|
||||
// Print final summary
|
||||
ui.printSummary({
|
||||
projectType,
|
||||
branch: currentBranch,
|
||||
commitType: answerBucket.getAnswerFor('commitType'),
|
||||
commitScope: answerBucket.getAnswerFor('commitScope'),
|
||||
commitMessage: answerBucket.getAnswerFor('commitDescription'),
|
||||
commitType: answerBucket.getAnswerFor("commitType"),
|
||||
commitScope: answerBucket.getAnswerFor("commitScope"),
|
||||
commitMessage: answerBucket.getAnswerFor("commitDescription"),
|
||||
newVersion: newVersion,
|
||||
commitSha: commitSha,
|
||||
pushed: willPush,
|
||||
released: releasedRegistries.length > 0,
|
||||
releasedRegistries: releasedRegistries.length > 0 ? releasedRegistries : undefined,
|
||||
releasedRegistries:
|
||||
releasedRegistries.length > 0 ? releasedRegistries : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
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 = (
|
||||
answerBucket: plugins.smartinteract.AnswerBucket,
|
||||
) => {
|
||||
const commitType = answerBucket.getAnswerFor('commitType');
|
||||
const commitScope = answerBucket.getAnswerFor('commitScope');
|
||||
const commitDescription = answerBucket.getAnswerFor('commitDescription');
|
||||
const commitType = answerBucket.getAnswerFor("commitType");
|
||||
const commitScope = answerBucket.getAnswerFor("commitScope");
|
||||
const commitDescription = answerBucket.getAnswerFor("commitDescription");
|
||||
return `${commitType}(${commitScope}): ${commitDescription}`;
|
||||
};
|
||||
|
||||
export function showHelp(mode?: ICliMode): void {
|
||||
if (mode?.json) {
|
||||
printJson({
|
||||
command: "commit",
|
||||
usage: "gitzone commit [recommend] [options]",
|
||||
description:
|
||||
"Creates semantic commits or emits a read-only recommendation.",
|
||||
commands: [
|
||||
{
|
||||
name: "recommend",
|
||||
description:
|
||||
"Generate a commit recommendation without mutating the repository",
|
||||
},
|
||||
],
|
||||
flags: [
|
||||
{ flag: "-y, --yes", description: "Auto-accept AI recommendations" },
|
||||
{ flag: "-p, --push", description: "Push to origin after commit" },
|
||||
{ flag: "-t, --test", description: "Run tests before the commit flow" },
|
||||
{
|
||||
flag: "-b, --build",
|
||||
description: "Run the build after the commit flow",
|
||||
},
|
||||
{
|
||||
flag: "-r, --release",
|
||||
description: "Publish to configured registries after push",
|
||||
},
|
||||
{
|
||||
flag: "--format",
|
||||
description: "Run gitzone format before committing",
|
||||
},
|
||||
{
|
||||
flag: "--json",
|
||||
description: "Emit JSON for `commit recommend` only",
|
||||
},
|
||||
],
|
||||
examples: [
|
||||
"gitzone commit recommend --json",
|
||||
"gitzone commit -y",
|
||||
"gitzone commit -ypbr",
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log("Usage: gitzone commit [recommend] [options]");
|
||||
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 AI recommendations");
|
||||
console.log(" -p, --push Push to origin after commit");
|
||||
console.log(" -t, --test Run tests before the commit flow");
|
||||
console.log(" -b, --build Run the build after the commit flow");
|
||||
console.log(
|
||||
" -r, --release Publish to configured registries after push",
|
||||
);
|
||||
console.log(" --format Run gitzone format before committing");
|
||||
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 -ypbr");
|
||||
console.log("");
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface ICommitConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages commit configuration stored in npmextra.json
|
||||
* Manages commit configuration stored in .smartconfig.json
|
||||
* under @git.zone/cli.commit namespace
|
||||
*/
|
||||
export class CommitConfig {
|
||||
@@ -28,11 +28,11 @@ export class CommitConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from npmextra.json
|
||||
* Load configuration from .smartconfig.json
|
||||
*/
|
||||
public async load(): Promise<void> {
|
||||
const npmextraInstance = new plugins.npmextra.Npmextra(this.cwd);
|
||||
const gitzoneConfig = npmextraInstance.dataFor<any>('@git.zone/cli', {});
|
||||
const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
|
||||
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
|
||||
|
||||
this.config = {
|
||||
alwaysTest: gitzoneConfig?.commit?.alwaysTest ?? false,
|
||||
@@ -41,37 +41,37 @@ export class CommitConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Save configuration to npmextra.json
|
||||
* Save configuration to .smartconfig.json
|
||||
*/
|
||||
public async save(): Promise<void> {
|
||||
const npmextraPath = plugins.path.join(this.cwd, 'npmextra.json');
|
||||
let npmextraData: any = {};
|
||||
const smartconfigPath = plugins.path.join(this.cwd, '.smartconfig.json');
|
||||
let smartconfigData: any = {};
|
||||
|
||||
// Read existing npmextra.json
|
||||
if (await plugins.smartfs.file(npmextraPath).exists()) {
|
||||
const content = await plugins.smartfs.file(npmextraPath).encoding('utf8').read();
|
||||
npmextraData = JSON.parse(content as string);
|
||||
// Read existing .smartconfig.json
|
||||
if (await plugins.smartfs.file(smartconfigPath).exists()) {
|
||||
const content = await plugins.smartfs.file(smartconfigPath).encoding('utf8').read();
|
||||
smartconfigData = JSON.parse(content as string);
|
||||
}
|
||||
|
||||
// Ensure @git.zone/cli namespace exists
|
||||
if (!npmextraData['@git.zone/cli']) {
|
||||
npmextraData['@git.zone/cli'] = {};
|
||||
if (!smartconfigData['@git.zone/cli']) {
|
||||
smartconfigData['@git.zone/cli'] = {};
|
||||
}
|
||||
|
||||
// Ensure commit object exists
|
||||
if (!npmextraData['@git.zone/cli'].commit) {
|
||||
npmextraData['@git.zone/cli'].commit = {};
|
||||
if (!smartconfigData['@git.zone/cli'].commit) {
|
||||
smartconfigData['@git.zone/cli'].commit = {};
|
||||
}
|
||||
|
||||
// Update commit settings
|
||||
npmextraData['@git.zone/cli'].commit.alwaysTest = this.config.alwaysTest;
|
||||
npmextraData['@git.zone/cli'].commit.alwaysBuild = this.config.alwaysBuild;
|
||||
smartconfigData['@git.zone/cli'].commit.alwaysTest = this.config.alwaysTest;
|
||||
smartconfigData['@git.zone/cli'].commit.alwaysBuild = this.config.alwaysBuild;
|
||||
|
||||
// Write back to file
|
||||
await plugins.smartfs
|
||||
.file(npmextraPath)
|
||||
.file(smartconfigPath)
|
||||
.encoding('utf8')
|
||||
.write(JSON.stringify(npmextraData, null, 2));
|
||||
.write(JSON.stringify(smartconfigData, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
export class ReleaseConfig {
|
||||
@@ -30,14 +30,14 @@ export class ReleaseConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from npmextra.json
|
||||
* Load configuration from .smartconfig.json
|
||||
*/
|
||||
public async load(): Promise<void> {
|
||||
const npmextraInstance = new plugins.npmextra.Npmextra(this.cwd);
|
||||
const gitzoneConfig = npmextraInstance.dataFor<any>('@git.zone/cli', {});
|
||||
const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
|
||||
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
|
||||
|
||||
// Also check szci for backward compatibility
|
||||
const szciConfig = npmextraInstance.dataFor<any>('@ship.zone/szci', {});
|
||||
const szciConfig = smartconfigInstance.dataFor<any>('@ship.zone/szci', {});
|
||||
|
||||
this.config = {
|
||||
registries: gitzoneConfig?.release?.registries || [],
|
||||
@@ -46,37 +46,37 @@ export class ReleaseConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Save configuration to npmextra.json
|
||||
* Save configuration to .smartconfig.json
|
||||
*/
|
||||
public async save(): Promise<void> {
|
||||
const npmextraPath = plugins.path.join(this.cwd, 'npmextra.json');
|
||||
let npmextraData: any = {};
|
||||
const smartconfigPath = plugins.path.join(this.cwd, '.smartconfig.json');
|
||||
let smartconfigData: any = {};
|
||||
|
||||
// Read existing npmextra.json
|
||||
if (await plugins.smartfs.file(npmextraPath).exists()) {
|
||||
const content = await plugins.smartfs.file(npmextraPath).encoding('utf8').read();
|
||||
npmextraData = JSON.parse(content as string);
|
||||
// Read existing .smartconfig.json
|
||||
if (await plugins.smartfs.file(smartconfigPath).exists()) {
|
||||
const content = await plugins.smartfs.file(smartconfigPath).encoding('utf8').read();
|
||||
smartconfigData = JSON.parse(content as string);
|
||||
}
|
||||
|
||||
// Ensure @git.zone/cli namespace exists
|
||||
if (!npmextraData['@git.zone/cli']) {
|
||||
npmextraData['@git.zone/cli'] = {};
|
||||
if (!smartconfigData['@git.zone/cli']) {
|
||||
smartconfigData['@git.zone/cli'] = {};
|
||||
}
|
||||
|
||||
// Ensure release object exists
|
||||
if (!npmextraData['@git.zone/cli'].release) {
|
||||
npmextraData['@git.zone/cli'].release = {};
|
||||
if (!smartconfigData['@git.zone/cli'].release) {
|
||||
smartconfigData['@git.zone/cli'].release = {};
|
||||
}
|
||||
|
||||
// Update registries and accessLevel
|
||||
npmextraData['@git.zone/cli'].release.registries = this.config.registries;
|
||||
npmextraData['@git.zone/cli'].release.accessLevel = this.config.accessLevel;
|
||||
smartconfigData['@git.zone/cli'].release.registries = this.config.registries;
|
||||
smartconfigData['@git.zone/cli'].release.accessLevel = this.config.accessLevel;
|
||||
|
||||
// Write back to file
|
||||
await plugins.smartfs
|
||||
.file(npmextraPath)
|
||||
.file(smartconfigPath)
|
||||
.encoding('utf8')
|
||||
.write(JSON.stringify(npmextraData, null, 2));
|
||||
.write(JSON.stringify(smartconfigData, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+491
-183
@@ -1,73 +1,116 @@
|
||||
// gitzone config - manage release registry configuration
|
||||
|
||||
import * as plugins from './mod.plugins.js';
|
||||
import { ReleaseConfig } from './classes.releaseconfig.js';
|
||||
import { CommitConfig } from './classes.commitconfig.js';
|
||||
import { runFormatter, type ICheckResult } from '../mod_format/index.js';
|
||||
import * as plugins from "./mod.plugins.js";
|
||||
import { ReleaseConfig } from "./classes.releaseconfig.js";
|
||||
import { CommitConfig } from "./classes.commitconfig.js";
|
||||
import { runFormatter, type ICheckResult } from "../mod_format/index.js";
|
||||
import type { ICliMode } from "../helpers.climode.js";
|
||||
import { getCliMode, printJson } from "../helpers.climode.js";
|
||||
import {
|
||||
getCliConfigValueFromData,
|
||||
readSmartconfigFile,
|
||||
setCliConfigValueInData,
|
||||
unsetCliConfigValueInData,
|
||||
writeSmartconfigFile,
|
||||
} from "../helpers.smartconfig.js";
|
||||
|
||||
export { ReleaseConfig, CommitConfig };
|
||||
|
||||
const defaultCliMode: ICliMode = {
|
||||
output: "human",
|
||||
interactive: true,
|
||||
json: false,
|
||||
plain: false,
|
||||
quiet: false,
|
||||
yes: false,
|
||||
help: false,
|
||||
agent: false,
|
||||
checkUpdates: true,
|
||||
isTty: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Format npmextra.json with diff preview
|
||||
* Format .smartconfig.json with diff preview
|
||||
* Shows diff first, asks for confirmation, then applies
|
||||
*/
|
||||
async function formatNpmextraWithDiff(): Promise<void> {
|
||||
async function formatSmartconfigWithDiff(mode: ICliMode): Promise<void> {
|
||||
if (!mode.interactive) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for diffs first
|
||||
const checkResult = await runFormatter('npmextra', {
|
||||
const checkResult = (await runFormatter("smartconfig", {
|
||||
checkOnly: true,
|
||||
showDiff: true,
|
||||
}) as ICheckResult | void;
|
||||
})) as ICheckResult | void;
|
||||
|
||||
if (checkResult && checkResult.hasDiff) {
|
||||
const shouldApply = await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||
'Apply formatting changes to npmextra.json?',
|
||||
true
|
||||
);
|
||||
const shouldApply =
|
||||
await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||
"Apply formatting changes to .smartconfig.json?",
|
||||
true,
|
||||
);
|
||||
if (shouldApply) {
|
||||
await runFormatter('npmextra', { silent: true });
|
||||
await runFormatter("smartconfig", { silent: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const run = async (argvArg: any) => {
|
||||
const mode = await getCliMode(argvArg);
|
||||
const command = argvArg._?.[1];
|
||||
const value = argvArg._?.[2];
|
||||
|
||||
if (mode.help || command === "help") {
|
||||
showHelp(mode);
|
||||
return;
|
||||
}
|
||||
|
||||
// If no command provided, show interactive menu
|
||||
if (!command) {
|
||||
if (!mode.interactive) {
|
||||
showHelp(mode);
|
||||
return;
|
||||
}
|
||||
await handleInteractiveMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 'show':
|
||||
await handleShow();
|
||||
case "show":
|
||||
await handleShow(mode);
|
||||
break;
|
||||
case 'add':
|
||||
await handleAdd(value);
|
||||
case "add":
|
||||
await handleAdd(value, mode);
|
||||
break;
|
||||
case 'remove':
|
||||
await handleRemove(value);
|
||||
case "remove":
|
||||
await handleRemove(value, mode);
|
||||
break;
|
||||
case 'clear':
|
||||
await handleClear();
|
||||
case "clear":
|
||||
await handleClear(mode);
|
||||
break;
|
||||
case 'access':
|
||||
case 'accessLevel':
|
||||
await handleAccessLevel(value);
|
||||
case "access":
|
||||
case "accessLevel":
|
||||
await handleAccessLevel(value, mode);
|
||||
break;
|
||||
case 'commit':
|
||||
await handleCommit(argvArg._?.[2], argvArg._?.[3]);
|
||||
case "commit":
|
||||
await handleCommit(argvArg._?.[2], argvArg._?.[3], mode);
|
||||
break;
|
||||
case 'services':
|
||||
await handleServices();
|
||||
case "services":
|
||||
await handleServices(mode);
|
||||
break;
|
||||
case 'help':
|
||||
showHelp();
|
||||
case "get":
|
||||
await handleGet(value, mode);
|
||||
break;
|
||||
case "set":
|
||||
await handleSet(value, argvArg._?.[3], mode);
|
||||
break;
|
||||
case "unset":
|
||||
await handleUnset(value, mode);
|
||||
break;
|
||||
default:
|
||||
plugins.logger.log('error', `Unknown command: ${command}`);
|
||||
showHelp();
|
||||
plugins.logger.log("error", `Unknown command: ${command}`);
|
||||
showHelp(mode);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,55 +118,61 @@ export const run = async (argvArg: any) => {
|
||||
* Interactive menu for config command
|
||||
*/
|
||||
async function handleInteractiveMenu(): Promise<void> {
|
||||
console.log('');
|
||||
console.log('╭─────────────────────────────────────────────────────────────╮');
|
||||
console.log('│ gitzone config - Project Configuration │');
|
||||
console.log('╰─────────────────────────────────────────────────────────────╯');
|
||||
console.log('');
|
||||
console.log("");
|
||||
console.log(
|
||||
"╭─────────────────────────────────────────────────────────────╮",
|
||||
);
|
||||
console.log(
|
||||
"│ gitzone config - Project Configuration │",
|
||||
);
|
||||
console.log(
|
||||
"╰─────────────────────────────────────────────────────────────╯",
|
||||
);
|
||||
console.log("");
|
||||
|
||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||
const response = await interactInstance.askQuestion({
|
||||
type: 'list',
|
||||
name: 'action',
|
||||
message: 'What would you like to do?',
|
||||
default: 'show',
|
||||
type: "list",
|
||||
name: "action",
|
||||
message: "What would you like to do?",
|
||||
default: "show",
|
||||
choices: [
|
||||
{ name: 'Show current configuration', value: 'show' },
|
||||
{ name: 'Add a registry', value: 'add' },
|
||||
{ name: 'Remove a registry', value: 'remove' },
|
||||
{ name: 'Clear all registries', value: 'clear' },
|
||||
{ name: 'Set access level (public/private)', value: 'access' },
|
||||
{ name: 'Configure commit options', value: 'commit' },
|
||||
{ name: 'Configure services', value: 'services' },
|
||||
{ name: 'Show help', value: 'help' },
|
||||
{ name: "Show current configuration", value: "show" },
|
||||
{ name: "Add a registry", value: "add" },
|
||||
{ name: "Remove a registry", value: "remove" },
|
||||
{ name: "Clear all registries", value: "clear" },
|
||||
{ name: "Set access level (public/private)", value: "access" },
|
||||
{ name: "Configure commit options", value: "commit" },
|
||||
{ name: "Configure services", value: "services" },
|
||||
{ name: "Show help", value: "help" },
|
||||
],
|
||||
});
|
||||
|
||||
const action = (response as any).value;
|
||||
|
||||
switch (action) {
|
||||
case 'show':
|
||||
await handleShow();
|
||||
case "show":
|
||||
await handleShow(defaultCliMode);
|
||||
break;
|
||||
case 'add':
|
||||
await handleAdd();
|
||||
case "add":
|
||||
await handleAdd(undefined, defaultCliMode);
|
||||
break;
|
||||
case 'remove':
|
||||
await handleRemove();
|
||||
case "remove":
|
||||
await handleRemove(undefined, defaultCliMode);
|
||||
break;
|
||||
case 'clear':
|
||||
await handleClear();
|
||||
case "clear":
|
||||
await handleClear(defaultCliMode);
|
||||
break;
|
||||
case 'access':
|
||||
await handleAccessLevel();
|
||||
case "access":
|
||||
await handleAccessLevel(undefined, defaultCliMode);
|
||||
break;
|
||||
case 'commit':
|
||||
await handleCommit();
|
||||
case "commit":
|
||||
await handleCommit(undefined, undefined, defaultCliMode);
|
||||
break;
|
||||
case 'services':
|
||||
await handleServices();
|
||||
case "services":
|
||||
await handleServices(defaultCliMode);
|
||||
break;
|
||||
case 'help':
|
||||
case "help":
|
||||
showHelp();
|
||||
break;
|
||||
}
|
||||
@@ -132,50 +181,69 @@ async function handleInteractiveMenu(): Promise<void> {
|
||||
/**
|
||||
* Show current registry configuration
|
||||
*/
|
||||
async function handleShow(): Promise<void> {
|
||||
async function handleShow(mode: ICliMode): Promise<void> {
|
||||
if (mode.json) {
|
||||
const smartconfigData = await readSmartconfigFile();
|
||||
printJson(getCliConfigValueFromData(smartconfigData, ""));
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await ReleaseConfig.fromCwd();
|
||||
const registries = config.getRegistries();
|
||||
const accessLevel = config.getAccessLevel();
|
||||
|
||||
console.log('');
|
||||
console.log('╭─────────────────────────────────────────────────────────────╮');
|
||||
console.log('│ Release Configuration │');
|
||||
console.log('╰─────────────────────────────────────────────────────────────╯');
|
||||
console.log('');
|
||||
console.log("");
|
||||
console.log(
|
||||
"╭─────────────────────────────────────────────────────────────╮",
|
||||
);
|
||||
console.log(
|
||||
"│ Release Configuration │",
|
||||
);
|
||||
console.log(
|
||||
"╰─────────────────────────────────────────────────────────────╯",
|
||||
);
|
||||
console.log("");
|
||||
|
||||
// Show access level
|
||||
plugins.logger.log('info', `Access Level: ${accessLevel}`);
|
||||
console.log('');
|
||||
plugins.logger.log("info", `Access Level: ${accessLevel}`);
|
||||
console.log("");
|
||||
|
||||
if (registries.length === 0) {
|
||||
plugins.logger.log('info', 'No release registries configured.');
|
||||
console.log('');
|
||||
console.log(' Run `gitzone config add <registry-url>` to add one.');
|
||||
console.log('');
|
||||
plugins.logger.log("info", "No release registries configured.");
|
||||
console.log("");
|
||||
console.log(" Run `gitzone config add <registry-url>` to add one.");
|
||||
console.log("");
|
||||
} else {
|
||||
plugins.logger.log('info', `Configured registries (${registries.length}):`);
|
||||
console.log('');
|
||||
plugins.logger.log("info", `Configured registries (${registries.length}):`);
|
||||
console.log("");
|
||||
registries.forEach((url, index) => {
|
||||
console.log(` ${index + 1}. ${url}`);
|
||||
});
|
||||
console.log('');
|
||||
console.log("");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a registry URL
|
||||
*/
|
||||
async function handleAdd(url?: string): Promise<void> {
|
||||
async function handleAdd(
|
||||
url: string | undefined,
|
||||
mode: ICliMode,
|
||||
): Promise<void> {
|
||||
if (!url) {
|
||||
if (!mode.interactive) {
|
||||
throw new Error("Registry URL is required in non-interactive mode");
|
||||
}
|
||||
|
||||
// Interactive mode
|
||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||
const response = await interactInstance.askQuestion({
|
||||
type: 'input',
|
||||
name: 'registryUrl',
|
||||
message: 'Enter registry URL:',
|
||||
default: 'https://registry.npmjs.org',
|
||||
type: "input",
|
||||
name: "registryUrl",
|
||||
message: "Enter registry URL:",
|
||||
default: "https://registry.npmjs.org",
|
||||
validate: (input: string) => {
|
||||
return !!(input && input.trim() !== '');
|
||||
return !!(input && input.trim() !== "");
|
||||
},
|
||||
});
|
||||
url = (response as any).value;
|
||||
@@ -186,32 +254,48 @@ async function handleAdd(url?: string): Promise<void> {
|
||||
|
||||
if (added) {
|
||||
await config.save();
|
||||
plugins.logger.log('success', `Added registry: ${url}`);
|
||||
await formatNpmextraWithDiff();
|
||||
if (mode.json) {
|
||||
printJson({
|
||||
ok: true,
|
||||
action: "add",
|
||||
registry: url,
|
||||
registries: config.getRegistries(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
plugins.logger.log("success", `Added registry: ${url}`);
|
||||
await formatSmartconfigWithDiff(mode);
|
||||
} else {
|
||||
plugins.logger.log('warn', `Registry already exists: ${url}`);
|
||||
plugins.logger.log("warn", `Registry already exists: ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a registry URL
|
||||
*/
|
||||
async function handleRemove(url?: string): Promise<void> {
|
||||
async function handleRemove(
|
||||
url: string | undefined,
|
||||
mode: ICliMode,
|
||||
): Promise<void> {
|
||||
const config = await ReleaseConfig.fromCwd();
|
||||
const registries = config.getRegistries();
|
||||
|
||||
if (registries.length === 0) {
|
||||
plugins.logger.log('warn', 'No registries configured to remove.');
|
||||
plugins.logger.log("warn", "No registries configured to remove.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
if (!mode.interactive) {
|
||||
throw new Error("Registry URL is required in non-interactive mode");
|
||||
}
|
||||
|
||||
// Interactive mode - show list to select from
|
||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||
const response = await interactInstance.askQuestion({
|
||||
type: 'list',
|
||||
name: 'registryUrl',
|
||||
message: 'Select registry to remove:',
|
||||
type: "list",
|
||||
name: "registryUrl",
|
||||
message: "Select registry to remove:",
|
||||
choices: registries,
|
||||
default: registries[0],
|
||||
});
|
||||
@@ -222,99 +306,135 @@ async function handleRemove(url?: string): Promise<void> {
|
||||
|
||||
if (removed) {
|
||||
await config.save();
|
||||
plugins.logger.log('success', `Removed registry: ${url}`);
|
||||
await formatNpmextraWithDiff();
|
||||
if (mode.json) {
|
||||
printJson({
|
||||
ok: true,
|
||||
action: "remove",
|
||||
registry: url,
|
||||
registries: config.getRegistries(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
plugins.logger.log("success", `Removed registry: ${url}`);
|
||||
await formatSmartconfigWithDiff(mode);
|
||||
} else {
|
||||
plugins.logger.log('warn', `Registry not found: ${url}`);
|
||||
plugins.logger.log("warn", `Registry not found: ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registries
|
||||
*/
|
||||
async function handleClear(): Promise<void> {
|
||||
async function handleClear(mode: ICliMode): Promise<void> {
|
||||
const config = await ReleaseConfig.fromCwd();
|
||||
|
||||
if (!config.hasRegistries()) {
|
||||
plugins.logger.log('info', 'No registries to clear.');
|
||||
plugins.logger.log("info", "No registries to clear.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm before clearing
|
||||
const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||
'Clear all configured registries?',
|
||||
false
|
||||
);
|
||||
const confirmed = mode.interactive
|
||||
? await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||
"Clear all configured registries?",
|
||||
false,
|
||||
)
|
||||
: true;
|
||||
|
||||
if (confirmed) {
|
||||
config.clearRegistries();
|
||||
await config.save();
|
||||
plugins.logger.log('success', 'All registries cleared.');
|
||||
await formatNpmextraWithDiff();
|
||||
if (mode.json) {
|
||||
printJson({ ok: true, action: "clear", registries: [] });
|
||||
return;
|
||||
}
|
||||
plugins.logger.log("success", "All registries cleared.");
|
||||
await formatSmartconfigWithDiff(mode);
|
||||
} else {
|
||||
plugins.logger.log('info', 'Operation cancelled.');
|
||||
plugins.logger.log("info", "Operation cancelled.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or toggle access level
|
||||
*/
|
||||
async function handleAccessLevel(level?: string): Promise<void> {
|
||||
async function handleAccessLevel(
|
||||
level: string | undefined,
|
||||
mode: ICliMode,
|
||||
): Promise<void> {
|
||||
const config = await ReleaseConfig.fromCwd();
|
||||
const currentLevel = config.getAccessLevel();
|
||||
|
||||
if (!level) {
|
||||
if (!mode.interactive) {
|
||||
throw new Error("Access level is required in non-interactive mode");
|
||||
}
|
||||
|
||||
// Interactive mode - toggle or ask
|
||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||
const response = await interactInstance.askQuestion({
|
||||
type: 'list',
|
||||
name: 'accessLevel',
|
||||
message: 'Select npm access level for publishing:',
|
||||
choices: ['public', 'private'],
|
||||
type: "list",
|
||||
name: "accessLevel",
|
||||
message: "Select npm access level for publishing:",
|
||||
choices: ["public", "private"],
|
||||
default: currentLevel,
|
||||
});
|
||||
level = (response as any).value;
|
||||
}
|
||||
|
||||
// Validate the level
|
||||
if (level !== 'public' && level !== 'private') {
|
||||
plugins.logger.log('error', `Invalid access level: ${level}. Must be 'public' or 'private'.`);
|
||||
if (level !== "public" && level !== "private") {
|
||||
plugins.logger.log(
|
||||
"error",
|
||||
`Invalid access level: ${level}. Must be 'public' or 'private'.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (level === currentLevel) {
|
||||
plugins.logger.log('info', `Access level is already set to: ${level}`);
|
||||
plugins.logger.log("info", `Access level is already set to: ${level}`);
|
||||
return;
|
||||
}
|
||||
|
||||
config.setAccessLevel(level as 'public' | 'private');
|
||||
config.setAccessLevel(level as "public" | "private");
|
||||
await config.save();
|
||||
plugins.logger.log('success', `Access level set to: ${level}`);
|
||||
await formatNpmextraWithDiff();
|
||||
if (mode.json) {
|
||||
printJson({ ok: true, action: "access", accessLevel: level });
|
||||
return;
|
||||
}
|
||||
plugins.logger.log("success", `Access level set to: ${level}`);
|
||||
await formatSmartconfigWithDiff(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle commit configuration
|
||||
*/
|
||||
async function handleCommit(setting?: string, value?: string): Promise<void> {
|
||||
async function handleCommit(
|
||||
setting: string | undefined,
|
||||
value: string | undefined,
|
||||
mode: ICliMode,
|
||||
): Promise<void> {
|
||||
const config = await CommitConfig.fromCwd();
|
||||
|
||||
// No setting = interactive mode
|
||||
if (!setting) {
|
||||
if (!mode.interactive) {
|
||||
throw new Error("Commit setting is required in non-interactive mode");
|
||||
}
|
||||
await handleCommitInteractive(config);
|
||||
return;
|
||||
}
|
||||
|
||||
// Direct setting
|
||||
switch (setting) {
|
||||
case 'alwaysTest':
|
||||
await handleCommitSetting(config, 'alwaysTest', value);
|
||||
case "alwaysTest":
|
||||
await handleCommitSetting(config, "alwaysTest", value, mode);
|
||||
break;
|
||||
case 'alwaysBuild':
|
||||
await handleCommitSetting(config, 'alwaysBuild', value);
|
||||
case "alwaysBuild":
|
||||
await handleCommitSetting(config, "alwaysBuild", value, mode);
|
||||
break;
|
||||
default:
|
||||
plugins.logger.log('error', `Unknown commit setting: ${setting}`);
|
||||
plugins.logger.log("error", `Unknown commit setting: ${setting}`);
|
||||
showCommitHelp();
|
||||
}
|
||||
}
|
||||
@@ -323,109 +443,297 @@ async function handleCommit(setting?: string, value?: string): Promise<void> {
|
||||
* Interactive commit configuration
|
||||
*/
|
||||
async function handleCommitInteractive(config: CommitConfig): Promise<void> {
|
||||
console.log('');
|
||||
console.log('╭─────────────────────────────────────────────────────────────╮');
|
||||
console.log('│ Commit Configuration │');
|
||||
console.log('╰─────────────────────────────────────────────────────────────╯');
|
||||
console.log('');
|
||||
console.log("");
|
||||
console.log(
|
||||
"╭─────────────────────────────────────────────────────────────╮",
|
||||
);
|
||||
console.log(
|
||||
"│ Commit Configuration │",
|
||||
);
|
||||
console.log(
|
||||
"╰─────────────────────────────────────────────────────────────╯",
|
||||
);
|
||||
console.log("");
|
||||
|
||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||
const response = await interactInstance.askQuestion({
|
||||
type: 'checkbox',
|
||||
name: 'commitOptions',
|
||||
message: 'Select commit options to enable:',
|
||||
type: "checkbox",
|
||||
name: "commitOptions",
|
||||
message: "Select commit options to enable:",
|
||||
choices: [
|
||||
{ name: 'Always run tests before commit (-t)', value: 'alwaysTest' },
|
||||
{ name: 'Always build after commit (-b)', value: 'alwaysBuild' },
|
||||
{ name: "Always run tests before commit (-t)", value: "alwaysTest" },
|
||||
{ name: "Always build after commit (-b)", value: "alwaysBuild" },
|
||||
],
|
||||
default: [
|
||||
...(config.getAlwaysTest() ? ['alwaysTest'] : []),
|
||||
...(config.getAlwaysBuild() ? ['alwaysBuild'] : []),
|
||||
...(config.getAlwaysTest() ? ["alwaysTest"] : []),
|
||||
...(config.getAlwaysBuild() ? ["alwaysBuild"] : []),
|
||||
],
|
||||
});
|
||||
|
||||
const selected = (response as any).value || [];
|
||||
config.setAlwaysTest(selected.includes('alwaysTest'));
|
||||
config.setAlwaysBuild(selected.includes('alwaysBuild'));
|
||||
config.setAlwaysTest(selected.includes("alwaysTest"));
|
||||
config.setAlwaysBuild(selected.includes("alwaysBuild"));
|
||||
await config.save();
|
||||
|
||||
plugins.logger.log('success', 'Commit configuration updated');
|
||||
await formatNpmextraWithDiff();
|
||||
plugins.logger.log("success", "Commit configuration updated");
|
||||
await formatSmartconfigWithDiff(defaultCliMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a specific commit setting
|
||||
*/
|
||||
async function handleCommitSetting(config: CommitConfig, setting: string, value?: string): Promise<void> {
|
||||
async function handleCommitSetting(
|
||||
config: CommitConfig,
|
||||
setting: string,
|
||||
value: string | undefined,
|
||||
mode: ICliMode,
|
||||
): Promise<void> {
|
||||
// Parse boolean value
|
||||
const boolValue = value === 'true' || value === '1' || value === 'on';
|
||||
const boolValue = value === "true" || value === "1" || value === "on";
|
||||
|
||||
if (setting === 'alwaysTest') {
|
||||
if (setting === "alwaysTest") {
|
||||
config.setAlwaysTest(boolValue);
|
||||
} else if (setting === 'alwaysBuild') {
|
||||
} else if (setting === "alwaysBuild") {
|
||||
config.setAlwaysBuild(boolValue);
|
||||
}
|
||||
|
||||
await config.save();
|
||||
plugins.logger.log('success', `Set ${setting} to ${boolValue}`);
|
||||
await formatNpmextraWithDiff();
|
||||
if (mode.json) {
|
||||
printJson({ ok: true, action: "commit", setting, value: boolValue });
|
||||
return;
|
||||
}
|
||||
plugins.logger.log("success", `Set ${setting} to ${boolValue}`);
|
||||
await formatSmartconfigWithDiff(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show help for commit subcommand
|
||||
*/
|
||||
function showCommitHelp(): void {
|
||||
console.log('');
|
||||
console.log('Usage: gitzone config commit [setting] [value]');
|
||||
console.log('');
|
||||
console.log('Settings:');
|
||||
console.log(' alwaysTest [true|false] Always run tests before commit');
|
||||
console.log(' alwaysBuild [true|false] Always build after commit');
|
||||
console.log('');
|
||||
console.log('Examples:');
|
||||
console.log(' gitzone config commit # Interactive mode');
|
||||
console.log(' gitzone config commit alwaysTest true');
|
||||
console.log(' gitzone config commit alwaysBuild false');
|
||||
console.log('');
|
||||
console.log("");
|
||||
console.log("Usage: gitzone config commit [setting] [value]");
|
||||
console.log("");
|
||||
console.log("Settings:");
|
||||
console.log(" alwaysTest [true|false] Always run tests before commit");
|
||||
console.log(" alwaysBuild [true|false] Always build after commit");
|
||||
console.log("");
|
||||
console.log("Examples:");
|
||||
console.log(" gitzone config commit # Interactive mode");
|
||||
console.log(" gitzone config commit alwaysTest true");
|
||||
console.log(" gitzone config commit alwaysBuild false");
|
||||
console.log("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle services configuration
|
||||
*/
|
||||
async function handleServices(): Promise<void> {
|
||||
async function handleServices(mode: ICliMode): Promise<void> {
|
||||
if (!mode.interactive) {
|
||||
throw new Error(
|
||||
"Use `gitzone services config --json` or `gitzone services set ...` in non-interactive mode",
|
||||
);
|
||||
}
|
||||
|
||||
// Import and use ServiceManager's configureServices
|
||||
const { ServiceManager } = await import('../mod_services/classes.servicemanager.js');
|
||||
const { ServiceManager } =
|
||||
await import("../mod_services/classes.servicemanager.js");
|
||||
const serviceManager = new ServiceManager();
|
||||
await serviceManager.init();
|
||||
await serviceManager.configureServices();
|
||||
}
|
||||
|
||||
async function handleGet(
|
||||
configPath: string | undefined,
|
||||
mode: ICliMode,
|
||||
): Promise<void> {
|
||||
if (!configPath) {
|
||||
throw new Error("Configuration path is required");
|
||||
}
|
||||
|
||||
const smartconfigData = await readSmartconfigFile();
|
||||
const value = getCliConfigValueFromData(smartconfigData, configPath);
|
||||
|
||||
if (mode.json) {
|
||||
printJson({ path: configPath, value, exists: value !== undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
plugins.logger.log("warn", `No value set for ${configPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
console.log(value);
|
||||
return;
|
||||
}
|
||||
|
||||
printJson(value);
|
||||
}
|
||||
|
||||
async function handleSet(
|
||||
configPath: string | undefined,
|
||||
rawValue: string | undefined,
|
||||
mode: ICliMode,
|
||||
): Promise<void> {
|
||||
if (!configPath) {
|
||||
throw new Error("Configuration path is required");
|
||||
}
|
||||
if (rawValue === undefined) {
|
||||
throw new Error("Configuration value is required");
|
||||
}
|
||||
|
||||
const smartconfigData = await readSmartconfigFile();
|
||||
const parsedValue = parseConfigValue(rawValue);
|
||||
setCliConfigValueInData(smartconfigData, configPath, parsedValue);
|
||||
await writeSmartconfigFile(smartconfigData);
|
||||
|
||||
if (mode.json) {
|
||||
printJson({
|
||||
ok: true,
|
||||
action: "set",
|
||||
path: configPath,
|
||||
value: parsedValue,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
plugins.logger.log("success", `Set ${configPath}`);
|
||||
}
|
||||
|
||||
async function handleUnset(
|
||||
configPath: string | undefined,
|
||||
mode: ICliMode,
|
||||
): Promise<void> {
|
||||
if (!configPath) {
|
||||
throw new Error("Configuration path is required");
|
||||
}
|
||||
|
||||
const smartconfigData = await readSmartconfigFile();
|
||||
const removed = unsetCliConfigValueInData(smartconfigData, configPath);
|
||||
if (!removed) {
|
||||
if (mode.json) {
|
||||
printJson({
|
||||
ok: false,
|
||||
action: "unset",
|
||||
path: configPath,
|
||||
removed: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
plugins.logger.log("warn", `No value set for ${configPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await writeSmartconfigFile(smartconfigData);
|
||||
|
||||
if (mode.json) {
|
||||
printJson({ ok: true, action: "unset", path: configPath, removed: true });
|
||||
return;
|
||||
}
|
||||
|
||||
plugins.logger.log("success", `Unset ${configPath}`);
|
||||
}
|
||||
|
||||
function parseConfigValue(rawValue: string): any {
|
||||
const trimmedValue = rawValue.trim();
|
||||
if (trimmedValue === "true") {
|
||||
return true;
|
||||
}
|
||||
if (trimmedValue === "false") {
|
||||
return false;
|
||||
}
|
||||
if (trimmedValue === "null") {
|
||||
return null;
|
||||
}
|
||||
if (/^-?\d+(\.\d+)?$/.test(trimmedValue)) {
|
||||
return Number(trimmedValue);
|
||||
}
|
||||
if (
|
||||
(trimmedValue.startsWith("{") && trimmedValue.endsWith("}")) ||
|
||||
(trimmedValue.startsWith("[") && trimmedValue.endsWith("]")) ||
|
||||
(trimmedValue.startsWith('"') && trimmedValue.endsWith('"'))
|
||||
) {
|
||||
return JSON.parse(trimmedValue);
|
||||
}
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show help for config command
|
||||
*/
|
||||
function showHelp(): void {
|
||||
console.log('');
|
||||
console.log('Usage: gitzone config <command> [options]');
|
||||
console.log('');
|
||||
console.log('Commands:');
|
||||
console.log(' show Display current release configuration');
|
||||
console.log(' add [url] Add a registry URL');
|
||||
console.log(' remove [url] Remove a registry URL');
|
||||
console.log(' clear Clear all registries');
|
||||
console.log(' access [public|private] Set npm access level for publishing');
|
||||
console.log(' commit [setting] [value] Configure commit options');
|
||||
console.log(' services Configure which services are enabled');
|
||||
console.log('');
|
||||
console.log('Examples:');
|
||||
console.log(' gitzone config show');
|
||||
console.log(' gitzone config add https://registry.npmjs.org');
|
||||
console.log(' gitzone config add https://verdaccio.example.com');
|
||||
console.log(' gitzone config remove https://registry.npmjs.org');
|
||||
console.log(' gitzone config clear');
|
||||
console.log(' gitzone config access public');
|
||||
console.log(' gitzone config access private');
|
||||
console.log(' gitzone config commit # Interactive');
|
||||
console.log(' gitzone config commit alwaysTest true');
|
||||
console.log(' gitzone config services # Interactive');
|
||||
console.log('');
|
||||
export function showHelp(mode?: ICliMode): void {
|
||||
if (mode?.json) {
|
||||
printJson({
|
||||
command: "config",
|
||||
usage: "gitzone config <command> [options]",
|
||||
commands: [
|
||||
{
|
||||
name: "show",
|
||||
description: "Display current @git.zone/cli configuration",
|
||||
},
|
||||
{ name: "get <path>", description: "Read a single config value" },
|
||||
{ name: "set <path> <value>", description: "Write a config value" },
|
||||
{ name: "unset <path>", description: "Delete a config value" },
|
||||
{ name: "add [url]", description: "Add a release registry" },
|
||||
{ name: "remove [url]", description: "Remove a release registry" },
|
||||
{ name: "clear", description: "Clear all release registries" },
|
||||
{
|
||||
name: "access [public|private]",
|
||||
description: "Set npm publish access level",
|
||||
},
|
||||
{
|
||||
name: "commit <setting> <value>",
|
||||
description: "Set commit defaults",
|
||||
},
|
||||
],
|
||||
examples: [
|
||||
"gitzone config show --json",
|
||||
"gitzone config get release.accessLevel",
|
||||
"gitzone config set cli.interactive false",
|
||||
"gitzone config set cli.output json",
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log("Usage: gitzone config <command> [options]");
|
||||
console.log("");
|
||||
console.log("Commands:");
|
||||
console.log(
|
||||
" show Display current @git.zone/cli configuration",
|
||||
);
|
||||
console.log(" get <path> Read a single config value");
|
||||
console.log(" set <path> <value> Write a config value");
|
||||
console.log(" unset <path> Delete a config value");
|
||||
console.log(" add [url] Add a registry URL");
|
||||
console.log(" remove [url] Remove a registry URL");
|
||||
console.log(" clear Clear all registries");
|
||||
console.log(
|
||||
" access [public|private] Set npm access level for publishing",
|
||||
);
|
||||
console.log(" commit [setting] [value] Configure commit options");
|
||||
console.log(
|
||||
" services Configure which services are enabled",
|
||||
);
|
||||
console.log("");
|
||||
console.log("Examples:");
|
||||
console.log(" gitzone config show");
|
||||
console.log(" gitzone config show --json");
|
||||
console.log(" gitzone config get release.accessLevel");
|
||||
console.log(" gitzone config set cli.interactive false");
|
||||
console.log(" gitzone config set cli.output json");
|
||||
console.log(" gitzone config unset cli.output");
|
||||
console.log(" gitzone config add https://registry.npmjs.org");
|
||||
console.log(" gitzone config add https://verdaccio.example.com");
|
||||
console.log(" gitzone config remove https://registry.npmjs.org");
|
||||
console.log(" gitzone config clear");
|
||||
console.log(" gitzone config access public");
|
||||
console.log(" gitzone config access private");
|
||||
console.log(" gitzone config commit # Interactive");
|
||||
console.log(" gitzone config commit alwaysTest true");
|
||||
console.log(" gitzone config services # Interactive");
|
||||
console.log("");
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ import * as plugins from './mod.plugins.js';
|
||||
import { FormatContext } from './classes.formatcontext.js';
|
||||
import type { IPlannedChange, ICheckResult } from './interfaces.format.js';
|
||||
import { Project } from '../classes.project.js';
|
||||
import { FormatStats } from './classes.formatstats.js';
|
||||
|
||||
export abstract class BaseFormatter {
|
||||
protected context: FormatContext;
|
||||
protected project: Project;
|
||||
protected stats: any; // Will be FormatStats from context
|
||||
protected stats: FormatStats;
|
||||
|
||||
constructor(context: FormatContext, project: Project) {
|
||||
this.context = context;
|
||||
@@ -36,9 +37,6 @@ export abstract class BaseFormatter {
|
||||
}
|
||||
|
||||
await this.postExecute();
|
||||
} catch (error) {
|
||||
// Don't rollback here - let the FormatPlanner handle it
|
||||
throw error;
|
||||
} finally {
|
||||
this.stats.endModule(this.name, startTime);
|
||||
}
|
||||
@@ -53,13 +51,10 @@ export abstract class BaseFormatter {
|
||||
}
|
||||
|
||||
protected async modifyFile(filepath: string, content: string): Promise<void> {
|
||||
// Validate filepath before writing
|
||||
if (!filepath || filepath.trim() === '') {
|
||||
throw new Error(`Invalid empty filepath in modifyFile`);
|
||||
}
|
||||
|
||||
// Ensure we have a proper path with directory component
|
||||
// If the path has no directory component (e.g., "package.json"), prepend "./"
|
||||
let normalizedPath = filepath;
|
||||
if (!plugins.path.parse(filepath).dir) {
|
||||
normalizedPath = './' + filepath;
|
||||
@@ -69,44 +64,46 @@ export abstract class BaseFormatter {
|
||||
}
|
||||
|
||||
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> {
|
||||
await plugins.smartfs.file(filepath).delete();
|
||||
}
|
||||
|
||||
protected async shouldProcessFile(filepath: string): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for diffs without applying changes
|
||||
* Returns information about what would change
|
||||
*/
|
||||
async check(): Promise<ICheckResult> {
|
||||
const changes = await this.analyze();
|
||||
const diffs: ICheckResult['diffs'] = [];
|
||||
|
||||
for (const change of changes) {
|
||||
// Skip generic changes that don't have actual content
|
||||
if (change.path === '<various files>') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (change.type === 'modify' || change.type === 'create') {
|
||||
// Read current content if file exists
|
||||
let currentContent: string | undefined;
|
||||
try {
|
||||
currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
|
||||
} catch {
|
||||
// File doesn't exist yet
|
||||
currentContent = undefined;
|
||||
}
|
||||
|
||||
const newContent = change.content;
|
||||
|
||||
// Check if there's an actual diff
|
||||
if (currentContent !== newContent && newContent !== undefined) {
|
||||
diffs.push({
|
||||
path: change.path,
|
||||
@@ -116,7 +113,6 @@ export abstract class BaseFormatter {
|
||||
});
|
||||
}
|
||||
} else if (change.type === 'delete') {
|
||||
// Check if file exists before marking for deletion
|
||||
try {
|
||||
const currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
|
||||
diffs.push({
|
||||
@@ -137,9 +133,6 @@ export abstract class BaseFormatter {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a single diff using smartdiff
|
||||
*/
|
||||
displayDiff(diff: ICheckResult['diffs'][0]): void {
|
||||
console.log(`\n--- ${diff.path}`);
|
||||
if (diff.before && diff.after) {
|
||||
@@ -150,7 +143,6 @@ export abstract class BaseFormatter {
|
||||
}));
|
||||
} else if (diff.after && !diff.before) {
|
||||
console.log(' (new file)');
|
||||
// Show first few lines of new content
|
||||
const lines = diff.after.split('\n').slice(0, 10);
|
||||
lines.forEach(line => console.log(` + ${line}`));
|
||||
if (diff.after.split('\n').length > 10) {
|
||||
@@ -161,9 +153,6 @@ export abstract class BaseFormatter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display all diffs from a check result
|
||||
*/
|
||||
displayAllDiffs(result: ICheckResult): void {
|
||||
if (!result.hasDiff) {
|
||||
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 { FormatStats } from './classes.formatstats.js';
|
||||
import * as plugins from "./mod.plugins.js";
|
||||
import { FormatStats } from "./classes.formatstats.js";
|
||||
|
||||
interface IFormatContextOptions {
|
||||
interactive?: boolean;
|
||||
jsonOutput?: boolean;
|
||||
}
|
||||
|
||||
export class FormatContext {
|
||||
private formatStats: FormatStats;
|
||||
private interactive: boolean;
|
||||
private jsonOutput: boolean;
|
||||
|
||||
constructor() {
|
||||
constructor(options: IFormatContextOptions = {}) {
|
||||
this.formatStats = new FormatStats();
|
||||
this.interactive = options.interactive ?? true;
|
||||
this.jsonOutput = options.jsonOutput ?? false;
|
||||
}
|
||||
|
||||
getFormatStats(): FormatStats {
|
||||
return this.formatStats;
|
||||
}
|
||||
|
||||
isInteractive(): boolean {
|
||||
return this.interactive;
|
||||
}
|
||||
|
||||
isJsonOutput(): boolean {
|
||||
return this.jsonOutput;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,12 @@ import * as plugins from './mod.plugins.js';
|
||||
import { FormatContext } from './classes.formatcontext.js';
|
||||
import { BaseFormatter } from './classes.baseformatter.js';
|
||||
import type { IFormatPlan, IPlannedChange } from './interfaces.format.js';
|
||||
import { getModuleIcon } from './interfaces.format.js';
|
||||
import { logger } from '../gitzone.logging.js';
|
||||
import { DependencyAnalyzer } from './classes.dependency-analyzer.js';
|
||||
import { DiffReporter } from './classes.diffreporter.js';
|
||||
|
||||
export class FormatPlanner {
|
||||
private plannedChanges: Map<string, IPlannedChange[]> = new Map();
|
||||
private dependencyAnalyzer = new DependencyAnalyzer();
|
||||
private diffReporter = new DiffReporter();
|
||||
|
||||
async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> {
|
||||
@@ -18,7 +17,6 @@ export class FormatPlanner {
|
||||
filesAdded: 0,
|
||||
filesModified: 0,
|
||||
filesRemoved: 0,
|
||||
estimatedTime: 0,
|
||||
},
|
||||
changes: [],
|
||||
warnings: [],
|
||||
@@ -32,7 +30,6 @@ export class FormatPlanner {
|
||||
for (const change of changes) {
|
||||
plan.changes.push(change);
|
||||
|
||||
// Update summary
|
||||
switch (change.type) {
|
||||
case 'create':
|
||||
plan.summary.filesAdded++;
|
||||
@@ -58,7 +55,6 @@ export class FormatPlanner {
|
||||
plan.summary.filesAdded +
|
||||
plan.summary.filesModified +
|
||||
plan.summary.filesRemoved;
|
||||
plan.summary.estimatedTime = plan.summary.totalFiles * 100; // 100ms per file estimate
|
||||
|
||||
return plan;
|
||||
}
|
||||
@@ -67,27 +63,20 @@ export class FormatPlanner {
|
||||
plan: IFormatPlan,
|
||||
modules: BaseFormatter[],
|
||||
context: FormatContext,
|
||||
parallel: boolean = false,
|
||||
): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Always use sequential execution to avoid race conditions
|
||||
for (const module of modules) {
|
||||
const changes = this.plannedChanges.get(module.name) || [];
|
||||
for (const module of modules) {
|
||||
const changes = this.plannedChanges.get(module.name) || [];
|
||||
|
||||
if (changes.length > 0) {
|
||||
logger.log('info', `Executing ${module.name} formatter...`);
|
||||
await module.execute(changes);
|
||||
}
|
||||
if (changes.length > 0) {
|
||||
logger.log('info', `Executing ${module.name} formatter...`);
|
||||
await module.execute(changes);
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
logger.log('info', `Format operations completed in ${duration}ms`);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logger.log('info', `Format operations completed in ${duration}ms`);
|
||||
}
|
||||
|
||||
async displayPlan(
|
||||
@@ -103,7 +92,6 @@ export class FormatPlanner {
|
||||
console.log('');
|
||||
console.log('Changes by module:');
|
||||
|
||||
// Group changes by module
|
||||
const changesByModule = new Map<string, IPlannedChange[]>();
|
||||
for (const change of plan.changes) {
|
||||
const moduleChanges = changesByModule.get(change.module) || [];
|
||||
@@ -113,14 +101,13 @@ export class FormatPlanner {
|
||||
|
||||
for (const [module, changes] of changesByModule) {
|
||||
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) {
|
||||
const icon = this.getChangeIcon(change.type);
|
||||
console.log(` ${icon} ${change.path} - ${change.description}`);
|
||||
|
||||
// Show diff for modified files if detailed view is requested
|
||||
if (detailed && change.type === 'modify') {
|
||||
const diff = await this.diffReporter.generateDiffForChange(change);
|
||||
if (diff) {
|
||||
@@ -141,22 +128,6 @@ export class FormatPlanner {
|
||||
console.log('\n' + '━'.repeat(50));
|
||||
}
|
||||
|
||||
private getModuleIcon(module: string): string {
|
||||
const icons: Record<string, string> = {
|
||||
packagejson: '📦',
|
||||
license: '📝',
|
||||
tsconfig: '🔧',
|
||||
cleanup: '🚮',
|
||||
gitignore: '🔒',
|
||||
prettier: '✨',
|
||||
readme: '📖',
|
||||
templates: '📄',
|
||||
npmextra: '⚙️',
|
||||
copy: '📋',
|
||||
};
|
||||
return icons[module] || '📁';
|
||||
}
|
||||
|
||||
private getChangeIcon(type: 'create' | 'modify' | 'delete'): string {
|
||||
switch (type) {
|
||||
case 'create':
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as plugins from './mod.plugins.js';
|
||||
import { logger } from '../gitzone.logging.js';
|
||||
import { getModuleIcon } from './interfaces.format.js';
|
||||
|
||||
export interface IModuleStats {
|
||||
name: string;
|
||||
@@ -23,8 +24,6 @@ export interface IFormatStats {
|
||||
totalModified: number;
|
||||
totalDeleted: number;
|
||||
totalErrors: number;
|
||||
cacheHits: number;
|
||||
cacheMisses: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,8 +42,6 @@ export class FormatStats {
|
||||
totalModified: 0,
|
||||
totalDeleted: 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 {
|
||||
this.stats.endTime = Date.now();
|
||||
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(` Errors: ${this.stats.overallStats.totalErrors}`);
|
||||
|
||||
if (
|
||||
this.stats.overallStats.cacheHits > 0 ||
|
||||
this.stats.overallStats.cacheMisses > 0
|
||||
) {
|
||||
const cacheHitRate =
|
||||
(this.stats.overallStats.cacheHits /
|
||||
(this.stats.overallStats.cacheHits +
|
||||
this.stats.overallStats.cacheMisses)) *
|
||||
100;
|
||||
console.log(` Cache Hit Rate: ${cacheHitRate.toFixed(1)}%`);
|
||||
console.log(` • Hits: ${this.stats.overallStats.cacheHits}`);
|
||||
console.log(` • Misses: ${this.stats.overallStats.cacheMisses}`);
|
||||
}
|
||||
|
||||
// Module stats
|
||||
console.log('\nModule Breakdown:');
|
||||
console.log('─'.repeat(50));
|
||||
@@ -159,7 +134,7 @@ export class FormatStats {
|
||||
|
||||
for (const moduleStats of sortedModules) {
|
||||
console.log(
|
||||
`\n${this.getModuleIcon(moduleStats.name)} ${moduleStats.name}:`,
|
||||
`\n${getModuleIcon(moduleStats.name)} ${moduleStats.name}:`,
|
||||
);
|
||||
console.log(
|
||||
` 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,151 +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]) {
|
||||
if (!npmextraJson[newKey]) {
|
||||
// New key doesn't exist - simple rename
|
||||
npmextraJson[newKey] = npmextraJson[oldKey];
|
||||
} else {
|
||||
// New key exists - merge old into new (old values don't overwrite new)
|
||||
npmextraJson[newKey] = {
|
||||
...npmextraJson[oldKey],
|
||||
...npmextraJson[newKey],
|
||||
};
|
||||
}
|
||||
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 type { IPlannedChange } from '../interfaces.format.js';
|
||||
import * as plugins from '../mod.plugins.js';
|
||||
import * as cleanupFormatter from '../format.cleanup.js';
|
||||
|
||||
export class CleanupFormatter extends BaseFormatter {
|
||||
get name(): string {
|
||||
|
||||
@@ -17,15 +17,15 @@ export class CopyFormatter extends BaseFormatter {
|
||||
async analyze(): Promise<IPlannedChange[]> {
|
||||
const changes: IPlannedChange[] = [];
|
||||
|
||||
// Get copy configuration from npmextra.json
|
||||
const npmextraConfig = new plugins.npmextra.Npmextra();
|
||||
const copyConfig = npmextraConfig.dataFor<{ patterns: ICopyPattern[] }>(
|
||||
// Get copy configuration from .smartconfig.json
|
||||
const smartconfigInstance = new plugins.smartconfig.Smartconfig();
|
||||
const copyConfig = smartconfigInstance.dataFor<{ patterns: ICopyPattern[] }>(
|
||||
'gitzone.format.copy',
|
||||
{ patterns: [] },
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -103,10 +103,6 @@ export class CopyFormatter extends BaseFormatter {
|
||||
async applyChange(change: IPlannedChange): Promise<void> {
|
||||
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') {
|
||||
await this.createFile(change.path, change.content);
|
||||
} else {
|
||||
|
||||
@@ -1,42 +1,39 @@
|
||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||
import type { IPlannedChange } from '../interfaces.format.js';
|
||||
import * as plugins from '../mod.plugins.js';
|
||||
import * as paths from '../../paths.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 {
|
||||
get name(): string {
|
||||
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[]> {
|
||||
const changes: IPlannedChange[] = [];
|
||||
const gitignorePath = '.gitignore';
|
||||
|
||||
const standardTemplate = await this.getStandardTemplate();
|
||||
|
||||
// Check if file exists and extract custom content
|
||||
let customContent = '';
|
||||
const exists = await plugins.smartfs.file(gitignorePath).exists();
|
||||
@@ -59,11 +56,11 @@ export class GitignoreFormatter extends BaseFormatter {
|
||||
}
|
||||
|
||||
// Compute new content
|
||||
let newContent = GITIGNORE_TEMPLATE;
|
||||
let newContent = standardTemplate;
|
||||
if (customContent) {
|
||||
newContent = GITIGNORE_TEMPLATE + '\n' + customContent + '\n';
|
||||
newContent = standardTemplate + '\n' + customContent + '\n';
|
||||
} else {
|
||||
newContent = GITIGNORE_TEMPLATE + '\n';
|
||||
newContent = standardTemplate + '\n';
|
||||
}
|
||||
|
||||
// Read current content to compare
|
||||
@@ -75,7 +72,6 @@ export class GitignoreFormatter extends BaseFormatter {
|
||||
.read()) as string;
|
||||
}
|
||||
|
||||
// Determine change type
|
||||
if (!exists) {
|
||||
changes.push({
|
||||
type: 'create',
|
||||
|
||||
@@ -1,174 +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]) {
|
||||
if (!npmextraJson[newKey]) {
|
||||
// New key doesn't exist - simple rename
|
||||
npmextraJson[newKey] = npmextraJson[oldKey];
|
||||
} else {
|
||||
// New key exists - merge old into new (old values don't overwrite new)
|
||||
npmextraJson[newKey] = {
|
||||
...npmextraJson[oldKey],
|
||||
...npmextraJson[newKey],
|
||||
};
|
||||
}
|
||||
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 { 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 {
|
||||
get name(): string {
|
||||
return 'packagejson';
|
||||
@@ -100,9 +29,9 @@ export class PackageJsonFormatter extends BaseFormatter {
|
||||
// Parse and compute new content
|
||||
const packageJson = JSON.parse(currentContent);
|
||||
|
||||
// Get gitzone config from npmextra
|
||||
const npmextraConfig = new plugins.npmextra.Npmextra(paths.cwd);
|
||||
const gitzoneData: any = npmextraConfig.dataFor('@git.zone/cli', {});
|
||||
// Get gitzone config from smartconfig
|
||||
const smartconfigInstance = new plugins.smartconfig.Smartconfig(paths.cwd);
|
||||
const gitzoneData: any = smartconfigInstance.dataFor('@git.zone/cli', {});
|
||||
|
||||
// Set metadata from gitzone config
|
||||
if (gitzoneData.module) {
|
||||
@@ -141,11 +70,6 @@ export class PackageJsonFormatter extends BaseFormatter {
|
||||
packageJson.scripts.build = `echo "Not needed for now"`;
|
||||
}
|
||||
|
||||
// Ensure buildDocs script exists
|
||||
if (!packageJson.scripts.buildDocs) {
|
||||
packageJson.scripts.buildDocs = `tsdoc`;
|
||||
}
|
||||
|
||||
// Set files array
|
||||
packageJson.files = [
|
||||
'ts/**/*',
|
||||
@@ -156,25 +80,10 @@ export class PackageJsonFormatter extends BaseFormatter {
|
||||
'dist_ts_web/**/*',
|
||||
'assets/**/*',
|
||||
'cli.js',
|
||||
'npmextra.json',
|
||||
'.smartconfig.json',
|
||||
'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
|
||||
try {
|
||||
const overridesContent = (await plugins.smartfs
|
||||
|
||||
@@ -21,7 +21,7 @@ export class PrettierFormatter extends BaseFormatter {
|
||||
const rootConfigFiles = [
|
||||
'package.json',
|
||||
'tsconfig.json',
|
||||
'npmextra.json',
|
||||
'.smartconfig.json',
|
||||
'.prettierrc',
|
||||
'.prettierrc.json',
|
||||
'.prettierrc.js',
|
||||
@@ -79,12 +79,9 @@ export class PrettierFormatter extends BaseFormatter {
|
||||
// Remove duplicates
|
||||
const uniqueFiles = [...new Set(allFiles)];
|
||||
|
||||
// Get all files that match the pattern
|
||||
const files = uniqueFiles;
|
||||
|
||||
// Ensure we only process actual files (not directories)
|
||||
const validFiles: string[] = [];
|
||||
for (const file of files) {
|
||||
for (const file of uniqueFiles) {
|
||||
try {
|
||||
const stats = await plugins.smartfs.file(file).stat();
|
||||
if (!stats.isDirectory) {
|
||||
@@ -96,14 +93,7 @@ export class PrettierFormatter extends BaseFormatter {
|
||||
}
|
||||
}
|
||||
|
||||
// Check which files need formatting
|
||||
for (const file of validFiles) {
|
||||
// Skip files that haven't changed
|
||||
if (!(await this.shouldProcessFile(file))) {
|
||||
logVerbose(`Skipping ${file} - no changes detected`);
|
||||
continue;
|
||||
}
|
||||
|
||||
changes.push({
|
||||
type: 'modify',
|
||||
path: file,
|
||||
@@ -232,7 +222,7 @@ export class PrettierFormatter extends BaseFormatter {
|
||||
|
||||
private async getPrettierConfig(): Promise<any> {
|
||||
// Try to load prettier config from the project
|
||||
const prettierConfig = new plugins.npmextra.Npmextra();
|
||||
const prettierConfig = new plugins.smartconfig.Smartconfig();
|
||||
return prettierConfig.dataFor('prettier', {
|
||||
// Default prettier config
|
||||
singleQuote: true,
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
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 .smartconfig.json from old namespace keys to new package-scoped keys
|
||||
*/
|
||||
const migrateNamespaceKeys = (smartconfigJson: 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 (smartconfigJson[oldKey]) {
|
||||
if (!smartconfigJson[newKey]) {
|
||||
smartconfigJson[newKey] = smartconfigJson[oldKey];
|
||||
} else {
|
||||
smartconfigJson[newKey] = {
|
||||
...smartconfigJson[oldKey],
|
||||
...smartconfigJson[newKey],
|
||||
};
|
||||
}
|
||||
delete smartconfigJson[oldKey];
|
||||
migrated = true;
|
||||
}
|
||||
}
|
||||
return migrated;
|
||||
};
|
||||
|
||||
/**
|
||||
* Migrates npmAccessLevel from @ship.zone/szci to @git.zone/cli.release.accessLevel
|
||||
*/
|
||||
const migrateAccessLevel = (smartconfigJson: any): boolean => {
|
||||
const szciConfig = smartconfigJson["@ship.zone/szci"];
|
||||
|
||||
if (!szciConfig?.npmAccessLevel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const gitzoneConfig = smartconfigJson["@git.zone/cli"] || {};
|
||||
if (gitzoneConfig?.release?.accessLevel) {
|
||||
delete szciConfig.npmAccessLevel;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!smartconfigJson["@git.zone/cli"]) {
|
||||
smartconfigJson["@git.zone/cli"] = {};
|
||||
}
|
||||
if (!smartconfigJson["@git.zone/cli"].release) {
|
||||
smartconfigJson["@git.zone/cli"].release = {};
|
||||
}
|
||||
|
||||
smartconfigJson["@git.zone/cli"].release.accessLevel =
|
||||
szciConfig.npmAccessLevel;
|
||||
delete szciConfig.npmAccessLevel;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
// Apply key migrations
|
||||
migrateNamespaceKeys(smartconfigJson);
|
||||
migrateAccessLevel(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';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[]> {
|
||||
const changes: IPlannedChange[] = [];
|
||||
const project = this.project;
|
||||
@@ -25,7 +51,8 @@ export class TemplatesFormatter extends BaseFormatter {
|
||||
switch (projectType) {
|
||||
case 'npm':
|
||||
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 ciChanges = await this.analyzeTemplate(ciTemplate, [
|
||||
{ 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' },
|
||||
]);
|
||||
changes.push(...websiteChanges);
|
||||
} else if (projectType === 'service') {
|
||||
const serviceChanges = await this.analyzeTemplate('service_update', []);
|
||||
changes.push(...serviceChanges);
|
||||
} else if (projectType === 'wcc') {
|
||||
const wccChanges = await this.analyzeTemplate('wcc_update', [
|
||||
{ templatePath: 'html/index.html', destPath: 'html/index.html' },
|
||||
@@ -83,53 +107,47 @@ export class TemplatesFormatter extends BaseFormatter {
|
||||
const changes: IPlannedChange[] = [];
|
||||
const templateDir = plugins.path.join(paths.templatesDir, templateName);
|
||||
|
||||
// Check if template exists
|
||||
const templateExists = await plugins.smartfs.directory(templateDir).exists();
|
||||
if (!templateExists) {
|
||||
logVerbose(`Template ${templateName} not found`);
|
||||
return changes;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const templateFilePath = plugins.path.join(templateDir, file.templatePath);
|
||||
const destFilePath = file.destPath;
|
||||
let renderedFiles: Map<string, string>;
|
||||
try {
|
||||
renderedFiles = await this.renderTemplate(templateName);
|
||||
} catch (error) {
|
||||
logVerbose(`Failed to render template ${templateName}: ${error.message}`);
|
||||
return changes;
|
||||
}
|
||||
|
||||
// Check if template file exists
|
||||
const fileExists = await plugins.smartfs.file(templateFilePath).exists();
|
||||
if (!fileExists) {
|
||||
logVerbose(`Template file ${templateFilePath} not found`);
|
||||
for (const file of files) {
|
||||
// Look up by templatePath first, then destPath (frontmatter may rename files)
|
||||
const processedContent = renderedFiles.get(file.templatePath)
|
||||
|| renderedFiles.get(file.destPath);
|
||||
|
||||
if (!processedContent) {
|
||||
logVerbose(`Template file ${file.templatePath} not found in rendered output`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read template content
|
||||
const templateContent = (await plugins.smartfs
|
||||
.file(templateFilePath)
|
||||
const destExists = await plugins.smartfs.file(file.destPath).exists();
|
||||
let currentContent = '';
|
||||
if (destExists) {
|
||||
currentContent = (await plugins.smartfs
|
||||
.file(file.destPath)
|
||||
.encoding('utf8')
|
||||
.read()) as string;
|
||||
}
|
||||
|
||||
// Check if destination file exists
|
||||
const destExists = await plugins.smartfs.file(destFilePath).exists();
|
||||
let currentContent = '';
|
||||
if (destExists) {
|
||||
currentContent = (await plugins.smartfs
|
||||
.file(destFilePath)
|
||||
.encoding('utf8')
|
||||
.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}`);
|
||||
if (processedContent !== currentContent) {
|
||||
changes.push({
|
||||
type: destExists ? 'modify' : 'create',
|
||||
path: file.destPath,
|
||||
module: this.name,
|
||||
description: `Apply template ${templateName}/${file.templatePath}`,
|
||||
content: processedContent,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,12 +157,6 @@ export class TemplatesFormatter extends BaseFormatter {
|
||||
async applyChange(change: IPlannedChange): Promise<void> {
|
||||
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') {
|
||||
await this.createFile(change.path, change.content);
|
||||
} else {
|
||||
|
||||
@@ -30,9 +30,10 @@ export class TsconfigFormatter extends BaseFormatter {
|
||||
const tsconfigObject = JSON.parse(currentContent);
|
||||
tsconfigObject.compilerOptions = tsconfigObject.compilerOptions || {};
|
||||
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 {
|
||||
const tsPublishMod = await import('@git.zone/tspublish');
|
||||
const tsPublishInstance = new tsPublishMod.TsPublish();
|
||||
@@ -40,7 +41,7 @@ export class TsconfigFormatter extends BaseFormatter {
|
||||
|
||||
for (const publishModule of Object.keys(publishModules)) {
|
||||
const publishConfig = publishModules[publishModule];
|
||||
tsconfigObject.compilerOptions.paths[`${publishConfig.name}`] = [
|
||||
tspublishPaths[`${publishConfig.name}`] = [
|
||||
`./${publishModule}/index.js`,
|
||||
];
|
||||
}
|
||||
@@ -48,6 +49,8 @@ export class TsconfigFormatter extends BaseFormatter {
|
||||
logVerbose(`Could not get tspublish modules: ${error.message}`);
|
||||
}
|
||||
|
||||
tsconfigObject.compilerOptions.paths = { ...existingPaths, ...tspublishPaths };
|
||||
|
||||
const newContent = JSON.stringify(tsconfigObject, null, 2);
|
||||
|
||||
// Only add change if content differs
|
||||
|
||||
+315
-165
@@ -1,120 +1,239 @@
|
||||
import * as plugins from './mod.plugins.js';
|
||||
import { Project } from '../classes.project.js';
|
||||
import { FormatContext } from './classes.formatcontext.js';
|
||||
import { FormatPlanner } from './classes.formatplanner.js';
|
||||
import { BaseFormatter } from './classes.baseformatter.js';
|
||||
import { logger, setVerboseMode } from '../gitzone.logging.js';
|
||||
import * as plugins from "./mod.plugins.js";
|
||||
import { Project } from "../classes.project.js";
|
||||
import { FormatContext } from "./classes.formatcontext.js";
|
||||
import { FormatPlanner } from "./classes.formatplanner.js";
|
||||
import { BaseFormatter } from "./classes.baseformatter.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 { NpmextraFormatter } from './formatters/npmextra.formatter.js';
|
||||
import { LicenseFormatter } from './formatters/license.formatter.js';
|
||||
import { PackageJsonFormatter } from './formatters/packagejson.formatter.js';
|
||||
import { TemplatesFormatter } from './formatters/templates.formatter.js';
|
||||
import { GitignoreFormatter } from './formatters/gitignore.formatter.js';
|
||||
import { TsconfigFormatter } from './formatters/tsconfig.formatter.js';
|
||||
import { PrettierFormatter } from './formatters/prettier.formatter.js';
|
||||
import { ReadmeFormatter } from './formatters/readme.formatter.js';
|
||||
import { CopyFormatter } from './formatters/copy.formatter.js';
|
||||
import { CleanupFormatter } from "./formatters/cleanup.formatter.js";
|
||||
import { SmartconfigFormatter } from "./formatters/smartconfig.formatter.js";
|
||||
import { LicenseFormatter } from "./formatters/license.formatter.js";
|
||||
import { PackageJsonFormatter } from "./formatters/packagejson.formatter.js";
|
||||
import { TemplatesFormatter } from "./formatters/templates.formatter.js";
|
||||
import { GitignoreFormatter } from "./formatters/gitignore.formatter.js";
|
||||
import { TsconfigFormatter } from "./formatters/tsconfig.formatter.js";
|
||||
import { PrettierFormatter } from "./formatters/prettier.formatter.js";
|
||||
import { ReadmeFormatter } from "./formatters/readme.formatter.js";
|
||||
import { CopyFormatter } from "./formatters/copy.formatter.js";
|
||||
|
||||
/**
|
||||
* 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 createActiveFormatters = async (options: {
|
||||
interactive: boolean;
|
||||
jsonOutput: boolean;
|
||||
}) => {
|
||||
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 activeFormatters = formatters.filter((formatter) => {
|
||||
if (formatConfig.modules.only.length > 0) {
|
||||
return formatConfig.modules.only.includes(formatter.name);
|
||||
}
|
||||
if (formatConfig.modules.skip.includes(formatter.name)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return {
|
||||
context,
|
||||
planner,
|
||||
formatConfig,
|
||||
activeFormatters,
|
||||
};
|
||||
};
|
||||
|
||||
const buildFormatPlan = async (options: {
|
||||
fromPlan?: string;
|
||||
interactive: boolean;
|
||||
jsonOutput: boolean;
|
||||
}) => {
|
||||
const { context, planner, formatConfig, activeFormatters } =
|
||||
await createActiveFormatters({
|
||||
interactive: options.interactive,
|
||||
jsonOutput: options.jsonOutput,
|
||||
});
|
||||
|
||||
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,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
export let run = async (
|
||||
options: {
|
||||
write?: boolean; // Explicitly write changes (default: false, dry-mode)
|
||||
dryRun?: boolean; // Deprecated, kept for compatibility
|
||||
write?: boolean;
|
||||
dryRun?: boolean; // Deprecated, kept for compatibility
|
||||
yes?: boolean;
|
||||
planOnly?: boolean;
|
||||
savePlan?: string;
|
||||
fromPlan?: string;
|
||||
detailed?: boolean;
|
||||
interactive?: boolean;
|
||||
parallel?: boolean;
|
||||
verbose?: boolean;
|
||||
diff?: boolean; // Show file diffs
|
||||
diff?: boolean;
|
||||
[key: string]: 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) {
|
||||
setVerboseMode(true);
|
||||
}
|
||||
|
||||
// Determine if we should write changes
|
||||
// Default is dry-mode (no writing) unless --write/-w is specified
|
||||
const shouldWrite = options.write ?? (options.dryRun === false);
|
||||
const shouldWrite = options.write ?? options.dryRun === false;
|
||||
const treatAsPlan = subcommand === "plan";
|
||||
|
||||
const project = await Project.fromCwd({ requireProjectType: false });
|
||||
const context = new FormatContext();
|
||||
// Cache system removed - no longer needed
|
||||
const planner = new FormatPlanner();
|
||||
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;
|
||||
}
|
||||
|
||||
// Get configuration from npmextra
|
||||
const npmextraConfig = new plugins.npmextra.Npmextra();
|
||||
const formatConfig = npmextraConfig.dataFor<any>('@git.zone/cli.format', {
|
||||
interactive: true,
|
||||
showDiffs: false,
|
||||
autoApprove: false,
|
||||
planTimeout: 30000,
|
||||
rollback: {
|
||||
enabled: true,
|
||||
autoRollbackOnError: true,
|
||||
backupRetentionDays: 7,
|
||||
maxBackupSize: '100MB',
|
||||
excludePatterns: ['node_modules/**', '.git/**'],
|
||||
},
|
||||
modules: {
|
||||
skip: [],
|
||||
only: [],
|
||||
order: [],
|
||||
},
|
||||
parallel: true,
|
||||
cache: {
|
||||
enabled: true,
|
||||
clean: true, // Clean invalid entries from cache
|
||||
},
|
||||
});
|
||||
// Migrate config file before anything reads it
|
||||
await migrateConfigFile(shouldWrite);
|
||||
|
||||
// Cache cleaning removed - no longer using cache system
|
||||
|
||||
// Override config with command options
|
||||
const interactive = options.interactive ?? formatConfig.interactive;
|
||||
const formatConfig = await getFormatConfig();
|
||||
const interactive =
|
||||
options.interactive ?? (mode.interactive && formatConfig.interactive);
|
||||
const autoApprove = options.yes ?? formatConfig.autoApprove;
|
||||
const parallel = options.parallel ?? formatConfig.parallel;
|
||||
|
||||
try {
|
||||
// Initialize formatters
|
||||
const formatters = [
|
||||
new CleanupFormatter(context, project),
|
||||
new NpmextraFormatter(context, project),
|
||||
new LicenseFormatter(context, project),
|
||||
new PackageJsonFormatter(context, project),
|
||||
new TemplatesFormatter(context, project),
|
||||
new GitignoreFormatter(context, project),
|
||||
new TsconfigFormatter(context, project),
|
||||
new PrettierFormatter(context, project),
|
||||
new ReadmeFormatter(context, project),
|
||||
new CopyFormatter(context, project),
|
||||
];
|
||||
const planBuilder = async () => {
|
||||
return await buildFormatPlan({
|
||||
fromPlan: options.fromPlan,
|
||||
interactive,
|
||||
jsonOutput: mode.json,
|
||||
});
|
||||
};
|
||||
|
||||
// Filter formatters based on configuration
|
||||
const activeFormatters = formatters.filter((formatter) => {
|
||||
if (formatConfig.modules.only.length > 0) {
|
||||
return formatConfig.modules.only.includes(formatter.name);
|
||||
}
|
||||
if (formatConfig.modules.skip.includes(formatter.name)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!mode.json) {
|
||||
logger.log("info", "Analyzing project for format operations...");
|
||||
}
|
||||
const { context, planner, activeFormatters, plan } = mode.json
|
||||
? await runWithSuppressedOutput(planBuilder)
|
||||
: await planBuilder();
|
||||
|
||||
// Plan phase
|
||||
logger.log('info', 'Analyzing project for format operations...');
|
||||
let plan = options.fromPlan
|
||||
? JSON.parse(
|
||||
(await plugins.smartfs
|
||||
.file(options.fromPlan)
|
||||
.encoding('utf8')
|
||||
.read()) as string,
|
||||
)
|
||||
: await planner.planFormat(activeFormatters);
|
||||
if (mode.json) {
|
||||
printJson(serializePlan(plan));
|
||||
return;
|
||||
}
|
||||
|
||||
// Display plan
|
||||
await planner.displayPlan(plan, options.detailed);
|
||||
@@ -123,34 +242,35 @@ export let run = async (
|
||||
if (options.savePlan) {
|
||||
await plugins.smartfs
|
||||
.file(options.savePlan)
|
||||
.encoding('utf8')
|
||||
.encoding("utf8")
|
||||
.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) {
|
||||
if (options.planOnly || treatAsPlan) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show diffs if requested (works in both dry-run and write modes)
|
||||
if (options.diff) {
|
||||
logger.log('info', 'Showing file diffs:');
|
||||
console.log('');
|
||||
// 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) {
|
||||
const checkResult = await formatter.check();
|
||||
if (checkResult.hasDiff) {
|
||||
logger.log('info', `[${formatter.name}]`);
|
||||
logger.log("info", `[${formatter.name}]`);
|
||||
formatter.displayAllDiffs(checkResult);
|
||||
console.log('');
|
||||
console.log("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dry-run mode (default behavior)
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -158,73 +278,45 @@ export let run = async (
|
||||
if (interactive && !autoApprove) {
|
||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||
const response = await interactInstance.askQuestion({
|
||||
type: 'confirm',
|
||||
name: 'proceed',
|
||||
message: 'Proceed with formatting?',
|
||||
type: "confirm",
|
||||
name: "proceed",
|
||||
message: "Proceed with formatting?",
|
||||
default: true,
|
||||
});
|
||||
|
||||
if (!(response as any).value) {
|
||||
logger.log('info', 'Format operation cancelled by user');
|
||||
logger.log("info", "Format operation cancelled by user");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute phase
|
||||
logger.log(
|
||||
'info',
|
||||
`Executing format operations${parallel ? ' in parallel' : ' sequentially'}...`,
|
||||
);
|
||||
await planner.executePlan(plan, activeFormatters, context, parallel);
|
||||
logger.log("info", "Executing format operations...");
|
||||
await planner.executePlan(plan, activeFormatters, context);
|
||||
|
||||
// Finish statistics tracking
|
||||
context.getFormatStats().finish();
|
||||
|
||||
// Display statistics
|
||||
const showStats = npmextraConfig.dataFor('gitzone.format.showStats', true);
|
||||
const showStats = formatConfig.showStats ?? true;
|
||||
if (showStats) {
|
||||
context.getFormatStats().displayStats();
|
||||
}
|
||||
|
||||
// Save stats if requested
|
||||
if (options.detailed) {
|
||||
const statsPath = `.nogit/format-stats-${Date.now()}.json`;
|
||||
await context.getFormatStats().saveReport(statsPath);
|
||||
}
|
||||
|
||||
logger.log('success', 'Format operations completed successfully!');
|
||||
logger.log("success", "Format operations completed successfully!");
|
||||
} catch (error) {
|
||||
logger.log('error', `Format operation failed: ${error.message}`);
|
||||
|
||||
// Rollback system has been removed for stability
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.log("error", `Format operation failed: ${errorMessage}`);
|
||||
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';
|
||||
import type { ICheckResult } from "./interfaces.format.js";
|
||||
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)
|
||||
*/
|
||||
@@ -232,28 +324,14 @@ export const runFormatter = async (
|
||||
formatterName: string,
|
||||
options: {
|
||||
silent?: boolean;
|
||||
checkOnly?: boolean; // Only check for diffs, don't apply
|
||||
showDiff?: boolean; // Show the diff output
|
||||
} = {}
|
||||
checkOnly?: boolean;
|
||||
showDiff?: boolean;
|
||||
} = {},
|
||||
): Promise<ICheckResult | void> => {
|
||||
// Determine if this formatter requires projectType
|
||||
const requireProjectType = !formattersNotRequiringProjectType.includes(formatterName);
|
||||
const requireProjectType =
|
||||
!formattersNotRequiringProjectType.includes(formatterName);
|
||||
const project = await Project.fromCwd({ requireProjectType });
|
||||
const context = new FormatContext();
|
||||
|
||||
// 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 context = new FormatContext({ interactive: true, jsonOutput: false });
|
||||
|
||||
const FormatterClass = formatterMap[formatterName];
|
||||
if (!FormatterClass) {
|
||||
@@ -262,7 +340,6 @@ export const runFormatter = async (
|
||||
|
||||
const formatter = new FormatterClass(context, project);
|
||||
|
||||
// Check-only mode: just check for diffs and optionally display them
|
||||
if (options.checkOnly) {
|
||||
const result = await formatter.check();
|
||||
if (result.hasDiff && options.showDiff) {
|
||||
@@ -271,7 +348,6 @@ export const runFormatter = async (
|
||||
return result;
|
||||
}
|
||||
|
||||
// Normal mode: analyze and apply changes
|
||||
const changes = await formatter.analyze();
|
||||
|
||||
for (const change of changes) {
|
||||
@@ -279,6 +355,80 @@ export const runFormatter = async (
|
||||
}
|
||||
|
||||
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] [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: "--json", description: "Emit a read-only format plan as JSON" },
|
||||
],
|
||||
examples: [
|
||||
"gitzone format",
|
||||
"gitzone format plan --json",
|
||||
"gitzone format --write --yes",
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log("Usage: gitzone format [plan] [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(" --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 --write --yes");
|
||||
console.log("");
|
||||
}
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
export type IFormatOperation = {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
files: Array<{
|
||||
path: string;
|
||||
originalContent: string;
|
||||
checksum: string;
|
||||
permissions: string;
|
||||
}>;
|
||||
status: 'pending' | 'in-progress' | 'completed' | 'failed' | 'rolled-back';
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
export type IFormatPlan = {
|
||||
summary: {
|
||||
totalFiles: number;
|
||||
filesAdded: number;
|
||||
filesModified: number;
|
||||
filesRemoved: number;
|
||||
estimatedTime: number;
|
||||
};
|
||||
changes: Array<{
|
||||
type: 'create' | 'modify' | 'delete';
|
||||
path: string;
|
||||
module: string;
|
||||
description: string;
|
||||
diff?: string;
|
||||
size?: number;
|
||||
}>;
|
||||
warnings: Array<{
|
||||
level: 'info' | 'warning' | 'error';
|
||||
@@ -40,9 +24,6 @@ export type IPlannedChange = {
|
||||
module: string;
|
||||
description: string;
|
||||
content?: string; // New content for create/modify operations
|
||||
originalContent?: string; // Original content for comparison
|
||||
diff?: string;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
export interface ICheckResult {
|
||||
@@ -54,3 +35,19 @@ export interface ICheckResult {
|
||||
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';
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import * as path from 'path';
|
||||
import * as lik from '@push.rocks/lik';
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
import * as smartgulp from '@push.rocks/smartgulp';
|
||||
import * as smartinteract from '@push.rocks/smartinteract';
|
||||
import * as smartlegal from '@push.rocks/smartlegal';
|
||||
import * as smartobject from '@push.rocks/smartobject';
|
||||
import * as smartnpm from '@push.rocks/smartnpm';
|
||||
import * as smartstream from '@push.rocks/smartstream';
|
||||
import * as through2 from 'through2';
|
||||
import * as npmextra from '@push.rocks/npmextra';
|
||||
import * as smartconfig from '@push.rocks/smartconfig';
|
||||
import * as smartdiff from '@push.rocks/smartdiff';
|
||||
import * as smartscaf from '@push.rocks/smartscaf';
|
||||
|
||||
export {
|
||||
crypto,
|
||||
path,
|
||||
lik,
|
||||
smartfile,
|
||||
smartgulp,
|
||||
smartinteract,
|
||||
smartlegal,
|
||||
smartobject,
|
||||
smartnpm,
|
||||
smartstream,
|
||||
through2,
|
||||
npmextra,
|
||||
smartconfig,
|
||||
smartdiff,
|
||||
smartscaf,
|
||||
};
|
||||
|
||||
@@ -26,11 +26,11 @@ export interface IGlobalRegistryData {
|
||||
|
||||
export class GlobalRegistry {
|
||||
private static instance: GlobalRegistry | null = null;
|
||||
private kvStore: plugins.npmextra.KeyValueStore<IGlobalRegistryData>;
|
||||
private kvStore: plugins.smartconfig.KeyValueStore<IGlobalRegistryData>;
|
||||
private docker: DockerContainer;
|
||||
|
||||
private constructor() {
|
||||
this.kvStore = new plugins.npmextra.KeyValueStore({
|
||||
this.kvStore = new plugins.smartconfig.KeyValueStore({
|
||||
typeArg: 'userHomeDir',
|
||||
identityArg: 'gitzone-services',
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ export class ServiceManager {
|
||||
await this.config.loadOrCreate();
|
||||
logger.log('info', `📋 Project: ${this.config.getConfig().PROJECT_NAME}`);
|
||||
|
||||
// Load service selection from npmextra.json
|
||||
// Load service selection from .smartconfig.json
|
||||
await this.loadServiceConfiguration();
|
||||
|
||||
// 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> {
|
||||
const npmextraConfig = new plugins.npmextra.Npmextra(process.cwd());
|
||||
const gitzoneConfig = npmextraConfig.dataFor<any>('@git.zone/cli', {});
|
||||
const smartconfigInstance = new plugins.smartconfig.Smartconfig(process.cwd());
|
||||
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
|
||||
|
||||
// Check if services array exists
|
||||
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'];
|
||||
|
||||
// Save to npmextra.json
|
||||
// Save to .smartconfig.json
|
||||
await this.saveServiceConfiguration(this.enabledServices);
|
||||
} else {
|
||||
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> {
|
||||
const npmextraPath = plugins.path.join(process.cwd(), 'npmextra.json');
|
||||
let npmextraData: any = {};
|
||||
const smartconfigPath = plugins.path.join(process.cwd(), '.smartconfig.json');
|
||||
let smartconfigData: any = {};
|
||||
|
||||
// Read existing npmextra.json if it exists
|
||||
if (await plugins.smartfs.file(npmextraPath).exists()) {
|
||||
const content = await plugins.smartfs.file(npmextraPath).encoding('utf8').read();
|
||||
npmextraData = JSON.parse(content as string);
|
||||
// Read existing .smartconfig.json if it exists
|
||||
if (await plugins.smartfs.file(smartconfigPath).exists()) {
|
||||
const content = await plugins.smartfs.file(smartconfigPath).encoding('utf8').read();
|
||||
smartconfigData = JSON.parse(content as string);
|
||||
}
|
||||
|
||||
// Update @git.zone/cli.services
|
||||
if (!npmextraData['@git.zone/cli']) {
|
||||
npmextraData['@git.zone/cli'] = {};
|
||||
if (!smartconfigData['@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
|
||||
.file(npmextraPath)
|
||||
.file(smartconfigPath)
|
||||
.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(', ')}`);
|
||||
}
|
||||
|
||||
@@ -904,7 +904,7 @@ export class ServiceManager {
|
||||
|
||||
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
|
||||
|
||||
// Save to npmextra.json
|
||||
// Save to .smartconfig.json
|
||||
await this.saveServiceConfiguration(this.enabledServices);
|
||||
|
||||
logger.log('ok', '✅ Service configuration updated');
|
||||
|
||||
+550
-183
@@ -1,12 +1,26 @@
|
||||
import * as plugins from './mod.plugins.js';
|
||||
import * as helpers from './helpers.js';
|
||||
import { ServiceManager } from './classes.servicemanager.js';
|
||||
import { GlobalRegistry } from './classes.globalregistry.js';
|
||||
import { logger } from '../gitzone.logging.js';
|
||||
import * as plugins from "./mod.plugins.js";
|
||||
import * as helpers from "./helpers.js";
|
||||
import { ServiceManager } from "./classes.servicemanager.js";
|
||||
import { GlobalRegistry } from "./classes.globalregistry.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) => {
|
||||
const mode = await getCliMode(argvArg);
|
||||
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
|
||||
if (isGlobal) {
|
||||
@@ -14,264 +28,597 @@ export const run = async (argvArg: any) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Local project commands
|
||||
const serviceManager = new ServiceManager();
|
||||
await serviceManager.init();
|
||||
|
||||
const service = argvArg._[2] || 'all';
|
||||
const service = argvArg._[2] || "all";
|
||||
|
||||
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 'config':
|
||||
if (service === 'services' || argvArg._[2] === 'services') {
|
||||
case "config":
|
||||
if (service === "services" || argvArg._[2] === "services") {
|
||||
const serviceManager = new ServiceManager();
|
||||
await serviceManager.init();
|
||||
await handleConfigureServices(serviceManager);
|
||||
} else {
|
||||
await serviceManager.showConfig();
|
||||
await handleShowConfig(mode);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'compass':
|
||||
await serviceManager.showCompassConnection();
|
||||
case "set":
|
||||
await handleSetServices(argvArg._[2], mode);
|
||||
break;
|
||||
|
||||
case 'logs':
|
||||
const lines = parseInt(argvArg._[3]) || 20;
|
||||
await serviceManager.showLogs(service, lines);
|
||||
|
||||
case "enable":
|
||||
await handleEnableServices(argvArg._.slice(2), mode);
|
||||
break;
|
||||
|
||||
case 'remove':
|
||||
await handleRemove(serviceManager);
|
||||
|
||||
case "disable":
|
||||
await handleDisableServices(argvArg._.slice(2), mode);
|
||||
break;
|
||||
|
||||
case 'clean':
|
||||
await handleClean(serviceManager);
|
||||
|
||||
case "start":
|
||||
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;
|
||||
|
||||
case 'reconfigure':
|
||||
await serviceManager.reconfigure();
|
||||
break;
|
||||
|
||||
case 'help':
|
||||
}
|
||||
default:
|
||||
showHelp();
|
||||
showHelp(mode);
|
||||
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) {
|
||||
helpers.printHeader('Starting Services');
|
||||
helpers.printHeader("Starting Services");
|
||||
|
||||
switch (service) {
|
||||
case 'mongo':
|
||||
case 'mongodb':
|
||||
case "mongo":
|
||||
case "mongodb":
|
||||
await serviceManager.startMongoDB();
|
||||
break;
|
||||
|
||||
case 'minio':
|
||||
case 's3':
|
||||
case "minio":
|
||||
case "s3":
|
||||
await serviceManager.startMinIO();
|
||||
break;
|
||||
|
||||
case 'elasticsearch':
|
||||
case 'es':
|
||||
case "elasticsearch":
|
||||
case "es":
|
||||
await serviceManager.startElasticsearch();
|
||||
break;
|
||||
|
||||
case 'all':
|
||||
case '':
|
||||
case "all":
|
||||
case "":
|
||||
await serviceManager.startAll();
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.log('error', `Unknown service: ${service}`);
|
||||
logger.log('note', 'Use: mongo, s3, elasticsearch, or all');
|
||||
logger.log("error", `Unknown service: ${service}`);
|
||||
logger.log("note", "Use: mongo, s3, elasticsearch, or all");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStop(serviceManager: ServiceManager, service: string) {
|
||||
helpers.printHeader('Stopping Services');
|
||||
helpers.printHeader("Stopping Services");
|
||||
|
||||
switch (service) {
|
||||
case 'mongo':
|
||||
case 'mongodb':
|
||||
case "mongo":
|
||||
case "mongodb":
|
||||
await serviceManager.stopMongoDB();
|
||||
break;
|
||||
|
||||
case 'minio':
|
||||
case 's3':
|
||||
case "minio":
|
||||
case "s3":
|
||||
await serviceManager.stopMinIO();
|
||||
break;
|
||||
|
||||
case 'elasticsearch':
|
||||
case 'es':
|
||||
case "elasticsearch":
|
||||
case "es":
|
||||
await serviceManager.stopElasticsearch();
|
||||
break;
|
||||
|
||||
case 'all':
|
||||
case '':
|
||||
case "all":
|
||||
case "":
|
||||
await serviceManager.stopAll();
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.log('error', `Unknown service: ${service}`);
|
||||
logger.log('note', 'Use: mongo, s3, elasticsearch, or all');
|
||||
logger.log("error", `Unknown service: ${service}`);
|
||||
logger.log("note", "Use: mongo, s3, elasticsearch, or all");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRestart(serviceManager: ServiceManager, service: string) {
|
||||
helpers.printHeader('Restarting Services');
|
||||
helpers.printHeader("Restarting Services");
|
||||
|
||||
switch (service) {
|
||||
case 'mongo':
|
||||
case 'mongodb':
|
||||
case "mongo":
|
||||
case "mongodb":
|
||||
await serviceManager.stopMongoDB();
|
||||
await plugins.smartdelay.delayFor(2000);
|
||||
await serviceManager.startMongoDB();
|
||||
break;
|
||||
|
||||
case 'minio':
|
||||
case 's3':
|
||||
case "minio":
|
||||
case "s3":
|
||||
await serviceManager.stopMinIO();
|
||||
await plugins.smartdelay.delayFor(2000);
|
||||
await serviceManager.startMinIO();
|
||||
break;
|
||||
|
||||
case 'elasticsearch':
|
||||
case 'es':
|
||||
case "elasticsearch":
|
||||
case "es":
|
||||
await serviceManager.stopElasticsearch();
|
||||
await plugins.smartdelay.delayFor(2000);
|
||||
await serviceManager.startElasticsearch();
|
||||
break;
|
||||
|
||||
case 'all':
|
||||
case '':
|
||||
case "all":
|
||||
case "":
|
||||
await serviceManager.stopAll();
|
||||
await plugins.smartdelay.delayFor(2000);
|
||||
await serviceManager.startAll();
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.log('error', `Unknown service: ${service}`);
|
||||
logger.log("error", `Unknown service: ${service}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemove(serviceManager: ServiceManager) {
|
||||
helpers.printHeader('Removing Containers');
|
||||
logger.log('note', '⚠️ This will remove containers but preserve data');
|
||||
|
||||
const shouldContinue = await plugins.smartinteract.SmartInteract.getCliConfirmation('Continue?', false);
|
||||
|
||||
helpers.printHeader("Removing Containers");
|
||||
logger.log("note", "⚠️ This will remove containers but preserve data");
|
||||
|
||||
const shouldContinue =
|
||||
await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||
"Continue?",
|
||||
false,
|
||||
);
|
||||
|
||||
if (shouldContinue) {
|
||||
await serviceManager.removeContainers();
|
||||
} else {
|
||||
logger.log('note', 'Cancelled');
|
||||
logger.log("note", "Cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClean(serviceManager: ServiceManager) {
|
||||
helpers.printHeader('Clean All');
|
||||
logger.log('error', '⚠️ WARNING: This will remove all containers and data!');
|
||||
logger.log('error', 'This action cannot be undone!');
|
||||
helpers.printHeader("Clean All");
|
||||
logger.log("error", "⚠️ WARNING: This will remove all containers and data!");
|
||||
logger.log("error", "This action cannot be undone!");
|
||||
|
||||
const smartinteraction = new plugins.smartinteract.SmartInteract();
|
||||
const confirmAnswer = await smartinteraction.askQuestion({
|
||||
name: 'confirm',
|
||||
type: 'input',
|
||||
name: "confirm",
|
||||
type: "input",
|
||||
message: 'Type "yes" to confirm:',
|
||||
default: 'no'
|
||||
default: "no",
|
||||
});
|
||||
|
||||
if (confirmAnswer.value === 'yes') {
|
||||
if (confirmAnswer.value === "yes") {
|
||||
await serviceManager.removeContainers();
|
||||
console.log();
|
||||
await serviceManager.cleanData();
|
||||
logger.log('ok', 'All cleaned ✓');
|
||||
logger.log("ok", "All cleaned ✓");
|
||||
} else {
|
||||
logger.log('note', 'Cancelled');
|
||||
logger.log("note", "Cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfigureServices(serviceManager: ServiceManager) {
|
||||
helpers.printHeader('Configure Services');
|
||||
helpers.printHeader("Configure Services");
|
||||
await serviceManager.configureServices();
|
||||
}
|
||||
|
||||
function showHelp() {
|
||||
helpers.printHeader('GitZone Services Manager');
|
||||
export function showHelp(mode?: ICliMode) {
|
||||
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();
|
||||
|
||||
logger.log('note', 'Commands:');
|
||||
logger.log('info', ' start [service] Start services (mongo|s3|elasticsearch|all)');
|
||||
logger.log('info', ' stop [service] Stop services (mongo|s3|elasticsearch|all)');
|
||||
logger.log('info', ' restart [service] Restart services (mongo|s3|elasticsearch|all)');
|
||||
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', ' 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');
|
||||
logger.log("note", "Commands:");
|
||||
logger.log(
|
||||
"info",
|
||||
" start [service] Start services (mongo|s3|elasticsearch|all)",
|
||||
);
|
||||
logger.log(
|
||||
"info",
|
||||
" stop [service] Stop services (mongo|s3|elasticsearch|all)",
|
||||
);
|
||||
logger.log(
|
||||
"info",
|
||||
" restart [service] Restart services (mongo|s3|elasticsearch|all)",
|
||||
);
|
||||
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();
|
||||
|
||||
logger.log('note', 'Available Services:');
|
||||
logger.log('info', ' • MongoDB (mongo) - Document database');
|
||||
logger.log('info', ' • MinIO (s3) - S3-compatible object storage');
|
||||
logger.log('info', ' • Elasticsearch (elasticsearch) - Search and analytics engine');
|
||||
logger.log("note", "Available Services:");
|
||||
logger.log("info", " • MongoDB (mongo) - Document database");
|
||||
logger.log("info", " • MinIO (s3) - S3-compatible object storage");
|
||||
logger.log(
|
||||
"info",
|
||||
" • Elasticsearch (elasticsearch) - Search and analytics engine",
|
||||
);
|
||||
console.log();
|
||||
|
||||
logger.log('note', 'Features:');
|
||||
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('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');
|
||||
logger.log("note", "Features:");
|
||||
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("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();
|
||||
|
||||
logger.log('note', 'Examples:');
|
||||
logger.log('info', ' gitzone services start # Start all services');
|
||||
logger.log('info', ' gitzone services start mongo # Start only MongoDB');
|
||||
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 compass # Get MongoDB Compass connection');
|
||||
logger.log('info', ' gitzone services logs elasticsearch # Show Elasticsearch logs');
|
||||
logger.log("note", "Examples:");
|
||||
logger.log(
|
||||
"info",
|
||||
" gitzone services start # Start all services",
|
||||
);
|
||||
logger.log(
|
||||
"info",
|
||||
" gitzone services start mongo # Start only MongoDB",
|
||||
);
|
||||
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();
|
||||
|
||||
logger.log('note', 'Global Commands (-g/--global):');
|
||||
logger.log('info', ' list -g List all registered projects');
|
||||
logger.log('info', ' status -g Show status across all projects');
|
||||
logger.log('info', ' stop -g Stop all containers across all projects');
|
||||
logger.log('info', ' cleanup -g Remove stale registry entries');
|
||||
logger.log("note", "Global Commands (-g/--global):");
|
||||
logger.log("info", " list -g List all registered projects");
|
||||
logger.log("info", " status -g Show status across all projects");
|
||||
logger.log(
|
||||
"info",
|
||||
" stop -g Stop all containers across all projects",
|
||||
);
|
||||
logger.log("info", " cleanup -g Remove stale registry entries");
|
||||
console.log();
|
||||
|
||||
logger.log('note', 'Global Examples:');
|
||||
logger.log('info', ' 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)');
|
||||
logger.log("note", "Global Examples:");
|
||||
logger.log(
|
||||
"info",
|
||||
" 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 ====================
|
||||
@@ -280,23 +627,23 @@ async function handleGlobalCommand(command: string) {
|
||||
const globalRegistry = GlobalRegistry.getInstance();
|
||||
|
||||
switch (command) {
|
||||
case 'list':
|
||||
case "list":
|
||||
await handleGlobalList(globalRegistry);
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
case "status":
|
||||
await handleGlobalStatus(globalRegistry);
|
||||
break;
|
||||
|
||||
case 'stop':
|
||||
case "stop":
|
||||
await handleGlobalStop(globalRegistry);
|
||||
break;
|
||||
|
||||
case 'cleanup':
|
||||
case "cleanup":
|
||||
await handleGlobalCleanup(globalRegistry);
|
||||
break;
|
||||
|
||||
case 'help':
|
||||
case "help":
|
||||
default:
|
||||
showHelp();
|
||||
break;
|
||||
@@ -304,13 +651,13 @@ async function handleGlobalCommand(command: string) {
|
||||
}
|
||||
|
||||
async function handleGlobalList(globalRegistry: GlobalRegistry) {
|
||||
helpers.printHeader('Registered Projects (Global)');
|
||||
helpers.printHeader("Registered Projects (Global)");
|
||||
|
||||
const projects = await globalRegistry.getAllProjects();
|
||||
const projectPaths = Object.keys(projects);
|
||||
|
||||
if (projectPaths.length === 0) {
|
||||
logger.log('note', 'No projects registered');
|
||||
logger.log("note", "No projects registered");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -319,20 +666,20 @@ async function handleGlobalList(globalRegistry: GlobalRegistry) {
|
||||
const lastActive = new Date(project.lastActive).toLocaleString();
|
||||
|
||||
console.log();
|
||||
logger.log('ok', `📁 ${project.projectName}`);
|
||||
logger.log('info', ` Path: ${project.projectPath}`);
|
||||
logger.log('info', ` Services: ${project.enabledServices.join(', ')}`);
|
||||
logger.log('info', ` Last Active: ${lastActive}`);
|
||||
logger.log("ok", `📁 ${project.projectName}`);
|
||||
logger.log("info", ` Path: ${project.projectPath}`);
|
||||
logger.log("info", ` Services: ${project.enabledServices.join(", ")}`);
|
||||
logger.log("info", ` Last Active: ${lastActive}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGlobalStatus(globalRegistry: GlobalRegistry) {
|
||||
helpers.printHeader('Global Service Status');
|
||||
helpers.printHeader("Global Service Status");
|
||||
|
||||
const statuses = await globalRegistry.getGlobalStatus();
|
||||
|
||||
if (statuses.length === 0) {
|
||||
logger.log('note', 'No projects registered');
|
||||
logger.log("note", "No projects registered");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -341,28 +688,39 @@ async function handleGlobalStatus(globalRegistry: GlobalRegistry) {
|
||||
|
||||
for (const project of statuses) {
|
||||
console.log();
|
||||
logger.log('ok', `📁 ${project.projectName}`);
|
||||
logger.log('info', ` Path: ${project.projectPath}`);
|
||||
logger.log("ok", `📁 ${project.projectName}`);
|
||||
logger.log("info", ` Path: ${project.projectPath}`);
|
||||
|
||||
if (project.containers.length === 0) {
|
||||
logger.log('note', ' No containers configured');
|
||||
logger.log("note", " No containers configured");
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const container of project.containers) {
|
||||
totalContainers++;
|
||||
const statusIcon = container.status === 'running' ? '🟢' : container.status === 'exited' ? '🟡' : '⚪';
|
||||
if (container.status === 'running') runningCount++;
|
||||
logger.log('info', ` ${statusIcon} ${container.name}: ${container.status}`);
|
||||
const statusIcon =
|
||||
container.status === "running"
|
||||
? "🟢"
|
||||
: container.status === "exited"
|
||||
? "🟡"
|
||||
: "⚪";
|
||||
if (container.status === "running") runningCount++;
|
||||
logger.log(
|
||||
"info",
|
||||
` ${statusIcon} ${container.name}: ${container.status}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
helpers.printHeader('Stop All Containers (Global)');
|
||||
helpers.printHeader("Stop All Containers (Global)");
|
||||
|
||||
const statuses = await globalRegistry.getGlobalStatus();
|
||||
|
||||
@@ -370,64 +728,73 @@ async function handleGlobalStop(globalRegistry: GlobalRegistry) {
|
||||
let runningCount = 0;
|
||||
for (const project of statuses) {
|
||||
for (const container of project.containers) {
|
||||
if (container.status === 'running') runningCount++;
|
||||
if (container.status === "running") runningCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (runningCount === 0) {
|
||||
logger.log('note', 'No running containers found');
|
||||
logger.log("note", "No running containers found");
|
||||
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();
|
||||
|
||||
// Show what will be stopped
|
||||
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) {
|
||||
logger.log('info', `${project.projectName}:`);
|
||||
logger.log("info", `${project.projectName}:`);
|
||||
for (const container of runningContainers) {
|
||||
logger.log('info', ` • ${container.name}`);
|
||||
logger.log("info", ` • ${container.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
const shouldContinue = await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||
'Stop all containers?',
|
||||
false
|
||||
);
|
||||
const shouldContinue =
|
||||
await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||
"Stop all containers?",
|
||||
false,
|
||||
);
|
||||
|
||||
if (!shouldContinue) {
|
||||
logger.log('note', 'Cancelled');
|
||||
logger.log("note", "Cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log('note', 'Stopping all containers...');
|
||||
logger.log("note", "Stopping all containers...");
|
||||
const result = await globalRegistry.stopAll();
|
||||
|
||||
if (result.stopped.length > 0) {
|
||||
logger.log('ok', `Stopped: ${result.stopped.join(', ')}`);
|
||||
logger.log("ok", `Stopped: ${result.stopped.join(", ")}`);
|
||||
}
|
||||
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) {
|
||||
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();
|
||||
|
||||
if (removed.length === 0) {
|
||||
logger.log('ok', 'No stale entries found');
|
||||
logger.log("ok", "No stale entries found");
|
||||
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) {
|
||||
logger.log('info', ` • ${path}`);
|
||||
logger.log("info", ` • ${path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+191
-58
@@ -1,91 +1,224 @@
|
||||
/* -----------------------------------------------
|
||||
* executes as standard task
|
||||
* ----------------------------------------------- */
|
||||
import * as plugins from './mod.plugins.js';
|
||||
import * as paths from '../paths.js';
|
||||
import * as plugins from "./mod.plugins.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 () => {
|
||||
console.log('');
|
||||
console.log('╭─────────────────────────────────────────────────────────────╮');
|
||||
console.log('│ gitzone - Development Workflow CLI │');
|
||||
console.log('╰─────────────────────────────────────────────────────────────╯');
|
||||
console.log('');
|
||||
type ICommandHelpSummary = {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const commandSummaries: ICommandHelpSummary[] = [
|
||||
{
|
||||
name: "commit",
|
||||
description:
|
||||
"Create semantic commits or generate read-only commit recommendations",
|
||||
},
|
||||
{ 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: "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 response = await interactInstance.askQuestion({
|
||||
type: 'list',
|
||||
name: 'action',
|
||||
message: 'What would you like to do?',
|
||||
default: 'commit',
|
||||
type: "list",
|
||||
name: "action",
|
||||
message: "What would you like to do?",
|
||||
default: "commit",
|
||||
choices: [
|
||||
{ name: 'Commit changes (semantic versioning)', value: 'commit' },
|
||||
{ name: 'Format project files', value: 'format' },
|
||||
{ name: 'Configure release settings', value: 'config' },
|
||||
{ name: 'Create from template', value: 'template' },
|
||||
{ name: 'Manage dev services (MongoDB, S3)', value: 'services' },
|
||||
{ name: 'Open project assets', value: 'open' },
|
||||
{ name: 'Show help', value: 'help' },
|
||||
{ name: "Commit changes (semantic versioning)", value: "commit" },
|
||||
{ name: "Format project files", value: "format" },
|
||||
{ name: "Configure release settings", value: "config" },
|
||||
{ name: "Create from template", value: "template" },
|
||||
{ name: "Manage dev services (MongoDB, S3)", value: "services" },
|
||||
{ name: "Open project assets", value: "open" },
|
||||
{ name: "Show help", value: "help" },
|
||||
],
|
||||
});
|
||||
|
||||
const action = (response as any).value;
|
||||
|
||||
switch (action) {
|
||||
case 'commit': {
|
||||
const modCommit = await import('../mod_commit/index.js');
|
||||
await modCommit.run({ _: ['commit'] });
|
||||
case "commit": {
|
||||
const modCommit = await import("../mod_commit/index.js");
|
||||
await modCommit.run({ _: ["commit"] });
|
||||
break;
|
||||
}
|
||||
case 'format': {
|
||||
const modFormat = await import('../mod_format/index.js');
|
||||
case "format": {
|
||||
const modFormat = await import("../mod_format/index.js");
|
||||
await modFormat.run({ interactive: true });
|
||||
break;
|
||||
}
|
||||
case 'config': {
|
||||
const modConfig = await import('../mod_config/index.js');
|
||||
await modConfig.run({ _: ['config'] });
|
||||
case "config": {
|
||||
const modConfig = await import("../mod_config/index.js");
|
||||
await modConfig.run({ _: ["config"] });
|
||||
break;
|
||||
}
|
||||
case 'template': {
|
||||
const modTemplate = await import('../mod_template/index.js');
|
||||
await modTemplate.run({ _: ['template'] });
|
||||
case "template": {
|
||||
const modTemplate = await import("../mod_template/index.js");
|
||||
await modTemplate.run({ _: ["template"] });
|
||||
break;
|
||||
}
|
||||
case 'services': {
|
||||
const modServices = await import('../mod_services/index.js');
|
||||
await modServices.run({ _: ['services'] });
|
||||
case "services": {
|
||||
const modServices = await import("../mod_services/index.js");
|
||||
await modServices.run({ _: ["services"] });
|
||||
break;
|
||||
}
|
||||
case 'open': {
|
||||
const modOpen = await import('../mod_open/index.js');
|
||||
await modOpen.run({ _: ['open'] });
|
||||
case "open": {
|
||||
const modOpen = await import("../mod_open/index.js");
|
||||
await modOpen.run({ _: ["open"] });
|
||||
break;
|
||||
}
|
||||
case 'help':
|
||||
showHelp();
|
||||
case "help":
|
||||
await showHelp(mode);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
function showHelp(): void {
|
||||
console.log('');
|
||||
console.log('Usage: gitzone <command> [options]');
|
||||
console.log('');
|
||||
console.log('Commands:');
|
||||
console.log(' commit Create a semantic commit with versioning');
|
||||
console.log(' format Format and standardize project files');
|
||||
console.log(' config Manage release registry configuration');
|
||||
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');
|
||||
console.log(' deprecate Deprecate a package on npm');
|
||||
console.log(' meta Run meta commands');
|
||||
console.log(' start Start working on a project');
|
||||
console.log(' helpers Run helper utilities');
|
||||
console.log('');
|
||||
console.log('Run gitzone <command> --help for more information on a command.');
|
||||
console.log('');
|
||||
export async function showHelp(
|
||||
mode: ICliMode,
|
||||
commandName?: string,
|
||||
): Promise<void> {
|
||||
if (commandName) {
|
||||
const handled = await showCommandHelp(commandName, mode);
|
||||
if (handled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mode.json) {
|
||||
printJson({
|
||||
name: "gitzone",
|
||||
usage: "gitzone <command> [options]",
|
||||
commands: commandSummaries,
|
||||
globalFlags: [
|
||||
{ flag: "--help, -h", description: "Show help output" },
|
||||
{
|
||||
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 format plan --json");
|
||||
console.log(" gitzone services set mongodb,minio");
|
||||
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 "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;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import * as smartlog from '@push.rocks/smartlog';
|
||||
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 projectinfo from '@push.rocks/projectinfo';
|
||||
import * as smartcli from '@push.rocks/smartcli';
|
||||
@@ -20,7 +20,7 @@ export const smartfs = new SmartFs(new SmartFsProviderNode());
|
||||
export {
|
||||
smartlog,
|
||||
smartlogDestinationLocal,
|
||||
npmextra,
|
||||
smartconfig,
|
||||
path,
|
||||
projectinfo,
|
||||
smartcli,
|
||||
|
||||
Reference in New Issue
Block a user