Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad7f2742ff | |||
| b234ecc12a | |||
| 278df40ba7 | |||
| 6f0928e7c7 | |||
| 26effadcc9 | |||
| c38e94bcf3 | |||
| b9b51f29d1 | |||
| a3ad48368d | |||
| c10b764c0a | |||
| 7686504e4e | |||
| d96b220703 | |||
| 06f2de3230 | |||
| cc3128f07b | |||
| 358d677e72 | |||
| f421c5851d | |||
| a420157287 | |||
| 0e27d54ad2 | |||
| 738fbaa64f | |||
| fe7a9d93d1 | |||
| 9a4c8795d4 | |||
| faee6a1698 |
+32
-6
@@ -1,13 +1,12 @@
|
|||||||
{
|
{
|
||||||
"szci": {
|
"@ship.zone/szci": {
|
||||||
"npmGlobalTools": [],
|
"npmGlobalTools": []
|
||||||
"npmAccessLevel": "private",
|
|
||||||
"npmRegistryUrl": "verdaccio.lossless.one"
|
|
||||||
},
|
},
|
||||||
"tsdoc": {
|
"@git.zone/tsdoc": {
|
||||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||||
},
|
},
|
||||||
"@git.zone/cli": {
|
"@git.zone/cli": {
|
||||||
|
"schemaVersion": 2,
|
||||||
"projectType": "npm",
|
"projectType": "npm",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "gitlab.com",
|
"githost": "gitlab.com",
|
||||||
@@ -35,12 +34,39 @@
|
|||||||
"CI/CD"
|
"CI/CD"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"commit": {
|
||||||
|
"confirmation": "prompt",
|
||||||
|
"steps": ["analyze", "changelog", "commit"]
|
||||||
|
},
|
||||||
"release": {
|
"release": {
|
||||||
|
"confirmation": "prompt",
|
||||||
|
"preflight": {
|
||||||
|
"requireCleanTree": true,
|
||||||
|
"test": false,
|
||||||
|
"build": true
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"git": {
|
||||||
|
"enabled": true,
|
||||||
|
"remote": "origin",
|
||||||
|
"pushBranch": true,
|
||||||
|
"pushTags": true
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"enabled": true,
|
||||||
"registries": [
|
"registries": [
|
||||||
"https://verdaccio.lossless.digital",
|
"https://verdaccio.lossless.digital",
|
||||||
"https://registry.npmjs.org"
|
"https://registry.npmjs.org"
|
||||||
],
|
],
|
||||||
"accessLevel": "public"
|
"accessLevel": "public",
|
||||||
|
"alreadyPublished": "success"
|
||||||
|
},
|
||||||
|
"docker": {
|
||||||
|
"enabled": false,
|
||||||
|
"engine": "tsdocker",
|
||||||
|
"patterns": []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,13 @@
|
|||||||
"npmPackagename": "{{module.npmPackagename}}",
|
"npmPackagename": "{{module.npmPackagename}}",
|
||||||
"license": "{{module.license}}",
|
"license": "{{module.license}}",
|
||||||
"projectDomain": "{{module.projectDomain}}"
|
"projectDomain": "{{module.projectDomain}}"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"targets": {
|
||||||
|
"npm": {
|
||||||
|
"registries": ["{{npmPrivateRegistry}}"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@ship.zone/szci": {
|
"@ship.zone/szci": {
|
||||||
@@ -18,7 +25,6 @@
|
|||||||
},
|
},
|
||||||
"dockerBuildargEnvMap": {
|
"dockerBuildargEnvMap": {
|
||||||
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
|
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
|
||||||
},
|
}
|
||||||
"npmRegistryUrl": "{{npmPrivateRegistry}}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,12 @@ fileName: .smartconfig.json
|
|||||||
"projectDomain": "{{module.projectDomain}}"
|
"projectDomain": "{{module.projectDomain}}"
|
||||||
},
|
},
|
||||||
"release": {
|
"release": {
|
||||||
|
"targets": {
|
||||||
|
"npm": {
|
||||||
"accessLevel": "{{module.npmAccessLevel}}"
|
"accessLevel": "{{module.npmAccessLevel}}"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"@ship.zone/szci": {
|
"@ship.zone/szci": {
|
||||||
"npmGlobalTools": []
|
"npmGlobalTools": []
|
||||||
|
|||||||
@@ -9,6 +9,13 @@
|
|||||||
"npmPackagename": "{{module.npmPackagename}}",
|
"npmPackagename": "{{module.npmPackagename}}",
|
||||||
"license": "{{module.license}}",
|
"license": "{{module.license}}",
|
||||||
"projectDomain": "{{module.projectDomain}}"
|
"projectDomain": "{{module.projectDomain}}"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"targets": {
|
||||||
|
"npm": {
|
||||||
|
"registries": ["{{private.npmRegistryUrl}}"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@ship.zone/szci": {
|
"@ship.zone/szci": {
|
||||||
@@ -18,7 +25,6 @@
|
|||||||
},
|
},
|
||||||
"dockerBuildargEnvMap": {
|
"dockerBuildargEnvMap": {
|
||||||
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
|
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
|
||||||
},
|
}
|
||||||
"npmRegistryUrl": "{{private.npmRegistryUrl}}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,77 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Pending
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-05-14 - 2.19.1
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- migrate legacy release arrays during config fixes and validate release config shape (config)
|
||||||
|
- Automatically converts legacy release registry arrays into release.targets.npm.registries during smartconfig migration and config fix runs.
|
||||||
|
- Re-runs doctor checks after applying known migrations so resolved issues do not require the external fixer.
|
||||||
|
- Reports an explicit validation error when release config is not an object.
|
||||||
|
- Updates config fix prompts and help text to use generic configuration repair wording.
|
||||||
|
|
||||||
|
## 2026-05-13 - 2.19.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Delegate Docker release targets to `tsdocker`.
|
||||||
|
- Replace Docker image template publishing with `tsdocker push` execution.
|
||||||
|
- Move Docker registry behavior to `@git.zone/tsdocker` config and validate removed image config as invalid.
|
||||||
|
|
||||||
|
## 2026-05-10 - 2.18.1
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Run `gitzone config fix` opencode handoff with inherited terminal I/O.
|
||||||
|
|
||||||
|
## 2026-05-10 - 2.18.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add `gitzone config fix` to invoke opencode for configuration repair.
|
||||||
|
|
||||||
|
## 2026-05-10 - 2.17.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add guided project, CLI, release, and doctor flows to `gitzone config`.
|
||||||
|
|
||||||
|
## 2026-05-10 - 2.16.1
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Prevent startup update checks from crashing when installed package metadata is incomplete.
|
||||||
|
|
||||||
|
## 2026-05-10 - 2.16.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add `gitzone tools` for managing the global `@git.zone` toolchain from the main CLI.
|
||||||
|
|
||||||
|
## 2026-05-10 - 2.15.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Split `gitzone commit` and `gitzone release` into commit workflow and target-based release configuration.
|
||||||
|
- Add pending changelog handling so commits accumulate unreleased notes and releases move them into version sections.
|
||||||
|
- Add first-class release targets for git, npm, and Docker publishing.
|
||||||
|
|
||||||
|
## 2026-04-30 - 2.14.3 - fix(test)
|
||||||
|
move test workspace into .nogit and add bundled fixture project files
|
||||||
|
|
||||||
|
- updates package scripts to clone and run the sandbox test repository from .nogit/test
|
||||||
|
- adds a complete test fixture project under test/ for CLI, template, and documentation-related test scenarios
|
||||||
|
|
||||||
|
## 2026-04-30 - 2.14.2 - fix(package)
|
||||||
|
correct package entry point extension and align test scripts with pnpm
|
||||||
|
|
||||||
|
- Change the package main field from dist_ts/index.ts to dist_ts/index.js for the built CLI entry point.
|
||||||
|
- Update test and testBuild scripts to use pnpm run instead of npm run for consistent package manager usage.
|
||||||
|
|
||||||
## 2026-04-16 - 2.14.1 - fix(repo)
|
## 2026-04-16 - 2.14.1 - fix(repo)
|
||||||
no changes to commit
|
no changes to commit
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2015 Task Venture Capital GmbH (hello@lossless.com)
|
Copyright (c) 2015 Task Venture Capital GmbH (hello@task.vc)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
+17
-17
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/cli",
|
"name": "@git.zone/cli",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "2.14.1",
|
"version": "2.19.1",
|
||||||
"description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.",
|
"description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.",
|
||||||
"main": "dist_ts/index.ts",
|
"main": "dist_ts/index.js",
|
||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -11,21 +11,21 @@
|
|||||||
"gzone": "./cli.js"
|
"gzone": "./cli.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(npm run clean && npm run prepareTest && npm run testCli && npm run testFormat && npm run testCommit && npm run testDeprecate && npm run testVersion && npm run testReadme && npm run testUpdate && npm run testTemplateNpm && npm run testTemplateLit) && rm -rf test",
|
"test": "(pnpm run clean && pnpm run prepareTest && pnpm run testCli && pnpm run testFormat && pnpm run testCommit && pnpm run testDeprecate && pnpm run testVersion && pnpm run testReadme && pnpm run testUpdate && pnpm run testTemplateNpm && pnpm run testTemplateLit) && rm -rf .nogit/test",
|
||||||
"build": "tsbuild tsfolders",
|
"build": "tsbuild tsfolders",
|
||||||
"clean": "(rm -rf test/)",
|
"clean": "(rm -rf .nogit/test/)",
|
||||||
"prepareTest": "(git clone https://gitlab.com/sandboxzone/sandbox-npmts.git test/)",
|
"prepareTest": "(mkdir -p .nogit && git clone https://gitlab.com/sandboxzone/sandbox-npmts.git .nogit/test/)",
|
||||||
"testBuild": "npm run build && rm -r dist/",
|
"testBuild": "pnpm run build && rm -r dist/",
|
||||||
"testCli": "(cd test && node ../cli.ts.js)",
|
"testCli": "(cd .nogit/test && node ../../cli.ts.js)",
|
||||||
"testCommit": "(cd test && node ../cli.ts.js commit)",
|
"testCommit": "(cd .nogit/test && node ../../cli.ts.js commit)",
|
||||||
"testDeprecate": "(cd test && node ../cli.ts.js deprecate)",
|
"testDeprecate": "(cd .nogit/test && node ../../cli.ts.js deprecate)",
|
||||||
"testOpen": "(cd test && node ../cli.ts.js open ci)",
|
"testOpen": "(cd .nogit/test && node ../../cli.ts.js open ci)",
|
||||||
"testReadme": "(cd test && node ../cli.ts.js readme)",
|
"testReadme": "(cd .nogit/test && node ../../cli.ts.js readme)",
|
||||||
"testFormat": "(cd test && node ../cli.ts.js format)",
|
"testFormat": "(cd .nogit/test && node ../../cli.ts.js format)",
|
||||||
"testTemplateNpm": "(rm -rf test/testtemplate_npm/ && mkdir test/testtemplate_npm && cd test/testtemplate_npm && node ../../cli.ts.js template npm)",
|
"testTemplateNpm": "(rm -rf .nogit/test/testtemplate_npm/ && mkdir -p .nogit/test/testtemplate_npm && cd .nogit/test/testtemplate_npm && node ../../../cli.ts.js template npm)",
|
||||||
"testTemplateLit": "(rm -rf test/testtemplate_lit/ && mkdir test/testtemplate_lit && cd test/testtemplate_lit && node ../../cli.ts.js template lit)",
|
"testTemplateLit": "(rm -rf .nogit/test/testtemplate_lit/ && mkdir -p .nogit/test/testtemplate_lit && cd .nogit/test/testtemplate_lit && node ../../../cli.ts.js template lit)",
|
||||||
"testUpdate": "(cd test && node ../cli.ts.js update)",
|
"testUpdate": "(cd .nogit/test && node ../../cli.ts.js update)",
|
||||||
"testVersion": "(cd test && node ../cli.ts.js -v)",
|
"testVersion": "(cd .nogit/test && node ../../cli.ts.js -v)",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartscaf": "^4.0.21",
|
"@push.rocks/smartscaf": "^4.0.21",
|
||||||
"@push.rocks/smartshell": "^3.3.7",
|
"@push.rocks/smartshell": "^3.5.0",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@push.rocks/smartupdate": "^2.0.6",
|
"@push.rocks/smartupdate": "^2.0.6",
|
||||||
"prettier": "^3.8.1"
|
"prettier": "^3.8.1"
|
||||||
|
|||||||
Generated
+35
-23
@@ -81,8 +81,8 @@ importers:
|
|||||||
specifier: ^4.0.21
|
specifier: ^4.0.21
|
||||||
version: 4.0.21
|
version: 4.0.21
|
||||||
'@push.rocks/smartshell':
|
'@push.rocks/smartshell':
|
||||||
specifier: ^3.3.7
|
specifier: ^3.5.0
|
||||||
version: 3.3.7
|
version: 3.5.0
|
||||||
'@push.rocks/smartunique':
|
'@push.rocks/smartunique':
|
||||||
specifier: ^3.0.9
|
specifier: ^3.0.9
|
||||||
version: 3.0.9
|
version: 3.0.9
|
||||||
@@ -1063,6 +1063,9 @@ packages:
|
|||||||
'@push.rocks/smartdelay@3.0.5':
|
'@push.rocks/smartdelay@3.0.5':
|
||||||
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
|
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
|
||||||
|
|
||||||
|
'@push.rocks/smartdelay@3.1.0':
|
||||||
|
resolution: {integrity: sha512-59xveBMbWmbFhh/rqhQnYG/klg/VONG9hV8+RQ7ftqsNRkcmUT+VM5etAbODgAUvsF4lxK+xVR0tbZOo0kGhRQ==}
|
||||||
|
|
||||||
'@push.rocks/smartdiff@1.1.0':
|
'@push.rocks/smartdiff@1.1.0':
|
||||||
resolution: {integrity: sha512-AAz/unmko0C+g+60odOoK32PE3Ci3YLoB+zfg1LGLyVRCthcdzjqa1C2Km0MfG7IyJQKPdj8J5HPubtpm3ZeaQ==}
|
resolution: {integrity: sha512-AAz/unmko0C+g+60odOoK32PE3Ci3YLoB+zfg1LGLyVRCthcdzjqa1C2Km0MfG7IyJQKPdj8J5HPubtpm3ZeaQ==}
|
||||||
|
|
||||||
@@ -1192,6 +1195,9 @@ packages:
|
|||||||
'@push.rocks/smartpromise@4.2.3':
|
'@push.rocks/smartpromise@4.2.3':
|
||||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||||
|
|
||||||
|
'@push.rocks/smartpromise@4.2.4':
|
||||||
|
resolution: {integrity: sha512-8FUyYt94hOIY9mqHjitn4h69u0jbEtTF2RKKw2DpiTVFjpDTk9gXbVHZ/V+xEcBrN4mrzdQES0OiDmkNPoddEQ==}
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5':
|
'@push.rocks/smartpuppeteer@2.0.5':
|
||||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||||
|
|
||||||
@@ -1222,8 +1228,8 @@ packages:
|
|||||||
'@push.rocks/smartserve@2.0.1':
|
'@push.rocks/smartserve@2.0.1':
|
||||||
resolution: {integrity: sha512-YQb2qexfCzCqOlLWBBXKMg6xG4zahCPAxomz/KEKAwHtW6wMTtuHKSTSkRTQ0vl9jssLMAmRz2OyafiL9XGJXQ==}
|
resolution: {integrity: sha512-YQb2qexfCzCqOlLWBBXKMg6xG4zahCPAxomz/KEKAwHtW6wMTtuHKSTSkRTQ0vl9jssLMAmRz2OyafiL9XGJXQ==}
|
||||||
|
|
||||||
'@push.rocks/smartshell@3.3.7':
|
'@push.rocks/smartshell@3.5.0':
|
||||||
resolution: {integrity: sha512-b3st2+FjHUVhZZRlXfw93+SQA0UMVlURqe55uVpWdjJX7jeGXTTeszuYygtiR99zC5iZ8WZhGDct3N2L1qc/qw==}
|
resolution: {integrity: sha512-Hx9TVvC/AWxZsnm1GDb+W4Fe58nf1FkKbSBABUgkxct4XRYugBI2z9Twnjm3R9vdRry8oy0enfR9NPVhisGaGA==}
|
||||||
|
|
||||||
'@push.rocks/smartspawn@3.0.3':
|
'@push.rocks/smartspawn@3.0.3':
|
||||||
resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==}
|
resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==}
|
||||||
@@ -3917,9 +3923,9 @@ packages:
|
|||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
which@6.0.1:
|
which@7.0.0:
|
||||||
resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==}
|
resolution: {integrity: sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==}
|
||||||
engines: {node: ^20.17.0 || >=22.9.0}
|
engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
wordwrap@1.0.0:
|
wordwrap@1.0.0:
|
||||||
@@ -4802,7 +4808,7 @@ snapshots:
|
|||||||
'@push.rocks/smartlog': 3.2.1
|
'@push.rocks/smartlog': 3.2.1
|
||||||
'@push.rocks/smartlog-destination-local': 9.0.2
|
'@push.rocks/smartlog-destination-local': 9.0.2
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartshell': 3.3.7
|
'@push.rocks/smartshell': 3.5.0
|
||||||
'@push.rocks/smarttime': 4.2.3
|
'@push.rocks/smarttime': 4.2.3
|
||||||
typedoc: 0.28.17(typescript@5.9.3)
|
typedoc: 0.28.17(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@@ -4832,7 +4838,7 @@ snapshots:
|
|||||||
'@push.rocks/smartnpm': 2.0.6
|
'@push.rocks/smartnpm': 2.0.6
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartrequest': 5.0.1
|
'@push.rocks/smartrequest': 5.0.1
|
||||||
'@push.rocks/smartshell': 3.3.7
|
'@push.rocks/smartshell': 3.5.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
- aws-crt
|
- aws-crt
|
||||||
@@ -4845,7 +4851,7 @@ snapshots:
|
|||||||
'@git.zone/tsrun@2.0.1':
|
'@git.zone/tsrun@2.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartfile': 13.1.2
|
'@push.rocks/smartfile': 13.1.2
|
||||||
'@push.rocks/smartshell': 3.3.7
|
'@push.rocks/smartshell': 3.5.0
|
||||||
tsx: 4.21.0
|
tsx: 4.21.0
|
||||||
|
|
||||||
'@git.zone/tstest@3.3.2(socks@2.8.7)(typescript@5.9.3)':
|
'@git.zone/tstest@3.3.2(socks@2.8.7)(typescript@5.9.3)':
|
||||||
@@ -4870,7 +4876,7 @@ snapshots:
|
|||||||
'@push.rocks/smartrequest': 5.0.1
|
'@push.rocks/smartrequest': 5.0.1
|
||||||
'@push.rocks/smarts3': 5.3.0
|
'@push.rocks/smarts3': 5.3.0
|
||||||
'@push.rocks/smartserve': 2.0.1
|
'@push.rocks/smartserve': 2.0.1
|
||||||
'@push.rocks/smartshell': 3.3.7
|
'@push.rocks/smartshell': 3.5.0
|
||||||
'@push.rocks/smarttime': 4.2.3
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@push.rocks/smartwatch': 6.3.0
|
'@push.rocks/smartwatch': 6.3.0
|
||||||
'@types/ws': 8.18.1
|
'@types/ws': 8.18.1
|
||||||
@@ -5590,7 +5596,7 @@ snapshots:
|
|||||||
'@push.rocks/smartai': 2.0.0(typescript@5.9.3)(ws@8.19.0)(zod@3.25.76)
|
'@push.rocks/smartai': 2.0.0(typescript@5.9.3)(ws@8.19.0)(zod@3.25.76)
|
||||||
'@push.rocks/smartfs': 1.5.0
|
'@push.rocks/smartfs': 1.5.0
|
||||||
'@push.rocks/smartrequest': 5.0.1
|
'@push.rocks/smartrequest': 5.0.1
|
||||||
'@push.rocks/smartshell': 3.3.7
|
'@push.rocks/smartshell': 3.5.0
|
||||||
ai: 6.0.116(zod@3.25.76)
|
ai: 6.0.116(zod@3.25.76)
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -5775,6 +5781,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
|
||||||
|
'@push.rocks/smartdelay@3.1.0':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartpromise': 4.2.4
|
||||||
|
|
||||||
'@push.rocks/smartdiff@1.1.0':
|
'@push.rocks/smartdiff@1.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
diff: 8.0.3
|
diff: 8.0.3
|
||||||
@@ -5818,7 +5828,7 @@ snapshots:
|
|||||||
'@push.rocks/smartexit@2.0.3':
|
'@push.rocks/smartexit@2.0.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.3.1
|
'@push.rocks/lik': 6.3.1
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.4
|
||||||
|
|
||||||
'@push.rocks/smartexpect@2.5.0':
|
'@push.rocks/smartexpect@2.5.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5897,7 +5907,7 @@ snapshots:
|
|||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartshell': 3.3.7
|
'@push.rocks/smartshell': 3.5.0
|
||||||
'@push.rocks/smartstring': 4.1.0
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smarttime': 4.2.3
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@types/diff': 8.0.0
|
'@types/diff': 8.0.0
|
||||||
@@ -5968,7 +5978,7 @@ snapshots:
|
|||||||
'@push.rocks/smartmustache': 3.0.2
|
'@push.rocks/smartmustache': 3.0.2
|
||||||
'@push.rocks/smartpnpm': 1.0.6
|
'@push.rocks/smartpnpm': 1.0.6
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartshell': 3.3.7
|
'@push.rocks/smartshell': 3.5.0
|
||||||
'@tsclass/tsclass': 4.4.4
|
'@tsclass/tsclass': 4.4.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -6159,14 +6169,16 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartpnpm@1.0.6':
|
'@push.rocks/smartpnpm@1.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartshell': 3.3.7
|
'@push.rocks/smartshell': 3.5.0
|
||||||
|
|
||||||
'@push.rocks/smartpromise@4.2.3': {}
|
'@push.rocks/smartpromise@4.2.3': {}
|
||||||
|
|
||||||
|
'@push.rocks/smartpromise@4.2.4': {}
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
|
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartshell': 3.3.7
|
'@push.rocks/smartshell': 3.5.0
|
||||||
puppeteer: 24.35.0(typescript@5.9.3)
|
puppeteer: 24.35.0(typescript@5.9.3)
|
||||||
tree-kill: 1.2.2
|
tree-kill: 1.2.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -6233,7 +6245,7 @@ snapshots:
|
|||||||
'@push.rocks/smartinteract': 2.0.16
|
'@push.rocks/smartinteract': 2.0.16
|
||||||
'@push.rocks/smartobject': 1.0.12
|
'@push.rocks/smartobject': 1.0.12
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartshell': 3.3.7
|
'@push.rocks/smartshell': 3.5.0
|
||||||
'@push.rocks/smartyaml': 3.0.4
|
'@push.rocks/smartyaml': 3.0.4
|
||||||
|
|
||||||
'@push.rocks/smartserve@2.0.1':
|
'@push.rocks/smartserve@2.0.1':
|
||||||
@@ -6249,13 +6261,13 @@ snapshots:
|
|||||||
- bufferutil
|
- bufferutil
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
'@push.rocks/smartshell@3.3.7':
|
'@push.rocks/smartshell@3.5.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.1.0
|
||||||
'@push.rocks/smartexit': 2.0.3
|
'@push.rocks/smartexit': 2.0.3
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.4
|
||||||
'@types/which': 3.0.4
|
'@types/which': 3.0.4
|
||||||
which: 6.0.1
|
which: 7.0.0
|
||||||
|
|
||||||
'@push.rocks/smartspawn@3.0.3':
|
'@push.rocks/smartspawn@3.0.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9513,7 +9525,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
isexe: 2.0.0
|
isexe: 2.0.0
|
||||||
|
|
||||||
which@6.0.1:
|
which@7.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
isexe: 4.0.0
|
isexe: 4.0.0
|
||||||
|
|
||||||
|
|||||||
+25
-17
@@ -38,7 +38,7 @@ The format module is responsible for project standardization:
|
|||||||
2. **copy** - File copying with glob patterns (fully implemented)
|
2. **copy** - File copying with glob patterns (fully implemented)
|
||||||
3. **gitignore** - Creates/updates .gitignore from templates
|
3. **gitignore** - Creates/updates .gitignore from templates
|
||||||
4. **license** - Checks dependency licenses for compatibility
|
4. **license** - Checks dependency licenses for compatibility
|
||||||
5. **npmextra** - Manages project metadata and configuration
|
5. **smartconfig** - Manages project metadata and configuration
|
||||||
6. **packagejson** - Formats and updates package.json
|
6. **packagejson** - Formats and updates package.json
|
||||||
7. **prettier** - Applies code formatting with batching
|
7. **prettier** - Applies code formatting with batching
|
||||||
8. **readme** - Ensures readme files exist
|
8. **readme** - Ensures readme files exist
|
||||||
@@ -89,28 +89,26 @@ The format module is responsible for project standardization:
|
|||||||
5. **Performance Optimizations**: Parallel execution and caching
|
5. **Performance Optimizations**: Parallel execution and caching
|
||||||
6. **Reporting**: Diff views, statistics, verbose logging
|
6. **Reporting**: Diff views, statistics, verbose logging
|
||||||
7. **Architecture**: Clean separation of concerns with new classes
|
7. **Architecture**: Clean separation of concerns with new classes
|
||||||
8. **Unified Version Bumping**: Self-managed version updates eliminating npm warning pollution in deno.json
|
8. **Split Commit/Release Workflows**: `commit` creates source commits; `release` owns versioning, tags, and artifact publishing
|
||||||
|
|
||||||
### Version Bumping Refactor (Latest)
|
### Commit/Release Workflow Refactor (Latest)
|
||||||
|
|
||||||
The commit module's version bumping has been refactored to eliminate npm command dependencies:
|
The commit module no longer bumps versions, creates tags, or publishes packages. Release work now belongs to `gitzone release`:
|
||||||
|
|
||||||
**Changes:**
|
**Changes:**
|
||||||
|
|
||||||
- Removed `bumpNpmVersion()` - was causing npm warnings to pollute deno.json
|
- `gitzone commit` analyzes changes, updates `changelog.md` `Pending`, commits, and optionally pushes.
|
||||||
- Removed `syncVersionToDenoJson()` - no longer needed with unified approach
|
- `gitzone release` reads `Pending`, bumps versions, moves changelog entries into a version section, tags, pushes, and publishes configured artifacts.
|
||||||
- Removed separate `bumpDenoVersion()` - replaced by unified implementation
|
- Commit workflow steps are configured in `.smartconfig.json` under `@git.zone/cli.commit.steps`.
|
||||||
- Added `readCurrentVersion()` helper - reads from either package.json or deno.json
|
- Smartconfig schema versioning lives at `@git.zone/cli.schemaVersion`; run `gitzone config migrate <version>` for targeted migrations.
|
||||||
- Added `updateVersionFile()` helper - updates JSON files directly
|
- Release publishing is target-based under `@git.zone/cli.release.targets`.
|
||||||
- Unified `bumpProjectVersion()` - handles npm/deno/both with single clean code path
|
- NPM registries only live under `@git.zone/cli.release.targets.npm.registries`.
|
||||||
|
|
||||||
**Benefits:**
|
**Benefits:**
|
||||||
|
|
||||||
- No npm warning pollution in version fields
|
- Commit is safer and has no publishing side effects.
|
||||||
- Full control over version bumping process
|
- Multiple source commits can accumulate into one release via `Pending`.
|
||||||
- Simpler git history (no amending, no force-tagging)
|
- Per-artifact release results can distinguish published, already-published, skipped, and failed targets.
|
||||||
- Same code path for all project types
|
|
||||||
- Reuses existing `calculateNewVersion()` function
|
|
||||||
|
|
||||||
### Auto-Accept Flag for Commits
|
### Auto-Accept Flag for Commits
|
||||||
|
|
||||||
@@ -195,12 +193,22 @@ gitzone format
|
|||||||
# Read-only JSON plan
|
# Read-only JSON plan
|
||||||
gitzone format plan --json
|
gitzone format plan --json
|
||||||
|
|
||||||
|
# CI-friendly check, exits non-zero when changes or validator errors remain
|
||||||
|
gitzone format check
|
||||||
|
|
||||||
# Dry run to preview changes
|
# Dry run to preview changes
|
||||||
gitzone format --dry-run
|
gitzone format --dry-run
|
||||||
|
|
||||||
|
# Limit formatter modules
|
||||||
|
gitzone format --only prettier,packagejson
|
||||||
|
gitzone format --skip license
|
||||||
|
|
||||||
# Non-interactive apply
|
# Non-interactive apply
|
||||||
gitzone format --write --yes
|
gitzone format --write --yes
|
||||||
|
|
||||||
|
# Deterministic format first, opencode for remaining issues
|
||||||
|
gitzone format fix
|
||||||
|
|
||||||
# Plan only (no execution)
|
# Plan only (no execution)
|
||||||
gitzone format --plan-only
|
gitzone format --plan-only
|
||||||
|
|
||||||
@@ -219,7 +227,7 @@ gitzone format --detailed
|
|||||||
# Inspect config for agents and scripts
|
# Inspect config for agents and scripts
|
||||||
gitzone config show --json
|
gitzone config show --json
|
||||||
gitzone config set cli.output json
|
gitzone config set cli.output json
|
||||||
gitzone config get release.accessLevel
|
gitzone config get release.targets.npm.accessLevel
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Issues (Now Resolved)
|
## Common Issues (Now Resolved)
|
||||||
@@ -291,7 +299,7 @@ All sync methods must become async. Functions that were previously synchronous (
|
|||||||
**Affected Modules:**
|
**Affected Modules:**
|
||||||
|
|
||||||
- ts/mod_format/\* (largest area - 15+ files)
|
- ts/mod_format/\* (largest area - 15+ files)
|
||||||
- ts/mod_commit/\* (version bumping)
|
- ts/mod_commit/\* and ts/mod_release/\* (commit/release workflows)
|
||||||
- ts/mod_services/\* (configuration management)
|
- ts/mod_services/\* (configuration management)
|
||||||
- ts/mod_meta/\* (meta repository management)
|
- ts/mod_meta/\* (meta repository management)
|
||||||
- ts/mod_standard/\* (template listing)
|
- ts/mod_standard/\* (template listing)
|
||||||
|
|||||||
@@ -1,574 +1,448 @@
|
|||||||
# @git.zone/cli 🚀
|
# @git.zone/cli 🚀
|
||||||
|
|
||||||
**The ultimate CLI toolbelt for modern TypeScript development workflows**
|
`@git.zone/cli` is the development workflow CLI behind the `gitzone` and `gzone` commands. It helps TypeScript-heavy teams keep projects tidy, create semantic source commits, manage local Docker-backed services, scaffold new modules, and release software through explicit, target-based release configuration.
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/@git.zone/cli)
|
It is opinionated where that saves time: source commits and releases are separate, changelog entries flow through a standard `Pending` section, project config lives in `.smartconfig.json`, and release targets make side effects visible before they happen.
|
||||||
[](https://opensource.org/licenses/MIT)
|
|
||||||
|
|
||||||
## 🎯 What is gitzone?
|
|
||||||
|
|
||||||
gitzone is a powerful command-line interface that supercharges your development workflow with automated project management, intelligent code formatting, seamless version control, and development service orchestration. Whether you're bootstrapping a new TypeScript project, maintaining code quality, managing complex multi-repository setups, or spinning up local development databases, gitzone has got you covered.
|
|
||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
|
|
||||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||||
|
|
||||||
## 🏃♂️ Quick Start
|
## Install
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install globally via pnpm (recommended)
|
|
||||||
pnpm add -g @git.zone/cli
|
pnpm add -g @git.zone/cli
|
||||||
|
|
||||||
# Or with npm
|
|
||||||
npm install -g @git.zone/cli
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Once installed, you can use either `gitzone` or the shorter `gzone` command from anywhere in your terminal.
|
After installation, both binaries point to the same CLI:
|
||||||
|
|
||||||
### Your First Commands
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a new TypeScript npm package
|
gitzone --help
|
||||||
gitzone template npm
|
gzone --help
|
||||||
|
```
|
||||||
|
|
||||||
# Format your entire codebase (dry-run by default)
|
## The Big Idea
|
||||||
|
|
||||||
|
`gitzone commit` handles source history.
|
||||||
|
|
||||||
|
`gitzone release` handles release transactions.
|
||||||
|
|
||||||
|
That split is intentional. A commit should not unexpectedly publish npm packages, push Docker images, or trigger remote release pipelines. A release should clearly show which targets it will publish to.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Preview project standardization work
|
||||||
gitzone format
|
gitzone format
|
||||||
|
|
||||||
# Apply formatting changes
|
# Apply formatting changes
|
||||||
gitzone format --write
|
gitzone format --write
|
||||||
|
|
||||||
# Start local MongoDB and MinIO services
|
# Create a semantic source commit
|
||||||
gitzone services start
|
|
||||||
|
|
||||||
# Create a semantic commit with AI-powered suggestions
|
|
||||||
gitzone commit
|
gitzone commit
|
||||||
|
|
||||||
|
# Preview the configured release transaction
|
||||||
|
gitzone release --plan
|
||||||
|
|
||||||
|
# Release pending changelog entries to configured targets
|
||||||
|
gitzone release
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠️ Core Features
|
## Commands
|
||||||
|
|
||||||
### 🔀 Semantic Commits & Versioning
|
| Command | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `commit` | Analyze changes and create one semantic source commit |
|
||||||
|
| `release` | Turn pending changelog entries into a versioned release and publish targets |
|
||||||
|
| `format` | Plan or apply project formatting and standardization |
|
||||||
|
| `config` | Inspect, update, and migrate `.smartconfig.json` |
|
||||||
|
| `services` | Manage local MongoDB, MinIO, and Elasticsearch containers |
|
||||||
|
| `tools` | Manage the global `@git.zone` toolchain |
|
||||||
|
| `template` | Scaffold projects from built-in templates |
|
||||||
|
| `meta` | Manage multi-repository workspaces |
|
||||||
|
| `open` | Open repository assets like CI pages |
|
||||||
|
| `docker` | Run Docker maintenance tasks |
|
||||||
|
| `deprecate` | Deprecate npm packages across registries |
|
||||||
|
| `start` | Prepare an existing project for local work |
|
||||||
|
| `helpers` | Run small helper utilities |
|
||||||
|
|
||||||
Create standardized commits with AI-powered suggestions that automatically handle versioning:
|
Global flags include `--help`, `--json`, `--plain`, `--agent`, `--no-interactive`, and `--no-check-updates`.
|
||||||
|
|
||||||
|
## Toolchain Management
|
||||||
|
|
||||||
|
`gitzone tools` replaces the former `gtools` command from `@git.zone/tools`. It manages globally installed `@git.zone` development tools through pnpm.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Interactive commit with AI recommendations
|
# Check installed @git.zone tools and update outdated packages
|
||||||
|
gitzone tools update
|
||||||
|
|
||||||
|
# Update without prompts
|
||||||
|
gitzone tools update -y
|
||||||
|
|
||||||
|
# Install missing managed @git.zone tools
|
||||||
|
gitzone tools install
|
||||||
|
```
|
||||||
|
|
||||||
|
`gitzone tools update` checks `@git.zone/cli` first. If the CLI itself needs an update, it updates `@git.zone/cli` and asks you to rerun the command before updating the rest of the toolchain.
|
||||||
|
|
||||||
|
## Commit Workflow
|
||||||
|
|
||||||
|
`gitzone commit` creates one semantic source commit. It does not bump versions, create tags, publish packages, or push Docker images.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive semantic commit
|
||||||
gitzone commit
|
gitzone commit
|
||||||
|
|
||||||
# Read-only recommendation for agents and scripts
|
# Read-only AI recommendation
|
||||||
gitzone commit recommend --json
|
gitzone commit recommend --json
|
||||||
|
|
||||||
# Auto-accept AI recommendations (skipped for BREAKING CHANGEs)
|
# Auto-accept safe recommendations
|
||||||
gitzone commit -y
|
gitzone commit -y
|
||||||
|
|
||||||
# Auto-accept, push, build, and release
|
# Auto-accept, test, build, and push
|
||||||
gitzone commit -ypbr
|
gitzone commit -ytbp
|
||||||
|
|
||||||
|
# Show the resolved workflow without mutating anything
|
||||||
|
gitzone commit --plan
|
||||||
```
|
```
|
||||||
|
|
||||||
**Flags:**
|
The commit flow:
|
||||||
|
|
||||||
| Flag | Long Form | Description |
|
1. Analyze the working tree.
|
||||||
| ---- | ----------- | ---------------------------------------- |
|
2. Suggest commit type, scope, and message.
|
||||||
| `-y` | `--yes` | Auto-accept AI recommendations |
|
3. Write a human-readable entry into `changelog.md` under `## Pending`.
|
||||||
| `-p` | `--push` | Push to remote after commit |
|
4. Stage and create one semantic source commit.
|
||||||
| `-t` | `--test` | Run tests before committing |
|
5. Optionally run formatting, tests, build, and push based on flags or config.
|
||||||
| `-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:**
|
Commit flags:
|
||||||
|
|
||||||
1. 🤖 **AI-powered analysis** — analyzes your changes and suggests commit type, scope, and message
|
| Flag | Meaning |
|
||||||
2. 📝 Interactive commit message builder (type: `fix`/`feat`/`BREAKING CHANGE`, scope, description)
|
| --- | --- |
|
||||||
3. 📜 Automatic changelog generation
|
| `-y`, `--yes` | Auto-accept safe recommendations |
|
||||||
4. 🏷️ Automatic version bumping (major/minor/patch) with git tag creation
|
| `-t`, `--test` | Add test step |
|
||||||
5. 🔨 Optional build & verification
|
| `-b`, `--build` | Add build step |
|
||||||
6. 🚀 Optional push to origin
|
| `-p`, `--push` | Push after the source commit |
|
||||||
7. 📦 Optional publish to npm registries
|
| `-f`, `--format` | Run `gitzone format --write` before commit |
|
||||||
|
| `--plan` | Show resolved workflow only |
|
||||||
|
|
||||||
Supports both npm (`package.json`) and Deno (`deno.json`) projects, including dual-type projects.
|
`-r` is intentionally not part of commit anymore. Use `gitzone release`.
|
||||||
|
|
||||||
### 🎨 Intelligent Code Formatting
|
## Release Workflow
|
||||||
|
|
||||||
Automatically format and standardize your entire codebase. **Dry-run by default** — nothing changes until you explicitly use `--write`:
|
`gitzone release` performs the release core once, then publishes to configured targets.
|
||||||
|
|
||||||
|
The release core is not configurable plumbing. It always follows the same professional release transaction:
|
||||||
|
|
||||||
|
1. Run configured preflight checks.
|
||||||
|
2. Read `changelog.md` `## Pending` entries.
|
||||||
|
3. Infer or accept a semver bump.
|
||||||
|
4. Update version files and baked commit info.
|
||||||
|
5. Move pending changelog entries into the new version section.
|
||||||
|
6. Create the local release commit.
|
||||||
|
7. Create the local release tag.
|
||||||
|
|
||||||
|
Targets decide what happens after that:
|
||||||
|
|
||||||
|
| Target | What it does |
|
||||||
|
| --- | --- |
|
||||||
|
| `git` | Pushes the release commit and tags, often triggering remote CI release builds |
|
||||||
|
| `npm` | Publishes the package to configured npm registries |
|
||||||
|
| `docker` | Delegates container builds and pushes to `tsdocker` |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Preview what would change (default behavior)
|
# Preview the resolved release plan
|
||||||
|
gitzone release --plan
|
||||||
|
|
||||||
|
# Release to configured targets
|
||||||
|
gitzone release
|
||||||
|
|
||||||
|
# Release only to npm
|
||||||
|
gitzone release --target npm
|
||||||
|
|
||||||
|
# Release only to git and Docker
|
||||||
|
gitzone release --target git,docker
|
||||||
|
|
||||||
|
# Skip package/container publishing and keep only git target
|
||||||
|
gitzone release --no-publish
|
||||||
|
|
||||||
|
# Override inferred semver level
|
||||||
|
gitzone release --minor
|
||||||
|
```
|
||||||
|
|
||||||
|
Release flags:
|
||||||
|
|
||||||
|
| Flag | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `-y`, `--yes` | Run without interactive confirmation |
|
||||||
|
| `-t`, `--test` | Enable preflight tests |
|
||||||
|
| `-b`, `--build` | Enable preflight build |
|
||||||
|
| `-p`, `--push` | Enable the `git` target |
|
||||||
|
| `--target <csv>` | Use only selected targets, e.g. `git,npm` |
|
||||||
|
| `--npm` | Enable the `npm` target |
|
||||||
|
| `--docker` | Enable the `docker` target |
|
||||||
|
| `--no-publish` | Keep release core and `git` target only |
|
||||||
|
| `--no-build` | Disable preflight build for this run |
|
||||||
|
| `--major`, `--minor`, `--patch` | Override inferred semver level |
|
||||||
|
| `--plan` | Show resolved workflow only |
|
||||||
|
|
||||||
|
## Standard Changelog
|
||||||
|
|
||||||
|
The changelog is convention-based and intentionally not configured.
|
||||||
|
|
||||||
|
`gitzone commit` appends entries to:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Pending
|
||||||
|
```
|
||||||
|
|
||||||
|
`gitzone release` moves those pending entries into a dated version section:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 2026-05-10 - 2.15.0
|
||||||
|
```
|
||||||
|
|
||||||
|
The standard buckets are `Breaking Changes`, `Features`, `Fixes`, `Documentation`, and `Maintenance`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
CLI workflow config lives under `@git.zone/cli` in `.smartconfig.json`. Docker build and registry behavior lives under `@git.zone/tsdocker` and is used by the Docker release target.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@git.zone/cli": {
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"projectType": "npm",
|
||||||
|
"commit": {
|
||||||
|
"confirmation": "prompt",
|
||||||
|
"steps": ["analyze", "test", "build", "changelog", "commit", "push"]
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"confirmation": "prompt",
|
||||||
|
"preflight": {
|
||||||
|
"requireCleanTree": true,
|
||||||
|
"test": false,
|
||||||
|
"build": true
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"git": {
|
||||||
|
"enabled": true,
|
||||||
|
"remote": "origin",
|
||||||
|
"pushBranch": true,
|
||||||
|
"pushTags": true
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"enabled": true,
|
||||||
|
"registries": ["https://registry.npmjs.org"],
|
||||||
|
"accessLevel": "public",
|
||||||
|
"alreadyPublished": "success"
|
||||||
|
},
|
||||||
|
"docker": {
|
||||||
|
"enabled": true,
|
||||||
|
"engine": "tsdocker",
|
||||||
|
"patterns": [],
|
||||||
|
"cached": true,
|
||||||
|
"parallel": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@git.zone/tsdocker": {
|
||||||
|
"registries": ["registry.gitlab.com"],
|
||||||
|
"registryRepoMap": {
|
||||||
|
"registry.gitlab.com": "myorg/myproject"
|
||||||
|
},
|
||||||
|
"platforms": ["linux/amd64", "linux/arm64"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
NPM registries belong only here:
|
||||||
|
|
||||||
|
```text
|
||||||
|
@git.zone/cli.release.targets.npm.registries
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker registries belong only here and should be registry hosts without `http://` or `https://`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
@git.zone/tsdocker.registries
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful config commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show current @git.zone/cli config
|
||||||
|
gitzone config show --json
|
||||||
|
|
||||||
|
# Configure project basics, CLI behavior, and release targets interactively
|
||||||
|
gitzone config project
|
||||||
|
gitzone config cli
|
||||||
|
gitzone config release
|
||||||
|
|
||||||
|
# Validate schema, legacy keys, release targets, registries, and npm auth
|
||||||
|
gitzone config doctor
|
||||||
|
|
||||||
|
# Use opencode to repair configuration issues found by doctor
|
||||||
|
gitzone config fix
|
||||||
|
|
||||||
|
# Read the npm release target registries
|
||||||
|
gitzone config get release.targets.npm.registries
|
||||||
|
|
||||||
|
# Add an npm release target registry
|
||||||
|
gitzone config add https://registry.npmjs.org
|
||||||
|
|
||||||
|
# Set npm target access level
|
||||||
|
gitzone config access public
|
||||||
|
|
||||||
|
# Run schema migration to v2
|
||||||
|
gitzone config migrate 2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
`gitzone format` is dry-run by default. That makes it safe to run in any repo.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Preview changes
|
||||||
gitzone format
|
gitzone format
|
||||||
|
|
||||||
# Emit a machine-readable plan
|
# Emit a machine-readable plan
|
||||||
gitzone format plan --json
|
gitzone format plan --json
|
||||||
|
|
||||||
|
# Fail when formatting changes or validator errors remain
|
||||||
|
gitzone format check
|
||||||
|
|
||||||
|
# Run a subset of formatters
|
||||||
|
gitzone format --only prettier,packagejson
|
||||||
|
|
||||||
# Apply changes
|
# Apply changes
|
||||||
gitzone format --write
|
gitzone format --write
|
||||||
|
|
||||||
# Auto-approve without prompts
|
# Apply without prompt
|
||||||
gitzone format --yes --write
|
gitzone format --write --yes
|
||||||
|
|
||||||
# Show detailed diffs
|
# Apply deterministic fixes, then use opencode for remaining issues
|
||||||
gitzone format --diff
|
gitzone format fix
|
||||||
|
|
||||||
# Enable verbose logging
|
|
||||||
gitzone format --verbose
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Flags:**
|
Formatters include cleanup, smartconfig normalization, dependency license checks, package metadata normalization, template updates, `.gitignore`, TypeScript config, Prettier, README existence checks, and configured copy operations.
|
||||||
|
|
||||||
| Flag | Description |
|
`gitzone format fix` intentionally lives outside the default format path. Normal format runs stay deterministic; the fix command uses opencode only after deterministic formatters have done what they can.
|
||||||
| -------------------- | --------------------------------------------- |
|
|
||||||
| `--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):**
|
## Development Services
|
||||||
|
|
||||||
1. 🧹 **Cleanup** — removes obsolete files (yarn.lock, package-lock.json, tslint.json, etc.)
|
`gitzone services` manages local Docker-backed services for development projects.
|
||||||
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
|
|
||||||
6. 🙈 **Gitignore** — updates repository ignore rules
|
|
||||||
7. 🔧 **Tsconfig** — optimizes TypeScript configuration
|
|
||||||
8. ✨ **Prettier** — applies code formatting
|
|
||||||
9. 📖 **Readme** — ensures readme files exist
|
|
||||||
10. 📂 **Copy** — copies configured files
|
|
||||||
|
|
||||||
### 🐳 Development Services Management
|
Supported services:
|
||||||
|
|
||||||
Effortlessly manage local development services (MongoDB, MinIO S3, Elasticsearch) with Docker:
|
| Service | Aliases |
|
||||||
|
| --- | --- |
|
||||||
|
| MongoDB | `mongo`, `mongodb` |
|
||||||
|
| MinIO | `minio`, `s3` |
|
||||||
|
| Elasticsearch | `elasticsearch`, `es` |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gitzone services [command]
|
# Start configured services
|
||||||
```
|
|
||||||
|
|
||||||
**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 |
|
|
||||||
| `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:**
|
|
||||||
|
|
||||||
- `mongo` / `mongodb` — MongoDB
|
|
||||||
- `minio` / `s3` — MinIO (S3-compatible storage)
|
|
||||||
- `elasticsearch` / `es` — Elasticsearch
|
|
||||||
- `all` — All services (default)
|
|
||||||
|
|
||||||
**Key features:**
|
|
||||||
|
|
||||||
- 🎲 **Smart port assignment** — automatically assigns random ports (20000–30000) to avoid conflicts
|
|
||||||
- 📦 **Project isolation** — each project gets its own containers with unique names
|
|
||||||
- 💾 **Data persistence** — data stored in `.nogit/` survives container restarts
|
|
||||||
- 🔗 **MongoDB Compass support** — instantly get connection strings for GUI access
|
|
||||||
- 🌐 **Network IP detection** — detects your local network IP for remote connections
|
|
||||||
- ⚙️ **Auto-configuration** — creates `.nogit/env.json` with smart defaults
|
|
||||||
|
|
||||||
**Global operations (`-g` flag):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# List all registered projects
|
|
||||||
gitzone services list -g
|
|
||||||
|
|
||||||
# Show status across all projects
|
|
||||||
gitzone services status -g
|
|
||||||
|
|
||||||
# Stop all containers across all projects
|
|
||||||
gitzone services stop -g
|
|
||||||
|
|
||||||
# Remove stale registry entries
|
|
||||||
gitzone services cleanup -g
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example workflow:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start all services for your project
|
|
||||||
gitzone services start
|
gitzone services start
|
||||||
|
|
||||||
# Configure enabled services without prompts
|
# Enable specific services non-interactively
|
||||||
gitzone services set mongodb,minio
|
gitzone services set mongodb,minio
|
||||||
|
|
||||||
# Check what's running
|
# Check status
|
||||||
gitzone services status
|
gitzone services status
|
||||||
|
|
||||||
# Get MongoDB Compass connection string
|
# Print MongoDB Compass connection string
|
||||||
gitzone services compass
|
gitzone services compass
|
||||||
# Output: mongodb://defaultadmin:defaultpass@192.168.1.100:27018/myproject?authSource=admin
|
|
||||||
|
|
||||||
# View MongoDB logs
|
# Show logs
|
||||||
gitzone services logs mongo 50
|
gitzone services logs mongo 50
|
||||||
|
|
||||||
# Stop services when done
|
# Stop containers but keep data
|
||||||
gitzone services stop
|
gitzone services stop
|
||||||
|
|
||||||
|
# Remove containers and data
|
||||||
|
gitzone services clean
|
||||||
```
|
```
|
||||||
|
|
||||||
### ⚙️ Release & Commit Configuration
|
Service config is stored in `.nogit/env.json`. Data is stored below `.nogit/`, so it stays out of Git.
|
||||||
|
|
||||||
Manage release registries and commit settings:
|
## Templates
|
||||||
|
|
||||||
|
Start new projects with built-in scaffolds:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gitzone config [subcommand]
|
gitzone template npm
|
||||||
|
gitzone template service
|
||||||
|
gitzone template website
|
||||||
|
gitzone template wcc
|
||||||
```
|
```
|
||||||
|
|
||||||
| Command | Description |
|
Templates are rendered through SmartScaf and then can be normalized with `gitzone format`.
|
||||||
| ---------------------------------- | ---------------------------------------------------------- |
|
|
||||||
| `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 `.smartconfig.json` under the `@git.zone/cli` key.
|
## Meta Repositories
|
||||||
|
|
||||||
### 📦 Project Templates
|
Use `gitzone meta` when one workspace coordinates multiple repositories.
|
||||||
|
|
||||||
Instantly scaffold production-ready projects with best practices built-in:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gitzone template [template-name]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Interactive templates:**
|
|
||||||
|
|
||||||
- **`npm`** — TypeScript npm package with testing, CI/CD, and full tooling
|
|
||||||
- **`service`** — Microservice architecture with Docker support
|
|
||||||
- **`website`** — Modern web application with LitElement and service workers
|
|
||||||
- **`wcc`** — Web Component Collection for reusable UI components
|
|
||||||
|
|
||||||
Each template comes pre-configured with:
|
|
||||||
|
|
||||||
- ✅ TypeScript with modern configurations
|
|
||||||
- ✅ Automated testing setup with `@git.zone/tstest`
|
|
||||||
- ✅ CI/CD pipelines (GitLab/GitHub)
|
|
||||||
- ✅ Code formatting and linting
|
|
||||||
- ✅ Documentation structure
|
|
||||||
|
|
||||||
### 🏗️ Meta Repository Management
|
|
||||||
|
|
||||||
Manage multiple related repositories as a cohesive unit:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Initialize a meta repository
|
|
||||||
gitzone meta init
|
gitzone meta init
|
||||||
|
gitzone meta add frontend https://example.com/org/frontend.git
|
||||||
# Add a sub-project
|
|
||||||
gitzone meta add [name] [git-url]
|
|
||||||
|
|
||||||
# Update all sub-projects (clone missing, clean superfluous)
|
|
||||||
gitzone meta update
|
gitzone meta update
|
||||||
|
gitzone meta remove frontend
|
||||||
# Remove a sub-project
|
|
||||||
gitzone meta remove [name]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🐳 Docker Management
|
## Other Utilities
|
||||||
|
|
||||||
Streamline your Docker workflow:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clean up all Docker resources (containers, images, volumes, networks)
|
# Docker cleanup
|
||||||
gitzone docker prune
|
gitzone docker prune
|
||||||
```
|
|
||||||
|
|
||||||
### 🔗 Quick CI/CD Access
|
# Open GitLab CI settings or pipelines for the current repo
|
||||||
|
|
||||||
Jump directly to your CI/CD configurations:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Open CI/CD settings
|
|
||||||
gitzone open ci
|
gitzone open ci
|
||||||
|
|
||||||
# Open pipelines view
|
|
||||||
gitzone open pipelines
|
gitzone open pipelines
|
||||||
```
|
|
||||||
|
|
||||||
Works with GitLab repositories to provide instant access to your deployment configurations.
|
# Deprecate an old npm package interactively
|
||||||
|
|
||||||
### 📝 Package Deprecation
|
|
||||||
|
|
||||||
Smoothly transition users from old to new packages:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gitzone deprecate
|
gitzone deprecate
|
||||||
```
|
|
||||||
|
|
||||||
Interactive wizard that prompts for registry URLs, old package name, and new package name — then runs `npm deprecate` across all specified registries.
|
# Prepare a project for local work
|
||||||
|
|
||||||
### 🚦 Project Initialization
|
|
||||||
|
|
||||||
Prepare existing projects for development:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gitzone start
|
gitzone start
|
||||||
```
|
|
||||||
|
|
||||||
Automatically checks out master, pulls latest changes, and installs dependencies.
|
# Generate a short unique ID
|
||||||
|
|
||||||
### 🔧 Helper Utilities
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate a unique short ID
|
|
||||||
gitzone helpers shortid
|
gitzone helpers shortid
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📋 Configuration
|
## Troubleshooting
|
||||||
|
|
||||||
### .smartconfig.json
|
Format only previews changes:
|
||||||
|
|
||||||
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"],
|
|
||||||
"accessLevel": "public"
|
|
||||||
},
|
|
||||||
"commit": {
|
|
||||||
"alwaysTest": false,
|
|
||||||
"alwaysBuild": false
|
|
||||||
},
|
|
||||||
"format": {
|
|
||||||
"interactive": true,
|
|
||||||
"showStats": true,
|
|
||||||
"modules": {
|
|
||||||
"skip": ["prettier"],
|
|
||||||
"only": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
- `CI` — Detect CI environment for automated workflows
|
|
||||||
- `DEBUG` — Enable debug output
|
|
||||||
- `GITZONE_FORMAT_PARALLEL` — Control parallel formatting
|
|
||||||
|
|
||||||
## 🎯 Common Workflows
|
|
||||||
|
|
||||||
### Full-Stack Development Cycle
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Start fresh
|
|
||||||
gitzone start
|
|
||||||
|
|
||||||
# 2. Spin up databases and services
|
|
||||||
gitzone services start
|
|
||||||
|
|
||||||
# 3. Make changes
|
|
||||||
# ... your development work ...
|
|
||||||
|
|
||||||
# 4. Check service logs if needed
|
|
||||||
gitzone services logs mongo
|
|
||||||
|
|
||||||
# 5. Preview format changes, then apply
|
|
||||||
gitzone format
|
|
||||||
gitzone format --write
|
gitzone format --write
|
||||||
|
```
|
||||||
|
|
||||||
# 6. Commit with semantic versioning
|
Release says there is nothing to release:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make sure commits have populated the Pending changelog section
|
||||||
gitzone commit
|
gitzone commit
|
||||||
|
|
||||||
# 7. Stop services when done
|
|
||||||
gitzone services stop
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Automated CI/CD Commit
|
Docker services fail to start:
|
||||||
|
|
||||||
```bash
|
|
||||||
# Auto-accept, test, build, push, and release in one command
|
|
||||||
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
|
|
||||||
# 1. Set up meta repository
|
|
||||||
gitzone meta init
|
|
||||||
|
|
||||||
# 2. Add all related projects
|
|
||||||
gitzone meta add frontend https://github.com/org/frontend.git
|
|
||||||
gitzone meta add backend https://github.com/org/backend.git
|
|
||||||
gitzone meta add shared https://github.com/org/shared.git
|
|
||||||
|
|
||||||
# 3. Synchronize updates
|
|
||||||
gitzone meta update
|
|
||||||
```
|
|
||||||
|
|
||||||
### Safe Formatting with Plan Review
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Preview changes (default)
|
|
||||||
gitzone format
|
|
||||||
|
|
||||||
# 2. Save plan for review
|
|
||||||
gitzone format --save-plan format-changes.json
|
|
||||||
|
|
||||||
# 3. Apply from saved plan
|
|
||||||
gitzone format --from-plan format-changes.json --write
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database-Driven Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Start MongoDB, MinIO, and Elasticsearch
|
|
||||||
gitzone services start
|
|
||||||
|
|
||||||
# 2. Get connection details
|
|
||||||
gitzone services config
|
|
||||||
|
|
||||||
# 3. Connect with MongoDB Compass
|
|
||||||
gitzone services compass
|
|
||||||
|
|
||||||
# 4. Monitor services
|
|
||||||
gitzone services status
|
|
||||||
|
|
||||||
# 5. Clean everything when done
|
|
||||||
gitzone services clean # ⚠️ Warning: deletes data
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔌 Integrations
|
|
||||||
|
|
||||||
### CI/CD Platforms
|
|
||||||
|
|
||||||
- **GitLab CI** — full pipeline support with templates
|
|
||||||
- **GitHub Actions** — automated workflows
|
|
||||||
- **Docker** — container-based deployments
|
|
||||||
|
|
||||||
### Development Tools
|
|
||||||
|
|
||||||
- **TypeScript** — first-class support
|
|
||||||
- **Prettier** — code formatting
|
|
||||||
- **pnpm** — package management
|
|
||||||
- **MongoDB** — local database service
|
|
||||||
- **MinIO** — S3-compatible object storage
|
|
||||||
- **Elasticsearch** — search and analytics
|
|
||||||
- **MongoDB Compass** — database GUI integration
|
|
||||||
|
|
||||||
### Version Control
|
|
||||||
|
|
||||||
- **Git** — deep integration
|
|
||||||
- **Semantic Versioning** — automatic version bumping
|
|
||||||
- **Conventional Commits** — standardized commit messages
|
|
||||||
- **AI-Powered Analysis** — intelligent commit suggestions via `@git.zone/tsdoc`
|
|
||||||
|
|
||||||
## 💡 Pro Tips
|
|
||||||
|
|
||||||
1. **Use aliases**: Add `alias gz='gitzone'` to your shell profile
|
|
||||||
2. **Combine flags**: `gitzone commit -ypbr` for the full auto workflow
|
|
||||||
3. **Leverage templates**: Start projects right with proven structures
|
|
||||||
4. **Enable caching**: Dramatically speeds up formatting operations
|
|
||||||
5. **Save format plans**: Review changes before applying
|
|
||||||
6. **Port management**: Let services auto-assign ports to avoid conflicts
|
|
||||||
7. **Use MongoDB Compass**: `gitzone services compass` for visual DB management
|
|
||||||
8. **Global service management**: `gitzone services status -g` to see all projects' services at once
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### Format Command Shows "Cancelled"
|
|
||||||
|
|
||||||
- Check your `npmextra.json` configuration
|
|
||||||
- Try with `--yes --write` flags
|
|
||||||
- Use `--verbose` for detailed output
|
|
||||||
|
|
||||||
### Docker Commands Fail
|
|
||||||
|
|
||||||
Ensure Docker daemon is running:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker info
|
docker info
|
||||||
```
|
gitzone services status
|
||||||
|
|
||||||
### Services Won't Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Services auto-assign ports, but you can check the config
|
|
||||||
cat .nogit/env.json
|
|
||||||
|
|
||||||
# Verify Docker is running
|
|
||||||
docker ps
|
|
||||||
|
|
||||||
# Reassign ports if there are conflicts
|
|
||||||
gitzone services reconfigure
|
gitzone services reconfigure
|
||||||
```
|
```
|
||||||
|
|
||||||
### Template Creation Issues
|
Config looks outdated:
|
||||||
|
|
||||||
Verify pnpm/npm is properly configured:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm config get registry
|
gitzone config migrate 2
|
||||||
|
gitzone config show --json
|
||||||
```
|
```
|
||||||
|
|
||||||
### MongoDB Connection Issues
|
|
||||||
|
|
||||||
- Ensure services are running: `gitzone services status`
|
|
||||||
- Check firewall settings for the assigned ports
|
|
||||||
- Use `gitzone services compass` for the correct connection string
|
|
||||||
|
|
||||||
## 📈 Performance
|
|
||||||
|
|
||||||
gitzone is optimized for speed:
|
|
||||||
|
|
||||||
- ⚡ **Parallel processing** for format operations
|
|
||||||
- 🧠 **Smart caching** to avoid redundant work
|
|
||||||
- 📊 **Incremental updates** for meta repositories
|
|
||||||
- 🐳 **Isolated services** prevent resource conflicts
|
|
||||||
- 🎲 **Auto port assignment** eliminates manual configuration
|
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/cli',
|
name: '@git.zone/cli',
|
||||||
version: '2.14.1',
|
version: '2.19.1',
|
||||||
description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
|
description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
|
||||||
}
|
}
|
||||||
|
|||||||
+106
-120
@@ -1,151 +1,137 @@
|
|||||||
import * as plugins from "./plugins.js";
|
import * as plugins from "./plugins.js";
|
||||||
import * as paths from "./paths.js";
|
import * as paths from "./paths.js";
|
||||||
import { GitzoneConfig } from "./classes.gitzoneconfig.js";
|
import {
|
||||||
import { getRawCliMode } from "./helpers.climode.js";
|
getProcessUserArgv,
|
||||||
|
getRawCliMode,
|
||||||
|
parseCliArgv,
|
||||||
|
} from "./helpers.climode.js";
|
||||||
|
import { commitinfo } from "./00_commitinfo_data.js";
|
||||||
|
|
||||||
const gitzoneSmartcli = new plugins.smartcli.Smartcli();
|
const runParsedCommand = async (argvArg: any): Promise<void> => {
|
||||||
|
const command = argvArg._?.[0];
|
||||||
|
|
||||||
export let run = async () => {
|
switch (command) {
|
||||||
const done = plugins.smartpromise.defer();
|
case undefined:
|
||||||
const rawCliMode = await getRawCliMode();
|
case "help": {
|
||||||
|
|
||||||
// get packageInfo
|
|
||||||
const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
|
||||||
|
|
||||||
// check for updates
|
|
||||||
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 <======
|
|
||||||
|
|
||||||
/**
|
|
||||||
* standard task
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.standardCommand().subscribe(async (argvArg) => {
|
|
||||||
const modStandard = await import("./mod_standard/index.js");
|
const modStandard = await import("./mod_standard/index.js");
|
||||||
await modStandard.run(argvArg);
|
await modStandard.run(argvArg);
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
gitzoneSmartcli.addCommand("help").subscribe(async (argvArg) => {
|
case "commit": {
|
||||||
const modStandard = await import("./mod_standard/index.js");
|
|
||||||
await modStandard.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ======> Specific tasks <======
|
|
||||||
|
|
||||||
/**
|
|
||||||
* commit something
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("commit").subscribe(async (argvArg) => {
|
|
||||||
const modCommit = await import("./mod_commit/index.js");
|
const modCommit = await import("./mod_commit/index.js");
|
||||||
await modCommit.run(argvArg);
|
await modCommit.run(argvArg);
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
/**
|
case "release": {
|
||||||
* deprecate a package on npm
|
const modRelease = await import("./mod_release/index.js");
|
||||||
*/
|
await modRelease.run(argvArg);
|
||||||
gitzoneSmartcli.addCommand("deprecate").subscribe(async (argvArg) => {
|
break;
|
||||||
|
}
|
||||||
|
case "deprecate": {
|
||||||
const modDeprecate = await import("./mod_deprecate/index.js");
|
const modDeprecate = await import("./mod_deprecate/index.js");
|
||||||
await modDeprecate.run();
|
await modDeprecate.run();
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
/**
|
case "docker": {
|
||||||
* docker
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("docker").subscribe(async (argvArg) => {
|
|
||||||
const modDocker = await import("./mod_docker/index.js");
|
const modDocker = await import("./mod_docker/index.js");
|
||||||
await modDocker.run(argvArg);
|
await modDocker.run(argvArg);
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
/**
|
case "format": {
|
||||||
* Update all files that comply with the gitzone standard
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("format").subscribe(async (argvArg) => {
|
|
||||||
const config = GitzoneConfig.fromCwd();
|
|
||||||
const modFormat = await import("./mod_format/index.js");
|
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({
|
await modFormat.run({
|
||||||
...argvArg,
|
...argvArg,
|
||||||
write: argvArg.write || argvArg.w,
|
write: argvArg.write || argvArg.w,
|
||||||
dryRun: argvArg["dry-run"],
|
dryRun: argvArg["dry-run"],
|
||||||
yes: argvArg.yes,
|
yes: argvArg.yes || argvArg.y,
|
||||||
planOnly: argvArg["plan-only"],
|
planOnly: argvArg["plan-only"] || argvArg.planOnly,
|
||||||
savePlan: argvArg["save-plan"],
|
savePlan: argvArg["save-plan"] || argvArg.savePlan,
|
||||||
fromPlan: argvArg["from-plan"],
|
fromPlan: argvArg["from-plan"] || argvArg.fromPlan,
|
||||||
detailed: argvArg.detailed,
|
detailed: argvArg.detailed,
|
||||||
interactive: argvArg.interactive !== false,
|
interactive: argvArg.interactive !== false,
|
||||||
verbose: argvArg.verbose,
|
verbose: argvArg.verbose,
|
||||||
diff: argvArg.diff,
|
diff: argvArg.diff,
|
||||||
});
|
});
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
/**
|
case "meta": {
|
||||||
* run meta commands
|
|
||||||
*/
|
|
||||||
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);
|
await modMeta.run(argvArg);
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
/**
|
case "open": {
|
||||||
* open assets
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("open").subscribe(async (argvArg) => {
|
|
||||||
const modOpen = await import("./mod_open/index.js");
|
const modOpen = await import("./mod_open/index.js");
|
||||||
modOpen.run(argvArg);
|
await modOpen.run(argvArg);
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
/**
|
case "template": {
|
||||||
* add a readme to a project
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("template").subscribe(async (argvArg) => {
|
|
||||||
const modTemplate = await import("./mod_template/index.js");
|
const modTemplate = await import("./mod_template/index.js");
|
||||||
modTemplate.run(argvArg);
|
await modTemplate.run(argvArg);
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
/**
|
case "start": {
|
||||||
* start working on a project
|
const modStart = await import("./mod_start/index.js");
|
||||||
*/
|
await modStart.run(argvArg);
|
||||||
gitzoneSmartcli.addCommand("start").subscribe(async (argvArg) => {
|
break;
|
||||||
const modTemplate = await import("./mod_start/index.js");
|
}
|
||||||
modTemplate.run(argvArg);
|
case "helpers": {
|
||||||
});
|
|
||||||
|
|
||||||
gitzoneSmartcli.addCommand("helpers").subscribe(async (argvArg) => {
|
|
||||||
const modHelpers = await import("./mod_helpers/index.js");
|
const modHelpers = await import("./mod_helpers/index.js");
|
||||||
modHelpers.run(argvArg);
|
await modHelpers.run(argvArg);
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
/**
|
case "tools": {
|
||||||
* manage release configuration
|
const modTools = await import("./mod_tools/index.js");
|
||||||
*/
|
await modTools.run(argvArg);
|
||||||
gitzoneSmartcli.addCommand("config").subscribe(async (argvArg) => {
|
break;
|
||||||
|
}
|
||||||
|
case "config": {
|
||||||
const modConfig = await import("./mod_config/index.js");
|
const modConfig = await import("./mod_config/index.js");
|
||||||
await modConfig.run(argvArg);
|
await modConfig.run(argvArg);
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
/**
|
case "services": {
|
||||||
* manage development services (MongoDB, S3/MinIO)
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("services").subscribe(async (argvArg) => {
|
|
||||||
const modServices = await import("./mod_services/index.js");
|
const modServices = await import("./mod_services/index.js");
|
||||||
await modServices.run(argvArg);
|
await modServices.run(argvArg);
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
// start parsing of the cli
|
default: {
|
||||||
gitzoneSmartcli.startParse();
|
const modStandard = await import("./mod_standard/index.js");
|
||||||
return await done.promise;
|
await modStandard.run(argvArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export let run = async () => {
|
||||||
|
const rawCliMode = await getRawCliMode();
|
||||||
|
|
||||||
|
// get packageInfo
|
||||||
|
const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||||
|
const projectInfoVersion = (projectInfo.npm as any)?.version;
|
||||||
|
const packageVersion =
|
||||||
|
typeof projectInfoVersion === "string" && projectInfoVersion.length > 0
|
||||||
|
? projectInfoVersion
|
||||||
|
: commitinfo.version;
|
||||||
|
|
||||||
|
// check for updates
|
||||||
|
if (rawCliMode.checkUpdates) {
|
||||||
|
const smartupdateInstance = new plugins.smartupdate.SmartUpdate();
|
||||||
|
try {
|
||||||
|
await smartupdateInstance.check(
|
||||||
|
"gitzone",
|
||||||
|
packageVersion,
|
||||||
|
"http://gitzone.gitlab.io/gitzone/changelog.html",
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Update checks must never block actual CLI commands.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rawCliMode.output === "human") {
|
||||||
|
console.log("---------------------------------------------");
|
||||||
|
}
|
||||||
|
const argvArg = parseCliArgv(getProcessUserArgv());
|
||||||
|
if (argvArg.v || argvArg.version) {
|
||||||
|
console.log(packageVersion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await runParsedCommand(argvArg);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import * as plugins from "./plugins.js";
|
||||||
|
|
||||||
|
export type TChangelogBucket =
|
||||||
|
| "Breaking Changes"
|
||||||
|
| "Features"
|
||||||
|
| "Fixes"
|
||||||
|
| "Documentation"
|
||||||
|
| "Maintenance";
|
||||||
|
|
||||||
|
export interface IChangelogEntry {
|
||||||
|
type: string;
|
||||||
|
scope: string;
|
||||||
|
message: string;
|
||||||
|
details?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPendingChangelog {
|
||||||
|
block: string;
|
||||||
|
isEmpty: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucketForCommitType = (commitType: string): TChangelogBucket => {
|
||||||
|
switch (commitType) {
|
||||||
|
case "BREAKING CHANGE":
|
||||||
|
return "Breaking Changes";
|
||||||
|
case "feat":
|
||||||
|
return "Features";
|
||||||
|
case "fix":
|
||||||
|
return "Fixes";
|
||||||
|
case "docs":
|
||||||
|
return "Documentation";
|
||||||
|
default:
|
||||||
|
return "Maintenance";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const readChangelog = async (filePath: string): Promise<string> => {
|
||||||
|
if (!(await plugins.smartfs.file(filePath).exists())) {
|
||||||
|
return "# Changelog\n\n";
|
||||||
|
}
|
||||||
|
return (await plugins.smartfs.file(filePath).encoding("utf8").read()) as string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeChangelog = async (filePath: string, content: string): Promise<void> => {
|
||||||
|
await plugins.smartfs.file(filePath).encoding("utf8").write(content.endsWith("\n") ? content : `${content}\n`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findPendingSection = (
|
||||||
|
content: string,
|
||||||
|
sectionName: string,
|
||||||
|
): { start: number; bodyStart: number; end: number } | null => {
|
||||||
|
const headingRegex = new RegExp(`^##\\s+${sectionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
|
||||||
|
const match = headingRegex.exec(content);
|
||||||
|
if (!match || match.index === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyStart = match.index + match[0].length;
|
||||||
|
const rest = content.slice(bodyStart);
|
||||||
|
const nextHeadingMatch = /^##\s+/m.exec(rest);
|
||||||
|
const end = nextHeadingMatch ? bodyStart + nextHeadingMatch.index : content.length;
|
||||||
|
return { start: match.index, bodyStart, end };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ensurePendingSection = async (
|
||||||
|
filePath: string,
|
||||||
|
sectionName = "Pending",
|
||||||
|
): Promise<string> => {
|
||||||
|
let content = await readChangelog(filePath);
|
||||||
|
if (findPendingSection(content, sectionName)) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingSection = `## ${sectionName}\n\n`;
|
||||||
|
const titleMatch = /^#\s+.+$/m.exec(content);
|
||||||
|
if (titleMatch && titleMatch.index !== undefined) {
|
||||||
|
const insertAt = titleMatch.index + titleMatch[0].length;
|
||||||
|
content = `${content.slice(0, insertAt)}\n\n${pendingSection}${content.slice(insertAt).replace(/^\n+/, "")}`;
|
||||||
|
} else {
|
||||||
|
content = `# Changelog\n\n${pendingSection}${content}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeChangelog(filePath, content);
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appendPendingChangelogEntry = async (
|
||||||
|
filePath: string,
|
||||||
|
sectionName: string,
|
||||||
|
entry: IChangelogEntry,
|
||||||
|
): Promise<void> => {
|
||||||
|
let content = await ensurePendingSection(filePath, sectionName);
|
||||||
|
const pendingSection = findPendingSection(content, sectionName)!;
|
||||||
|
let pendingBody = content.slice(pendingSection.bodyStart, pendingSection.end);
|
||||||
|
const bucket = bucketForCommitType(entry.type);
|
||||||
|
const bucketHeading = `### ${bucket}`;
|
||||||
|
|
||||||
|
const entryLines = [`- ${entry.message}${entry.scope ? ` (${entry.scope})` : ""}`];
|
||||||
|
for (const detail of entry.details || []) {
|
||||||
|
entryLines.push(` - ${detail}`);
|
||||||
|
}
|
||||||
|
const renderedEntry = entryLines.join("\n");
|
||||||
|
|
||||||
|
const bucketRegex = new RegExp(`^###\\s+${bucket.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
|
||||||
|
const bucketMatch = bucketRegex.exec(pendingBody);
|
||||||
|
if (!bucketMatch || bucketMatch.index === undefined) {
|
||||||
|
pendingBody = `${pendingBody.trimEnd()}\n\n${bucketHeading}\n\n${renderedEntry}\n`;
|
||||||
|
} else {
|
||||||
|
const bucketBodyStart = bucketMatch.index + bucketMatch[0].length;
|
||||||
|
const afterBucket = pendingBody.slice(bucketBodyStart);
|
||||||
|
const nextBucketMatch = /^###\s+/m.exec(afterBucket);
|
||||||
|
const insertAt = nextBucketMatch ? bucketBodyStart + nextBucketMatch.index : pendingBody.length;
|
||||||
|
const beforeInsert = pendingBody.slice(0, insertAt).trimEnd();
|
||||||
|
const afterInsert = pendingBody.slice(insertAt).replace(/^\n+/, "");
|
||||||
|
pendingBody = `${beforeInsert}\n${renderedEntry}\n\n${afterInsert}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = `${content.slice(0, pendingSection.bodyStart)}\n${pendingBody.trim()}\n\n${content.slice(pendingSection.end).replace(/^\n+/, "")}`;
|
||||||
|
await writeChangelog(filePath, content);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readPendingChangelog = async (
|
||||||
|
filePath: string,
|
||||||
|
sectionName = "Pending",
|
||||||
|
): Promise<IPendingChangelog> => {
|
||||||
|
const content = await ensurePendingSection(filePath, sectionName);
|
||||||
|
const pendingSection = findPendingSection(content, sectionName)!;
|
||||||
|
const block = content.slice(pendingSection.bodyStart, pendingSection.end).trim();
|
||||||
|
return {
|
||||||
|
block,
|
||||||
|
isEmpty: block.length === 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inferVersionTypeFromPending = (pendingBlock: string): "patch" | "minor" | "major" => {
|
||||||
|
if (/^###\s+Breaking Changes\s*$/m.test(pendingBlock)) {
|
||||||
|
return "major";
|
||||||
|
}
|
||||||
|
if (/^###\s+Features\s*$/m.test(pendingBlock)) {
|
||||||
|
return "minor";
|
||||||
|
}
|
||||||
|
return "patch";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const movePendingToVersion = async (
|
||||||
|
filePath: string,
|
||||||
|
sectionName: string,
|
||||||
|
versionHeading: string,
|
||||||
|
version: string,
|
||||||
|
dateString: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
let content = await ensurePendingSection(filePath, sectionName);
|
||||||
|
const pendingSection = findPendingSection(content, sectionName)!;
|
||||||
|
const pendingBlock = content.slice(pendingSection.bodyStart, pendingSection.end).trim();
|
||||||
|
if (!pendingBlock) {
|
||||||
|
throw new Error("No pending changelog entries. Nothing to release.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedHeading = versionHeading
|
||||||
|
.replaceAll("{{version}}", version)
|
||||||
|
.replaceAll("{{date}}", dateString);
|
||||||
|
const nextContent = content.slice(pendingSection.end).replace(/^\n+/, "");
|
||||||
|
content = `${content.slice(0, pendingSection.bodyStart)}\n\n${renderedHeading}\n\n${pendingBlock}\n\n${nextContent}`;
|
||||||
|
await writeChangelog(filePath, content);
|
||||||
|
};
|
||||||
+36
-1
@@ -88,6 +88,41 @@ const parseRawArgv = (argv: string[]): TArgSource => {
|
|||||||
return parsedArgv;
|
return parsedArgv;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const parseCliArgv = parseRawArgv;
|
||||||
|
|
||||||
|
export const getProcessUserArgv = (): string[] => {
|
||||||
|
const rawArgv = process.argv;
|
||||||
|
const argv0Base = (rawArgv[0] || "").split(/[\\/]/).pop()?.toLowerCase();
|
||||||
|
const runtimeNames = new Set([
|
||||||
|
"node",
|
||||||
|
"node.exe",
|
||||||
|
"nodejs",
|
||||||
|
"nodejs.exe",
|
||||||
|
"bun",
|
||||||
|
"bun.exe",
|
||||||
|
"deno",
|
||||||
|
"deno.exe",
|
||||||
|
"tsx",
|
||||||
|
"tsx.exe",
|
||||||
|
"ts-node",
|
||||||
|
"ts-node.exe",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!runtimeNames.has(argv0Base || "")) {
|
||||||
|
return rawArgv.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstUserArg = rawArgv[1] || "";
|
||||||
|
const firstUserArgLooksLikeScript =
|
||||||
|
firstUserArg.includes("/") ||
|
||||||
|
firstUserArg.endsWith(".js") ||
|
||||||
|
firstUserArg.endsWith(".ts") ||
|
||||||
|
firstUserArg.endsWith(".mjs") ||
|
||||||
|
firstUserArg.endsWith(".cjs");
|
||||||
|
|
||||||
|
return rawArgv.slice(firstUserArgLooksLikeScript ? 2 : 1);
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeOutputMode = (value: unknown): TCliOutputMode | undefined => {
|
const normalizeOutputMode = (value: unknown): TCliOutputMode | undefined => {
|
||||||
if (value === "human" || value === "plain" || value === "json") {
|
if (value === "human" || value === "plain" || value === "json") {
|
||||||
return value;
|
return value;
|
||||||
@@ -171,7 +206,7 @@ export const getCliMode = async (
|
|||||||
|
|
||||||
export const getRawCliMode = async (): Promise<ICliMode> => {
|
export const getRawCliMode = async (): Promise<ICliMode> => {
|
||||||
const cliConfig = await getCliModeConfig();
|
const cliConfig = await getCliModeConfig();
|
||||||
const rawArgv = parseRawArgv(process.argv.slice(2));
|
const rawArgv = parseRawArgv(getProcessUserArgv());
|
||||||
return resolveCliMode(rawArgv, cliConfig);
|
return resolveCliMode(rawArgv, cliConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
export const CURRENT_GITZONE_CLI_SCHEMA_VERSION = 2;
|
||||||
|
|
||||||
|
export interface ISmartconfigMigrationResult {
|
||||||
|
migrated: boolean;
|
||||||
|
fromVersion: number;
|
||||||
|
toVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLI_NAMESPACE = "@git.zone/cli";
|
||||||
|
|
||||||
|
const isPlainObject = (value: unknown): value is Record<string, any> => {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureObject = (parent: Record<string, any>, key: string): Record<string, any> => {
|
||||||
|
if (!isPlainObject(parent[key])) {
|
||||||
|
parent[key] = {};
|
||||||
|
}
|
||||||
|
return parent[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeRegistryList = (registries: unknown[]): string[] => {
|
||||||
|
const result: string[] = [];
|
||||||
|
for (const registry of registries) {
|
||||||
|
if (typeof registry !== "string" || !registry.trim()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const normalizedRegistry = normalizeRegistryUrl(registry);
|
||||||
|
if (!result.includes(normalizedRegistry)) {
|
||||||
|
result.push(normalizedRegistry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrateLegacyReleaseArray = (smartconfigJson: Record<string, any>): boolean => {
|
||||||
|
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
|
||||||
|
if (!Array.isArray(cliConfig.release)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registries = normalizeRegistryList(cliConfig.release);
|
||||||
|
cliConfig.release = {
|
||||||
|
targets: {
|
||||||
|
npm: {
|
||||||
|
enabled: registries.length > 0,
|
||||||
|
registries,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrateNamespaceKeys = (smartconfigJson: Record<string, any>): boolean => {
|
||||||
|
let migrated = false;
|
||||||
|
const migrations = [
|
||||||
|
{ oldKey: "gitzone", newKey: CLI_NAMESPACE },
|
||||||
|
{ oldKey: "tsdoc", newKey: "@git.zone/tsdoc" },
|
||||||
|
{ oldKey: "npmdocker", newKey: "@git.zone/tsdocker" },
|
||||||
|
{ oldKey: "npmci", newKey: "@ship.zone/szci" },
|
||||||
|
{ oldKey: "szci", newKey: "@ship.zone/szci" },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { oldKey, newKey } of migrations) {
|
||||||
|
if (!isPlainObject(smartconfigJson[oldKey])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isPlainObject(smartconfigJson[newKey])) {
|
||||||
|
smartconfigJson[newKey] = smartconfigJson[oldKey];
|
||||||
|
} else {
|
||||||
|
smartconfigJson[newKey] = {
|
||||||
|
...smartconfigJson[oldKey],
|
||||||
|
...smartconfigJson[newKey],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
delete smartconfigJson[oldKey];
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrated;
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrateToV2 = (smartconfigJson: Record<string, any>): boolean => {
|
||||||
|
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
|
||||||
|
let migrated = migrateLegacyReleaseArray(smartconfigJson);
|
||||||
|
const releaseConfig = ensureObject(cliConfig, "release");
|
||||||
|
|
||||||
|
const targets = ensureObject(releaseConfig, "targets");
|
||||||
|
const shipzoneConfig = smartconfigJson["@ship.zone/szci"];
|
||||||
|
|
||||||
|
if (isPlainObject(releaseConfig.git) && !isPlainObject(targets.git)) {
|
||||||
|
targets.git = releaseConfig.git;
|
||||||
|
delete releaseConfig.git;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(releaseConfig.npm) && !isPlainObject(targets.npm)) {
|
||||||
|
targets.npm = releaseConfig.npm;
|
||||||
|
delete releaseConfig.npm;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(releaseConfig.docker)) {
|
||||||
|
targets.docker = isPlainObject(targets.docker)
|
||||||
|
? { ...releaseConfig.docker, ...targets.docker }
|
||||||
|
: releaseConfig.docker;
|
||||||
|
delete releaseConfig.docker;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(releaseConfig.registries)) {
|
||||||
|
const npmTarget = ensureObject(targets, "npm");
|
||||||
|
if (!Array.isArray(npmTarget.registries)) {
|
||||||
|
npmTarget.registries = releaseConfig.registries;
|
||||||
|
}
|
||||||
|
delete releaseConfig.registries;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releaseConfig.accessLevel) {
|
||||||
|
const npmTarget = ensureObject(targets, "npm");
|
||||||
|
if (!npmTarget.accessLevel) {
|
||||||
|
npmTarget.accessLevel = releaseConfig.accessLevel;
|
||||||
|
}
|
||||||
|
delete releaseConfig.accessLevel;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(shipzoneConfig)) {
|
||||||
|
if (shipzoneConfig.npmAccessLevel) {
|
||||||
|
const npmTarget = ensureObject(targets, "npm");
|
||||||
|
if (!npmTarget.accessLevel) {
|
||||||
|
npmTarget.accessLevel = shipzoneConfig.npmAccessLevel;
|
||||||
|
}
|
||||||
|
delete shipzoneConfig.npmAccessLevel;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shipzoneConfig.npmRegistryUrl) {
|
||||||
|
const npmTarget = ensureObject(targets, "npm");
|
||||||
|
const registry = normalizeRegistryUrl(shipzoneConfig.npmRegistryUrl);
|
||||||
|
const registries = Array.isArray(npmTarget.registries) ? npmTarget.registries : [];
|
||||||
|
if (!registries.includes(registry)) {
|
||||||
|
registries.push(registry);
|
||||||
|
}
|
||||||
|
npmTarget.registries = registries;
|
||||||
|
delete shipzoneConfig.npmRegistryUrl;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(releaseConfig.steps)) {
|
||||||
|
const steps = releaseConfig.steps as string[];
|
||||||
|
const preflight = ensureObject(releaseConfig, "preflight");
|
||||||
|
if (steps.includes("test") && preflight.test === undefined) {
|
||||||
|
preflight.test = true;
|
||||||
|
}
|
||||||
|
if (steps.includes("build") && preflight.build === undefined) {
|
||||||
|
preflight.build = true;
|
||||||
|
}
|
||||||
|
if (steps.includes("push")) {
|
||||||
|
const gitTarget = ensureObject(targets, "git");
|
||||||
|
if (gitTarget.enabled === undefined) {
|
||||||
|
gitTarget.enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (steps.includes("publishNpm")) {
|
||||||
|
const npmTarget = ensureObject(targets, "npm");
|
||||||
|
if (npmTarget.enabled === undefined) {
|
||||||
|
npmTarget.enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (steps.includes("publishDocker")) {
|
||||||
|
const dockerTarget = ensureObject(targets, "docker");
|
||||||
|
if (dockerTarget.enabled === undefined) {
|
||||||
|
dockerTarget.enabled = true;
|
||||||
|
}
|
||||||
|
dockerTarget.engine = "tsdocker";
|
||||||
|
}
|
||||||
|
delete releaseConfig.steps;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(targets.docker)) {
|
||||||
|
if (targets.docker.images) {
|
||||||
|
delete targets.docker.images;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
if (targets.docker.engine !== "tsdocker") {
|
||||||
|
targets.docker.engine = "tsdocker";
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(targets.docker.patterns)) {
|
||||||
|
targets.docker.patterns = [];
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releaseConfig.changelog) {
|
||||||
|
delete releaseConfig.changelog;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
cliConfig.schemaVersion = 2;
|
||||||
|
return migrated || true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeRegistryUrl = (url: string): string => {
|
||||||
|
let normalizedUrl = url.trim();
|
||||||
|
if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
|
||||||
|
normalizedUrl = `https://${normalizedUrl}`;
|
||||||
|
}
|
||||||
|
return normalizedUrl.endsWith("/") ? normalizedUrl.slice(0, -1) : normalizedUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const migrateSmartconfigData = (
|
||||||
|
smartconfigJson: Record<string, any>,
|
||||||
|
targetVersion = CURRENT_GITZONE_CLI_SCHEMA_VERSION,
|
||||||
|
): ISmartconfigMigrationResult => {
|
||||||
|
let migrated = false;
|
||||||
|
migrated = migrateNamespaceKeys(smartconfigJson) || migrated;
|
||||||
|
|
||||||
|
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
|
||||||
|
const fromVersion = typeof cliConfig.schemaVersion === "number" ? cliConfig.schemaVersion : 1;
|
||||||
|
let currentVersion = fromVersion;
|
||||||
|
|
||||||
|
if (targetVersion >= 2) {
|
||||||
|
migrated = migrateLegacyReleaseArray(smartconfigJson) || migrated;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentVersion < 2 && targetVersion >= 2) {
|
||||||
|
migrated = migrateToV2(smartconfigJson) || migrated;
|
||||||
|
currentVersion = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetVersion === CURRENT_GITZONE_CLI_SCHEMA_VERSION && cliConfig.schemaVersion !== targetVersion) {
|
||||||
|
cliConfig.schemaVersion = targetVersion;
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
migrated,
|
||||||
|
fromVersion,
|
||||||
|
toVersion: Math.min(targetVersion, CURRENT_GITZONE_CLI_SCHEMA_VERSION),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
import { getCliConfigValue } from "./helpers.smartconfig.js";
|
||||||
|
|
||||||
|
export type TConfirmationMode = "prompt" | "auto" | "plan";
|
||||||
|
|
||||||
|
export type TCommitStep =
|
||||||
|
| "format"
|
||||||
|
| "analyze"
|
||||||
|
| "test"
|
||||||
|
| "build"
|
||||||
|
| "changelog"
|
||||||
|
| "commit"
|
||||||
|
| "push";
|
||||||
|
|
||||||
|
export type TReleaseTarget = "git" | "npm" | "docker";
|
||||||
|
|
||||||
|
export interface ICommitWorkflowConfig {
|
||||||
|
confirmation?: TConfirmationMode;
|
||||||
|
staging?: "all";
|
||||||
|
steps?: TCommitStep[];
|
||||||
|
alwaysTest?: boolean;
|
||||||
|
alwaysBuild?: boolean;
|
||||||
|
analyze?: {
|
||||||
|
provider?: "ai";
|
||||||
|
requireConfirmationFor?: string[];
|
||||||
|
};
|
||||||
|
test?: {
|
||||||
|
command?: string;
|
||||||
|
};
|
||||||
|
build?: {
|
||||||
|
command?: string;
|
||||||
|
verifyCleanTree?: boolean;
|
||||||
|
};
|
||||||
|
push?: {
|
||||||
|
remote?: string;
|
||||||
|
followTags?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReleaseGitTargetConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
remote?: string;
|
||||||
|
pushBranch?: boolean;
|
||||||
|
pushTags?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReleaseNpmTargetConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
registries?: string[];
|
||||||
|
accessLevel?: "public" | "private";
|
||||||
|
alreadyPublished?: "success" | "error";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReleaseDockerTargetConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
engine?: "tsdocker";
|
||||||
|
patterns?: string[];
|
||||||
|
cached?: boolean;
|
||||||
|
parallel?: boolean | number;
|
||||||
|
context?: string;
|
||||||
|
noBuild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReleaseWorkflowConfig {
|
||||||
|
confirmation?: TConfirmationMode;
|
||||||
|
version?: {
|
||||||
|
strategy?: "semver";
|
||||||
|
source?: "pendingChangelog" | "manual";
|
||||||
|
};
|
||||||
|
preflight?: {
|
||||||
|
requireCleanTree?: boolean;
|
||||||
|
test?: boolean;
|
||||||
|
build?: boolean;
|
||||||
|
testCommand?: string;
|
||||||
|
buildCommand?: string;
|
||||||
|
};
|
||||||
|
targets?: {
|
||||||
|
git?: IReleaseGitTargetConfig;
|
||||||
|
npm?: IReleaseNpmTargetConfig;
|
||||||
|
docker?: IReleaseDockerTargetConfig;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResolvedCommitWorkflow {
|
||||||
|
confirmation: TConfirmationMode;
|
||||||
|
steps: TCommitStep[];
|
||||||
|
staging: "all";
|
||||||
|
testCommand: string;
|
||||||
|
buildCommand: string;
|
||||||
|
changelogFile: "changelog.md";
|
||||||
|
changelogSection: "Pending";
|
||||||
|
pushRemote: string;
|
||||||
|
pushFollowTags: boolean;
|
||||||
|
releaseFlagRequested: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResolvedReleaseWorkflow {
|
||||||
|
confirmation: TConfirmationMode;
|
||||||
|
plan: string[];
|
||||||
|
targets: TReleaseTarget[];
|
||||||
|
requireCleanTree: boolean;
|
||||||
|
runTests: boolean;
|
||||||
|
runBuild: boolean;
|
||||||
|
testCommand: string;
|
||||||
|
buildCommand: string;
|
||||||
|
changelogFile: "changelog.md";
|
||||||
|
changelogPendingSection: "Pending";
|
||||||
|
changelogVersionHeading: "## {{date}} - {{version}}";
|
||||||
|
gitEnabled: boolean;
|
||||||
|
gitRemote: string;
|
||||||
|
pushBranch: boolean;
|
||||||
|
pushTags: boolean;
|
||||||
|
npmEnabled: boolean;
|
||||||
|
npmRegistries: string[];
|
||||||
|
npmAccessLevel: "public" | "private";
|
||||||
|
npmAlreadyPublished: "success" | "error";
|
||||||
|
dockerEnabled: boolean;
|
||||||
|
dockerEngine: "tsdocker";
|
||||||
|
dockerPatterns: string[];
|
||||||
|
dockerCached: boolean;
|
||||||
|
dockerParallel: boolean | number;
|
||||||
|
dockerContext?: string;
|
||||||
|
dockerNoBuild: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICliWorkflowConfig {
|
||||||
|
commit?: ICommitWorkflowConfig;
|
||||||
|
release?: IReleaseWorkflowConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitFlagToStep: Record<string, TCommitStep | undefined> = {
|
||||||
|
f: "format",
|
||||||
|
t: "test",
|
||||||
|
b: "build",
|
||||||
|
p: "push",
|
||||||
|
};
|
||||||
|
|
||||||
|
const unique = <T>(items: T[]): T[] => {
|
||||||
|
const result: T[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
if (!result.includes(item)) {
|
||||||
|
result.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeConfirmation = (
|
||||||
|
value: unknown,
|
||||||
|
fallback: TConfirmationMode,
|
||||||
|
): TConfirmationMode => {
|
||||||
|
if (value === "prompt" || value === "auto" || value === "plan") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeRegistryUrl = (url: string): string => {
|
||||||
|
let normalizedUrl = url.trim();
|
||||||
|
if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
|
||||||
|
normalizedUrl = `https://${normalizedUrl}`;
|
||||||
|
}
|
||||||
|
return normalizedUrl.endsWith("/") ? normalizedUrl.slice(0, -1) : normalizedUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDisabled = (argvArg: any, ...keys: string[]): boolean => {
|
||||||
|
return keys.some((key) => argvArg[key] === false || argvArg[`no-${key}`] || argvArg[`no${key[0].toUpperCase()}${key.slice(1)}`]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const readCliWorkflowConfig = async (): Promise<ICliWorkflowConfig> => {
|
||||||
|
return await getCliConfigValue<ICliWorkflowConfig>("", {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrderedArgsAfterCommand = (commandName: string): string[] => {
|
||||||
|
const rawArgs = process.argv.slice(2);
|
||||||
|
const commandIndex = rawArgs.indexOf(commandName);
|
||||||
|
if (commandIndex === -1) {
|
||||||
|
return rawArgs;
|
||||||
|
}
|
||||||
|
return rawArgs.slice(commandIndex + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrderedShortFlags = (commandName: string): string[] => {
|
||||||
|
const orderedFlags: string[] = [];
|
||||||
|
for (const arg of getOrderedArgsAfterCommand(commandName)) {
|
||||||
|
if (arg === "--") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (arg.startsWith("--")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg.startsWith("-") && arg.length > 1) {
|
||||||
|
orderedFlags.push(...arg.slice(1).split(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return orderedFlags;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasExplicitCommitWorkflowFlags = (argvArg: any): boolean => {
|
||||||
|
return Boolean(
|
||||||
|
argvArg.f ||
|
||||||
|
argvArg.format ||
|
||||||
|
argvArg.t ||
|
||||||
|
argvArg.test ||
|
||||||
|
argvArg.b ||
|
||||||
|
argvArg.build ||
|
||||||
|
argvArg.p ||
|
||||||
|
argvArg.push,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeCommitSteps = (rawSteps: TCommitStep[]): TCommitStep[] => {
|
||||||
|
const steps = unique(rawSteps.filter(Boolean));
|
||||||
|
const pushRequested = steps.includes("push");
|
||||||
|
const prePushSteps = steps.filter((step) => step !== "push");
|
||||||
|
|
||||||
|
if (!prePushSteps.includes("analyze")) {
|
||||||
|
prePushSteps.unshift("analyze");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prePushSteps.includes("changelog")) {
|
||||||
|
const commitIndex = prePushSteps.indexOf("commit");
|
||||||
|
if (commitIndex === -1) {
|
||||||
|
prePushSteps.push("changelog");
|
||||||
|
} else {
|
||||||
|
prePushSteps.splice(commitIndex, 0, "changelog");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prePushSteps.includes("commit")) {
|
||||||
|
prePushSteps.push("commit");
|
||||||
|
}
|
||||||
|
|
||||||
|
const analyzeIndex = prePushSteps.indexOf("analyze");
|
||||||
|
const commitIndex = prePushSteps.indexOf("commit");
|
||||||
|
if (analyzeIndex > commitIndex) {
|
||||||
|
throw new Error("Commit workflow requires analyze before commit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const changelogIndex = prePushSteps.indexOf("changelog");
|
||||||
|
if (changelogIndex === -1 || changelogIndex > commitIndex) {
|
||||||
|
throw new Error("Commit workflow requires changelog before commit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pushRequested ? [...prePushSteps, "push"] : prePushSteps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTargetOverride = (argvArg: any): TReleaseTarget[] | undefined => {
|
||||||
|
const validTargets: TReleaseTarget[] = ["git", "npm", "docker"];
|
||||||
|
const rawTargets = argvArg.target || argvArg.targets;
|
||||||
|
if (typeof rawTargets === "string") {
|
||||||
|
return rawTargets
|
||||||
|
.split(",")
|
||||||
|
.map((target) => target.trim())
|
||||||
|
.filter((target): target is TReleaseTarget => validTargets.includes(target as TReleaseTarget));
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets: TReleaseTarget[] = [];
|
||||||
|
if (argvArg.git || argvArg.p || argvArg.push) targets.push("git");
|
||||||
|
if (argvArg.npm) targets.push("npm");
|
||||||
|
if (argvArg.docker) targets.push("docker");
|
||||||
|
return targets.length > 0 ? targets : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildReleasePlan = (options: {
|
||||||
|
requireCleanTree: boolean;
|
||||||
|
runTests: boolean;
|
||||||
|
runBuild: boolean;
|
||||||
|
targets: TReleaseTarget[];
|
||||||
|
}): string[] => {
|
||||||
|
const plan: string[] = [];
|
||||||
|
if (options.requireCleanTree) plan.push("preflight.cleanTree");
|
||||||
|
if (options.runTests) plan.push("preflight.test");
|
||||||
|
plan.push("core.version", "core.changelog", "core.commit", "core.tag");
|
||||||
|
if (options.runBuild) plan.push("core.build");
|
||||||
|
for (const target of options.targets) {
|
||||||
|
plan.push(`target.${target}`);
|
||||||
|
}
|
||||||
|
return plan;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveCommitWorkflow = async (argvArg: any): Promise<IResolvedCommitWorkflow> => {
|
||||||
|
const cliConfig = await readCliWorkflowConfig();
|
||||||
|
const commitConfig = cliConfig.commit || {};
|
||||||
|
const releaseFlagRequested = Boolean(argvArg.r || argvArg.release);
|
||||||
|
|
||||||
|
let confirmation = normalizeConfirmation(commitConfig.confirmation, "prompt");
|
||||||
|
if (argvArg.plan) {
|
||||||
|
confirmation = "plan";
|
||||||
|
} else if (argvArg.y || argvArg.yes) {
|
||||||
|
confirmation = "auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
let rawSteps: TCommitStep[];
|
||||||
|
if (hasExplicitCommitWorkflowFlags(argvArg)) {
|
||||||
|
const orderedFlags = getOrderedShortFlags("commit");
|
||||||
|
rawSteps = ["analyze"];
|
||||||
|
for (const shortFlag of orderedFlags) {
|
||||||
|
const step = commitFlagToStep[shortFlag];
|
||||||
|
if (step) {
|
||||||
|
rawSteps.push(step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (argvArg.format && !rawSteps.includes("format")) rawSteps.push("format");
|
||||||
|
if (argvArg.test && !rawSteps.includes("test")) rawSteps.push("test");
|
||||||
|
if (argvArg.build && !rawSteps.includes("build")) rawSteps.push("build");
|
||||||
|
if (argvArg.push && !rawSteps.includes("push")) rawSteps.push("push");
|
||||||
|
rawSteps.push("changelog");
|
||||||
|
rawSteps.push("commit");
|
||||||
|
} else if (Array.isArray(commitConfig.steps) && commitConfig.steps.length > 0) {
|
||||||
|
rawSteps = commitConfig.steps;
|
||||||
|
} else {
|
||||||
|
rawSteps = ["analyze"];
|
||||||
|
if (commitConfig.alwaysTest) rawSteps.push("test");
|
||||||
|
if (commitConfig.alwaysBuild) rawSteps.push("build");
|
||||||
|
rawSteps.push("changelog");
|
||||||
|
rawSteps.push("commit");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
confirmation,
|
||||||
|
steps: normalizeCommitSteps(rawSteps),
|
||||||
|
staging: commitConfig.staging || "all",
|
||||||
|
testCommand: commitConfig.test?.command || "pnpm test",
|
||||||
|
buildCommand: commitConfig.build?.command || "pnpm build",
|
||||||
|
changelogFile: "changelog.md",
|
||||||
|
changelogSection: "Pending",
|
||||||
|
pushRemote: commitConfig.push?.remote || "origin",
|
||||||
|
pushFollowTags: commitConfig.push?.followTags || false,
|
||||||
|
releaseFlagRequested,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedReleaseWorkflow> => {
|
||||||
|
const cliConfig = await readCliWorkflowConfig();
|
||||||
|
const releaseConfig = cliConfig.release || {};
|
||||||
|
const targetConfig = releaseConfig.targets || {};
|
||||||
|
const gitConfig = targetConfig.git || {};
|
||||||
|
const npmConfig = targetConfig.npm || {};
|
||||||
|
const dockerConfig = targetConfig.docker || {};
|
||||||
|
const npmRegistries = (npmConfig.registries || []).map(normalizeRegistryUrl);
|
||||||
|
const npmEnabled = npmConfig.enabled ?? npmRegistries.length > 0;
|
||||||
|
const gitEnabled = gitConfig.enabled ?? true;
|
||||||
|
const dockerEnabled = dockerConfig.enabled ?? false;
|
||||||
|
|
||||||
|
let confirmation = normalizeConfirmation(releaseConfig.confirmation, "prompt");
|
||||||
|
if (argvArg.plan) {
|
||||||
|
confirmation = "plan";
|
||||||
|
} else if (argvArg.y || argvArg.yes) {
|
||||||
|
confirmation = "auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
let requireCleanTree = releaseConfig.preflight?.requireCleanTree ?? true;
|
||||||
|
let runTests = releaseConfig.preflight?.test ?? false;
|
||||||
|
let runBuild = releaseConfig.preflight?.build ?? true;
|
||||||
|
if (argvArg.t || argvArg.test) runTests = true;
|
||||||
|
if (argvArg.b || argvArg.build) runBuild = true;
|
||||||
|
if (isDisabled(argvArg, "test")) runTests = false;
|
||||||
|
if (isDisabled(argvArg, "build")) runBuild = false;
|
||||||
|
if (isDisabled(argvArg, "preflight")) requireCleanTree = false;
|
||||||
|
|
||||||
|
const configuredTargets: TReleaseTarget[] = [];
|
||||||
|
if (gitEnabled) configuredTargets.push("git");
|
||||||
|
if (npmEnabled) configuredTargets.push("npm");
|
||||||
|
if (dockerEnabled) configuredTargets.push("docker");
|
||||||
|
let targets = getTargetOverride(argvArg) || configuredTargets;
|
||||||
|
if (isDisabled(argvArg, "git", "push")) {
|
||||||
|
targets = targets.filter((target) => target !== "git");
|
||||||
|
}
|
||||||
|
if (isDisabled(argvArg, "publish")) {
|
||||||
|
targets = targets.filter((target) => target === "git");
|
||||||
|
}
|
||||||
|
targets = unique(targets);
|
||||||
|
|
||||||
|
return {
|
||||||
|
confirmation,
|
||||||
|
plan: buildReleasePlan({ requireCleanTree, runTests, runBuild, targets }),
|
||||||
|
targets,
|
||||||
|
requireCleanTree,
|
||||||
|
runTests,
|
||||||
|
runBuild,
|
||||||
|
testCommand: releaseConfig.preflight?.testCommand || "pnpm test",
|
||||||
|
buildCommand: releaseConfig.preflight?.buildCommand || "pnpm build",
|
||||||
|
changelogFile: "changelog.md",
|
||||||
|
changelogPendingSection: "Pending",
|
||||||
|
changelogVersionHeading: "## {{date}} - {{version}}",
|
||||||
|
gitEnabled,
|
||||||
|
gitRemote: gitConfig.remote || "origin",
|
||||||
|
pushBranch: gitConfig.pushBranch ?? true,
|
||||||
|
pushTags: gitConfig.pushTags ?? true,
|
||||||
|
npmEnabled,
|
||||||
|
npmRegistries,
|
||||||
|
npmAccessLevel: npmConfig.accessLevel || "public",
|
||||||
|
npmAlreadyPublished: npmConfig.alreadyPublished || "success",
|
||||||
|
dockerEnabled,
|
||||||
|
dockerEngine: "tsdocker",
|
||||||
|
dockerPatterns: Array.isArray(dockerConfig.patterns) ? dockerConfig.patterns : [],
|
||||||
|
dockerCached: dockerConfig.cached ?? false,
|
||||||
|
dockerParallel: dockerConfig.parallel ?? false,
|
||||||
|
dockerContext: typeof dockerConfig.context === "string" && dockerConfig.context.trim()
|
||||||
|
? dockerConfig.context.trim()
|
||||||
|
: undefined,
|
||||||
|
dockerNoBuild: dockerConfig.noBuild ?? false,
|
||||||
|
};
|
||||||
|
};
|
||||||
+247
-449
@@ -3,15 +3,11 @@
|
|||||||
import * as plugins from "./mod.plugins.js";
|
import * as plugins from "./mod.plugins.js";
|
||||||
import * as paths from "../paths.js";
|
import * as paths from "../paths.js";
|
||||||
import { logger } from "../gitzone.logging.js";
|
import { logger } from "../gitzone.logging.js";
|
||||||
import * as helpers from "./mod.helpers.js";
|
|
||||||
import * as ui from "./mod.ui.js";
|
import * as ui from "./mod.ui.js";
|
||||||
import { ReleaseConfig } from "../mod_config/classes.releaseconfig.js";
|
|
||||||
import type { ICliMode } from "../helpers.climode.js";
|
import type { ICliMode } from "../helpers.climode.js";
|
||||||
import {
|
import { getCliMode, printJson, runWithSuppressedOutput } from "../helpers.climode.js";
|
||||||
getCliMode,
|
import { appendPendingChangelogEntry } from "../helpers.changelog.js";
|
||||||
printJson,
|
import { resolveCommitWorkflow, type IResolvedCommitWorkflow } from "../helpers.workflow.js";
|
||||||
runWithSuppressedOutput,
|
|
||||||
} from "../helpers.climode.js";
|
|
||||||
|
|
||||||
export const run = async (argvArg: any) => {
|
export const run = async (argvArg: any) => {
|
||||||
const mode = await getCliMode(argvArg);
|
const mode = await getCliMode(argvArg);
|
||||||
@@ -36,431 +32,247 @@ export const run = async (argvArg: any) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read commit config from .smartconfig.json
|
const workflow = await resolveCommitWorkflow(argvArg);
|
||||||
const smartconfigInstance = new plugins.smartconfig.Smartconfig();
|
if (workflow.releaseFlagRequested) {
|
||||||
const gitzoneConfig = smartconfigInstance.dataFor<{
|
|
||||||
commit?: {
|
|
||||||
alwaysTest?: boolean;
|
|
||||||
alwaysBuild?: boolean;
|
|
||||||
};
|
|
||||||
}>("@git.zone/cli", {});
|
|
||||||
const commitConfig = gitzoneConfig.commit || {};
|
|
||||||
|
|
||||||
// Check flags and merge with config options
|
|
||||||
const wantsRelease = !!(argvArg.r || argvArg.release);
|
|
||||||
const wantsTest = !!(argvArg.t || argvArg.test || commitConfig.alwaysTest);
|
|
||||||
const wantsBuild = !!(argvArg.b || argvArg.build || commitConfig.alwaysBuild);
|
|
||||||
let releaseConfig: ReleaseConfig | null = null;
|
|
||||||
|
|
||||||
if (wantsRelease) {
|
|
||||||
releaseConfig = await ReleaseConfig.fromCwd();
|
|
||||||
if (!releaseConfig.hasRegistries()) {
|
|
||||||
logger.log("error", "No release registries configured.");
|
|
||||||
console.log("");
|
|
||||||
console.log(
|
|
||||||
" Run `gitzone config add <registry-url>` to add registries.",
|
|
||||||
);
|
|
||||||
console.log("");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print execution plan at the start
|
|
||||||
ui.printExecutionPlan({
|
|
||||||
autoAccept: !!(argvArg.y || argvArg.yes),
|
|
||||||
push: !!(argvArg.p || argvArg.push),
|
|
||||||
test: wantsTest,
|
|
||||||
build: wantsBuild,
|
|
||||||
release: wantsRelease,
|
|
||||||
format: !!argvArg.format,
|
|
||||||
registries: releaseConfig?.getRegistries(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (argvArg.format) {
|
|
||||||
const formatMod = await import("../mod_format/index.js");
|
|
||||||
await formatMod.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run tests early to fail fast before analysis
|
|
||||||
if (wantsTest) {
|
|
||||||
ui.printHeader("🧪 Running tests...");
|
|
||||||
const smartshellForTest = new plugins.smartshell.Smartshell({
|
|
||||||
executor: "bash",
|
|
||||||
sourceFilePaths: [],
|
|
||||||
});
|
|
||||||
const testResult = await smartshellForTest.exec("pnpm test");
|
|
||||||
if (testResult.exitCode !== 0) {
|
|
||||||
logger.log("error", "Tests failed. Aborting commit.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
logger.log("success", "All tests passed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.printHeader("🔍 Analyzing repository changes...");
|
|
||||||
|
|
||||||
const aidoc = new plugins.tsdoc.AiDoc();
|
|
||||||
await aidoc.start();
|
|
||||||
|
|
||||||
const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd);
|
|
||||||
|
|
||||||
await aidoc.stop();
|
|
||||||
|
|
||||||
ui.printRecommendation({
|
|
||||||
recommendedNextVersion: nextCommitObject.recommendedNextVersion,
|
|
||||||
recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel,
|
|
||||||
recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope,
|
|
||||||
recommendedNextVersionMessage:
|
|
||||||
nextCommitObject.recommendedNextVersionMessage,
|
|
||||||
});
|
|
||||||
|
|
||||||
let answerBucket: plugins.smartinteract.AnswerBucket;
|
|
||||||
|
|
||||||
// Check if -y/--yes flag is set AND version is not a breaking change
|
|
||||||
// Breaking changes (major version bumps) always require manual confirmation
|
|
||||||
const isBreakingChange =
|
|
||||||
nextCommitObject.recommendedNextVersionLevel === "BREAKING CHANGE";
|
|
||||||
const canAutoAccept = (argvArg.y || argvArg.yes) && !isBreakingChange;
|
|
||||||
|
|
||||||
if (canAutoAccept) {
|
|
||||||
// Auto-mode: create AnswerBucket programmatically
|
|
||||||
logger.log("info", "✓ Auto-accepting AI recommendations (--yes flag)");
|
|
||||||
|
|
||||||
answerBucket = new plugins.smartinteract.AnswerBucket();
|
|
||||||
answerBucket.addAnswer({
|
|
||||||
name: "commitType",
|
|
||||||
value: nextCommitObject.recommendedNextVersionLevel,
|
|
||||||
});
|
|
||||||
answerBucket.addAnswer({
|
|
||||||
name: "commitScope",
|
|
||||||
value: nextCommitObject.recommendedNextVersionScope,
|
|
||||||
});
|
|
||||||
answerBucket.addAnswer({
|
|
||||||
name: "commitDescription",
|
|
||||||
value: nextCommitObject.recommendedNextVersionMessage,
|
|
||||||
});
|
|
||||||
answerBucket.addAnswer({
|
|
||||||
name: "pushToOrigin",
|
|
||||||
value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided
|
|
||||||
});
|
|
||||||
answerBucket.addAnswer({
|
|
||||||
name: "createRelease",
|
|
||||||
value: wantsRelease,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Warn if --yes was provided but we're requiring confirmation due to breaking change
|
|
||||||
if (isBreakingChange && (argvArg.y || argvArg.yes)) {
|
|
||||||
logger.log(
|
logger.log(
|
||||||
"warn",
|
"warn",
|
||||||
"⚠️ BREAKING CHANGE detected - manual confirmation required",
|
"`gitzone commit -r` is deprecated and no longer releases. Use `gitzone release` after committing.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Interactive mode: prompt user for input
|
|
||||||
const commitInteract = new plugins.smartinteract.SmartInteract();
|
printCommitExecutionPlan(workflow);
|
||||||
commitInteract.addQuestions([
|
if (workflow.confirmation === "plan") {
|
||||||
{
|
return;
|
||||||
type: "list",
|
|
||||||
name: `commitType`,
|
|
||||||
message: `Choose TYPE of the commit:`,
|
|
||||||
choices: [`fix`, `feat`, `BREAKING CHANGE`],
|
|
||||||
default: nextCommitObject.recommendedNextVersionLevel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "input",
|
|
||||||
name: `commitScope`,
|
|
||||||
message: `What is the SCOPE of the commit:`,
|
|
||||||
default: nextCommitObject.recommendedNextVersionScope,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: `input`,
|
|
||||||
name: `commitDescription`,
|
|
||||||
message: `What is the DESCRIPTION of the commit?`,
|
|
||||||
default: nextCommitObject.recommendedNextVersionMessage,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "confirm",
|
|
||||||
name: `pushToOrigin`,
|
|
||||||
message: `Do you want to push this version now?`,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "confirm",
|
|
||||||
name: `createRelease`,
|
|
||||||
message: `Do you want to publish to npm registries?`,
|
|
||||||
default: wantsRelease,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
answerBucket = await commitInteract.runQueue();
|
|
||||||
}
|
|
||||||
const commitString = createCommitStringFromAnswerBucket(answerBucket);
|
|
||||||
const 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.printCommitMessage(commitString);
|
|
||||||
const smartshellInstance = new plugins.smartshell.Smartshell({
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
executor: "bash",
|
executor: "bash",
|
||||||
sourceFilePaths: [],
|
sourceFilePaths: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load release config if user wants to release (interactively selected)
|
let nextCommitObject: any;
|
||||||
if (answerBucket.getAnswerFor("createRelease") && !releaseConfig) {
|
let answerBucket: plugins.smartinteract.AnswerBucket | undefined;
|
||||||
releaseConfig = await ReleaseConfig.fromCwd();
|
|
||||||
if (!releaseConfig.hasRegistries()) {
|
for (const step of workflow.steps) {
|
||||||
logger.log("error", "No release registries configured.");
|
switch (step) {
|
||||||
console.log("");
|
case "format":
|
||||||
console.log(
|
await runFormatStep();
|
||||||
" Run `gitzone config add <registry-url>` to add registries.",
|
break;
|
||||||
);
|
case "test":
|
||||||
console.log("");
|
await runCommandStep(smartshellInstance, "Running tests", workflow.testCommand);
|
||||||
process.exit(1);
|
break;
|
||||||
|
case "build":
|
||||||
|
await runCommandStep(smartshellInstance, "Running build", workflow.buildCommand);
|
||||||
|
break;
|
||||||
|
case "analyze":
|
||||||
|
nextCommitObject = await runAnalyzeStep();
|
||||||
|
answerBucket = await buildAnswerBucket(nextCommitObject, workflow, mode, argvArg);
|
||||||
|
break;
|
||||||
|
case "changelog":
|
||||||
|
assertAnalysisComplete(answerBucket, nextCommitObject);
|
||||||
|
await runChangelogStep(workflow, answerBucket!, nextCommitObject);
|
||||||
|
break;
|
||||||
|
case "commit":
|
||||||
|
assertAnalysisComplete(answerBucket, nextCommitObject);
|
||||||
|
await runCommitStep(smartshellInstance, answerBucket!);
|
||||||
|
break;
|
||||||
|
case "push":
|
||||||
|
await runPushStep(smartshellInstance, workflow);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine total steps based on options
|
const commitShaResult = await smartshellInstance.exec("git rev-parse --short HEAD");
|
||||||
// Note: test runs early (like format) so not counted in numbered steps
|
const currentBranch = await detectCurrentBranch(smartshellInstance);
|
||||||
const willPush =
|
|
||||||
answerBucket.getAnswerFor("pushToOrigin") && !(process.env.CI === "true");
|
|
||||||
const willRelease =
|
|
||||||
answerBucket.getAnswerFor("createRelease") &&
|
|
||||||
releaseConfig?.hasRegistries();
|
|
||||||
let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version
|
|
||||||
if (wantsBuild) totalSteps += 2; // build step + verification step
|
|
||||||
if (willPush) totalSteps++;
|
|
||||||
if (willRelease) totalSteps++;
|
|
||||||
let currentStep = 0;
|
|
||||||
|
|
||||||
// Step 1: Baking commitinfo
|
|
||||||
currentStep++;
|
|
||||||
ui.printStep(
|
|
||||||
currentStep,
|
|
||||||
totalSteps,
|
|
||||||
"🔧 Baking commit info into code",
|
|
||||||
"in-progress",
|
|
||||||
);
|
|
||||||
const commitInfo = new plugins.commitinfo.CommitInfo(
|
|
||||||
paths.cwd,
|
|
||||||
commitVersionType,
|
|
||||||
);
|
|
||||||
await commitInfo.writeIntoPotentialDirs();
|
|
||||||
ui.printStep(
|
|
||||||
currentStep,
|
|
||||||
totalSteps,
|
|
||||||
"🔧 Baking commit info into code",
|
|
||||||
"done",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 2: Writing changelog
|
|
||||||
currentStep++;
|
|
||||||
ui.printStep(
|
|
||||||
currentStep,
|
|
||||||
totalSteps,
|
|
||||||
"📄 Generating changelog.md",
|
|
||||||
"in-progress",
|
|
||||||
);
|
|
||||||
let changelog = nextCommitObject.changelog || "# Changelog\n";
|
|
||||||
changelog = changelog.replaceAll(
|
|
||||||
"{{nextVersion}}",
|
|
||||||
(await commitInfo.getNextPlannedVersion()).versionString,
|
|
||||||
);
|
|
||||||
changelog = changelog.replaceAll(
|
|
||||||
"{{nextVersionScope}}",
|
|
||||||
`${await answerBucket.getAnswerFor("commitType")}(${await answerBucket.getAnswerFor("commitScope")})`,
|
|
||||||
);
|
|
||||||
changelog = changelog.replaceAll(
|
|
||||||
"{{nextVersionMessage}}",
|
|
||||||
nextCommitObject.recommendedNextVersionMessage,
|
|
||||||
);
|
|
||||||
if (nextCommitObject.recommendedNextVersionDetails?.length > 0) {
|
|
||||||
changelog = changelog.replaceAll(
|
|
||||||
"{{nextVersionDetails}}",
|
|
||||||
"- " + nextCommitObject.recommendedNextVersionDetails.join("\n- "),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
changelog = changelog.replaceAll("\n{{nextVersionDetails}}", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
await plugins.smartfs
|
|
||||||
.file(plugins.path.join(paths.cwd, `changelog.md`))
|
|
||||||
.encoding("utf8")
|
|
||||||
.write(changelog);
|
|
||||||
ui.printStep(currentStep, totalSteps, "📄 Generating changelog.md", "done");
|
|
||||||
|
|
||||||
// Step 3: Staging files
|
|
||||||
currentStep++;
|
|
||||||
ui.printStep(currentStep, totalSteps, "📦 Staging files", "in-progress");
|
|
||||||
await smartshellInstance.exec(`git add -A`);
|
|
||||||
ui.printStep(currentStep, totalSteps, "📦 Staging files", "done");
|
|
||||||
|
|
||||||
// Step 4: Creating commit
|
|
||||||
currentStep++;
|
|
||||||
ui.printStep(
|
|
||||||
currentStep,
|
|
||||||
totalSteps,
|
|
||||||
"💾 Creating git commit",
|
|
||||||
"in-progress",
|
|
||||||
);
|
|
||||||
await smartshellInstance.exec(`git commit -m "${commitString}"`);
|
|
||||||
ui.printStep(currentStep, totalSteps, "💾 Creating git commit", "done");
|
|
||||||
|
|
||||||
// Step 5: Bumping version
|
|
||||||
currentStep++;
|
|
||||||
const projectType = await helpers.detectProjectType();
|
|
||||||
const newVersion = await helpers.bumpProjectVersion(
|
|
||||||
projectType,
|
|
||||||
commitVersionType,
|
|
||||||
currentStep,
|
|
||||||
totalSteps,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 6: Run build (optional)
|
|
||||||
if (wantsBuild) {
|
|
||||||
currentStep++;
|
|
||||||
ui.printStep(currentStep, totalSteps, "🔨 Running build", "in-progress");
|
|
||||||
const buildResult = await smartshellInstance.exec("pnpm build");
|
|
||||||
if (buildResult.exitCode !== 0) {
|
|
||||||
ui.printStep(currentStep, totalSteps, "🔨 Running build", "error");
|
|
||||||
logger.log("error", "Build failed. Aborting release.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
ui.printStep(currentStep, totalSteps, "🔨 Running build", "done");
|
|
||||||
|
|
||||||
// Step 7: Verify no uncommitted changes
|
|
||||||
currentStep++;
|
|
||||||
ui.printStep(
|
|
||||||
currentStep,
|
|
||||||
totalSteps,
|
|
||||||
"🔍 Verifying clean working tree",
|
|
||||||
"in-progress",
|
|
||||||
);
|
|
||||||
const statusResult = await smartshellInstance.exec(
|
|
||||||
"git status --porcelain",
|
|
||||||
);
|
|
||||||
if (statusResult.stdout.trim() !== "") {
|
|
||||||
ui.printStep(
|
|
||||||
currentStep,
|
|
||||||
totalSteps,
|
|
||||||
"🔍 Verifying clean working tree",
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
logger.log(
|
|
||||||
"error",
|
|
||||||
"Build produced uncommitted changes. This usually means build output is not gitignored.",
|
|
||||||
);
|
|
||||||
logger.log("error", "Uncommitted files:");
|
|
||||||
console.log(statusResult.stdout);
|
|
||||||
logger.log(
|
|
||||||
"error",
|
|
||||||
"Aborting release. Please ensure build artifacts are in .gitignore",
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
ui.printStep(
|
|
||||||
currentStep,
|
|
||||||
totalSteps,
|
|
||||||
"🔍 Verifying clean working tree",
|
|
||||||
"done",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step: Push to remote (optional)
|
|
||||||
const currentBranch = await helpers.detectCurrentBranch();
|
|
||||||
if (willPush) {
|
|
||||||
currentStep++;
|
|
||||||
ui.printStep(
|
|
||||||
currentStep,
|
|
||||||
totalSteps,
|
|
||||||
`🚀 Pushing to origin/${currentBranch}`,
|
|
||||||
"in-progress",
|
|
||||||
);
|
|
||||||
await smartshellInstance.exec(
|
|
||||||
`git push origin ${currentBranch} --follow-tags`,
|
|
||||||
);
|
|
||||||
ui.printStep(
|
|
||||||
currentStep,
|
|
||||||
totalSteps,
|
|
||||||
`🚀 Pushing to origin/${currentBranch}`,
|
|
||||||
"done",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 7: Publish to npm registries (optional)
|
|
||||||
let releasedRegistries: string[] = [];
|
|
||||||
if (willRelease && releaseConfig) {
|
|
||||||
currentStep++;
|
|
||||||
const registries = releaseConfig.getRegistries();
|
|
||||||
ui.printStep(
|
|
||||||
currentStep,
|
|
||||||
totalSteps,
|
|
||||||
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
|
|
||||||
"in-progress",
|
|
||||||
);
|
|
||||||
|
|
||||||
const accessLevel = releaseConfig.getAccessLevel();
|
|
||||||
for (const registry of registries) {
|
|
||||||
try {
|
|
||||||
await smartshellInstance.exec(
|
|
||||||
`npm publish --registry=${registry} --access=${accessLevel}`,
|
|
||||||
);
|
|
||||||
releasedRegistries.push(registry);
|
|
||||||
} catch (error) {
|
|
||||||
logger.log("error", `Failed to publish to ${registry}: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (releasedRegistries.length === registries.length) {
|
|
||||||
ui.printStep(
|
|
||||||
currentStep,
|
|
||||||
totalSteps,
|
|
||||||
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
|
|
||||||
"done",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ui.printStep(
|
|
||||||
currentStep,
|
|
||||||
totalSteps,
|
|
||||||
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(""); // Add spacing before summary
|
|
||||||
|
|
||||||
// Get commit SHA for summary
|
|
||||||
const commitShaResult = await smartshellInstance.exec(
|
|
||||||
"git rev-parse --short HEAD",
|
|
||||||
);
|
|
||||||
const commitSha = commitShaResult.stdout.trim();
|
|
||||||
|
|
||||||
// Print final summary
|
|
||||||
ui.printSummary({
|
ui.printSummary({
|
||||||
projectType,
|
projectType: "source",
|
||||||
branch: currentBranch,
|
branch: currentBranch,
|
||||||
commitType: answerBucket.getAnswerFor("commitType"),
|
commitType: answerBucket!.getAnswerFor("commitType"),
|
||||||
commitScope: answerBucket.getAnswerFor("commitScope"),
|
commitScope: answerBucket!.getAnswerFor("commitScope"),
|
||||||
commitMessage: answerBucket.getAnswerFor("commitDescription"),
|
commitMessage: answerBucket!.getAnswerFor("commitDescription"),
|
||||||
newVersion: newVersion,
|
commitSha: commitShaResult.stdout.trim(),
|
||||||
commitSha: commitSha,
|
pushed: workflow.steps.includes("push"),
|
||||||
pushed: willPush,
|
|
||||||
released: releasedRegistries.length > 0,
|
|
||||||
releasedRegistries:
|
|
||||||
releasedRegistries.length > 0 ? releasedRegistries : undefined,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function runFormatStep(): Promise<void> {
|
||||||
|
ui.printHeader("Formatting project files");
|
||||||
|
const formatMod = await import("../mod_format/index.js");
|
||||||
|
await formatMod.run({ write: true, yes: true, interactive: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommandStep(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
label: string,
|
||||||
|
command: string,
|
||||||
|
): Promise<void> {
|
||||||
|
ui.printHeader(label);
|
||||||
|
const result = await smartshellInstance.exec(command);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", `${label} failed. Aborting commit.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
logger.log("success", `${label} passed.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAnalyzeStep(): Promise<any> {
|
||||||
|
ui.printHeader("Analyzing repository changes");
|
||||||
|
const aidoc = new plugins.tsdoc.AiDoc();
|
||||||
|
await aidoc.start();
|
||||||
|
try {
|
||||||
|
const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd);
|
||||||
|
ui.printRecommendation({
|
||||||
|
recommendedNextVersion: nextCommitObject.recommendedNextVersion,
|
||||||
|
recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel,
|
||||||
|
recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope,
|
||||||
|
recommendedNextVersionMessage: nextCommitObject.recommendedNextVersionMessage,
|
||||||
|
});
|
||||||
|
return nextCommitObject;
|
||||||
|
} finally {
|
||||||
|
await aidoc.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildAnswerBucket(
|
||||||
|
nextCommitObject: any,
|
||||||
|
workflow: IResolvedCommitWorkflow,
|
||||||
|
mode: ICliMode,
|
||||||
|
argvArg: any,
|
||||||
|
): Promise<plugins.smartinteract.AnswerBucket> {
|
||||||
|
const isBreakingChange = nextCommitObject.recommendedNextVersionLevel === "BREAKING CHANGE";
|
||||||
|
const canAutoAccept = workflow.confirmation === "auto" && !isBreakingChange;
|
||||||
|
|
||||||
|
if (canAutoAccept) {
|
||||||
|
logger.log("info", "Auto-accepting AI recommendations");
|
||||||
|
return createAnswerBucket({
|
||||||
|
commitType: nextCommitObject.recommendedNextVersionLevel,
|
||||||
|
commitScope: nextCommitObject.recommendedNextVersionScope,
|
||||||
|
commitDescription: nextCommitObject.recommendedNextVersionMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBreakingChange && (workflow.confirmation === "auto" || argvArg.y || argvArg.yes)) {
|
||||||
|
logger.log("warn", "BREAKING CHANGE detected - manual confirmation required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mode.interactive) {
|
||||||
|
throw new Error("Commit confirmation requires an interactive terminal. Use `-y` or set commit.confirmation to `auto`.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitInteract = new plugins.smartinteract.SmartInteract();
|
||||||
|
commitInteract.addQuestions([
|
||||||
|
{
|
||||||
|
type: "list",
|
||||||
|
name: "commitType",
|
||||||
|
message: "Choose TYPE of the commit:",
|
||||||
|
choices: ["fix", "feat", "BREAKING CHANGE"],
|
||||||
|
default: nextCommitObject.recommendedNextVersionLevel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "commitScope",
|
||||||
|
message: "What is the SCOPE of the commit:",
|
||||||
|
default: nextCommitObject.recommendedNextVersionScope,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "commitDescription",
|
||||||
|
message: "What is the DESCRIPTION of the commit?",
|
||||||
|
default: nextCommitObject.recommendedNextVersionMessage,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
return await commitInteract.runQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAnswerBucket(answers: {
|
||||||
|
commitType: string;
|
||||||
|
commitScope: string;
|
||||||
|
commitDescription: string;
|
||||||
|
}): plugins.smartinteract.AnswerBucket {
|
||||||
|
const answerBucket = new plugins.smartinteract.AnswerBucket();
|
||||||
|
for (const [name, value] of Object.entries(answers)) {
|
||||||
|
answerBucket.addAnswer({ name, value });
|
||||||
|
}
|
||||||
|
return answerBucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runChangelogStep(
|
||||||
|
workflow: IResolvedCommitWorkflow,
|
||||||
|
answerBucket: plugins.smartinteract.AnswerBucket,
|
||||||
|
nextCommitObject: any,
|
||||||
|
): Promise<void> {
|
||||||
|
await appendPendingChangelogEntry(
|
||||||
|
plugins.path.join(paths.cwd, workflow.changelogFile),
|
||||||
|
workflow.changelogSection,
|
||||||
|
{
|
||||||
|
type: answerBucket.getAnswerFor("commitType"),
|
||||||
|
scope: answerBucket.getAnswerFor("commitScope"),
|
||||||
|
message: answerBucket.getAnswerFor("commitDescription"),
|
||||||
|
details: nextCommitObject.recommendedNextVersionDetails || [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
logger.log("success", `Updated ${workflow.changelogFile} pending section.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommitStep(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
answerBucket: plugins.smartinteract.AnswerBucket,
|
||||||
|
): Promise<void> {
|
||||||
|
ui.printHeader("Creating Semantic Commit");
|
||||||
|
const commitString = createCommitStringFromAnswerBucket(answerBucket);
|
||||||
|
ui.printCommitMessage(commitString);
|
||||||
|
await smartshellInstance.exec("git add -A");
|
||||||
|
const result = await smartshellInstance.exec(`git commit -m ${shellQuote(commitString)}`);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", "git commit failed.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPushStep(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
workflow: IResolvedCommitWorkflow,
|
||||||
|
): Promise<void> {
|
||||||
|
const currentBranch = await detectCurrentBranch(smartshellInstance);
|
||||||
|
const followTags = workflow.pushFollowTags ? " --follow-tags" : "";
|
||||||
|
const result = await smartshellInstance.exec(
|
||||||
|
`git push ${workflow.pushRemote} ${currentBranch}${followTags}`,
|
||||||
|
);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", "git push failed.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detectCurrentBranch(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
): Promise<string> {
|
||||||
|
const branchResult = await smartshellInstance.exec("git branch --show-current");
|
||||||
|
return branchResult.stdout.trim() || "master";
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertAnalysisComplete(
|
||||||
|
answerBucket: plugins.smartinteract.AnswerBucket | undefined,
|
||||||
|
nextCommitObject: any,
|
||||||
|
): void {
|
||||||
|
if (!answerBucket || !nextCommitObject) {
|
||||||
|
throw new Error("Commit workflow requires analyze before changelog and commit steps.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellQuote(value: string): string {
|
||||||
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printCommitExecutionPlan(workflow: IResolvedCommitWorkflow): void {
|
||||||
|
console.log("");
|
||||||
|
console.log("gitzone commit - resolved workflow");
|
||||||
|
console.log(`confirmation: ${workflow.confirmation}`);
|
||||||
|
console.log(`steps: ${workflow.steps.join(" -> ")}`);
|
||||||
|
console.log(`changelog: ${workflow.changelogFile}#${workflow.changelogSection}`);
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
async function handleRecommend(mode: ICliMode): Promise<void> {
|
async function handleRecommend(mode: ICliMode): Promise<void> {
|
||||||
const recommendationBuilder = async () => {
|
const recommendationBuilder = async () => {
|
||||||
const aidoc = new plugins.tsdoc.AiDoc();
|
const aidoc = new plugins.tsdoc.AiDoc();
|
||||||
@@ -507,40 +319,27 @@ export function showHelp(mode?: ICliMode): void {
|
|||||||
printJson({
|
printJson({
|
||||||
command: "commit",
|
command: "commit",
|
||||||
usage: "gitzone commit [recommend] [options]",
|
usage: "gitzone commit [recommend] [options]",
|
||||||
description:
|
description: "Analyzes changes and creates one semantic source commit.",
|
||||||
"Creates semantic commits or emits a read-only recommendation.",
|
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
name: "recommend",
|
name: "recommend",
|
||||||
description:
|
description: "Generate a commit recommendation without mutating the repository",
|
||||||
"Generate a commit recommendation without mutating the repository",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
flags: [
|
flags: [
|
||||||
{ flag: "-y, --yes", description: "Auto-accept AI recommendations" },
|
{ flag: "-y, --yes", description: "Auto-accept safe AI recommendations" },
|
||||||
{ flag: "-p, --push", description: "Push to origin after commit" },
|
{ flag: "-p, --push", description: "Push to origin after committing" },
|
||||||
{ flag: "-t, --test", description: "Run tests before the commit flow" },
|
{ flag: "-t, --test", description: "Run tests as part of the commit workflow" },
|
||||||
{
|
{ flag: "-b, --build", description: "Run build as part of the commit workflow" },
|
||||||
flag: "-b, --build",
|
{ flag: "-f, --format", description: "Run gitzone format before committing" },
|
||||||
description: "Run the build after the commit flow",
|
{ flag: "--plan", description: "Show resolved workflow without mutating files" },
|
||||||
},
|
{ flag: "--json", description: "Emit JSON for `commit recommend` only" },
|
||||||
{
|
|
||||||
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: [
|
examples: [
|
||||||
"gitzone commit recommend --json",
|
"gitzone commit recommend --json",
|
||||||
"gitzone commit -y",
|
"gitzone commit -y",
|
||||||
"gitzone commit -ypbr",
|
"gitzone commit -ytbp",
|
||||||
|
"gitzone release",
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -549,25 +348,24 @@ export function showHelp(mode?: ICliMode): void {
|
|||||||
console.log("");
|
console.log("");
|
||||||
console.log("Usage: gitzone commit [recommend] [options]");
|
console.log("Usage: gitzone commit [recommend] [options]");
|
||||||
console.log("");
|
console.log("");
|
||||||
|
console.log("Creates one semantic source commit. It does not version, tag, or publish.");
|
||||||
|
console.log("");
|
||||||
console.log("Commands:");
|
console.log("Commands:");
|
||||||
console.log(
|
console.log(" recommend Generate a commit recommendation without mutating the repository");
|
||||||
" recommend Generate a commit recommendation without mutating the repository",
|
|
||||||
);
|
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log("Flags:");
|
console.log("Flags:");
|
||||||
console.log(" -y, --yes Auto-accept AI recommendations");
|
console.log(" -y, --yes Auto-accept safe AI recommendations");
|
||||||
console.log(" -p, --push Push to origin after commit");
|
console.log(" -p, --push Push after commit");
|
||||||
console.log(" -t, --test Run tests before the commit flow");
|
console.log(" -t, --test Run tests in the configured order");
|
||||||
console.log(" -b, --build Run the build after the commit flow");
|
console.log(" -b, --build Run build in the configured order");
|
||||||
console.log(
|
console.log(" -f, --format Run gitzone format before committing");
|
||||||
" -r, --release Publish to configured registries after push",
|
console.log(" --plan Show resolved workflow without mutating files");
|
||||||
);
|
|
||||||
console.log(" --format Run gitzone format before committing");
|
|
||||||
console.log(" --json Emit JSON for `commit recommend` only");
|
console.log(" --json Emit JSON for `commit recommend` only");
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log("Examples:");
|
console.log("Examples:");
|
||||||
console.log(" gitzone commit recommend --json");
|
console.log(" gitzone commit recommend --json");
|
||||||
console.log(" gitzone commit -y");
|
console.log(" gitzone commit -y");
|
||||||
console.log(" gitzone commit -ypbr");
|
console.log(" gitzone commit -ytbp");
|
||||||
|
console.log(" gitzone release");
|
||||||
console.log("");
|
console.log("");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export async function detectProjectType(): Promise<ProjectType> {
|
|||||||
* @param versionType Type of version bump
|
* @param versionType Type of version bump
|
||||||
* @returns New version string
|
* @returns New version string
|
||||||
*/
|
*/
|
||||||
function calculateNewVersion(currentVersion: string, versionType: VersionType): string {
|
export function calculateNewVersion(currentVersion: string, versionType: VersionType): string {
|
||||||
const versionMatch = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/);
|
const versionMatch = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/);
|
||||||
|
|
||||||
if (!versionMatch) {
|
if (!versionMatch) {
|
||||||
@@ -95,7 +95,7 @@ function calculateNewVersion(currentVersion: string, versionType: VersionType):
|
|||||||
* @param projectType The project type to determine which file to read
|
* @param projectType The project type to determine which file to read
|
||||||
* @returns The current version string
|
* @returns The current version string
|
||||||
*/
|
*/
|
||||||
async function readCurrentVersion(projectType: ProjectType): Promise<string> {
|
export async function readCurrentVersion(projectType: ProjectType): Promise<string> {
|
||||||
if (projectType === 'npm' || projectType === 'both') {
|
if (projectType === 'npm' || projectType === 'both') {
|
||||||
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
||||||
const content = (await plugins.smartfs
|
const content = (await plugins.smartfs
|
||||||
@@ -128,7 +128,7 @@ async function readCurrentVersion(projectType: ProjectType): Promise<string> {
|
|||||||
* @param filePath Path to the JSON file
|
* @param filePath Path to the JSON file
|
||||||
* @param newVersion The new version to write
|
* @param newVersion The new version to write
|
||||||
*/
|
*/
|
||||||
async function updateVersionFile(filePath: string, newVersion: string): Promise<void> {
|
export async function updateVersionFile(filePath: string, newVersion: string): Promise<void> {
|
||||||
const content = (await plugins.smartfs
|
const content = (await plugins.smartfs
|
||||||
.file(filePath)
|
.file(filePath)
|
||||||
.encoding('utf8')
|
.encoding('utf8')
|
||||||
@@ -141,6 +141,30 @@ async function updateVersionFile(filePath: string, newVersion: string): Promise<
|
|||||||
.write(JSON.stringify(config, null, 2) + '\n');
|
.write(JSON.stringify(config, null, 2) + '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates project version files without creating commits or tags.
|
||||||
|
*/
|
||||||
|
export async function updateProjectVersionFiles(
|
||||||
|
projectType: ProjectType,
|
||||||
|
newVersion: string,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const filesToUpdate: string[] = [];
|
||||||
|
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
||||||
|
const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
|
||||||
|
|
||||||
|
if (projectType === 'npm' || projectType === 'both') {
|
||||||
|
await updateVersionFile(packageJsonPath, newVersion);
|
||||||
|
filesToUpdate.push('package.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectType === 'deno' || projectType === 'both') {
|
||||||
|
await updateVersionFile(denoJsonPath, newVersion);
|
||||||
|
filesToUpdate.push('deno.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
return filesToUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bumps the project version based on project type
|
* Bumps the project version based on project type
|
||||||
* Handles npm-only, deno-only, and dual projects with unified logic
|
* Handles npm-only, deno-only, and dual projects with unified logic
|
||||||
@@ -182,19 +206,7 @@ export async function bumpProjectVersion(
|
|||||||
logger.log('info', `Bumping version: ${currentVersion} → ${newVersion}`);
|
logger.log('info', `Bumping version: ${currentVersion} → ${newVersion}`);
|
||||||
|
|
||||||
// 3. Determine which files to update
|
// 3. Determine which files to update
|
||||||
const filesToUpdate: string[] = [];
|
const filesToUpdate = await updateProjectVersionFiles(projectType, newVersion);
|
||||||
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
|
||||||
const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
|
|
||||||
|
|
||||||
if (projectType === 'npm' || projectType === 'both') {
|
|
||||||
await updateVersionFile(packageJsonPath, newVersion);
|
|
||||||
filesToUpdate.push('package.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (projectType === 'deno' || projectType === 'both') {
|
|
||||||
await updateVersionFile(denoJsonPath, newVersion);
|
|
||||||
filesToUpdate.push('deno.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Stage all updated files
|
// 4. Stage all updated files
|
||||||
await smartshellInstance.exec(`git add ${filesToUpdate.join(' ')}`);
|
await smartshellInstance.exec(`git add ${filesToUpdate.join(' ')}`);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface ICommitSummary {
|
|||||||
commitType: string;
|
commitType: string;
|
||||||
commitScope: string;
|
commitScope: string;
|
||||||
commitMessage: string;
|
commitMessage: string;
|
||||||
newVersion: string;
|
newVersion?: string;
|
||||||
commitSha?: string;
|
commitSha?: string;
|
||||||
pushed: boolean;
|
pushed: boolean;
|
||||||
repoUrl?: string;
|
repoUrl?: string;
|
||||||
@@ -197,9 +197,14 @@ export function printSummary(summary: ICommitSummary): void {
|
|||||||
`Branch: 🌿 ${summary.branch}`,
|
`Branch: 🌿 ${summary.branch}`,
|
||||||
`Commit Type: ${getCommitTypeEmoji(summary.commitType)}`,
|
`Commit Type: ${getCommitTypeEmoji(summary.commitType)}`,
|
||||||
`Scope: 📍 ${summary.commitScope}`,
|
`Scope: 📍 ${summary.commitScope}`,
|
||||||
`New Version: 🏷️ v${summary.newVersion}`,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (summary.newVersion) {
|
||||||
|
lines.push(`New Version: 🏷️ v${summary.newVersion}`);
|
||||||
|
} else {
|
||||||
|
lines.push(`Version: ⊘ Not bumped`);
|
||||||
|
}
|
||||||
|
|
||||||
if (summary.commitSha) {
|
if (summary.commitSha) {
|
||||||
lines.push(`Commit SHA: 📌 ${summary.commitSha}`);
|
lines.push(`Commit SHA: 📌 ${summary.commitSha}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import * as plugins from './mod.plugins.js';
|
|||||||
export interface ICommitConfig {
|
export interface ICommitConfig {
|
||||||
alwaysTest: boolean;
|
alwaysTest: boolean;
|
||||||
alwaysBuild: boolean;
|
alwaysBuild: boolean;
|
||||||
|
confirmation: 'prompt' | 'auto' | 'plan';
|
||||||
|
steps: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,7 +17,7 @@ export class CommitConfig {
|
|||||||
|
|
||||||
constructor(cwd: string = process.cwd()) {
|
constructor(cwd: string = process.cwd()) {
|
||||||
this.cwd = cwd;
|
this.cwd = cwd;
|
||||||
this.config = { alwaysTest: false, alwaysBuild: false };
|
this.config = { alwaysTest: false, alwaysBuild: false, confirmation: 'prompt', steps: ['analyze', 'changelog', 'commit'] };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,9 +36,19 @@ export class CommitConfig {
|
|||||||
const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
|
const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
|
||||||
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
|
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
|
||||||
|
|
||||||
|
const alwaysTest = gitzoneConfig?.commit?.alwaysTest ?? false;
|
||||||
|
const alwaysBuild = gitzoneConfig?.commit?.alwaysBuild ?? false;
|
||||||
this.config = {
|
this.config = {
|
||||||
alwaysTest: gitzoneConfig?.commit?.alwaysTest ?? false,
|
alwaysTest,
|
||||||
alwaysBuild: gitzoneConfig?.commit?.alwaysBuild ?? false,
|
alwaysBuild,
|
||||||
|
confirmation: gitzoneConfig?.commit?.confirmation ?? 'prompt',
|
||||||
|
steps: gitzoneConfig?.commit?.steps || [
|
||||||
|
'analyze',
|
||||||
|
...(alwaysTest ? ['test'] : []),
|
||||||
|
...(alwaysBuild ? ['build'] : []),
|
||||||
|
'changelog',
|
||||||
|
'commit',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +78,8 @@ export class CommitConfig {
|
|||||||
// Update commit settings
|
// Update commit settings
|
||||||
smartconfigData['@git.zone/cli'].commit.alwaysTest = this.config.alwaysTest;
|
smartconfigData['@git.zone/cli'].commit.alwaysTest = this.config.alwaysTest;
|
||||||
smartconfigData['@git.zone/cli'].commit.alwaysBuild = this.config.alwaysBuild;
|
smartconfigData['@git.zone/cli'].commit.alwaysBuild = this.config.alwaysBuild;
|
||||||
|
smartconfigData['@git.zone/cli'].commit.confirmation = this.config.confirmation;
|
||||||
|
smartconfigData['@git.zone/cli'].commit.steps = this.config.steps;
|
||||||
|
|
||||||
// Write back to file
|
// Write back to file
|
||||||
await plugins.smartfs
|
await plugins.smartfs
|
||||||
@@ -101,4 +115,20 @@ export class CommitConfig {
|
|||||||
public setAlwaysBuild(value: boolean): void {
|
public setAlwaysBuild(value: boolean): void {
|
||||||
this.config.alwaysBuild = value;
|
this.config.alwaysBuild = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getConfirmation(): 'prompt' | 'auto' | 'plan' {
|
||||||
|
return this.config.confirmation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setConfirmation(value: 'prompt' | 'auto' | 'plan'): void {
|
||||||
|
this.config.confirmation = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSteps(): string[] {
|
||||||
|
return [...this.config.steps];
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSteps(steps: string[]): void {
|
||||||
|
this.config.steps = [...steps];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,13 +35,11 @@ export class ReleaseConfig {
|
|||||||
public async load(): Promise<void> {
|
public async load(): Promise<void> {
|
||||||
const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
|
const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
|
||||||
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
|
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
|
||||||
|
const npmTarget = gitzoneConfig?.release?.targets?.npm || {};
|
||||||
// Also check szci for backward compatibility
|
|
||||||
const szciConfig = smartconfigInstance.dataFor<any>('@ship.zone/szci', {});
|
|
||||||
|
|
||||||
this.config = {
|
this.config = {
|
||||||
registries: gitzoneConfig?.release?.registries || [],
|
registries: npmTarget.registries || [],
|
||||||
accessLevel: gitzoneConfig?.release?.accessLevel || szciConfig?.npmAccessLevel || 'public',
|
accessLevel: npmTarget.accessLevel || 'public',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,9 +66,18 @@ export class ReleaseConfig {
|
|||||||
smartconfigData['@git.zone/cli'].release = {};
|
smartconfigData['@git.zone/cli'].release = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!smartconfigData['@git.zone/cli'].release.targets) {
|
||||||
|
smartconfigData['@git.zone/cli'].release.targets = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!smartconfigData['@git.zone/cli'].release.targets.npm) {
|
||||||
|
smartconfigData['@git.zone/cli'].release.targets.npm = {};
|
||||||
|
}
|
||||||
|
|
||||||
// Update registries and accessLevel
|
// Update registries and accessLevel
|
||||||
smartconfigData['@git.zone/cli'].release.registries = this.config.registries;
|
smartconfigData['@git.zone/cli'].release.targets.npm.enabled = this.config.registries.length > 0;
|
||||||
smartconfigData['@git.zone/cli'].release.accessLevel = this.config.accessLevel;
|
smartconfigData['@git.zone/cli'].release.targets.npm.registries = this.config.registries;
|
||||||
|
smartconfigData['@git.zone/cli'].release.targets.npm.accessLevel = this.config.accessLevel;
|
||||||
|
|
||||||
// Write back to file
|
// Write back to file
|
||||||
await plugins.smartfs
|
await plugins.smartfs
|
||||||
|
|||||||
+1443
-50
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,10 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from './mod.plugins.js';
|
||||||
import { FormatContext } from './classes.formatcontext.js';
|
import { FormatContext } from './classes.formatcontext.js';
|
||||||
import type { IPlannedChange, ICheckResult } from './interfaces.format.js';
|
import type {
|
||||||
|
IPlannedChange,
|
||||||
|
ICheckResult,
|
||||||
|
IFormatWarning,
|
||||||
|
} from './interfaces.format.js';
|
||||||
import { Project } from '../classes.project.js';
|
import { Project } from '../classes.project.js';
|
||||||
import { FormatStats } from './classes.formatstats.js';
|
import { FormatStats } from './classes.formatstats.js';
|
||||||
|
|
||||||
@@ -19,6 +23,14 @@ export abstract class BaseFormatter {
|
|||||||
abstract analyze(): Promise<IPlannedChange[]>;
|
abstract analyze(): Promise<IPlannedChange[]>;
|
||||||
abstract applyChange(change: IPlannedChange): Promise<void>;
|
abstract applyChange(change: IPlannedChange): Promise<void>;
|
||||||
|
|
||||||
|
get runsWithoutChanges(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(): Promise<IFormatWarning[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
async execute(changes: IPlannedChange[]): Promise<void> {
|
async execute(changes: IPlannedChange[]): Promise<void> {
|
||||||
const startTime = this.stats.moduleStartTime(this.name);
|
const startTime = this.stats.moduleStartTime(this.name);
|
||||||
this.stats.startModule(this.name);
|
this.stats.startModule(this.name);
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from './mod.plugins.js';
|
||||||
import { FormatContext } from './classes.formatcontext.js';
|
import { FormatContext } from './classes.formatcontext.js';
|
||||||
import { BaseFormatter } from './classes.baseformatter.js';
|
import { BaseFormatter } from './classes.baseformatter.js';
|
||||||
import type { IFormatPlan, IPlannedChange } from './interfaces.format.js';
|
import type {
|
||||||
|
IFormatPlan,
|
||||||
|
IPlannedChange,
|
||||||
|
IFormatWarning,
|
||||||
|
} from './interfaces.format.js';
|
||||||
import { getModuleIcon } from './interfaces.format.js';
|
import { getModuleIcon } from './interfaces.format.js';
|
||||||
import { logger } from '../gitzone.logging.js';
|
import { logger } from '../gitzone.logging.js';
|
||||||
import { DiffReporter } from './classes.diffreporter.js';
|
import { DiffReporter } from './classes.diffreporter.js';
|
||||||
@@ -42,15 +46,21 @@ export class FormatPlanner {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const warnings = await module.validate();
|
||||||
|
plan.warnings.push(...warnings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
plan.warnings.push({
|
plan.warnings.push({
|
||||||
level: 'error',
|
level: 'error',
|
||||||
message: `Failed to analyze module ${module.name}: ${error.message}`,
|
message: `Failed to analyze module ${module.name}: ${errorMessage}`,
|
||||||
module: module.name,
|
module: module.name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plan.warnings.push(...this.detectConflictingChanges(plan.changes));
|
||||||
|
|
||||||
plan.summary.totalFiles =
|
plan.summary.totalFiles =
|
||||||
plan.summary.filesAdded +
|
plan.summary.filesAdded +
|
||||||
plan.summary.filesModified +
|
plan.summary.filesModified +
|
||||||
@@ -65,11 +75,12 @@ export class FormatPlanner {
|
|||||||
context: FormatContext,
|
context: FormatContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
const changesByModule = this.groupChangesByModule(plan.changes);
|
||||||
|
|
||||||
for (const module of modules) {
|
for (const module of modules) {
|
||||||
const changes = this.plannedChanges.get(module.name) || [];
|
const changes = changesByModule.get(module.name) || [];
|
||||||
|
|
||||||
if (changes.length > 0) {
|
if (changes.length > 0 || module.runsWithoutChanges) {
|
||||||
logger.log('info', `Executing ${module.name} formatter...`);
|
logger.log('info', `Executing ${module.name} formatter...`);
|
||||||
await module.execute(changes);
|
await module.execute(changes);
|
||||||
}
|
}
|
||||||
@@ -138,4 +149,55 @@ export class FormatPlanner {
|
|||||||
return '❌';
|
return '❌';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private groupChangesByModule(
|
||||||
|
changes: IPlannedChange[],
|
||||||
|
): Map<string, IPlannedChange[]> {
|
||||||
|
const changesByModule = new Map<string, IPlannedChange[]>();
|
||||||
|
for (const change of changes) {
|
||||||
|
const moduleChanges = changesByModule.get(change.module) || [];
|
||||||
|
moduleChanges.push(change);
|
||||||
|
changesByModule.set(change.module, moduleChanges);
|
||||||
|
}
|
||||||
|
return changesByModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
private detectConflictingChanges(
|
||||||
|
changes: IPlannedChange[],
|
||||||
|
): IFormatWarning[] {
|
||||||
|
const warnings: IFormatWarning[] = [];
|
||||||
|
const changesByPath = new Map<string, IPlannedChange[]>();
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
if (!change.path || change.path === '<various files>') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathChanges = changesByPath.get(change.path) || [];
|
||||||
|
pathChanges.push(change);
|
||||||
|
changesByPath.set(change.path, pathChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [path, pathChanges] of changesByPath) {
|
||||||
|
const modules = [...new Set(pathChanges.map((change) => change.module))];
|
||||||
|
if (modules.length < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDelete = pathChanges.some((change) => change.type === 'delete');
|
||||||
|
const plannedContents = pathChanges
|
||||||
|
.map((change) => change.content)
|
||||||
|
.filter((content): content is string => content !== undefined);
|
||||||
|
const uniqueContents = new Set(plannedContents);
|
||||||
|
const level = hasDelete || uniqueContents.size > 1 ? 'warning' : 'info';
|
||||||
|
|
||||||
|
warnings.push({
|
||||||
|
level,
|
||||||
|
module: 'planner',
|
||||||
|
message: `Multiple formatters plan changes for ${path}: ${modules.join(', ')}. They will run in formatter order.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import type { IPlannedChange } from '../interfaces.format.js';
|
import type { IFormatWarning, IPlannedChange } from '../interfaces.format.js';
|
||||||
import * as plugins from '../mod.plugins.js';
|
import * as plugins from '../mod.plugins.js';
|
||||||
import * as paths from '../../paths.js';
|
import * as paths from '../../paths.js';
|
||||||
import { logger } from '../../gitzone.logging.js';
|
import { logger } from '../../gitzone.logging.js';
|
||||||
@@ -11,6 +11,10 @@ export class LicenseFormatter extends BaseFormatter {
|
|||||||
return 'license';
|
return 'license';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get runsWithoutChanges(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async analyze(): Promise<IPlannedChange[]> {
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
// License formatter only checks for incompatible licenses
|
// License formatter only checks for incompatible licenses
|
||||||
// It does not modify any files, so return empty array
|
// It does not modify any files, so return empty array
|
||||||
@@ -18,29 +22,34 @@ export class LicenseFormatter extends BaseFormatter {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async validate(): Promise<IFormatWarning[]> {
|
||||||
|
const result = await this.checkLicenses();
|
||||||
|
if (!result || result.failingModules.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
level: 'error',
|
||||||
|
module: this.name,
|
||||||
|
message: `License check failed for ${result.failingModules.length} module(s): ${result.failingModules
|
||||||
|
.map((failedModule) => `${failedModule.name} (${failedModule.license})`)
|
||||||
|
.join(', ')}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
async execute(changes: IPlannedChange[]): Promise<void> {
|
async execute(changes: IPlannedChange[]): Promise<void> {
|
||||||
const startTime = this.stats.moduleStartTime(this.name);
|
const startTime = this.stats.moduleStartTime(this.name);
|
||||||
this.stats.startModule(this.name);
|
this.stats.startModule(this.name);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if node_modules exists
|
const licenseCheckResult = await this.checkLicenses();
|
||||||
const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
|
if (!licenseCheckResult) {
|
||||||
const nodeModulesExists = await plugins.smartfs
|
|
||||||
.directory(nodeModulesPath)
|
|
||||||
.exists();
|
|
||||||
|
|
||||||
if (!nodeModulesExists) {
|
|
||||||
logger.log('warn', 'No node_modules found. Skipping license check');
|
logger.log('warn', 'No node_modules found. Skipping license check');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run license check
|
|
||||||
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
|
|
||||||
const licenseCheckResult = await licenseChecker.excludeLicenseWithinPath(
|
|
||||||
paths.cwd,
|
|
||||||
INCOMPATIBLE_LICENSES,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (licenseCheckResult.failingModules.length === 0) {
|
if (licenseCheckResult.failingModules.length === 0) {
|
||||||
logger.log('info', 'License check passed - no incompatible licenses found');
|
logger.log('info', 'License check passed - no incompatible licenses found');
|
||||||
} else {
|
} else {
|
||||||
@@ -59,4 +68,23 @@ export class LicenseFormatter extends BaseFormatter {
|
|||||||
async applyChange(change: IPlannedChange): Promise<void> {
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
// No file changes for license formatter
|
// No file changes for license formatter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async checkLicenses(): Promise<{
|
||||||
|
failingModules: Array<{ name: string; license: string }>;
|
||||||
|
} | undefined> {
|
||||||
|
const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
|
||||||
|
const nodeModulesExists = await plugins.smartfs
|
||||||
|
.directory(nodeModulesPath)
|
||||||
|
.exists();
|
||||||
|
|
||||||
|
if (!nodeModulesExists) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
|
||||||
|
return await licenseChecker.excludeLicenseWithinPath(
|
||||||
|
paths.cwd,
|
||||||
|
INCOMPATIBLE_LICENSES,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
);
|
);
|
||||||
allFiles.push(...filteredFiles);
|
allFiles.push(...filteredFiles);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logVerbose(`Skipping directory ${dir}: ${error.message}`);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logVerbose(`Skipping directory ${dir}: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +73,8 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
|
const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
|
||||||
allFiles.push(...rootLevelFiles);
|
allFiles.push(...rootLevelFiles);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logVerbose(`Skipping pattern ${pattern}: ${error.message}`);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logVerbose(`Skipping pattern ${pattern}: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,20 +91,46 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Skip files that can't be accessed
|
// Skip files that can't be accessed
|
||||||
logVerbose(`Skipping ${file} - cannot access: ${error.message}`);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logVerbose(`Skipping ${file} - cannot access: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prettier = await import('prettier');
|
||||||
|
const prettierConfig = await this.getPrettierConfig();
|
||||||
|
|
||||||
for (const file of validFiles) {
|
for (const file of validFiles) {
|
||||||
|
try {
|
||||||
|
const fileExt = plugins.path.extname(file).toLowerCase();
|
||||||
|
if (!fileExt) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = (await plugins.smartfs
|
||||||
|
.file(file)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
const formatted = await prettier.format(content, {
|
||||||
|
filepath: file,
|
||||||
|
...prettierConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (formatted !== content) {
|
||||||
changes.push({
|
changes.push({
|
||||||
type: 'modify',
|
type: 'modify',
|
||||||
path: file,
|
path: file,
|
||||||
module: this.name,
|
module: this.name,
|
||||||
description: 'Format with Prettier',
|
description: 'Format with Prettier',
|
||||||
|
content: formatted,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logVerbose(`Skipping Prettier analysis for ${file}: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.log('info', `Found ${changes.length} files to format with Prettier`);
|
logger.log('info', `Found ${changes.length} files needing Prettier`);
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,9 +155,10 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
this.stats.recordFileOperation(this.name, change.type, true);
|
this.stats.recordFileOperation(this.name, change.type, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.stats.recordFileOperation(this.name, change.type, false);
|
this.stats.recordFileOperation(this.name, change.type, false);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
logger.log(
|
logger.log(
|
||||||
'error',
|
'error',
|
||||||
`Failed to format ${change.path}: ${error.message}`,
|
`Failed to format ${change.path}: ${errorMessage}`,
|
||||||
);
|
);
|
||||||
// Don't throw - continue with other files
|
// Don't throw - continue with other files
|
||||||
}
|
}
|
||||||
@@ -192,28 +221,32 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
logVerbose(`No formatting changes for ${change.path}`);
|
logVerbose(`No formatting changes for ${change.path}`);
|
||||||
}
|
}
|
||||||
} catch (prettierError) {
|
} catch (prettierError) {
|
||||||
|
const prettierErrorMessage = prettierError instanceof Error
|
||||||
|
? prettierError.message
|
||||||
|
: String(prettierError);
|
||||||
// Check if it's a parser error
|
// Check if it's a parser error
|
||||||
if (
|
if (prettierErrorMessage.includes('No parser could be inferred')) {
|
||||||
prettierError.message &&
|
logVerbose(`Skipping ${change.path} - ${prettierErrorMessage}`);
|
||||||
prettierError.message.includes('No parser could be inferred')
|
|
||||||
) {
|
|
||||||
logVerbose(`Skipping ${change.path} - ${prettierError.message}`);
|
|
||||||
return; // Skip this file silently
|
return; // Skip this file silently
|
||||||
}
|
}
|
||||||
throw prettierError;
|
throw prettierError;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
const errorStack = error instanceof Error ? error.stack : undefined;
|
||||||
// Log the full error stack for debugging mkdir issues
|
// Log the full error stack for debugging mkdir issues
|
||||||
if (error.message && error.message.includes('mkdir')) {
|
if (errorMessage.includes('mkdir')) {
|
||||||
logger.log(
|
logger.log(
|
||||||
'error',
|
'error',
|
||||||
`Failed to format ${change.path}: ${error.message}`,
|
`Failed to format ${change.path}: ${errorMessage}`,
|
||||||
);
|
);
|
||||||
logger.log('error', `Error stack: ${error.stack}`);
|
if (errorStack) {
|
||||||
|
logger.log('error', `Error stack: ${errorStack}`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.log(
|
logger.log(
|
||||||
'error',
|
'error',
|
||||||
`Failed to format ${change.path}: ${error.message}`,
|
`Failed to format ${change.path}: ${errorMessage}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@@ -234,52 +267,7 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Override check() to compute diffs on-the-fly by running prettier
|
|
||||||
*/
|
|
||||||
async check(): Promise<ICheckResult> {
|
async check(): Promise<ICheckResult> {
|
||||||
const changes = await this.analyze();
|
return await super.check();
|
||||||
const diffs: ICheckResult['diffs'] = [];
|
|
||||||
|
|
||||||
for (const change of changes) {
|
|
||||||
if (change.type !== 'modify') continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Read current content
|
|
||||||
const currentContent = (await plugins.smartfs
|
|
||||||
.file(change.path)
|
|
||||||
.encoding('utf8')
|
|
||||||
.read()) as string;
|
|
||||||
|
|
||||||
// Skip files without extension (prettier can't infer parser)
|
|
||||||
const fileExt = plugins.path.extname(change.path).toLowerCase();
|
|
||||||
if (!fileExt) continue;
|
|
||||||
|
|
||||||
// Format with prettier to get what it would produce
|
|
||||||
const prettier = await import('prettier');
|
|
||||||
const formatted = await prettier.format(currentContent, {
|
|
||||||
filepath: change.path,
|
|
||||||
...(await this.getPrettierConfig()),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only add to diffs if content differs
|
|
||||||
if (formatted !== currentContent) {
|
|
||||||
diffs.push({
|
|
||||||
path: change.path,
|
|
||||||
type: 'modify',
|
|
||||||
before: currentContent,
|
|
||||||
after: formatted,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Skip files that can't be processed
|
|
||||||
logVerbose(`Skipping diff for ${change.path}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasDiff: diffs.length > 0,
|
|
||||||
diffs,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,65 +2,7 @@ import { BaseFormatter } from "../classes.baseformatter.js";
|
|||||||
import type { IPlannedChange } from "../interfaces.format.js";
|
import type { IPlannedChange } from "../interfaces.format.js";
|
||||||
import * as plugins from "../mod.plugins.js";
|
import * as plugins from "../mod.plugins.js";
|
||||||
import { logger, logVerbose } from "../../gitzone.logging.js";
|
import { logger, logVerbose } from "../../gitzone.logging.js";
|
||||||
|
import { migrateSmartconfigData } from "../../helpers.smartconfigmigrations.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";
|
const CONFIG_FILE = ".smartconfig.json";
|
||||||
|
|
||||||
@@ -88,9 +30,7 @@ export class SmartconfigFormatter extends BaseFormatter {
|
|||||||
|
|
||||||
const smartconfigJson = JSON.parse(currentContent);
|
const smartconfigJson = JSON.parse(currentContent);
|
||||||
|
|
||||||
// Apply key migrations
|
migrateSmartconfigData(smartconfigJson);
|
||||||
migrateNamespaceKeys(smartconfigJson);
|
|
||||||
migrateAccessLevel(smartconfigJson);
|
|
||||||
|
|
||||||
// Ensure namespaces exist
|
// Ensure namespaces exist
|
||||||
if (!smartconfigJson["@git.zone/cli"]) {
|
if (!smartconfigJson["@git.zone/cli"]) {
|
||||||
|
|||||||
+289
-8
@@ -22,6 +22,7 @@ import { TsconfigFormatter } from "./formatters/tsconfig.formatter.js";
|
|||||||
import { PrettierFormatter } from "./formatters/prettier.formatter.js";
|
import { PrettierFormatter } from "./formatters/prettier.formatter.js";
|
||||||
import { ReadmeFormatter } from "./formatters/readme.formatter.js";
|
import { ReadmeFormatter } from "./formatters/readme.formatter.js";
|
||||||
import { CopyFormatter } from "./formatters/copy.formatter.js";
|
import { CopyFormatter } from "./formatters/copy.formatter.js";
|
||||||
|
import type { ICheckResult, IFormatPlan } from "./interfaces.format.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rename npmextra.json or smartconfig.json to .smartconfig.json
|
* Rename npmextra.json or smartconfig.json to .smartconfig.json
|
||||||
@@ -94,9 +95,39 @@ const getFormatConfig = async () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeModuleList = (value: unknown): string[] => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.flatMap((item) => normalizeModuleList(item));
|
||||||
|
}
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
.split(",")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlanStatus = (plan: IFormatPlan) => {
|
||||||
|
const errorWarnings = plan.warnings.filter(
|
||||||
|
(warning) => warning.level === "error",
|
||||||
|
);
|
||||||
|
const hasChanges = plan.summary.totalFiles > 0;
|
||||||
|
const hasErrors = errorWarnings.length > 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: !hasChanges && !hasErrors,
|
||||||
|
hasChanges,
|
||||||
|
hasErrors,
|
||||||
|
errorCount: errorWarnings.length,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const createActiveFormatters = async (options: {
|
const createActiveFormatters = async (options: {
|
||||||
interactive: boolean;
|
interactive: boolean;
|
||||||
jsonOutput: boolean;
|
jsonOutput: boolean;
|
||||||
|
only?: string[];
|
||||||
|
skip?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
const project = await Project.fromCwd({ requireProjectType: false });
|
const project = await Project.fromCwd({ requireProjectType: false });
|
||||||
const context = new FormatContext(options);
|
const context = new FormatContext(options);
|
||||||
@@ -107,11 +138,19 @@ const createActiveFormatters = async (options: {
|
|||||||
([, FormatterClass]) => new FormatterClass(context, project),
|
([, FormatterClass]) => new FormatterClass(context, project),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onlyModules = options.only?.length
|
||||||
|
? options.only
|
||||||
|
: formatConfig.modules.only;
|
||||||
|
const skipModules = [
|
||||||
|
...formatConfig.modules.skip,
|
||||||
|
...(options.skip || []),
|
||||||
|
];
|
||||||
|
|
||||||
const activeFormatters = formatters.filter((formatter) => {
|
const activeFormatters = formatters.filter((formatter) => {
|
||||||
if (formatConfig.modules.only.length > 0) {
|
if (onlyModules.length > 0) {
|
||||||
return formatConfig.modules.only.includes(formatter.name);
|
return onlyModules.includes(formatter.name);
|
||||||
}
|
}
|
||||||
if (formatConfig.modules.skip.includes(formatter.name)) {
|
if (skipModules.includes(formatter.name)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -129,11 +168,15 @@ const buildFormatPlan = async (options: {
|
|||||||
fromPlan?: string;
|
fromPlan?: string;
|
||||||
interactive: boolean;
|
interactive: boolean;
|
||||||
jsonOutput: boolean;
|
jsonOutput: boolean;
|
||||||
|
only?: string[];
|
||||||
|
skip?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
const { context, planner, formatConfig, activeFormatters } =
|
const { context, planner, formatConfig, activeFormatters } =
|
||||||
await createActiveFormatters({
|
await createActiveFormatters({
|
||||||
interactive: options.interactive,
|
interactive: options.interactive,
|
||||||
jsonOutput: options.jsonOutput,
|
jsonOutput: options.jsonOutput,
|
||||||
|
only: options.only,
|
||||||
|
skip: options.skip,
|
||||||
});
|
});
|
||||||
|
|
||||||
const plan = options.fromPlan
|
const plan = options.fromPlan
|
||||||
@@ -167,6 +210,182 @@ const serializePlan = (plan: any) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildFormatFixPrompt = (
|
||||||
|
plan: IFormatPlan,
|
||||||
|
extraInstructions: string,
|
||||||
|
): string => {
|
||||||
|
const promptParts = [
|
||||||
|
"Other /c-* commands can be found at ~/.config/opencode/commands/*",
|
||||||
|
"# gitzone format fix",
|
||||||
|
"",
|
||||||
|
`Working directory: ${process.cwd()}`,
|
||||||
|
"",
|
||||||
|
"Repair project formatting so `gitzone format check --json` passes.",
|
||||||
|
"",
|
||||||
|
"Rules:",
|
||||||
|
"- Read `.smartconfig.json`, `package.json`, `tsconfig.json`, and the current format plan before editing.",
|
||||||
|
"- Prefer deterministic gitzone standards, bundled assets, and existing project conventions.",
|
||||||
|
"- Keep changes focused on formatting, metadata normalization, templates, and config consistency.",
|
||||||
|
"- Do not commit, release, install dependencies, or modify unrelated files.",
|
||||||
|
"- Use pnpm commands only if commands are needed.",
|
||||||
|
"- Run `gitzone format --write --yes` after changes.",
|
||||||
|
"- Run `gitzone format check --json` after changes and keep fixing until it passes.",
|
||||||
|
"- Run `git diff --check` after changes to catch whitespace problems.",
|
||||||
|
"",
|
||||||
|
"Current format plan:",
|
||||||
|
JSON.stringify(serializePlan(plan), null, 2),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (extraInstructions) {
|
||||||
|
promptParts.push("", "Additional user instructions:", extraInstructions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promptParts.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormatFix = async (
|
||||||
|
options: Record<string, any>,
|
||||||
|
mode: ICliMode,
|
||||||
|
): Promise<void> => {
|
||||||
|
if (mode.json) {
|
||||||
|
printJson({
|
||||||
|
ok: false,
|
||||||
|
error:
|
||||||
|
"JSON output is not supported for `gitzone format fix`. Use `gitzone format check --json` for machine-readable diagnostics.",
|
||||||
|
});
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraInstructions = (options._?.slice(2).join(" ") || "").trim();
|
||||||
|
const force = Boolean(options.force);
|
||||||
|
const autoApprove = Boolean(options.yes || mode.yes);
|
||||||
|
const formatConfig = await getFormatConfig();
|
||||||
|
const interactive =
|
||||||
|
options.interactive ?? (mode.interactive && formatConfig.interactive);
|
||||||
|
const only = normalizeModuleList(options.only);
|
||||||
|
const skip = normalizeModuleList(options.skip);
|
||||||
|
|
||||||
|
const buildCurrentPlan = async () => {
|
||||||
|
return await buildFormatPlan({
|
||||||
|
interactive,
|
||||||
|
jsonOutput: false,
|
||||||
|
only,
|
||||||
|
skip,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.log("info", "Analyzing project for format fixes...");
|
||||||
|
let { plan } = await buildCurrentPlan();
|
||||||
|
let status = getPlanStatus(plan);
|
||||||
|
|
||||||
|
if (status.ok && !extraInstructions && !force) {
|
||||||
|
logger.log(
|
||||||
|
"success",
|
||||||
|
"Format check found no issues. Use `gitzone format fix --force` to run opencode anyway.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!autoApprove) {
|
||||||
|
if (!mode.interactive) {
|
||||||
|
throw new Error(
|
||||||
|
"Format fix requires an interactive terminal or `-y` to run non-interactively.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||||
|
`Run format fixes? (${plan.summary.totalFiles} planned change(s), ${status.errorCount} error warning(s))`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
logger.log("info", "Format fix cancelled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.hasChanges) {
|
||||||
|
logger.log("info", "Applying deterministic format changes first...");
|
||||||
|
await run({
|
||||||
|
_: ["format"],
|
||||||
|
write: true,
|
||||||
|
yes: true,
|
||||||
|
interactive: false,
|
||||||
|
verbose: options.verbose,
|
||||||
|
detailed: options.detailed,
|
||||||
|
only: options.only,
|
||||||
|
skip: options.skip,
|
||||||
|
});
|
||||||
|
|
||||||
|
({ plan } = await buildCurrentPlan());
|
||||||
|
status = getPlanStatus(plan);
|
||||||
|
if (status.ok && !extraInstructions && !force) {
|
||||||
|
logger.log("success", "Format fix completed successfully.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const opencodeArgs = [
|
||||||
|
"run",
|
||||||
|
"--title",
|
||||||
|
"gitzone format fix",
|
||||||
|
"--dir",
|
||||||
|
process.cwd(),
|
||||||
|
];
|
||||||
|
if (autoApprove) {
|
||||||
|
opencodeArgs.push("--dangerously-skip-permissions");
|
||||||
|
}
|
||||||
|
opencodeArgs.push(buildFormatFixPrompt(plan, extraInstructions));
|
||||||
|
|
||||||
|
logger.log("info", "Starting opencode format fix...");
|
||||||
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
|
executor: "bash",
|
||||||
|
sourceFilePaths: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
let result: plugins.smartshell.IExecResult;
|
||||||
|
try {
|
||||||
|
result = await smartshellInstance.execSpawn("opencode", opencodeArgs, {
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to run opencode: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", `opencode exited with code ${result.exitCode}`);
|
||||||
|
process.exitCode = result.exitCode || 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("info", "Running deterministic format pass after opencode...");
|
||||||
|
await run({
|
||||||
|
_: ["format"],
|
||||||
|
write: true,
|
||||||
|
yes: true,
|
||||||
|
interactive: false,
|
||||||
|
verbose: options.verbose,
|
||||||
|
detailed: options.detailed,
|
||||||
|
only: options.only,
|
||||||
|
skip: options.skip,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { planner: finalPlanner, plan: finalPlan } = await buildCurrentPlan();
|
||||||
|
await finalPlanner.displayPlan(finalPlan, options.detailed);
|
||||||
|
const finalStatus = getPlanStatus(finalPlan);
|
||||||
|
if (finalStatus.ok) {
|
||||||
|
logger.log("success", "Format fix completed successfully.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
"error",
|
||||||
|
`Format fix left ${finalPlan.summary.totalFiles} planned change(s) and ${finalStatus.errorCount} error warning(s).`,
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
};
|
||||||
|
|
||||||
export let run = async (
|
export let run = async (
|
||||||
options: {
|
options: {
|
||||||
write?: boolean;
|
write?: boolean;
|
||||||
@@ -194,8 +413,25 @@ export let run = async (
|
|||||||
setVerboseMode(true);
|
setVerboseMode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subcommand === "fix") {
|
||||||
|
await handleFormatFix(options, mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const shouldWrite = options.write ?? options.dryRun === false;
|
const shouldWrite = options.write ?? options.dryRun === false;
|
||||||
const treatAsPlan = subcommand === "plan";
|
const treatAsPlan = subcommand === "plan";
|
||||||
|
const treatAsCheck = subcommand === "check" || Boolean(options.check);
|
||||||
|
|
||||||
|
if (treatAsCheck && shouldWrite) {
|
||||||
|
const error = "`gitzone format check` is read-only and cannot be combined with --write.";
|
||||||
|
if (mode.json) {
|
||||||
|
printJson({ ok: false, error });
|
||||||
|
} else {
|
||||||
|
logger.log("error", error);
|
||||||
|
}
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (mode.json && shouldWrite) {
|
if (mode.json && shouldWrite) {
|
||||||
printJson({
|
printJson({
|
||||||
@@ -212,7 +448,9 @@ export let run = async (
|
|||||||
const formatConfig = await getFormatConfig();
|
const formatConfig = await getFormatConfig();
|
||||||
const interactive =
|
const interactive =
|
||||||
options.interactive ?? (mode.interactive && formatConfig.interactive);
|
options.interactive ?? (mode.interactive && formatConfig.interactive);
|
||||||
const autoApprove = options.yes ?? formatConfig.autoApprove;
|
const autoApprove = options.yes ?? (mode.yes || formatConfig.autoApprove);
|
||||||
|
const only = normalizeModuleList(options.only);
|
||||||
|
const skip = normalizeModuleList(options.skip);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const planBuilder = async () => {
|
const planBuilder = async () => {
|
||||||
@@ -220,6 +458,8 @@ export let run = async (
|
|||||||
fromPlan: options.fromPlan,
|
fromPlan: options.fromPlan,
|
||||||
interactive,
|
interactive,
|
||||||
jsonOutput: mode.json,
|
jsonOutput: mode.json,
|
||||||
|
only,
|
||||||
|
skip,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -231,7 +471,16 @@ export let run = async (
|
|||||||
: await planBuilder();
|
: await planBuilder();
|
||||||
|
|
||||||
if (mode.json) {
|
if (mode.json) {
|
||||||
printJson(serializePlan(plan));
|
const serializedPlan = serializePlan(plan);
|
||||||
|
if (treatAsCheck) {
|
||||||
|
const status = getPlanStatus(plan);
|
||||||
|
printJson({ ok: status.ok, ...serializedPlan });
|
||||||
|
if (!status.ok) {
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
printJson(serializedPlan);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +500,20 @@ export let run = async (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (treatAsCheck) {
|
||||||
|
const status = getPlanStatus(plan);
|
||||||
|
if (status.ok) {
|
||||||
|
logger.log("success", "Format check passed");
|
||||||
|
} else {
|
||||||
|
logger.log(
|
||||||
|
"error",
|
||||||
|
`Format check failed: ${plan.summary.totalFiles} planned change(s), ${status.errorCount} error warning(s)`,
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Show diffs if explicitly requested or before interactive write confirmation
|
// Show diffs if explicitly requested or before interactive write confirmation
|
||||||
const showDiffs =
|
const showDiffs =
|
||||||
options.diff || (shouldWrite && interactive && !autoApprove);
|
options.diff || (shouldWrite && interactive && !autoApprove);
|
||||||
@@ -314,7 +577,6 @@ export let run = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
import type { ICheckResult } from "./interfaces.format.js";
|
|
||||||
export type { ICheckResult };
|
export type { ICheckResult };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -363,7 +625,7 @@ export function showHelp(mode?: ICliMode): void {
|
|||||||
if (mode?.json) {
|
if (mode?.json) {
|
||||||
printJson({
|
printJson({
|
||||||
command: "format",
|
command: "format",
|
||||||
usage: "gitzone format [plan] [options]",
|
usage: "gitzone format [plan|check|fix] [options]",
|
||||||
description:
|
description:
|
||||||
"Plans formatting changes by default and applies them only with --write.",
|
"Plans formatting changes by default and applies them only with --write.",
|
||||||
flags: [
|
flags: [
|
||||||
@@ -393,19 +655,33 @@ export function showHelp(mode?: ICliMode): void {
|
|||||||
flag: "--diff",
|
flag: "--diff",
|
||||||
description: "Show per-file diffs before applying changes",
|
description: "Show per-file diffs before applying changes",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
flag: "--only <modules>",
|
||||||
|
description: "Run only the comma-separated formatter modules",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--skip <modules>",
|
||||||
|
description: "Skip the comma-separated formatter modules",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--force",
|
||||||
|
description: "Run `format fix` even when the deterministic plan is clean",
|
||||||
|
},
|
||||||
{ flag: "--json", description: "Emit a read-only format plan as JSON" },
|
{ flag: "--json", description: "Emit a read-only format plan as JSON" },
|
||||||
],
|
],
|
||||||
examples: [
|
examples: [
|
||||||
"gitzone format",
|
"gitzone format",
|
||||||
"gitzone format plan --json",
|
"gitzone format plan --json",
|
||||||
|
"gitzone format check",
|
||||||
"gitzone format --write --yes",
|
"gitzone format --write --yes",
|
||||||
|
"gitzone format fix",
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log("Usage: gitzone format [plan] [options]");
|
console.log("Usage: gitzone format [plan|check|fix] [options]");
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log(
|
console.log(
|
||||||
"Plans formatting changes by default and applies them only with --write.",
|
"Plans formatting changes by default and applies them only with --write.",
|
||||||
@@ -424,11 +700,16 @@ export function showHelp(mode?: ICliMode): void {
|
|||||||
console.log(
|
console.log(
|
||||||
" --diff Show per-file diffs before applying changes",
|
" --diff Show per-file diffs before applying changes",
|
||||||
);
|
);
|
||||||
|
console.log(" --only <modules> Run only comma-separated formatter modules");
|
||||||
|
console.log(" --skip <modules> Skip comma-separated formatter modules");
|
||||||
|
console.log(" --force Run format fix even when the plan is clean");
|
||||||
console.log(" --json Emit a read-only format plan as JSON");
|
console.log(" --json Emit a read-only format plan as JSON");
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log("Examples:");
|
console.log("Examples:");
|
||||||
console.log(" gitzone format");
|
console.log(" gitzone format");
|
||||||
console.log(" gitzone format plan --json");
|
console.log(" gitzone format plan --json");
|
||||||
|
console.log(" gitzone format check");
|
||||||
console.log(" gitzone format --write --yes");
|
console.log(" gitzone format --write --yes");
|
||||||
|
console.log(" gitzone format fix");
|
||||||
console.log("");
|
console.log("");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
export type IFormatWarning = {
|
||||||
|
level: 'info' | 'warning' | 'error';
|
||||||
|
message: string;
|
||||||
|
module: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type IFormatPlan = {
|
export type IFormatPlan = {
|
||||||
summary: {
|
summary: {
|
||||||
totalFiles: number;
|
totalFiles: number;
|
||||||
@@ -5,17 +11,8 @@ export type IFormatPlan = {
|
|||||||
filesModified: number;
|
filesModified: number;
|
||||||
filesRemoved: number;
|
filesRemoved: number;
|
||||||
};
|
};
|
||||||
changes: Array<{
|
changes: IPlannedChange[];
|
||||||
type: 'create' | 'modify' | 'delete';
|
warnings: IFormatWarning[];
|
||||||
path: string;
|
|
||||||
module: string;
|
|
||||||
description: string;
|
|
||||||
}>;
|
|
||||||
warnings: Array<{
|
|
||||||
level: 'info' | 'warning' | 'error';
|
|
||||||
message: string;
|
|
||||||
module: string;
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IPlannedChange = {
|
export type IPlannedChange = {
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
import * as plugins from "./mod.plugins.js";
|
||||||
|
import * as paths from "../paths.js";
|
||||||
|
import { logger } from "../gitzone.logging.js";
|
||||||
|
import type { ICliMode } from "../helpers.climode.js";
|
||||||
|
import { getCliMode, printJson } from "../helpers.climode.js";
|
||||||
|
import {
|
||||||
|
inferVersionTypeFromPending,
|
||||||
|
movePendingToVersion,
|
||||||
|
readPendingChangelog,
|
||||||
|
} from "../helpers.changelog.js";
|
||||||
|
import {
|
||||||
|
resolveReleaseWorkflow,
|
||||||
|
type IResolvedReleaseWorkflow,
|
||||||
|
} from "../helpers.workflow.js";
|
||||||
|
import * as commitHelpers from "../mod_commit/mod.helpers.js";
|
||||||
|
|
||||||
|
type TTargetStatus = "success" | "already-published" | "skipped" | "failed";
|
||||||
|
|
||||||
|
interface ITargetResult {
|
||||||
|
target: string;
|
||||||
|
status: TTargetStatus;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const run = async (argvArg: any) => {
|
||||||
|
const mode = await getCliMode(argvArg);
|
||||||
|
const subcommand = argvArg._?.[1];
|
||||||
|
|
||||||
|
if (mode.help || subcommand === "help") {
|
||||||
|
showHelp(mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode.json) {
|
||||||
|
printJson({
|
||||||
|
ok: false,
|
||||||
|
error: "JSON output is not supported for mutating release workflows yet. Use `gitzone release --plan` for a human-readable plan.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflow = await resolveReleaseWorkflow(argvArg);
|
||||||
|
printReleasePlan(workflow);
|
||||||
|
if (workflow.confirmation === "plan") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
|
executor: "bash",
|
||||||
|
sourceFilePaths: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const pending = await readPendingChangelog(
|
||||||
|
plugins.path.join(paths.cwd, workflow.changelogFile),
|
||||||
|
workflow.changelogPendingSection,
|
||||||
|
);
|
||||||
|
if (pending.isEmpty && !argvArg["allow-empty"] && !argvArg.allowEmpty) {
|
||||||
|
logger.log("error", "No pending changelog entries. Nothing to release.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionType = resolveVersionType(argvArg, pending.block);
|
||||||
|
const projectType = await commitHelpers.detectProjectType();
|
||||||
|
const currentVersion = await commitHelpers.readCurrentVersion(projectType);
|
||||||
|
const plannedVersion = commitHelpers.calculateNewVersion(currentVersion, versionType);
|
||||||
|
|
||||||
|
if (workflow.confirmation === "prompt") {
|
||||||
|
if (!mode.interactive) {
|
||||||
|
throw new Error("Release confirmation requires an interactive terminal. Use `-y` or set release.confirmation to `auto`.");
|
||||||
|
}
|
||||||
|
const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||||
|
`Release v${plannedVersion} (${versionType}) now?`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
logger.log("info", "Release cancelled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let newVersion = plannedVersion;
|
||||||
|
const gitResults: ITargetResult[] = [];
|
||||||
|
const npmResults: ITargetResult[] = [];
|
||||||
|
const dockerResults: ITargetResult[] = [];
|
||||||
|
|
||||||
|
if (workflow.requireCleanTree) {
|
||||||
|
await verifyCleanTree(smartshellInstance, "Working tree is not clean. Commit or stash changes before releasing.");
|
||||||
|
}
|
||||||
|
if (workflow.runTests) {
|
||||||
|
await runCommandStep(smartshellInstance, "Running tests", workflow.testCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
newVersion = await runVersionStep(projectType, versionType);
|
||||||
|
await runChangelogStep(workflow, newVersion);
|
||||||
|
await runReleaseCommitStep(smartshellInstance, newVersion);
|
||||||
|
await runTagStep(smartshellInstance, newVersion);
|
||||||
|
|
||||||
|
if (workflow.runBuild) {
|
||||||
|
await runCommandStep(smartshellInstance, "Running release build", workflow.buildCommand);
|
||||||
|
await verifyCleanTree(smartshellInstance, "Build produced uncommitted changes. Aborting release.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workflow.targets.includes("git")) {
|
||||||
|
gitResults.push(...(await runGitTarget(smartshellInstance, workflow)));
|
||||||
|
}
|
||||||
|
if (workflow.targets.includes("npm")) {
|
||||||
|
npmResults.push(...(await runNpmTarget(smartshellInstance, workflow)));
|
||||||
|
}
|
||||||
|
if (workflow.targets.includes("docker")) {
|
||||||
|
dockerResults.push(...(await runDockerTarget(smartshellInstance, workflow)));
|
||||||
|
}
|
||||||
|
|
||||||
|
printReleaseSummary(newVersion, gitResults, npmResults, dockerResults);
|
||||||
|
if ([...gitResults, ...npmResults, ...dockerResults].some((result) => result.status === "failed")) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveVersionType(argvArg: any, pendingBlock: string): commitHelpers.VersionType {
|
||||||
|
if (argvArg.major) return "major";
|
||||||
|
if (argvArg.minor) return "minor";
|
||||||
|
if (argvArg.patch) return "patch";
|
||||||
|
return inferVersionTypeFromPending(pendingBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommandStep(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
label: string,
|
||||||
|
command: string,
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(`\n${label}`);
|
||||||
|
const result = await smartshellInstance.exec(command);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", `${label} failed. Aborting release.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
logger.log("success", `${label} passed.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyCleanTree(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
errorMessage: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const statusResult = await smartshellInstance.exec("git status --porcelain");
|
||||||
|
if (statusResult.stdout.trim() !== "") {
|
||||||
|
logger.log("error", errorMessage);
|
||||||
|
console.log(statusResult.stdout);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runVersionStep(
|
||||||
|
projectType: commitHelpers.ProjectType,
|
||||||
|
versionType: commitHelpers.VersionType,
|
||||||
|
): Promise<string> {
|
||||||
|
const currentVersion = await commitHelpers.readCurrentVersion(projectType);
|
||||||
|
const newVersion = commitHelpers.calculateNewVersion(currentVersion, versionType);
|
||||||
|
logger.log("info", `Bumping version: ${currentVersion} -> ${newVersion}`);
|
||||||
|
|
||||||
|
const commitInfo = new plugins.commitinfo.CommitInfo(paths.cwd, versionType);
|
||||||
|
await commitInfo.writeIntoPotentialDirs();
|
||||||
|
await commitHelpers.updateProjectVersionFiles(projectType, newVersion);
|
||||||
|
return newVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runChangelogStep(
|
||||||
|
workflow: IResolvedReleaseWorkflow,
|
||||||
|
newVersion: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const dateString = new Date().toISOString().slice(0, 10);
|
||||||
|
await movePendingToVersion(
|
||||||
|
plugins.path.join(paths.cwd, workflow.changelogFile),
|
||||||
|
workflow.changelogPendingSection,
|
||||||
|
workflow.changelogVersionHeading,
|
||||||
|
newVersion,
|
||||||
|
dateString,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runReleaseCommitStep(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
newVersion: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await smartshellInstance.exec("git add -A");
|
||||||
|
const result = await smartshellInstance.exec(`git commit -m ${shellQuote(`v${newVersion}`)}`);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", "Release commit failed.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runTagStep(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
newVersion: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const result = await smartshellInstance.exec(`git tag v${newVersion} -m ${shellQuote(`v${newVersion}`)}`);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", "Release tag failed.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runGitTarget(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
workflow: IResolvedReleaseWorkflow,
|
||||||
|
): Promise<ITargetResult[]> {
|
||||||
|
const currentBranchResult = await smartshellInstance.exec("git branch --show-current");
|
||||||
|
const currentBranch = currentBranchResult.stdout.trim() || "master";
|
||||||
|
const commands: Array<{ target: string; command: string }> = [];
|
||||||
|
if (workflow.pushBranch) {
|
||||||
|
commands.push({
|
||||||
|
target: `${workflow.gitRemote}/${currentBranch}`,
|
||||||
|
command: `git push ${workflow.gitRemote} ${currentBranch}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (workflow.pushTags) {
|
||||||
|
commands.push({
|
||||||
|
target: `${workflow.gitRemote}/tags`,
|
||||||
|
command: `git push ${workflow.gitRemote} --tags`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: ITargetResult[] = [];
|
||||||
|
for (const { target, command } of commands) {
|
||||||
|
const result = await smartshellInstance.exec(command);
|
||||||
|
results.push({
|
||||||
|
target,
|
||||||
|
status: result.exitCode === 0 ? "success" : "failed",
|
||||||
|
message: result.exitCode === 0 ? undefined : "push failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runNpmTarget(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
workflow: IResolvedReleaseWorkflow,
|
||||||
|
): Promise<ITargetResult[]> {
|
||||||
|
if (!workflow.npmEnabled) {
|
||||||
|
return [{ target: "npm", status: "skipped", message: "disabled" }];
|
||||||
|
}
|
||||||
|
if (workflow.npmRegistries.length === 0) {
|
||||||
|
return [{ target: "npm", status: "failed", message: "no registries configured" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: ITargetResult[] = [];
|
||||||
|
for (const registry of workflow.npmRegistries) {
|
||||||
|
const command = `pnpm publish --registry=${registry} --access=${workflow.npmAccessLevel}`;
|
||||||
|
const result = await smartshellInstance.exec(command);
|
||||||
|
const output = `${result.stdout || ""}\n${(result as any).stderr || ""}\n${(result as any).combinedOutput || ""}`;
|
||||||
|
if (result.exitCode === 0) {
|
||||||
|
results.push({ target: registry, status: "success" });
|
||||||
|
} else if (isAlreadyPublishedOutput(output) && workflow.npmAlreadyPublished === "success") {
|
||||||
|
results.push({ target: registry, status: "already-published" });
|
||||||
|
} else {
|
||||||
|
results.push({ target: registry, status: "failed", message: firstMeaningfulLine(output) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runDockerTarget(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
workflow: IResolvedReleaseWorkflow,
|
||||||
|
): Promise<ITargetResult[]> {
|
||||||
|
if (!workflow.dockerEnabled) {
|
||||||
|
return [{ target: "docker", status: "skipped", message: "disabled" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = buildTsdockerPushCommand(workflow);
|
||||||
|
const result = await smartshellInstance.exec(command);
|
||||||
|
const output = `${result.stdout || ""}\n${(result as any).stderr || ""}\n${(result as any).combinedOutput || ""}`;
|
||||||
|
return [{
|
||||||
|
target: workflow.dockerPatterns.length > 0
|
||||||
|
? `tsdocker:${workflow.dockerPatterns.join(",")}`
|
||||||
|
: "tsdocker",
|
||||||
|
status: result.exitCode === 0 ? "success" : "failed",
|
||||||
|
message: result.exitCode === 0 ? undefined : firstMeaningfulLine(output),
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTsdockerPushCommand(workflow: IResolvedReleaseWorkflow): string {
|
||||||
|
const commandParts = ["tsdocker", "push"];
|
||||||
|
if (workflow.dockerNoBuild) {
|
||||||
|
commandParts.push("--no-build");
|
||||||
|
}
|
||||||
|
if (workflow.dockerCached) {
|
||||||
|
commandParts.push("--cached");
|
||||||
|
}
|
||||||
|
if (workflow.dockerParallel === true) {
|
||||||
|
commandParts.push("--parallel");
|
||||||
|
} else if (typeof workflow.dockerParallel === "number" && Number.isFinite(workflow.dockerParallel) && workflow.dockerParallel > 0) {
|
||||||
|
commandParts.push(`--parallel=${Math.floor(workflow.dockerParallel)}`);
|
||||||
|
}
|
||||||
|
if (workflow.dockerContext) {
|
||||||
|
commandParts.push(`--context=${shellQuote(workflow.dockerContext)}`);
|
||||||
|
}
|
||||||
|
for (const pattern of workflow.dockerPatterns) {
|
||||||
|
commandParts.push(shellQuote(pattern));
|
||||||
|
}
|
||||||
|
return commandParts.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAlreadyPublishedOutput(output: string): boolean {
|
||||||
|
return /previously published versions|cannot publish over|already exists/i.test(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstMeaningfulLine(output: string): string {
|
||||||
|
return output
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.find((line) => line.length > 0) || "command failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellQuote(value: string): string {
|
||||||
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printReleasePlan(workflow: IResolvedReleaseWorkflow): void {
|
||||||
|
console.log("");
|
||||||
|
console.log("gitzone release - resolved workflow");
|
||||||
|
console.log(`confirmation: ${workflow.confirmation}`);
|
||||||
|
console.log(`plan: ${workflow.plan.join(" -> ")}`);
|
||||||
|
console.log(`targets: ${workflow.targets.length > 0 ? workflow.targets.join(", ") : "none"}`);
|
||||||
|
console.log(`changelog: ${workflow.changelogFile}#${workflow.changelogPendingSection}`);
|
||||||
|
if (workflow.targets.includes("npm")) {
|
||||||
|
console.log(`npm registries: ${workflow.npmRegistries.length > 0 ? workflow.npmRegistries.join(", ") : "none"}`);
|
||||||
|
}
|
||||||
|
if (workflow.targets.includes("docker")) {
|
||||||
|
console.log(`docker engine: ${workflow.dockerEngine}`);
|
||||||
|
console.log(`docker patterns: ${workflow.dockerPatterns.length > 0 ? workflow.dockerPatterns.join(", ") : "all Dockerfiles"}`);
|
||||||
|
console.log(`docker options: ${formatDockerOptions(workflow)}`);
|
||||||
|
}
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDockerOptions(workflow: IResolvedReleaseWorkflow): string {
|
||||||
|
const options: string[] = [];
|
||||||
|
if (workflow.dockerCached) options.push("cached");
|
||||||
|
if (workflow.dockerParallel) options.push(`parallel=${workflow.dockerParallel === true ? "true" : workflow.dockerParallel}`);
|
||||||
|
if (workflow.dockerNoBuild) options.push("no-build");
|
||||||
|
if (workflow.dockerContext) options.push(`context=${workflow.dockerContext}`);
|
||||||
|
return options.length > 0 ? options.join(", ") : "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
function printReleaseSummary(
|
||||||
|
newVersion: string,
|
||||||
|
gitResults: ITargetResult[],
|
||||||
|
npmResults: ITargetResult[],
|
||||||
|
dockerResults: ITargetResult[],
|
||||||
|
): void {
|
||||||
|
console.log("");
|
||||||
|
console.log(`Release v${newVersion}`);
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
if (gitResults.length > 0) {
|
||||||
|
console.log("git:");
|
||||||
|
for (const result of gitResults) {
|
||||||
|
console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (npmResults.length > 0) {
|
||||||
|
console.log("npm:");
|
||||||
|
for (const result of npmResults) {
|
||||||
|
console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dockerResults.length > 0) {
|
||||||
|
console.log("docker:");
|
||||||
|
for (const result of dockerResults) {
|
||||||
|
console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showHelp(mode?: ICliMode): void {
|
||||||
|
if (mode?.json) {
|
||||||
|
printJson({
|
||||||
|
command: "release",
|
||||||
|
usage: "gitzone release [options]",
|
||||||
|
description: "Creates a versioned release from pending changelog entries and publishes configured artifacts.",
|
||||||
|
flags: [
|
||||||
|
{ flag: "-y, --yes", description: "Run without interactive confirmation" },
|
||||||
|
{ flag: "-t, --test", description: "Enable release preflight tests" },
|
||||||
|
{ flag: "-b, --build", description: "Enable release preflight build" },
|
||||||
|
{ flag: "-p, --push", description: "Enable the git release target" },
|
||||||
|
{ flag: "--target <names>", description: "Release only selected targets: git,npm,docker" },
|
||||||
|
{ flag: "--npm", description: "Enable the npm release target" },
|
||||||
|
{ flag: "--docker", description: "Enable the tsdocker release target" },
|
||||||
|
{ flag: "--no-publish", description: "Run release core and git target only" },
|
||||||
|
{ flag: "--plan", description: "Show resolved workflow without mutating files" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log("Usage: gitzone release [options]");
|
||||||
|
console.log("");
|
||||||
|
console.log("Creates a versioned release from changelog Pending entries.");
|
||||||
|
console.log("");
|
||||||
|
console.log("Flags:");
|
||||||
|
console.log(" -y, --yes Run without interactive confirmation");
|
||||||
|
console.log(" -t, --test Enable release preflight tests");
|
||||||
|
console.log(" -b, --build Enable release preflight build");
|
||||||
|
console.log(" -p, --push Enable the git release target");
|
||||||
|
console.log(" --target <names> Release only selected targets: git,npm,docker");
|
||||||
|
console.log(" --npm Enable the npm release target");
|
||||||
|
console.log(" --docker Enable the tsdocker release target");
|
||||||
|
console.log(" --no-publish Run release core and git target only");
|
||||||
|
console.log(" --major|--minor|--patch Override inferred semver level");
|
||||||
|
console.log(" --plan Show resolved workflow without mutating files");
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export * from "../plugins.js";
|
||||||
|
|
||||||
|
import * as commitinfo from "@push.rocks/commitinfo";
|
||||||
|
|
||||||
|
export { commitinfo };
|
||||||
@@ -17,11 +17,13 @@ const commandSummaries: ICommandHelpSummary[] = [
|
|||||||
{
|
{
|
||||||
name: "commit",
|
name: "commit",
|
||||||
description:
|
description:
|
||||||
"Create semantic commits or generate read-only commit recommendations",
|
"Analyze changes and create semantic source commits",
|
||||||
},
|
},
|
||||||
|
{ name: "release", description: "Create versioned releases from pending changelog entries" },
|
||||||
{ name: "format", description: "Plan or apply project formatting changes" },
|
{ name: "format", description: "Plan or apply project formatting changes" },
|
||||||
{ name: "config", description: "Read and change .smartconfig.json settings" },
|
{ name: "config", description: "Read and change .smartconfig.json settings" },
|
||||||
{ name: "services", description: "Manage or configure development services" },
|
{ name: "services", description: "Manage or configure development services" },
|
||||||
|
{ name: "tools", description: "Manage the global @git.zone toolchain" },
|
||||||
{ name: "template", description: "Create a project from a template" },
|
{ name: "template", description: "Create a project from a template" },
|
||||||
{ name: "open", description: "Open project assets and CI pages" },
|
{ name: "open", description: "Open project assets and CI pages" },
|
||||||
{ name: "docker", description: "Run Docker-related maintenance tasks" },
|
{ name: "docker", description: "Run Docker-related maintenance tasks" },
|
||||||
@@ -68,11 +70,13 @@ export let run = async (argvArg: any = {}) => {
|
|||||||
message: "What would you like to do?",
|
message: "What would you like to do?",
|
||||||
default: "commit",
|
default: "commit",
|
||||||
choices: [
|
choices: [
|
||||||
{ name: "Commit changes (semantic versioning)", value: "commit" },
|
{ name: "Commit changes", value: "commit" },
|
||||||
|
{ name: "Release pending changes", value: "release" },
|
||||||
{ name: "Format project files", value: "format" },
|
{ name: "Format project files", value: "format" },
|
||||||
{ name: "Configure release settings", value: "config" },
|
{ name: "Configure release settings", value: "config" },
|
||||||
{ name: "Create from template", value: "template" },
|
{ name: "Create from template", value: "template" },
|
||||||
{ name: "Manage dev services (MongoDB, S3)", value: "services" },
|
{ name: "Manage dev services (MongoDB, S3)", value: "services" },
|
||||||
|
{ name: "Manage global @git.zone tools", value: "tools" },
|
||||||
{ name: "Open project assets", value: "open" },
|
{ name: "Open project assets", value: "open" },
|
||||||
{ name: "Show help", value: "help" },
|
{ name: "Show help", value: "help" },
|
||||||
],
|
],
|
||||||
@@ -86,6 +90,11 @@ export let run = async (argvArg: any = {}) => {
|
|||||||
await modCommit.run({ _: ["commit"] });
|
await modCommit.run({ _: ["commit"] });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "release": {
|
||||||
|
const modRelease = await import("../mod_release/index.js");
|
||||||
|
await modRelease.run({ _: ["release"] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "format": {
|
case "format": {
|
||||||
const modFormat = await import("../mod_format/index.js");
|
const modFormat = await import("../mod_format/index.js");
|
||||||
await modFormat.run({ interactive: true });
|
await modFormat.run({ interactive: true });
|
||||||
@@ -106,6 +115,11 @@ export let run = async (argvArg: any = {}) => {
|
|||||||
await modServices.run({ _: ["services"] });
|
await modServices.run({ _: ["services"] });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "tools": {
|
||||||
|
const modTools = await import("../mod_tools/index.js");
|
||||||
|
await modTools.run({ _: ["tools"] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "open": {
|
case "open": {
|
||||||
const modOpen = await import("../mod_open/index.js");
|
const modOpen = await import("../mod_open/index.js");
|
||||||
await modOpen.run({ _: ["open"] });
|
await modOpen.run({ _: ["open"] });
|
||||||
@@ -186,8 +200,11 @@ export async function showHelp(
|
|||||||
console.log(" gitzone help commit");
|
console.log(" gitzone help commit");
|
||||||
console.log(" gitzone config show --json");
|
console.log(" gitzone config show --json");
|
||||||
console.log(" gitzone commit recommend --json");
|
console.log(" gitzone commit recommend --json");
|
||||||
|
console.log(" gitzone release --plan");
|
||||||
console.log(" gitzone format plan --json");
|
console.log(" gitzone format plan --json");
|
||||||
|
console.log(" gitzone format check");
|
||||||
console.log(" gitzone services set mongodb,minio");
|
console.log(" gitzone services set mongodb,minio");
|
||||||
|
console.log(" gitzone tools update");
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log("Run gitzone <command> --help for command-specific usage.");
|
console.log("Run gitzone <command> --help for command-specific usage.");
|
||||||
console.log("");
|
console.log("");
|
||||||
@@ -203,6 +220,11 @@ async function showCommandHelp(
|
|||||||
modCommit.showHelp(mode);
|
modCommit.showHelp(mode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case "release": {
|
||||||
|
const modRelease = await import("../mod_release/index.js");
|
||||||
|
modRelease.showHelp(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
case "config": {
|
case "config": {
|
||||||
const modConfig = await import("../mod_config/index.js");
|
const modConfig = await import("../mod_config/index.js");
|
||||||
modConfig.showHelp(mode);
|
modConfig.showHelp(mode);
|
||||||
@@ -218,6 +240,11 @@ async function showCommandHelp(
|
|||||||
modServices.showHelp(mode);
|
modServices.showHelp(mode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case "tools": {
|
||||||
|
const modTools = await import("../mod_tools/index.js");
|
||||||
|
modTools.showHelp(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import * as plugins from "./mod.plugins.js";
|
||||||
|
|
||||||
|
export interface IInstalledPackage {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPackageUpdateInfo {
|
||||||
|
name: string;
|
||||||
|
currentVersion: string;
|
||||||
|
latestVersion: string;
|
||||||
|
needsUpdate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPackageManagerInfo {
|
||||||
|
available: boolean;
|
||||||
|
currentVersion: string;
|
||||||
|
latestVersion: string | null;
|
||||||
|
needsUpdate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PackageManagerUtil {
|
||||||
|
private shell = new plugins.smartshell.Smartshell({
|
||||||
|
executor: "bash",
|
||||||
|
});
|
||||||
|
|
||||||
|
public async detectPnpm(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await this.shell.execSilent("pnpm --version 2>/dev/null");
|
||||||
|
return result.exitCode === 0 && Boolean(result.stdout.trim());
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPnpmVersionInfo(): Promise<IPackageManagerInfo> {
|
||||||
|
const available = await this.detectPnpm();
|
||||||
|
if (!available) {
|
||||||
|
return {
|
||||||
|
available: false,
|
||||||
|
currentVersion: "unknown",
|
||||||
|
latestVersion: null,
|
||||||
|
needsUpdate: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentVersion = await this.getCurrentPnpmVersion();
|
||||||
|
const latestVersion = await this.getLatestVersion("pnpm", ["https://registry.npmjs.org"]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
available: true,
|
||||||
|
currentVersion,
|
||||||
|
latestVersion,
|
||||||
|
needsUpdate: latestVersion ? this.isNewerVersion(currentVersion, latestVersion) : false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getInstalledPackages(): Promise<IInstalledPackage[]> {
|
||||||
|
const packages: IInstalledPackage[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.shell.execSilent("pnpm list -g --depth=0 --json 2>/dev/null || true");
|
||||||
|
const output = result.stdout.trim();
|
||||||
|
if (!output) {
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(output);
|
||||||
|
const dataArray = Array.isArray(data) ? data : [data];
|
||||||
|
for (const item of dataArray) {
|
||||||
|
const dependencies = item.dependencies || {};
|
||||||
|
for (const [name, info] of Object.entries(dependencies)) {
|
||||||
|
if (!name.startsWith("@git.zone/")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
packages.push({
|
||||||
|
name,
|
||||||
|
version: (info as any).version || "unknown",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLatestVersion(
|
||||||
|
packageName: string,
|
||||||
|
registries = ["https://verdaccio.lossless.digital", "https://registry.npmjs.org"],
|
||||||
|
): Promise<string | null> {
|
||||||
|
for (const registry of registries) {
|
||||||
|
const latest = await this.getLatestVersionFromRegistry(registry, packageName);
|
||||||
|
if (latest) {
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async installLatest(packageName: string): Promise<boolean> {
|
||||||
|
const packageSpecifier = `${packageName}@latest`;
|
||||||
|
console.log(` Installing ${packageSpecifier} via pnpm...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.shell.exec(`pnpm add -g ${shellQuote(packageSpecifier)}`);
|
||||||
|
return result.exitCode === 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isNewerVersion(current: string, latest: string): boolean {
|
||||||
|
const currentParts = normalizeSemver(current);
|
||||||
|
const latestParts = normalizeSemver(latest);
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
||||||
|
const currentPart = currentParts[i] || 0;
|
||||||
|
const latestPart = latestParts[i] || 0;
|
||||||
|
if (latestPart > currentPart) return true;
|
||||||
|
if (latestPart < currentPart) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getCurrentPnpmVersion(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const result = await this.shell.execSilent("pnpm --version 2>/dev/null");
|
||||||
|
const versionMatch = result.stdout.trim().match(/(\d+\.\d+\.\d+)/);
|
||||||
|
return versionMatch?.[1] || "unknown";
|
||||||
|
} catch {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getLatestVersionFromRegistry(
|
||||||
|
registry: string,
|
||||||
|
packageName: string,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const encodedName = packageName.replace("/", "%2f");
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 8000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${registry}/${encodedName}`, {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: {
|
||||||
|
accept: "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const latest = (data as any)["dist-tags"]?.latest;
|
||||||
|
return typeof latest === "string" && latest.length > 0 ? latest : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSemver(version: string): number[] {
|
||||||
|
return version
|
||||||
|
.replace(/^[^\d]*/, "")
|
||||||
|
.split(".")
|
||||||
|
.map((part) => parseInt(part, 10) || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellQuote(value: string): string {
|
||||||
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
import * as plugins from "./mod.plugins.js";
|
||||||
|
import { commitinfo } from "../00_commitinfo_data.js";
|
||||||
|
import type { ICliMode } from "../helpers.climode.js";
|
||||||
|
import { getCliMode, printJson } from "../helpers.climode.js";
|
||||||
|
import {
|
||||||
|
PackageManagerUtil,
|
||||||
|
type IInstalledPackage,
|
||||||
|
type IPackageUpdateInfo,
|
||||||
|
} from "./classes.packagemanager.js";
|
||||||
|
|
||||||
|
export const GITZONE_PACKAGES = [
|
||||||
|
"@git.zone/cli",
|
||||||
|
"@git.zone/tsdoc",
|
||||||
|
"@git.zone/tsbuild",
|
||||||
|
"@git.zone/tstest",
|
||||||
|
"@git.zone/tspublish",
|
||||||
|
"@git.zone/tsbundle",
|
||||||
|
"@git.zone/tsdocker",
|
||||||
|
"@git.zone/tsview",
|
||||||
|
"@git.zone/tswatch",
|
||||||
|
"@git.zone/tsrust",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const run = async (argvArg: any = {}): Promise<void> => {
|
||||||
|
const mode = await getCliMode(argvArg);
|
||||||
|
const command = argvArg._?.[1] || "help";
|
||||||
|
|
||||||
|
if (mode.help || command === "help") {
|
||||||
|
showHelp(mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "update":
|
||||||
|
await runUpdate(argvArg, mode);
|
||||||
|
break;
|
||||||
|
case "install":
|
||||||
|
await runInstall(argvArg, mode);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
showHelp(mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function runUpdate(argvArg: any, mode: ICliMode): Promise<void> {
|
||||||
|
const verbose = Boolean(argvArg.v || argvArg.verbose);
|
||||||
|
const pmUtil = new PackageManagerUtil();
|
||||||
|
|
||||||
|
console.log("Scanning for installed @git.zone packages...\n");
|
||||||
|
|
||||||
|
const pnpmInfo = await pmUtil.getPnpmVersionInfo();
|
||||||
|
if (!pnpmInfo.available) {
|
||||||
|
console.log("pnpm is required for gitzone tools update, but it was not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Package manager:\n");
|
||||||
|
console.log(" Name Current Latest Status");
|
||||||
|
console.log(" ----------------------------------------------");
|
||||||
|
const latestPnpm = (pnpmInfo.latestVersion || "unknown").padEnd(12);
|
||||||
|
const pnpmStatus = pnpmInfo.latestVersion === null
|
||||||
|
? "? Version unknown"
|
||||||
|
: pnpmInfo.needsUpdate
|
||||||
|
? "Update available"
|
||||||
|
: "Up to date";
|
||||||
|
console.log(` ${"pnpm".padEnd(9)}${pnpmInfo.currentVersion.padEnd(12)}${latestPnpm}${pnpmStatus}`);
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
console.log("Using pnpm as the supported global package manager.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
const selfUpdated = await handleSelfUpdate(pmUtil, mode);
|
||||||
|
if (selfUpdated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const installedPackages = await pmUtil.getInstalledPackages();
|
||||||
|
const packageInfos = await getPackageUpdateInfos(pmUtil, installedPackages);
|
||||||
|
|
||||||
|
if (packageInfos.length === 0) {
|
||||||
|
console.log("No managed @git.zone packages found installed globally.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Installed @git.zone packages:\n");
|
||||||
|
console.log(" Package Current Latest Status");
|
||||||
|
console.log(" ------------------------------------------------------------");
|
||||||
|
for (const packageInfo of packageInfos) {
|
||||||
|
const status = packageInfo.latestVersion === "unknown"
|
||||||
|
? "? Version unknown"
|
||||||
|
: packageInfo.needsUpdate
|
||||||
|
? "Update available"
|
||||||
|
: "Up to date";
|
||||||
|
console.log(
|
||||||
|
` ${packageInfo.name.padEnd(28)}${packageInfo.currentVersion.padEnd(12)}${packageInfo.latestVersion.padEnd(12)}${status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
await printMissingPackages(pmUtil, installedPackages);
|
||||||
|
|
||||||
|
const packagesToUpdate = packageInfos.filter((packageInfo) => packageInfo.needsUpdate);
|
||||||
|
if (packagesToUpdate.length === 0) {
|
||||||
|
console.log("All managed packages are up to date.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${packagesToUpdate.length} package(s) with available updates.\n`);
|
||||||
|
|
||||||
|
if (!mode.yes && !mode.interactive) {
|
||||||
|
console.log("Run gitzone tools update -y to update without prompts.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shouldUpdate = mode.yes;
|
||||||
|
if (!shouldUpdate) {
|
||||||
|
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||||
|
const answer = await interactInstance.askQuestion({
|
||||||
|
type: "confirm",
|
||||||
|
name: "confirmUpdate",
|
||||||
|
message: "Do you want to update these packages?",
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
|
shouldUpdate = answer.value === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldUpdate) {
|
||||||
|
console.log("Update cancelled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await installPackages(pmUtil, packagesToUpdate.map((packageInfo) => packageInfo.name), "updated");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runInstall(argvArg: any, mode: ICliMode): Promise<void> {
|
||||||
|
const verbose = Boolean(argvArg.v || argvArg.verbose);
|
||||||
|
const pmUtil = new PackageManagerUtil();
|
||||||
|
|
||||||
|
console.log("Scanning for missing @git.zone packages...\n");
|
||||||
|
|
||||||
|
const pnpmAvailable = await pmUtil.detectPnpm();
|
||||||
|
if (!pnpmAvailable) {
|
||||||
|
console.log("pnpm is required for gitzone tools install, but it was not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
console.log("Using pnpm as the supported global package manager.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
const installedPackages = await pmUtil.getInstalledPackages();
|
||||||
|
const installedNames = new Set(installedPackages.map((packageInfo) => packageInfo.name));
|
||||||
|
const missingPackages = GITZONE_PACKAGES.filter((packageName) => !installedNames.has(packageName));
|
||||||
|
|
||||||
|
if (missingPackages.length === 0) {
|
||||||
|
console.log("All managed @git.zone packages are already installed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${missingPackages.length} missing package(s).\n`);
|
||||||
|
|
||||||
|
if (!mode.yes && !mode.interactive) {
|
||||||
|
await printPackageListWithLatest(pmUtil, missingPackages);
|
||||||
|
console.log("Run gitzone tools install -y to install all missing packages without prompts.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedPackages = missingPackages;
|
||||||
|
if (!mode.yes) {
|
||||||
|
const choicesWithVersions: Array<{ name: string; value: string }> = [];
|
||||||
|
for (const packageName of missingPackages) {
|
||||||
|
const latest = await pmUtil.getLatestVersion(packageName);
|
||||||
|
choicesWithVersions.push({
|
||||||
|
name: `${packageName}${latest ? `@${latest}` : ""}`,
|
||||||
|
value: packageName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||||
|
const answer = await interactInstance.askQuestion({
|
||||||
|
type: "checkbox",
|
||||||
|
name: "packages",
|
||||||
|
message: "Select packages to install:",
|
||||||
|
default: missingPackages,
|
||||||
|
choices: choicesWithVersions,
|
||||||
|
});
|
||||||
|
|
||||||
|
selectedPackages = answer.value as string[];
|
||||||
|
if (selectedPackages.length === 0) {
|
||||||
|
console.log("No packages selected. Nothing to install.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await installPackages(pmUtil, selectedPackages, "installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSelfUpdate(
|
||||||
|
pmUtil: PackageManagerUtil,
|
||||||
|
mode: ICliMode,
|
||||||
|
): Promise<boolean> {
|
||||||
|
console.log("Checking for gitzone self-update...\n");
|
||||||
|
const currentVersion = commitinfo.version;
|
||||||
|
const latestVersion = await pmUtil.getLatestVersion("@git.zone/cli");
|
||||||
|
|
||||||
|
if (!latestVersion || !pmUtil.isNewerVersion(currentVersion, latestVersion)) {
|
||||||
|
console.log(` @git.zone/cli ${currentVersion} Up to date\n`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` @git.zone/cli ${currentVersion} -> ${latestVersion} Update available\n`);
|
||||||
|
|
||||||
|
if (!mode.yes && !mode.interactive) {
|
||||||
|
console.log("Run gitzone tools update -y to update gitzone first.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shouldUpdate = mode.yes;
|
||||||
|
if (!shouldUpdate) {
|
||||||
|
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||||
|
const answer = await interactInstance.askQuestion({
|
||||||
|
type: "confirm",
|
||||||
|
name: "confirmSelfUpdate",
|
||||||
|
message: "Do you want to update gitzone itself first?",
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
|
shouldUpdate = answer.value === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldUpdate) {
|
||||||
|
console.log("Skipping gitzone self-update.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await pmUtil.installLatest("@git.zone/cli");
|
||||||
|
if (!success) {
|
||||||
|
console.log("\ngitzone self-update failed. Continuing with the current version.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\ngitzone has been updated. Re-run gitzone tools update to check remaining packages.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPackageUpdateInfos(
|
||||||
|
pmUtil: PackageManagerUtil,
|
||||||
|
installedPackages: IInstalledPackage[],
|
||||||
|
): Promise<IPackageUpdateInfo[]> {
|
||||||
|
const packageInfos: IPackageUpdateInfo[] = [];
|
||||||
|
for (const installedPackage of installedPackages) {
|
||||||
|
if (!GITZONE_PACKAGES.includes(installedPackage.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const latestVersion = await pmUtil.getLatestVersion(installedPackage.name);
|
||||||
|
packageInfos.push({
|
||||||
|
name: installedPackage.name,
|
||||||
|
currentVersion: installedPackage.version,
|
||||||
|
latestVersion: latestVersion || "unknown",
|
||||||
|
needsUpdate: latestVersion
|
||||||
|
? pmUtil.isNewerVersion(installedPackage.version, latestVersion)
|
||||||
|
: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return packageInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function printMissingPackages(
|
||||||
|
pmUtil: PackageManagerUtil,
|
||||||
|
installedPackages: IInstalledPackage[],
|
||||||
|
): Promise<void> {
|
||||||
|
const installedNames = new Set(installedPackages.map((packageInfo) => packageInfo.name));
|
||||||
|
const missingPackages = GITZONE_PACKAGES.filter((packageName) => !installedNames.has(packageName));
|
||||||
|
if (missingPackages.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Not installed (managed @git.zone packages):\n");
|
||||||
|
await printPackageListWithLatest(pmUtil, missingPackages);
|
||||||
|
console.log("Run gitzone tools install to install missing packages.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function printPackageListWithLatest(
|
||||||
|
pmUtil: PackageManagerUtil,
|
||||||
|
packageNames: string[],
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(" Package Latest");
|
||||||
|
console.log(" ----------------------------------------");
|
||||||
|
for (const packageName of packageNames) {
|
||||||
|
const latest = await pmUtil.getLatestVersion(packageName);
|
||||||
|
console.log(` ${packageName.padEnd(28)} ${latest || "unknown"}`);
|
||||||
|
}
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installPackages(
|
||||||
|
pmUtil: PackageManagerUtil,
|
||||||
|
packageNames: string[],
|
||||||
|
action: "installed" | "updated",
|
||||||
|
): Promise<void> {
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
for (const packageName of packageNames) {
|
||||||
|
const success = await pmUtil.installLatest(packageName);
|
||||||
|
if (success) {
|
||||||
|
console.log(` ${packageName} ${action} successfully`);
|
||||||
|
successCount++;
|
||||||
|
} else {
|
||||||
|
console.log(` ${packageName} failed`);
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
if (failCount === 0) {
|
||||||
|
console.log(`All ${successCount} package(s) ${action} successfully.`);
|
||||||
|
} else {
|
||||||
|
console.log(`${successCount} package(s) ${action}, ${failCount} failed.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showHelp(mode?: ICliMode): void {
|
||||||
|
if (mode?.json) {
|
||||||
|
printJson({
|
||||||
|
name: "gitzone tools",
|
||||||
|
usage: "gitzone tools <command> [options]",
|
||||||
|
commands: [
|
||||||
|
{ name: "update", description: "Check and update globally installed @git.zone packages" },
|
||||||
|
{ name: "install", description: "Install missing managed @git.zone packages" },
|
||||||
|
],
|
||||||
|
flags: [
|
||||||
|
{ flag: "-y, --yes", description: "Run without confirmation prompts" },
|
||||||
|
{ flag: "-v, --verbose", description: "Show package manager diagnostics" },
|
||||||
|
],
|
||||||
|
packageManager: "pnpm",
|
||||||
|
managedPackages: GITZONE_PACKAGES,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log("Usage: gitzone tools <command> [options]");
|
||||||
|
console.log("");
|
||||||
|
console.log("Commands:");
|
||||||
|
console.log(" update Check and update globally installed @git.zone packages");
|
||||||
|
console.log(" install Install missing managed @git.zone packages");
|
||||||
|
console.log("");
|
||||||
|
console.log("Options:");
|
||||||
|
console.log(" -y, --yes Run without confirmation prompts");
|
||||||
|
console.log(" -v, --verbose Show package manager diagnostics");
|
||||||
|
console.log("");
|
||||||
|
console.log("Examples:");
|
||||||
|
console.log(" gitzone tools update");
|
||||||
|
console.log(" gitzone tools update -y");
|
||||||
|
console.log(" gitzone tools install");
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "../plugins.js";
|
||||||
Reference in New Issue
Block a user