Compare commits
176 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7686504e4e | |||
| d96b220703 | |||
| 06f2de3230 | |||
| cc3128f07b | |||
| 358d677e72 | |||
| f421c5851d | |||
| a420157287 | |||
| 0e27d54ad2 | |||
| 738fbaa64f | |||
| fe7a9d93d1 | |||
| 9a4c8795d4 | |||
| faee6a1698 | |||
| 9a1044783d | |||
| b16eb75d81 | |||
| 261f7ee6b2 | |||
| fd7a73398c | |||
| f43f88a3cb | |||
| 4c86ad62fb | |||
| 4214a1fdf1 | |||
| 1c33735799 | |||
| 274405e364 | |||
| bf858c8650 | |||
| b257c82bd6 | |||
| 5a1f6d8c76 | |||
| d44ad6e4e4 | |||
| 142adfd396 | |||
| b55e75d169 | |||
| d0d922e53b | |||
| eda67395fe | |||
| 470e87eb79 | |||
| 3358a0eacc | |||
| b65fac6257 | |||
| 4ab59609e6 | |||
| 32f106291f | |||
| b8aa5d61f6 | |||
| 71759c276e | |||
| 7938f12d43 | |||
| 3722258d69 | |||
| 68859d0e97 | |||
| ecadbc7a86 | |||
| 0243bc5ec7 | |||
| 92e618104f | |||
| c089c1f80d | |||
| 10a394c7d8 | |||
| 5980308bb8 | |||
| 398e36bdf7 | |||
| 1e78517547 | |||
| 55700ad87e | |||
| 773df5268b | |||
| b51fa88283 | |||
| cb9f717d54 | |||
| 70be11894c | |||
| 89ab63b153 | |||
| 44c193d4a8 | |||
| 44d259a0ae | |||
| f0adff8784 | |||
| fb453e62c3 | |||
| 001721a8e9 | |||
| b191464ff9 | |||
| 4d7eaa238f | |||
| 601e0d1063 | |||
| 4bb1a2f8c7 | |||
| b506bf8785 | |||
| d5fbeb3fc6 | |||
| 2ecdeff3dc | |||
| 5a663ae767 | |||
| 218c84a39b | |||
| 27d5cdca35 | |||
| 3ebf072bfb | |||
| 08f56ae0a4 | |||
| b2d2684895 | |||
| 1b328c3045 | |||
| f444a04876 | |||
| 6bd2d35992 | |||
| d038b1d681 | |||
| d60de5cef7 | |||
| e1076a60ad | |||
| 6deba06443 | |||
| 00cd9dc8e7 | |||
| 6b0941eea9 | |||
| 7348567a62 | |||
| ccdca55c9a | |||
| 6c62f80c57 | |||
| 7bb2f65669 | |||
| 48c4b0c9b2 | |||
| 106b72748c | |||
| 03bf82d8ee | |||
| 6a936603bd | |||
| 2a91662e63 | |||
| ea0c026c7e | |||
| d508e1d06c | |||
| 27f2d265de | |||
| af3e15e922 | |||
| b44624f2e7 | |||
| 847e679e92 | |||
| ddf5023ecb | |||
| e1d28bc10a | |||
| 2f3d67f9e3 | |||
| 6304953234 | |||
| 8d84620bc4 | |||
| efd6f04e63 | |||
| 97ce9db28e | |||
| 362b4c106e | |||
| 3efe385952 | |||
| f6886f172d | |||
| 81d6273346 | |||
| 7e6cf5f046 | |||
| 89cf7dca04 | |||
| 9639a64437 | |||
| 48305ebb6a | |||
| 485c0a3855 | |||
| adc828d9bb | |||
| fff1d39338 | |||
| 5afbe6ccbc | |||
| 9de17a428d | |||
| c9985102c3 | |||
| 73f98c1c3f | |||
| ae93e6f146 | |||
| 2abaeee500 | |||
| 0538ba2586 | |||
| a451779724 | |||
| cd3246d659 | |||
| d37ffd7177 | |||
| a69b613087 | |||
| 1ea186d233 | |||
| f5e7d43cf3 | |||
| d80faa044a | |||
| 64062e5c43 | |||
| bd22844280 | |||
| 366c4a0bc2 | |||
| 0d3b10bd00 | |||
| a41e3d5d2c | |||
| c45cff89de | |||
| 7bb43ad478 | |||
| 8dcaf1c631 | |||
| 422761806d | |||
| 31360240a9 | |||
| e338ee584f | |||
| 31d2e18830 | |||
| a162ddabbb | |||
| 5dfa1d72aa | |||
| 7074a19a7f | |||
| 5774fb4da2 | |||
| be45ce765d | |||
| 2a250b8823 | |||
| 9a436cb4be | |||
| 86782c39dd | |||
| fba3e9d2b0 | |||
| cc37f70185 | |||
| dbc1a1ba18 | |||
| ff57f8a322 | |||
| 968e67330d | |||
| 935ee20e83 | |||
| c205180991 | |||
| 4a53bc4abc | |||
| a86fb3bb8e | |||
| b187000ae4 | |||
| c715adfd6c | |||
| 7b9ebfdacb | |||
| 05b170cbac | |||
| b320af0b61 | |||
| 49e1ee1f39 | |||
| cef31cf1ff | |||
| 74ecdde1ac | |||
| 74a8229e43 | |||
| 859cbc733d | |||
| d32d47b706 | |||
| fd90cfe895 | |||
| c48f48fc8b | |||
| e21e7f0850 | |||
| 5f561527f9 | |||
| 9f5f568c3f | |||
| 39a31a4304 | |||
| b629a7d70b | |||
| 4003944139 | |||
| 83d374dffd |
@@ -6,19 +6,19 @@ on:
|
|||||||
- '**'
|
- '**'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${-{gitea.repository_owner}-}:${-{secrets.GITEA_TOKEN}-}@{{module.githost}}/${-{gitea.repository}-}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${-{secrets.NPMCI_TOKEN_NPM}-}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${-{secrets.NPMCI_TOKEN_NPM2}-}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
NPMCI_GIT_GITHUBTOKEN: ${-{secrets.NPMCI_GIT_GITHUBTOKEN}-}
|
||||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
NPMCI_URL_CLOUDLY: ${-{secrets.NPMCI_URL_CLOUDLY}-}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
security:
|
security:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
container:
|
container:
|
||||||
image: ${{ env.IMAGE }}
|
image: ${-{ env.IMAGE }-}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -44,11 +44,11 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
test:
|
test:
|
||||||
if: ${{ always() }}
|
if: ${-{ always() }-}
|
||||||
needs: security
|
needs: security
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: ${{ env.IMAGE }}
|
image: ${-{ env.IMAGE }-}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ on:
|
|||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${-{gitea.repository_owner}-}:${-{secrets.GITEA_TOKEN}-}@{{module.githost}}/${-{gitea.repository}-}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${-{secrets.NPMCI_TOKEN_NPM}-}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${-{secrets.NPMCI_TOKEN_NPM2}-}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
NPMCI_GIT_GITHUBTOKEN: ${-{secrets.NPMCI_GIT_GITHUBTOKEN}-}
|
||||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
NPMCI_URL_CLOUDLY: ${-{secrets.NPMCI_URL_CLOUDLY}-}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
security:
|
security:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
container:
|
container:
|
||||||
image: ${{ env.IMAGE }}
|
image: ${-{ env.IMAGE }-}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -42,11 +42,11 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
test:
|
test:
|
||||||
if: ${{ always() }}
|
if: ${-{ always() }-}
|
||||||
needs: security
|
needs: security
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: ${{ env.IMAGE }}
|
image: ${-{ env.IMAGE }-}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: ${{ env.IMAGE }}
|
image: ${-{ env.IMAGE }-}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -95,7 +95,7 @@ jobs:
|
|||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: ${{ env.IMAGE }}
|
image: ${-{ env.IMAGE }-}
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
+7
-1
@@ -16,4 +16,10 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
dist_*/
|
dist_*/
|
||||||
|
|
||||||
#------# custom
|
# AI
|
||||||
|
.claude/
|
||||||
|
.serena/
|
||||||
|
|
||||||
|
#------# custom
|
||||||
|
.serena
|
||||||
|
test-output.json
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
"npmci": {
|
"@ship.zone/szci": {
|
||||||
"npmGlobalTools": [],
|
"npmGlobalTools": []
|
||||||
"npmAccessLevel": "private",
|
|
||||||
"npmRegistryUrl": "verdaccio.lossless.one"
|
|
||||||
},
|
},
|
||||||
"gitzone": {
|
"@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"
|
||||||
|
},
|
||||||
|
"@git.zone/cli": {
|
||||||
|
"schemaVersion": 2,
|
||||||
"projectType": "npm",
|
"projectType": "npm",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "gitlab.com",
|
"githost": "gitlab.com",
|
||||||
@@ -31,9 +33,39 @@
|
|||||||
"Docker",
|
"Docker",
|
||||||
"CI/CD"
|
"CI/CD"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"commit": {
|
||||||
|
"confirmation": "prompt",
|
||||||
|
"steps": ["analyze", "changelog", "commit"]
|
||||||
|
},
|
||||||
|
"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://verdaccio.lossless.digital",
|
||||||
|
"https://registry.npmjs.org"
|
||||||
|
],
|
||||||
|
"accessLevel": "public",
|
||||||
|
"alreadyPublished": "success"
|
||||||
|
},
|
||||||
|
"docker": {
|
||||||
|
"enabled": false,
|
||||||
|
"images": []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,8 +27,8 @@ auditProductionDependencies:
|
|||||||
image: code.foss.global/hosttoday/ht-docker-node:npmci
|
image: code.foss.global/hosttoday/ht-docker-node:npmci
|
||||||
stage: security
|
stage: security
|
||||||
script:
|
script:
|
||||||
- npmci command npm config set registry https://registry.npmjs.org
|
- npmci command npm config set registry https://registry.npmjs.org
|
||||||
- npmci command pnpm audit --audit-level=high --prod
|
- npmci command pnpm audit --audit-level=high --prod
|
||||||
tags:
|
tags:
|
||||||
- private
|
- private
|
||||||
- docker
|
- docker
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ auditProductionDependencies:
|
|||||||
image: code.foss.global/hosttoday/ht-docker-node:npmci
|
image: code.foss.global/hosttoday/ht-docker-node:npmci
|
||||||
stage: security
|
stage: security
|
||||||
script:
|
script:
|
||||||
- npmci command npm config set registry https://registry.npmjs.org
|
- npmci command npm config set registry https://registry.npmjs.org
|
||||||
- npmci command pnpm audit --audit-level=high --prod
|
- npmci command pnpm audit --audit-level=high --prod
|
||||||
tags:
|
tags:
|
||||||
- private
|
- private
|
||||||
- docker
|
- docker
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ auditProductionDependencies:
|
|||||||
image: code.foss.global/hosttoday/ht-docker-node:npmci
|
image: code.foss.global/hosttoday/ht-docker-node:npmci
|
||||||
stage: security
|
stage: security
|
||||||
script:
|
script:
|
||||||
- npmci command npm config set registry https://registry.npmjs.org
|
- npmci command npm config set registry https://registry.npmjs.org
|
||||||
- npmci command pnpm audit --audit-level=high --prod
|
- npmci command pnpm audit --audit-level=high --prod
|
||||||
tags:
|
tags:
|
||||||
- private
|
- private
|
||||||
- docker
|
- docker
|
||||||
|
|||||||
@@ -19,4 +19,12 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
dist_*/
|
dist_*/
|
||||||
|
|
||||||
|
# rust
|
||||||
|
rust/target/
|
||||||
|
dist_rust/
|
||||||
|
|
||||||
|
# AI
|
||||||
|
.claude/
|
||||||
|
.serena/
|
||||||
|
|
||||||
#------# custom
|
#------# custom
|
||||||
@@ -2,4 +2,3 @@ runafter:
|
|||||||
- git add -A && git commit -m initial
|
- git add -A && git commit -m initial
|
||||||
- git push origin master
|
- git push origin master
|
||||||
- gitzone meta update
|
- gitzone meta update
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"target": "ES2022",
|
||||||
|
"checkJs": true
|
||||||
|
},
|
||||||
|
"nodeModulesDir": true
|
||||||
|
}
|
||||||
@@ -12,17 +12,17 @@ fileName: package.json
|
|||||||
"author": "{{module.author}}",
|
"author": "{{module.author}}",
|
||||||
"license": "{{module.license}}",
|
"license": "{{module.license}}",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/ --web)",
|
"test": "(tstest test/ --verbose --logfile --timeout 60)",
|
||||||
"build": "(tsbuild --web --allowimplicitany)",
|
"build": "(tsbuild tsfolders --allowimplicitany)",
|
||||||
"buildDocs": "(tsdoc)"
|
"buildDocs": "(tsdoc)"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.25",
|
"@git.zone/tsbuild": "^4.1.2",
|
||||||
"@git.zone/tsbundle": "^2.0.5",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@git.zone/tsrun": "^1.2.46",
|
"@git.zone/tstest": "^3.1.8",
|
||||||
"@git.zone/tstest": "^1.0.44",
|
"@types/node": "^25.2.0"
|
||||||
"@push.rocks/tapbundle": "^5.0.15",
|
|
||||||
"@types/node": "^20.8.7"
|
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"dependencies": {
|
||||||
|
"@push.rocks/smartpath": "^6.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as {{module.name}} from '../ts/index.js'
|
import * as {{module.name}} from '../ts/index.js'
|
||||||
|
|
||||||
tap.test('first test', async () => {
|
tap.test('first test', async () => {
|
||||||
console.log({{module.name}})
|
console.log({{module.name}})
|
||||||
})
|
})
|
||||||
|
|
||||||
tap.start()
|
export default tap.start()
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import * as plugins from './{{module.name}}.plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
export let demoExport = 'Hi there! :) This is an exported string';
|
export let demoExport = 'Hi there! :) This is an exported string';
|
||||||
|
|||||||
@@ -17,18 +17,18 @@ fileName: package.json
|
|||||||
"build": "(tsbuild --web --allowimplicitany)"
|
"build": "(tsbuild --web --allowimplicitany)"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.17",
|
"@git.zone/tsbuild": "^3.1.2",
|
||||||
"@git.zone/tsrun": "^1.2.8",
|
"@git.zone/tsrun": "^2.0.0",
|
||||||
"@git.zone/tstest": "^1.0.28",
|
"@git.zone/tstest": "^3.1.3",
|
||||||
"@git.zone/tswatch": "^2.0.1",
|
"@git.zone/tswatch": "^2.0.1",
|
||||||
"@push.rocks/tapbundle": "^5.5.4"
|
"@types/node": "^24.10.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedserver": "^3.0.53",
|
"@api.global/typedserver": "^3.0.53",
|
||||||
"@push.rocks/projectinfo": "^5.0.1",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/qenv": "^6.1.0",
|
"@push.rocks/qenv": "^6.1.0",
|
||||||
"@push.rocks/smartdata": "^5.0.7",
|
"@push.rocks/smartdata": "^5.0.7",
|
||||||
"@push.rocks/smartpath": "^5.0.5",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartstate": "^2.0.0"
|
"@push.rocks/smartstate": "^2.0.0"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"gitzone": {
|
"@git.zone/cli": {
|
||||||
"projectType": "{{projectType}}",
|
"projectType": "{{projectType}}",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "{{module.githost}}",
|
"githost": "{{module.githost}}",
|
||||||
@@ -9,16 +9,22 @@
|
|||||||
"npmPackagename": "{{module.npmPackagename}}",
|
"npmPackagename": "{{module.npmPackagename}}",
|
||||||
"license": "{{module.license}}",
|
"license": "{{module.license}}",
|
||||||
"projectDomain": "{{module.projectDomain}}"
|
"projectDomain": "{{module.projectDomain}}"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"targets": {
|
||||||
|
"npm": {
|
||||||
|
"registries": ["{{npmPrivateRegistry}}"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npmci": {
|
"@ship.zone/szci": {
|
||||||
"npmGlobalTools": [],
|
"npmGlobalTools": [],
|
||||||
"dockerRegistryRepoMap": {
|
"dockerRegistryRepoMap": {
|
||||||
"registry.gitlab.com": "{{dockerTargetImagePath}}"
|
"registry.gitlab.com": "{{dockerTargetImagePath}}"
|
||||||
},
|
},
|
||||||
"dockerBuildargEnvMap": {
|
"dockerBuildargEnvMap": {
|
||||||
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
|
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
|
||||||
},
|
}
|
||||||
"npmRegistryUrl": "{{npmPrivateRegistry}}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-4
@@ -1,5 +1,8 @@
|
|||||||
|
---
|
||||||
|
fileName: .smartconfig.json
|
||||||
|
---
|
||||||
{
|
{
|
||||||
"gitzone": {
|
"@git.zone/cli": {
|
||||||
"projectType": "{{projectType}}",
|
"projectType": "{{projectType}}",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "{{module.githost}}",
|
"githost": "{{module.githost}}",
|
||||||
@@ -9,10 +12,16 @@
|
|||||||
"npmPackagename": "{{module.npmPackagename}}",
|
"npmPackagename": "{{module.npmPackagename}}",
|
||||||
"license": "{{module.license}}",
|
"license": "{{module.license}}",
|
||||||
"projectDomain": "{{module.projectDomain}}"
|
"projectDomain": "{{module.projectDomain}}"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"targets": {
|
||||||
|
"npm": {
|
||||||
|
"accessLevel": "{{module.npmAccessLevel}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npmci": {
|
"@ship.zone/szci": {
|
||||||
"npmGlobalTools": [],
|
"npmGlobalTools": []
|
||||||
"npmAccessLevel": "{{module.npmAccessLevel}}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"useDefineForClassFields": false,
|
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"json.schemas": [
|
"json.schemas": [
|
||||||
{
|
{
|
||||||
"fileMatch": ["/npmextra.json"],
|
"fileMatch": ["/.smartconfig.json"],
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"gitzone": {
|
"@git.zone/cli": {
|
||||||
"projectType": "website",
|
"projectType": "website",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "{{module.githost}}",
|
"githost": "{{module.githost}}",
|
||||||
@@ -9,16 +9,22 @@
|
|||||||
"npmPackagename": "{{module.npmPackagename}}",
|
"npmPackagename": "{{module.npmPackagename}}",
|
||||||
"license": "{{module.license}}",
|
"license": "{{module.license}}",
|
||||||
"projectDomain": "{{module.projectDomain}}"
|
"projectDomain": "{{module.projectDomain}}"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"targets": {
|
||||||
|
"npm": {
|
||||||
|
"registries": ["{{private.npmRegistryUrl}}"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npmci": {
|
"@ship.zone/szci": {
|
||||||
"npmGlobalTools": [],
|
"npmGlobalTools": [],
|
||||||
"dockerRegistryRepoMap": {
|
"dockerRegistryRepoMap": {
|
||||||
"registry.gitlab.com": "{{docker.registryImageTag}}"
|
"registry.gitlab.com": "{{docker.registryImageTag}}"
|
||||||
},
|
},
|
||||||
"dockerBuildargEnvMap": {
|
"dockerBuildargEnvMap": {
|
||||||
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
|
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
|
||||||
},
|
}
|
||||||
"npmRegistryUrl": "{{private.npmRegistryUrl}}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+721
-3
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2015 Task Venture Capital GmbH (hello@lossless.com)
|
Copyright (c) 2015 Task Venture Capital GmbH (hello@task.vc)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -16,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|||||||
+45
-46
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/cli",
|
"name": "@git.zone/cli",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.16.2",
|
"version": "2.17.0",
|
||||||
"description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.",
|
"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 --web --allowimplicitany)",
|
"build": "tsbuild tsfolders",
|
||||||
"clean": "(rm -rf test/)",
|
"clean": "(rm -rf .nogit/test/)",
|
||||||
"prepareTest": "(git clone https://gitlab.com/sandboxzone/sandbox-npmts.git test/)",
|
"prepareTest": "(mkdir -p .nogit && git clone https://gitlab.com/sandboxzone/sandbox-npmts.git .nogit/test/)",
|
||||||
"testBuild": "npm run build && rm -r dist/",
|
"testBuild": "pnpm run build && rm -r dist/",
|
||||||
"testCli": "(cd test && node ../cli.ts.js)",
|
"testCli": "(cd .nogit/test && node ../../cli.ts.js)",
|
||||||
"testCommit": "(cd test && node ../cli.ts.js commit)",
|
"testCommit": "(cd .nogit/test && node ../../cli.ts.js commit)",
|
||||||
"testDeprecate": "(cd test && node ../cli.ts.js deprecate)",
|
"testDeprecate": "(cd .nogit/test && node ../../cli.ts.js deprecate)",
|
||||||
"testOpen": "(cd test && node ../cli.ts.js open ci)",
|
"testOpen": "(cd .nogit/test && node ../../cli.ts.js open ci)",
|
||||||
"testReadme": "(cd test && node ../cli.ts.js readme)",
|
"testReadme": "(cd .nogit/test && node ../../cli.ts.js readme)",
|
||||||
"testFormat": "(cd test && node ../cli.ts.js format)",
|
"testFormat": "(cd .nogit/test && node ../../cli.ts.js format)",
|
||||||
"testTemplateNpm": "(rm -rf test/testtemplate_npm/ && mkdir test/testtemplate_npm && cd test/testtemplate_npm && node ../../cli.ts.js template npm)",
|
"testTemplateNpm": "(rm -rf .nogit/test/testtemplate_npm/ && mkdir -p .nogit/test/testtemplate_npm && cd .nogit/test/testtemplate_npm && node ../../../cli.ts.js template npm)",
|
||||||
"testTemplateLit": "(rm -rf test/testtemplate_lit/ && mkdir test/testtemplate_lit && cd test/testtemplate_lit && node ../../cli.ts.js template lit)",
|
"testTemplateLit": "(rm -rf .nogit/test/testtemplate_lit/ && mkdir -p .nogit/test/testtemplate_lit && cd .nogit/test/testtemplate_lit && node ../../../cli.ts.js template lit)",
|
||||||
"testUpdate": "(cd test && node ../cli.ts.js update)",
|
"testUpdate": "(cd .nogit/test && node ../../cli.ts.js update)",
|
||||||
"testVersion": "(cd test && node ../cli.ts.js -v)",
|
"testVersion": "(cd .nogit/test && node ../../cli.ts.js -v)",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -57,45 +57,40 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://gitlab.com/gitzone/private/gitzone#readme",
|
"homepage": "https://gitlab.com/gitzone/private/gitzone#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.3.2",
|
"@git.zone/tsbuild": "^4.3.0",
|
||||||
"@git.zone/tsrun": "^1.3.3",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@git.zone/tstest": "^1.0.96",
|
"@git.zone/tstest": "^3.3.2",
|
||||||
"@types/node": "^22.15.18"
|
"@types/node": "^25.4.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git.zone/tsdoc": "^1.5.0",
|
"@git.zone/tsdoc": "^2.0.0",
|
||||||
"@git.zone/tspublish": "^1.9.1",
|
"@git.zone/tspublish": "^1.11.2",
|
||||||
"@push.rocks/commitinfo": "^1.0.12",
|
"@push.rocks/commitinfo": "^1.0.12",
|
||||||
"@push.rocks/early": "^4.0.4",
|
"@push.rocks/early": "^4.0.4",
|
||||||
"@push.rocks/gulp-function": "^3.0.7",
|
|
||||||
"@push.rocks/lik": "^6.2.2",
|
|
||||||
"@push.rocks/npmextra": "^5.1.2",
|
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/smartchok": "^1.0.34",
|
"@push.rocks/smartcli": "^4.0.20",
|
||||||
"@push.rocks/smartcli": "^4.0.11",
|
"@push.rocks/smartconfig": "^6.0.1",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"@push.rocks/smartdiff": "^1.0.3",
|
"@push.rocks/smartdiff": "^1.1.0",
|
||||||
"@push.rocks/smartfile": "^11.2.0",
|
"@push.rocks/smartfile": "^13.1.2",
|
||||||
"@push.rocks/smartgulp": "^3.0.4",
|
"@push.rocks/smartfs": "^1.5.0",
|
||||||
"@push.rocks/smartinteract": "^2.0.15",
|
"@push.rocks/smartinteract": "^2.0.16",
|
||||||
"@push.rocks/smartjson": "^5.0.20",
|
"@push.rocks/smartjson": "^6.0.0",
|
||||||
"@push.rocks/smartlegal": "^1.0.27",
|
"@push.rocks/smartlegal": "^1.0.27",
|
||||||
"@push.rocks/smartlog": "^3.0.9",
|
"@push.rocks/smartlog": "^3.2.1",
|
||||||
"@push.rocks/smartlog-destination-local": "^9.0.2",
|
"@push.rocks/smartlog-destination-local": "^9.0.2",
|
||||||
"@push.rocks/smartmustache": "^3.0.2",
|
"@push.rocks/smartmustache": "^3.0.2",
|
||||||
"@push.rocks/smartnpm": "^2.0.4",
|
"@push.rocks/smartnetwork": "^4.4.0",
|
||||||
|
"@push.rocks/smartnpm": "^2.0.6",
|
||||||
"@push.rocks/smartobject": "^1.0.12",
|
"@push.rocks/smartobject": "^1.0.12",
|
||||||
"@push.rocks/smartopen": "^2.0.0",
|
"@push.rocks/smartopen": "^2.0.0",
|
||||||
"@push.rocks/smartpath": "^5.0.18",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartscaf": "^4.0.16",
|
"@push.rocks/smartscaf": "^4.0.21",
|
||||||
"@push.rocks/smartshell": "^3.2.3",
|
"@push.rocks/smartshell": "^3.3.7",
|
||||||
"@push.rocks/smartstream": "^3.2.5",
|
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@push.rocks/smartupdate": "^2.0.6",
|
"@push.rocks/smartupdate": "^2.0.6",
|
||||||
"@types/through2": "^2.0.41",
|
"prettier": "^3.8.1"
|
||||||
"prettier": "^3.5.3",
|
|
||||||
"through2": "^4.0.2"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
@@ -106,16 +101,20 @@
|
|||||||
"dist_ts_web/**/*",
|
"dist_ts_web/**/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
"cli.js",
|
"cli.js",
|
||||||
"npmextra.json",
|
".smartconfig.json",
|
||||||
"readme.md"
|
"readme.md"
|
||||||
],
|
],
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 1 chrome versions"
|
"last 1 chrome versions"
|
||||||
],
|
],
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {},
|
||||||
"peek-readable": "5.3.1"
|
"onlyBuiltDependencies": [
|
||||||
}
|
"esbuild",
|
||||||
|
"mongodb-memory-server",
|
||||||
|
"puppeteer",
|
||||||
|
"sharp"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+4208
-6129
File diff suppressed because it is too large
Load Diff
+150
-37
@@ -1,10 +1,11 @@
|
|||||||
# Gitzone CLI - Development Hints
|
# Gitzone CLI - Development Hints
|
||||||
|
|
||||||
* the cli of the git.zone project.
|
- the cli of the git.zone project.
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
Gitzone CLI (`@git.zone/cli`) is a comprehensive toolbelt for streamlining local development cycles. It provides utilities for:
|
Gitzone CLI (`@git.zone/cli`) is a comprehensive toolbelt for streamlining local development cycles. It provides utilities for:
|
||||||
|
|
||||||
- Project initialization and templating (via smartscaf)
|
- Project initialization and templating (via smartscaf)
|
||||||
- Code formatting and standardization
|
- Code formatting and standardization
|
||||||
- Version control and commit management
|
- Version control and commit management
|
||||||
@@ -14,27 +15,30 @@ Gitzone CLI (`@git.zone/cli`) is a comprehensive toolbelt for streamlining local
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Core Structure
|
### Core Structure
|
||||||
|
|
||||||
- Main CLI entry: `cli.ts` / `cli.child.ts`
|
- Main CLI entry: `cli.ts` / `cli.child.ts`
|
||||||
- Modular architecture with separate modules in `ts/mod_*` directories
|
- Modular architecture with separate modules in `ts/mod_*` directories
|
||||||
- Each module handles specific functionality (format, commit, docker, etc.)
|
- Each module handles specific functionality (format, commit, docker, etc.)
|
||||||
- Extensive use of plugins pattern via `plugins.ts` files
|
- Extensive use of plugins pattern via `plugins.ts` files
|
||||||
|
|
||||||
### Configuration Management
|
### Configuration Management
|
||||||
- Uses `npmextra.json` for all tool configuration
|
|
||||||
- Configuration stored under `gitzone` key in npmextra
|
- Uses `.smartconfig.json` for tool configuration
|
||||||
- No separate `.gitzonerc` file - everything in npmextra.json
|
- CLI settings live under the `@git.zone/cli` namespace
|
||||||
- Project type and module metadata also stored in npmextra
|
- Agent and non-interactive defaults now belong under `@git.zone/cli.cli`
|
||||||
|
- Project type, module metadata, release settings, commit defaults, and format settings live in the same file
|
||||||
|
|
||||||
### Format Module (`mod_format`) - SIGNIFICANTLY ENHANCED
|
### Format Module (`mod_format`) - SIGNIFICANTLY ENHANCED
|
||||||
|
|
||||||
The format module is responsible for project standardization:
|
The format module is responsible for project standardization:
|
||||||
|
|
||||||
#### Current Modules:
|
#### Current Modules:
|
||||||
|
|
||||||
1. **cleanup** - Removes obsolete files (yarn.lock, tslint.json, etc.)
|
1. **cleanup** - Removes obsolete files (yarn.lock, tslint.json, etc.)
|
||||||
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
|
||||||
@@ -42,6 +46,7 @@ The format module is responsible for project standardization:
|
|||||||
10. **tsconfig** - Formats TypeScript configuration
|
10. **tsconfig** - Formats TypeScript configuration
|
||||||
|
|
||||||
#### Execution Order (Dependency-Based):
|
#### Execution Order (Dependency-Based):
|
||||||
|
|
||||||
- Modules are now executed in parallel groups based on dependencies
|
- Modules are now executed in parallel groups based on dependencies
|
||||||
- Independent modules run concurrently for better performance
|
- Independent modules run concurrently for better performance
|
||||||
- Dependency analyzer ensures correct execution order
|
- Dependency analyzer ensures correct execution order
|
||||||
@@ -79,16 +84,53 @@ The format module is responsible for project standardization:
|
|||||||
|
|
||||||
1. **Plan → Action Workflow**: Shows changes before applying them
|
1. **Plan → Action Workflow**: Shows changes before applying them
|
||||||
2. **Rollback Mechanism**: Full backup and restore on failures
|
2. **Rollback Mechanism**: Full backup and restore on failures
|
||||||
3. **Enhanced Configuration**: Granular control via npmextra.json
|
3. **Enhanced Configuration**: Granular control via `.smartconfig.json`
|
||||||
4. **Better Error Handling**: Detailed errors with recovery options
|
4. **Better Error Handling**: Detailed errors with recovery options
|
||||||
5. **Performance Optimizations**: Parallel execution and caching
|
5. **Performance Optimizations**: Parallel execution and caching
|
||||||
6. **Reporting**: Diff views, statistics, verbose logging
|
6. **Reporting**: Diff views, statistics, verbose logging
|
||||||
7. **Architecture**: Clean separation of concerns with new classes
|
7. **Architecture**: Clean separation of concerns with new classes
|
||||||
|
8. **Split Commit/Release Workflows**: `commit` creates source commits; `release` owns versioning, tags, and artifact publishing
|
||||||
|
|
||||||
|
### Commit/Release Workflow Refactor (Latest)
|
||||||
|
|
||||||
|
The commit module no longer bumps versions, creates tags, or publishes packages. Release work now belongs to `gitzone release`:
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
|
||||||
|
- `gitzone commit` analyzes changes, updates `changelog.md` `Pending`, commits, and optionally pushes.
|
||||||
|
- `gitzone release` reads `Pending`, bumps versions, moves changelog entries into a version section, tags, pushes, and publishes configured artifacts.
|
||||||
|
- Commit workflow steps are configured in `.smartconfig.json` under `@git.zone/cli.commit.steps`.
|
||||||
|
- Smartconfig schema versioning lives at `@git.zone/cli.schemaVersion`; run `gitzone config migrate <version>` for targeted migrations.
|
||||||
|
- Release publishing is target-based under `@git.zone/cli.release.targets`.
|
||||||
|
- NPM registries only live under `@git.zone/cli.release.targets.npm.registries`.
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
|
||||||
|
- Commit is safer and has no publishing side effects.
|
||||||
|
- Multiple source commits can accumulate into one release via `Pending`.
|
||||||
|
- Per-artifact release results can distinguish published, already-published, skipped, and failed targets.
|
||||||
|
|
||||||
|
### Auto-Accept Flag for Commits
|
||||||
|
|
||||||
|
The commit module now supports `-y/--yes` flag for non-interactive commits:
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
- `gitzone commit -y` - Auto-accepts AI recommendations without prompts
|
||||||
|
- `gitzone commit -yp` - Auto-accepts and pushes to origin
|
||||||
|
- Separate `-p/--push` flag controls push behavior
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
|
||||||
|
- Creates AnswerBucket programmatically when `-y` flag detected
|
||||||
|
- Preserves all UI output for transparency
|
||||||
|
- Fully backward compatible with interactive mode
|
||||||
|
- CI/CD friendly for automated workflows
|
||||||
|
|
||||||
## Development Tips
|
## Development Tips
|
||||||
|
|
||||||
- Always check readme.plan.md for ongoing improvement plans
|
- Always check readme.plan.md for ongoing improvement plans
|
||||||
- Use npmextra.json for any new configuration options
|
- Use `.smartconfig.json` for any new configuration options
|
||||||
- Keep modules focused and single-purpose
|
- Keep modules focused and single-purpose
|
||||||
- Maintain the existing plugin pattern for dependencies
|
- Maintain the existing plugin pattern for dependencies
|
||||||
- Test format operations on sample projects before deploying
|
- Test format operations on sample projects before deploying
|
||||||
@@ -100,30 +142,18 @@ The format module is responsible for project standardization:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"gitzone": {
|
"@git.zone/cli": {
|
||||||
|
"cli": {
|
||||||
|
"interactive": true,
|
||||||
|
"output": "human",
|
||||||
|
"checkUpdates": true
|
||||||
|
},
|
||||||
"format": {
|
"format": {
|
||||||
"interactive": true,
|
"interactive": true,
|
||||||
"parallel": true,
|
|
||||||
"showStats": true,
|
"showStats": true,
|
||||||
"cache": {
|
|
||||||
"enabled": true,
|
|
||||||
"clean": true
|
|
||||||
},
|
|
||||||
"rollback": {
|
|
||||||
"enabled": true,
|
|
||||||
"autoRollbackOnError": true,
|
|
||||||
"backupRetentionDays": 7
|
|
||||||
},
|
|
||||||
"modules": {
|
"modules": {
|
||||||
"skip": ["prettier"],
|
"skip": ["prettier"],
|
||||||
"only": [],
|
"only": []
|
||||||
"order": []
|
|
||||||
},
|
|
||||||
"licenses": {
|
|
||||||
"allowed": ["MIT", "Apache-2.0"],
|
|
||||||
"exceptions": {
|
|
||||||
"some-package": "GPL-3.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,15 +162,42 @@ The format module is responsible for project standardization:
|
|||||||
|
|
||||||
## CLI Usage
|
## CLI Usage
|
||||||
|
|
||||||
|
### Commit Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive commit (default)
|
||||||
|
gitzone commit
|
||||||
|
|
||||||
|
# Read-only recommendation
|
||||||
|
gitzone commit recommend --json
|
||||||
|
|
||||||
|
# Auto-accept AI recommendations (no prompts)
|
||||||
|
gitzone commit -y
|
||||||
|
gitzone commit --yes
|
||||||
|
|
||||||
|
# Auto-accept and push to origin
|
||||||
|
gitzone commit -yp
|
||||||
|
gitzone commit -y -p
|
||||||
|
gitzone commit --yes --push
|
||||||
|
|
||||||
|
# Run format before commit
|
||||||
|
gitzone commit --format
|
||||||
|
```
|
||||||
|
|
||||||
|
### Format Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Basic format
|
# Basic format
|
||||||
gitzone format
|
gitzone format
|
||||||
|
|
||||||
|
# Read-only JSON plan
|
||||||
|
gitzone format plan --json
|
||||||
|
|
||||||
# Dry run to preview changes
|
# Dry run to preview changes
|
||||||
gitzone format --dry-run
|
gitzone format --dry-run
|
||||||
|
|
||||||
# Non-interactive mode
|
# Non-interactive apply
|
||||||
gitzone format --yes
|
gitzone format --write --yes
|
||||||
|
|
||||||
# Plan only (no execution)
|
# Plan only (no execution)
|
||||||
gitzone format --plan-only
|
gitzone format --plan-only
|
||||||
@@ -157,11 +214,10 @@ gitzone format --verbose
|
|||||||
# Detailed diff views
|
# Detailed diff views
|
||||||
gitzone format --detailed
|
gitzone format --detailed
|
||||||
|
|
||||||
# Rollback operations
|
# Inspect config for agents and scripts
|
||||||
gitzone format --rollback
|
gitzone config show --json
|
||||||
gitzone format --rollback <operation-id>
|
gitzone config set cli.output json
|
||||||
gitzone format --list-backups
|
gitzone config get release.targets.npm.accessLevel
|
||||||
gitzone format --clean-backups
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Issues (Now Resolved)
|
## Common Issues (Now Resolved)
|
||||||
@@ -182,7 +238,64 @@ gitzone format --clean-backups
|
|||||||
|
|
||||||
## API Changes
|
## API Changes
|
||||||
|
|
||||||
- smartfile API updated to use fs.* and memory.* namespaces
|
### Smartfile v13 Migration (Latest - Completed)
|
||||||
|
|
||||||
|
The project has been fully migrated from @push.rocks/smartfile v11 to v13, which introduced a major breaking change where filesystem operations were split into two separate packages:
|
||||||
|
|
||||||
|
**Packages:**
|
||||||
|
|
||||||
|
- `@push.rocks/smartfile` v13.0.1 - File representation classes (SmartFile, StreamFile, VirtualDirectory)
|
||||||
|
- `@push.rocks/smartfs` v1.1.0 - Filesystem operations (read, write, exists, stat, etc.)
|
||||||
|
|
||||||
|
**Key API Changes:**
|
||||||
|
|
||||||
|
1. **File Reading**:
|
||||||
|
- Old: `plugins.smartfile.fs.toStringSync(path)` or `plugins.smartfile.fs.toObjectSync(path)`
|
||||||
|
- New: `await plugins.smartfs.file(path).encoding('utf8').read()` + JSON.parse if needed
|
||||||
|
- Important: `read()` returns `string | Buffer` - use `as string` type assertion when encoding is set
|
||||||
|
|
||||||
|
2. **File Writing**:
|
||||||
|
- Old: `plugins.smartfile.memory.toFs(content, path)` or `plugins.smartfile.memory.toFsSync(content, path)`
|
||||||
|
- New: `await plugins.smartfs.file(path).encoding('utf8').write(content)`
|
||||||
|
|
||||||
|
3. **File Existence**:
|
||||||
|
- Old: `plugins.smartfile.fs.fileExists(path)` or `plugins.smartfile.fs.fileExistsSync(path)`
|
||||||
|
- New: `await plugins.smartfs.file(path).exists()`
|
||||||
|
|
||||||
|
4. **Directory Operations**:
|
||||||
|
- Old: `plugins.smartfile.fs.ensureDir(path)`
|
||||||
|
- New: `await plugins.smartfs.directory(path).recursive().create()`
|
||||||
|
- Old: `plugins.smartfile.fs.remove(path)`
|
||||||
|
- New: `await plugins.smartfs.directory(path).recursive().delete()` or `await plugins.smartfs.file(path).delete()`
|
||||||
|
|
||||||
|
5. **Directory Listing**:
|
||||||
|
- Old: `plugins.smartfile.fs.listFolders(path)` or `plugins.smartfile.fs.listFoldersSync(path)`
|
||||||
|
- New: `await plugins.smartfs.directory(path).list()` then filter by `stats.isDirectory`
|
||||||
|
- Note: `list()` returns `IDirectoryEntry[]` with `path` and `name` properties - use `stat()` to check if directory
|
||||||
|
|
||||||
|
6. **File Stats**:
|
||||||
|
- Old: `stats.isDirectory()` (method)
|
||||||
|
- New: `stats.isDirectory` (boolean property)
|
||||||
|
- Old: `stats.mtimeMs`
|
||||||
|
- New: `stats.mtime.getTime()`
|
||||||
|
|
||||||
|
7. **SmartFile Factory**:
|
||||||
|
- Old: Direct SmartFile instantiation
|
||||||
|
- New: `plugins.smartfile.SmartFileFactory.nodeFs()` then factory methods
|
||||||
|
|
||||||
|
**Migration Pattern:**
|
||||||
|
All sync methods must become async. Functions that were previously synchronous (like `getProjectName()`) now return `Promise<T>` and must be awaited.
|
||||||
|
|
||||||
|
**Affected Modules:**
|
||||||
|
|
||||||
|
- ts/mod_format/\* (largest area - 15+ files)
|
||||||
|
- ts/mod_commit/\* and ts/mod_release/\* (commit/release workflows)
|
||||||
|
- ts/mod_services/\* (configuration management)
|
||||||
|
- ts/mod_meta/\* (meta repository management)
|
||||||
|
- ts/mod_standard/\* (template listing)
|
||||||
|
- ts/mod_template/\* (template operations)
|
||||||
|
|
||||||
|
**Previous API Changes:**
|
||||||
|
|
||||||
- smartnpm requires instance creation: `new NpmRegistry()`
|
- smartnpm requires instance creation: `new NpmRegistry()`
|
||||||
- All file operations now use updated APIs
|
- Type imports use `import type` for proper verbatim module syntax
|
||||||
- Type imports use `import type` for proper verbatim module syntax
|
|
||||||
|
|||||||
@@ -1,406 +1,432 @@
|
|||||||
# @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?
|
## Issue Reporting and Security
|
||||||
|
|
||||||
gitzone is a powerful command-line interface that supercharges your development workflow with automated project management, intelligent code formatting, and seamless version control. Whether you're bootstrapping a new TypeScript project, maintaining code quality, or managing complex multi-repository setups, gitzone has got you covered.
|
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||||
|
|
||||||
## 🏃♂️ Quick Start
|
## Install
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install globally via npm
|
|
||||||
npm install -g @git.zone/cli
|
|
||||||
|
|
||||||
# Or with pnpm (recommended)
|
|
||||||
pnpm add -g @git.zone/cli
|
pnpm add -g @git.zone/cli
|
||||||
```
|
```
|
||||||
|
|
||||||
Once installed, you can use either `gitzone` or the shorter `gzone` command from anywhere in your terminal.
|
After installation, both binaries point to the same CLI:
|
||||||
|
|
||||||
### Your First Command
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a new TypeScript npm package
|
gitzone --help
|
||||||
gitzone template npm
|
gzone --help
|
||||||
|
```
|
||||||
|
|
||||||
# Format your entire codebase
|
## The Big Idea
|
||||||
|
|
||||||
|
`gitzone commit` handles source history.
|
||||||
|
|
||||||
|
`gitzone release` handles release transactions.
|
||||||
|
|
||||||
|
That split is intentional. A commit should not unexpectedly publish npm packages, push Docker images, or trigger remote release pipelines. A release should clearly show which targets it will publish to.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Preview project standardization work
|
||||||
gitzone format
|
gitzone format
|
||||||
|
|
||||||
# Create a semantic commit
|
# Apply formatting changes
|
||||||
|
gitzone format --write
|
||||||
|
|
||||||
|
# Create a semantic source commit
|
||||||
gitzone commit
|
gitzone commit
|
||||||
|
|
||||||
|
# Preview the configured release transaction
|
||||||
|
gitzone release --plan
|
||||||
|
|
||||||
|
# Release pending changelog entries to configured targets
|
||||||
|
gitzone release
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠️ Core Features
|
## Commands
|
||||||
|
|
||||||
### 📦 Project Templates
|
| 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 |
|
||||||
|
|
||||||
Instantly scaffold production-ready projects with best practices built-in:
|
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
|
||||||
gitzone template [template-name]
|
# 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
|
||||||
```
|
```
|
||||||
|
|
||||||
**Available templates:**
|
`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.
|
||||||
- **`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:
|
## Commit Workflow
|
||||||
- ✅ TypeScript with modern configurations
|
|
||||||
- ✅ Automated testing setup
|
|
||||||
- ✅ CI/CD pipelines (GitLab/GitHub)
|
|
||||||
- ✅ Code formatting and linting
|
|
||||||
- ✅ Documentation structure
|
|
||||||
|
|
||||||
### 🎨 Intelligent Code Formatting
|
`gitzone commit` creates one semantic source commit. It does not bump versions, create tags, publish packages, or push Docker images.
|
||||||
|
|
||||||
The most powerful feature of gitzone - automatically format and standardize your entire codebase:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Preview changes without applying them
|
|
||||||
gitzone format --dry-run
|
|
||||||
|
|
||||||
# Format with automatic approval
|
|
||||||
gitzone format --yes
|
|
||||||
|
|
||||||
# Save formatting plan for later execution
|
|
||||||
gitzone format --save-plan format-plan.json
|
|
||||||
|
|
||||||
# Execute a saved plan
|
|
||||||
gitzone format --from-plan format-plan.json
|
|
||||||
|
|
||||||
# Enable verbose output for debugging
|
|
||||||
gitzone format --verbose
|
|
||||||
```
|
|
||||||
|
|
||||||
**Format features:**
|
|
||||||
- 🔄 **Smart caching** - Only processes changed files
|
|
||||||
- 🛡️ **Rollback support** - Undo formatting changes if needed
|
|
||||||
- 📊 **Detailed reporting** - See exactly what changed
|
|
||||||
- ⚡ **Parallel execution** - Format multiple files simultaneously
|
|
||||||
- 🎯 **Module-specific formatting** - Target specific formatters
|
|
||||||
|
|
||||||
**Rollback capabilities:**
|
|
||||||
```bash
|
|
||||||
# List all available backups
|
|
||||||
gitzone format --list-backups
|
|
||||||
|
|
||||||
# Rollback to the last operation
|
|
||||||
gitzone format --rollback
|
|
||||||
|
|
||||||
# Rollback to a specific operation
|
|
||||||
gitzone format --rollback [operation-id]
|
|
||||||
|
|
||||||
# Clean old backups
|
|
||||||
gitzone format --clean-backups
|
|
||||||
```
|
|
||||||
|
|
||||||
**Formatters included:**
|
|
||||||
- **Prettier** - JavaScript/TypeScript code formatting
|
|
||||||
- **License** - Ensure proper licensing
|
|
||||||
- **Package.json** - Standardize package configurations
|
|
||||||
- **Tsconfig** - TypeScript configuration optimization
|
|
||||||
- **Readme** - Documentation formatting
|
|
||||||
- **Gitignore** - Repository ignore rules
|
|
||||||
- **Templates** - Project template updates
|
|
||||||
- **Npmextra** - Extended npm configurations
|
|
||||||
|
|
||||||
### 🔀 Semantic Commits & Versioning
|
|
||||||
|
|
||||||
Create standardized commits that automatically handle versioning:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Interactive semantic commit
|
||||||
gitzone commit
|
gitzone commit
|
||||||
|
|
||||||
|
# Read-only AI recommendation
|
||||||
|
gitzone commit recommend --json
|
||||||
|
|
||||||
|
# Auto-accept safe recommendations
|
||||||
|
gitzone commit -y
|
||||||
|
|
||||||
|
# Auto-accept, test, build, and push
|
||||||
|
gitzone commit -ytbp
|
||||||
|
|
||||||
|
# Show the resolved workflow without mutating anything
|
||||||
|
gitzone commit --plan
|
||||||
```
|
```
|
||||||
|
|
||||||
Features:
|
The commit flow:
|
||||||
- 📝 Interactive commit message builder
|
|
||||||
- 🏷️ Automatic version bumping (major/minor/patch)
|
|
||||||
- 📜 Changelog generation
|
|
||||||
- 🚀 Optional auto-push to origin
|
|
||||||
- 🎯 Conventional commit compliance
|
|
||||||
|
|
||||||
The commit wizard guides you through:
|
1. Analyze the working tree.
|
||||||
1. **Type selection** (feat/fix/docs/style/refactor/perf/test/chore)
|
2. Suggest commit type, scope, and message.
|
||||||
2. **Scope definition** (component/module affected)
|
3. Write a human-readable entry into `changelog.md` under `## Pending`.
|
||||||
3. **Description crafting**
|
4. Stage and create one semantic source commit.
|
||||||
4. **Breaking change detection**
|
5. Optionally run formatting, tests, build, and push based on flags or config.
|
||||||
5. **Version bump determination**
|
|
||||||
|
|
||||||
### 🏗️ Meta Repository Management
|
Commit flags:
|
||||||
|
|
||||||
Manage multiple related repositories as a cohesive unit:
|
| Flag | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `-y`, `--yes` | Auto-accept safe recommendations |
|
||||||
|
| `-t`, `--test` | Add test step |
|
||||||
|
| `-b`, `--build` | Add build step |
|
||||||
|
| `-p`, `--push` | Push after the source commit |
|
||||||
|
| `-f`, `--format` | Run `gitzone format --write` before commit |
|
||||||
|
| `--plan` | Show resolved workflow only |
|
||||||
|
|
||||||
|
`-r` is intentionally not part of commit anymore. Use `gitzone release`.
|
||||||
|
|
||||||
|
## Release Workflow
|
||||||
|
|
||||||
|
`gitzone release` performs the release core once, then publishes to configured targets.
|
||||||
|
|
||||||
|
The release core is not configurable plumbing. It always follows the same professional release transaction:
|
||||||
|
|
||||||
|
1. Run configured preflight checks.
|
||||||
|
2. Read `changelog.md` `## Pending` entries.
|
||||||
|
3. Infer or accept a semver bump.
|
||||||
|
4. Update version files and baked commit info.
|
||||||
|
5. Move pending changelog entries into the new version section.
|
||||||
|
6. Create the local release commit.
|
||||||
|
7. Create the local release tag.
|
||||||
|
|
||||||
|
Targets decide what happens after that:
|
||||||
|
|
||||||
|
| Target | What it does |
|
||||||
|
| --- | --- |
|
||||||
|
| `git` | Pushes the release commit and tags, often triggering remote CI release builds |
|
||||||
|
| `npm` | Publishes the package to configured npm registries |
|
||||||
|
| `docker` | Builds and pushes configured Docker images |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Initialize a meta repository
|
# Preview the resolved release plan
|
||||||
gitzone meta init
|
gitzone release --plan
|
||||||
|
|
||||||
# Add a sub-project
|
# Release to configured targets
|
||||||
gitzone meta add [name] [git-url]
|
gitzone release
|
||||||
|
|
||||||
# Update all sub-projects
|
# Release only to npm
|
||||||
gitzone meta update
|
gitzone release --target npm
|
||||||
|
|
||||||
# Remove a sub-project
|
# Release only to git and Docker
|
||||||
gitzone meta remove [name]
|
gitzone release --target git,docker
|
||||||
|
|
||||||
|
# Skip package/container publishing and keep only git target
|
||||||
|
gitzone release --no-publish
|
||||||
|
|
||||||
|
# Override inferred semver level
|
||||||
|
gitzone release --minor
|
||||||
```
|
```
|
||||||
|
|
||||||
Perfect for:
|
Release flags:
|
||||||
- Monorepo management
|
|
||||||
- Multi-package projects
|
|
||||||
- Coordinated deployments
|
|
||||||
- Synchronized versioning
|
|
||||||
|
|
||||||
### 🐳 Docker Management
|
| 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 |
|
||||||
|
|
||||||
Streamline your Docker workflow:
|
## Standard Changelog
|
||||||
|
|
||||||
```bash
|
The changelog is convention-based and intentionally not configured.
|
||||||
# Clean up all Docker resources
|
|
||||||
gitzone docker prune
|
`gitzone commit` appends entries to:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Pending
|
||||||
```
|
```
|
||||||
|
|
||||||
This command removes:
|
`gitzone release` moves those pending entries into a dated version section:
|
||||||
- Stopped containers
|
|
||||||
- Unused images
|
|
||||||
- Dangling volumes
|
|
||||||
- Unused networks
|
|
||||||
|
|
||||||
### 🔗 Quick CI/CD Access
|
```markdown
|
||||||
|
## 2026-05-10 - 2.15.0
|
||||||
Jump directly to your CI/CD configurations:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Open CI/CD settings
|
|
||||||
gitzone open ci
|
|
||||||
|
|
||||||
# Open pipelines view
|
|
||||||
gitzone open pipelines
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Works with GitLab repositories to provide instant access to your deployment configurations.
|
The standard buckets are `Breaking Changes`, `Features`, `Fixes`, `Documentation`, and `Maintenance`.
|
||||||
|
|
||||||
### 📝 Package Deprecation
|
## Configuration
|
||||||
|
|
||||||
Smoothly transition users from old to new packages:
|
All CLI config lives under `@git.zone/cli` in `.smartconfig.json`.
|
||||||
|
|
||||||
```bash
|
|
||||||
gitzone deprecate
|
|
||||||
```
|
|
||||||
|
|
||||||
Interactive wizard for:
|
|
||||||
- Setting deprecation notices
|
|
||||||
- Guiding users to replacements
|
|
||||||
- Updating registry metadata
|
|
||||||
- Coordinating migration paths
|
|
||||||
|
|
||||||
### 🚦 Project Initialization
|
|
||||||
|
|
||||||
Prepare existing projects for development:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gitzone start
|
|
||||||
```
|
|
||||||
|
|
||||||
Automatically:
|
|
||||||
- Checks out master branch
|
|
||||||
- Pulls latest changes
|
|
||||||
- Installs dependencies
|
|
||||||
- Sets up development environment
|
|
||||||
|
|
||||||
### 🔧 Helper Utilities
|
|
||||||
|
|
||||||
Quick utilities for common tasks:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate a unique short ID
|
|
||||||
gitzone helpers shortid
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Configuration
|
|
||||||
|
|
||||||
### npmextra.json Configuration
|
|
||||||
|
|
||||||
Customize gitzone behavior through `npmextra.json`:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"gitzone": {
|
"@git.zone/cli": {
|
||||||
"format": {
|
"schemaVersion": 2,
|
||||||
"interactive": true,
|
"projectType": "npm",
|
||||||
"showDiffs": false,
|
"commit": {
|
||||||
"autoApprove": false,
|
"confirmation": "prompt",
|
||||||
"parallel": true,
|
"steps": ["analyze", "test", "build", "changelog", "commit", "push"]
|
||||||
"rollback": {
|
},
|
||||||
"enabled": true,
|
"release": {
|
||||||
"autoRollbackOnError": true,
|
"confirmation": "prompt",
|
||||||
"backupRetentionDays": 7
|
"preflight": {
|
||||||
|
"requireCleanTree": true,
|
||||||
|
"test": false,
|
||||||
|
"build": true
|
||||||
},
|
},
|
||||||
"modules": {
|
"targets": {
|
||||||
"skip": ["prettier"],
|
"git": {
|
||||||
"only": [],
|
"enabled": true,
|
||||||
"order": []
|
"remote": "origin",
|
||||||
},
|
"pushBranch": true,
|
||||||
"cache": {
|
"pushTags": true
|
||||||
"enabled": true,
|
},
|
||||||
"clean": true
|
"npm": {
|
||||||
|
"enabled": true,
|
||||||
|
"registries": ["https://registry.npmjs.org"],
|
||||||
|
"accessLevel": "public",
|
||||||
|
"alreadyPublished": "success"
|
||||||
|
},
|
||||||
|
"docker": {
|
||||||
|
"enabled": false,
|
||||||
|
"images": []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
NPM registries belong only here:
|
||||||
|
|
||||||
- `CI` - Detect CI environment for automated workflows
|
```text
|
||||||
- `DEBUG` - Enable debug output
|
@git.zone/cli.release.targets.npm.registries
|
||||||
- `GITZONE_FORMAT_PARALLEL` - Control parallel formatting
|
```
|
||||||
|
|
||||||
## 🏆 Best Practices
|
Useful config commands:
|
||||||
|
|
||||||
### For New Projects
|
|
||||||
1. Start with a template: `gitzone template npm`
|
|
||||||
2. Customize the generated structure
|
|
||||||
3. Run initial format: `gitzone format`
|
|
||||||
4. Set up CI/CD: `gitzone open ci`
|
|
||||||
|
|
||||||
### For Existing Projects
|
|
||||||
1. Initialize: `gitzone start`
|
|
||||||
2. Format codebase: `gitzone format --dry-run` (preview first!)
|
|
||||||
3. Apply formatting: `gitzone format --yes`
|
|
||||||
4. Commit changes: `gitzone commit`
|
|
||||||
|
|
||||||
### For Teams
|
|
||||||
1. Document format preferences in `npmextra.json`
|
|
||||||
2. Use `--save-plan` for reviewable format changes
|
|
||||||
3. Enable rollback for safety
|
|
||||||
4. Standardize commit conventions
|
|
||||||
|
|
||||||
## 🎯 Common Workflows
|
|
||||||
|
|
||||||
### Clean Development Cycle
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Start fresh
|
# Show current @git.zone/cli config
|
||||||
gitzone start
|
gitzone config show --json
|
||||||
|
|
||||||
# 2. Make changes
|
# Configure project basics, CLI behavior, and release targets interactively
|
||||||
# ... your development work ...
|
gitzone config project
|
||||||
|
gitzone config cli
|
||||||
|
gitzone config release
|
||||||
|
|
||||||
# 3. Format code
|
# Validate schema, legacy keys, release targets, registries, and npm auth
|
||||||
|
gitzone config doctor
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
# 4. Commit with semantic versioning
|
# Emit a machine-readable plan
|
||||||
gitzone commit
|
gitzone format plan --json
|
||||||
|
|
||||||
# 5. Deploy (if CI/CD configured)
|
# Apply changes
|
||||||
# Automatic via git push
|
gitzone format --write
|
||||||
|
|
||||||
|
# Apply without prompt
|
||||||
|
gitzone format --write --yes
|
||||||
```
|
```
|
||||||
|
|
||||||
### Multi-Repository Management
|
Formatters include cleanup, smartconfig normalization, dependency license checks, package metadata normalization, template updates, `.gitignore`, TypeScript config, Prettier, README existence checks, and configured copy operations.
|
||||||
|
|
||||||
|
## Development Services
|
||||||
|
|
||||||
|
`gitzone services` manages local Docker-backed services for development projects.
|
||||||
|
|
||||||
|
Supported services:
|
||||||
|
|
||||||
|
| Service | Aliases |
|
||||||
|
| --- | --- |
|
||||||
|
| MongoDB | `mongo`, `mongodb` |
|
||||||
|
| MinIO | `minio`, `s3` |
|
||||||
|
| Elasticsearch | `elasticsearch`, `es` |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start configured services
|
||||||
|
gitzone services start
|
||||||
|
|
||||||
|
# Enable specific services non-interactively
|
||||||
|
gitzone services set mongodb,minio
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
gitzone services status
|
||||||
|
|
||||||
|
# Print MongoDB Compass connection string
|
||||||
|
gitzone services compass
|
||||||
|
|
||||||
|
# Show logs
|
||||||
|
gitzone services logs mongo 50
|
||||||
|
|
||||||
|
# Stop containers but keep data
|
||||||
|
gitzone services stop
|
||||||
|
|
||||||
|
# Remove containers and data
|
||||||
|
gitzone services clean
|
||||||
|
```
|
||||||
|
|
||||||
|
Service config is stored in `.nogit/env.json`. Data is stored below `.nogit/`, so it stays out of Git.
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
Start new projects with built-in scaffolds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gitzone template npm
|
||||||
|
gitzone template service
|
||||||
|
gitzone template website
|
||||||
|
gitzone template wcc
|
||||||
|
```
|
||||||
|
|
||||||
|
Templates are rendered through SmartScaf and then can be normalized with `gitzone format`.
|
||||||
|
|
||||||
|
## Meta Repositories
|
||||||
|
|
||||||
|
Use `gitzone meta` when one workspace coordinates multiple repositories.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Set up meta repository
|
|
||||||
gitzone meta init
|
gitzone meta init
|
||||||
|
gitzone meta add frontend https://example.com/org/frontend.git
|
||||||
# 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
|
gitzone meta update
|
||||||
|
gitzone meta remove frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
### Safe Formatting with Rollback
|
## Other Utilities
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Preview changes
|
# Docker cleanup
|
||||||
gitzone format --dry-run
|
gitzone docker prune
|
||||||
|
|
||||||
# 2. Save plan for review
|
# Open GitLab CI settings or pipelines for the current repo
|
||||||
gitzone format --save-plan format-changes.json
|
gitzone open ci
|
||||||
|
gitzone open pipelines
|
||||||
|
|
||||||
# 3. Apply formatting
|
# Deprecate an old npm package interactively
|
||||||
gitzone format --from-plan format-changes.json
|
gitzone deprecate
|
||||||
|
|
||||||
# 4. If something goes wrong, rollback
|
# Prepare a project for local work
|
||||||
gitzone format --rollback
|
gitzone start
|
||||||
|
|
||||||
|
# Generate a short unique ID
|
||||||
|
gitzone helpers shortid
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔌 Integrations
|
## Troubleshooting
|
||||||
|
|
||||||
### CI/CD Platforms
|
Format only previews changes:
|
||||||
- **GitLab CI** - Full pipeline support with templates
|
|
||||||
- **GitHub Actions** - Automated workflows
|
|
||||||
- **Docker** - Container-based deployments
|
|
||||||
|
|
||||||
### Development Tools
|
```bash
|
||||||
- **TypeScript** - First-class support
|
gitzone format --write
|
||||||
- **Prettier** - Code formatting
|
```
|
||||||
- **ESLint** - Linting (via format modules)
|
|
||||||
- **npm/pnpm** - Package management
|
|
||||||
|
|
||||||
### Version Control
|
Release says there is nothing to release:
|
||||||
- **Git** - Deep integration
|
|
||||||
- **Semantic Versioning** - Automatic version bumping
|
|
||||||
- **Conventional Commits** - Standardized commit messages
|
|
||||||
|
|
||||||
## 💡 Pro Tips
|
```bash
|
||||||
|
# Make sure commits have populated the Pending changelog section
|
||||||
|
gitzone commit
|
||||||
|
```
|
||||||
|
|
||||||
1. **Use aliases**: Add `alias gz='gitzone'` to your shell profile
|
Docker services fail to start:
|
||||||
2. **Combine commands**: `gitzone format --yes && gitzone commit`
|
|
||||||
3. **Leverage templates**: Start projects right with proven structures
|
|
||||||
4. **Enable caching**: Dramatically speeds up formatting operations
|
|
||||||
5. **Save format plans**: Review changes before applying in production
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### Format Command Shows "Cancelled"
|
|
||||||
If the format command shows cancelled even after confirming:
|
|
||||||
- Check your `npmextra.json` configuration
|
|
||||||
- Try with `--yes` flag to skip confirmation
|
|
||||||
- Use `--verbose` for detailed output
|
|
||||||
|
|
||||||
### Docker Commands Fail
|
|
||||||
Ensure Docker daemon is running:
|
|
||||||
```bash
|
```bash
|
||||||
docker info
|
docker info
|
||||||
|
gitzone services status
|
||||||
|
gitzone services reconfigure
|
||||||
```
|
```
|
||||||
|
|
||||||
### Template Creation Issues
|
Config looks outdated:
|
||||||
Verify npm/pnpm is properly configured:
|
|
||||||
```bash
|
```bash
|
||||||
npm config get registry
|
gitzone config migrate 2
|
||||||
|
gitzone config show --json
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📈 Performance
|
|
||||||
|
|
||||||
gitzone is optimized for speed:
|
|
||||||
- **Parallel processing** for format operations
|
|
||||||
- **Smart caching** to avoid redundant work
|
|
||||||
- **Incremental updates** for meta repositories
|
|
||||||
- **Minimal dependencies** for fast installation
|
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
This repository contains open-source code 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.
|
||||||
|
|
||||||
### Trademarks
|
### Trademarks
|
||||||
|
|
||||||
This 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.
|
This 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 or third parties, 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 or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||||
|
|
||||||
### Company Information
|
### Company Information
|
||||||
|
|
||||||
Task Venture Capital GmbH
|
Task Venture Capital GmbH
|
||||||
Registered at District court Bremen HRB 35230 HB, Germany
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
|
|||||||
+104
-142
@@ -1,170 +1,132 @@
|
|||||||
# Gitzone Format Module Improvement Plan
|
# GitZone Services Command Implementation Plan
|
||||||
|
|
||||||
Please reread /home/philkunz/.claude/CLAUDE.md before proceeding with any implementation.
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
This plan outlines improvements for the gitzone format module to enhance its functionality, reliability, and maintainability.
|
|
||||||
|
|
||||||
## Phase 1: Core Improvements (High Priority) - COMPLETED ✅
|
Implement the `gitzone services` command to manage MongoDB and MinIO containers for development projects.
|
||||||
|
|
||||||
### 1. Enhanced Error Handling & Recovery ✅
|
## Tasks
|
||||||
- [x] Implement rollback mechanism for failed format operations
|
|
||||||
- [x] Add detailed error messages with recovery suggestions
|
|
||||||
- [x] Create a `--dry-run` flag to preview changes before applying
|
|
||||||
- [x] Add transaction-like behavior: all-or-nothing formatting
|
|
||||||
- [x] Implement plan → action workflow as default behavior
|
|
||||||
|
|
||||||
### 2. Complete Missing Functionality ✅
|
### Module Structure Setup
|
||||||
- [x] Implement the `ensureDependency` function in format.packagejson.ts
|
|
||||||
- [x] Develop the copy module for file pattern-based copying
|
|
||||||
- [x] Add dependency version constraint management
|
|
||||||
- [x] Support workspace/monorepo configurations (via configuration)
|
|
||||||
|
|
||||||
### 3. Configuration & Flexibility ✅
|
- [x] Create `ts/mod_services/` directory
|
||||||
- [x] Extend npmextra.json gitzone configuration section
|
- [x] Create `mod.plugins.ts` with required imports
|
||||||
- [x] Allow custom license exclusion/inclusion lists
|
- [x] Create `helpers.ts` with utility functions
|
||||||
- [x] Make format steps configurable (skip/include specific modules)
|
- [x] Create `classes.serviceconfiguration.ts` for config handling
|
||||||
- [x] Support custom template directories (via configuration)
|
- [x] Create `classes.dockercontainer.ts` for Docker operations
|
||||||
- [x] Add format profiles for different project types
|
- [x] Create `classes.servicemanager.ts` for service management
|
||||||
|
- [x] Create `index.ts` with main command logic
|
||||||
|
|
||||||
### 4. Architecture Changes ✅
|
### Core Functionality
|
||||||
- [x] Introduce a `FormatContext` class to manage state across modules
|
|
||||||
- [x] Create abstract `BaseFormatter` class for consistent module structure
|
|
||||||
- [x] Implement event system for inter-module communication (via context)
|
|
||||||
- [x] Add validation layer before format execution
|
|
||||||
- [x] Implement `FormatPlanner` class for plan → action workflow
|
|
||||||
|
|
||||||
## Phase 2: Performance & Reporting (Medium Priority) - COMPLETED ✅
|
- [x] Implement ServiceConfiguration class
|
||||||
|
- [x] Load/create `.nogit/env.json` configuration
|
||||||
|
- [x] Generate random available ports (20000-30000 range)
|
||||||
|
- [x] Preserve existing custom values
|
||||||
|
- [x] Provide default values for missing fields
|
||||||
|
|
||||||
### 5. Performance Optimizations ✅
|
- [x] Implement DockerContainer class
|
||||||
- [x] Implement parallel execution for independent format modules
|
- [x] Check container status
|
||||||
- [x] Add file change detection to skip unchanged files
|
- [x] Start/stop/restart containers
|
||||||
- [x] Create format cache to track last formatted state
|
- [x] Execute Docker commands
|
||||||
- [x] Optimize Prettier runs by batching files
|
- [x] Handle container logs
|
||||||
|
- [x] Manage volumes and port bindings
|
||||||
|
|
||||||
### 6. Enhanced Reporting & Visibility ✅
|
- [x] Implement ServiceManager class
|
||||||
- [x] Generate comprehensive format report showing all changes
|
- [x] Manage MongoDB containers
|
||||||
- [x] Add diff view for file modifications
|
- [x] Manage MinIO containers
|
||||||
- [x] Create verbose logging option
|
- [x] Handle container lifecycle
|
||||||
- [x] Add format statistics (files changed, time taken, etc.)
|
- [x] Generate project-specific container names
|
||||||
|
- [x] Manage data directories in `.nogit/`
|
||||||
|
- [x] Generate MongoDB Compass connection strings
|
||||||
|
|
||||||
## Phase 3: Advanced Features (Lower Priority) - PARTIALLY COMPLETED
|
### Commands Implementation
|
||||||
|
|
||||||
### 7. Better Integration & Extensibility ⏳
|
- [x] `start` command - Start services (mongo|s3|all)
|
||||||
- [ ] Create plugin system for custom format modules
|
- [x] `stop` command - Stop services (mongo|s3|all)
|
||||||
- [ ] Add hooks for pre/post format operations
|
- [x] `restart` command - Restart services (mongo|s3|all)
|
||||||
- [ ] Support custom validation rules
|
- [x] `status` command - Show service status
|
||||||
- [ ] Integrate with git hooks for pre-commit formatting
|
- [x] `config` command - Show current configuration
|
||||||
|
- [x] `compass` command - Show MongoDB Compass connection string
|
||||||
|
- [x] `logs` command - Show service logs with line count
|
||||||
|
- [x] `remove` command - Remove containers (preserve data)
|
||||||
|
- [x] `clean` command - Remove containers and data
|
||||||
|
|
||||||
### 8. Improved Template Integration ⏳
|
### Integration
|
||||||
- [ ] Better error handling when smartscaf operations fail
|
|
||||||
- [ ] Add pre/post template hooks for custom processing
|
|
||||||
- [ ] Validate template results before proceeding with format
|
|
||||||
- [ ] Support skipping template updates via configuration
|
|
||||||
|
|
||||||
### 9. Enhanced License Management ⏳
|
- [x] Add `@push.rocks/smartshell` to main plugins.ts
|
||||||
- [ ] Make license checking configurable (partial)
|
- [x] Add `@push.rocks/smartnetwork` to main plugins.ts
|
||||||
- [ ] Add license compatibility matrix
|
- [x] Add `@push.rocks/smartinteraction` to main plugins.ts
|
||||||
- [x] Support license exceptions for specific packages
|
- [x] Register services command in `gitzone.cli.ts`
|
||||||
- [ ] Generate license report for compliance
|
|
||||||
|
|
||||||
### 10. Better Package.json Management ⏳
|
### Features
|
||||||
- [ ] Smart dependency sorting and grouping
|
|
||||||
- [ ] Automated script generation based on project type
|
|
||||||
- [ ] Support for pnpm workspace configurations
|
|
||||||
- [ ] Validation of package.json schema
|
|
||||||
|
|
||||||
### 11. Quality of Life Improvements ⏳
|
- [x] Auto-configuration with smart defaults
|
||||||
- [ ] Interactive mode for format configuration
|
- [x] Random port assignment to avoid conflicts
|
||||||
- [ ] Undo/redo capability for format operations
|
- [x] Project isolation with unique container names
|
||||||
- [ ] Format presets for common scenarios
|
- [x] Data persistence in `.nogit/` directories
|
||||||
- [x] Better progress indicators and user feedback
|
- [x] Status display (running/stopped/not installed)
|
||||||
|
- [x] Interactive confirmations for destructive operations
|
||||||
|
- [x] Colored console output
|
||||||
|
- [x] MinIO bucket auto-creation
|
||||||
|
- [x] MongoDB Compass connection string with network IP
|
||||||
|
|
||||||
## Implementation Status
|
### Testing
|
||||||
|
|
||||||
### ✅ Completed Features
|
- [ ] Test service start/stop operations
|
||||||
|
- [ ] Test configuration creation and updates
|
||||||
|
- [ ] Test port collision handling
|
||||||
|
- [ ] Test data persistence
|
||||||
|
- [ ] Test MongoDB Compass connection string generation
|
||||||
|
- [ ] Test all command variations
|
||||||
|
|
||||||
1. **Rollback Mechanism**
|
## Configuration Format
|
||||||
- Full backup/restore functionality
|
|
||||||
- Manifest tracking and integrity checks
|
|
||||||
- CLI commands for rollback operations
|
|
||||||
|
|
||||||
2. **Plan → Action Workflow**
|
```json
|
||||||
- Two-phase approach (analyze then execute)
|
{
|
||||||
- Interactive confirmation
|
"PROJECT_NAME": "derived-from-package-name",
|
||||||
- Dry-run support
|
"MONGODB_HOST": "localhost",
|
||||||
|
"MONGODB_NAME": "project-name",
|
||||||
|
"MONGODB_PORT": "random-port",
|
||||||
|
"MONGODB_USER": "defaultadmin",
|
||||||
|
"MONGODB_PASS": "defaultpass",
|
||||||
|
"S3_HOST": "localhost",
|
||||||
|
"S3_PORT": "random-port",
|
||||||
|
"S3_CONSOLE_PORT": "s3-port+1",
|
||||||
|
"S3_USER": "defaultadmin",
|
||||||
|
"S3_PASS": "defaultpass",
|
||||||
|
"S3_BUCKET": "project-name-documents"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
3. **Configuration System**
|
## Command Examples
|
||||||
- Comprehensive npmextra.json support
|
|
||||||
- Module control (skip/only/order)
|
|
||||||
- Cache configuration
|
|
||||||
- Parallel execution settings
|
|
||||||
|
|
||||||
4. **Performance Improvements**
|
```bash
|
||||||
- Parallel execution by dependency analysis
|
gitzone services start # Start all services
|
||||||
- File change caching
|
gitzone services start mongo # Start only MongoDB
|
||||||
- Prettier batching
|
gitzone services stop # Stop all services
|
||||||
- Execution time tracking
|
gitzone services status # Check service status
|
||||||
|
gitzone services config # Show configuration
|
||||||
|
gitzone services compass # Show MongoDB Compass connection string
|
||||||
|
gitzone services logs mongo 50 # Show last 50 lines of MongoDB logs
|
||||||
|
gitzone services remove # Remove containers (preserve data)
|
||||||
|
gitzone services clean # Remove containers and data
|
||||||
|
```
|
||||||
|
|
||||||
5. **Reporting & Statistics**
|
## Progress Notes
|
||||||
- Detailed diff views
|
|
||||||
- Execution statistics
|
|
||||||
- Verbose logging mode
|
|
||||||
- Save reports to file
|
|
||||||
|
|
||||||
6. **Architecture Improvements**
|
Implementation started: 2025-08-14
|
||||||
- BaseFormatter abstract class
|
Implementation completed: 2025-08-14
|
||||||
- FormatContext for state management
|
|
||||||
- DependencyAnalyzer for parallel execution
|
|
||||||
- Type-safe interfaces
|
|
||||||
|
|
||||||
### 🚧 Partially Completed
|
## Summary
|
||||||
|
|
||||||
1. **License Management**
|
Successfully implemented the `gitzone services` command in TypeScript, providing a complete replacement for the `services.sh` shell script. The implementation includes:
|
||||||
- Basic configuration support
|
|
||||||
- Exception handling for specific packages
|
|
||||||
- Need: compatibility matrix, compliance reports
|
|
||||||
|
|
||||||
2. **Package.json Management**
|
1. **Complete Docker service management** for MongoDB and MinIO containers
|
||||||
- Basic ensureDependency implementation
|
2. **Smart configuration management** with automatic port assignment and conflict avoidance
|
||||||
- Need: smart sorting, script generation, validation
|
3. **MongoDB Compass support** with network IP detection for remote connections
|
||||||
|
4. **Project isolation** using project-specific container names
|
||||||
|
5. **Data persistence** in `.nogit/` directories
|
||||||
|
6. **Interactive confirmations** for destructive operations
|
||||||
|
7. **Comprehensive command set** including start, stop, restart, status, config, compass, logs, remove, and clean commands
|
||||||
|
|
||||||
### ⏳ Not Started
|
The module is fully integrated into the gitzone CLI and ready for testing.
|
||||||
|
|
||||||
1. **Plugin System**
|
|
||||||
- Need to design plugin API
|
|
||||||
- Hook system for pre/post operations
|
|
||||||
- Custom validation rules
|
|
||||||
|
|
||||||
2. **Git Integration**
|
|
||||||
- Pre-commit hooks
|
|
||||||
- Automatic formatting on commit
|
|
||||||
|
|
||||||
3. **Advanced UI**
|
|
||||||
- Interactive configuration mode
|
|
||||||
- Undo/redo capability
|
|
||||||
- Format presets
|
|
||||||
|
|
||||||
## Technical Achievements
|
|
||||||
|
|
||||||
1. **Type Safety**: All new code uses TypeScript interfaces and types
|
|
||||||
2. **Error Handling**: Comprehensive try-catch blocks with rollback
|
|
||||||
3. **API Compatibility**: Updated to use latest smartfile/smartnpm APIs
|
|
||||||
4. **Testing**: Ready for comprehensive test suite
|
|
||||||
5. **Performance**: Significant improvements through caching and parallelization
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. Write comprehensive tests for all new functionality
|
|
||||||
2. Create user documentation for new features
|
|
||||||
3. Consider plugin API design for extensibility
|
|
||||||
4. Implement remaining Phase 3 features based on user feedback
|
|
||||||
5. Performance benchmarking and optimization
|
|
||||||
|
|
||||||
## Success Metrics Achieved
|
|
||||||
|
|
||||||
- ✅ Reduced error rates through rollback mechanism
|
|
||||||
- ✅ Faster execution through parallel processing and caching
|
|
||||||
- ✅ Enhanced user control through configuration
|
|
||||||
- ✅ Better visibility through reporting and statistics
|
|
||||||
- ✅ Improved maintainability through better architecture
|
|
||||||
|
|||||||
Submodule
+1
Submodule test added at 0b89443584
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/cli',
|
name: '@git.zone/cli',
|
||||||
version: '1.16.1',
|
version: '2.17.0',
|
||||||
description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
|
description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,11 +38,21 @@ export class GitzoneConfig {
|
|||||||
public data: IGitzoneConfigData;
|
public data: IGitzoneConfigData;
|
||||||
|
|
||||||
public async readConfigFromCwd() {
|
public async readConfigFromCwd() {
|
||||||
const npmextraInstance = new plugins.npmextra.Npmextra(paths.cwd);
|
const smartconfigInstance = new plugins.smartconfig.Smartconfig(paths.cwd);
|
||||||
this.data = npmextraInstance.dataFor<IGitzoneConfigData>('gitzone', {});
|
this.data = smartconfigInstance.dataFor<IGitzoneConfigData>('@git.zone/cli', {});
|
||||||
this.data.npmciOptions = npmextraInstance.dataFor<IGitzoneConfigData['npmciOptions']>('npmci', {
|
|
||||||
npmAccessLevel: 'public',
|
// Read szci config for backward compatibility
|
||||||
});
|
const szciConfig = smartconfigInstance.dataFor<any>('@ship.zone/szci', {});
|
||||||
|
|
||||||
|
// Prefer accessLevel from @git.zone/cli.release, fallback to @ship.zone/szci.npmAccessLevel
|
||||||
|
const accessLevel =
|
||||||
|
(this.data as any)?.release?.accessLevel ||
|
||||||
|
szciConfig?.npmAccessLevel ||
|
||||||
|
'public';
|
||||||
|
|
||||||
|
this.data.npmciOptions = {
|
||||||
|
npmAccessLevel: accessLevel,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import type { TGitzoneProjectType } from './classes.gitzoneconfig.js';
|
|||||||
* the Project class is a tool to work with a gitzone project
|
* the Project class is a tool to work with a gitzone project
|
||||||
*/
|
*/
|
||||||
export class Project {
|
export class Project {
|
||||||
public static async fromCwd() {
|
public static async fromCwd(options: { requireProjectType?: boolean } = {}) {
|
||||||
const gitzoneConfig = await GitzoneConfig.fromCwd();
|
const gitzoneConfig = await GitzoneConfig.fromCwd();
|
||||||
const project = new Project(gitzoneConfig);
|
const project = new Project(gitzoneConfig);
|
||||||
if (!project.gitzoneConfig.data.projectType) {
|
const requireProjectType = options.requireProjectType ?? true;
|
||||||
|
if (requireProjectType && !project.gitzoneConfig.data.projectType) {
|
||||||
throw new Error('Please define a project type');
|
throw new Error('Please define a project type');
|
||||||
}
|
}
|
||||||
return project;
|
return project;
|
||||||
|
|||||||
+94
-54
@@ -1,24 +1,40 @@
|
|||||||
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 { GitzoneConfig } from "./classes.gitzoneconfig.js";
|
||||||
|
import { getRawCliMode } from "./helpers.climode.js";
|
||||||
|
import { commitinfo } from "./00_commitinfo_data.js";
|
||||||
|
|
||||||
const gitzoneSmartcli = new plugins.smartcli.Smartcli();
|
const gitzoneSmartcli = new plugins.smartcli.Smartcli();
|
||||||
|
|
||||||
export let run = async () => {
|
export let run = async () => {
|
||||||
const done = plugins.smartpromise.defer();
|
const done = plugins.smartpromise.defer();
|
||||||
|
const rawCliMode = await getRawCliMode();
|
||||||
|
|
||||||
// get packageInfo
|
// get packageInfo
|
||||||
const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||||
|
const projectInfoVersion = (projectInfo.npm as any)?.version;
|
||||||
|
const packageVersion =
|
||||||
|
typeof projectInfoVersion === "string" && projectInfoVersion.length > 0
|
||||||
|
? projectInfoVersion
|
||||||
|
: commitinfo.version;
|
||||||
|
|
||||||
// check for updates
|
// check for updates
|
||||||
const smartupdateInstance = new plugins.smartupdate.SmartUpdate();
|
if (rawCliMode.checkUpdates) {
|
||||||
await smartupdateInstance.check(
|
const smartupdateInstance = new plugins.smartupdate.SmartUpdate();
|
||||||
'gitzone',
|
try {
|
||||||
projectInfo.npm.version,
|
await smartupdateInstance.check(
|
||||||
'http://gitzone.gitlab.io/gitzone/changelog.html',
|
"gitzone",
|
||||||
);
|
packageVersion,
|
||||||
console.log('---------------------------------------------');
|
"http://gitzone.gitlab.io/gitzone/changelog.html",
|
||||||
gitzoneSmartcli.addVersion(projectInfo.npm.version);
|
);
|
||||||
|
} catch {
|
||||||
|
// Update checks must never block actual CLI commands.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rawCliMode.output === "human") {
|
||||||
|
console.log("---------------------------------------------");
|
||||||
|
}
|
||||||
|
gitzoneSmartcli.addVersion(packageVersion);
|
||||||
|
|
||||||
// ======> Standard task <======
|
// ======> Standard task <======
|
||||||
|
|
||||||
@@ -26,8 +42,13 @@ export let run = async () => {
|
|||||||
* standard task
|
* standard task
|
||||||
*/
|
*/
|
||||||
gitzoneSmartcli.standardCommand().subscribe(async (argvArg) => {
|
gitzoneSmartcli.standardCommand().subscribe(async (argvArg) => {
|
||||||
const modStandard = await import('./mod_standard/index.js');
|
const modStandard = await import("./mod_standard/index.js");
|
||||||
await modStandard.run();
|
await modStandard.run(argvArg);
|
||||||
|
});
|
||||||
|
|
||||||
|
gitzoneSmartcli.addCommand("help").subscribe(async (argvArg) => {
|
||||||
|
const modStandard = await import("./mod_standard/index.js");
|
||||||
|
await modStandard.run(argvArg);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ======> Specific tasks <======
|
// ======> Specific tasks <======
|
||||||
@@ -35,102 +56,121 @@ export let run = async () => {
|
|||||||
/**
|
/**
|
||||||
* commit something
|
* commit something
|
||||||
*/
|
*/
|
||||||
gitzoneSmartcli.addCommand('commit').subscribe(async (argvArg) => {
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a release from pending changelog entries
|
||||||
|
*/
|
||||||
|
gitzoneSmartcli.addCommand("release").subscribe(async (argvArg) => {
|
||||||
|
const modRelease = await import("./mod_release/index.js");
|
||||||
|
await modRelease.run(argvArg);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* deprecate a package on npm
|
* deprecate a package on npm
|
||||||
*/
|
*/
|
||||||
gitzoneSmartcli.addCommand('deprecate').subscribe(async (argvArg) => {
|
gitzoneSmartcli.addCommand("deprecate").subscribe(async (argvArg) => {
|
||||||
const modDeprecate = await import('./mod_deprecate/index.js');
|
const modDeprecate = await import("./mod_deprecate/index.js");
|
||||||
await modDeprecate.run();
|
await modDeprecate.run();
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* docker
|
* docker
|
||||||
*/
|
*/
|
||||||
gitzoneSmartcli.addCommand('docker').subscribe(async (argvArg) => {
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update all files that comply with the gitzone standard
|
* Update all files that comply with the gitzone standard
|
||||||
*/
|
*/
|
||||||
gitzoneSmartcli.addCommand('format').subscribe(async (argvArg) => {
|
gitzoneSmartcli.addCommand("format").subscribe(async (argvArg) => {
|
||||||
const config = GitzoneConfig.fromCwd();
|
const config = GitzoneConfig.fromCwd();
|
||||||
const modFormat = await import('./mod_format/index.js');
|
const modFormat = await import("./mod_format/index.js");
|
||||||
|
|
||||||
// Handle rollback commands
|
|
||||||
if (argvArg.rollback) {
|
|
||||||
await modFormat.handleRollback(argvArg.rollback);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (argvArg['list-backups']) {
|
|
||||||
await modFormat.handleListBackups();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (argvArg['clean-backups']) {
|
|
||||||
await modFormat.handleCleanBackups();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle format with options
|
// Handle format with options
|
||||||
|
// Default is dry-mode, use --write/-w to apply changes
|
||||||
await modFormat.run({
|
await modFormat.run({
|
||||||
dryRun: argvArg['dry-run'],
|
...argvArg,
|
||||||
|
write: argvArg.write || argvArg.w,
|
||||||
|
dryRun: argvArg["dry-run"],
|
||||||
yes: argvArg.yes,
|
yes: argvArg.yes,
|
||||||
planOnly: argvArg['plan-only'],
|
planOnly: argvArg["plan-only"],
|
||||||
savePlan: argvArg['save-plan'],
|
savePlan: argvArg["save-plan"],
|
||||||
fromPlan: argvArg['from-plan'],
|
fromPlan: argvArg["from-plan"],
|
||||||
detailed: argvArg.detailed,
|
detailed: argvArg.detailed,
|
||||||
interactive: argvArg.interactive !== false,
|
interactive: argvArg.interactive !== false,
|
||||||
parallel: argvArg.parallel !== false,
|
verbose: argvArg.verbose,
|
||||||
verbose: argvArg.verbose
|
diff: argvArg.diff,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* run meta commands
|
* run meta commands
|
||||||
*/
|
*/
|
||||||
gitzoneSmartcli.addCommand('meta').subscribe(async (argvArg) => {
|
gitzoneSmartcli.addCommand("meta").subscribe(async (argvArg) => {
|
||||||
const config = GitzoneConfig.fromCwd();
|
const config = GitzoneConfig.fromCwd();
|
||||||
const modMeta = await import('./mod_meta/index.js');
|
const modMeta = await import("./mod_meta/index.js");
|
||||||
modMeta.run(argvArg);
|
modMeta.run(argvArg);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* open assets
|
* open assets
|
||||||
*/
|
*/
|
||||||
gitzoneSmartcli.addCommand('open').subscribe(async (argvArg) => {
|
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);
|
modOpen.run(argvArg);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* add a readme to a project
|
* add a readme to a project
|
||||||
*/
|
*/
|
||||||
gitzoneSmartcli.addCommand('template').subscribe(async (argvArg) => {
|
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);
|
modTemplate.run(argvArg);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* start working on a project
|
* start working on a project
|
||||||
*/
|
*/
|
||||||
gitzoneSmartcli.addCommand('start').subscribe(async (argvArg) => {
|
gitzoneSmartcli.addCommand("start").subscribe(async (argvArg) => {
|
||||||
const modTemplate = await import('./mod_start/index.js');
|
const modTemplate = await import("./mod_start/index.js");
|
||||||
modTemplate.run(argvArg);
|
modTemplate.run(argvArg);
|
||||||
});
|
});
|
||||||
|
|
||||||
gitzoneSmartcli.addCommand('helpers').subscribe(async (argvArg) => {
|
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);
|
modHelpers.run(argvArg);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* manage the global @git.zone toolchain
|
||||||
|
*/
|
||||||
|
gitzoneSmartcli.addCommand("tools").subscribe(async (argvArg) => {
|
||||||
|
const modTools = await import("./mod_tools/index.js");
|
||||||
|
await modTools.run(argvArg);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* manage release configuration
|
||||||
|
*/
|
||||||
|
gitzoneSmartcli.addCommand("config").subscribe(async (argvArg) => {
|
||||||
|
const modConfig = await import("./mod_config/index.js");
|
||||||
|
await modConfig.run(argvArg);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* manage development services (MongoDB, S3/MinIO)
|
||||||
|
*/
|
||||||
|
gitzoneSmartcli.addCommand("services").subscribe(async (argvArg) => {
|
||||||
|
const modServices = await import("./mod_services/index.js");
|
||||||
|
await modServices.run(argvArg);
|
||||||
|
});
|
||||||
|
|
||||||
// start parsing of the cli
|
// start parsing of the cli
|
||||||
gitzoneSmartcli.startParse();
|
gitzoneSmartcli.startParse();
|
||||||
return await done.promise;
|
return await done.promise;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import * as plugins from './plugins.js';
|
|||||||
export const logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo);
|
export const logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo);
|
||||||
|
|
||||||
// Add console destination
|
// Add console destination
|
||||||
const consoleDestination = new plugins.smartlogDestinationLocal.DestinationLocal();
|
const consoleDestination =
|
||||||
|
new plugins.smartlogDestinationLocal.DestinationLocal();
|
||||||
logger.addLogDestination(consoleDestination);
|
logger.addLogDestination(consoleDestination);
|
||||||
|
|
||||||
// Verbose logging helper
|
// Verbose logging helper
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import * as plugins from "./plugins.js";
|
||||||
|
|
||||||
|
export type TChangelogBucket =
|
||||||
|
| "Breaking Changes"
|
||||||
|
| "Features"
|
||||||
|
| "Fixes"
|
||||||
|
| "Documentation"
|
||||||
|
| "Maintenance";
|
||||||
|
|
||||||
|
export interface IChangelogEntry {
|
||||||
|
type: string;
|
||||||
|
scope: string;
|
||||||
|
message: string;
|
||||||
|
details?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPendingChangelog {
|
||||||
|
block: string;
|
||||||
|
isEmpty: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucketForCommitType = (commitType: string): TChangelogBucket => {
|
||||||
|
switch (commitType) {
|
||||||
|
case "BREAKING CHANGE":
|
||||||
|
return "Breaking Changes";
|
||||||
|
case "feat":
|
||||||
|
return "Features";
|
||||||
|
case "fix":
|
||||||
|
return "Fixes";
|
||||||
|
case "docs":
|
||||||
|
return "Documentation";
|
||||||
|
default:
|
||||||
|
return "Maintenance";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const readChangelog = async (filePath: string): Promise<string> => {
|
||||||
|
if (!(await plugins.smartfs.file(filePath).exists())) {
|
||||||
|
return "# Changelog\n\n";
|
||||||
|
}
|
||||||
|
return (await plugins.smartfs.file(filePath).encoding("utf8").read()) as string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeChangelog = async (filePath: string, content: string): Promise<void> => {
|
||||||
|
await plugins.smartfs.file(filePath).encoding("utf8").write(content.endsWith("\n") ? content : `${content}\n`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findPendingSection = (
|
||||||
|
content: string,
|
||||||
|
sectionName: string,
|
||||||
|
): { start: number; bodyStart: number; end: number } | null => {
|
||||||
|
const headingRegex = new RegExp(`^##\\s+${sectionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
|
||||||
|
const match = headingRegex.exec(content);
|
||||||
|
if (!match || match.index === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyStart = match.index + match[0].length;
|
||||||
|
const rest = content.slice(bodyStart);
|
||||||
|
const nextHeadingMatch = /^##\s+/m.exec(rest);
|
||||||
|
const end = nextHeadingMatch ? bodyStart + nextHeadingMatch.index : content.length;
|
||||||
|
return { start: match.index, bodyStart, end };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ensurePendingSection = async (
|
||||||
|
filePath: string,
|
||||||
|
sectionName = "Pending",
|
||||||
|
): Promise<string> => {
|
||||||
|
let content = await readChangelog(filePath);
|
||||||
|
if (findPendingSection(content, sectionName)) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingSection = `## ${sectionName}\n\n`;
|
||||||
|
const titleMatch = /^#\s+.+$/m.exec(content);
|
||||||
|
if (titleMatch && titleMatch.index !== undefined) {
|
||||||
|
const insertAt = titleMatch.index + titleMatch[0].length;
|
||||||
|
content = `${content.slice(0, insertAt)}\n\n${pendingSection}${content.slice(insertAt).replace(/^\n+/, "")}`;
|
||||||
|
} else {
|
||||||
|
content = `# Changelog\n\n${pendingSection}${content}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeChangelog(filePath, content);
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appendPendingChangelogEntry = async (
|
||||||
|
filePath: string,
|
||||||
|
sectionName: string,
|
||||||
|
entry: IChangelogEntry,
|
||||||
|
): Promise<void> => {
|
||||||
|
let content = await ensurePendingSection(filePath, sectionName);
|
||||||
|
const pendingSection = findPendingSection(content, sectionName)!;
|
||||||
|
let pendingBody = content.slice(pendingSection.bodyStart, pendingSection.end);
|
||||||
|
const bucket = bucketForCommitType(entry.type);
|
||||||
|
const bucketHeading = `### ${bucket}`;
|
||||||
|
|
||||||
|
const entryLines = [`- ${entry.message}${entry.scope ? ` (${entry.scope})` : ""}`];
|
||||||
|
for (const detail of entry.details || []) {
|
||||||
|
entryLines.push(` - ${detail}`);
|
||||||
|
}
|
||||||
|
const renderedEntry = entryLines.join("\n");
|
||||||
|
|
||||||
|
const bucketRegex = new RegExp(`^###\\s+${bucket.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
|
||||||
|
const bucketMatch = bucketRegex.exec(pendingBody);
|
||||||
|
if (!bucketMatch || bucketMatch.index === undefined) {
|
||||||
|
pendingBody = `${pendingBody.trimEnd()}\n\n${bucketHeading}\n\n${renderedEntry}\n`;
|
||||||
|
} else {
|
||||||
|
const bucketBodyStart = bucketMatch.index + bucketMatch[0].length;
|
||||||
|
const afterBucket = pendingBody.slice(bucketBodyStart);
|
||||||
|
const nextBucketMatch = /^###\s+/m.exec(afterBucket);
|
||||||
|
const insertAt = nextBucketMatch ? bucketBodyStart + nextBucketMatch.index : pendingBody.length;
|
||||||
|
const beforeInsert = pendingBody.slice(0, insertAt).trimEnd();
|
||||||
|
const afterInsert = pendingBody.slice(insertAt).replace(/^\n+/, "");
|
||||||
|
pendingBody = `${beforeInsert}\n${renderedEntry}\n\n${afterInsert}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = `${content.slice(0, pendingSection.bodyStart)}\n${pendingBody.trim()}\n\n${content.slice(pendingSection.end).replace(/^\n+/, "")}`;
|
||||||
|
await writeChangelog(filePath, content);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readPendingChangelog = async (
|
||||||
|
filePath: string,
|
||||||
|
sectionName = "Pending",
|
||||||
|
): Promise<IPendingChangelog> => {
|
||||||
|
const content = await ensurePendingSection(filePath, sectionName);
|
||||||
|
const pendingSection = findPendingSection(content, sectionName)!;
|
||||||
|
const block = content.slice(pendingSection.bodyStart, pendingSection.end).trim();
|
||||||
|
return {
|
||||||
|
block,
|
||||||
|
isEmpty: block.length === 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inferVersionTypeFromPending = (pendingBlock: string): "patch" | "minor" | "major" => {
|
||||||
|
if (/^###\s+Breaking Changes\s*$/m.test(pendingBlock)) {
|
||||||
|
return "major";
|
||||||
|
}
|
||||||
|
if (/^###\s+Features\s*$/m.test(pendingBlock)) {
|
||||||
|
return "minor";
|
||||||
|
}
|
||||||
|
return "patch";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const movePendingToVersion = async (
|
||||||
|
filePath: string,
|
||||||
|
sectionName: string,
|
||||||
|
versionHeading: string,
|
||||||
|
version: string,
|
||||||
|
dateString: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
let content = await ensurePendingSection(filePath, sectionName);
|
||||||
|
const pendingSection = findPendingSection(content, sectionName)!;
|
||||||
|
const pendingBlock = content.slice(pendingSection.bodyStart, pendingSection.end).trim();
|
||||||
|
if (!pendingBlock) {
|
||||||
|
throw new Error("No pending changelog entries. Nothing to release.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedHeading = versionHeading
|
||||||
|
.replaceAll("{{version}}", version)
|
||||||
|
.replaceAll("{{date}}", dateString);
|
||||||
|
const nextContent = content.slice(pendingSection.end).replace(/^\n+/, "");
|
||||||
|
content = `${content.slice(0, pendingSection.bodyStart)}\n\n${renderedHeading}\n\n${pendingBlock}\n\n${nextContent}`;
|
||||||
|
await writeChangelog(filePath, content);
|
||||||
|
};
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { getCliConfigValue } from "./helpers.smartconfig.js";
|
||||||
|
|
||||||
|
export type TCliOutputMode = "human" | "plain" | "json";
|
||||||
|
|
||||||
|
export interface ICliMode {
|
||||||
|
output: TCliOutputMode;
|
||||||
|
interactive: boolean;
|
||||||
|
json: boolean;
|
||||||
|
plain: boolean;
|
||||||
|
quiet: boolean;
|
||||||
|
yes: boolean;
|
||||||
|
help: boolean;
|
||||||
|
agent: boolean;
|
||||||
|
checkUpdates: boolean;
|
||||||
|
isTty: boolean;
|
||||||
|
command?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICliConfigSettings {
|
||||||
|
interactive?: boolean;
|
||||||
|
output?: TCliOutputMode;
|
||||||
|
checkUpdates?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TArgSource = Record<string, any> & { _?: string[] };
|
||||||
|
|
||||||
|
const camelCase = (value: string): string => {
|
||||||
|
return value.replace(/-([a-z])/g, (_match, group: string) =>
|
||||||
|
group.toUpperCase(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getArgValue = (argvArg: TArgSource, key: string): any => {
|
||||||
|
const keyVariants = [key, camelCase(key), key.replace(/-/g, "")];
|
||||||
|
for (const keyVariant of keyVariants) {
|
||||||
|
if (argvArg[keyVariant] !== undefined) {
|
||||||
|
return argvArg[keyVariant];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseRawArgv = (argv: string[]): TArgSource => {
|
||||||
|
const parsedArgv: TArgSource = { _: [] };
|
||||||
|
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const currentArg = argv[i];
|
||||||
|
|
||||||
|
if (currentArg.startsWith("--no-")) {
|
||||||
|
const key = currentArg.slice(5);
|
||||||
|
parsedArgv[key] = false;
|
||||||
|
parsedArgv[camelCase(key)] = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentArg.startsWith("--")) {
|
||||||
|
const withoutPrefix = currentArg.slice(2);
|
||||||
|
const [rawKey, inlineValue] = withoutPrefix.split("=", 2);
|
||||||
|
if (inlineValue !== undefined) {
|
||||||
|
parsedArgv[rawKey] = inlineValue;
|
||||||
|
parsedArgv[camelCase(rawKey)] = inlineValue;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextArg = argv[i + 1];
|
||||||
|
if (nextArg && !nextArg.startsWith("-")) {
|
||||||
|
parsedArgv[rawKey] = nextArg;
|
||||||
|
parsedArgv[camelCase(rawKey)] = nextArg;
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
parsedArgv[rawKey] = true;
|
||||||
|
parsedArgv[camelCase(rawKey)] = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentArg.startsWith("-") && currentArg.length > 1) {
|
||||||
|
for (const shortFlag of currentArg.slice(1).split("")) {
|
||||||
|
parsedArgv[shortFlag] = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedArgv._ = parsedArgv._ || [];
|
||||||
|
parsedArgv._.push(currentArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedArgv;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeOutputMode = (value: unknown): TCliOutputMode | undefined => {
|
||||||
|
if (value === "human" || value === "plain" || value === "json") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveCliMode = (
|
||||||
|
argvArg: TArgSource,
|
||||||
|
cliConfig: ICliConfigSettings,
|
||||||
|
): ICliMode => {
|
||||||
|
const isTty = Boolean(process.stdout?.isTTY && process.stdin?.isTTY);
|
||||||
|
const agentMode = Boolean(getArgValue(argvArg, "agent"));
|
||||||
|
const outputOverride = normalizeOutputMode(getArgValue(argvArg, "output"));
|
||||||
|
|
||||||
|
let output: TCliOutputMode =
|
||||||
|
normalizeOutputMode(cliConfig.output) || (isTty ? "human" : "plain");
|
||||||
|
if (agentMode || getArgValue(argvArg, "json")) {
|
||||||
|
output = "json";
|
||||||
|
} else if (getArgValue(argvArg, "plain")) {
|
||||||
|
output = "plain";
|
||||||
|
} else if (outputOverride) {
|
||||||
|
output = outputOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
const interactiveSetting = getArgValue(argvArg, "interactive");
|
||||||
|
let interactive = cliConfig.interactive ?? isTty;
|
||||||
|
if (interactiveSetting === true) {
|
||||||
|
interactive = true;
|
||||||
|
} else if (interactiveSetting === false) {
|
||||||
|
interactive = false;
|
||||||
|
}
|
||||||
|
if (!isTty || output !== "human" || agentMode) {
|
||||||
|
interactive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkUpdatesSetting = getArgValue(argvArg, "check-updates");
|
||||||
|
let checkUpdates = cliConfig.checkUpdates ?? output === "human";
|
||||||
|
if (checkUpdatesSetting === true) {
|
||||||
|
checkUpdates = true;
|
||||||
|
} else if (checkUpdatesSetting === false) {
|
||||||
|
checkUpdates = false;
|
||||||
|
}
|
||||||
|
if (output !== "human" || agentMode) {
|
||||||
|
checkUpdates = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
output,
|
||||||
|
interactive,
|
||||||
|
json: output === "json",
|
||||||
|
plain: output === "plain",
|
||||||
|
quiet: Boolean(
|
||||||
|
getArgValue(argvArg, "quiet") ||
|
||||||
|
getArgValue(argvArg, "q") ||
|
||||||
|
output === "json",
|
||||||
|
),
|
||||||
|
yes: Boolean(getArgValue(argvArg, "yes") || getArgValue(argvArg, "y")),
|
||||||
|
help: Boolean(
|
||||||
|
getArgValue(argvArg, "help") ||
|
||||||
|
getArgValue(argvArg, "h") ||
|
||||||
|
argvArg._?.[0] === "help",
|
||||||
|
),
|
||||||
|
agent: agentMode,
|
||||||
|
checkUpdates,
|
||||||
|
isTty,
|
||||||
|
command: argvArg._?.[0],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCliModeConfig = async (): Promise<ICliConfigSettings> => {
|
||||||
|
return await getCliConfigValue<ICliConfigSettings>("cli", {});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCliMode = async (
|
||||||
|
argvArg: TArgSource = {},
|
||||||
|
): Promise<ICliMode> => {
|
||||||
|
const cliConfig = await getCliModeConfig();
|
||||||
|
return resolveCliMode(argvArg, cliConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRawCliMode = async (): Promise<ICliMode> => {
|
||||||
|
const cliConfig = await getCliModeConfig();
|
||||||
|
const rawArgv = parseRawArgv(process.argv.slice(2));
|
||||||
|
return resolveCliMode(rawArgv, cliConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const printJson = (data: unknown): void => {
|
||||||
|
console.log(JSON.stringify(data, null, 2));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runWithSuppressedOutput = async <T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
): Promise<T> => {
|
||||||
|
const originalConsole = {
|
||||||
|
log: console.log,
|
||||||
|
info: console.info,
|
||||||
|
warn: console.warn,
|
||||||
|
error: console.error,
|
||||||
|
};
|
||||||
|
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
||||||
|
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
||||||
|
const noop = () => undefined;
|
||||||
|
|
||||||
|
console.log = noop;
|
||||||
|
console.info = noop;
|
||||||
|
console.warn = noop;
|
||||||
|
console.error = noop;
|
||||||
|
process.stdout.write = (() => true) as typeof process.stdout.write;
|
||||||
|
process.stderr.write = (() => true) as typeof process.stderr.write;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
console.log = originalConsole.log;
|
||||||
|
console.info = originalConsole.info;
|
||||||
|
console.warn = originalConsole.warn;
|
||||||
|
console.error = originalConsole.error;
|
||||||
|
process.stdout.write = originalStdoutWrite;
|
||||||
|
process.stderr.write = originalStderrWrite;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import * as plugins from "./plugins.js";
|
||||||
|
import { rename, writeFile } from "fs/promises";
|
||||||
|
|
||||||
|
export const CLI_NAMESPACE = "@git.zone/cli";
|
||||||
|
|
||||||
|
const isPlainObject = (value: unknown): value is Record<string, any> => {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSmartconfigPath = (cwd: string = process.cwd()): string => {
|
||||||
|
return plugins.path.join(cwd, ".smartconfig.json");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readSmartconfigFile = async (
|
||||||
|
cwd: string = process.cwd(),
|
||||||
|
): Promise<Record<string, any>> => {
|
||||||
|
const smartconfigPath = getSmartconfigPath(cwd);
|
||||||
|
if (!(await plugins.smartfs.file(smartconfigPath).exists())) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = (await plugins.smartfs
|
||||||
|
.file(smartconfigPath)
|
||||||
|
.encoding("utf8")
|
||||||
|
.read()) as string;
|
||||||
|
if (content.trim() === "") {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return JSON.parse(content);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const writeSmartconfigFile = async (
|
||||||
|
data: Record<string, any>,
|
||||||
|
cwd: string = process.cwd(),
|
||||||
|
): Promise<void> => {
|
||||||
|
const smartconfigPath = getSmartconfigPath(cwd);
|
||||||
|
const tempPath = `${smartconfigPath}.tmp-${Date.now()}`;
|
||||||
|
const content = JSON.stringify(data, null, 2);
|
||||||
|
await writeFile(tempPath, content, "utf8");
|
||||||
|
await rename(tempPath, smartconfigPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeCliConfigPath = (configPath: string): string => {
|
||||||
|
const trimmedPath = configPath.trim();
|
||||||
|
if (!trimmedPath || trimmedPath === CLI_NAMESPACE) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmedPath.startsWith(`${CLI_NAMESPACE}.`)) {
|
||||||
|
return trimmedPath.slice(`${CLI_NAMESPACE}.`.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmedPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCliConfigPathSegments = (configPath: string): string[] => {
|
||||||
|
const normalizedPath = normalizeCliConfigPath(configPath);
|
||||||
|
if (!normalizedPath) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedPath
|
||||||
|
.split(".")
|
||||||
|
.map((segment) => segment.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCliNamespaceConfig = (
|
||||||
|
smartconfigData: Record<string, any>,
|
||||||
|
): Record<string, any> => {
|
||||||
|
const cliConfig = smartconfigData[CLI_NAMESPACE];
|
||||||
|
if (isPlainObject(cliConfig)) {
|
||||||
|
return cliConfig;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCliConfigValueFromData = (
|
||||||
|
smartconfigData: Record<string, any>,
|
||||||
|
configPath: string,
|
||||||
|
): any => {
|
||||||
|
const segments = getCliConfigPathSegments(configPath);
|
||||||
|
let currentValue: any = getCliNamespaceConfig(smartconfigData);
|
||||||
|
|
||||||
|
for (const segment of segments) {
|
||||||
|
if (!isPlainObject(currentValue) && !Array.isArray(currentValue)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
currentValue = (currentValue as any)?.[segment];
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCliConfigValue = async <T>(
|
||||||
|
configPath: string,
|
||||||
|
defaultValue: T,
|
||||||
|
cwd: string = process.cwd(),
|
||||||
|
): Promise<T> => {
|
||||||
|
const smartconfigData = await readSmartconfigFile(cwd);
|
||||||
|
const configValue = getCliConfigValueFromData(smartconfigData, configPath);
|
||||||
|
|
||||||
|
if (configValue === undefined) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(defaultValue) && isPlainObject(configValue)) {
|
||||||
|
return {
|
||||||
|
...defaultValue,
|
||||||
|
...configValue,
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return configValue as T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setCliConfigValueInData = (
|
||||||
|
smartconfigData: Record<string, any>,
|
||||||
|
configPath: string,
|
||||||
|
value: any,
|
||||||
|
): Record<string, any> => {
|
||||||
|
const segments = getCliConfigPathSegments(configPath);
|
||||||
|
|
||||||
|
if (!isPlainObject(smartconfigData[CLI_NAMESPACE])) {
|
||||||
|
smartconfigData[CLI_NAMESPACE] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segments.length === 0) {
|
||||||
|
smartconfigData[CLI_NAMESPACE] = value;
|
||||||
|
return smartconfigData;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentValue = smartconfigData[CLI_NAMESPACE];
|
||||||
|
for (const segment of segments.slice(0, -1)) {
|
||||||
|
if (!isPlainObject(currentValue[segment])) {
|
||||||
|
currentValue[segment] = {};
|
||||||
|
}
|
||||||
|
currentValue = currentValue[segment];
|
||||||
|
}
|
||||||
|
|
||||||
|
currentValue[segments[segments.length - 1]] = value;
|
||||||
|
return smartconfigData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const unsetCliConfigValueInData = (
|
||||||
|
smartconfigData: Record<string, any>,
|
||||||
|
configPath: string,
|
||||||
|
): boolean => {
|
||||||
|
const segments = getCliConfigPathSegments(configPath);
|
||||||
|
if (segments.length === 0) {
|
||||||
|
if (smartconfigData[CLI_NAMESPACE] !== undefined) {
|
||||||
|
delete smartconfigData[CLI_NAMESPACE];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentSegments = segments.slice(0, -1);
|
||||||
|
let currentValue: any = getCliNamespaceConfig(smartconfigData);
|
||||||
|
const objectPath: Array<Record<string, any>> = [currentValue];
|
||||||
|
|
||||||
|
for (const segment of parentSegments) {
|
||||||
|
if (!isPlainObject(currentValue[segment])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
currentValue = currentValue[segment];
|
||||||
|
objectPath.push(currentValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSegment = segments[segments.length - 1];
|
||||||
|
if (!(lastSegment in currentValue)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete currentValue[lastSegment];
|
||||||
|
|
||||||
|
for (let i = objectPath.length - 1; i >= 1; i--) {
|
||||||
|
if (Object.keys(objectPath[i]).length > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentObject = objectPath[i - 1];
|
||||||
|
const parentKey = parentSegments[i - 1];
|
||||||
|
delete parentObject[parentKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(getCliNamespaceConfig(smartconfigData)).length === 0) {
|
||||||
|
delete smartconfigData[CLI_NAMESPACE];
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
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 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);
|
||||||
|
const releaseConfig = ensureObject(cliConfig, "release");
|
||||||
|
|
||||||
|
let migrated = false;
|
||||||
|
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) && !isPlainObject(targets.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete releaseConfig.steps;
|
||||||
|
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 (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,387 @@
|
|||||||
|
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;
|
||||||
|
images?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
dockerImages: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
dockerImages: dockerConfig.images || [],
|
||||||
|
};
|
||||||
|
};
|
||||||
+350
-84
@@ -1,105 +1,371 @@
|
|||||||
// this file contains code to create commits in a consistent way
|
// this file contains code to create commits in a consistent way
|
||||||
|
|
||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from "./mod.plugins.js";
|
||||||
import * as paths from '../paths.js';
|
import * as paths from "../paths.js";
|
||||||
import { logger } from '../gitzone.logging.js';
|
import { logger } from "../gitzone.logging.js";
|
||||||
|
import * as ui from "./mod.ui.js";
|
||||||
|
import type { ICliMode } from "../helpers.climode.js";
|
||||||
|
import { getCliMode, printJson, runWithSuppressedOutput } from "../helpers.climode.js";
|
||||||
|
import { appendPendingChangelogEntry } from "../helpers.changelog.js";
|
||||||
|
import { resolveCommitWorkflow, type IResolvedCommitWorkflow } from "../helpers.workflow.js";
|
||||||
|
|
||||||
export const run = async (argvArg: any) => {
|
export const run = async (argvArg: any) => {
|
||||||
if (argvArg.format) {
|
const mode = await getCliMode(argvArg);
|
||||||
const formatMod = await import('../mod_format/index.js');
|
const subcommand = argvArg._?.[1];
|
||||||
await formatMod.run();
|
|
||||||
|
if (mode.help || subcommand === "help") {
|
||||||
|
showHelp(mode);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subcommand === "recommend") {
|
||||||
|
await handleRecommend(mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
logger.log('info', `gathering facts...`);
|
if (mode.json) {
|
||||||
const aidoc = new plugins.tsdoc.AiDoc();
|
printJson({
|
||||||
await aidoc.start();
|
ok: false,
|
||||||
|
error:
|
||||||
|
"JSON output is only supported for the read-only recommendation flow. Use `gitzone commit recommend --json`.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd);
|
const workflow = await resolveCommitWorkflow(argvArg);
|
||||||
|
if (workflow.releaseFlagRequested) {
|
||||||
|
logger.log(
|
||||||
|
"warn",
|
||||||
|
"`gitzone commit -r` is deprecated and no longer releases. Use `gitzone release` after committing.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
logger.log('info', `---------
|
printCommitExecutionPlan(workflow);
|
||||||
Next recommended commit would be:
|
if (workflow.confirmation === "plan") {
|
||||||
===========
|
return;
|
||||||
-> ${nextCommitObject.recommendedNextVersion}:
|
}
|
||||||
-> ${nextCommitObject.recommendedNextVersionLevel}(${nextCommitObject.recommendedNextVersionScope}): ${nextCommitObject.recommendedNextVersionMessage}
|
|
||||||
===========
|
|
||||||
`);
|
|
||||||
const commitInteract = new plugins.smartinteract.SmartInteract();
|
|
||||||
commitInteract.addQuestions([
|
|
||||||
{
|
|
||||||
type: 'list',
|
|
||||||
name: `commitType`,
|
|
||||||
message: `Choose TYPE of the commit:`,
|
|
||||||
choices: [`fix`, `feat`, `BREAKING CHANGE`],
|
|
||||||
default: nextCommitObject.recommendedNextVersionLevel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
name: `commitScope`,
|
|
||||||
message: `What is the SCOPE of the commit:`,
|
|
||||||
default: nextCommitObject.recommendedNextVersionScope,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: `input`,
|
|
||||||
name: `commitDescription`,
|
|
||||||
message: `What is the DESCRIPTION of the commit?`,
|
|
||||||
default: nextCommitObject.recommendedNextVersionMessage,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: `pushToOrigin`,
|
|
||||||
message: `Do you want to push this version now?`,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const answerBucket = await commitInteract.runQueue();
|
|
||||||
const commitString = createCommitStringFromAnswerBucket(answerBucket);
|
|
||||||
const commitVersionType = (() => {
|
|
||||||
switch (answerBucket.getAnswerFor('commitType')) {
|
|
||||||
case 'fix':
|
|
||||||
return 'patch';
|
|
||||||
case 'feat':
|
|
||||||
return 'minor';
|
|
||||||
case 'BREAKING CHANGE':
|
|
||||||
return 'major';
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
logger.log('info', `OK! Creating commit with message '${commitString}'`);
|
|
||||||
const smartshellInstance = new plugins.smartshell.Smartshell({
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
executor: 'bash',
|
executor: "bash",
|
||||||
sourceFilePaths: [],
|
sourceFilePaths: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.log('info', `Baking commitinfo into code ...`);
|
let nextCommitObject: any;
|
||||||
const commitInfo = new plugins.commitinfo.CommitInfo(paths.cwd, commitVersionType);
|
let answerBucket: plugins.smartinteract.AnswerBucket | undefined;
|
||||||
await commitInfo.writeIntoPotentialDirs();
|
|
||||||
|
|
||||||
logger.log('info', `Writing changelog.md ...`);
|
for (const step of workflow.steps) {
|
||||||
let changelog = nextCommitObject.changelog;
|
switch (step) {
|
||||||
changelog = changelog.replaceAll('{{nextVersion}}', (await commitInfo.getNextPlannedVersion()).versionString);
|
case "format":
|
||||||
changelog = changelog.replaceAll('{{nextVersionScope}}', `${await answerBucket.getAnswerFor('commitType')}(${await answerBucket.getAnswerFor('commitScope')})`);
|
await runFormatStep();
|
||||||
changelog = changelog.replaceAll('{{nextVersionMessage}}', nextCommitObject.recommendedNextVersionMessage);
|
break;
|
||||||
if (nextCommitObject.recommendedNextVersionDetails?.length > 0) {
|
case "test":
|
||||||
changelog = changelog.replaceAll('{{nextVersionDetails}}', '- ' + nextCommitObject.recommendedNextVersionDetails.join('\n- '));
|
await runCommandStep(smartshellInstance, "Running tests", workflow.testCommand);
|
||||||
} else {
|
break;
|
||||||
changelog = changelog.replaceAll('\n{{nextVersionDetails}}', '');
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await plugins.smartfile.memory.toFs(changelog, plugins.path.join(paths.cwd, `changelog.md`));
|
const commitShaResult = await smartshellInstance.exec("git rev-parse --short HEAD");
|
||||||
|
const currentBranch = await detectCurrentBranch(smartshellInstance);
|
||||||
logger.log('info', `Staging files for commit:`);
|
ui.printSummary({
|
||||||
await smartshellInstance.exec(`git add -A`);
|
projectType: "source",
|
||||||
await smartshellInstance.exec(`git commit -m "${commitString}"`);
|
branch: currentBranch,
|
||||||
await smartshellInstance.exec(`npm version ${commitVersionType}`);
|
commitType: answerBucket!.getAnswerFor("commitType"),
|
||||||
if (answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true')) {
|
commitScope: answerBucket!.getAnswerFor("commitScope"),
|
||||||
await smartshellInstance.exec(`git push origin master --follow-tags`);
|
commitMessage: answerBucket!.getAnswerFor("commitDescription"),
|
||||||
}
|
commitSha: commitShaResult.stdout.trim(),
|
||||||
|
pushed: workflow.steps.includes("push"),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const createCommitStringFromAnswerBucket = (answerBucket: plugins.smartinteract.AnswerBucket) => {
|
async function runFormatStep(): Promise<void> {
|
||||||
const commitType = answerBucket.getAnswerFor('commitType');
|
ui.printHeader("Formatting project files");
|
||||||
const commitScope = answerBucket.getAnswerFor('commitScope');
|
const formatMod = await import("../mod_format/index.js");
|
||||||
const commitDescription = answerBucket.getAnswerFor('commitDescription');
|
await formatMod.run({ write: true, yes: true, interactive: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommandStep(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
label: string,
|
||||||
|
command: string,
|
||||||
|
): Promise<void> {
|
||||||
|
ui.printHeader(label);
|
||||||
|
const result = await smartshellInstance.exec(command);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", `${label} failed. Aborting commit.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
logger.log("success", `${label} passed.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAnalyzeStep(): Promise<any> {
|
||||||
|
ui.printHeader("Analyzing repository changes");
|
||||||
|
const aidoc = new plugins.tsdoc.AiDoc();
|
||||||
|
await aidoc.start();
|
||||||
|
try {
|
||||||
|
const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd);
|
||||||
|
ui.printRecommendation({
|
||||||
|
recommendedNextVersion: nextCommitObject.recommendedNextVersion,
|
||||||
|
recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel,
|
||||||
|
recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope,
|
||||||
|
recommendedNextVersionMessage: nextCommitObject.recommendedNextVersionMessage,
|
||||||
|
});
|
||||||
|
return nextCommitObject;
|
||||||
|
} finally {
|
||||||
|
await aidoc.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildAnswerBucket(
|
||||||
|
nextCommitObject: any,
|
||||||
|
workflow: IResolvedCommitWorkflow,
|
||||||
|
mode: ICliMode,
|
||||||
|
argvArg: any,
|
||||||
|
): Promise<plugins.smartinteract.AnswerBucket> {
|
||||||
|
const isBreakingChange = nextCommitObject.recommendedNextVersionLevel === "BREAKING CHANGE";
|
||||||
|
const canAutoAccept = workflow.confirmation === "auto" && !isBreakingChange;
|
||||||
|
|
||||||
|
if (canAutoAccept) {
|
||||||
|
logger.log("info", "Auto-accepting AI recommendations");
|
||||||
|
return createAnswerBucket({
|
||||||
|
commitType: nextCommitObject.recommendedNextVersionLevel,
|
||||||
|
commitScope: nextCommitObject.recommendedNextVersionScope,
|
||||||
|
commitDescription: nextCommitObject.recommendedNextVersionMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBreakingChange && (workflow.confirmation === "auto" || argvArg.y || argvArg.yes)) {
|
||||||
|
logger.log("warn", "BREAKING CHANGE detected - manual confirmation required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mode.interactive) {
|
||||||
|
throw new Error("Commit confirmation requires an interactive terminal. Use `-y` or set commit.confirmation to `auto`.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitInteract = new plugins.smartinteract.SmartInteract();
|
||||||
|
commitInteract.addQuestions([
|
||||||
|
{
|
||||||
|
type: "list",
|
||||||
|
name: "commitType",
|
||||||
|
message: "Choose TYPE of the commit:",
|
||||||
|
choices: ["fix", "feat", "BREAKING CHANGE"],
|
||||||
|
default: nextCommitObject.recommendedNextVersionLevel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "commitScope",
|
||||||
|
message: "What is the SCOPE of the commit:",
|
||||||
|
default: nextCommitObject.recommendedNextVersionScope,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "commitDescription",
|
||||||
|
message: "What is the DESCRIPTION of the commit?",
|
||||||
|
default: nextCommitObject.recommendedNextVersionMessage,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
return await commitInteract.runQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAnswerBucket(answers: {
|
||||||
|
commitType: string;
|
||||||
|
commitScope: string;
|
||||||
|
commitDescription: string;
|
||||||
|
}): plugins.smartinteract.AnswerBucket {
|
||||||
|
const answerBucket = new plugins.smartinteract.AnswerBucket();
|
||||||
|
for (const [name, value] of Object.entries(answers)) {
|
||||||
|
answerBucket.addAnswer({ name, value });
|
||||||
|
}
|
||||||
|
return answerBucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runChangelogStep(
|
||||||
|
workflow: IResolvedCommitWorkflow,
|
||||||
|
answerBucket: plugins.smartinteract.AnswerBucket,
|
||||||
|
nextCommitObject: any,
|
||||||
|
): Promise<void> {
|
||||||
|
await appendPendingChangelogEntry(
|
||||||
|
plugins.path.join(paths.cwd, workflow.changelogFile),
|
||||||
|
workflow.changelogSection,
|
||||||
|
{
|
||||||
|
type: answerBucket.getAnswerFor("commitType"),
|
||||||
|
scope: answerBucket.getAnswerFor("commitScope"),
|
||||||
|
message: answerBucket.getAnswerFor("commitDescription"),
|
||||||
|
details: nextCommitObject.recommendedNextVersionDetails || [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
logger.log("success", `Updated ${workflow.changelogFile} pending section.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommitStep(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
answerBucket: plugins.smartinteract.AnswerBucket,
|
||||||
|
): Promise<void> {
|
||||||
|
ui.printHeader("Creating Semantic Commit");
|
||||||
|
const commitString = createCommitStringFromAnswerBucket(answerBucket);
|
||||||
|
ui.printCommitMessage(commitString);
|
||||||
|
await smartshellInstance.exec("git add -A");
|
||||||
|
const result = await smartshellInstance.exec(`git commit -m ${shellQuote(commitString)}`);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", "git commit failed.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPushStep(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
workflow: IResolvedCommitWorkflow,
|
||||||
|
): Promise<void> {
|
||||||
|
const currentBranch = await detectCurrentBranch(smartshellInstance);
|
||||||
|
const followTags = workflow.pushFollowTags ? " --follow-tags" : "";
|
||||||
|
const result = await smartshellInstance.exec(
|
||||||
|
`git push ${workflow.pushRemote} ${currentBranch}${followTags}`,
|
||||||
|
);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", "git push failed.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detectCurrentBranch(
|
||||||
|
smartshellInstance: plugins.smartshell.Smartshell,
|
||||||
|
): Promise<string> {
|
||||||
|
const branchResult = await smartshellInstance.exec("git branch --show-current");
|
||||||
|
return branchResult.stdout.trim() || "master";
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertAnalysisComplete(
|
||||||
|
answerBucket: plugins.smartinteract.AnswerBucket | undefined,
|
||||||
|
nextCommitObject: any,
|
||||||
|
): void {
|
||||||
|
if (!answerBucket || !nextCommitObject) {
|
||||||
|
throw new Error("Commit workflow requires analyze before changelog and commit steps.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellQuote(value: string): string {
|
||||||
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printCommitExecutionPlan(workflow: IResolvedCommitWorkflow): void {
|
||||||
|
console.log("");
|
||||||
|
console.log("gitzone commit - resolved workflow");
|
||||||
|
console.log(`confirmation: ${workflow.confirmation}`);
|
||||||
|
console.log(`steps: ${workflow.steps.join(" -> ")}`);
|
||||||
|
console.log(`changelog: ${workflow.changelogFile}#${workflow.changelogSection}`);
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRecommend(mode: ICliMode): Promise<void> {
|
||||||
|
const recommendationBuilder = async () => {
|
||||||
|
const aidoc = new plugins.tsdoc.AiDoc();
|
||||||
|
await aidoc.start();
|
||||||
|
try {
|
||||||
|
return await aidoc.buildNextCommitObject(paths.cwd);
|
||||||
|
} finally {
|
||||||
|
await aidoc.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const recommendation = mode.json
|
||||||
|
? await runWithSuppressedOutput(recommendationBuilder)
|
||||||
|
: await recommendationBuilder();
|
||||||
|
|
||||||
|
if (mode.json) {
|
||||||
|
printJson(recommendation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.printRecommendation({
|
||||||
|
recommendedNextVersion: recommendation.recommendedNextVersion,
|
||||||
|
recommendedNextVersionLevel: recommendation.recommendedNextVersionLevel,
|
||||||
|
recommendedNextVersionScope: recommendation.recommendedNextVersionScope,
|
||||||
|
recommendedNextVersionMessage: recommendation.recommendedNextVersionMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Suggested commit: ${recommendation.recommendedNextVersionLevel}(${recommendation.recommendedNextVersionScope}): ${recommendation.recommendedNextVersionMessage}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createCommitStringFromAnswerBucket = (
|
||||||
|
answerBucket: plugins.smartinteract.AnswerBucket,
|
||||||
|
) => {
|
||||||
|
const commitType = answerBucket.getAnswerFor("commitType");
|
||||||
|
const commitScope = answerBucket.getAnswerFor("commitScope");
|
||||||
|
const commitDescription = answerBucket.getAnswerFor("commitDescription");
|
||||||
return `${commitType}(${commitScope}): ${commitDescription}`;
|
return `${commitType}(${commitScope}): ${commitDescription}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function showHelp(mode?: ICliMode): void {
|
||||||
|
if (mode?.json) {
|
||||||
|
printJson({
|
||||||
|
command: "commit",
|
||||||
|
usage: "gitzone commit [recommend] [options]",
|
||||||
|
description: "Analyzes changes and creates one semantic source commit.",
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
name: "recommend",
|
||||||
|
description: "Generate a commit recommendation without mutating the repository",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
flags: [
|
||||||
|
{ flag: "-y, --yes", description: "Auto-accept safe AI recommendations" },
|
||||||
|
{ flag: "-p, --push", description: "Push to origin after committing" },
|
||||||
|
{ flag: "-t, --test", description: "Run tests as part of the commit workflow" },
|
||||||
|
{ flag: "-b, --build", description: "Run build as part of the commit workflow" },
|
||||||
|
{ flag: "-f, --format", description: "Run gitzone format before committing" },
|
||||||
|
{ flag: "--plan", description: "Show resolved workflow without mutating files" },
|
||||||
|
{ flag: "--json", description: "Emit JSON for `commit recommend` only" },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
"gitzone commit recommend --json",
|
||||||
|
"gitzone commit -y",
|
||||||
|
"gitzone commit -ytbp",
|
||||||
|
"gitzone release",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log("Usage: gitzone commit [recommend] [options]");
|
||||||
|
console.log("");
|
||||||
|
console.log("Creates one semantic source commit. It does not version, tag, or publish.");
|
||||||
|
console.log("");
|
||||||
|
console.log("Commands:");
|
||||||
|
console.log(" recommend Generate a commit recommendation without mutating the repository");
|
||||||
|
console.log("");
|
||||||
|
console.log("Flags:");
|
||||||
|
console.log(" -y, --yes Auto-accept safe AI recommendations");
|
||||||
|
console.log(" -p, --push Push after commit");
|
||||||
|
console.log(" -t, --test Run tests in the configured order");
|
||||||
|
console.log(" -b, --build Run build in the configured order");
|
||||||
|
console.log(" -f, --format Run gitzone format before committing");
|
||||||
|
console.log(" --plan Show resolved workflow without mutating files");
|
||||||
|
console.log(" --json Emit JSON for `commit recommend` only");
|
||||||
|
console.log("");
|
||||||
|
console.log("Examples:");
|
||||||
|
console.log(" gitzone commit recommend --json");
|
||||||
|
console.log(" gitzone commit -y");
|
||||||
|
console.log(" gitzone commit -ytbp");
|
||||||
|
console.log(" gitzone release");
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
import * as plugins from './mod.plugins.js';
|
||||||
|
import * as paths from '../paths.js';
|
||||||
|
import { logger } from '../gitzone.logging.js';
|
||||||
|
import * as ui from './mod.ui.js';
|
||||||
|
|
||||||
|
export type ProjectType = 'npm' | 'deno' | 'both' | 'none';
|
||||||
|
export type VersionType = 'patch' | 'minor' | 'major';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects the current git branch
|
||||||
|
* @returns The current branch name, defaults to 'master' if detection fails
|
||||||
|
*/
|
||||||
|
export async function detectCurrentBranch(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
|
executor: 'bash',
|
||||||
|
sourceFilePaths: [],
|
||||||
|
});
|
||||||
|
const result = await smartshellInstance.exec('git branch --show-current');
|
||||||
|
const branchName = result.stdout.trim();
|
||||||
|
|
||||||
|
if (!branchName) {
|
||||||
|
logger.log('warn', 'Could not detect current branch, falling back to "master"');
|
||||||
|
return 'master';
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', `Detected current branch: ${branchName}`);
|
||||||
|
return branchName;
|
||||||
|
} catch (error) {
|
||||||
|
logger.log('warn', `Failed to detect branch: ${error.message}, falling back to "master"`);
|
||||||
|
return 'master';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects the project type based on presence of package.json and/or deno.json
|
||||||
|
* @returns The project type
|
||||||
|
*/
|
||||||
|
export async function detectProjectType(): Promise<ProjectType> {
|
||||||
|
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
||||||
|
const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
|
||||||
|
|
||||||
|
const hasPackageJson = await plugins.smartfs.file(packageJsonPath).exists();
|
||||||
|
const hasDenoJson = await plugins.smartfs.file(denoJsonPath).exists();
|
||||||
|
|
||||||
|
if (hasPackageJson && hasDenoJson) {
|
||||||
|
logger.log('info', 'Detected dual project (npm + deno)');
|
||||||
|
return 'both';
|
||||||
|
} else if (hasPackageJson) {
|
||||||
|
logger.log('info', 'Detected npm project');
|
||||||
|
return 'npm';
|
||||||
|
} else if (hasDenoJson) {
|
||||||
|
logger.log('info', 'Detected deno project');
|
||||||
|
return 'deno';
|
||||||
|
} else {
|
||||||
|
throw new Error('No package.json or deno.json found in current directory');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a semantic version string and bumps it according to the version type
|
||||||
|
* @param currentVersion Current version string (e.g., "1.2.3")
|
||||||
|
* @param versionType Type of version bump
|
||||||
|
* @returns New version string
|
||||||
|
*/
|
||||||
|
export function calculateNewVersion(currentVersion: string, versionType: VersionType): string {
|
||||||
|
const versionMatch = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/);
|
||||||
|
|
||||||
|
if (!versionMatch) {
|
||||||
|
throw new Error(`Invalid version format: ${currentVersion}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let [, major, minor, patch] = versionMatch.map(Number);
|
||||||
|
|
||||||
|
switch (versionType) {
|
||||||
|
case 'major':
|
||||||
|
major += 1;
|
||||||
|
minor = 0;
|
||||||
|
patch = 0;
|
||||||
|
break;
|
||||||
|
case 'minor':
|
||||||
|
minor += 1;
|
||||||
|
patch = 0;
|
||||||
|
break;
|
||||||
|
case 'patch':
|
||||||
|
patch += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${major}.${minor}.${patch}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the current version from package.json or deno.json
|
||||||
|
* @param projectType The project type to determine which file to read
|
||||||
|
* @returns The current version string
|
||||||
|
*/
|
||||||
|
export async function readCurrentVersion(projectType: ProjectType): Promise<string> {
|
||||||
|
if (projectType === 'npm' || projectType === 'both') {
|
||||||
|
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
||||||
|
const content = (await plugins.smartfs
|
||||||
|
.file(packageJsonPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
const packageJson = JSON.parse(content) as { version?: string };
|
||||||
|
|
||||||
|
if (!packageJson.version) {
|
||||||
|
throw new Error('package.json does not contain a version field');
|
||||||
|
}
|
||||||
|
return packageJson.version;
|
||||||
|
} else {
|
||||||
|
const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
|
||||||
|
const content = (await plugins.smartfs
|
||||||
|
.file(denoJsonPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
const denoConfig = JSON.parse(content) as { version?: string };
|
||||||
|
|
||||||
|
if (!denoConfig.version) {
|
||||||
|
throw new Error('deno.json does not contain a version field');
|
||||||
|
}
|
||||||
|
return denoConfig.version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the version field in a JSON file (package.json or deno.json)
|
||||||
|
* @param filePath Path to the JSON file
|
||||||
|
* @param newVersion The new version to write
|
||||||
|
*/
|
||||||
|
export async function updateVersionFile(filePath: string, newVersion: string): Promise<void> {
|
||||||
|
const content = (await plugins.smartfs
|
||||||
|
.file(filePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
const config = JSON.parse(content) as { version?: string };
|
||||||
|
config.version = newVersion;
|
||||||
|
await plugins.smartfs
|
||||||
|
.file(filePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.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
|
||||||
|
* Handles npm-only, deno-only, and dual projects with unified logic
|
||||||
|
* @param projectType The detected project type
|
||||||
|
* @param versionType The type of version bump
|
||||||
|
* @param currentStep The current step number for progress display
|
||||||
|
* @param totalSteps The total number of steps for progress display
|
||||||
|
* @returns The new version string
|
||||||
|
*/
|
||||||
|
export async function bumpProjectVersion(
|
||||||
|
projectType: ProjectType,
|
||||||
|
versionType: VersionType,
|
||||||
|
currentStep?: number,
|
||||||
|
totalSteps?: number
|
||||||
|
): Promise<string> {
|
||||||
|
if (projectType === 'none') {
|
||||||
|
throw new Error('Cannot bump version: no package.json or deno.json found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectEmoji = projectType === 'npm' ? '📦' : projectType === 'deno' ? '🦕' : '🔀';
|
||||||
|
const description = `🏷️ Bumping version (${projectEmoji} ${projectType})`;
|
||||||
|
|
||||||
|
if (currentStep && totalSteps) {
|
||||||
|
ui.printStep(currentStep, totalSteps, description, 'in-progress');
|
||||||
|
}
|
||||||
|
|
||||||
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
|
executor: 'bash',
|
||||||
|
sourceFilePaths: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Read current version
|
||||||
|
const currentVersion = await readCurrentVersion(projectType);
|
||||||
|
|
||||||
|
// 2. Calculate new version (reuse existing function!)
|
||||||
|
const newVersion = calculateNewVersion(currentVersion, versionType);
|
||||||
|
|
||||||
|
logger.log('info', `Bumping version: ${currentVersion} → ${newVersion}`);
|
||||||
|
|
||||||
|
// 3. Determine which files to update
|
||||||
|
const filesToUpdate = await updateProjectVersionFiles(projectType, newVersion);
|
||||||
|
|
||||||
|
// 4. Stage all updated files
|
||||||
|
await smartshellInstance.exec(`git add ${filesToUpdate.join(' ')}`);
|
||||||
|
|
||||||
|
// 5. Create version commit
|
||||||
|
await smartshellInstance.exec(`git commit -m "v${newVersion}"`);
|
||||||
|
|
||||||
|
// 6. Create version tag
|
||||||
|
await smartshellInstance.exec(`git tag v${newVersion} -m "v${newVersion}"`);
|
||||||
|
|
||||||
|
logger.log('info', `Created commit and tag v${newVersion}`);
|
||||||
|
|
||||||
|
if (currentStep && totalSteps) {
|
||||||
|
ui.printStep(currentStep, totalSteps, description, 'done');
|
||||||
|
}
|
||||||
|
|
||||||
|
return newVersion;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to bump project version: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
import { logger } from '../gitzone.logging.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI helper module for beautiful CLI output
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface ICommitSummary {
|
||||||
|
projectType: string;
|
||||||
|
branch: string;
|
||||||
|
commitType: string;
|
||||||
|
commitScope: string;
|
||||||
|
commitMessage: string;
|
||||||
|
newVersion?: string;
|
||||||
|
commitSha?: string;
|
||||||
|
pushed: boolean;
|
||||||
|
repoUrl?: string;
|
||||||
|
released?: boolean;
|
||||||
|
releasedRegistries?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IExecutionPlanOptions {
|
||||||
|
autoAccept: boolean;
|
||||||
|
push: boolean;
|
||||||
|
test: boolean;
|
||||||
|
build: boolean;
|
||||||
|
release: boolean;
|
||||||
|
format: boolean;
|
||||||
|
registries?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRecommendation {
|
||||||
|
recommendedNextVersion: string;
|
||||||
|
recommendedNextVersionLevel: string;
|
||||||
|
recommendedNextVersionScope: string;
|
||||||
|
recommendedNextVersionMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print a header with a box around it
|
||||||
|
*/
|
||||||
|
export function printHeader(title: string): void {
|
||||||
|
const width = 57;
|
||||||
|
const padding = Math.max(0, width - title.length - 2);
|
||||||
|
const leftPad = Math.floor(padding / 2);
|
||||||
|
const rightPad = padding - leftPad;
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('╭─' + '─'.repeat(width) + '─╮');
|
||||||
|
console.log('│ ' + title + ' '.repeat(rightPad + leftPad) + ' │');
|
||||||
|
console.log('╰─' + '─'.repeat(width) + '─╯');
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print execution plan at the start of commit
|
||||||
|
*/
|
||||||
|
export function printExecutionPlan(options: IExecutionPlanOptions): void {
|
||||||
|
console.log('');
|
||||||
|
console.log('╭───────────────────────────────────────────────────────────────╮');
|
||||||
|
console.log('│ gitzone commit - Execution Plan │');
|
||||||
|
console.log('╰───────────────────────────────────────────────────────────────╯');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Show active options
|
||||||
|
console.log(' Options:');
|
||||||
|
console.log(` Auto-accept ${options.autoAccept ? '✓ enabled (-y)' : '○ interactive mode'}`);
|
||||||
|
console.log(` Push to remote ${options.push ? '✓ enabled (-p)' : '○ disabled'}`);
|
||||||
|
console.log(` Test first ${options.test ? '✓ enabled (-t)' : '○ disabled'}`);
|
||||||
|
console.log(` Build & verify ${options.build ? '✓ enabled (-b)' : '○ disabled'}`);
|
||||||
|
console.log(` Release to npm ${options.release ? '✓ enabled (-r)' : '○ disabled'}`);
|
||||||
|
if (options.format) {
|
||||||
|
console.log(` Format first ✓ enabled (--format)`);
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Show steps
|
||||||
|
console.log(' Steps:');
|
||||||
|
let stepNum = 1;
|
||||||
|
if (options.format) {
|
||||||
|
console.log(` ${stepNum++}. Format project files`);
|
||||||
|
}
|
||||||
|
if (options.test) {
|
||||||
|
console.log(` ${stepNum++}. Run tests`);
|
||||||
|
}
|
||||||
|
console.log(` ${stepNum++}. Analyze repository changes`);
|
||||||
|
console.log(` ${stepNum++}. Bake commit info into code`);
|
||||||
|
console.log(` ${stepNum++}. Generate changelog.md`);
|
||||||
|
console.log(` ${stepNum++}. Stage and commit files`);
|
||||||
|
console.log(` ${stepNum++}. Bump version`);
|
||||||
|
if (options.build) {
|
||||||
|
console.log(` ${stepNum++}. Run build`);
|
||||||
|
console.log(` ${stepNum++}. Verify clean working tree`);
|
||||||
|
}
|
||||||
|
if (options.push) {
|
||||||
|
console.log(` ${stepNum++}. Push to remote`);
|
||||||
|
}
|
||||||
|
if (options.release) {
|
||||||
|
console.log(` ${stepNum++}. Publish to npm registries`);
|
||||||
|
if (options.registries && options.registries.length > 0) {
|
||||||
|
options.registries.forEach(r => console.log(` → ${r}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print a section with a border
|
||||||
|
*/
|
||||||
|
export function printSection(title: string, lines: string[]): void {
|
||||||
|
const width = 59;
|
||||||
|
|
||||||
|
console.log('┌─ ' + title + ' ' + '─'.repeat(Math.max(0, width - title.length - 3)) + '┐');
|
||||||
|
console.log('│' + ' '.repeat(width) + '│');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const padding = width - line.length;
|
||||||
|
console.log('│ ' + line + ' '.repeat(Math.max(0, padding - 2)) + '│');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('│' + ' '.repeat(width) + '│');
|
||||||
|
console.log('└─' + '─'.repeat(width) + '─┘');
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print AI recommendations in a nice box
|
||||||
|
*/
|
||||||
|
export function printRecommendation(recommendation: IRecommendation): void {
|
||||||
|
const lines = [
|
||||||
|
`Suggested Version: v${recommendation.recommendedNextVersion}`,
|
||||||
|
`Suggested Type: ${recommendation.recommendedNextVersionLevel}`,
|
||||||
|
`Suggested Scope: ${recommendation.recommendedNextVersionScope}`,
|
||||||
|
`Suggested Message: ${recommendation.recommendedNextVersionMessage}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
printSection('📊 AI Recommendations', lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print a progress step
|
||||||
|
*/
|
||||||
|
export function printStep(
|
||||||
|
current: number,
|
||||||
|
total: number,
|
||||||
|
description: string,
|
||||||
|
status: 'in-progress' | 'done' | 'error'
|
||||||
|
): void {
|
||||||
|
const statusIcon = status === 'done' ? '✓' : status === 'error' ? '✗' : '⏳';
|
||||||
|
const dots = '.'.repeat(Math.max(0, 40 - description.length));
|
||||||
|
|
||||||
|
console.log(` [${current}/${total}] ${description}${dots} ${statusIcon}`);
|
||||||
|
|
||||||
|
// Clear the line on next update if in progress
|
||||||
|
if (status === 'in-progress') {
|
||||||
|
process.stdout.write('\x1b[1A'); // Move cursor up one line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get emoji for project type
|
||||||
|
*/
|
||||||
|
function getProjectTypeEmoji(projectType: string): string {
|
||||||
|
switch (projectType) {
|
||||||
|
case 'npm':
|
||||||
|
return '📦 npm';
|
||||||
|
case 'deno':
|
||||||
|
return '🦕 Deno';
|
||||||
|
case 'both':
|
||||||
|
return '🔀 npm + Deno';
|
||||||
|
default:
|
||||||
|
return '❓ Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get emoji for commit type
|
||||||
|
*/
|
||||||
|
function getCommitTypeEmoji(commitType: string): string {
|
||||||
|
switch (commitType) {
|
||||||
|
case 'fix':
|
||||||
|
return '🔧 fix';
|
||||||
|
case 'feat':
|
||||||
|
return '✨ feat';
|
||||||
|
case 'BREAKING CHANGE':
|
||||||
|
return '💥 BREAKING CHANGE';
|
||||||
|
default:
|
||||||
|
return commitType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print final commit summary
|
||||||
|
*/
|
||||||
|
export function printSummary(summary: ICommitSummary): void {
|
||||||
|
const lines = [
|
||||||
|
`Project Type: ${getProjectTypeEmoji(summary.projectType)}`,
|
||||||
|
`Branch: 🌿 ${summary.branch}`,
|
||||||
|
`Commit Type: ${getCommitTypeEmoji(summary.commitType)}`,
|
||||||
|
`Scope: 📍 ${summary.commitScope}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (summary.newVersion) {
|
||||||
|
lines.push(`New Version: 🏷️ v${summary.newVersion}`);
|
||||||
|
} else {
|
||||||
|
lines.push(`Version: ⊘ Not bumped`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary.commitSha) {
|
||||||
|
lines.push(`Commit SHA: 📌 ${summary.commitSha}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary.pushed) {
|
||||||
|
lines.push(`Remote: ✓ Pushed successfully`);
|
||||||
|
} else {
|
||||||
|
lines.push(`Remote: ⊘ Not pushed (local only)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary.released && summary.releasedRegistries && summary.releasedRegistries.length > 0) {
|
||||||
|
lines.push(`Published: ✓ Released to ${summary.releasedRegistries.length} registr${summary.releasedRegistries.length === 1 ? 'y' : 'ies'}`);
|
||||||
|
summary.releasedRegistries.forEach((registry) => {
|
||||||
|
lines.push(` → ${registry}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary.repoUrl && summary.commitSha) {
|
||||||
|
lines.push('');
|
||||||
|
lines.push(`View at: ${summary.repoUrl}/commit/${summary.commitSha}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
printSection('✅ Commit Summary', lines);
|
||||||
|
|
||||||
|
if (summary.released) {
|
||||||
|
console.log('🎉 All done! Your changes are committed, pushed, and released.\n');
|
||||||
|
} else if (summary.pushed) {
|
||||||
|
console.log('🎉 All done! Your changes are committed and pushed.\n');
|
||||||
|
} else {
|
||||||
|
console.log('✓ Commit created successfully.\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print an info message with consistent formatting
|
||||||
|
*/
|
||||||
|
export function printInfo(message: string): void {
|
||||||
|
console.log(` ℹ️ ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print a success message
|
||||||
|
*/
|
||||||
|
export function printSuccess(message: string): void {
|
||||||
|
console.log(` ✓ ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print a warning message
|
||||||
|
*/
|
||||||
|
export function printWarning(message: string): void {
|
||||||
|
logger.log('warn', `⚠️ ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print an error message
|
||||||
|
*/
|
||||||
|
export function printError(message: string): void {
|
||||||
|
logger.log('error', `✗ ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print commit message being created
|
||||||
|
*/
|
||||||
|
export function printCommitMessage(commitString: string): void {
|
||||||
|
console.log(`\n 📝 Commit: ${commitString}\n`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import * as plugins from './mod.plugins.js';
|
||||||
|
|
||||||
|
export interface ICommitConfig {
|
||||||
|
alwaysTest: boolean;
|
||||||
|
alwaysBuild: boolean;
|
||||||
|
confirmation: 'prompt' | 'auto' | 'plan';
|
||||||
|
steps: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages commit configuration stored in .smartconfig.json
|
||||||
|
* under @git.zone/cli.commit namespace
|
||||||
|
*/
|
||||||
|
export class CommitConfig {
|
||||||
|
private cwd: string;
|
||||||
|
private config: ICommitConfig;
|
||||||
|
|
||||||
|
constructor(cwd: string = process.cwd()) {
|
||||||
|
this.cwd = cwd;
|
||||||
|
this.config = { alwaysTest: false, alwaysBuild: false, confirmation: 'prompt', steps: ['analyze', 'changelog', 'commit'] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a CommitConfig instance from current working directory
|
||||||
|
*/
|
||||||
|
public static async fromCwd(cwd: string = process.cwd()): Promise<CommitConfig> {
|
||||||
|
const instance = new CommitConfig(cwd);
|
||||||
|
await instance.load();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load configuration from .smartconfig.json
|
||||||
|
*/
|
||||||
|
public async load(): Promise<void> {
|
||||||
|
const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
|
||||||
|
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
|
||||||
|
|
||||||
|
const alwaysTest = gitzoneConfig?.commit?.alwaysTest ?? false;
|
||||||
|
const alwaysBuild = gitzoneConfig?.commit?.alwaysBuild ?? false;
|
||||||
|
this.config = {
|
||||||
|
alwaysTest,
|
||||||
|
alwaysBuild,
|
||||||
|
confirmation: gitzoneConfig?.commit?.confirmation ?? 'prompt',
|
||||||
|
steps: gitzoneConfig?.commit?.steps || [
|
||||||
|
'analyze',
|
||||||
|
...(alwaysTest ? ['test'] : []),
|
||||||
|
...(alwaysBuild ? ['build'] : []),
|
||||||
|
'changelog',
|
||||||
|
'commit',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save configuration to .smartconfig.json
|
||||||
|
*/
|
||||||
|
public async save(): Promise<void> {
|
||||||
|
const smartconfigPath = plugins.path.join(this.cwd, '.smartconfig.json');
|
||||||
|
let smartconfigData: any = {};
|
||||||
|
|
||||||
|
// Read existing .smartconfig.json
|
||||||
|
if (await plugins.smartfs.file(smartconfigPath).exists()) {
|
||||||
|
const content = await plugins.smartfs.file(smartconfigPath).encoding('utf8').read();
|
||||||
|
smartconfigData = JSON.parse(content as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure @git.zone/cli namespace exists
|
||||||
|
if (!smartconfigData['@git.zone/cli']) {
|
||||||
|
smartconfigData['@git.zone/cli'] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure commit object exists
|
||||||
|
if (!smartconfigData['@git.zone/cli'].commit) {
|
||||||
|
smartconfigData['@git.zone/cli'].commit = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update commit settings
|
||||||
|
smartconfigData['@git.zone/cli'].commit.alwaysTest = this.config.alwaysTest;
|
||||||
|
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
|
||||||
|
await plugins.smartfs
|
||||||
|
.file(smartconfigPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.write(JSON.stringify(smartconfigData, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get alwaysTest setting
|
||||||
|
*/
|
||||||
|
public getAlwaysTest(): boolean {
|
||||||
|
return this.config.alwaysTest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set alwaysTest setting
|
||||||
|
*/
|
||||||
|
public setAlwaysTest(value: boolean): void {
|
||||||
|
this.config.alwaysTest = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get alwaysBuild setting
|
||||||
|
*/
|
||||||
|
public getAlwaysBuild(): boolean {
|
||||||
|
return this.config.alwaysBuild;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set alwaysBuild setting
|
||||||
|
*/
|
||||||
|
public setAlwaysBuild(value: boolean): void {
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import * as plugins from './mod.plugins.js';
|
||||||
|
|
||||||
|
export type TAccessLevel = 'public' | 'private';
|
||||||
|
|
||||||
|
export interface IReleaseConfig {
|
||||||
|
registries: string[];
|
||||||
|
accessLevel: TAccessLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages release configuration stored in .smartconfig.json
|
||||||
|
* under @git.zone/cli.release namespace
|
||||||
|
*/
|
||||||
|
export class ReleaseConfig {
|
||||||
|
private cwd: string;
|
||||||
|
private config: IReleaseConfig;
|
||||||
|
|
||||||
|
constructor(cwd: string = process.cwd()) {
|
||||||
|
this.cwd = cwd;
|
||||||
|
this.config = { registries: [], accessLevel: 'public' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a ReleaseConfig instance from current working directory
|
||||||
|
*/
|
||||||
|
public static async fromCwd(cwd: string = process.cwd()): Promise<ReleaseConfig> {
|
||||||
|
const instance = new ReleaseConfig(cwd);
|
||||||
|
await instance.load();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load configuration from .smartconfig.json
|
||||||
|
*/
|
||||||
|
public async load(): Promise<void> {
|
||||||
|
const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
|
||||||
|
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
|
||||||
|
const npmTarget = gitzoneConfig?.release?.targets?.npm || {};
|
||||||
|
|
||||||
|
this.config = {
|
||||||
|
registries: npmTarget.registries || [],
|
||||||
|
accessLevel: npmTarget.accessLevel || 'public',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save configuration to .smartconfig.json
|
||||||
|
*/
|
||||||
|
public async save(): Promise<void> {
|
||||||
|
const smartconfigPath = plugins.path.join(this.cwd, '.smartconfig.json');
|
||||||
|
let smartconfigData: any = {};
|
||||||
|
|
||||||
|
// Read existing .smartconfig.json
|
||||||
|
if (await plugins.smartfs.file(smartconfigPath).exists()) {
|
||||||
|
const content = await plugins.smartfs.file(smartconfigPath).encoding('utf8').read();
|
||||||
|
smartconfigData = JSON.parse(content as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure @git.zone/cli namespace exists
|
||||||
|
if (!smartconfigData['@git.zone/cli']) {
|
||||||
|
smartconfigData['@git.zone/cli'] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure release object exists
|
||||||
|
if (!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
|
||||||
|
smartconfigData['@git.zone/cli'].release.targets.npm.enabled = this.config.registries.length > 0;
|
||||||
|
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
|
||||||
|
await plugins.smartfs
|
||||||
|
.file(smartconfigPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.write(JSON.stringify(smartconfigData, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all configured registries
|
||||||
|
*/
|
||||||
|
public getRegistries(): string[] {
|
||||||
|
return [...this.config.registries];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any registries are configured
|
||||||
|
*/
|
||||||
|
public hasRegistries(): boolean {
|
||||||
|
return this.config.registries.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a registry URL
|
||||||
|
* @returns true if added, false if already exists
|
||||||
|
*/
|
||||||
|
public addRegistry(url: string): boolean {
|
||||||
|
const normalizedUrl = this.normalizeUrl(url);
|
||||||
|
|
||||||
|
if (this.config.registries.includes(normalizedUrl)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config.registries.push(normalizedUrl);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a registry URL
|
||||||
|
* @returns true if removed, false if not found
|
||||||
|
*/
|
||||||
|
public removeRegistry(url: string): boolean {
|
||||||
|
const normalizedUrl = this.normalizeUrl(url);
|
||||||
|
const index = this.config.registries.indexOf(normalizedUrl);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config.registries.splice(index, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all registries
|
||||||
|
*/
|
||||||
|
public clearRegistries(): void {
|
||||||
|
this.config.registries = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the npm access level
|
||||||
|
*/
|
||||||
|
public getAccessLevel(): TAccessLevel {
|
||||||
|
return this.config.accessLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the npm access level
|
||||||
|
*/
|
||||||
|
public setAccessLevel(level: TAccessLevel): void {
|
||||||
|
this.config.accessLevel = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a registry URL (ensure it has https:// prefix)
|
||||||
|
*/
|
||||||
|
private normalizeUrl(url: string): string {
|
||||||
|
let normalized = url.trim();
|
||||||
|
|
||||||
|
// Add https:// if no protocol specified
|
||||||
|
if (!normalized.startsWith('http://') && !normalized.startsWith('https://')) {
|
||||||
|
normalized = `https://${normalized}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing slash
|
||||||
|
if (normalized.endsWith('/')) {
|
||||||
|
normalized = normalized.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
|||||||
|
// mod_config plugins
|
||||||
|
export * from '../plugins.js';
|
||||||
|
export { logger } from '../gitzone.logging.js';
|
||||||
@@ -36,7 +36,10 @@ export const run = async () => {
|
|||||||
const registryUrls = answerBucket.getAnswerFor(`registryUrls`).split(',');
|
const registryUrls = answerBucket.getAnswerFor(`registryUrls`).split(',');
|
||||||
const oldPackageName = answerBucket.getAnswerFor(`oldPackageName`);
|
const oldPackageName = answerBucket.getAnswerFor(`oldPackageName`);
|
||||||
const newPackageName = answerBucket.getAnswerFor(`newPackageName`);
|
const newPackageName = answerBucket.getAnswerFor(`newPackageName`);
|
||||||
logger.log('info', `Deprecating package ${oldPackageName} in favour of ${newPackageName}`);
|
logger.log(
|
||||||
|
'info',
|
||||||
|
`Deprecating package ${oldPackageName} in favour of ${newPackageName}`,
|
||||||
|
);
|
||||||
const smartshellInstance = new plugins.smartshell.Smartshell({
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
executor: 'bash',
|
executor: 'bash',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,33 +1,31 @@
|
|||||||
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 } from './interfaces.format.js';
|
import type { IPlannedChange, ICheckResult } from './interfaces.format.js';
|
||||||
import { Project } from '../classes.project.js';
|
import { Project } from '../classes.project.js';
|
||||||
import { ChangeCache } from './classes.changecache.js';
|
import { FormatStats } from './classes.formatstats.js';
|
||||||
|
|
||||||
export abstract class BaseFormatter {
|
export abstract class BaseFormatter {
|
||||||
protected context: FormatContext;
|
protected context: FormatContext;
|
||||||
protected project: Project;
|
protected project: Project;
|
||||||
protected cache: ChangeCache;
|
protected stats: FormatStats;
|
||||||
protected stats: any; // Will be FormatStats from context
|
|
||||||
|
|
||||||
constructor(context: FormatContext, project: Project) {
|
constructor(context: FormatContext, project: Project) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.project = project;
|
this.project = project;
|
||||||
this.cache = context.getChangeCache();
|
|
||||||
this.stats = context.getFormatStats();
|
this.stats = context.getFormatStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract get name(): string;
|
abstract get name(): string;
|
||||||
abstract analyze(): Promise<IPlannedChange[]>;
|
abstract analyze(): Promise<IPlannedChange[]>;
|
||||||
abstract applyChange(change: IPlannedChange): Promise<void>;
|
abstract applyChange(change: IPlannedChange): Promise<void>;
|
||||||
|
|
||||||
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 {
|
||||||
await this.preExecute();
|
await this.preExecute();
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
try {
|
try {
|
||||||
await this.applyChange(change);
|
await this.applyChange(change);
|
||||||
@@ -37,57 +35,131 @@ export abstract class BaseFormatter {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.postExecute();
|
await this.postExecute();
|
||||||
} catch (error) {
|
|
||||||
await this.context.rollbackOperation();
|
|
||||||
throw error;
|
|
||||||
} finally {
|
} finally {
|
||||||
this.stats.endModule(this.name, startTime);
|
this.stats.endModule(this.name, startTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async preExecute(): Promise<void> {
|
protected async preExecute(): Promise<void> {
|
||||||
// Override in subclasses if needed
|
// Override in subclasses if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async postExecute(): Promise<void> {
|
protected async postExecute(): Promise<void> {
|
||||||
// Override in subclasses if needed
|
// Override in subclasses if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async modifyFile(filepath: string, content: string): Promise<void> {
|
protected async modifyFile(filepath: string, content: string): Promise<void> {
|
||||||
await this.context.trackFileChange(filepath);
|
if (!filepath || filepath.trim() === '') {
|
||||||
await plugins.smartfile.memory.toFs(content, filepath);
|
throw new Error(`Invalid empty filepath in modifyFile`);
|
||||||
await this.cache.updateFileCache(filepath);
|
}
|
||||||
|
|
||||||
|
let normalizedPath = filepath;
|
||||||
|
if (!plugins.path.parse(filepath).dir) {
|
||||||
|
normalizedPath = './' + filepath;
|
||||||
|
}
|
||||||
|
|
||||||
|
await plugins.smartfs.file(normalizedPath).encoding('utf8').write(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async createFile(filepath: string, content: string): Promise<void> {
|
protected async createFile(filepath: string, content: string): Promise<void> {
|
||||||
await plugins.smartfile.memory.toFs(content, filepath);
|
let normalizedPath = filepath;
|
||||||
await this.cache.updateFileCache(filepath);
|
if (!plugins.path.parse(filepath).dir) {
|
||||||
|
normalizedPath = './' + filepath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure parent directory exists
|
||||||
|
const dir = plugins.path.dirname(normalizedPath);
|
||||||
|
if (dir && dir !== '.') {
|
||||||
|
await plugins.smartfs.directory(dir).recursive().create();
|
||||||
|
}
|
||||||
|
|
||||||
|
await plugins.smartfs.file(normalizedPath).encoding('utf8').write(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async deleteFile(filepath: string): Promise<void> {
|
protected async deleteFile(filepath: string): Promise<void> {
|
||||||
await this.context.trackFileChange(filepath);
|
await plugins.smartfs.file(filepath).delete();
|
||||||
await plugins.smartfile.fs.remove(filepath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async shouldProcessFile(filepath: string): Promise<boolean> {
|
/**
|
||||||
const config = new plugins.npmextra.Npmextra();
|
* Check for diffs without applying changes
|
||||||
const useCache = config.dataFor('gitzone.format.cache.enabled', true);
|
*/
|
||||||
|
async check(): Promise<ICheckResult> {
|
||||||
if (!useCache) {
|
const changes = await this.analyze();
|
||||||
return true; // Process all files if cache is disabled
|
const diffs: ICheckResult['diffs'] = [];
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
if (change.path === '<various files>') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.type === 'modify' || change.type === 'create') {
|
||||||
|
let currentContent: string | undefined;
|
||||||
|
try {
|
||||||
|
currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
|
||||||
|
} catch {
|
||||||
|
currentContent = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContent = change.content;
|
||||||
|
|
||||||
|
if (currentContent !== newContent && newContent !== undefined) {
|
||||||
|
diffs.push({
|
||||||
|
path: change.path,
|
||||||
|
type: change.type,
|
||||||
|
before: currentContent,
|
||||||
|
after: newContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (change.type === 'delete') {
|
||||||
|
try {
|
||||||
|
const currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
|
||||||
|
diffs.push({
|
||||||
|
path: change.path,
|
||||||
|
type: 'delete',
|
||||||
|
before: currentContent,
|
||||||
|
after: undefined,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist, nothing to delete
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChanged = await this.cache.hasFileChanged(filepath);
|
return {
|
||||||
|
hasDiff: diffs.length > 0,
|
||||||
// Record cache statistics
|
diffs,
|
||||||
if (hasChanged) {
|
};
|
||||||
this.stats.recordCacheMiss();
|
|
||||||
} else {
|
|
||||||
this.stats.recordCacheHit();
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasChanged;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
displayDiff(diff: ICheckResult['diffs'][0]): void {
|
||||||
|
console.log(`\n--- ${diff.path}`);
|
||||||
|
if (diff.before && diff.after) {
|
||||||
|
console.log(plugins.smartdiff.formatUnifiedDiffForConsole(diff.before, diff.after, {
|
||||||
|
originalFileName: diff.path,
|
||||||
|
revisedFileName: diff.path,
|
||||||
|
context: 3,
|
||||||
|
}));
|
||||||
|
} else if (diff.after && !diff.before) {
|
||||||
|
console.log(' (new file)');
|
||||||
|
const lines = diff.after.split('\n').slice(0, 10);
|
||||||
|
lines.forEach(line => console.log(` + ${line}`));
|
||||||
|
if (diff.after.split('\n').length > 10) {
|
||||||
|
console.log(' ... (truncated)');
|
||||||
|
}
|
||||||
|
} else if (diff.before && !diff.after) {
|
||||||
|
console.log(' (file will be deleted)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displayAllDiffs(result: ICheckResult): void {
|
||||||
|
if (!result.hasDiff) {
|
||||||
|
console.log(' No changes detected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const diff of result.diffs) {
|
||||||
|
this.displayDiff(diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
|
|
||||||
export interface IFileCache {
|
|
||||||
path: string;
|
|
||||||
checksum: string;
|
|
||||||
modified: number;
|
|
||||||
size: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICacheManifest {
|
|
||||||
version: string;
|
|
||||||
lastFormat: number;
|
|
||||||
files: IFileCache[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ChangeCache {
|
|
||||||
private cacheDir: string;
|
|
||||||
private manifestPath: string;
|
|
||||||
private cacheVersion = '1.0.0';
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.cacheDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-cache');
|
|
||||||
this.manifestPath = plugins.path.join(this.cacheDir, 'manifest.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
|
||||||
await plugins.smartfile.fs.ensureDir(this.cacheDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getManifest(): Promise<ICacheManifest> {
|
|
||||||
const exists = await plugins.smartfile.fs.fileExists(this.manifestPath);
|
|
||||||
if (!exists) {
|
|
||||||
return {
|
|
||||||
version: this.cacheVersion,
|
|
||||||
lastFormat: 0,
|
|
||||||
files: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await plugins.smartfile.fs.toStringSync(this.manifestPath);
|
|
||||||
return JSON.parse(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveManifest(manifest: ICacheManifest): Promise<void> {
|
|
||||||
await plugins.smartfile.memory.toFs(JSON.stringify(manifest, null, 2), this.manifestPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
async hasFileChanged(filePath: string): Promise<boolean> {
|
|
||||||
const absolutePath = plugins.path.isAbsolute(filePath)
|
|
||||||
? filePath
|
|
||||||
: plugins.path.join(paths.cwd, filePath);
|
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
const exists = await plugins.smartfile.fs.fileExists(absolutePath);
|
|
||||||
if (!exists) {
|
|
||||||
return true; // File doesn't exist, so it's "changed" (will be created)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current file stats
|
|
||||||
const stats = await plugins.smartfile.fs.stat(absolutePath);
|
|
||||||
const content = await plugins.smartfile.fs.toStringSync(absolutePath);
|
|
||||||
const currentChecksum = this.calculateChecksum(content);
|
|
||||||
|
|
||||||
// Get cached info
|
|
||||||
const manifest = await this.getManifest();
|
|
||||||
const cachedFile = manifest.files.find(f => f.path === filePath);
|
|
||||||
|
|
||||||
if (!cachedFile) {
|
|
||||||
return true; // Not in cache, so it's changed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare checksums
|
|
||||||
return cachedFile.checksum !== currentChecksum ||
|
|
||||||
cachedFile.size !== stats.size ||
|
|
||||||
cachedFile.modified !== stats.mtimeMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateFileCache(filePath: string): Promise<void> {
|
|
||||||
const absolutePath = plugins.path.isAbsolute(filePath)
|
|
||||||
? filePath
|
|
||||||
: plugins.path.join(paths.cwd, filePath);
|
|
||||||
|
|
||||||
// Get current file stats
|
|
||||||
const stats = await plugins.smartfile.fs.stat(absolutePath);
|
|
||||||
const content = await plugins.smartfile.fs.toStringSync(absolutePath);
|
|
||||||
const checksum = this.calculateChecksum(content);
|
|
||||||
|
|
||||||
// Update manifest
|
|
||||||
const manifest = await this.getManifest();
|
|
||||||
const existingIndex = manifest.files.findIndex(f => f.path === filePath);
|
|
||||||
|
|
||||||
const cacheEntry: IFileCache = {
|
|
||||||
path: filePath,
|
|
||||||
checksum,
|
|
||||||
modified: stats.mtimeMs,
|
|
||||||
size: stats.size
|
|
||||||
};
|
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
|
||||||
manifest.files[existingIndex] = cacheEntry;
|
|
||||||
} else {
|
|
||||||
manifest.files.push(cacheEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest.lastFormat = Date.now();
|
|
||||||
await this.saveManifest(manifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getChangedFiles(filePaths: string[]): Promise<string[]> {
|
|
||||||
const changedFiles: string[] = [];
|
|
||||||
|
|
||||||
for (const filePath of filePaths) {
|
|
||||||
if (await this.hasFileChanged(filePath)) {
|
|
||||||
changedFiles.push(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return changedFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
async clean(): Promise<void> {
|
|
||||||
const manifest = await this.getManifest();
|
|
||||||
const validFiles: IFileCache[] = [];
|
|
||||||
|
|
||||||
// Remove entries for files that no longer exist
|
|
||||||
for (const file of manifest.files) {
|
|
||||||
const absolutePath = plugins.path.isAbsolute(file.path)
|
|
||||||
? file.path
|
|
||||||
: plugins.path.join(paths.cwd, file.path);
|
|
||||||
|
|
||||||
if (await plugins.smartfile.fs.fileExists(absolutePath)) {
|
|
||||||
validFiles.push(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest.files = validFiles;
|
|
||||||
await this.saveManifest(manifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
private calculateChecksum(content: string | Buffer): string {
|
|
||||||
return plugins.crypto.createHash('sha256').update(content).digest('hex');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import { BaseFormatter } from './classes.baseformatter.js';
|
|
||||||
|
|
||||||
export interface IModuleDependency {
|
|
||||||
module: string;
|
|
||||||
dependencies: Set<string>;
|
|
||||||
dependents: Set<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DependencyAnalyzer {
|
|
||||||
private moduleDependencies: Map<string, IModuleDependency> = new Map();
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.initializeDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
private initializeDependencies(): void {
|
|
||||||
// Define dependencies between format modules
|
|
||||||
const dependencies = {
|
|
||||||
'cleanup': [], // No dependencies
|
|
||||||
'npmextra': [], // No dependencies
|
|
||||||
'license': ['npmextra'], // Depends on npmextra for config
|
|
||||||
'packagejson': ['npmextra'], // Depends on npmextra for config
|
|
||||||
'templates': ['npmextra', 'packagejson'], // Depends on both
|
|
||||||
'gitignore': ['templates'], // Depends on templates
|
|
||||||
'tsconfig': ['packagejson'], // Depends on package.json
|
|
||||||
'prettier': ['cleanup', 'npmextra', 'packagejson', 'templates', 'gitignore', 'tsconfig'], // Runs after most others
|
|
||||||
'readme': ['npmextra', 'packagejson'], // Depends on project metadata
|
|
||||||
'copy': ['npmextra'], // Depends on config
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize all modules
|
|
||||||
for (const [module, deps] of Object.entries(dependencies)) {
|
|
||||||
this.moduleDependencies.set(module, {
|
|
||||||
module,
|
|
||||||
dependencies: new Set(deps),
|
|
||||||
dependents: new Set()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build reverse dependencies (dependents)
|
|
||||||
for (const [module, deps] of Object.entries(dependencies)) {
|
|
||||||
for (const dep of deps) {
|
|
||||||
const depModule = this.moduleDependencies.get(dep);
|
|
||||||
if (depModule) {
|
|
||||||
depModule.dependents.add(module);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getExecutionGroups(modules: BaseFormatter[]): BaseFormatter[][] {
|
|
||||||
const modulesMap = new Map(modules.map(m => [m.name, m]));
|
|
||||||
const executed = new Set<string>();
|
|
||||||
const groups: BaseFormatter[][] = [];
|
|
||||||
|
|
||||||
while (executed.size < modules.length) {
|
|
||||||
const currentGroup: BaseFormatter[] = [];
|
|
||||||
|
|
||||||
for (const module of modules) {
|
|
||||||
if (executed.has(module.name)) continue;
|
|
||||||
|
|
||||||
const dependency = this.moduleDependencies.get(module.name);
|
|
||||||
if (!dependency) {
|
|
||||||
// Unknown module, execute in isolation
|
|
||||||
currentGroup.push(module);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if all dependencies have been executed
|
|
||||||
const allDepsExecuted = Array.from(dependency.dependencies)
|
|
||||||
.every(dep => executed.has(dep) || !modulesMap.has(dep));
|
|
||||||
|
|
||||||
if (allDepsExecuted) {
|
|
||||||
currentGroup.push(module);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentGroup.length === 0) {
|
|
||||||
// Circular dependency or error - execute remaining modules
|
|
||||||
for (const module of modules) {
|
|
||||||
if (!executed.has(module.name)) {
|
|
||||||
currentGroup.push(module);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentGroup.forEach(m => executed.add(m.name));
|
|
||||||
groups.push(currentGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
canRunInParallel(module1: string, module2: string): boolean {
|
|
||||||
const dep1 = this.moduleDependencies.get(module1);
|
|
||||||
const dep2 = this.moduleDependencies.get(module2);
|
|
||||||
|
|
||||||
if (!dep1 || !dep2) return false;
|
|
||||||
|
|
||||||
// Check if module1 depends on module2 or vice versa
|
|
||||||
return !dep1.dependencies.has(module2) &&
|
|
||||||
!dep2.dependencies.has(module1) &&
|
|
||||||
!dep1.dependents.has(module2) &&
|
|
||||||
!dep2.dependents.has(module1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,72 +4,86 @@ import { logger } from '../gitzone.logging.js';
|
|||||||
|
|
||||||
export class DiffReporter {
|
export class DiffReporter {
|
||||||
private diffs: Map<string, string> = new Map();
|
private diffs: Map<string, string> = new Map();
|
||||||
|
|
||||||
async generateDiff(filePath: string, oldContent: string, newContent: string): Promise<string> {
|
async generateDiff(
|
||||||
|
filePath: string,
|
||||||
|
oldContent: string,
|
||||||
|
newContent: string,
|
||||||
|
): Promise<string> {
|
||||||
const diff = plugins.smartdiff.createDiff(oldContent, newContent);
|
const diff = plugins.smartdiff.createDiff(oldContent, newContent);
|
||||||
this.diffs.set(filePath, diff);
|
this.diffs.set(filePath, diff);
|
||||||
return diff;
|
return diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateDiffForChange(change: IPlannedChange): Promise<string | null> {
|
async generateDiffForChange(change: IPlannedChange): Promise<string | null> {
|
||||||
if (change.type !== 'modify') {
|
if (change.type !== 'modify') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const exists = await plugins.smartfile.fs.fileExists(change.path);
|
const exists = await plugins.smartfs.file(change.path).exists();
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentContent = await plugins.smartfile.fs.toStringSync(change.path);
|
const currentContent = (await plugins.smartfs
|
||||||
|
.file(change.path)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
// For planned changes, we need the new content
|
// For planned changes, we need the new content
|
||||||
if (!change.content) {
|
if (!change.content) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.generateDiff(change.path, currentContent, change.content);
|
return await this.generateDiff(
|
||||||
|
change.path,
|
||||||
|
currentContent,
|
||||||
|
change.content,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log('error', `Failed to generate diff for ${change.path}: ${error.message}`);
|
logger.log(
|
||||||
|
'error',
|
||||||
|
`Failed to generate diff for ${change.path}: ${error.message}`,
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
displayDiff(filePath: string, diff?: string): void {
|
displayDiff(filePath: string, diff?: string): void {
|
||||||
const diffToShow = diff || this.diffs.get(filePath);
|
const diffToShow = diff || this.diffs.get(filePath);
|
||||||
|
|
||||||
if (!diffToShow) {
|
if (!diffToShow) {
|
||||||
logger.log('warn', `No diff available for ${filePath}`);
|
logger.log('warn', `No diff available for ${filePath}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n${this.formatDiffHeader(filePath)}`);
|
console.log(`\n${this.formatDiffHeader(filePath)}`);
|
||||||
console.log(this.colorDiff(diffToShow));
|
console.log(this.colorDiff(diffToShow));
|
||||||
console.log('━'.repeat(50));
|
console.log('━'.repeat(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
displayAllDiffs(): void {
|
displayAllDiffs(): void {
|
||||||
if (this.diffs.size === 0) {
|
if (this.diffs.size === 0) {
|
||||||
logger.log('info', 'No diffs to display');
|
logger.log('info', 'No diffs to display');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\nFile Changes:');
|
console.log('\nFile Changes:');
|
||||||
console.log('═'.repeat(50));
|
console.log('═'.repeat(50));
|
||||||
|
|
||||||
for (const [filePath, diff] of this.diffs) {
|
for (const [filePath, diff] of this.diffs) {
|
||||||
this.displayDiff(filePath, diff);
|
this.displayDiff(filePath, diff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatDiffHeader(filePath: string): string {
|
private formatDiffHeader(filePath: string): string {
|
||||||
return `📄 ${filePath}`;
|
return `📄 ${filePath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private colorDiff(diff: string): string {
|
private colorDiff(diff: string): string {
|
||||||
const lines = diff.split('\n');
|
const lines = diff.split('\n');
|
||||||
const coloredLines = lines.map(line => {
|
const coloredLines = lines.map((line) => {
|
||||||
if (line.startsWith('+') && !line.startsWith('+++')) {
|
if (line.startsWith('+') && !line.startsWith('+++')) {
|
||||||
return `\x1b[32m${line}\x1b[0m`; // Green for additions
|
return `\x1b[32m${line}\x1b[0m`; // Green for additions
|
||||||
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
||||||
@@ -80,29 +94,32 @@ export class DiffReporter {
|
|||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return coloredLines.join('\n');
|
return coloredLines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveDiffReport(outputPath: string): Promise<void> {
|
async saveDiffReport(outputPath: string): Promise<void> {
|
||||||
const report = {
|
const report = {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
totalFiles: this.diffs.size,
|
totalFiles: this.diffs.size,
|
||||||
diffs: Array.from(this.diffs.entries()).map(([path, diff]) => ({
|
diffs: Array.from(this.diffs.entries()).map(([path, diff]) => ({
|
||||||
path,
|
path,
|
||||||
diff
|
diff,
|
||||||
}))
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
await plugins.smartfile.memory.toFs(JSON.stringify(report, null, 2), outputPath);
|
await plugins.smartfs
|
||||||
|
.file(outputPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.write(JSON.stringify(report, null, 2));
|
||||||
logger.log('info', `Diff report saved to ${outputPath}`);
|
logger.log('info', `Diff report saved to ${outputPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasAnyDiffs(): boolean {
|
hasAnyDiffs(): boolean {
|
||||||
return this.diffs.size > 0;
|
return this.diffs.size > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDiffCount(): number {
|
getDiffCount(): number {
|
||||||
return this.diffs.size;
|
return this.diffs.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,31 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from "./mod.plugins.js";
|
||||||
import { RollbackManager } from './classes.rollbackmanager.js';
|
import { FormatStats } from "./classes.formatstats.js";
|
||||||
import { ChangeCache } from './classes.changecache.js';
|
|
||||||
import { FormatStats } from './classes.formatstats.js';
|
interface IFormatContextOptions {
|
||||||
import type { IFormatOperation, IFormatPlan } from './interfaces.format.js';
|
interactive?: boolean;
|
||||||
|
jsonOutput?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class FormatContext {
|
export class FormatContext {
|
||||||
private rollbackManager: RollbackManager;
|
|
||||||
private currentOperation: IFormatOperation | null = null;
|
|
||||||
private changeCache: ChangeCache;
|
|
||||||
private formatStats: FormatStats;
|
private formatStats: FormatStats;
|
||||||
|
private interactive: boolean;
|
||||||
constructor() {
|
private jsonOutput: boolean;
|
||||||
this.rollbackManager = new RollbackManager();
|
|
||||||
this.changeCache = new ChangeCache();
|
constructor(options: IFormatContextOptions = {}) {
|
||||||
this.formatStats = new FormatStats();
|
this.formatStats = new FormatStats();
|
||||||
|
this.interactive = options.interactive ?? true;
|
||||||
|
this.jsonOutput = options.jsonOutput ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async beginOperation(): Promise<void> {
|
|
||||||
this.currentOperation = await this.rollbackManager.createOperation();
|
|
||||||
}
|
|
||||||
|
|
||||||
async trackFileChange(filepath: string): Promise<void> {
|
|
||||||
if (!this.currentOperation) {
|
|
||||||
throw new Error('No operation in progress. Call beginOperation() first.');
|
|
||||||
}
|
|
||||||
await this.rollbackManager.backupFile(filepath, this.currentOperation.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async commitOperation(): Promise<void> {
|
|
||||||
if (!this.currentOperation) {
|
|
||||||
throw new Error('No operation in progress. Call beginOperation() first.');
|
|
||||||
}
|
|
||||||
await this.rollbackManager.markComplete(this.currentOperation.id);
|
|
||||||
this.currentOperation = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async rollbackOperation(): Promise<void> {
|
|
||||||
if (!this.currentOperation) {
|
|
||||||
throw new Error('No operation in progress. Call beginOperation() first.');
|
|
||||||
}
|
|
||||||
await this.rollbackManager.rollback(this.currentOperation.id);
|
|
||||||
this.currentOperation = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async rollbackTo(operationId: string): Promise<void> {
|
|
||||||
await this.rollbackManager.rollback(operationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRollbackManager(): RollbackManager {
|
|
||||||
return this.rollbackManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
getChangeCache(): ChangeCache {
|
|
||||||
return this.changeCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
async initializeCache(): Promise<void> {
|
|
||||||
await this.changeCache.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormatStats(): FormatStats {
|
getFormatStats(): FormatStats {
|
||||||
return this.formatStats;
|
return this.formatStats;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
isInteractive(): boolean {
|
||||||
|
return this.interactive;
|
||||||
|
}
|
||||||
|
|
||||||
|
isJsonOutput(): boolean {
|
||||||
|
return this.jsonOutput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ 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 } from './interfaces.format.js';
|
||||||
|
import { getModuleIcon } from './interfaces.format.js';
|
||||||
import { logger } from '../gitzone.logging.js';
|
import { logger } from '../gitzone.logging.js';
|
||||||
import { DependencyAnalyzer } from './classes.dependency-analyzer.js';
|
|
||||||
import { DiffReporter } from './classes.diffreporter.js';
|
import { DiffReporter } from './classes.diffreporter.js';
|
||||||
|
|
||||||
export class FormatPlanner {
|
export class FormatPlanner {
|
||||||
private plannedChanges: Map<string, IPlannedChange[]> = new Map();
|
private plannedChanges: Map<string, IPlannedChange[]> = new Map();
|
||||||
private dependencyAnalyzer = new DependencyAnalyzer();
|
|
||||||
private diffReporter = new DiffReporter();
|
private diffReporter = new DiffReporter();
|
||||||
|
|
||||||
async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> {
|
async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> {
|
||||||
const plan: IFormatPlan = {
|
const plan: IFormatPlan = {
|
||||||
summary: {
|
summary: {
|
||||||
@@ -18,21 +17,19 @@ export class FormatPlanner {
|
|||||||
filesAdded: 0,
|
filesAdded: 0,
|
||||||
filesModified: 0,
|
filesModified: 0,
|
||||||
filesRemoved: 0,
|
filesRemoved: 0,
|
||||||
estimatedTime: 0
|
|
||||||
},
|
},
|
||||||
changes: [],
|
changes: [],
|
||||||
warnings: []
|
warnings: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const module of modules) {
|
for (const module of modules) {
|
||||||
try {
|
try {
|
||||||
const changes = await module.analyze();
|
const changes = await module.analyze();
|
||||||
this.plannedChanges.set(module.name, changes);
|
this.plannedChanges.set(module.name, changes);
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
plan.changes.push(change);
|
plan.changes.push(change);
|
||||||
|
|
||||||
// Update summary
|
|
||||||
switch (change.type) {
|
switch (change.type) {
|
||||||
case 'create':
|
case 'create':
|
||||||
plan.summary.filesAdded++;
|
plan.summary.filesAdded++;
|
||||||
@@ -49,67 +46,43 @@ export class FormatPlanner {
|
|||||||
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}: ${error.message}`,
|
||||||
module: module.name
|
module: module.name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plan.summary.totalFiles = plan.summary.filesAdded + plan.summary.filesModified + plan.summary.filesRemoved;
|
plan.summary.totalFiles =
|
||||||
plan.summary.estimatedTime = plan.summary.totalFiles * 100; // 100ms per file estimate
|
plan.summary.filesAdded +
|
||||||
|
plan.summary.filesModified +
|
||||||
|
plan.summary.filesRemoved;
|
||||||
|
|
||||||
return plan;
|
return plan;
|
||||||
}
|
}
|
||||||
|
|
||||||
async executePlan(plan: IFormatPlan, modules: BaseFormatter[], context: FormatContext, parallel: boolean = true): Promise<void> {
|
async executePlan(
|
||||||
await context.beginOperation();
|
plan: IFormatPlan,
|
||||||
|
modules: BaseFormatter[],
|
||||||
|
context: FormatContext,
|
||||||
|
): Promise<void> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
for (const module of modules) {
|
||||||
if (parallel) {
|
const changes = this.plannedChanges.get(module.name) || [];
|
||||||
// Get execution groups based on dependencies
|
|
||||||
const executionGroups = this.dependencyAnalyzer.getExecutionGroups(modules);
|
if (changes.length > 0) {
|
||||||
|
logger.log('info', `Executing ${module.name} formatter...`);
|
||||||
logger.log('info', `Executing formatters in ${executionGroups.length} groups...`);
|
await module.execute(changes);
|
||||||
|
|
||||||
for (let i = 0; i < executionGroups.length; i++) {
|
|
||||||
const group = executionGroups[i];
|
|
||||||
logger.log('info', `Executing group ${i + 1}: ${group.map(m => m.name).join(', ')}`);
|
|
||||||
|
|
||||||
// Execute modules in this group in parallel
|
|
||||||
const promises = group.map(async (module) => {
|
|
||||||
const changes = this.plannedChanges.get(module.name) || [];
|
|
||||||
if (changes.length > 0) {
|
|
||||||
logger.log('info', `Executing ${module.name} formatter...`);
|
|
||||||
await module.execute(changes);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Sequential execution (original implementation)
|
|
||||||
for (const module of modules) {
|
|
||||||
const changes = this.plannedChanges.get(module.name) || [];
|
|
||||||
|
|
||||||
if (changes.length > 0) {
|
|
||||||
logger.log('info', `Executing ${module.name} formatter...`);
|
|
||||||
await module.execute(changes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const endTime = Date.now();
|
|
||||||
const duration = endTime - startTime;
|
|
||||||
logger.log('info', `Format operations completed in ${duration}ms`);
|
|
||||||
|
|
||||||
await context.commitOperation();
|
|
||||||
} catch (error) {
|
|
||||||
await context.rollbackOperation();
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.log('info', `Format operations completed in ${duration}ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async displayPlan(plan: IFormatPlan, detailed: boolean = false): Promise<void> {
|
async displayPlan(
|
||||||
|
plan: IFormatPlan,
|
||||||
|
detailed: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
console.log('\nFormat Plan:');
|
console.log('\nFormat Plan:');
|
||||||
console.log('━'.repeat(50));
|
console.log('━'.repeat(50));
|
||||||
console.log(`Summary: ${plan.summary.totalFiles} files will be changed`);
|
console.log(`Summary: ${plan.summary.totalFiles} files will be changed`);
|
||||||
@@ -118,23 +91,23 @@ export class FormatPlanner {
|
|||||||
console.log(` • ${plan.summary.filesRemoved} deleted files`);
|
console.log(` • ${plan.summary.filesRemoved} deleted files`);
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('Changes by module:');
|
console.log('Changes by module:');
|
||||||
|
|
||||||
// Group changes by module
|
|
||||||
const changesByModule = new Map<string, IPlannedChange[]>();
|
const changesByModule = new Map<string, IPlannedChange[]>();
|
||||||
for (const change of plan.changes) {
|
for (const change of plan.changes) {
|
||||||
const moduleChanges = changesByModule.get(change.module) || [];
|
const moduleChanges = changesByModule.get(change.module) || [];
|
||||||
moduleChanges.push(change);
|
moduleChanges.push(change);
|
||||||
changesByModule.set(change.module, moduleChanges);
|
changesByModule.set(change.module, moduleChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [module, changes] of changesByModule) {
|
for (const [module, changes] of changesByModule) {
|
||||||
console.log(`\n${this.getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`);
|
console.log(
|
||||||
|
`\n${getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`,
|
||||||
|
);
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
const icon = this.getChangeIcon(change.type);
|
const icon = this.getChangeIcon(change.type);
|
||||||
console.log(` ${icon} ${change.path} - ${change.description}`);
|
console.log(` ${icon} ${change.path} - ${change.description}`);
|
||||||
|
|
||||||
// Show diff for modified files if detailed view is requested
|
|
||||||
if (detailed && change.type === 'modify') {
|
if (detailed && change.type === 'modify') {
|
||||||
const diff = await this.diffReporter.generateDiffForChange(change);
|
const diff = await this.diffReporter.generateDiffForChange(change);
|
||||||
if (diff) {
|
if (diff) {
|
||||||
@@ -143,7 +116,7 @@ export class FormatPlanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plan.warnings.length > 0) {
|
if (plan.warnings.length > 0) {
|
||||||
console.log('\nWarnings:');
|
console.log('\nWarnings:');
|
||||||
for (const warning of plan.warnings) {
|
for (const warning of plan.warnings) {
|
||||||
@@ -151,26 +124,10 @@ export class FormatPlanner {
|
|||||||
console.log(` ${icon} ${warning.message}`);
|
console.log(` ${icon} ${warning.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\n' + '━'.repeat(50));
|
console.log('\n' + '━'.repeat(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
private getModuleIcon(module: string): string {
|
|
||||||
const icons: Record<string, string> = {
|
|
||||||
'packagejson': '📦',
|
|
||||||
'license': '📝',
|
|
||||||
'tsconfig': '🔧',
|
|
||||||
'cleanup': '🚮',
|
|
||||||
'gitignore': '🔒',
|
|
||||||
'prettier': '✨',
|
|
||||||
'readme': '📖',
|
|
||||||
'templates': '📄',
|
|
||||||
'npmextra': '⚙️',
|
|
||||||
'copy': '📋'
|
|
||||||
};
|
|
||||||
return icons[module] || '📁';
|
|
||||||
}
|
|
||||||
|
|
||||||
private getChangeIcon(type: 'create' | 'modify' | 'delete'): string {
|
private getChangeIcon(type: 'create' | 'modify' | 'delete'): string {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'create':
|
case 'create':
|
||||||
@@ -181,4 +138,4 @@ export class FormatPlanner {
|
|||||||
return '❌';
|
return '❌';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from './mod.plugins.js';
|
||||||
import { logger } from '../gitzone.logging.js';
|
import { logger } from '../gitzone.logging.js';
|
||||||
|
import { getModuleIcon } from './interfaces.format.js';
|
||||||
|
|
||||||
export interface IModuleStats {
|
export interface IModuleStats {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -23,14 +24,12 @@ export interface IFormatStats {
|
|||||||
totalModified: number;
|
totalModified: number;
|
||||||
totalDeleted: number;
|
totalDeleted: number;
|
||||||
totalErrors: number;
|
totalErrors: number;
|
||||||
cacheHits: number;
|
|
||||||
cacheMisses: number;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FormatStats {
|
export class FormatStats {
|
||||||
private stats: IFormatStats;
|
private stats: IFormatStats;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.stats = {
|
this.stats = {
|
||||||
totalExecutionTime: 0,
|
totalExecutionTime: 0,
|
||||||
@@ -43,12 +42,10 @@ export class FormatStats {
|
|||||||
totalModified: 0,
|
totalModified: 0,
|
||||||
totalDeleted: 0,
|
totalDeleted: 0,
|
||||||
totalErrors: 0,
|
totalErrors: 0,
|
||||||
cacheHits: 0,
|
},
|
||||||
cacheMisses: 0
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
startModule(moduleName: string): void {
|
startModule(moduleName: string): void {
|
||||||
this.stats.moduleStats.set(moduleName, {
|
this.stats.moduleStats.set(moduleName, {
|
||||||
name: moduleName,
|
name: moduleName,
|
||||||
@@ -58,31 +55,35 @@ export class FormatStats {
|
|||||||
successes: 0,
|
successes: 0,
|
||||||
filesCreated: 0,
|
filesCreated: 0,
|
||||||
filesModified: 0,
|
filesModified: 0,
|
||||||
filesDeleted: 0
|
filesDeleted: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
moduleStartTime(moduleName: string): number {
|
moduleStartTime(moduleName: string): number {
|
||||||
return Date.now();
|
return Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
endModule(moduleName: string, startTime: number): void {
|
endModule(moduleName: string, startTime: number): void {
|
||||||
const moduleStats = this.stats.moduleStats.get(moduleName);
|
const moduleStats = this.stats.moduleStats.get(moduleName);
|
||||||
if (moduleStats) {
|
if (moduleStats) {
|
||||||
moduleStats.executionTime = Date.now() - startTime;
|
moduleStats.executionTime = Date.now() - startTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recordFileOperation(moduleName: string, operation: 'create' | 'modify' | 'delete', success: boolean = true): void {
|
recordFileOperation(
|
||||||
|
moduleName: string,
|
||||||
|
operation: 'create' | 'modify' | 'delete',
|
||||||
|
success: boolean = true,
|
||||||
|
): void {
|
||||||
const moduleStats = this.stats.moduleStats.get(moduleName);
|
const moduleStats = this.stats.moduleStats.get(moduleName);
|
||||||
if (!moduleStats) return;
|
if (!moduleStats) return;
|
||||||
|
|
||||||
moduleStats.filesProcessed++;
|
moduleStats.filesProcessed++;
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
moduleStats.successes++;
|
moduleStats.successes++;
|
||||||
this.stats.overallStats.totalFiles++;
|
this.stats.overallStats.totalFiles++;
|
||||||
|
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case 'create':
|
case 'create':
|
||||||
moduleStats.filesCreated++;
|
moduleStats.filesCreated++;
|
||||||
@@ -102,53 +103,44 @@ export class FormatStats {
|
|||||||
this.stats.overallStats.totalErrors++;
|
this.stats.overallStats.totalErrors++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recordCacheHit(): void {
|
|
||||||
this.stats.overallStats.cacheHits++;
|
|
||||||
}
|
|
||||||
|
|
||||||
recordCacheMiss(): void {
|
|
||||||
this.stats.overallStats.cacheMisses++;
|
|
||||||
}
|
|
||||||
|
|
||||||
finish(): void {
|
finish(): void {
|
||||||
this.stats.endTime = Date.now();
|
this.stats.endTime = Date.now();
|
||||||
this.stats.totalExecutionTime = this.stats.endTime - this.stats.startTime;
|
this.stats.totalExecutionTime = this.stats.endTime - this.stats.startTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
displayStats(): void {
|
displayStats(): void {
|
||||||
console.log('\n📊 Format Operation Statistics:');
|
console.log('\n📊 Format Operation Statistics:');
|
||||||
console.log('═'.repeat(50));
|
console.log('═'.repeat(50));
|
||||||
|
|
||||||
// Overall stats
|
// Overall stats
|
||||||
console.log('\nOverall Summary:');
|
console.log('\nOverall Summary:');
|
||||||
console.log(` Total Execution Time: ${this.formatDuration(this.stats.totalExecutionTime)}`);
|
console.log(
|
||||||
|
` Total Execution Time: ${this.formatDuration(this.stats.totalExecutionTime)}`,
|
||||||
|
);
|
||||||
console.log(` Files Processed: ${this.stats.overallStats.totalFiles}`);
|
console.log(` Files Processed: ${this.stats.overallStats.totalFiles}`);
|
||||||
console.log(` • Created: ${this.stats.overallStats.totalCreated}`);
|
console.log(` • Created: ${this.stats.overallStats.totalCreated}`);
|
||||||
console.log(` • Modified: ${this.stats.overallStats.totalModified}`);
|
console.log(` • Modified: ${this.stats.overallStats.totalModified}`);
|
||||||
console.log(` • Deleted: ${this.stats.overallStats.totalDeleted}`);
|
console.log(` • Deleted: ${this.stats.overallStats.totalDeleted}`);
|
||||||
console.log(` Errors: ${this.stats.overallStats.totalErrors}`);
|
console.log(` Errors: ${this.stats.overallStats.totalErrors}`);
|
||||||
|
|
||||||
if (this.stats.overallStats.cacheHits > 0 || this.stats.overallStats.cacheMisses > 0) {
|
|
||||||
const cacheHitRate = this.stats.overallStats.cacheHits /
|
|
||||||
(this.stats.overallStats.cacheHits + this.stats.overallStats.cacheMisses) * 100;
|
|
||||||
console.log(` Cache Hit Rate: ${cacheHitRate.toFixed(1)}%`);
|
|
||||||
console.log(` • Hits: ${this.stats.overallStats.cacheHits}`);
|
|
||||||
console.log(` • Misses: ${this.stats.overallStats.cacheMisses}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Module stats
|
// Module stats
|
||||||
console.log('\nModule Breakdown:');
|
console.log('\nModule Breakdown:');
|
||||||
console.log('─'.repeat(50));
|
console.log('─'.repeat(50));
|
||||||
|
|
||||||
const sortedModules = Array.from(this.stats.moduleStats.values())
|
const sortedModules = Array.from(this.stats.moduleStats.values()).sort(
|
||||||
.sort((a, b) => b.filesProcessed - a.filesProcessed);
|
(a, b) => b.filesProcessed - a.filesProcessed,
|
||||||
|
);
|
||||||
|
|
||||||
for (const moduleStats of sortedModules) {
|
for (const moduleStats of sortedModules) {
|
||||||
console.log(`\n${this.getModuleIcon(moduleStats.name)} ${moduleStats.name}:`);
|
console.log(
|
||||||
console.log(` Execution Time: ${this.formatDuration(moduleStats.executionTime)}`);
|
`\n${getModuleIcon(moduleStats.name)} ${moduleStats.name}:`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` Execution Time: ${this.formatDuration(moduleStats.executionTime)}`,
|
||||||
|
);
|
||||||
console.log(` Files Processed: ${moduleStats.filesProcessed}`);
|
console.log(` Files Processed: ${moduleStats.filesProcessed}`);
|
||||||
|
|
||||||
if (moduleStats.filesCreated > 0) {
|
if (moduleStats.filesCreated > 0) {
|
||||||
console.log(` • Created: ${moduleStats.filesCreated}`);
|
console.log(` • Created: ${moduleStats.filesCreated}`);
|
||||||
}
|
}
|
||||||
@@ -158,27 +150,30 @@ export class FormatStats {
|
|||||||
if (moduleStats.filesDeleted > 0) {
|
if (moduleStats.filesDeleted > 0) {
|
||||||
console.log(` • Deleted: ${moduleStats.filesDeleted}`);
|
console.log(` • Deleted: ${moduleStats.filesDeleted}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (moduleStats.errors > 0) {
|
if (moduleStats.errors > 0) {
|
||||||
console.log(` ❌ Errors: ${moduleStats.errors}`);
|
console.log(` ❌ Errors: ${moduleStats.errors}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\n' + '═'.repeat(50));
|
console.log('\n' + '═'.repeat(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveReport(outputPath: string): Promise<void> {
|
async saveReport(outputPath: string): Promise<void> {
|
||||||
const report = {
|
const report = {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
executionTime: this.stats.totalExecutionTime,
|
executionTime: this.stats.totalExecutionTime,
|
||||||
overallStats: this.stats.overallStats,
|
overallStats: this.stats.overallStats,
|
||||||
moduleStats: Array.from(this.stats.moduleStats.values())
|
moduleStats: Array.from(this.stats.moduleStats.values()),
|
||||||
};
|
};
|
||||||
|
|
||||||
await plugins.smartfile.memory.toFs(JSON.stringify(report, null, 2), outputPath);
|
await plugins.smartfs
|
||||||
|
.file(outputPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.write(JSON.stringify(report, null, 2));
|
||||||
logger.log('info', `Statistics report saved to ${outputPath}`);
|
logger.log('info', `Statistics report saved to ${outputPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatDuration(ms: number): string {
|
private formatDuration(ms: number): string {
|
||||||
if (ms < 1000) {
|
if (ms < 1000) {
|
||||||
return `${ms}ms`;
|
return `${ms}ms`;
|
||||||
@@ -190,20 +185,5 @@ export class FormatStats {
|
|||||||
return `${minutes}m ${seconds}s`;
|
return `${minutes}m ${seconds}s`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getModuleIcon(module: string): string {
|
}
|
||||||
const icons: Record<string, string> = {
|
|
||||||
'packagejson': '📦',
|
|
||||||
'license': '📝',
|
|
||||||
'tsconfig': '🔧',
|
|
||||||
'cleanup': '🚮',
|
|
||||||
'gitignore': '🔒',
|
|
||||||
'prettier': '✨',
|
|
||||||
'readme': '📖',
|
|
||||||
'templates': '📄',
|
|
||||||
'npmextra': '⚙️',
|
|
||||||
'copy': '📋'
|
|
||||||
};
|
|
||||||
return icons[module] || '📁';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,218 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
import type { IFormatOperation } from './interfaces.format.js';
|
|
||||||
|
|
||||||
export class RollbackManager {
|
|
||||||
private backupDir: string;
|
|
||||||
private manifestPath: string;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.backupDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-backups');
|
|
||||||
this.manifestPath = plugins.path.join(this.backupDir, 'manifest.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
async createOperation(): Promise<IFormatOperation> {
|
|
||||||
await this.ensureBackupDir();
|
|
||||||
|
|
||||||
const operation: IFormatOperation = {
|
|
||||||
id: this.generateOperationId(),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
files: [],
|
|
||||||
status: 'pending'
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.updateManifest(operation);
|
|
||||||
return operation;
|
|
||||||
}
|
|
||||||
|
|
||||||
async backupFile(filepath: string, operationId: string): Promise<void> {
|
|
||||||
const operation = await this.getOperation(operationId);
|
|
||||||
if (!operation) {
|
|
||||||
throw new Error(`Operation ${operationId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const absolutePath = plugins.path.isAbsolute(filepath)
|
|
||||||
? filepath
|
|
||||||
: plugins.path.join(paths.cwd, filepath);
|
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
const exists = await plugins.smartfile.fs.fileExists(absolutePath);
|
|
||||||
if (!exists) {
|
|
||||||
// File doesn't exist yet (will be created), so we skip backup
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read file content and metadata
|
|
||||||
const content = await plugins.smartfile.fs.toStringSync(absolutePath);
|
|
||||||
const stats = await plugins.smartfile.fs.stat(absolutePath);
|
|
||||||
const checksum = this.calculateChecksum(content);
|
|
||||||
|
|
||||||
// Create backup
|
|
||||||
const backupPath = this.getBackupPath(operationId, filepath);
|
|
||||||
await plugins.smartfile.fs.ensureDir(plugins.path.dirname(backupPath));
|
|
||||||
await plugins.smartfile.memory.toFs(content, backupPath);
|
|
||||||
|
|
||||||
// Update operation
|
|
||||||
operation.files.push({
|
|
||||||
path: filepath,
|
|
||||||
originalContent: content,
|
|
||||||
checksum,
|
|
||||||
permissions: stats.mode.toString(8)
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.updateManifest(operation);
|
|
||||||
}
|
|
||||||
|
|
||||||
async rollback(operationId: string): Promise<void> {
|
|
||||||
const operation = await this.getOperation(operationId);
|
|
||||||
if (!operation) {
|
|
||||||
throw new Error(`Operation ${operationId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (operation.status === 'rolled-back') {
|
|
||||||
throw new Error(`Operation ${operationId} has already been rolled back`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore files in reverse order
|
|
||||||
for (let i = operation.files.length - 1; i >= 0; i--) {
|
|
||||||
const file = operation.files[i];
|
|
||||||
const absolutePath = plugins.path.isAbsolute(file.path)
|
|
||||||
? file.path
|
|
||||||
: plugins.path.join(paths.cwd, file.path);
|
|
||||||
|
|
||||||
// Verify backup integrity
|
|
||||||
const backupPath = this.getBackupPath(operationId, file.path);
|
|
||||||
const backupContent = await plugins.smartfile.fs.toStringSync(backupPath);
|
|
||||||
const backupChecksum = this.calculateChecksum(backupContent);
|
|
||||||
|
|
||||||
if (backupChecksum !== file.checksum) {
|
|
||||||
throw new Error(`Backup integrity check failed for ${file.path}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore file
|
|
||||||
await plugins.smartfile.memory.toFs(file.originalContent, absolutePath);
|
|
||||||
|
|
||||||
// Restore permissions
|
|
||||||
const mode = parseInt(file.permissions, 8);
|
|
||||||
// Note: Permissions restoration may not work on all platforms
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update operation status
|
|
||||||
operation.status = 'rolled-back';
|
|
||||||
await this.updateManifest(operation);
|
|
||||||
}
|
|
||||||
|
|
||||||
async markComplete(operationId: string): Promise<void> {
|
|
||||||
const operation = await this.getOperation(operationId);
|
|
||||||
if (!operation) {
|
|
||||||
throw new Error(`Operation ${operationId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
operation.status = 'completed';
|
|
||||||
await this.updateManifest(operation);
|
|
||||||
}
|
|
||||||
|
|
||||||
async cleanOldBackups(retentionDays: number): Promise<void> {
|
|
||||||
const manifest = await this.getManifest();
|
|
||||||
const cutoffTime = Date.now() - (retentionDays * 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
const operationsToDelete = manifest.operations.filter(op =>
|
|
||||||
op.timestamp < cutoffTime && op.status === 'completed'
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const operation of operationsToDelete) {
|
|
||||||
// Remove backup files
|
|
||||||
const operationDir = plugins.path.join(this.backupDir, 'operations', operation.id);
|
|
||||||
await plugins.smartfile.fs.remove(operationDir);
|
|
||||||
|
|
||||||
// Remove from manifest
|
|
||||||
manifest.operations = manifest.operations.filter(op => op.id !== operation.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.saveManifest(manifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyBackup(operationId: string): Promise<boolean> {
|
|
||||||
const operation = await this.getOperation(operationId);
|
|
||||||
if (!operation) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const file of operation.files) {
|
|
||||||
const backupPath = this.getBackupPath(operationId, file.path);
|
|
||||||
const exists = await plugins.smartfile.fs.fileExists(backupPath);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await plugins.smartfile.fs.toStringSync(backupPath);
|
|
||||||
const checksum = this.calculateChecksum(content);
|
|
||||||
|
|
||||||
if (checksum !== file.checksum) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async listBackups(): Promise<IFormatOperation[]> {
|
|
||||||
const manifest = await this.getManifest();
|
|
||||||
return manifest.operations;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ensureBackupDir(): Promise<void> {
|
|
||||||
await plugins.smartfile.fs.ensureDir(this.backupDir);
|
|
||||||
await plugins.smartfile.fs.ensureDir(plugins.path.join(this.backupDir, 'operations'));
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateOperationId(): string {
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
||||||
const random = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `${timestamp}-${random}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getBackupPath(operationId: string, filepath: string): string {
|
|
||||||
const filename = plugins.path.basename(filepath);
|
|
||||||
const dir = plugins.path.dirname(filepath);
|
|
||||||
const safeDir = dir.replace(/[/\\]/g, '__');
|
|
||||||
return plugins.path.join(this.backupDir, 'operations', operationId, 'files', safeDir, `${filename}.backup`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private calculateChecksum(content: string | Buffer): string {
|
|
||||||
return plugins.crypto.createHash('sha256').update(content).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getManifest(): Promise<{ operations: IFormatOperation[] }> {
|
|
||||||
const exists = await plugins.smartfile.fs.fileExists(this.manifestPath);
|
|
||||||
if (!exists) {
|
|
||||||
return { operations: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await plugins.smartfile.fs.toStringSync(this.manifestPath);
|
|
||||||
return JSON.parse(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async saveManifest(manifest: { operations: IFormatOperation[] }): Promise<void> {
|
|
||||||
await plugins.smartfile.memory.toFs(JSON.stringify(manifest, null, 2), this.manifestPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getOperation(operationId: string): Promise<IFormatOperation | null> {
|
|
||||||
const manifest = await this.getManifest();
|
|
||||||
return manifest.operations.find(op => op.id === operationId) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateManifest(operation: IFormatOperation): Promise<void> {
|
|
||||||
const manifest = await this.getManifest();
|
|
||||||
const existingIndex = manifest.operations.findIndex(op => op.id === operation.id);
|
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
|
||||||
manifest.operations[existingIndex] = operation;
|
|
||||||
} else {
|
|
||||||
manifest.operations.push(operation);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.saveManifest(manifest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
|
||||||
import { Project } from '../classes.project.js';
|
|
||||||
|
|
||||||
const filesToDelete = ['defaults.yml', 'yarn.lock', 'package-lock.json', 'tslint.json'];
|
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
for (const relativeFilePath of filesToDelete) {
|
|
||||||
const fileExists = plugins.smartfile.fs.fileExistsSync(relativeFilePath);
|
|
||||||
if (fileExists) {
|
|
||||||
logger.log('info', `Found ${relativeFilePath}! Removing it!`);
|
|
||||||
plugins.smartfile.fs.removeSync(plugins.path.join(paths.cwd, relativeFilePath));
|
|
||||||
} else {
|
|
||||||
logger.log('info', `Project is free of ${relativeFilePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import type { Project } from '../classes.project.js';
|
|
||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
const gitzoneConfig = await projectArg.gitzoneConfig;
|
|
||||||
|
|
||||||
// Get copy configuration from npmextra.json
|
|
||||||
const npmextraConfig = new plugins.npmextra.Npmextra();
|
|
||||||
const copyConfig = npmextraConfig.dataFor<any>('gitzone.format.copy', {
|
|
||||||
patterns: []
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!copyConfig.patterns || copyConfig.patterns.length === 0) {
|
|
||||||
logger.log('info', 'No copy patterns configured in npmextra.json');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const pattern of copyConfig.patterns) {
|
|
||||||
if (!pattern.from || !pattern.to) {
|
|
||||||
logger.log('warn', 'Invalid copy pattern - missing "from" or "to" field');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Handle glob patterns
|
|
||||||
const files = await plugins.smartfile.fs.listFileTree('.', pattern.from);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const sourcePath = file;
|
|
||||||
let destPath = pattern.to;
|
|
||||||
|
|
||||||
// If destination is a directory, preserve filename
|
|
||||||
if (pattern.to.endsWith('/')) {
|
|
||||||
const filename = plugins.path.basename(file);
|
|
||||||
destPath = plugins.path.join(pattern.to, filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle template variables in destination path
|
|
||||||
if (pattern.preservePath) {
|
|
||||||
const relativePath = plugins.path.relative(
|
|
||||||
plugins.path.dirname(pattern.from.replace(/\*/g, '')),
|
|
||||||
file
|
|
||||||
);
|
|
||||||
destPath = plugins.path.join(pattern.to, relativePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure destination directory exists
|
|
||||||
await plugins.smartfile.fs.ensureDir(plugins.path.dirname(destPath));
|
|
||||||
|
|
||||||
// Copy file
|
|
||||||
await plugins.smartfile.fs.copy(sourcePath, destPath);
|
|
||||||
logger.log('info', `Copied ${sourcePath} to ${destPath}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.log('error', `Failed to copy pattern ${pattern.from}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example npmextra.json configuration:
|
|
||||||
* {
|
|
||||||
* "gitzone": {
|
|
||||||
* "format": {
|
|
||||||
* "copy": {
|
|
||||||
* "patterns": [
|
|
||||||
* {
|
|
||||||
* "from": "src/assets/*",
|
|
||||||
* "to": "dist/assets/",
|
|
||||||
* "preservePath": true
|
|
||||||
* },
|
|
||||||
* {
|
|
||||||
* "from": "config/*.json",
|
|
||||||
* "to": "dist/"
|
|
||||||
* }
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
|
|
||||||
import { Project } from '../classes.project.js';
|
|
||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
|
||||||
const gitignorePath = plugins.path.join(paths.cwd, './.gitignore');
|
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
const gitignoreExists = await plugins.smartfile.fs.fileExists(gitignorePath);
|
|
||||||
const templateModule = await import('../mod_template/index.js');
|
|
||||||
const ciTemplate = await templateModule.getTemplate('gitignore');
|
|
||||||
if (gitignoreExists) {
|
|
||||||
// lets get the existing gitignore file
|
|
||||||
const existingGitIgnoreString = plugins.smartfile.fs.toStringSync(gitignorePath);
|
|
||||||
let customPart = existingGitIgnoreString.split('# custom\n')[1];
|
|
||||||
customPart ? null : (customPart = '');
|
|
||||||
}
|
|
||||||
ciTemplate.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', 'Added a .gitignore!');
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
import { Project } from '../classes.project.js';
|
|
||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
|
||||||
|
|
||||||
const incompatibleLicenses: string[] = ['AGPL', 'GPL', 'SSPL'];
|
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
const nodeModulesInstalled = await plugins.smartfile.fs.isDirectory(
|
|
||||||
plugins.path.join(paths.cwd, 'node_modules'),
|
|
||||||
);
|
|
||||||
if (!nodeModulesInstalled) {
|
|
||||||
logger.log('warn', 'No node_modules found. Skipping license check');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
|
|
||||||
const licenseCheckResult = await licenseChecker.excludeLicenseWithinPath(
|
|
||||||
paths.cwd,
|
|
||||||
incompatibleLicenses,
|
|
||||||
);
|
|
||||||
if (licenseCheckResult.failingModules.length === 0) {
|
|
||||||
logger.log('info', 'Success -> licenses passed!');
|
|
||||||
} else {
|
|
||||||
logger.log('error', 'Error -> licenses failed. Here is why:');
|
|
||||||
for (const failedModule of licenseCheckResult.failingModules) {
|
|
||||||
console.log(`${failedModule.name} fails with license ${failedModule.license}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
import * as gulpFunction from '@push.rocks/gulp-function';
|
|
||||||
import { Project } from '../classes.project.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* runs the npmextra file checking
|
|
||||||
*/
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
const formatSmartstream = new plugins.smartstream.StreamWrapper([
|
|
||||||
plugins.smartgulp.src([`npmextra.json`]),
|
|
||||||
gulpFunction.forEach(async (fileArg: plugins.smartfile.SmartFile) => {
|
|
||||||
const fileString = fileArg.contents.toString();
|
|
||||||
const npmextraJson = JSON.parse(fileString);
|
|
||||||
|
|
||||||
if (!npmextraJson.gitzone) {
|
|
||||||
npmextraJson.gitzone = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const expectedRepoInformation: string[] = [
|
|
||||||
'projectType',
|
|
||||||
'module.githost',
|
|
||||||
'module.gitscope',
|
|
||||||
'module.gitrepo',
|
|
||||||
'module.description',
|
|
||||||
'module.npmPackagename',
|
|
||||||
'module.license',
|
|
||||||
];
|
|
||||||
|
|
||||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
|
||||||
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
|
||||||
if (!plugins.smartobject.smartGet(npmextraJson.gitzone, expectedRepoInformationItem)) {
|
|
||||||
interactInstance.addQuestions([
|
|
||||||
{
|
|
||||||
message: `What is the value of ${expectedRepoInformationItem}`,
|
|
||||||
name: expectedRepoInformationItem,
|
|
||||||
type: 'input',
|
|
||||||
default: 'undefined variable',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const answerbucket = await interactInstance.runQueue();
|
|
||||||
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
|
||||||
const cliProvidedValue = answerbucket.getAnswerFor(expectedRepoInformationItem);
|
|
||||||
if (cliProvidedValue) {
|
|
||||||
plugins.smartobject.smartAdd(
|
|
||||||
npmextraJson.gitzone,
|
|
||||||
expectedRepoInformationItem,
|
|
||||||
cliProvidedValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete obsolete
|
|
||||||
// tbd
|
|
||||||
|
|
||||||
if (!npmextraJson.npmci) {
|
|
||||||
npmextraJson.npmci = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
fileArg.setContentsFromString(JSON.stringify(npmextraJson, null, 2));
|
|
||||||
}),
|
|
||||||
plugins.smartgulp.replace(),
|
|
||||||
]);
|
|
||||||
await formatSmartstream.run().catch((error) => {
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
import * as gulpFunction from '@push.rocks/gulp-function';
|
|
||||||
import { Project } from '../classes.project.js';
|
|
||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ensures a certain dependency
|
|
||||||
*/
|
|
||||||
const ensureDependency = async (
|
|
||||||
packageJsonObjectArg: any,
|
|
||||||
position: 'dep' | 'devDep' | 'everywhere',
|
|
||||||
constraint: 'exclude' | 'include' | 'latest',
|
|
||||||
dependencyArg: string,
|
|
||||||
) => {
|
|
||||||
const [packageName, version] = dependencyArg.includes('@')
|
|
||||||
? dependencyArg.split('@').filter(Boolean)
|
|
||||||
: [dependencyArg, 'latest'];
|
|
||||||
|
|
||||||
const targetSections: string[] = [];
|
|
||||||
|
|
||||||
switch (position) {
|
|
||||||
case 'dep':
|
|
||||||
targetSections.push('dependencies');
|
|
||||||
break;
|
|
||||||
case 'devDep':
|
|
||||||
targetSections.push('devDependencies');
|
|
||||||
break;
|
|
||||||
case 'everywhere':
|
|
||||||
targetSections.push('dependencies', 'devDependencies');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const section of targetSections) {
|
|
||||||
if (!packageJsonObjectArg[section]) {
|
|
||||||
packageJsonObjectArg[section] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (constraint) {
|
|
||||||
case 'exclude':
|
|
||||||
delete packageJsonObjectArg[section][packageName];
|
|
||||||
break;
|
|
||||||
case 'include':
|
|
||||||
if (!packageJsonObjectArg[section][packageName]) {
|
|
||||||
packageJsonObjectArg[section][packageName] = version === 'latest' ? '^1.0.0' : version;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'latest':
|
|
||||||
// Fetch latest version from npm
|
|
||||||
try {
|
|
||||||
const registry = new plugins.smartnpm.NpmRegistry();
|
|
||||||
const packageInfo = await registry.getPackageInfo(packageName);
|
|
||||||
const latestVersion = packageInfo['dist-tags'].latest;
|
|
||||||
packageJsonObjectArg[section][packageName] = `^${latestVersion}`;
|
|
||||||
} catch (error) {
|
|
||||||
logger.log('warn', `Could not fetch latest version for ${packageName}, using existing or default`);
|
|
||||||
if (!packageJsonObjectArg[section][packageName]) {
|
|
||||||
packageJsonObjectArg[section][packageName] = version === 'latest' ? '^1.0.0' : version;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
const formatStreamWrapper = new plugins.smartstream.StreamWrapper([
|
|
||||||
plugins.smartgulp.src([`package.json`]),
|
|
||||||
gulpFunction.forEach(async (fileArg: plugins.smartfile.SmartFile) => {
|
|
||||||
const npmextraConfig = new plugins.npmextra.Npmextra(paths.cwd);
|
|
||||||
const gitzoneData: any = npmextraConfig.dataFor('gitzone', {});
|
|
||||||
const fileString = fileArg.contents.toString();
|
|
||||||
const packageJson = JSON.parse(fileString);
|
|
||||||
|
|
||||||
// metadata
|
|
||||||
packageJson.repository = {
|
|
||||||
type: 'git',
|
|
||||||
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}.git`,
|
|
||||||
};
|
|
||||||
(packageJson.bugs = {
|
|
||||||
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}/issues`,
|
|
||||||
}),
|
|
||||||
(packageJson.homepage = `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}#readme`);
|
|
||||||
|
|
||||||
// Check for module type
|
|
||||||
if (!packageJson.type) {
|
|
||||||
logger.log('info', `setting packageJson.type to "module"`);
|
|
||||||
packageJson.type = 'module';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for private or public
|
|
||||||
if (packageJson.private !== undefined) {
|
|
||||||
logger.log('info', 'Success -> found private/public info in package.json!');
|
|
||||||
} else {
|
|
||||||
logger.log('error', 'found no private boolean! Setting it to private for now!');
|
|
||||||
packageJson.private = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for license
|
|
||||||
if (packageJson.license) {
|
|
||||||
logger.log('info', 'Success -> found license in package.json!');
|
|
||||||
} else {
|
|
||||||
logger.log('error', 'found no license! Setting it to UNLICENSED for now!');
|
|
||||||
packageJson.license = 'UNLICENSED';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for build script
|
|
||||||
if (packageJson.scripts.build) {
|
|
||||||
logger.log('info', 'Success -> found build script in package.json!');
|
|
||||||
} else {
|
|
||||||
logger.log('error', 'found no build script! Putting a placeholder there for now!');
|
|
||||||
packageJson.scripts.build = `echo "Not needed for now"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for buildDocs script
|
|
||||||
if (!packageJson.scripts.buildDocs) {
|
|
||||||
logger.log('info', 'found no buildDocs script! Putting tsdoc script there now.');
|
|
||||||
packageJson.scripts.buildDocs = `tsdoc`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for files
|
|
||||||
packageJson.files = [
|
|
||||||
'ts/**/*',
|
|
||||||
'ts_web/**/*',
|
|
||||||
'dist/**/*',
|
|
||||||
'dist_*/**/*',
|
|
||||||
'dist_ts/**/*',
|
|
||||||
'dist_ts_web/**/*',
|
|
||||||
'assets/**/*',
|
|
||||||
'cli.js',
|
|
||||||
'npmextra.json',
|
|
||||||
'readme.md',
|
|
||||||
];
|
|
||||||
|
|
||||||
// check for dependencies
|
|
||||||
await ensureDependency(packageJson, 'devDep', 'latest', '@push.rocks/tapbundle');
|
|
||||||
await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tstest');
|
|
||||||
await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tsbuild');
|
|
||||||
|
|
||||||
// set overrides
|
|
||||||
const overrides = plugins.smartfile.fs.toObjectSync(
|
|
||||||
plugins.path.join(paths.assetsDir, 'overrides.json'),
|
|
||||||
);
|
|
||||||
packageJson.pnpm = packageJson.pnpm || {};
|
|
||||||
packageJson.pnpm.overrides = overrides;
|
|
||||||
|
|
||||||
// exclude
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
fileArg.setContentsFromString(JSON.stringify(packageJson, null, 2));
|
|
||||||
}),
|
|
||||||
plugins.smartgulp.replace(),
|
|
||||||
]);
|
|
||||||
await formatStreamWrapper.run().catch((error) => {
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import prettier from 'prettier';
|
|
||||||
import { Project } from '../classes.project.js';
|
|
||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
|
||||||
|
|
||||||
const prettierDefaultTypeScriptConfig: prettier.Options = {
|
|
||||||
printWidth: 100,
|
|
||||||
parser: 'typescript',
|
|
||||||
singleQuote: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const prettierDefaultMarkdownConfig: prettier.Options = {
|
|
||||||
singleQuote: true,
|
|
||||||
printWidth: 100,
|
|
||||||
parser: 'markdown',
|
|
||||||
};
|
|
||||||
|
|
||||||
const filesToFormat = [`ts/**/*.ts`, `test/**/*.ts`, `readme.md`, `docs/**/*.md`];
|
|
||||||
|
|
||||||
const choosePrettierConfig = (fileArg: plugins.smartfile.SmartFile) => {
|
|
||||||
switch (fileArg.parsedPath.ext) {
|
|
||||||
case '.ts':
|
|
||||||
return prettierDefaultTypeScriptConfig;
|
|
||||||
case '.md':
|
|
||||||
return prettierDefaultMarkdownConfig;
|
|
||||||
default:
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const prettierTypeScriptPipestop = plugins.through2.obj(
|
|
||||||
async (fileArg: plugins.smartfile.SmartFile, enc, cb) => {
|
|
||||||
const fileString = fileArg.contentBuffer.toString();
|
|
||||||
const chosenConfig = choosePrettierConfig(fileArg);
|
|
||||||
const filePasses = await prettier.check(fileString, chosenConfig);
|
|
||||||
if (filePasses) {
|
|
||||||
logger.log('info', `OK! -> ${fileArg.path} passes!`);
|
|
||||||
cb(null);
|
|
||||||
} else {
|
|
||||||
logger.log('info', `${fileArg.path} is being reformated!`);
|
|
||||||
const formatedFileString = await prettier.format(fileString, chosenConfig);
|
|
||||||
fileArg.setContentsFromString(formatedFileString);
|
|
||||||
cb(null, fileArg);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
const formatStreamWrapper = new plugins.smartstream.StreamWrapper([
|
|
||||||
plugins.smartgulp.src(filesToFormat),
|
|
||||||
prettierTypeScriptPipestop,
|
|
||||||
plugins.smartgulp.replace(),
|
|
||||||
]);
|
|
||||||
await formatStreamWrapper.run().catch((error) => {
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
|
|
||||||
export const run = async () => {
|
|
||||||
const readmePath = plugins.path.join(paths.cwd, 'readme.md');
|
|
||||||
const readmeHintsPath = plugins.path.join(paths.cwd, 'readme.hints.md');
|
|
||||||
|
|
||||||
// Check and initialize readme.md if it doesn't exist
|
|
||||||
const readmeExists = await plugins.smartfile.fs.fileExists(readmePath);
|
|
||||||
if (!readmeExists) {
|
|
||||||
await plugins.smartfile.fs.toFs(
|
|
||||||
'# Project Readme\n\nThis is the initial readme file.',
|
|
||||||
readmePath,
|
|
||||||
);
|
|
||||||
console.log('Initialized readme.md');
|
|
||||||
} else {
|
|
||||||
console.log('readme.md already exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check and initialize readme.hints.md if it doesn't exist
|
|
||||||
const readmeHintsExists = await plugins.smartfile.fs.fileExists(readmeHintsPath);
|
|
||||||
if (!readmeHintsExists) {
|
|
||||||
await plugins.smartfile.fs.toFs(
|
|
||||||
'# Project Readme Hints\n\nThis is the initial readme hints file.',
|
|
||||||
readmeHintsPath,
|
|
||||||
);
|
|
||||||
console.log('Initialized readme.hints.md');
|
|
||||||
} else {
|
|
||||||
console.log('readme.hints.md already exists');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
|
||||||
import { Project } from '../classes.project.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* takes care of updating files from templates
|
|
||||||
*/
|
|
||||||
export const run = async (project: Project) => {
|
|
||||||
const templateModule = await import('../mod_template/index.js');
|
|
||||||
|
|
||||||
// update tslint
|
|
||||||
// getting template
|
|
||||||
const tslintTemplate = await templateModule.getTemplate('tslint');
|
|
||||||
await tslintTemplate.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', 'Updated tslint.json!');
|
|
||||||
|
|
||||||
// update vscode
|
|
||||||
const vscodeTemplate = await templateModule.getTemplate('vscode');
|
|
||||||
await vscodeTemplate.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', `Updated vscode template!`);
|
|
||||||
|
|
||||||
// update gitlab ci and Dockerfile
|
|
||||||
switch (project.gitzoneConfig.data.projectType) {
|
|
||||||
case 'npm':
|
|
||||||
case 'wcc':
|
|
||||||
if (project.gitzoneConfig.data.npmciOptions.npmAccessLevel === 'public') {
|
|
||||||
const ciTemplateDefault = await templateModule.getTemplate('ci_default');
|
|
||||||
ciTemplateDefault.writeToDisk(paths.cwd);
|
|
||||||
} else {
|
|
||||||
const ciTemplateDefault = await templateModule.getTemplate('ci_default_private');
|
|
||||||
ciTemplateDefault.writeToDisk(paths.cwd);
|
|
||||||
}
|
|
||||||
logger.log('info', 'Updated .gitlabci.yml!');
|
|
||||||
break;
|
|
||||||
case 'service':
|
|
||||||
case 'website':
|
|
||||||
const ciTemplateDocker = await templateModule.getTemplate('ci_docker');
|
|
||||||
await ciTemplateDocker.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', 'Updated CI/CD config files!');
|
|
||||||
|
|
||||||
// lets care about docker
|
|
||||||
const dockerTemplate = await templateModule.getTemplate('dockerfile_service');
|
|
||||||
dockerTemplate.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', 'Updated Dockerfile!');
|
|
||||||
|
|
||||||
// lets care about cli
|
|
||||||
const cliTemplate = await templateModule.getTemplate('cli');
|
|
||||||
await cliTemplate.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', 'Updated cli.ts.js and cli.js!');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update html
|
|
||||||
if (project.gitzoneConfig.data.projectType === 'website') {
|
|
||||||
const websiteUpdateTemplate = await templateModule.getTemplate('website_update');
|
|
||||||
const variables ={
|
|
||||||
assetbrokerUrl: project.gitzoneConfig.data.module.assetbrokerUrl,
|
|
||||||
legalUrl: project.gitzoneConfig.data.module.legalUrl,
|
|
||||||
};
|
|
||||||
console.log('updating website template with variables\n', JSON.stringify(variables, null, 2));
|
|
||||||
websiteUpdateTemplate.supplyVariables(variables);
|
|
||||||
await websiteUpdateTemplate.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', `Updated html for website!`);
|
|
||||||
} else if (project.gitzoneConfig.data.projectType === 'service') {
|
|
||||||
const websiteUpdateTemplate = await templateModule.getTemplate('service_update');
|
|
||||||
await websiteUpdateTemplate.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', `Updated html for element template!`);
|
|
||||||
} else if (project.gitzoneConfig.data.projectType === 'wcc') {
|
|
||||||
const wccUpdateTemplate = await templateModule.getTemplate('wcc_update');
|
|
||||||
await wccUpdateTemplate.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', `Updated html for wcc template!`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
|
||||||
import * as paths from '../paths.js';
|
|
||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
|
||||||
import { Project } from '../classes.project.js';
|
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
|
||||||
// lets care about tsconfig.json
|
|
||||||
logger.log('info', 'Formatting tsconfig.json...');
|
|
||||||
const tsconfigSmartfile = await plugins.smartfile.SmartFile.fromFilePath(
|
|
||||||
plugins.path.join(paths.cwd, 'tsconfig.json'),
|
|
||||||
);
|
|
||||||
const tsconfigObject = JSON.parse(tsconfigSmartfile.contentBuffer.toString());
|
|
||||||
tsconfigObject.compilerOptions = tsconfigObject.compilerOptions || {};
|
|
||||||
tsconfigObject.compilerOptions.baseUrl = '.';
|
|
||||||
tsconfigObject.compilerOptions.paths = {};
|
|
||||||
const tsPublishMod = await import('@git.zone/tspublish');
|
|
||||||
const tsPublishInstance = new tsPublishMod.TsPublish();
|
|
||||||
const publishModules = await tsPublishInstance.getModuleSubDirs(paths.cwd);
|
|
||||||
for (const publishModule of Object.keys(publishModules)) {
|
|
||||||
const publishConfig = publishModules[publishModule];
|
|
||||||
tsconfigObject.compilerOptions.paths[`${publishConfig.name}`] = [`./${publishModule}/index.js`];
|
|
||||||
}
|
|
||||||
tsconfigSmartfile.setContentsFromString(JSON.stringify(tsconfigObject, null, 2));
|
|
||||||
await tsconfigSmartfile.write();
|
|
||||||
};
|
|
||||||
@@ -1,34 +1,38 @@
|
|||||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import type { IPlannedChange } from '../interfaces.format.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
import * as plugins from '../mod.plugins.js';
|
import * as plugins from '../mod.plugins.js';
|
||||||
import * as cleanupFormatter from '../format.cleanup.js';
|
|
||||||
|
|
||||||
export class CleanupFormatter extends BaseFormatter {
|
export class CleanupFormatter extends BaseFormatter {
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return 'cleanup';
|
return 'cleanup';
|
||||||
}
|
}
|
||||||
|
|
||||||
async analyze(): Promise<IPlannedChange[]> {
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
const changes: IPlannedChange[] = [];
|
const changes: IPlannedChange[] = [];
|
||||||
|
|
||||||
// List of files to remove
|
// List of files to remove
|
||||||
const filesToRemove = ['yarn.lock', 'package-lock.json', 'tslint.json', 'defaults.yml'];
|
const filesToRemove = [
|
||||||
|
'yarn.lock',
|
||||||
|
'package-lock.json',
|
||||||
|
'tslint.json',
|
||||||
|
'defaults.yml',
|
||||||
|
];
|
||||||
|
|
||||||
for (const file of filesToRemove) {
|
for (const file of filesToRemove) {
|
||||||
const exists = await plugins.smartfile.fs.fileExists(file);
|
const exists = await plugins.smartfs.file(file).exists();
|
||||||
if (exists) {
|
if (exists) {
|
||||||
changes.push({
|
changes.push({
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
path: file,
|
path: file,
|
||||||
module: this.name,
|
module: this.name,
|
||||||
description: `Remove obsolete file`
|
description: `Remove obsolete file`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyChange(change: IPlannedChange): Promise<void> {
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
switch (change.type) {
|
switch (change.type) {
|
||||||
case 'delete':
|
case 'delete':
|
||||||
@@ -36,4 +40,4 @@ export class CleanupFormatter extends BaseFormatter {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,113 @@
|
|||||||
import { LegacyFormatter } from './legacy.formatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import * as formatCopy from '../format.copy.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
export class CopyFormatter extends LegacyFormatter {
|
interface ICopyPattern {
|
||||||
constructor(context: any, project: any) {
|
from: string;
|
||||||
super(context, project, 'copy', formatCopy);
|
to: string;
|
||||||
|
preservePath?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CopyFormatter extends BaseFormatter {
|
||||||
|
get name(): string {
|
||||||
|
return 'copy';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
|
const changes: IPlannedChange[] = [];
|
||||||
|
|
||||||
|
// Get copy configuration from .smartconfig.json
|
||||||
|
const smartconfigInstance = new plugins.smartconfig.Smartconfig();
|
||||||
|
const copyConfig = smartconfigInstance.dataFor<{ patterns: ICopyPattern[] }>(
|
||||||
|
'gitzone.format.copy',
|
||||||
|
{ patterns: [] },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!copyConfig.patterns || copyConfig.patterns.length === 0) {
|
||||||
|
logVerbose('No copy patterns configured in .smartconfig.json');
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pattern of copyConfig.patterns) {
|
||||||
|
if (!pattern.from || !pattern.to) {
|
||||||
|
logVerbose('Invalid copy pattern - missing "from" or "to" field');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Handle glob patterns
|
||||||
|
const entries = await plugins.smartfs
|
||||||
|
.directory('.')
|
||||||
|
.recursive()
|
||||||
|
.filter(pattern.from)
|
||||||
|
.list();
|
||||||
|
const files = entries.map((entry) => entry.path);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const sourcePath = file;
|
||||||
|
let destPath = pattern.to;
|
||||||
|
|
||||||
|
// If destination is a directory, preserve filename
|
||||||
|
if (pattern.to.endsWith('/')) {
|
||||||
|
const filename = plugins.path.basename(file);
|
||||||
|
destPath = plugins.path.join(pattern.to, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle template variables in destination path
|
||||||
|
if (pattern.preservePath) {
|
||||||
|
const relativePath = plugins.path.relative(
|
||||||
|
plugins.path.dirname(pattern.from.replace(/\*/g, '')),
|
||||||
|
file,
|
||||||
|
);
|
||||||
|
destPath = plugins.path.join(pattern.to, relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read source content
|
||||||
|
const content = (await plugins.smartfs
|
||||||
|
.file(sourcePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
|
// Check if destination exists and has same content
|
||||||
|
let needsCopy = true;
|
||||||
|
const destExists = await plugins.smartfs.file(destPath).exists();
|
||||||
|
if (destExists) {
|
||||||
|
const existingContent = (await plugins.smartfs
|
||||||
|
.file(destPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
if (existingContent === content) {
|
||||||
|
needsCopy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsCopy) {
|
||||||
|
changes.push({
|
||||||
|
type: destExists ? 'modify' : 'create',
|
||||||
|
path: destPath,
|
||||||
|
module: this.name,
|
||||||
|
description: `Copy from ${sourcePath}`,
|
||||||
|
content: content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logVerbose(`Failed to process pattern ${pattern.from}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
|
if (!change.content) return;
|
||||||
|
|
||||||
|
if (change.type === 'create') {
|
||||||
|
await this.createFile(change.path, change.content);
|
||||||
|
} else {
|
||||||
|
await this.modifyFile(change.path, change.content);
|
||||||
|
}
|
||||||
|
logger.log('info', `Copied to ${change.path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,107 @@
|
|||||||
import { LegacyFormatter } from './legacy.formatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import * as formatGitignore from '../format.gitignore.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import * as paths from '../../paths.js';
|
||||||
|
import { logger } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
export class GitignoreFormatter extends LegacyFormatter {
|
export class GitignoreFormatter extends BaseFormatter {
|
||||||
constructor(context: any, project: any) {
|
get name(): string {
|
||||||
super(context, project, 'gitignore', formatGitignore);
|
return 'gitignore';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Read the standard gitignore template from the asset file,
|
||||||
|
* stripping the YAML frontmatter.
|
||||||
|
*/
|
||||||
|
private async getStandardTemplate(): Promise<string> {
|
||||||
|
const templatePath = plugins.path.join(paths.templatesDir, 'gitignore', '_gitignore');
|
||||||
|
const raw = (await plugins.smartfs
|
||||||
|
.file(templatePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
|
// Strip YAML frontmatter (---\n...\n---)
|
||||||
|
const frontmatterEnd = raw.indexOf('---', 3);
|
||||||
|
if (frontmatterEnd !== -1) {
|
||||||
|
return raw.slice(frontmatterEnd + 3).trimStart();
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
|
const changes: IPlannedChange[] = [];
|
||||||
|
const gitignorePath = '.gitignore';
|
||||||
|
|
||||||
|
const standardTemplate = await this.getStandardTemplate();
|
||||||
|
|
||||||
|
// Check if file exists and extract custom content
|
||||||
|
let customContent = '';
|
||||||
|
const exists = await plugins.smartfs.file(gitignorePath).exists();
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
const existingContent = (await plugins.smartfs
|
||||||
|
.file(gitignorePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
|
// Extract custom section content
|
||||||
|
const customMarkers = ['#------# custom', '# custom'];
|
||||||
|
for (const marker of customMarkers) {
|
||||||
|
const splitResult = existingContent.split(marker);
|
||||||
|
if (splitResult.length > 1) {
|
||||||
|
customContent = splitResult[1].trim();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute new content
|
||||||
|
let newContent = standardTemplate;
|
||||||
|
if (customContent) {
|
||||||
|
newContent = standardTemplate + '\n' + customContent + '\n';
|
||||||
|
} else {
|
||||||
|
newContent = standardTemplate + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read current content to compare
|
||||||
|
let currentContent = '';
|
||||||
|
if (exists) {
|
||||||
|
currentContent = (await plugins.smartfs
|
||||||
|
.file(gitignorePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
changes.push({
|
||||||
|
type: 'create',
|
||||||
|
path: gitignorePath,
|
||||||
|
module: this.name,
|
||||||
|
description: 'Create .gitignore',
|
||||||
|
content: newContent,
|
||||||
|
});
|
||||||
|
} else if (newContent !== currentContent) {
|
||||||
|
changes.push({
|
||||||
|
type: 'modify',
|
||||||
|
path: gitignorePath,
|
||||||
|
module: this.name,
|
||||||
|
description: 'Update .gitignore (preserving custom section)',
|
||||||
|
content: newContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
|
if (!change.content) return;
|
||||||
|
|
||||||
|
if (change.type === 'create') {
|
||||||
|
await this.createFile(change.path, change.content);
|
||||||
|
logger.log('info', 'Created .gitignore');
|
||||||
|
} else if (change.type === 'modify') {
|
||||||
|
await this.modifyFile(change.path, change.content);
|
||||||
|
logger.log('info', 'Updated .gitignore (preserved custom section)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
|
||||||
import type { IPlannedChange } from '../interfaces.format.js';
|
|
||||||
import { Project } from '../../classes.project.js';
|
|
||||||
import * as plugins from '../mod.plugins.js';
|
|
||||||
|
|
||||||
// This is a wrapper for existing format modules
|
|
||||||
export class LegacyFormatter extends BaseFormatter {
|
|
||||||
private moduleName: string;
|
|
||||||
private formatModule: any;
|
|
||||||
|
|
||||||
constructor(context: any, project: Project, moduleName: string, formatModule: any) {
|
|
||||||
super(context, project);
|
|
||||||
this.moduleName = moduleName;
|
|
||||||
this.formatModule = formatModule;
|
|
||||||
}
|
|
||||||
|
|
||||||
get name(): string {
|
|
||||||
return this.moduleName;
|
|
||||||
}
|
|
||||||
|
|
||||||
async analyze(): Promise<IPlannedChange[]> {
|
|
||||||
// For legacy modules, we can't easily predict changes
|
|
||||||
// So we'll return a generic change that indicates the module will run
|
|
||||||
return [{
|
|
||||||
type: 'modify',
|
|
||||||
path: '<various files>',
|
|
||||||
module: this.name,
|
|
||||||
description: `Run ${this.name} formatter`
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
async applyChange(change: IPlannedChange): Promise<void> {
|
|
||||||
// Run the legacy format module
|
|
||||||
await this.formatModule.run(this.project);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,62 @@
|
|||||||
import { LegacyFormatter } from './legacy.formatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import * as formatLicense from '../format.license.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import * as paths from '../../paths.js';
|
||||||
|
import { logger } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
export class LicenseFormatter extends LegacyFormatter {
|
const INCOMPATIBLE_LICENSES: string[] = ['AGPL', 'GPL', 'SSPL'];
|
||||||
constructor(context: any, project: any) {
|
|
||||||
super(context, project, 'license', formatLicense);
|
export class LicenseFormatter extends BaseFormatter {
|
||||||
|
get name(): string {
|
||||||
|
return 'license';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
|
// License formatter only checks for incompatible licenses
|
||||||
|
// It does not modify any files, so return empty array
|
||||||
|
// The actual check happens in execute() for reporting purposes
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(changes: IPlannedChange[]): Promise<void> {
|
||||||
|
const startTime = this.stats.moduleStartTime(this.name);
|
||||||
|
this.stats.startModule(this.name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if node_modules exists
|
||||||
|
const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
|
||||||
|
const nodeModulesExists = await plugins.smartfs
|
||||||
|
.directory(nodeModulesPath)
|
||||||
|
.exists();
|
||||||
|
|
||||||
|
if (!nodeModulesExists) {
|
||||||
|
logger.log('warn', 'No node_modules found. Skipping license check');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run license check
|
||||||
|
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
|
||||||
|
const licenseCheckResult = await licenseChecker.excludeLicenseWithinPath(
|
||||||
|
paths.cwd,
|
||||||
|
INCOMPATIBLE_LICENSES,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (licenseCheckResult.failingModules.length === 0) {
|
||||||
|
logger.log('info', 'License check passed - no incompatible licenses found');
|
||||||
|
} else {
|
||||||
|
logger.log('error', 'License check failed - incompatible licenses found:');
|
||||||
|
for (const failedModule of licenseCheckResult.failingModules) {
|
||||||
|
console.log(
|
||||||
|
` ${failedModule.name} has license ${failedModule.license}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.stats.endModule(this.name, startTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
|
// No file changes for license formatter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import { LegacyFormatter } from './legacy.formatter.js';
|
|
||||||
import * as formatNpmextra from '../format.npmextra.js';
|
|
||||||
|
|
||||||
export class NpmextraFormatter extends LegacyFormatter {
|
|
||||||
constructor(context: any, project: any) {
|
|
||||||
super(context, project, 'npmextra', formatNpmextra);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,122 @@
|
|||||||
import { LegacyFormatter } from './legacy.formatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import * as formatPackageJson from '../format.packagejson.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import * as paths from '../../paths.js';
|
||||||
|
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
export class PackageJsonFormatter extends LegacyFormatter {
|
export class PackageJsonFormatter extends BaseFormatter {
|
||||||
constructor(context: any, project: any) {
|
get name(): string {
|
||||||
super(context, project, 'packagejson', formatPackageJson);
|
return 'packagejson';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
|
const changes: IPlannedChange[] = [];
|
||||||
|
const packageJsonPath = 'package.json';
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
const exists = await plugins.smartfs.file(packageJsonPath).exists();
|
||||||
|
if (!exists) {
|
||||||
|
logVerbose('package.json does not exist, skipping');
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read current content
|
||||||
|
const currentContent = (await plugins.smartfs
|
||||||
|
.file(packageJsonPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
|
// Parse and compute new content
|
||||||
|
const packageJson = JSON.parse(currentContent);
|
||||||
|
|
||||||
|
// Get gitzone config from smartconfig
|
||||||
|
const smartconfigInstance = new plugins.smartconfig.Smartconfig(paths.cwd);
|
||||||
|
const gitzoneData: any = smartconfigInstance.dataFor('@git.zone/cli', {});
|
||||||
|
|
||||||
|
// Set metadata from gitzone config
|
||||||
|
if (gitzoneData.module) {
|
||||||
|
packageJson.repository = {
|
||||||
|
type: 'git',
|
||||||
|
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}.git`,
|
||||||
|
};
|
||||||
|
packageJson.bugs = {
|
||||||
|
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}/issues`,
|
||||||
|
};
|
||||||
|
packageJson.homepage = `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}#readme`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure module type
|
||||||
|
if (!packageJson.type) {
|
||||||
|
packageJson.type = 'module';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure private field exists
|
||||||
|
if (packageJson.private === undefined) {
|
||||||
|
packageJson.private = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure license field exists
|
||||||
|
if (!packageJson.license) {
|
||||||
|
packageJson.license = 'UNLICENSED';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure scripts object exists
|
||||||
|
if (!packageJson.scripts) {
|
||||||
|
packageJson.scripts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure build script exists
|
||||||
|
if (!packageJson.scripts.build) {
|
||||||
|
packageJson.scripts.build = `echo "Not needed for now"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set files array
|
||||||
|
packageJson.files = [
|
||||||
|
'ts/**/*',
|
||||||
|
'ts_web/**/*',
|
||||||
|
'dist/**/*',
|
||||||
|
'dist_*/**/*',
|
||||||
|
'dist_ts/**/*',
|
||||||
|
'dist_ts_web/**/*',
|
||||||
|
'assets/**/*',
|
||||||
|
'cli.js',
|
||||||
|
'.smartconfig.json',
|
||||||
|
'readme.md',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Set pnpm overrides from assets
|
||||||
|
try {
|
||||||
|
const overridesContent = (await plugins.smartfs
|
||||||
|
.file(plugins.path.join(paths.assetsDir, 'overrides.json'))
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
const overrides = JSON.parse(overridesContent);
|
||||||
|
packageJson.pnpm = packageJson.pnpm || {};
|
||||||
|
packageJson.pnpm.overrides = overrides;
|
||||||
|
} catch (error) {
|
||||||
|
logVerbose(`Could not read overrides.json: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContent = JSON.stringify(packageJson, null, 2);
|
||||||
|
|
||||||
|
// Only add change if content differs
|
||||||
|
if (newContent !== currentContent) {
|
||||||
|
changes.push({
|
||||||
|
type: 'modify',
|
||||||
|
path: packageJsonPath,
|
||||||
|
module: this.name,
|
||||||
|
description: 'Format package.json',
|
||||||
|
content: newContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
|
if (change.type !== 'modify' || !change.content) return;
|
||||||
|
|
||||||
|
await this.modifyFile(change.path, change.content);
|
||||||
|
logger.log('info', 'Updated package.json');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 { IPlannedChange, ICheckResult } 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';
|
||||||
|
|
||||||
@@ -7,111 +7,222 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
get name(): string {
|
get name(): string {
|
||||||
return 'prettier';
|
return 'prettier';
|
||||||
}
|
}
|
||||||
|
|
||||||
async analyze(): Promise<IPlannedChange[]> {
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
const changes: IPlannedChange[] = [];
|
const changes: IPlannedChange[] = [];
|
||||||
const globPattern = '**/*.{ts,tsx,js,jsx,json,md,css,scss,html,xml,yaml,yml}';
|
|
||||||
|
// Define directories to format (TypeScript directories by default)
|
||||||
// Get all files that match the pattern
|
const includeDirs = ['ts', 'ts_*', 'test', 'tests'];
|
||||||
const files = await plugins.smartfile.fs.listFileTree('.', globPattern);
|
|
||||||
|
// File extensions to format
|
||||||
// Check which files need formatting
|
const extensions = '{ts,tsx,js,jsx,json,md,css,scss,html,xml,yaml,yml}';
|
||||||
for (const file of files) {
|
|
||||||
// Skip files that haven't changed
|
// Also format root-level config files
|
||||||
if (!await this.shouldProcessFile(file)) {
|
const rootConfigFiles = [
|
||||||
logVerbose(`Skipping ${file} - no changes detected`);
|
'package.json',
|
||||||
continue;
|
'tsconfig.json',
|
||||||
|
'.smartconfig.json',
|
||||||
|
'.prettierrc',
|
||||||
|
'.prettierrc.json',
|
||||||
|
'.prettierrc.js',
|
||||||
|
'readme.md',
|
||||||
|
'README.md',
|
||||||
|
'changelog.md',
|
||||||
|
'CHANGELOG.md',
|
||||||
|
// Skip files without extensions as prettier can't infer parser
|
||||||
|
// 'license',
|
||||||
|
// 'LICENSE',
|
||||||
|
'*.md',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Collect all files to format
|
||||||
|
const allFiles: string[] = [];
|
||||||
|
|
||||||
|
// Add files from TypeScript directories
|
||||||
|
for (const dir of includeDirs) {
|
||||||
|
try {
|
||||||
|
const globPattern = `${dir}/**/*.${extensions}`;
|
||||||
|
const dirEntries = await plugins.smartfs
|
||||||
|
.directory('.')
|
||||||
|
.recursive()
|
||||||
|
.filter(globPattern)
|
||||||
|
.list();
|
||||||
|
const dirFiles = dirEntries.map((entry) => entry.path);
|
||||||
|
// Filter out files in excluded directories
|
||||||
|
const filteredFiles = dirFiles.filter((f) =>
|
||||||
|
!f.includes('node_modules/') &&
|
||||||
|
!f.includes('.nogit/') &&
|
||||||
|
!f.includes('.git/')
|
||||||
|
);
|
||||||
|
allFiles.push(...filteredFiles);
|
||||||
|
} catch (error) {
|
||||||
|
logVerbose(`Skipping directory ${dir}: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add root config files (only check root level, no recursive needed)
|
||||||
|
for (const pattern of rootConfigFiles) {
|
||||||
|
try {
|
||||||
|
const rootEntries = await plugins.smartfs
|
||||||
|
.directory('.')
|
||||||
|
.filter(pattern)
|
||||||
|
.list();
|
||||||
|
const rootFiles = rootEntries.map((entry) => entry.path);
|
||||||
|
// Only include files at root level (no slashes in path)
|
||||||
|
const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
|
||||||
|
allFiles.push(...rootLevelFiles);
|
||||||
|
} catch (error) {
|
||||||
|
logVerbose(`Skipping pattern ${pattern}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicates
|
||||||
|
const uniqueFiles = [...new Set(allFiles)];
|
||||||
|
|
||||||
|
// Ensure we only process actual files (not directories)
|
||||||
|
const validFiles: string[] = [];
|
||||||
|
for (const file of uniqueFiles) {
|
||||||
|
try {
|
||||||
|
const stats = await plugins.smartfs.file(file).stat();
|
||||||
|
if (!stats.isDirectory) {
|
||||||
|
validFiles.push(file);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Skip files that can't be accessed
|
||||||
|
logVerbose(`Skipping ${file} - cannot access: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of validFiles) {
|
||||||
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',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('info', `Found ${changes.length} files to format with Prettier`);
|
logger.log('info', `Found ${changes.length} files to format with Prettier`);
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
await this.preExecute();
|
await this.preExecute();
|
||||||
|
|
||||||
// Batch process files
|
logVerbose(`Processing ${changes.length} files sequentially`);
|
||||||
const batchSize = 10; // Process 10 files at a time
|
|
||||||
const batches: IPlannedChange[][] = [];
|
// Process files sequentially to avoid prettier cache/state issues
|
||||||
|
for (let i = 0; i < changes.length; i++) {
|
||||||
for (let i = 0; i < changes.length; i += batchSize) {
|
const change = changes[i];
|
||||||
batches.push(changes.slice(i, i + batchSize));
|
logVerbose(
|
||||||
|
`Processing file ${i + 1}/${changes.length}: ${change.path}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.applyChange(change);
|
||||||
|
this.stats.recordFileOperation(this.name, change.type, true);
|
||||||
|
} catch (error) {
|
||||||
|
this.stats.recordFileOperation(this.name, change.type, false);
|
||||||
|
logger.log(
|
||||||
|
'error',
|
||||||
|
`Failed to format ${change.path}: ${error.message}`,
|
||||||
|
);
|
||||||
|
// Don't throw - continue with other files
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logVerbose(`Processing ${changes.length} files in ${batches.length} batches`);
|
|
||||||
|
|
||||||
for (let i = 0; i < batches.length; i++) {
|
|
||||||
const batch = batches[i];
|
|
||||||
logVerbose(`Processing batch ${i + 1}/${batches.length} (${batch.length} files)`);
|
|
||||||
|
|
||||||
// Process batch in parallel
|
|
||||||
const promises = batch.map(async (change) => {
|
|
||||||
try {
|
|
||||||
await this.applyChange(change);
|
|
||||||
this.stats.recordFileOperation(this.name, change.type, true);
|
|
||||||
} catch (error) {
|
|
||||||
this.stats.recordFileOperation(this.name, change.type, false);
|
|
||||||
logger.log('error', `Failed to format ${change.path}: ${error.message}`);
|
|
||||||
// Don't throw - continue with other files
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.postExecute();
|
await this.postExecute();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await this.context.rollbackOperation();
|
// Rollback removed - no longer tracking operations
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
this.stats.endModule(this.name, startTime);
|
this.stats.endModule(this.name, startTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyChange(change: IPlannedChange): Promise<void> {
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
if (change.type !== 'modify') return;
|
if (change.type !== 'modify') return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Validate the path before processing
|
||||||
|
if (!change.path || change.path.trim() === '') {
|
||||||
|
logger.log(
|
||||||
|
'error',
|
||||||
|
`Invalid empty path in change: ${JSON.stringify(change)}`,
|
||||||
|
);
|
||||||
|
throw new Error('Invalid empty path');
|
||||||
|
}
|
||||||
|
|
||||||
// Read current content
|
// Read current content
|
||||||
const content = await plugins.smartfile.fs.toStringSync(change.path);
|
const content = (await plugins.smartfs
|
||||||
|
.file(change.path)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
// Format with prettier
|
// Format with prettier
|
||||||
const prettier = await import('prettier');
|
const prettier = await import('prettier');
|
||||||
const formatted = await prettier.format(content, {
|
|
||||||
filepath: change.path,
|
// Skip files that prettier can't parse without explicit parser
|
||||||
...(await this.getPrettierConfig())
|
const fileExt = plugins.path.extname(change.path).toLowerCase();
|
||||||
});
|
if (!fileExt || fileExt === '') {
|
||||||
|
// Files without extensions need explicit parser
|
||||||
// Only write if content actually changed
|
logVerbose(
|
||||||
if (formatted !== content) {
|
`Skipping ${change.path} - no file extension for parser inference`,
|
||||||
await this.modifyFile(change.path, formatted);
|
);
|
||||||
logVerbose(`Formatted ${change.path}`);
|
return;
|
||||||
} else {
|
}
|
||||||
// Still update cache even if content didn't change
|
|
||||||
await this.cache.updateFileCache(change.path);
|
try {
|
||||||
logVerbose(`No formatting changes for ${change.path}`);
|
const formatted = await prettier.format(content, {
|
||||||
|
filepath: change.path,
|
||||||
|
...(await this.getPrettierConfig()),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only write if content actually changed
|
||||||
|
if (formatted !== content) {
|
||||||
|
// Debug: log the path being written
|
||||||
|
logVerbose(`Writing formatted content to: ${change.path}`);
|
||||||
|
await this.modifyFile(change.path, formatted);
|
||||||
|
logVerbose(`Formatted ${change.path}`);
|
||||||
|
} else {
|
||||||
|
logVerbose(`No formatting changes for ${change.path}`);
|
||||||
|
}
|
||||||
|
} catch (prettierError) {
|
||||||
|
// Check if it's a parser error
|
||||||
|
if (
|
||||||
|
prettierError.message &&
|
||||||
|
prettierError.message.includes('No parser could be inferred')
|
||||||
|
) {
|
||||||
|
logVerbose(`Skipping ${change.path} - ${prettierError.message}`);
|
||||||
|
return; // Skip this file silently
|
||||||
|
}
|
||||||
|
throw prettierError;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log('error', `Failed to format ${change.path}: ${error.message}`);
|
// Log the full error stack for debugging mkdir issues
|
||||||
|
if (error.message && error.message.includes('mkdir')) {
|
||||||
|
logger.log(
|
||||||
|
'error',
|
||||||
|
`Failed to format ${change.path}: ${error.message}`,
|
||||||
|
);
|
||||||
|
logger.log('error', `Error stack: ${error.stack}`);
|
||||||
|
} else {
|
||||||
|
logger.log(
|
||||||
|
'error',
|
||||||
|
`Failed to format ${change.path}: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getPrettierConfig(): Promise<any> {
|
private async getPrettierConfig(): Promise<any> {
|
||||||
// Try to load prettier config from the project
|
// Try to load prettier config from the project
|
||||||
const prettierConfig = new plugins.npmextra.Npmextra();
|
const prettierConfig = new plugins.smartconfig.Smartconfig();
|
||||||
return prettierConfig.dataFor('prettier', {
|
return prettierConfig.dataFor('prettier', {
|
||||||
// Default prettier config
|
// Default prettier config
|
||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
@@ -119,7 +230,56 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
printWidth: 80,
|
printWidth: 80,
|
||||||
tabWidth: 2,
|
tabWidth: 2,
|
||||||
semi: true,
|
semi: true,
|
||||||
arrowParens: 'always'
|
arrowParens: 'always',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Override check() to compute diffs on-the-fly by running prettier
|
||||||
|
*/
|
||||||
|
async check(): Promise<ICheckResult> {
|
||||||
|
const changes = await this.analyze();
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +1,55 @@
|
|||||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import type { IPlannedChange } from '../interfaces.format.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
import * as formatReadme from '../format.readme.js';
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import { logger } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
|
const DEFAULT_README_CONTENT = `# Project Readme
|
||||||
|
|
||||||
|
This is the initial readme file.`;
|
||||||
|
|
||||||
|
const DEFAULT_README_HINTS_CONTENT = `# Project Readme Hints
|
||||||
|
|
||||||
|
This is the initial readme hints file.`;
|
||||||
|
|
||||||
export class ReadmeFormatter extends BaseFormatter {
|
export class ReadmeFormatter extends BaseFormatter {
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return 'readme';
|
return 'readme';
|
||||||
}
|
}
|
||||||
|
|
||||||
async analyze(): Promise<IPlannedChange[]> {
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
return [{
|
const changes: IPlannedChange[] = [];
|
||||||
type: 'modify',
|
|
||||||
path: 'readme.md',
|
// Check readme.md
|
||||||
module: this.name,
|
const readmeExists = await plugins.smartfs.file('readme.md').exists();
|
||||||
description: 'Ensure readme files exist'
|
if (!readmeExists) {
|
||||||
}];
|
changes.push({
|
||||||
|
type: 'create',
|
||||||
|
path: 'readme.md',
|
||||||
|
module: this.name,
|
||||||
|
description: 'Create readme.md',
|
||||||
|
content: DEFAULT_README_CONTENT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check readme.hints.md
|
||||||
|
const hintsExists = await plugins.smartfs.file('readme.hints.md').exists();
|
||||||
|
if (!hintsExists) {
|
||||||
|
changes.push({
|
||||||
|
type: 'create',
|
||||||
|
path: 'readme.hints.md',
|
||||||
|
module: this.name,
|
||||||
|
description: 'Create readme.hints.md',
|
||||||
|
content: DEFAULT_README_HINTS_CONTENT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyChange(change: IPlannedChange): Promise<void> {
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
await formatReadme.run();
|
if (change.type !== 'create' || !change.content) return;
|
||||||
|
|
||||||
|
await this.createFile(change.path, change.content);
|
||||||
|
logger.log('info', `Created ${change.path}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { BaseFormatter } from "../classes.baseformatter.js";
|
||||||
|
import type { IPlannedChange } from "../interfaces.format.js";
|
||||||
|
import * as plugins from "../mod.plugins.js";
|
||||||
|
import { logger, logVerbose } from "../../gitzone.logging.js";
|
||||||
|
import { migrateSmartconfigData } from "../../helpers.smartconfigmigrations.js";
|
||||||
|
|
||||||
|
const CONFIG_FILE = ".smartconfig.json";
|
||||||
|
|
||||||
|
export class SmartconfigFormatter extends BaseFormatter {
|
||||||
|
get name(): string {
|
||||||
|
return "smartconfig";
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
|
const changes: IPlannedChange[] = [];
|
||||||
|
|
||||||
|
// File rename (npmextra.json/smartconfig.json → .smartconfig.json)
|
||||||
|
// is handled by the orchestrator before analysis.
|
||||||
|
// This formatter only operates on .smartconfig.json.
|
||||||
|
const exists = await plugins.smartfs.file(CONFIG_FILE).exists();
|
||||||
|
if (!exists) {
|
||||||
|
logVerbose(".smartconfig.json does not exist, skipping");
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentContent = (await plugins.smartfs
|
||||||
|
.file(CONFIG_FILE)
|
||||||
|
.encoding("utf8")
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
|
const smartconfigJson = JSON.parse(currentContent);
|
||||||
|
|
||||||
|
migrateSmartconfigData(smartconfigJson);
|
||||||
|
|
||||||
|
// Ensure namespaces exist
|
||||||
|
if (!smartconfigJson["@git.zone/cli"]) {
|
||||||
|
smartconfigJson["@git.zone/cli"] = {};
|
||||||
|
}
|
||||||
|
if (!smartconfigJson["@ship.zone/szci"]) {
|
||||||
|
smartconfigJson["@ship.zone/szci"] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContent = JSON.stringify(smartconfigJson, null, 2);
|
||||||
|
|
||||||
|
if (newContent !== currentContent) {
|
||||||
|
changes.push({
|
||||||
|
type: "modify",
|
||||||
|
path: CONFIG_FILE,
|
||||||
|
module: this.name,
|
||||||
|
description: "Migrate and format .smartconfig.json",
|
||||||
|
content: newContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
|
if (change.type !== "modify" || !change.content) return;
|
||||||
|
|
||||||
|
const smartconfigJson = JSON.parse(change.content);
|
||||||
|
|
||||||
|
// Check for missing required module information
|
||||||
|
const expectedRepoInformation: string[] = [
|
||||||
|
"projectType",
|
||||||
|
"module.githost",
|
||||||
|
"module.gitscope",
|
||||||
|
"module.gitrepo",
|
||||||
|
"module.description",
|
||||||
|
"module.npmPackagename",
|
||||||
|
"module.license",
|
||||||
|
];
|
||||||
|
|
||||||
|
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||||
|
const missingRepoInformation = expectedRepoInformation.filter(
|
||||||
|
(expectedRepoInformationItem) => {
|
||||||
|
return !plugins.smartobject.smartGet(
|
||||||
|
smartconfigJson["@git.zone/cli"],
|
||||||
|
expectedRepoInformationItem,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (missingRepoInformation.length > 0 && !this.context.isInteractive()) {
|
||||||
|
throw new Error(
|
||||||
|
`Missing required .smartconfig.json fields: ${missingRepoInformation.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
||||||
|
if (
|
||||||
|
!plugins.smartobject.smartGet(
|
||||||
|
smartconfigJson["@git.zone/cli"],
|
||||||
|
expectedRepoInformationItem,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
interactInstance.addQuestions([
|
||||||
|
{
|
||||||
|
message: `What is the value of ${expectedRepoInformationItem}`,
|
||||||
|
name: expectedRepoInformationItem,
|
||||||
|
type: "input",
|
||||||
|
default: "undefined variable",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const answerbucket = await interactInstance.runQueue();
|
||||||
|
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
||||||
|
const cliProvidedValue = answerbucket.getAnswerFor(
|
||||||
|
expectedRepoInformationItem,
|
||||||
|
);
|
||||||
|
if (cliProvidedValue) {
|
||||||
|
plugins.smartobject.smartAdd(
|
||||||
|
smartconfigJson["@git.zone/cli"],
|
||||||
|
expectedRepoInformationItem,
|
||||||
|
cliProvidedValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalContent = JSON.stringify(smartconfigJson, null, 2);
|
||||||
|
await this.modifyFile(change.path, finalContent);
|
||||||
|
logger.log("info", "Updated .smartconfig.json");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,167 @@
|
|||||||
import { LegacyFormatter } from './legacy.formatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import * as formatTemplates from '../format.templates.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import * as paths from '../../paths.js';
|
||||||
|
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
export class TemplatesFormatter extends LegacyFormatter {
|
export class TemplatesFormatter extends BaseFormatter {
|
||||||
constructor(context: any, project: any) {
|
get name(): string {
|
||||||
super(context, project, 'templates', formatTemplates);
|
return 'templates';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Render a template directory through smartscaf and return a map of path → content.
|
||||||
|
*/
|
||||||
|
private async renderTemplate(templateName: string): Promise<Map<string, string>> {
|
||||||
|
const templateDir = plugins.path.join(paths.templatesDir, templateName);
|
||||||
|
|
||||||
|
const scafTemplate = new plugins.smartscaf.ScafTemplate(templateDir);
|
||||||
|
await scafTemplate.readTemplateFromDir();
|
||||||
|
|
||||||
|
const gitzoneData = this.project.gitzoneConfig?.data;
|
||||||
|
if (gitzoneData) {
|
||||||
|
await scafTemplate.supplyVariables({
|
||||||
|
module: gitzoneData.module,
|
||||||
|
projectType: gitzoneData.projectType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedFiles = await scafTemplate.renderToMemory();
|
||||||
|
|
||||||
|
const fileMap = new Map<string, string>();
|
||||||
|
for (const file of renderedFiles) {
|
||||||
|
fileMap.set(file.path, file.contents.toString());
|
||||||
|
}
|
||||||
|
return fileMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
|
const changes: IPlannedChange[] = [];
|
||||||
|
const project = this.project;
|
||||||
|
const projectType = project.gitzoneConfig?.data?.projectType;
|
||||||
|
|
||||||
|
// VSCode template - for all projects
|
||||||
|
const vscodeChanges = await this.analyzeTemplate('vscode', [
|
||||||
|
{ templatePath: '.vscode/settings.json', destPath: '.vscode/settings.json' },
|
||||||
|
{ templatePath: '.vscode/launch.json', destPath: '.vscode/launch.json' },
|
||||||
|
]);
|
||||||
|
changes.push(...vscodeChanges);
|
||||||
|
|
||||||
|
// CI and other templates based on projectType
|
||||||
|
switch (projectType) {
|
||||||
|
case 'npm':
|
||||||
|
case 'wcc':
|
||||||
|
const accessLevel = (project.gitzoneConfig?.data as any)?.release?.accessLevel
|
||||||
|
|| project.gitzoneConfig?.data?.npmciOptions?.npmAccessLevel;
|
||||||
|
const ciTemplate = accessLevel === 'public' ? 'ci_default' : 'ci_default_private';
|
||||||
|
const ciChanges = await this.analyzeTemplate(ciTemplate, [
|
||||||
|
{ templatePath: '.gitea/workflows/default_nottags.yaml', destPath: '.gitea/workflows/default_nottags.yaml' },
|
||||||
|
{ templatePath: '.gitea/workflows/default_tags.yaml', destPath: '.gitea/workflows/default_tags.yaml' },
|
||||||
|
]);
|
||||||
|
changes.push(...ciChanges);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'service':
|
||||||
|
case 'website':
|
||||||
|
const dockerCiChanges = await this.analyzeTemplate('ci_docker', [
|
||||||
|
{ templatePath: '.gitea/workflows/docker_nottags.yaml', destPath: '.gitea/workflows/docker_nottags.yaml' },
|
||||||
|
{ templatePath: '.gitea/workflows/docker_tags.yaml', destPath: '.gitea/workflows/docker_tags.yaml' },
|
||||||
|
]);
|
||||||
|
changes.push(...dockerCiChanges);
|
||||||
|
|
||||||
|
const dockerfileChanges = await this.analyzeTemplate('dockerfile_service', [
|
||||||
|
{ templatePath: 'Dockerfile', destPath: 'Dockerfile' },
|
||||||
|
{ templatePath: 'dockerignore', destPath: '.dockerignore' },
|
||||||
|
]);
|
||||||
|
changes.push(...dockerfileChanges);
|
||||||
|
|
||||||
|
const cliChanges = await this.analyzeTemplate('cli', [
|
||||||
|
{ templatePath: 'cli.js', destPath: 'cli.js' },
|
||||||
|
{ templatePath: 'cli.ts.js', destPath: 'cli.ts.js' },
|
||||||
|
]);
|
||||||
|
changes.push(...cliChanges);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update templates based on projectType
|
||||||
|
if (projectType === 'website') {
|
||||||
|
const websiteChanges = await this.analyzeTemplate('website_update', [
|
||||||
|
{ templatePath: 'html/index.html', destPath: 'html/index.html' },
|
||||||
|
]);
|
||||||
|
changes.push(...websiteChanges);
|
||||||
|
} else if (projectType === 'wcc') {
|
||||||
|
const wccChanges = await this.analyzeTemplate('wcc_update', [
|
||||||
|
{ templatePath: 'html/index.html', destPath: 'html/index.html' },
|
||||||
|
{ templatePath: 'html/index.ts', destPath: 'html/index.ts' },
|
||||||
|
]);
|
||||||
|
changes.push(...wccChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async analyzeTemplate(
|
||||||
|
templateName: string,
|
||||||
|
files: Array<{ templatePath: string; destPath: string }>,
|
||||||
|
): Promise<IPlannedChange[]> {
|
||||||
|
const changes: IPlannedChange[] = [];
|
||||||
|
const templateDir = plugins.path.join(paths.templatesDir, templateName);
|
||||||
|
|
||||||
|
const templateExists = await plugins.smartfs.directory(templateDir).exists();
|
||||||
|
if (!templateExists) {
|
||||||
|
logVerbose(`Template ${templateName} not found`);
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderedFiles: Map<string, string>;
|
||||||
|
try {
|
||||||
|
renderedFiles = await this.renderTemplate(templateName);
|
||||||
|
} catch (error) {
|
||||||
|
logVerbose(`Failed to render template ${templateName}: ${error.message}`);
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
// Look up by templatePath first, then destPath (frontmatter may rename files)
|
||||||
|
const processedContent = renderedFiles.get(file.templatePath)
|
||||||
|
|| renderedFiles.get(file.destPath);
|
||||||
|
|
||||||
|
if (!processedContent) {
|
||||||
|
logVerbose(`Template file ${file.templatePath} not found in rendered output`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const destExists = await plugins.smartfs.file(file.destPath).exists();
|
||||||
|
let currentContent = '';
|
||||||
|
if (destExists) {
|
||||||
|
currentContent = (await plugins.smartfs
|
||||||
|
.file(file.destPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processedContent !== currentContent) {
|
||||||
|
changes.push({
|
||||||
|
type: destExists ? 'modify' : 'create',
|
||||||
|
path: file.destPath,
|
||||||
|
module: this.name,
|
||||||
|
description: `Apply template ${templateName}/${file.templatePath}`,
|
||||||
|
content: processedContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
|
if (!change.content) return;
|
||||||
|
|
||||||
|
if (change.type === 'create') {
|
||||||
|
await this.createFile(change.path, change.content);
|
||||||
|
} else {
|
||||||
|
await this.modifyFile(change.path, change.content);
|
||||||
|
}
|
||||||
|
logger.log('info', `Applied template to ${change.path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,76 @@
|
|||||||
import { LegacyFormatter } from './legacy.formatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import * as formatTsconfig from '../format.tsconfig.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import * as paths from '../../paths.js';
|
||||||
|
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
export class TsconfigFormatter extends LegacyFormatter {
|
export class TsconfigFormatter extends BaseFormatter {
|
||||||
constructor(context: any, project: any) {
|
get name(): string {
|
||||||
super(context, project, 'tsconfig', formatTsconfig);
|
return 'tsconfig';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
|
const changes: IPlannedChange[] = [];
|
||||||
|
const tsconfigPath = 'tsconfig.json';
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
const exists = await plugins.smartfs.file(tsconfigPath).exists();
|
||||||
|
if (!exists) {
|
||||||
|
logVerbose('tsconfig.json does not exist, skipping');
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read current content
|
||||||
|
const currentContent = (await plugins.smartfs
|
||||||
|
.file(tsconfigPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
|
// Parse and compute new content
|
||||||
|
const tsconfigObject = JSON.parse(currentContent);
|
||||||
|
tsconfigObject.compilerOptions = tsconfigObject.compilerOptions || {};
|
||||||
|
tsconfigObject.compilerOptions.baseUrl = '.';
|
||||||
|
const existingPaths = tsconfigObject.compilerOptions.paths || {};
|
||||||
|
|
||||||
|
// Get module paths from tspublish, merging with existing custom paths
|
||||||
|
const tspublishPaths: Record<string, string[]> = {};
|
||||||
|
try {
|
||||||
|
const tsPublishMod = await import('@git.zone/tspublish');
|
||||||
|
const tsPublishInstance = new tsPublishMod.TsPublish();
|
||||||
|
const publishModules = await tsPublishInstance.getModuleSubDirs(paths.cwd);
|
||||||
|
|
||||||
|
for (const publishModule of Object.keys(publishModules)) {
|
||||||
|
const publishConfig = publishModules[publishModule];
|
||||||
|
tspublishPaths[`${publishConfig.name}`] = [
|
||||||
|
`./${publishModule}/index.js`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logVerbose(`Could not get tspublish modules: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
tsconfigObject.compilerOptions.paths = { ...existingPaths, ...tspublishPaths };
|
||||||
|
|
||||||
|
const newContent = JSON.stringify(tsconfigObject, null, 2);
|
||||||
|
|
||||||
|
// Only add change if content differs
|
||||||
|
if (newContent !== currentContent) {
|
||||||
|
changes.push({
|
||||||
|
type: 'modify',
|
||||||
|
path: tsconfigPath,
|
||||||
|
module: this.name,
|
||||||
|
description: 'Format tsconfig.json with path mappings',
|
||||||
|
content: newContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
|
if (change.type !== 'modify' || !change.content) return;
|
||||||
|
|
||||||
|
await this.modifyFile(change.path, change.content);
|
||||||
|
logger.log('info', 'Updated tsconfig.json');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+382
-196
@@ -1,248 +1,434 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from "./mod.plugins.js";
|
||||||
import { Project } from '../classes.project.js';
|
import { Project } from "../classes.project.js";
|
||||||
import { FormatContext } from './classes.formatcontext.js';
|
import { FormatContext } from "./classes.formatcontext.js";
|
||||||
import { FormatPlanner } from './classes.formatplanner.js';
|
import { FormatPlanner } from "./classes.formatplanner.js";
|
||||||
import { logger, setVerboseMode } from '../gitzone.logging.js';
|
import { BaseFormatter } from "./classes.baseformatter.js";
|
||||||
|
import { logger, setVerboseMode } from "../gitzone.logging.js";
|
||||||
|
import type { ICliMode } from "../helpers.climode.js";
|
||||||
|
import {
|
||||||
|
getCliMode,
|
||||||
|
printJson,
|
||||||
|
runWithSuppressedOutput,
|
||||||
|
} from "../helpers.climode.js";
|
||||||
|
import { getCliConfigValue } from "../helpers.smartconfig.js";
|
||||||
|
|
||||||
// Import wrapper classes for formatters
|
import { CleanupFormatter } from "./formatters/cleanup.formatter.js";
|
||||||
import { CleanupFormatter } from './formatters/cleanup.formatter.js';
|
import { SmartconfigFormatter } from "./formatters/smartconfig.formatter.js";
|
||||||
import { NpmextraFormatter } from './formatters/npmextra.formatter.js';
|
import { LicenseFormatter } from "./formatters/license.formatter.js";
|
||||||
import { LicenseFormatter } from './formatters/license.formatter.js';
|
import { PackageJsonFormatter } from "./formatters/packagejson.formatter.js";
|
||||||
import { PackageJsonFormatter } from './formatters/packagejson.formatter.js';
|
import { TemplatesFormatter } from "./formatters/templates.formatter.js";
|
||||||
import { TemplatesFormatter } from './formatters/templates.formatter.js';
|
import { GitignoreFormatter } from "./formatters/gitignore.formatter.js";
|
||||||
import { GitignoreFormatter } from './formatters/gitignore.formatter.js';
|
import { TsconfigFormatter } from "./formatters/tsconfig.formatter.js";
|
||||||
import { TsconfigFormatter } from './formatters/tsconfig.formatter.js';
|
import { PrettierFormatter } from "./formatters/prettier.formatter.js";
|
||||||
import { PrettierFormatter } from './formatters/prettier.formatter.js';
|
import { ReadmeFormatter } from "./formatters/readme.formatter.js";
|
||||||
import { ReadmeFormatter } from './formatters/readme.formatter.js';
|
import { CopyFormatter } from "./formatters/copy.formatter.js";
|
||||||
import { CopyFormatter } from './formatters/copy.formatter.js';
|
|
||||||
|
|
||||||
export let run = async (options: {
|
/**
|
||||||
dryRun?: boolean;
|
* Rename npmextra.json or smartconfig.json to .smartconfig.json
|
||||||
yes?: boolean;
|
* before any formatter tries to read config.
|
||||||
planOnly?: boolean;
|
*/
|
||||||
savePlan?: string;
|
async function migrateConfigFile(allowWrite: boolean): Promise<void> {
|
||||||
fromPlan?: string;
|
const target = ".smartconfig.json";
|
||||||
detailed?: boolean;
|
const targetExists = await plugins.smartfs.file(target).exists();
|
||||||
interactive?: boolean;
|
if (targetExists) return;
|
||||||
parallel?: boolean;
|
|
||||||
verbose?: boolean;
|
for (const oldName of ["smartconfig.json", "npmextra.json"]) {
|
||||||
} = {}): Promise<any> => {
|
const exists = await plugins.smartfs.file(oldName).exists();
|
||||||
// Set verbose mode if requested
|
if (exists) {
|
||||||
if (options.verbose) {
|
if (!allowWrite) {
|
||||||
setVerboseMode(true);
|
return;
|
||||||
|
}
|
||||||
|
const content = (await plugins.smartfs
|
||||||
|
.file(oldName)
|
||||||
|
.encoding("utf8")
|
||||||
|
.read()) as string;
|
||||||
|
await plugins.smartfs.file(`./${target}`).encoding("utf8").write(content);
|
||||||
|
await plugins.smartfs.file(oldName).delete();
|
||||||
|
logger.log("info", `Migrated ${oldName} to ${target}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const project = await Project.fromCwd();
|
|
||||||
const context = new FormatContext();
|
// Shared formatter class map used by both run() and runFormatter()
|
||||||
await context.initializeCache(); // Initialize the cache system
|
const formatterMap: Record<
|
||||||
const planner = new FormatPlanner();
|
string,
|
||||||
|
new (ctx: FormatContext, proj: Project) => BaseFormatter
|
||||||
// Get configuration from npmextra
|
> = {
|
||||||
const npmextraConfig = new plugins.npmextra.Npmextra();
|
cleanup: CleanupFormatter,
|
||||||
const formatConfig = npmextraConfig.dataFor<any>('gitzone.format', {
|
smartconfig: SmartconfigFormatter,
|
||||||
|
license: LicenseFormatter,
|
||||||
|
packagejson: PackageJsonFormatter,
|
||||||
|
templates: TemplatesFormatter,
|
||||||
|
gitignore: GitignoreFormatter,
|
||||||
|
tsconfig: TsconfigFormatter,
|
||||||
|
prettier: PrettierFormatter,
|
||||||
|
readme: ReadmeFormatter,
|
||||||
|
copy: CopyFormatter,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Formatters that don't require projectType to be set
|
||||||
|
const formattersNotRequiringProjectType = [
|
||||||
|
"smartconfig",
|
||||||
|
"prettier",
|
||||||
|
"cleanup",
|
||||||
|
"packagejson",
|
||||||
|
];
|
||||||
|
|
||||||
|
const getFormatConfig = async () => {
|
||||||
|
const rawFormatConfig = await getCliConfigValue<Record<string, any>>(
|
||||||
|
"format",
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
return {
|
||||||
interactive: true,
|
interactive: true,
|
||||||
showDiffs: false,
|
showDiffs: false,
|
||||||
autoApprove: false,
|
autoApprove: false,
|
||||||
planTimeout: 30000,
|
showStats: true,
|
||||||
rollback: {
|
|
||||||
enabled: true,
|
|
||||||
autoRollbackOnError: true,
|
|
||||||
backupRetentionDays: 7,
|
|
||||||
maxBackupSize: '100MB',
|
|
||||||
excludePatterns: ['node_modules/**', '.git/**']
|
|
||||||
},
|
|
||||||
modules: {
|
modules: {
|
||||||
skip: [],
|
skip: [],
|
||||||
only: [],
|
only: [],
|
||||||
order: []
|
...(rawFormatConfig.modules || {}),
|
||||||
},
|
},
|
||||||
parallel: true,
|
...rawFormatConfig,
|
||||||
cache: {
|
};
|
||||||
enabled: true,
|
};
|
||||||
clean: true // Clean invalid entries from cache
|
|
||||||
|
const createActiveFormatters = async (options: {
|
||||||
|
interactive: boolean;
|
||||||
|
jsonOutput: boolean;
|
||||||
|
}) => {
|
||||||
|
const project = await Project.fromCwd({ requireProjectType: false });
|
||||||
|
const context = new FormatContext(options);
|
||||||
|
const planner = new FormatPlanner();
|
||||||
|
|
||||||
|
const formatConfig = await getFormatConfig();
|
||||||
|
const formatters = Object.entries(formatterMap).map(
|
||||||
|
([, FormatterClass]) => new FormatterClass(context, project),
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeFormatters = formatters.filter((formatter) => {
|
||||||
|
if (formatConfig.modules.only.length > 0) {
|
||||||
|
return formatConfig.modules.only.includes(formatter.name);
|
||||||
}
|
}
|
||||||
|
if (formatConfig.modules.skip.includes(formatter.name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean cache if configured
|
return {
|
||||||
if (formatConfig.cache.clean) {
|
context,
|
||||||
await context.getChangeCache().clean();
|
planner,
|
||||||
}
|
formatConfig,
|
||||||
|
activeFormatters,
|
||||||
// Override config with command options
|
};
|
||||||
const interactive = options.interactive ?? formatConfig.interactive;
|
};
|
||||||
const autoApprove = options.yes ?? formatConfig.autoApprove;
|
|
||||||
const parallel = options.parallel ?? formatConfig.parallel;
|
const buildFormatPlan = async (options: {
|
||||||
|
fromPlan?: string;
|
||||||
try {
|
interactive: boolean;
|
||||||
// Initialize formatters
|
jsonOutput: boolean;
|
||||||
const formatters = [
|
}) => {
|
||||||
new CleanupFormatter(context, project),
|
const { context, planner, formatConfig, activeFormatters } =
|
||||||
new NpmextraFormatter(context, project),
|
await createActiveFormatters({
|
||||||
new LicenseFormatter(context, project),
|
interactive: options.interactive,
|
||||||
new PackageJsonFormatter(context, project),
|
jsonOutput: options.jsonOutput,
|
||||||
new TemplatesFormatter(context, project),
|
|
||||||
new GitignoreFormatter(context, project),
|
|
||||||
new TsconfigFormatter(context, project),
|
|
||||||
new PrettierFormatter(context, project),
|
|
||||||
new ReadmeFormatter(context, project),
|
|
||||||
new CopyFormatter(context, project),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Filter formatters based on configuration
|
|
||||||
const activeFormatters = formatters.filter(formatter => {
|
|
||||||
if (formatConfig.modules.only.length > 0) {
|
|
||||||
return formatConfig.modules.only.includes(formatter.name);
|
|
||||||
}
|
|
||||||
if (formatConfig.modules.skip.includes(formatter.name)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Plan phase
|
const plan = options.fromPlan
|
||||||
logger.log('info', 'Analyzing project for format operations...');
|
? JSON.parse(
|
||||||
let plan = options.fromPlan
|
(await plugins.smartfs
|
||||||
? JSON.parse(await plugins.smartfile.fs.toStringSync(options.fromPlan))
|
.file(options.fromPlan)
|
||||||
: await planner.planFormat(activeFormatters);
|
.encoding("utf8")
|
||||||
|
.read()) as string,
|
||||||
|
)
|
||||||
|
: await planner.planFormat(activeFormatters);
|
||||||
|
|
||||||
|
return {
|
||||||
|
context,
|
||||||
|
planner,
|
||||||
|
formatConfig,
|
||||||
|
activeFormatters,
|
||||||
|
plan,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const serializePlan = (plan: any) => {
|
||||||
|
return {
|
||||||
|
summary: plan.summary,
|
||||||
|
warnings: plan.warnings,
|
||||||
|
changes: plan.changes.map((change: any) => ({
|
||||||
|
type: change.type,
|
||||||
|
path: change.path,
|
||||||
|
module: change.module,
|
||||||
|
description: change.description,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export let run = async (
|
||||||
|
options: {
|
||||||
|
write?: boolean;
|
||||||
|
dryRun?: boolean; // Deprecated, kept for compatibility
|
||||||
|
yes?: boolean;
|
||||||
|
planOnly?: boolean;
|
||||||
|
savePlan?: string;
|
||||||
|
fromPlan?: string;
|
||||||
|
detailed?: boolean;
|
||||||
|
interactive?: boolean;
|
||||||
|
verbose?: boolean;
|
||||||
|
diff?: boolean;
|
||||||
|
[key: string]: any;
|
||||||
|
} = {},
|
||||||
|
): Promise<any> => {
|
||||||
|
const mode = await getCliMode(options as any);
|
||||||
|
const subcommand = (options as any)?._?.[1];
|
||||||
|
|
||||||
|
if (mode.help || subcommand === "help") {
|
||||||
|
showHelp(mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.verbose) {
|
||||||
|
setVerboseMode(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldWrite = options.write ?? options.dryRun === false;
|
||||||
|
const treatAsPlan = subcommand === "plan";
|
||||||
|
|
||||||
|
if (mode.json && shouldWrite) {
|
||||||
|
printJson({
|
||||||
|
ok: false,
|
||||||
|
error:
|
||||||
|
"JSON output is only supported for read-only format planning. Use `gitzone format plan --json` or omit `--json` when applying changes.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate config file before anything reads it
|
||||||
|
await migrateConfigFile(shouldWrite);
|
||||||
|
|
||||||
|
const formatConfig = await getFormatConfig();
|
||||||
|
const interactive =
|
||||||
|
options.interactive ?? (mode.interactive && formatConfig.interactive);
|
||||||
|
const autoApprove = options.yes ?? formatConfig.autoApprove;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const planBuilder = async () => {
|
||||||
|
return await buildFormatPlan({
|
||||||
|
fromPlan: options.fromPlan,
|
||||||
|
interactive,
|
||||||
|
jsonOutput: mode.json,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!mode.json) {
|
||||||
|
logger.log("info", "Analyzing project for format operations...");
|
||||||
|
}
|
||||||
|
const { context, planner, activeFormatters, plan } = mode.json
|
||||||
|
? await runWithSuppressedOutput(planBuilder)
|
||||||
|
: await planBuilder();
|
||||||
|
|
||||||
|
if (mode.json) {
|
||||||
|
printJson(serializePlan(plan));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Display plan
|
// Display plan
|
||||||
await planner.displayPlan(plan, options.detailed);
|
await planner.displayPlan(plan, options.detailed);
|
||||||
|
|
||||||
// Save plan if requested
|
// Save plan if requested
|
||||||
if (options.savePlan) {
|
if (options.savePlan) {
|
||||||
await plugins.smartfile.memory.toFs(JSON.stringify(plan, null, 2), options.savePlan);
|
await plugins.smartfs
|
||||||
logger.log('info', `Plan saved to ${options.savePlan}`);
|
.file(options.savePlan)
|
||||||
|
.encoding("utf8")
|
||||||
|
.write(JSON.stringify(plan, null, 2));
|
||||||
|
logger.log("info", `Plan saved to ${options.savePlan}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit if plan-only mode
|
if (options.planOnly || treatAsPlan) {
|
||||||
if (options.planOnly) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dry-run mode
|
// Show diffs if explicitly requested or before interactive write confirmation
|
||||||
if (options.dryRun) {
|
const showDiffs =
|
||||||
logger.log('info', 'Dry-run mode - no changes will be made');
|
options.diff || (shouldWrite && interactive && !autoApprove);
|
||||||
|
if (showDiffs) {
|
||||||
|
logger.log("info", "Showing file diffs:");
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
for (const formatter of activeFormatters) {
|
||||||
|
const checkResult = await formatter.check();
|
||||||
|
if (checkResult.hasDiff) {
|
||||||
|
logger.log("info", `[${formatter.name}]`);
|
||||||
|
formatter.displayAllDiffs(checkResult);
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dry-run mode (default behavior)
|
||||||
|
if (!shouldWrite) {
|
||||||
|
logger.log("info", "Dry-run mode - use --write (-w) to apply changes");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interactive confirmation
|
// Interactive confirmation
|
||||||
if (interactive && !autoApprove) {
|
if (interactive && !autoApprove) {
|
||||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||||
const response = await interactInstance.askQuestion({
|
const response = await interactInstance.askQuestion({
|
||||||
type: 'confirm',
|
type: "confirm",
|
||||||
name: 'proceed',
|
name: "proceed",
|
||||||
message: 'Proceed with formatting?',
|
message: "Proceed with formatting?",
|
||||||
default: true
|
default: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!(response as any).value) {
|
if (!(response as any).value) {
|
||||||
logger.log('info', 'Format operation cancelled by user');
|
logger.log("info", "Format operation cancelled by user");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute phase
|
// Execute phase
|
||||||
logger.log('info', `Executing format operations${parallel ? ' in parallel' : ' sequentially'}...`);
|
logger.log("info", "Executing format operations...");
|
||||||
await planner.executePlan(plan, activeFormatters, context, parallel);
|
await planner.executePlan(plan, activeFormatters, context);
|
||||||
|
|
||||||
// Finish statistics tracking
|
|
||||||
context.getFormatStats().finish();
|
context.getFormatStats().finish();
|
||||||
|
|
||||||
// Display statistics
|
const showStats = formatConfig.showStats ?? true;
|
||||||
const showStats = npmextraConfig.dataFor('gitzone.format.showStats', true);
|
|
||||||
if (showStats) {
|
if (showStats) {
|
||||||
context.getFormatStats().displayStats();
|
context.getFormatStats().displayStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save stats if requested
|
|
||||||
if (options.detailed) {
|
if (options.detailed) {
|
||||||
const statsPath = `.nogit/format-stats-${Date.now()}.json`;
|
const statsPath = `.nogit/format-stats-${Date.now()}.json`;
|
||||||
await context.getFormatStats().saveReport(statsPath);
|
await context.getFormatStats().saveReport(statsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('success', 'Format operations completed successfully!');
|
logger.log("success", "Format operations completed successfully!");
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log('error', `Format operation failed: ${error.message}`);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.log("error", `Format operation failed: ${errorMessage}`);
|
||||||
// Automatic rollback if enabled
|
|
||||||
if (formatConfig.rollback.enabled && formatConfig.rollback.autoRollbackOnError) {
|
|
||||||
logger.log('info', 'Attempting automatic rollback...');
|
|
||||||
try {
|
|
||||||
await context.rollbackOperation();
|
|
||||||
logger.log('success', 'Rollback completed successfully');
|
|
||||||
} catch (rollbackError) {
|
|
||||||
logger.log('error', `Rollback failed: ${rollbackError.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export CLI command handlers
|
import type { ICheckResult } from "./interfaces.format.js";
|
||||||
export const handleRollback = async (operationId?: string): Promise<void> => {
|
export type { ICheckResult };
|
||||||
const context = new FormatContext();
|
|
||||||
const rollbackManager = context.getRollbackManager();
|
/**
|
||||||
|
* Run a single formatter by name (for use by other modules)
|
||||||
if (!operationId) {
|
*/
|
||||||
// Rollback to last operation
|
export const runFormatter = async (
|
||||||
const backups = await rollbackManager.listBackups();
|
formatterName: string,
|
||||||
const lastOperation = backups
|
options: {
|
||||||
.filter(op => op.status !== 'rolled-back')
|
silent?: boolean;
|
||||||
.sort((a, b) => b.timestamp - a.timestamp)[0];
|
checkOnly?: boolean;
|
||||||
|
showDiff?: boolean;
|
||||||
if (!lastOperation) {
|
} = {},
|
||||||
logger.log('warn', 'No operations available for rollback');
|
): Promise<ICheckResult | void> => {
|
||||||
return;
|
const requireProjectType =
|
||||||
}
|
!formattersNotRequiringProjectType.includes(formatterName);
|
||||||
|
const project = await Project.fromCwd({ requireProjectType });
|
||||||
operationId = lastOperation.id;
|
const context = new FormatContext({ interactive: true, jsonOutput: false });
|
||||||
|
|
||||||
|
const FormatterClass = formatterMap[formatterName];
|
||||||
|
if (!FormatterClass) {
|
||||||
|
throw new Error(`Unknown formatter: ${formatterName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const formatter = new FormatterClass(context, project);
|
||||||
await rollbackManager.rollback(operationId);
|
|
||||||
logger.log('success', `Successfully rolled back operation ${operationId}`);
|
if (options.checkOnly) {
|
||||||
} catch (error) {
|
const result = await formatter.check();
|
||||||
logger.log('error', `Rollback failed: ${error.message}`);
|
if (result.hasDiff && options.showDiff) {
|
||||||
throw error;
|
formatter.displayAllDiffs(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changes = await formatter.analyze();
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
await formatter.applyChange(change);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.silent) {
|
||||||
|
logger.log("success", `Formatter '${formatterName}' completed`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleListBackups = async (): Promise<void> => {
|
export function showHelp(mode?: ICliMode): void {
|
||||||
const context = new FormatContext();
|
if (mode?.json) {
|
||||||
const rollbackManager = context.getRollbackManager();
|
printJson({
|
||||||
const backups = await rollbackManager.listBackups();
|
command: "format",
|
||||||
|
usage: "gitzone format [plan] [options]",
|
||||||
if (backups.length === 0) {
|
description:
|
||||||
logger.log('info', 'No backup operations found');
|
"Plans formatting changes by default and applies them only with --write.",
|
||||||
|
flags: [
|
||||||
|
{ flag: "--write, -w", description: "Apply planned changes" },
|
||||||
|
{
|
||||||
|
flag: "--yes",
|
||||||
|
description: "Skip the interactive confirmation before writing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--plan-only",
|
||||||
|
description: "Show the plan without applying changes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--save-plan <file>",
|
||||||
|
description: "Write the format plan to a file",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--from-plan <file>",
|
||||||
|
description: "Load a previously saved plan",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--detailed",
|
||||||
|
description: "Show detailed diffs and save stats",
|
||||||
|
},
|
||||||
|
{ flag: "--verbose", description: "Enable verbose logging" },
|
||||||
|
{
|
||||||
|
flag: "--diff",
|
||||||
|
description: "Show per-file diffs before applying changes",
|
||||||
|
},
|
||||||
|
{ flag: "--json", description: "Emit a read-only format plan as JSON" },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
"gitzone format",
|
||||||
|
"gitzone format plan --json",
|
||||||
|
"gitzone format --write --yes",
|
||||||
|
],
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\nAvailable backups:');
|
|
||||||
console.log('━'.repeat(50));
|
|
||||||
|
|
||||||
for (const backup of backups) {
|
|
||||||
const date = new Date(backup.timestamp).toLocaleString();
|
|
||||||
const status = backup.status;
|
|
||||||
const filesCount = backup.files.length;
|
|
||||||
|
|
||||||
console.log(`ID: ${backup.id}`);
|
|
||||||
console.log(`Date: ${date}`);
|
|
||||||
console.log(`Status: ${status}`);
|
|
||||||
console.log(`Files: ${filesCount}`);
|
|
||||||
console.log('─'.repeat(50));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const handleCleanBackups = async (): Promise<void> => {
|
console.log("");
|
||||||
const context = new FormatContext();
|
console.log("Usage: gitzone format [plan] [options]");
|
||||||
const rollbackManager = context.getRollbackManager();
|
console.log("");
|
||||||
|
console.log(
|
||||||
// Get retention days from config
|
"Plans formatting changes by default and applies them only with --write.",
|
||||||
const npmextraConfig = new plugins.npmextra.Npmextra();
|
);
|
||||||
const retentionDays = npmextraConfig.dataFor<any>('gitzone.format.rollback.backupRetentionDays', 7);
|
console.log("");
|
||||||
|
console.log("Flags:");
|
||||||
await rollbackManager.cleanOldBackups(retentionDays);
|
console.log(" --write, -w Apply planned changes");
|
||||||
logger.log('success', `Cleaned backups older than ${retentionDays} days`);
|
console.log(
|
||||||
};
|
" --yes Skip the interactive confirmation before writing",
|
||||||
|
);
|
||||||
|
console.log(" --plan-only Show the plan without applying changes");
|
||||||
|
console.log(" --save-plan <file> Write the format plan to a file");
|
||||||
|
console.log(" --from-plan <file> Load a previously saved plan");
|
||||||
|
console.log(" --detailed Show detailed diffs and save stats");
|
||||||
|
console.log(" --verbose Enable verbose logging");
|
||||||
|
console.log(
|
||||||
|
" --diff Show per-file diffs before applying changes",
|
||||||
|
);
|
||||||
|
console.log(" --json Emit a read-only format plan as JSON");
|
||||||
|
console.log("");
|
||||||
|
console.log("Examples:");
|
||||||
|
console.log(" gitzone format");
|
||||||
|
console.log(" gitzone format plan --json");
|
||||||
|
console.log(" gitzone format --write --yes");
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,45 +1,53 @@
|
|||||||
export type IFormatOperation = {
|
|
||||||
id: string;
|
|
||||||
timestamp: number;
|
|
||||||
files: Array<{
|
|
||||||
path: string;
|
|
||||||
originalContent: string;
|
|
||||||
checksum: string;
|
|
||||||
permissions: string;
|
|
||||||
}>;
|
|
||||||
status: 'pending' | 'in-progress' | 'completed' | 'failed' | 'rolled-back';
|
|
||||||
error?: Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IFormatPlan = {
|
export type IFormatPlan = {
|
||||||
summary: {
|
summary: {
|
||||||
totalFiles: number;
|
totalFiles: number;
|
||||||
filesAdded: number;
|
filesAdded: number;
|
||||||
filesModified: number;
|
filesModified: number;
|
||||||
filesRemoved: number;
|
filesRemoved: number;
|
||||||
estimatedTime: number;
|
|
||||||
};
|
};
|
||||||
changes: Array<{
|
changes: Array<{
|
||||||
type: 'create' | 'modify' | 'delete';
|
type: 'create' | 'modify' | 'delete';
|
||||||
path: string;
|
path: string;
|
||||||
module: string;
|
module: string;
|
||||||
description: string;
|
description: string;
|
||||||
diff?: string;
|
|
||||||
size?: number;
|
|
||||||
}>;
|
}>;
|
||||||
warnings: Array<{
|
warnings: Array<{
|
||||||
level: 'info' | 'warning' | 'error';
|
level: 'info' | 'warning' | 'error';
|
||||||
message: string;
|
message: string;
|
||||||
module: string;
|
module: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type IPlannedChange = {
|
export type IPlannedChange = {
|
||||||
type: 'create' | 'modify' | 'delete';
|
type: 'create' | 'modify' | 'delete';
|
||||||
path: string;
|
path: string;
|
||||||
module: string;
|
module: string;
|
||||||
description: string;
|
description: string;
|
||||||
content?: string; // For create/modify operations
|
content?: string; // New content for create/modify operations
|
||||||
diff?: string;
|
};
|
||||||
size?: number;
|
|
||||||
}
|
export interface ICheckResult {
|
||||||
|
hasDiff: boolean;
|
||||||
|
diffs: Array<{
|
||||||
|
path: string;
|
||||||
|
type: 'create' | 'modify' | 'delete';
|
||||||
|
before?: string;
|
||||||
|
after?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getModuleIcon(module: string): string {
|
||||||
|
const icons: Record<string, string> = {
|
||||||
|
packagejson: '📦',
|
||||||
|
license: '📝',
|
||||||
|
tsconfig: '🔧',
|
||||||
|
cleanup: '🚮',
|
||||||
|
gitignore: '🔒',
|
||||||
|
prettier: '✨',
|
||||||
|
readme: '📖',
|
||||||
|
templates: '📄',
|
||||||
|
smartconfig: '⚙️',
|
||||||
|
copy: '📋',
|
||||||
|
};
|
||||||
|
return icons[module] || '📁';
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,31 +1,21 @@
|
|||||||
export * from '../plugins.js';
|
export * from '../plugins.js';
|
||||||
|
|
||||||
import * as crypto from 'crypto';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as lik from '@push.rocks/lik';
|
|
||||||
import * as smartfile from '@push.rocks/smartfile';
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
import * as smartgulp from '@push.rocks/smartgulp';
|
|
||||||
import * as smartinteract from '@push.rocks/smartinteract';
|
import * as smartinteract from '@push.rocks/smartinteract';
|
||||||
import * as smartlegal from '@push.rocks/smartlegal';
|
import * as smartlegal from '@push.rocks/smartlegal';
|
||||||
import * as smartobject from '@push.rocks/smartobject';
|
import * as smartobject from '@push.rocks/smartobject';
|
||||||
import * as smartnpm from '@push.rocks/smartnpm';
|
import * as smartconfig from '@push.rocks/smartconfig';
|
||||||
import * as smartstream from '@push.rocks/smartstream';
|
|
||||||
import * as through2 from 'through2';
|
|
||||||
import * as npmextra from '@push.rocks/npmextra';
|
|
||||||
import * as smartdiff from '@push.rocks/smartdiff';
|
import * as smartdiff from '@push.rocks/smartdiff';
|
||||||
|
import * as smartscaf from '@push.rocks/smartscaf';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
crypto,
|
|
||||||
path,
|
path,
|
||||||
lik,
|
|
||||||
smartfile,
|
smartfile,
|
||||||
smartgulp,
|
|
||||||
smartinteract,
|
smartinteract,
|
||||||
smartlegal,
|
smartlegal,
|
||||||
smartobject,
|
smartobject,
|
||||||
smartnpm,
|
smartconfig,
|
||||||
smartstream,
|
|
||||||
through2,
|
|
||||||
npmextra,
|
|
||||||
smartdiff,
|
smartdiff,
|
||||||
|
smartscaf,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ export class Meta {
|
|||||||
* sorts the metaRepoData
|
* sorts the metaRepoData
|
||||||
*/
|
*/
|
||||||
public async sortMetaRepoData() {
|
public async sortMetaRepoData() {
|
||||||
const stringifiedMetadata = plugins.smartjson.stringify(this.metaRepoData, []);
|
const stringifiedMetadata = plugins.smartjson.stringify(
|
||||||
|
this.metaRepoData,
|
||||||
|
[],
|
||||||
|
);
|
||||||
this.metaRepoData = plugins.smartjson.parse(stringifiedMetadata);
|
this.metaRepoData = plugins.smartjson.parse(stringifiedMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,11 +48,17 @@ export class Meta {
|
|||||||
public async readDirectory() {
|
public async readDirectory() {
|
||||||
await this.syncToRemote(true);
|
await this.syncToRemote(true);
|
||||||
logger.log('info', `reading directory`);
|
logger.log('info', `reading directory`);
|
||||||
const metaFileExists = plugins.smartfile.fs.fileExistsSync(this.filePaths.metaJson);
|
const metaFileExists = await plugins.smartfs
|
||||||
|
.file(this.filePaths.metaJson)
|
||||||
|
.exists();
|
||||||
if (!metaFileExists) {
|
if (!metaFileExists) {
|
||||||
throw new Error(`meta file does not exist at ${this.filePaths.metaJson}`);
|
throw new Error(`meta file does not exist at ${this.filePaths.metaJson}`);
|
||||||
}
|
}
|
||||||
this.metaRepoData = plugins.smartfile.fs.toObjectSync(this.filePaths.metaJson);
|
const content = (await plugins.smartfs
|
||||||
|
.file(this.filePaths.metaJson)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
this.metaRepoData = JSON.parse(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,12 +80,15 @@ export class Meta {
|
|||||||
*/
|
*/
|
||||||
public async writeToDisk() {
|
public async writeToDisk() {
|
||||||
// write .meta.json to disk
|
// write .meta.json to disk
|
||||||
plugins.smartfile.memory.toFsSync(
|
await plugins.smartfs
|
||||||
JSON.stringify(this.metaRepoData, null, 2),
|
.file(this.filePaths.metaJson)
|
||||||
this.filePaths.metaJson,
|
.encoding('utf8')
|
||||||
);
|
.write(JSON.stringify(this.metaRepoData, null, 2));
|
||||||
// write .gitignore to disk
|
// write .gitignore to disk
|
||||||
plugins.smartfile.memory.toFsSync(await this.generateGitignore(), this.filePaths.gitIgnore);
|
await plugins.smartfs
|
||||||
|
.file(this.filePaths.gitIgnore)
|
||||||
|
.encoding('utf8')
|
||||||
|
.write(await this.generateGitignore());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,13 +96,17 @@ export class Meta {
|
|||||||
*/
|
*/
|
||||||
public async syncToRemote(gitCleanArg = false) {
|
public async syncToRemote(gitCleanArg = false) {
|
||||||
logger.log('info', `syncing from origin master`);
|
logger.log('info', `syncing from origin master`);
|
||||||
await this.smartshellInstance.exec(`cd ${this.cwd} && git pull origin master`);
|
await this.smartshellInstance.exec(
|
||||||
|
`cd ${this.cwd} && git pull origin master`,
|
||||||
|
);
|
||||||
if (gitCleanArg) {
|
if (gitCleanArg) {
|
||||||
logger.log('info', `cleaning the repository from old directories`);
|
logger.log('info', `cleaning the repository from old directories`);
|
||||||
await this.smartshellInstance.exec(`cd ${this.cwd} && git clean -fd`);
|
await this.smartshellInstance.exec(`cd ${this.cwd} && git clean -fd`);
|
||||||
}
|
}
|
||||||
logger.log('info', `syncing to remote origin master`);
|
logger.log('info', `syncing to remote origin master`);
|
||||||
await this.smartshellInstance.exec(`cd ${this.cwd} && git push origin master`);
|
await this.smartshellInstance.exec(
|
||||||
|
`cd ${this.cwd} && git push origin master`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,8 +114,25 @@ export class Meta {
|
|||||||
*/
|
*/
|
||||||
public async updateLocalRepos() {
|
public async updateLocalRepos() {
|
||||||
await this.syncToRemote();
|
await this.syncToRemote();
|
||||||
const projects = plugins.smartfile.fs.toObjectSync(this.filePaths.metaJson).projects;
|
const metaContent = (await plugins.smartfs
|
||||||
const preExistingFolders = plugins.smartfile.fs.listFoldersSync(this.cwd);
|
.file(this.filePaths.metaJson)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
const projects = JSON.parse(metaContent).projects;
|
||||||
|
const entries = await plugins.smartfs.directory(this.cwd).list();
|
||||||
|
const preExistingFolders: string[] = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
try {
|
||||||
|
const stats = await plugins.smartfs
|
||||||
|
.file(plugins.path.join(this.cwd, entry.path))
|
||||||
|
.stat();
|
||||||
|
if (stats.isDirectory) {
|
||||||
|
preExistingFolders.push(entry.name);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip entries that can't be accessed
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const preExistingFolderArg of preExistingFolders) {
|
for (const preExistingFolderArg of preExistingFolders) {
|
||||||
if (
|
if (
|
||||||
preExistingFolderArg !== '.git' &&
|
preExistingFolderArg !== '.git' &&
|
||||||
@@ -107,14 +140,18 @@ export class Meta {
|
|||||||
projectFolder.startsWith(preExistingFolderArg),
|
projectFolder.startsWith(preExistingFolderArg),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
const response = await plugins.smartinteraction.SmartInteract.getCliConfirmation(
|
const response =
|
||||||
`Do you want to delete superfluous directory >>${preExistingFolderArg}<< ?`,
|
await plugins.smartinteraction.SmartInteract.getCliConfirmation(
|
||||||
true,
|
`Do you want to delete superfluous directory >>${preExistingFolderArg}<< ?`,
|
||||||
);
|
true,
|
||||||
|
);
|
||||||
if (response) {
|
if (response) {
|
||||||
logger.log('warn', `Deleting >>${preExistingFolderArg}<<!`);
|
logger.log('warn', `Deleting >>${preExistingFolderArg}<<!`);
|
||||||
} else {
|
} else {
|
||||||
logger.log('warn', `Not deleting ${preExistingFolderArg} by request!`);
|
logger.log(
|
||||||
|
'warn',
|
||||||
|
`Not deleting ${preExistingFolderArg} by request!`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,9 +160,17 @@ export class Meta {
|
|||||||
await this.sortMetaRepoData();
|
await this.sortMetaRepoData();
|
||||||
const missingRepos: string[] = [];
|
const missingRepos: string[] = [];
|
||||||
for (const key of Object.keys(this.metaRepoData.projects)) {
|
for (const key of Object.keys(this.metaRepoData.projects)) {
|
||||||
plugins.smartfile.fs.isDirectory(key)
|
const fullPath = plugins.path.join(this.cwd, key);
|
||||||
? logger.log('ok', `${key} -> is already cloned`)
|
try {
|
||||||
: missingRepos.push(key);
|
const stats = await plugins.smartfs.file(fullPath).stat();
|
||||||
|
if (stats.isDirectory) {
|
||||||
|
logger.log('ok', `${key} -> is already cloned`);
|
||||||
|
} else {
|
||||||
|
missingRepos.push(key);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
missingRepos.push(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('info', `found ${missingRepos.length} missing repos`);
|
logger.log('info', `found ${missingRepos.length} missing repos`);
|
||||||
@@ -145,7 +190,20 @@ export class Meta {
|
|||||||
await this.syncToRemote();
|
await this.syncToRemote();
|
||||||
|
|
||||||
// go recursive
|
// go recursive
|
||||||
const folders = await plugins.smartfile.fs.listFolders(this.cwd);
|
const listEntries = await plugins.smartfs.directory(this.cwd).list();
|
||||||
|
const folders: string[] = [];
|
||||||
|
for (const entry of listEntries) {
|
||||||
|
try {
|
||||||
|
const stats = await plugins.smartfs
|
||||||
|
.file(plugins.path.join(this.cwd, entry.path))
|
||||||
|
.stat();
|
||||||
|
if (stats.isDirectory) {
|
||||||
|
folders.push(entry.name);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip entries that can't be accessed
|
||||||
|
}
|
||||||
|
}
|
||||||
const childMetaRepositories: string[] = [];
|
const childMetaRepositories: string[] = [];
|
||||||
for (const folder of folders) {
|
for (const folder of folders) {
|
||||||
logger.log('info', folder);
|
logger.log('info', folder);
|
||||||
@@ -160,25 +218,40 @@ export class Meta {
|
|||||||
*/
|
*/
|
||||||
public async initProject() {
|
public async initProject() {
|
||||||
await this.syncToRemote(true);
|
await this.syncToRemote(true);
|
||||||
const fileExists = await plugins.smartfile.fs.fileExists(this.filePaths.metaJson);
|
const fileExists = await plugins.smartfs
|
||||||
|
.file(this.filePaths.metaJson)
|
||||||
|
.exists();
|
||||||
if (!fileExists) {
|
if (!fileExists) {
|
||||||
await plugins.smartfile.memory.toFs(
|
await plugins.smartfs
|
||||||
JSON.stringify({
|
.file(this.filePaths.metaJson)
|
||||||
projects: {},
|
.encoding('utf8')
|
||||||
}),
|
.write(
|
||||||
this.filePaths.metaJson,
|
JSON.stringify({
|
||||||
|
projects: {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
`success`,
|
||||||
|
`created a new .meta.json in directory ${this.cwd}`,
|
||||||
);
|
);
|
||||||
logger.log(`success`, `created a new .meta.json in directory ${this.cwd}`);
|
await plugins.smartfs
|
||||||
await plugins.smartfile.memory.toFs(
|
.file(this.filePaths.packageJson)
|
||||||
JSON.stringify({
|
.encoding('utf8')
|
||||||
name: this.dirName,
|
.write(
|
||||||
version: '1.0.0',
|
JSON.stringify({
|
||||||
}),
|
name: this.dirName,
|
||||||
this.filePaths.packageJson,
|
version: '1.0.0',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
`success`,
|
||||||
|
`created a new package.json in directory ${this.cwd}`,
|
||||||
);
|
);
|
||||||
logger.log(`success`, `created a new package.json in directory ${this.cwd}`);
|
|
||||||
} else {
|
} else {
|
||||||
logger.log(`error`, `directory ${this.cwd} already has a .metaJson file. Doing nothing.`);
|
logger.log(
|
||||||
|
`error`,
|
||||||
|
`directory ${this.cwd} already has a .metaJson file. Doing nothing.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await this.smartshellInstance.exec(
|
await this.smartshellInstance.exec(
|
||||||
`cd ${this.cwd} && git add -A && git commit -m "feat(project): init meta project for ${this.dirName}"`,
|
`cd ${this.cwd} && git add -A && git commit -m "feat(project): init meta project for ${this.dirName}"`,
|
||||||
@@ -195,7 +268,9 @@ export class Meta {
|
|||||||
const existingProject = this.metaRepoData.projects[projectNameArg];
|
const existingProject = this.metaRepoData.projects[projectNameArg];
|
||||||
|
|
||||||
if (existingProject) {
|
if (existingProject) {
|
||||||
throw new Error('Project already exists! Please remove it first before adding it again.');
|
throw new Error(
|
||||||
|
'Project already exists! Please remove it first before adding it again.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.metaRepoData.projects[projectNameArg] = gitUrlArg;
|
this.metaRepoData.projects[projectNameArg] = gitUrlArg;
|
||||||
@@ -217,7 +292,10 @@ export class Meta {
|
|||||||
const existingProject = this.metaRepoData.projects[projectNameArg];
|
const existingProject = this.metaRepoData.projects[projectNameArg];
|
||||||
|
|
||||||
if (!existingProject) {
|
if (!existingProject) {
|
||||||
logger.log('error', `Project ${projectNameArg} does not exist! So it cannot be removed`);
|
logger.log(
|
||||||
|
'error',
|
||||||
|
`Project ${projectNameArg} does not exist! So it cannot be removed`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +306,10 @@ export class Meta {
|
|||||||
await this.writeToDisk();
|
await this.writeToDisk();
|
||||||
|
|
||||||
logger.log('info', 'removing directory from cwd');
|
logger.log('info', 'removing directory from cwd');
|
||||||
await plugins.smartfile.fs.remove(plugins.path.join(paths.cwd, projectNameArg));
|
await plugins.smartfs
|
||||||
|
.directory(plugins.path.join(paths.cwd, projectNameArg))
|
||||||
|
.recursive()
|
||||||
|
.delete();
|
||||||
await this.updateLocalRepos();
|
await this.updateLocalRepos();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,393 @@
|
|||||||
|
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, newVersion)));
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
newVersion: string,
|
||||||
|
): Promise<ITargetResult[]> {
|
||||||
|
if (!workflow.dockerEnabled) {
|
||||||
|
return [{ target: "docker", status: "skipped", message: "disabled" }];
|
||||||
|
}
|
||||||
|
if (workflow.dockerImages.length === 0) {
|
||||||
|
return [{ target: "docker", status: "failed", message: "no images configured" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: ITargetResult[] = [];
|
||||||
|
for (const imageTemplate of workflow.dockerImages) {
|
||||||
|
const image = imageTemplate.replaceAll("{{version}}", newVersion);
|
||||||
|
const buildResult = await smartshellInstance.exec(`docker build -t ${shellQuote(image)} .`);
|
||||||
|
if (buildResult.exitCode !== 0) {
|
||||||
|
results.push({ target: image, status: "failed", message: "docker build failed" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const pushResult = await smartshellInstance.exec(`docker push ${shellQuote(image)}`);
|
||||||
|
results.push({
|
||||||
|
target: image,
|
||||||
|
status: pushResult.exitCode === 0 ? "success" : "failed",
|
||||||
|
message: pushResult.exitCode === 0 ? undefined : "docker push failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 images: ${workflow.dockerImages.length > 0 ? workflow.dockerImages.join(", ") : "none"}`);
|
||||||
|
}
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Docker 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 Docker 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 };
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
import * as plugins from './mod.plugins.js';
|
||||||
|
import * as helpers from './helpers.js';
|
||||||
|
import { logger } from '../gitzone.logging.js';
|
||||||
|
|
||||||
|
export type ContainerStatus = 'running' | 'stopped' | 'not_exists';
|
||||||
|
|
||||||
|
export interface IDockerRunOptions {
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
ports?: { [key: string]: string };
|
||||||
|
volumes?: { [key: string]: string };
|
||||||
|
environment?: { [key: string]: string };
|
||||||
|
restart?: string;
|
||||||
|
command?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DockerContainer {
|
||||||
|
private smartshell: plugins.smartshell.Smartshell;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.smartshell = new plugins.smartshell.Smartshell({
|
||||||
|
executor: 'bash',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Docker is installed and available
|
||||||
|
*/
|
||||||
|
public async checkDocker(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await this.smartshell.exec('docker --version');
|
||||||
|
return result.exitCode === 0;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get container status
|
||||||
|
*/
|
||||||
|
public async getStatus(containerName: string): Promise<ContainerStatus> {
|
||||||
|
try {
|
||||||
|
// Check if running
|
||||||
|
const runningResult = await this.smartshell.exec(
|
||||||
|
`docker ps --format '{{.Names}}' | grep -q "^${containerName}$"`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (runningResult.exitCode === 0) {
|
||||||
|
return 'running';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if exists but stopped
|
||||||
|
const existsResult = await this.smartshell.exec(
|
||||||
|
`docker ps -a --format '{{.Names}}' | grep -q "^${containerName}$"`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existsResult.exitCode === 0) {
|
||||||
|
return 'stopped';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'not_exists';
|
||||||
|
} catch (error) {
|
||||||
|
return 'not_exists';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a container
|
||||||
|
*/
|
||||||
|
public async start(containerName: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await this.smartshell.exec(`docker start ${containerName}`);
|
||||||
|
return result.exitCode === 0;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop a container
|
||||||
|
*/
|
||||||
|
public async stop(containerName: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await this.smartshell.exec(`docker stop ${containerName}`);
|
||||||
|
return result.exitCode === 0;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a container
|
||||||
|
*/
|
||||||
|
public async remove(containerName: string, force: boolean = false): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const forceFlag = force ? '-f' : '';
|
||||||
|
const result = await this.smartshell.exec(`docker rm ${forceFlag} ${containerName}`);
|
||||||
|
return result.exitCode === 0;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a new container
|
||||||
|
*/
|
||||||
|
public async run(options: IDockerRunOptions): Promise<boolean> {
|
||||||
|
let command = 'docker run -d';
|
||||||
|
|
||||||
|
// Add name
|
||||||
|
command += ` --name ${options.name}`;
|
||||||
|
|
||||||
|
// Add ports
|
||||||
|
if (options.ports) {
|
||||||
|
for (const [hostPort, containerPort] of Object.entries(options.ports)) {
|
||||||
|
command += ` -p ${hostPort}:${containerPort}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add volumes
|
||||||
|
if (options.volumes) {
|
||||||
|
for (const [hostPath, containerPath] of Object.entries(options.volumes)) {
|
||||||
|
command += ` -v "${hostPath}:${containerPath}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add environment variables
|
||||||
|
if (options.environment) {
|
||||||
|
for (const [key, value] of Object.entries(options.environment)) {
|
||||||
|
command += ` -e ${key}="${value}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add restart policy
|
||||||
|
if (options.restart) {
|
||||||
|
command += ` --restart ${options.restart}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add image
|
||||||
|
command += ` ${options.image}`;
|
||||||
|
|
||||||
|
// Add command if provided
|
||||||
|
if (options.command) {
|
||||||
|
command += ` ${options.command}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.smartshell.exec(command);
|
||||||
|
return result.exitCode === 0;
|
||||||
|
} catch (error) {
|
||||||
|
logger.log('error', `Failed to run container: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a command in a running container
|
||||||
|
*/
|
||||||
|
public async exec(containerName: string, command: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const result = await this.smartshell.exec(`docker exec ${containerName} ${command}`);
|
||||||
|
if (result.exitCode === 0) {
|
||||||
|
return result.stdout;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
} catch (error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get container logs
|
||||||
|
*/
|
||||||
|
public async logs(containerName: string, lines?: number): Promise<string> {
|
||||||
|
try {
|
||||||
|
const tailFlag = lines ? `--tail ${lines}` : '';
|
||||||
|
const result = await this.smartshell.exec(`docker logs ${tailFlag} ${containerName}`);
|
||||||
|
return result.stdout;
|
||||||
|
} catch (error) {
|
||||||
|
return `Error getting logs: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a container exists
|
||||||
|
*/
|
||||||
|
public async exists(containerName: string): Promise<boolean> {
|
||||||
|
const status = await this.getStatus(containerName);
|
||||||
|
return status !== 'not_exists';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a container is running
|
||||||
|
*/
|
||||||
|
public async isRunning(containerName: string): Promise<boolean> {
|
||||||
|
const status = await this.getStatus(containerName);
|
||||||
|
return status === 'running';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a container to be ready
|
||||||
|
*/
|
||||||
|
public async waitForReady(containerName: string, maxAttempts: number = 30): Promise<boolean> {
|
||||||
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
|
if (await this.isRunning(containerName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await plugins.smartdelay.delayFor(1000);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get container information
|
||||||
|
*/
|
||||||
|
public async inspect(containerName: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const result = await this.smartshell.execSilent(`docker inspect ${containerName}`);
|
||||||
|
if (result.exitCode === 0) {
|
||||||
|
return JSON.parse(result.stdout);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get port mappings for a container
|
||||||
|
*/
|
||||||
|
public async getPortMappings(containerName: string): Promise<{ [key: string]: string } | null> {
|
||||||
|
try {
|
||||||
|
// Use docker inspect without format to get full JSON, then extract PortBindings
|
||||||
|
const result = await this.smartshell.execSilent(`docker inspect ${containerName}`);
|
||||||
|
|
||||||
|
if (result.exitCode === 0 && result.stdout) {
|
||||||
|
const inspectData = JSON.parse(result.stdout);
|
||||||
|
if (inspectData && inspectData[0] && inspectData[0].HostConfig && inspectData[0].HostConfig.PortBindings) {
|
||||||
|
const portBindings = inspectData[0].HostConfig.PortBindings;
|
||||||
|
const mappings: { [key: string]: string } = {};
|
||||||
|
|
||||||
|
// Convert Docker's port binding format to simple host:container mapping
|
||||||
|
for (const [containerPort, hostBindings] of Object.entries(portBindings)) {
|
||||||
|
if (Array.isArray(hostBindings) && hostBindings.length > 0) {
|
||||||
|
const hostPort = (hostBindings[0] as any).HostPort;
|
||||||
|
if (hostPort) {
|
||||||
|
mappings[containerPort.replace('/tcp', '').replace('/udp', '')] = hostPort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
// Silently fail - container might not exist
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { DockerContainer } from './classes.dockercontainer.js';
|
||||||
|
import { logger } from '../gitzone.logging.js';
|
||||||
|
|
||||||
|
export interface IRegisteredProject {
|
||||||
|
projectPath: string;
|
||||||
|
projectName: string;
|
||||||
|
containers: {
|
||||||
|
mongo?: string;
|
||||||
|
minio?: string;
|
||||||
|
elasticsearch?: string;
|
||||||
|
};
|
||||||
|
ports: {
|
||||||
|
mongo?: number;
|
||||||
|
s3?: number;
|
||||||
|
s3Console?: number;
|
||||||
|
elasticsearch?: number;
|
||||||
|
};
|
||||||
|
enabledServices: string[];
|
||||||
|
lastActive: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGlobalRegistryData {
|
||||||
|
projects: { [projectPath: string]: IRegisteredProject };
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GlobalRegistry {
|
||||||
|
private static instance: GlobalRegistry | null = null;
|
||||||
|
private kvStore: plugins.smartconfig.KeyValueStore<IGlobalRegistryData>;
|
||||||
|
private docker: DockerContainer;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.kvStore = new plugins.smartconfig.KeyValueStore({
|
||||||
|
typeArg: 'userHomeDir',
|
||||||
|
identityArg: 'gitzone-services',
|
||||||
|
});
|
||||||
|
this.docker = new DockerContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the singleton instance
|
||||||
|
*/
|
||||||
|
public static getInstance(): GlobalRegistry {
|
||||||
|
if (!GlobalRegistry.instance) {
|
||||||
|
GlobalRegistry.instance = new GlobalRegistry();
|
||||||
|
}
|
||||||
|
return GlobalRegistry.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register or update a project in the global registry
|
||||||
|
*/
|
||||||
|
public async registerProject(data: Omit<IRegisteredProject, 'lastActive'>): Promise<void> {
|
||||||
|
const allData = await this.kvStore.readAll();
|
||||||
|
const projects = allData.projects || {};
|
||||||
|
|
||||||
|
projects[data.projectPath] = {
|
||||||
|
...data,
|
||||||
|
lastActive: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.kvStore.writeKey('projects', projects);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a project from the registry
|
||||||
|
*/
|
||||||
|
public async unregisterProject(projectPath: string): Promise<void> {
|
||||||
|
const allData = await this.kvStore.readAll();
|
||||||
|
const projects = allData.projects || {};
|
||||||
|
|
||||||
|
if (projects[projectPath]) {
|
||||||
|
delete projects[projectPath];
|
||||||
|
await this.kvStore.writeKey('projects', projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the lastActive timestamp for a project
|
||||||
|
*/
|
||||||
|
public async touchProject(projectPath: string): Promise<void> {
|
||||||
|
const allData = await this.kvStore.readAll();
|
||||||
|
const projects = allData.projects || {};
|
||||||
|
|
||||||
|
if (projects[projectPath]) {
|
||||||
|
projects[projectPath].lastActive = Date.now();
|
||||||
|
await this.kvStore.writeKey('projects', projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered projects
|
||||||
|
*/
|
||||||
|
public async getAllProjects(): Promise<{ [path: string]: IRegisteredProject }> {
|
||||||
|
const allData = await this.kvStore.readAll();
|
||||||
|
return allData.projects || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a project is registered
|
||||||
|
*/
|
||||||
|
public async isRegistered(projectPath: string): Promise<boolean> {
|
||||||
|
const projects = await this.getAllProjects();
|
||||||
|
return !!projects[projectPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status of all containers across all registered projects
|
||||||
|
*/
|
||||||
|
public async getGlobalStatus(): Promise<
|
||||||
|
Array<{
|
||||||
|
projectPath: string;
|
||||||
|
projectName: string;
|
||||||
|
containers: Array<{ name: string; status: string }>;
|
||||||
|
lastActive: number;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const projects = await this.getAllProjects();
|
||||||
|
const result: Array<{
|
||||||
|
projectPath: string;
|
||||||
|
projectName: string;
|
||||||
|
containers: Array<{ name: string; status: string }>;
|
||||||
|
lastActive: number;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const [path, project] of Object.entries(projects)) {
|
||||||
|
const containerStatuses: Array<{ name: string; status: string }> = [];
|
||||||
|
|
||||||
|
for (const containerName of Object.values(project.containers)) {
|
||||||
|
if (containerName) {
|
||||||
|
const status = await this.docker.getStatus(containerName);
|
||||||
|
containerStatuses.push({ name: containerName, status });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
projectPath: path,
|
||||||
|
projectName: project.projectName,
|
||||||
|
containers: containerStatuses,
|
||||||
|
lastActive: project.lastActive,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop all containers across all registered projects
|
||||||
|
*/
|
||||||
|
public async stopAll(): Promise<{ stopped: string[]; failed: string[] }> {
|
||||||
|
const projects = await this.getAllProjects();
|
||||||
|
const stopped: string[] = [];
|
||||||
|
const failed: string[] = [];
|
||||||
|
|
||||||
|
for (const project of Object.values(projects)) {
|
||||||
|
for (const containerName of Object.values(project.containers)) {
|
||||||
|
if (containerName) {
|
||||||
|
const status = await this.docker.getStatus(containerName);
|
||||||
|
if (status === 'running') {
|
||||||
|
if (await this.docker.stop(containerName)) {
|
||||||
|
stopped.push(containerName);
|
||||||
|
} else {
|
||||||
|
failed.push(containerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { stopped, failed };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove stale registry entries (projects that no longer exist on disk)
|
||||||
|
*/
|
||||||
|
public async cleanup(): Promise<string[]> {
|
||||||
|
const projects = await this.getAllProjects();
|
||||||
|
const removed: string[] = [];
|
||||||
|
|
||||||
|
for (const projectPath of Object.keys(projects)) {
|
||||||
|
const exists = await plugins.smartfs.directory(projectPath).exists();
|
||||||
|
if (!exists) {
|
||||||
|
await this.unregisterProject(projectPath);
|
||||||
|
removed.push(projectPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,518 @@
|
|||||||
|
import * as plugins from './mod.plugins.js';
|
||||||
|
import * as helpers from './helpers.js';
|
||||||
|
import { logger } from '../gitzone.logging.js';
|
||||||
|
import { DockerContainer } from './classes.dockercontainer.js';
|
||||||
|
|
||||||
|
export interface IServiceConfig {
|
||||||
|
PROJECT_NAME: string;
|
||||||
|
MONGODB_HOST: string;
|
||||||
|
MONGODB_NAME: string;
|
||||||
|
MONGODB_PORT: string;
|
||||||
|
MONGODB_USER: string;
|
||||||
|
MONGODB_PASS: string;
|
||||||
|
MONGODB_URL: string;
|
||||||
|
S3_HOST: string;
|
||||||
|
S3_PORT: string;
|
||||||
|
S3_CONSOLE_PORT: string;
|
||||||
|
S3_ACCESSKEY: string;
|
||||||
|
S3_SECRETKEY: string;
|
||||||
|
S3_BUCKET: string;
|
||||||
|
S3_ENDPOINT: string;
|
||||||
|
S3_USESSL: boolean;
|
||||||
|
ELASTICSEARCH_HOST: string;
|
||||||
|
ELASTICSEARCH_PORT: string;
|
||||||
|
ELASTICSEARCH_USER: string;
|
||||||
|
ELASTICSEARCH_PASS: string;
|
||||||
|
ELASTICSEARCH_URL: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ServiceConfiguration {
|
||||||
|
private configPath: string;
|
||||||
|
private config: IServiceConfig;
|
||||||
|
private docker: DockerContainer;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.configPath = plugins.path.join(process.cwd(), '.nogit', 'env.json');
|
||||||
|
this.docker = new DockerContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load or create the configuration
|
||||||
|
*/
|
||||||
|
public async loadOrCreate(): Promise<IServiceConfig> {
|
||||||
|
await this.ensureNogitDirectory();
|
||||||
|
|
||||||
|
if (await this.configExists()) {
|
||||||
|
await this.loadConfig();
|
||||||
|
await this.updateMissingFields();
|
||||||
|
} else {
|
||||||
|
await this.createDefaultConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync ports from existing Docker containers if they exist
|
||||||
|
await this.syncPortsFromDocker();
|
||||||
|
|
||||||
|
return this.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current configuration
|
||||||
|
*/
|
||||||
|
public getConfig(): IServiceConfig {
|
||||||
|
return this.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the configuration to file
|
||||||
|
*/
|
||||||
|
public async saveConfig(): Promise<void> {
|
||||||
|
await plugins.smartfs
|
||||||
|
.file(this.configPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.write(JSON.stringify(this.config, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure .nogit directory exists
|
||||||
|
*/
|
||||||
|
private async ensureNogitDirectory(): Promise<void> {
|
||||||
|
const nogitPath = plugins.path.join(process.cwd(), '.nogit');
|
||||||
|
await plugins.smartfs.directory(nogitPath).recursive().create();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if configuration file exists
|
||||||
|
*/
|
||||||
|
private async configExists(): Promise<boolean> {
|
||||||
|
return plugins.smartfs.file(this.configPath).exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load configuration from file
|
||||||
|
*/
|
||||||
|
private async loadConfig(): Promise<void> {
|
||||||
|
const configContent = (await plugins.smartfs
|
||||||
|
.file(this.configPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
this.config = JSON.parse(configContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create default configuration
|
||||||
|
*/
|
||||||
|
private async createDefaultConfig(): Promise<void> {
|
||||||
|
const projectName = await helpers.getProjectName();
|
||||||
|
const mongoPort = await helpers.getRandomAvailablePort();
|
||||||
|
const s3Port = await helpers.getRandomAvailablePort();
|
||||||
|
let s3ConsolePort = s3Port + 1;
|
||||||
|
|
||||||
|
// Ensure console port is also available
|
||||||
|
while (!(await helpers.isPortAvailable(s3ConsolePort))) {
|
||||||
|
s3ConsolePort++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mongoUser = 'defaultadmin';
|
||||||
|
const mongoPass = 'defaultpass';
|
||||||
|
const mongoHost = 'localhost';
|
||||||
|
const mongoName = projectName;
|
||||||
|
const mongoPortStr = mongoPort.toString();
|
||||||
|
const s3Host = 'localhost';
|
||||||
|
const s3PortStr = s3Port.toString();
|
||||||
|
const esHost = 'localhost';
|
||||||
|
const esPort = '9200';
|
||||||
|
const esUser = 'elastic';
|
||||||
|
const esPass = 'elastic';
|
||||||
|
|
||||||
|
this.config = {
|
||||||
|
PROJECT_NAME: projectName,
|
||||||
|
MONGODB_HOST: mongoHost,
|
||||||
|
MONGODB_NAME: mongoName,
|
||||||
|
MONGODB_PORT: mongoPortStr,
|
||||||
|
MONGODB_USER: mongoUser,
|
||||||
|
MONGODB_PASS: mongoPass,
|
||||||
|
MONGODB_URL: `mongodb://${mongoUser}:${mongoPass}@${mongoHost}:${mongoPortStr}/${mongoName}?authSource=admin`,
|
||||||
|
S3_HOST: s3Host,
|
||||||
|
S3_PORT: s3PortStr,
|
||||||
|
S3_CONSOLE_PORT: s3ConsolePort.toString(),
|
||||||
|
S3_ACCESSKEY: 'defaultadmin',
|
||||||
|
S3_SECRETKEY: 'defaultpass',
|
||||||
|
S3_BUCKET: `${projectName}-documents`,
|
||||||
|
S3_ENDPOINT: s3Host,
|
||||||
|
S3_USESSL: false,
|
||||||
|
ELASTICSEARCH_HOST: esHost,
|
||||||
|
ELASTICSEARCH_PORT: esPort,
|
||||||
|
ELASTICSEARCH_USER: esUser,
|
||||||
|
ELASTICSEARCH_PASS: esPass,
|
||||||
|
ELASTICSEARCH_URL: `http://${esUser}:${esPass}@${esHost}:${esPort}`
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.saveConfig();
|
||||||
|
|
||||||
|
logger.log('ok', '✅ Created .nogit/env.json with project defaults');
|
||||||
|
logger.log('info', `📍 MongoDB port: ${mongoPort}`);
|
||||||
|
logger.log('info', `📍 S3 API port: ${s3Port}`);
|
||||||
|
logger.log('info', `📍 S3 Console port: ${s3ConsolePort}`);
|
||||||
|
logger.log('info', `📍 Elasticsearch port: ${esPort}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update missing fields in existing configuration
|
||||||
|
*/
|
||||||
|
private async updateMissingFields(): Promise<void> {
|
||||||
|
const projectName = await helpers.getProjectName();
|
||||||
|
let updated = false;
|
||||||
|
const fieldsAdded: string[] = [];
|
||||||
|
|
||||||
|
// Check and add missing fields
|
||||||
|
if (!this.config.PROJECT_NAME) {
|
||||||
|
this.config.PROJECT_NAME = projectName;
|
||||||
|
fieldsAdded.push('PROJECT_NAME');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.MONGODB_HOST) {
|
||||||
|
this.config.MONGODB_HOST = 'localhost';
|
||||||
|
fieldsAdded.push('MONGODB_HOST');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.MONGODB_NAME) {
|
||||||
|
this.config.MONGODB_NAME = projectName;
|
||||||
|
fieldsAdded.push('MONGODB_NAME');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.MONGODB_PORT) {
|
||||||
|
const port = await helpers.getRandomAvailablePort();
|
||||||
|
this.config.MONGODB_PORT = port.toString();
|
||||||
|
fieldsAdded.push(`MONGODB_PORT(${port})`);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.MONGODB_USER) {
|
||||||
|
this.config.MONGODB_USER = 'defaultadmin';
|
||||||
|
fieldsAdded.push('MONGODB_USER');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.MONGODB_PASS) {
|
||||||
|
this.config.MONGODB_PASS = 'defaultpass';
|
||||||
|
fieldsAdded.push('MONGODB_PASS');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always update MONGODB_URL based on current settings
|
||||||
|
const oldUrl = this.config.MONGODB_URL;
|
||||||
|
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
|
||||||
|
if (oldUrl !== this.config.MONGODB_URL) {
|
||||||
|
fieldsAdded.push('MONGODB_URL');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.S3_HOST) {
|
||||||
|
this.config.S3_HOST = 'localhost';
|
||||||
|
fieldsAdded.push('S3_HOST');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.S3_PORT) {
|
||||||
|
const port = await helpers.getRandomAvailablePort();
|
||||||
|
this.config.S3_PORT = port.toString();
|
||||||
|
fieldsAdded.push(`S3_PORT(${port})`);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.S3_CONSOLE_PORT) {
|
||||||
|
const s3Port = parseInt(this.config.S3_PORT);
|
||||||
|
let consolePort = s3Port + 1;
|
||||||
|
|
||||||
|
while (!(await helpers.isPortAvailable(consolePort))) {
|
||||||
|
consolePort++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config.S3_CONSOLE_PORT = consolePort.toString();
|
||||||
|
fieldsAdded.push(`S3_CONSOLE_PORT(${consolePort})`);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.S3_ACCESSKEY) {
|
||||||
|
this.config.S3_ACCESSKEY = 'defaultadmin';
|
||||||
|
fieldsAdded.push('S3_ACCESSKEY');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.S3_SECRETKEY) {
|
||||||
|
this.config.S3_SECRETKEY = 'defaultpass';
|
||||||
|
fieldsAdded.push('S3_SECRETKEY');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.S3_BUCKET) {
|
||||||
|
this.config.S3_BUCKET = `${projectName}-documents`;
|
||||||
|
fieldsAdded.push('S3_BUCKET');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.S3_USESSL) {
|
||||||
|
this.config.S3_USESSL = false;
|
||||||
|
fieldsAdded.push('S3_USESSL');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always update S3_ENDPOINT based on current settings
|
||||||
|
const oldEndpoint = this.config.S3_ENDPOINT;
|
||||||
|
this.config.S3_ENDPOINT = this.config.S3_HOST;
|
||||||
|
if (oldEndpoint !== this.config.S3_ENDPOINT) {
|
||||||
|
fieldsAdded.push('S3_ENDPOINT');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.ELASTICSEARCH_HOST) {
|
||||||
|
this.config.ELASTICSEARCH_HOST = 'localhost';
|
||||||
|
fieldsAdded.push('ELASTICSEARCH_HOST');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.ELASTICSEARCH_PORT) {
|
||||||
|
this.config.ELASTICSEARCH_PORT = '9200';
|
||||||
|
fieldsAdded.push('ELASTICSEARCH_PORT');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.ELASTICSEARCH_USER) {
|
||||||
|
this.config.ELASTICSEARCH_USER = 'elastic';
|
||||||
|
fieldsAdded.push('ELASTICSEARCH_USER');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.ELASTICSEARCH_PASS) {
|
||||||
|
this.config.ELASTICSEARCH_PASS = 'elastic';
|
||||||
|
fieldsAdded.push('ELASTICSEARCH_PASS');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always update ELASTICSEARCH_URL based on current settings
|
||||||
|
const oldEsUrl = this.config.ELASTICSEARCH_URL;
|
||||||
|
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
|
||||||
|
if (oldEsUrl !== this.config.ELASTICSEARCH_URL) {
|
||||||
|
fieldsAdded.push('ELASTICSEARCH_URL');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
await this.saveConfig();
|
||||||
|
logger.log('ok', `✅ Added missing fields: ${fieldsAdded.join(', ')}`);
|
||||||
|
} else {
|
||||||
|
logger.log('ok', '✅ Configuration complete');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get MongoDB connection string
|
||||||
|
*/
|
||||||
|
public getMongoConnectionString(useNetworkIp: boolean = false): string {
|
||||||
|
const host = useNetworkIp ? '${networkIp}' : this.config.MONGODB_HOST;
|
||||||
|
return `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${host}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get container names
|
||||||
|
*/
|
||||||
|
public getContainerNames() {
|
||||||
|
return {
|
||||||
|
mongo: `${this.config.PROJECT_NAME}-mongodb`,
|
||||||
|
minio: `${this.config.PROJECT_NAME}-minio`,
|
||||||
|
elasticsearch: `${this.config.PROJECT_NAME}-elasticsearch`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data directories
|
||||||
|
*/
|
||||||
|
public getDataDirectories() {
|
||||||
|
return {
|
||||||
|
mongo: plugins.path.join(process.cwd(), '.nogit', 'mongodata'),
|
||||||
|
minio: plugins.path.join(process.cwd(), '.nogit', 'miniodata'),
|
||||||
|
elasticsearch: plugins.path.join(process.cwd(), '.nogit', 'esdata')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync port configuration from existing Docker containers
|
||||||
|
*/
|
||||||
|
private async syncPortsFromDocker(): Promise<void> {
|
||||||
|
const containers = this.getContainerNames();
|
||||||
|
let updated = false;
|
||||||
|
|
||||||
|
// Check MongoDB container
|
||||||
|
const mongoStatus = await this.docker.getStatus(containers.mongo);
|
||||||
|
if (mongoStatus !== 'not_exists') {
|
||||||
|
const portMappings = await this.docker.getPortMappings(containers.mongo);
|
||||||
|
if (portMappings && portMappings['27017']) {
|
||||||
|
const dockerPort = portMappings['27017'];
|
||||||
|
if (this.config.MONGODB_PORT !== dockerPort) {
|
||||||
|
logger.log('note', `📍 Syncing MongoDB port from Docker: ${dockerPort}`);
|
||||||
|
this.config.MONGODB_PORT = dockerPort;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check MinIO container
|
||||||
|
const minioStatus = await this.docker.getStatus(containers.minio);
|
||||||
|
if (minioStatus !== 'not_exists') {
|
||||||
|
const portMappings = await this.docker.getPortMappings(containers.minio);
|
||||||
|
if (portMappings) {
|
||||||
|
if (portMappings['9000']) {
|
||||||
|
const dockerPort = portMappings['9000'];
|
||||||
|
if (this.config.S3_PORT !== dockerPort) {
|
||||||
|
logger.log('note', `📍 Syncing S3 API port from Docker: ${dockerPort}`);
|
||||||
|
this.config.S3_PORT = dockerPort;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (portMappings['9001']) {
|
||||||
|
const dockerPort = portMappings['9001'];
|
||||||
|
if (this.config.S3_CONSOLE_PORT !== dockerPort) {
|
||||||
|
logger.log('note', `📍 Syncing S3 Console port from Docker: ${dockerPort}`);
|
||||||
|
this.config.S3_CONSOLE_PORT = dockerPort;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Elasticsearch container
|
||||||
|
const esStatus = await this.docker.getStatus(containers.elasticsearch);
|
||||||
|
if (esStatus !== 'not_exists') {
|
||||||
|
const portMappings = await this.docker.getPortMappings(containers.elasticsearch);
|
||||||
|
if (portMappings && portMappings['9200']) {
|
||||||
|
const dockerPort = portMappings['9200'];
|
||||||
|
if (this.config.ELASTICSEARCH_PORT !== dockerPort) {
|
||||||
|
logger.log('note', `📍 Syncing Elasticsearch port from Docker: ${dockerPort}`);
|
||||||
|
this.config.ELASTICSEARCH_PORT = dockerPort;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
// Update derived fields
|
||||||
|
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
|
||||||
|
this.config.S3_ENDPOINT = this.config.S3_HOST;
|
||||||
|
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
|
||||||
|
|
||||||
|
await this.saveConfig();
|
||||||
|
logger.log('ok', '✅ Configuration synced with Docker containers');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and update ports if they're not available
|
||||||
|
*/
|
||||||
|
public async validateAndUpdatePorts(): Promise<boolean> {
|
||||||
|
let updated = false;
|
||||||
|
const containers = this.getContainerNames();
|
||||||
|
|
||||||
|
// Check if containers exist - if they do, ports are fine
|
||||||
|
const mongoExists = await this.docker.exists(containers.mongo);
|
||||||
|
const minioExists = await this.docker.exists(containers.minio);
|
||||||
|
const esExists = await this.docker.exists(containers.elasticsearch);
|
||||||
|
|
||||||
|
// Only check port availability if containers don't exist
|
||||||
|
if (!mongoExists) {
|
||||||
|
const mongoPort = parseInt(this.config.MONGODB_PORT);
|
||||||
|
if (!(await helpers.isPortAvailable(mongoPort))) {
|
||||||
|
logger.log('note', `⚠️ MongoDB port ${mongoPort} is in use, finding new port...`);
|
||||||
|
const newPort = await helpers.getRandomAvailablePort();
|
||||||
|
this.config.MONGODB_PORT = newPort.toString();
|
||||||
|
logger.log('ok', `✅ New MongoDB port: ${newPort}`);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!minioExists) {
|
||||||
|
const s3Port = parseInt(this.config.S3_PORT);
|
||||||
|
const s3ConsolePort = parseInt(this.config.S3_CONSOLE_PORT);
|
||||||
|
|
||||||
|
if (!(await helpers.isPortAvailable(s3Port))) {
|
||||||
|
logger.log('note', `⚠️ S3 API port ${s3Port} is in use, finding new port...`);
|
||||||
|
const newPort = await helpers.getRandomAvailablePort();
|
||||||
|
this.config.S3_PORT = newPort.toString();
|
||||||
|
logger.log('ok', `✅ New S3 API port: ${newPort}`);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await helpers.isPortAvailable(s3ConsolePort))) {
|
||||||
|
logger.log('note', `⚠️ S3 Console port ${s3ConsolePort} is in use, finding new port...`);
|
||||||
|
let newPort = parseInt(this.config.S3_PORT) + 1;
|
||||||
|
while (!(await helpers.isPortAvailable(newPort))) {
|
||||||
|
newPort++;
|
||||||
|
}
|
||||||
|
this.config.S3_CONSOLE_PORT = newPort.toString();
|
||||||
|
logger.log('ok', `✅ New S3 Console port: ${newPort}`);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!esExists) {
|
||||||
|
const esPort = parseInt(this.config.ELASTICSEARCH_PORT);
|
||||||
|
if (!(await helpers.isPortAvailable(esPort))) {
|
||||||
|
logger.log('note', `⚠️ Elasticsearch port ${esPort} is in use, finding new port...`);
|
||||||
|
const newPort = await helpers.getRandomAvailablePort();
|
||||||
|
this.config.ELASTICSEARCH_PORT = newPort.toString();
|
||||||
|
logger.log('ok', `✅ New Elasticsearch port: ${newPort}`);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
// Update derived fields
|
||||||
|
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
|
||||||
|
this.config.S3_ENDPOINT = this.config.S3_HOST;
|
||||||
|
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
|
||||||
|
|
||||||
|
await this.saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force reconfigure all ports with new available ones
|
||||||
|
*/
|
||||||
|
public async reconfigurePorts(): Promise<void> {
|
||||||
|
logger.log('note', '🔄 Finding new available ports...');
|
||||||
|
|
||||||
|
const mongoPort = await helpers.getRandomAvailablePort();
|
||||||
|
const s3Port = await helpers.getRandomAvailablePort();
|
||||||
|
let s3ConsolePort = s3Port + 1;
|
||||||
|
|
||||||
|
// Ensure console port is also available
|
||||||
|
while (!(await helpers.isPortAvailable(s3ConsolePort))) {
|
||||||
|
s3ConsolePort++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elasticsearch uses standard port 9200
|
||||||
|
const esPort = '9200';
|
||||||
|
|
||||||
|
this.config.MONGODB_PORT = mongoPort.toString();
|
||||||
|
this.config.S3_PORT = s3Port.toString();
|
||||||
|
this.config.S3_CONSOLE_PORT = s3ConsolePort.toString();
|
||||||
|
this.config.ELASTICSEARCH_PORT = esPort;
|
||||||
|
|
||||||
|
// Update derived fields
|
||||||
|
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
|
||||||
|
this.config.S3_ENDPOINT = this.config.S3_HOST;
|
||||||
|
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
|
||||||
|
|
||||||
|
await this.saveConfig();
|
||||||
|
|
||||||
|
logger.log('ok', '✅ New port configuration:');
|
||||||
|
logger.log('info', ` 📍 MongoDB: ${mongoPort}`);
|
||||||
|
logger.log('info', ` 📍 S3 API: ${s3Port}`);
|
||||||
|
logger.log('info', ` 📍 S3 Console: ${s3ConsolePort}`);
|
||||||
|
logger.log('info', ` 📍 Elasticsearch: ${esPort}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,956 @@
|
|||||||
|
import * as plugins from './mod.plugins.js';
|
||||||
|
import * as helpers from './helpers.js';
|
||||||
|
import { ServiceConfiguration } from './classes.serviceconfiguration.js';
|
||||||
|
import { DockerContainer } from './classes.dockercontainer.js';
|
||||||
|
import { GlobalRegistry } from './classes.globalregistry.js';
|
||||||
|
import { logger } from '../gitzone.logging.js';
|
||||||
|
|
||||||
|
export class ServiceManager {
|
||||||
|
private config: ServiceConfiguration;
|
||||||
|
private docker: DockerContainer;
|
||||||
|
private enabledServices: string[] | null = null;
|
||||||
|
private globalRegistry: GlobalRegistry;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.config = new ServiceConfiguration();
|
||||||
|
this.docker = new DockerContainer();
|
||||||
|
this.globalRegistry = GlobalRegistry.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the service manager
|
||||||
|
*/
|
||||||
|
public async init(): Promise<void> {
|
||||||
|
// Check Docker availability
|
||||||
|
if (!(await this.docker.checkDocker())) {
|
||||||
|
logger.log('error', 'Error: Docker is not installed. Please install Docker first.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load or create configuration
|
||||||
|
await this.config.loadOrCreate();
|
||||||
|
logger.log('info', `📋 Project: ${this.config.getConfig().PROJECT_NAME}`);
|
||||||
|
|
||||||
|
// Load service selection from .smartconfig.json
|
||||||
|
await this.loadServiceConfiguration();
|
||||||
|
|
||||||
|
// Validate and update ports if needed
|
||||||
|
await this.config.validateAndUpdatePorts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load service configuration from .smartconfig.json
|
||||||
|
*/
|
||||||
|
private async loadServiceConfiguration(): Promise<void> {
|
||||||
|
const smartconfigInstance = new plugins.smartconfig.Smartconfig(process.cwd());
|
||||||
|
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
|
||||||
|
|
||||||
|
// Check if services array exists
|
||||||
|
if (!gitzoneConfig.services || !Array.isArray(gitzoneConfig.services) || gitzoneConfig.services.length === 0) {
|
||||||
|
// Prompt user to select services
|
||||||
|
const smartinteract = new plugins.smartinteract.SmartInteract();
|
||||||
|
const response = await smartinteract.askQuestion({
|
||||||
|
name: 'services',
|
||||||
|
type: 'checkbox',
|
||||||
|
message: 'Which services do you want to enable for this project?',
|
||||||
|
choices: [
|
||||||
|
{ name: 'MongoDB', value: 'mongodb' },
|
||||||
|
{ name: 'MinIO (S3)', value: 'minio' },
|
||||||
|
{ name: 'Elasticsearch', value: 'elasticsearch' }
|
||||||
|
],
|
||||||
|
default: ['mongodb', 'minio', 'elasticsearch']
|
||||||
|
});
|
||||||
|
|
||||||
|
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
|
||||||
|
|
||||||
|
// Save to .smartconfig.json
|
||||||
|
await this.saveServiceConfiguration(this.enabledServices);
|
||||||
|
} else {
|
||||||
|
this.enabledServices = gitzoneConfig.services;
|
||||||
|
logger.log('info', `🔧 Enabled services: ${this.enabledServices.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save service configuration to .smartconfig.json
|
||||||
|
*/
|
||||||
|
private async saveServiceConfiguration(services: string[]): Promise<void> {
|
||||||
|
const smartconfigPath = plugins.path.join(process.cwd(), '.smartconfig.json');
|
||||||
|
let smartconfigData: any = {};
|
||||||
|
|
||||||
|
// Read existing .smartconfig.json if it exists
|
||||||
|
if (await plugins.smartfs.file(smartconfigPath).exists()) {
|
||||||
|
const content = await plugins.smartfs.file(smartconfigPath).encoding('utf8').read();
|
||||||
|
smartconfigData = JSON.parse(content as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update @git.zone/cli.services
|
||||||
|
if (!smartconfigData['@git.zone/cli']) {
|
||||||
|
smartconfigData['@git.zone/cli'] = {};
|
||||||
|
}
|
||||||
|
smartconfigData['@git.zone/cli'].services = services;
|
||||||
|
|
||||||
|
// Write back to .smartconfig.json
|
||||||
|
await plugins.smartfs
|
||||||
|
.file(smartconfigPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.write(JSON.stringify(smartconfigData, null, 2));
|
||||||
|
|
||||||
|
logger.log('ok', `✅ Saved service configuration to .smartconfig.json`);
|
||||||
|
logger.log('info', `🔧 Enabled services: ${services.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a service is enabled
|
||||||
|
*/
|
||||||
|
private isServiceEnabled(service: string): boolean {
|
||||||
|
if (!this.enabledServices) {
|
||||||
|
return true; // If no configuration, enable all
|
||||||
|
}
|
||||||
|
return this.enabledServices.includes(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register this project with the global registry
|
||||||
|
*/
|
||||||
|
private async registerWithGlobalRegistry(): Promise<void> {
|
||||||
|
const config = this.config.getConfig();
|
||||||
|
const containers = this.config.getContainerNames();
|
||||||
|
|
||||||
|
await this.globalRegistry.registerProject({
|
||||||
|
projectPath: process.cwd(),
|
||||||
|
projectName: config.PROJECT_NAME,
|
||||||
|
containers: {
|
||||||
|
mongo: containers.mongo,
|
||||||
|
minio: containers.minio,
|
||||||
|
elasticsearch: containers.elasticsearch,
|
||||||
|
},
|
||||||
|
ports: {
|
||||||
|
mongo: parseInt(config.MONGODB_PORT),
|
||||||
|
s3: parseInt(config.S3_PORT),
|
||||||
|
s3Console: parseInt(config.S3_CONSOLE_PORT),
|
||||||
|
elasticsearch: parseInt(config.ELASTICSEARCH_PORT),
|
||||||
|
},
|
||||||
|
enabledServices: this.enabledServices || ['mongodb', 'minio', 'elasticsearch'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start all enabled services
|
||||||
|
*/
|
||||||
|
public async startAll(): Promise<void> {
|
||||||
|
let first = true;
|
||||||
|
if (this.isServiceEnabled('mongodb')) {
|
||||||
|
if (!first) console.log();
|
||||||
|
await this.startMongoDB();
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
if (this.isServiceEnabled('minio')) {
|
||||||
|
if (!first) console.log();
|
||||||
|
await this.startMinIO();
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
if (this.isServiceEnabled('elasticsearch')) {
|
||||||
|
if (!first) console.log();
|
||||||
|
await this.startElasticsearch();
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register with global registry
|
||||||
|
await this.registerWithGlobalRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop all enabled services
|
||||||
|
*/
|
||||||
|
public async stopAll(): Promise<void> {
|
||||||
|
let first = true;
|
||||||
|
if (this.isServiceEnabled('mongodb')) {
|
||||||
|
if (!first) console.log();
|
||||||
|
await this.stopMongoDB();
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
if (this.isServiceEnabled('minio')) {
|
||||||
|
if (!first) console.log();
|
||||||
|
await this.stopMinIO();
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
if (this.isServiceEnabled('elasticsearch')) {
|
||||||
|
if (!first) console.log();
|
||||||
|
await this.stopElasticsearch();
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start MongoDB service
|
||||||
|
*/
|
||||||
|
public async startMongoDB(): Promise<void> {
|
||||||
|
logger.log('note', '📦 MongoDB:');
|
||||||
|
|
||||||
|
const config = this.config.getConfig();
|
||||||
|
const containers = this.config.getContainerNames();
|
||||||
|
const directories = this.config.getDataDirectories();
|
||||||
|
|
||||||
|
// Ensure data directory exists
|
||||||
|
await plugins.smartfs.directory(directories.mongo).recursive().create();
|
||||||
|
|
||||||
|
const status = await this.docker.getStatus(containers.mongo);
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'running':
|
||||||
|
logger.log('ok', ' Already running ✓');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'stopped':
|
||||||
|
// Check if port mapping matches config
|
||||||
|
const mongoPortMappings = await this.docker.getPortMappings(containers.mongo);
|
||||||
|
if (mongoPortMappings && mongoPortMappings['27017'] !== config.MONGODB_PORT) {
|
||||||
|
logger.log('note', ' Port configuration changed, recreating container...');
|
||||||
|
await this.docker.remove(containers.mongo, true);
|
||||||
|
// Fall through to create new container
|
||||||
|
const success = await this.docker.run({
|
||||||
|
name: containers.mongo,
|
||||||
|
image: 'mongo:7.0',
|
||||||
|
ports: {
|
||||||
|
[`0.0.0.0:${config.MONGODB_PORT}`]: '27017'
|
||||||
|
},
|
||||||
|
volumes: {
|
||||||
|
[directories.mongo]: '/data/db'
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: config.MONGODB_USER,
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: config.MONGODB_PASS,
|
||||||
|
MONGO_INITDB_DATABASE: config.MONGODB_NAME
|
||||||
|
},
|
||||||
|
restart: 'unless-stopped',
|
||||||
|
command: '--bind_ip_all'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logger.log('ok', ' Recreated with new port ✓');
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to recreate container');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ports match, just start the container
|
||||||
|
if (await this.docker.start(containers.mongo)) {
|
||||||
|
logger.log('ok', ' Started ✓');
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to start');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'not_exists':
|
||||||
|
logger.log('note', ' Creating container...');
|
||||||
|
|
||||||
|
const success = await this.docker.run({
|
||||||
|
name: containers.mongo,
|
||||||
|
image: 'mongo:7.0',
|
||||||
|
ports: {
|
||||||
|
[`0.0.0.0:${config.MONGODB_PORT}`]: '27017'
|
||||||
|
},
|
||||||
|
volumes: {
|
||||||
|
[directories.mongo]: '/data/db'
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: config.MONGODB_USER,
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: config.MONGODB_PASS,
|
||||||
|
MONGO_INITDB_DATABASE: config.MONGODB_NAME
|
||||||
|
},
|
||||||
|
restart: 'unless-stopped',
|
||||||
|
command: '--bind_ip_all'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logger.log('ok', ' Created and started ✓');
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to create container');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', ` Container: ${containers.mongo}`);
|
||||||
|
logger.log('info', ` Port: ${config.MONGODB_PORT}`);
|
||||||
|
logger.log('info', ` Connection: ${this.config.getMongoConnectionString()}`);
|
||||||
|
|
||||||
|
// Show Compass connection string
|
||||||
|
const networkIp = await helpers.getLocalNetworkIp();
|
||||||
|
const compassString = `mongodb://${config.MONGODB_USER}:${config.MONGODB_PASS}@${networkIp}:${config.MONGODB_PORT}/${config.MONGODB_NAME}?authSource=admin`;
|
||||||
|
logger.log('ok', ` Compass: ${compassString}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start MinIO service
|
||||||
|
*/
|
||||||
|
public async startMinIO(): Promise<void> {
|
||||||
|
logger.log('note', '📦 S3/MinIO:');
|
||||||
|
|
||||||
|
const config = this.config.getConfig();
|
||||||
|
const containers = this.config.getContainerNames();
|
||||||
|
const directories = this.config.getDataDirectories();
|
||||||
|
|
||||||
|
// Ensure data directory exists
|
||||||
|
await plugins.smartfs.directory(directories.minio).recursive().create();
|
||||||
|
|
||||||
|
const status = await this.docker.getStatus(containers.minio);
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'running':
|
||||||
|
logger.log('ok', ' Already running ✓');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'stopped':
|
||||||
|
// Check if port mapping matches config
|
||||||
|
const minioPortMappings = await this.docker.getPortMappings(containers.minio);
|
||||||
|
if (minioPortMappings &&
|
||||||
|
(minioPortMappings['9000'] !== config.S3_PORT ||
|
||||||
|
minioPortMappings['9001'] !== config.S3_CONSOLE_PORT)) {
|
||||||
|
logger.log('note', ' Port configuration changed, recreating container...');
|
||||||
|
await this.docker.remove(containers.minio, true);
|
||||||
|
// Fall through to create new container
|
||||||
|
const success = await this.docker.run({
|
||||||
|
name: containers.minio,
|
||||||
|
image: 'minio/minio',
|
||||||
|
ports: {
|
||||||
|
[config.S3_PORT]: '9000',
|
||||||
|
[config.S3_CONSOLE_PORT]: '9001'
|
||||||
|
},
|
||||||
|
volumes: {
|
||||||
|
[directories.minio]: '/data'
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
MINIO_ROOT_USER: config.S3_ACCESSKEY,
|
||||||
|
MINIO_ROOT_PASSWORD: config.S3_SECRETKEY
|
||||||
|
},
|
||||||
|
restart: 'unless-stopped',
|
||||||
|
command: 'server /data --console-address ":9001"'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logger.log('ok', ' Recreated with new ports ✓');
|
||||||
|
|
||||||
|
// Wait for MinIO to be ready
|
||||||
|
await plugins.smartdelay.delayFor(3000);
|
||||||
|
|
||||||
|
// Create default bucket
|
||||||
|
await this.docker.exec(
|
||||||
|
containers.minio,
|
||||||
|
`mc alias set local http://localhost:9000 ${config.S3_ACCESSKEY} ${config.S3_SECRETKEY}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.docker.exec(
|
||||||
|
containers.minio,
|
||||||
|
`mc mb local/${config.S3_BUCKET}`
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.log('ok', ` Bucket '${config.S3_BUCKET}' created ✓`);
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to recreate container');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ports match, just start the container
|
||||||
|
if (await this.docker.start(containers.minio)) {
|
||||||
|
logger.log('ok', ' Started ✓');
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to start');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'not_exists':
|
||||||
|
logger.log('note', ' Creating container...');
|
||||||
|
|
||||||
|
const success = await this.docker.run({
|
||||||
|
name: containers.minio,
|
||||||
|
image: 'minio/minio',
|
||||||
|
ports: {
|
||||||
|
[config.S3_PORT]: '9000',
|
||||||
|
[config.S3_CONSOLE_PORT]: '9001'
|
||||||
|
},
|
||||||
|
volumes: {
|
||||||
|
[directories.minio]: '/data'
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
MINIO_ROOT_USER: config.S3_ACCESSKEY,
|
||||||
|
MINIO_ROOT_PASSWORD: config.S3_SECRETKEY
|
||||||
|
},
|
||||||
|
restart: 'unless-stopped',
|
||||||
|
command: 'server /data --console-address ":9001"'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logger.log('ok', ' Created and started ✓');
|
||||||
|
|
||||||
|
// Wait for MinIO to be ready
|
||||||
|
await plugins.smartdelay.delayFor(3000);
|
||||||
|
|
||||||
|
// Create default bucket
|
||||||
|
await this.docker.exec(
|
||||||
|
containers.minio,
|
||||||
|
`mc alias set local http://localhost:9000 ${config.S3_ACCESSKEY} ${config.S3_SECRETKEY}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.docker.exec(
|
||||||
|
containers.minio,
|
||||||
|
`mc mb local/${config.S3_BUCKET}`
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.log('ok', ` Bucket '${config.S3_BUCKET}' created ✓`);
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to create container');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', ` Container: ${containers.minio}`);
|
||||||
|
logger.log('info', ` Port: ${config.S3_PORT}`);
|
||||||
|
logger.log('info', ` Bucket: ${config.S3_BUCKET}`);
|
||||||
|
logger.log('info', ` API: http://${config.S3_HOST}:${config.S3_PORT}`);
|
||||||
|
logger.log('info', ` Console: http://${config.S3_HOST}:${config.S3_CONSOLE_PORT} (login: ${config.S3_ACCESSKEY}/***)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start Elasticsearch service
|
||||||
|
*/
|
||||||
|
public async startElasticsearch(): Promise<void> {
|
||||||
|
logger.log('note', '📦 Elasticsearch:');
|
||||||
|
|
||||||
|
const config = this.config.getConfig();
|
||||||
|
const containers = this.config.getContainerNames();
|
||||||
|
const directories = this.config.getDataDirectories();
|
||||||
|
|
||||||
|
// Ensure data directory exists
|
||||||
|
await plugins.smartfs.directory(directories.elasticsearch).recursive().create();
|
||||||
|
|
||||||
|
const status = await this.docker.getStatus(containers.elasticsearch);
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'running':
|
||||||
|
logger.log('ok', ' Already running ✓');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'stopped':
|
||||||
|
// Check if port mapping matches config
|
||||||
|
const esPortMappings = await this.docker.getPortMappings(containers.elasticsearch);
|
||||||
|
if (esPortMappings && esPortMappings['9200'] !== config.ELASTICSEARCH_PORT) {
|
||||||
|
logger.log('note', ' Port configuration changed, recreating container...');
|
||||||
|
await this.docker.remove(containers.elasticsearch, true);
|
||||||
|
// Fall through to create new container
|
||||||
|
const success = await this.docker.run({
|
||||||
|
name: containers.elasticsearch,
|
||||||
|
image: 'elasticsearch:8.11.0',
|
||||||
|
ports: {
|
||||||
|
[`0.0.0.0:${config.ELASTICSEARCH_PORT}`]: '9200'
|
||||||
|
},
|
||||||
|
volumes: {
|
||||||
|
[directories.elasticsearch]: '/usr/share/elasticsearch/data'
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
'discovery.type': 'single-node',
|
||||||
|
'xpack.security.enabled': 'true',
|
||||||
|
'ELASTIC_PASSWORD': config.ELASTICSEARCH_PASS,
|
||||||
|
'ES_JAVA_OPTS': '-Xms512m -Xmx512m'
|
||||||
|
},
|
||||||
|
restart: 'unless-stopped'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logger.log('ok', ' Recreated with new port ✓');
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to recreate container');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ports match, just start the container
|
||||||
|
if (await this.docker.start(containers.elasticsearch)) {
|
||||||
|
logger.log('ok', ' Started ✓');
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to start');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'not_exists':
|
||||||
|
logger.log('note', ' Creating container...');
|
||||||
|
|
||||||
|
const success = await this.docker.run({
|
||||||
|
name: containers.elasticsearch,
|
||||||
|
image: 'elasticsearch:8.11.0',
|
||||||
|
ports: {
|
||||||
|
[`0.0.0.0:${config.ELASTICSEARCH_PORT}`]: '9200'
|
||||||
|
},
|
||||||
|
volumes: {
|
||||||
|
[directories.elasticsearch]: '/usr/share/elasticsearch/data'
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
'discovery.type': 'single-node',
|
||||||
|
'xpack.security.enabled': 'true',
|
||||||
|
'ELASTIC_PASSWORD': config.ELASTICSEARCH_PASS,
|
||||||
|
'ES_JAVA_OPTS': '-Xms512m -Xmx512m'
|
||||||
|
},
|
||||||
|
restart: 'unless-stopped'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logger.log('ok', ' Created and started ✓');
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to create container');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', ` Container: ${containers.elasticsearch}`);
|
||||||
|
logger.log('info', ` Port: ${config.ELASTICSEARCH_PORT}`);
|
||||||
|
logger.log('info', ` Connection: ${config.ELASTICSEARCH_URL}`);
|
||||||
|
logger.log('info', ` Username: ${config.ELASTICSEARCH_USER}`);
|
||||||
|
logger.log('info', ` Password: ${config.ELASTICSEARCH_PASS}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop MongoDB service
|
||||||
|
*/
|
||||||
|
public async stopMongoDB(): Promise<void> {
|
||||||
|
logger.log('note', '📦 MongoDB:');
|
||||||
|
|
||||||
|
const containers = this.config.getContainerNames();
|
||||||
|
const status = await this.docker.getStatus(containers.mongo);
|
||||||
|
|
||||||
|
if (status === 'running') {
|
||||||
|
if (await this.docker.stop(containers.mongo)) {
|
||||||
|
logger.log('ok', ' Stopped ✓');
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to stop');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.log('note', ' Not running');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop MinIO service
|
||||||
|
*/
|
||||||
|
public async stopMinIO(): Promise<void> {
|
||||||
|
logger.log('note', '📦 S3/MinIO:');
|
||||||
|
|
||||||
|
const containers = this.config.getContainerNames();
|
||||||
|
const status = await this.docker.getStatus(containers.minio);
|
||||||
|
|
||||||
|
if (status === 'running') {
|
||||||
|
if (await this.docker.stop(containers.minio)) {
|
||||||
|
logger.log('ok', ' Stopped ✓');
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to stop');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.log('note', ' Not running');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop Elasticsearch service
|
||||||
|
*/
|
||||||
|
public async stopElasticsearch(): Promise<void> {
|
||||||
|
logger.log('note', '📦 Elasticsearch:');
|
||||||
|
|
||||||
|
const containers = this.config.getContainerNames();
|
||||||
|
const status = await this.docker.getStatus(containers.elasticsearch);
|
||||||
|
|
||||||
|
if (status === 'running') {
|
||||||
|
if (await this.docker.stop(containers.elasticsearch)) {
|
||||||
|
logger.log('ok', ' Stopped ✓');
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to stop');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.log('note', ' Not running');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show service status
|
||||||
|
*/
|
||||||
|
public async showStatus(): Promise<void> {
|
||||||
|
helpers.printHeader('Service Status');
|
||||||
|
|
||||||
|
const config = this.config.getConfig();
|
||||||
|
const containers = this.config.getContainerNames();
|
||||||
|
|
||||||
|
logger.log('info', `Project: ${config.PROJECT_NAME}`);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// MongoDB status
|
||||||
|
const mongoStatus = await this.docker.getStatus(containers.mongo);
|
||||||
|
switch (mongoStatus) {
|
||||||
|
case 'running':
|
||||||
|
logger.log('ok', '📦 MongoDB: 🟢 Running');
|
||||||
|
logger.log('info', ` ├─ Container: ${containers.mongo}`);
|
||||||
|
logger.log('info', ` ├─ Port: ${config.MONGODB_PORT}`);
|
||||||
|
logger.log('info', ` ├─ Connection: ${this.config.getMongoConnectionString()}`);
|
||||||
|
|
||||||
|
// Show Compass connection string
|
||||||
|
const networkIp = await helpers.getLocalNetworkIp();
|
||||||
|
const compassString = `mongodb://${config.MONGODB_USER}:${config.MONGODB_PASS}@${networkIp}:${config.MONGODB_PORT}/${config.MONGODB_NAME}?authSource=admin`;
|
||||||
|
logger.log('ok', ` └─ Compass: ${compassString}`);
|
||||||
|
break;
|
||||||
|
case 'stopped':
|
||||||
|
logger.log('note', '📦 MongoDB: 🟡 Stopped');
|
||||||
|
logger.log('info', ` ├─ Container: ${containers.mongo}`);
|
||||||
|
logger.log('info', ` └─ Port: ${config.MONGODB_PORT}`);
|
||||||
|
break;
|
||||||
|
case 'not_exists':
|
||||||
|
logger.log('info', '📦 MongoDB: ⚪ Not installed');
|
||||||
|
// Check port availability
|
||||||
|
const mongoPort = parseInt(config.MONGODB_PORT);
|
||||||
|
const mongoAvailable = await helpers.isPortAvailable(mongoPort);
|
||||||
|
if (!mongoAvailable) {
|
||||||
|
logger.log('error', ` └─ ⚠️ Port ${mongoPort} is in use by another process`);
|
||||||
|
} else {
|
||||||
|
logger.log('info', ` └─ Port ${mongoPort} is available`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinIO status
|
||||||
|
const minioStatus = await this.docker.getStatus(containers.minio);
|
||||||
|
switch (minioStatus) {
|
||||||
|
case 'running':
|
||||||
|
logger.log('ok', '📦 S3/MinIO: 🟢 Running');
|
||||||
|
logger.log('info', ` ├─ Container: ${containers.minio}`);
|
||||||
|
logger.log('info', ` ├─ API: http://${config.S3_HOST}:${config.S3_PORT}`);
|
||||||
|
logger.log('info', ` ├─ Console: http://${config.S3_HOST}:${config.S3_CONSOLE_PORT}`);
|
||||||
|
logger.log('info', ` └─ Bucket: ${config.S3_BUCKET}`);
|
||||||
|
break;
|
||||||
|
case 'stopped':
|
||||||
|
logger.log('note', '📦 S3/MinIO: 🟡 Stopped');
|
||||||
|
logger.log('info', ` ├─ Container: ${containers.minio}`);
|
||||||
|
logger.log('info', ` ├─ API Port: ${config.S3_PORT}`);
|
||||||
|
logger.log('info', ` └─ Console Port: ${config.S3_CONSOLE_PORT}`);
|
||||||
|
break;
|
||||||
|
case 'not_exists':
|
||||||
|
logger.log('info', '📦 S3/MinIO: ⚪ Not installed');
|
||||||
|
// Check port availability
|
||||||
|
const s3Port = parseInt(config.S3_PORT);
|
||||||
|
const s3ConsolePort = parseInt(config.S3_CONSOLE_PORT);
|
||||||
|
const s3Available = await helpers.isPortAvailable(s3Port);
|
||||||
|
const consoleAvailable = await helpers.isPortAvailable(s3ConsolePort);
|
||||||
|
|
||||||
|
if (!s3Available || !consoleAvailable) {
|
||||||
|
if (!s3Available) {
|
||||||
|
logger.log('error', ` ├─ ⚠️ API Port ${s3Port} is in use`);
|
||||||
|
} else {
|
||||||
|
logger.log('info', ` ├─ API Port ${s3Port} is available`);
|
||||||
|
}
|
||||||
|
if (!consoleAvailable) {
|
||||||
|
logger.log('error', ` └─ ⚠️ Console Port ${s3ConsolePort} is in use`);
|
||||||
|
} else {
|
||||||
|
logger.log('info', ` └─ Console Port ${s3ConsolePort} is available`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.log('info', ` ├─ API Port ${s3Port} is available`);
|
||||||
|
logger.log('info', ` └─ Console Port ${s3ConsolePort} is available`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elasticsearch status
|
||||||
|
const esStatus = await this.docker.getStatus(containers.elasticsearch);
|
||||||
|
switch (esStatus) {
|
||||||
|
case 'running':
|
||||||
|
logger.log('ok', '📦 Elasticsearch: 🟢 Running');
|
||||||
|
logger.log('info', ` ├─ Container: ${containers.elasticsearch}`);
|
||||||
|
logger.log('info', ` ├─ Port: ${config.ELASTICSEARCH_PORT}`);
|
||||||
|
logger.log('info', ` ├─ Connection: ${config.ELASTICSEARCH_URL}`);
|
||||||
|
logger.log('info', ` └─ Credentials: ${config.ELASTICSEARCH_USER}/${config.ELASTICSEARCH_PASS}`);
|
||||||
|
break;
|
||||||
|
case 'stopped':
|
||||||
|
logger.log('note', '📦 Elasticsearch: 🟡 Stopped');
|
||||||
|
logger.log('info', ` ├─ Container: ${containers.elasticsearch}`);
|
||||||
|
logger.log('info', ` └─ Port: ${config.ELASTICSEARCH_PORT}`);
|
||||||
|
break;
|
||||||
|
case 'not_exists':
|
||||||
|
logger.log('info', '📦 Elasticsearch: ⚪ Not installed');
|
||||||
|
// Check port availability
|
||||||
|
const esPort = parseInt(config.ELASTICSEARCH_PORT);
|
||||||
|
const esAvailable = await helpers.isPortAvailable(esPort);
|
||||||
|
if (!esAvailable) {
|
||||||
|
logger.log('error', ` └─ ⚠️ Port ${esPort} is in use by another process`);
|
||||||
|
} else {
|
||||||
|
logger.log('info', ` └─ Port ${esPort} is available`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show configuration
|
||||||
|
*/
|
||||||
|
public async showConfig(): Promise<void> {
|
||||||
|
helpers.printHeader('Current Configuration');
|
||||||
|
|
||||||
|
const config = this.config.getConfig();
|
||||||
|
|
||||||
|
logger.log('info', `Project: ${config.PROJECT_NAME}`);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
logger.log('note', 'MongoDB:');
|
||||||
|
logger.log('info', ` Host: ${config.MONGODB_HOST}:${config.MONGODB_PORT}`);
|
||||||
|
logger.log('info', ` Database: ${config.MONGODB_NAME}`);
|
||||||
|
logger.log('info', ` User: ${config.MONGODB_USER}`);
|
||||||
|
logger.log('info', ' Password: ***');
|
||||||
|
logger.log('info', ` Container: ${this.config.getContainerNames().mongo}`);
|
||||||
|
logger.log('info', ` Data: ${this.config.getDataDirectories().mongo}`);
|
||||||
|
logger.log('info', ` Connection: ${this.config.getMongoConnectionString()}`);
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
logger.log('note', 'S3/MinIO:');
|
||||||
|
logger.log('info', ` Host: ${config.S3_HOST}`);
|
||||||
|
logger.log('info', ` API Port: ${config.S3_PORT}`);
|
||||||
|
logger.log('info', ` Console Port: ${config.S3_CONSOLE_PORT}`);
|
||||||
|
logger.log('info', ` Access Key: ${config.S3_ACCESSKEY}`);
|
||||||
|
logger.log('info', ' Secret Key: ***');
|
||||||
|
logger.log('info', ` Bucket: ${config.S3_BUCKET}`);
|
||||||
|
logger.log('info', ` Use SSL: ${config.S3_USESSL}`);
|
||||||
|
logger.log('info', ` Container: ${this.config.getContainerNames().minio}`);
|
||||||
|
logger.log('info', ` Data: ${this.config.getDataDirectories().minio}`);
|
||||||
|
logger.log('info', ` Endpoint: ${config.S3_ENDPOINT}`);
|
||||||
|
logger.log('info', ` Console URL: http://${config.S3_HOST}:${config.S3_CONSOLE_PORT}`);
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
logger.log('note', 'Elasticsearch:');
|
||||||
|
logger.log('info', ` Host: ${config.ELASTICSEARCH_HOST}:${config.ELASTICSEARCH_PORT}`);
|
||||||
|
logger.log('info', ` User: ${config.ELASTICSEARCH_USER}`);
|
||||||
|
logger.log('info', ' Password: ***');
|
||||||
|
logger.log('info', ` Container: ${this.config.getContainerNames().elasticsearch}`);
|
||||||
|
logger.log('info', ` Data: ${this.config.getDataDirectories().elasticsearch}`);
|
||||||
|
logger.log('info', ` Connection: ${config.ELASTICSEARCH_URL}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show MongoDB Compass connection string
|
||||||
|
*/
|
||||||
|
public async showCompassConnection(): Promise<void> {
|
||||||
|
helpers.printHeader('MongoDB Compass Connection');
|
||||||
|
|
||||||
|
const config = this.config.getConfig();
|
||||||
|
const networkIp = await helpers.getLocalNetworkIp();
|
||||||
|
|
||||||
|
const connectionString = `mongodb://${config.MONGODB_USER}:${config.MONGODB_PASS}@${networkIp}:${config.MONGODB_PORT}/${config.MONGODB_NAME}?authSource=admin`;
|
||||||
|
|
||||||
|
logger.log('info', 'MongoDB Compass is a GUI tool for MongoDB. To connect:');
|
||||||
|
console.log();
|
||||||
|
logger.log('info', '1. Download MongoDB Compass from:');
|
||||||
|
logger.log('info', ' https://www.mongodb.com/products/compass');
|
||||||
|
console.log();
|
||||||
|
logger.log('info', '2. Open Compass and paste this connection string:');
|
||||||
|
logger.log('ok', ` ${connectionString}`);
|
||||||
|
console.log();
|
||||||
|
logger.log('note', 'Connection Details:');
|
||||||
|
logger.log('info', ` Network IP: ${networkIp}`);
|
||||||
|
logger.log('info', ` Port: ${config.MONGODB_PORT}`);
|
||||||
|
logger.log('info', ` Database: ${config.MONGODB_NAME}`);
|
||||||
|
logger.log('info', ` Username: ${config.MONGODB_USER}`);
|
||||||
|
logger.log('info', ` Auth Source: admin`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show logs for a service
|
||||||
|
*/
|
||||||
|
public async showLogs(service: string, lines: number = 20): Promise<void> {
|
||||||
|
const containers = this.config.getContainerNames();
|
||||||
|
|
||||||
|
switch (service) {
|
||||||
|
case 'mongo':
|
||||||
|
case 'mongodb':
|
||||||
|
if (await this.docker.isRunning(containers.mongo)) {
|
||||||
|
helpers.printHeader(`MongoDB Logs (last ${lines} lines)`);
|
||||||
|
const logs = await this.docker.logs(containers.mongo, lines);
|
||||||
|
console.log(logs);
|
||||||
|
} else {
|
||||||
|
logger.log('note', 'MongoDB container is not running');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'minio':
|
||||||
|
case 's3':
|
||||||
|
if (await this.docker.isRunning(containers.minio)) {
|
||||||
|
helpers.printHeader(`S3/MinIO Logs (last ${lines} lines)`);
|
||||||
|
const logs = await this.docker.logs(containers.minio, lines);
|
||||||
|
console.log(logs);
|
||||||
|
} else {
|
||||||
|
logger.log('note', 'S3/MinIO container is not running');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'elasticsearch':
|
||||||
|
case 'es':
|
||||||
|
if (await this.docker.isRunning(containers.elasticsearch)) {
|
||||||
|
helpers.printHeader(`Elasticsearch Logs (last ${lines} lines)`);
|
||||||
|
const logs = await this.docker.logs(containers.elasticsearch, lines);
|
||||||
|
console.log(logs);
|
||||||
|
} else {
|
||||||
|
logger.log('note', 'Elasticsearch container is not running');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'all':
|
||||||
|
case '':
|
||||||
|
await this.showLogs('mongo', lines);
|
||||||
|
console.log();
|
||||||
|
await this.showLogs('minio', lines);
|
||||||
|
console.log();
|
||||||
|
await this.showLogs('elasticsearch', lines);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.log('note', 'Usage: gitzone services logs [mongo|s3|elasticsearch|all] [lines]');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove containers
|
||||||
|
*/
|
||||||
|
public async removeContainers(): Promise<void> {
|
||||||
|
const containers = this.config.getContainerNames();
|
||||||
|
let removed = false;
|
||||||
|
|
||||||
|
if (await this.docker.exists(containers.mongo)) {
|
||||||
|
if (await this.docker.remove(containers.mongo, true)) {
|
||||||
|
logger.log('ok', ' MongoDB container removed ✓');
|
||||||
|
removed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.docker.exists(containers.minio)) {
|
||||||
|
if (await this.docker.remove(containers.minio, true)) {
|
||||||
|
logger.log('ok', ' S3/MinIO container removed ✓');
|
||||||
|
removed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.docker.exists(containers.elasticsearch)) {
|
||||||
|
if (await this.docker.remove(containers.elasticsearch, true)) {
|
||||||
|
logger.log('ok', ' Elasticsearch container removed ✓');
|
||||||
|
removed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!removed) {
|
||||||
|
logger.log('note', ' No containers to remove');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all containers are gone, then unregister from global registry
|
||||||
|
const mongoExists = await this.docker.exists(containers.mongo);
|
||||||
|
const minioExists = await this.docker.exists(containers.minio);
|
||||||
|
const esExists = await this.docker.exists(containers.elasticsearch);
|
||||||
|
|
||||||
|
if (!mongoExists && !minioExists && !esExists) {
|
||||||
|
await this.globalRegistry.unregisterProject(process.cwd());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean data directories
|
||||||
|
*/
|
||||||
|
public async cleanData(): Promise<void> {
|
||||||
|
const directories = this.config.getDataDirectories();
|
||||||
|
let cleaned = false;
|
||||||
|
|
||||||
|
if (await plugins.smartfs.directory(directories.mongo).exists()) {
|
||||||
|
await plugins.smartfs.directory(directories.mongo).recursive().delete();
|
||||||
|
logger.log('ok', ' MongoDB data removed ✓');
|
||||||
|
cleaned = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await plugins.smartfs.directory(directories.minio).exists()) {
|
||||||
|
await plugins.smartfs.directory(directories.minio).recursive().delete();
|
||||||
|
logger.log('ok', ' S3/MinIO data removed ✓');
|
||||||
|
cleaned = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await plugins.smartfs.directory(directories.elasticsearch).exists()) {
|
||||||
|
await plugins.smartfs.directory(directories.elasticsearch).recursive().delete();
|
||||||
|
logger.log('ok', ' Elasticsearch data removed ✓');
|
||||||
|
cleaned = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cleaned) {
|
||||||
|
logger.log('note', ' No data to clean');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure which services are enabled
|
||||||
|
*/
|
||||||
|
public async configureServices(): Promise<void> {
|
||||||
|
logger.log('note', 'Select which services to enable for this project:');
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
const currentServices = this.enabledServices || ['mongodb', 'minio', 'elasticsearch'];
|
||||||
|
|
||||||
|
const smartinteract = new plugins.smartinteract.SmartInteract();
|
||||||
|
const response = await smartinteract.askQuestion({
|
||||||
|
name: 'services',
|
||||||
|
type: 'checkbox',
|
||||||
|
message: 'Which services do you want to enable?',
|
||||||
|
choices: [
|
||||||
|
{ name: 'MongoDB', value: 'mongodb' },
|
||||||
|
{ name: 'MinIO (S3)', value: 'minio' },
|
||||||
|
{ name: 'Elasticsearch', value: 'elasticsearch' }
|
||||||
|
],
|
||||||
|
default: currentServices
|
||||||
|
});
|
||||||
|
|
||||||
|
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
|
||||||
|
|
||||||
|
// Save to .smartconfig.json
|
||||||
|
await this.saveServiceConfiguration(this.enabledServices);
|
||||||
|
|
||||||
|
logger.log('ok', '✅ Service configuration updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconfigure services with new ports
|
||||||
|
*/
|
||||||
|
public async reconfigure(): Promise<void> {
|
||||||
|
helpers.printHeader('Reconfiguring Services');
|
||||||
|
|
||||||
|
const containers = this.config.getContainerNames();
|
||||||
|
|
||||||
|
// Stop existing containers
|
||||||
|
logger.log('note', '🛑 Stopping existing containers...');
|
||||||
|
|
||||||
|
if (await this.docker.exists(containers.mongo)) {
|
||||||
|
await this.docker.stop(containers.mongo);
|
||||||
|
logger.log('ok', ' MongoDB stopped ✓');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.docker.exists(containers.minio)) {
|
||||||
|
await this.docker.stop(containers.minio);
|
||||||
|
logger.log('ok', ' S3/MinIO stopped ✓');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.docker.exists(containers.elasticsearch)) {
|
||||||
|
await this.docker.stop(containers.elasticsearch);
|
||||||
|
logger.log('ok', ' Elasticsearch stopped ✓');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconfigure ports
|
||||||
|
await this.config.reconfigurePorts();
|
||||||
|
|
||||||
|
// Ask if user wants to restart services
|
||||||
|
const smartinteract = new plugins.smartinteract.SmartInteract();
|
||||||
|
const response = await smartinteract.askQuestion({
|
||||||
|
name: 'restart',
|
||||||
|
type: 'confirm',
|
||||||
|
message: 'Do you want to start services with new ports?',
|
||||||
|
default: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.value) {
|
||||||
|
console.log();
|
||||||
|
await this.startAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import * as plugins from './mod.plugins.js';
|
||||||
|
import * as net from 'net';
|
||||||
|
import { logger } from '../gitzone.logging.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a port is available
|
||||||
|
*/
|
||||||
|
export const isPortAvailable = async (port: number): Promise<boolean> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const server = net.createServer();
|
||||||
|
|
||||||
|
server.once('error', () => {
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.once('listening', () => {
|
||||||
|
server.close();
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(port, '0.0.0.0');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a random available port between 20000 and 30000
|
||||||
|
*/
|
||||||
|
export const getRandomAvailablePort = async (): Promise<number> => {
|
||||||
|
const maxAttempts = 100;
|
||||||
|
|
||||||
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
|
const port = Math.floor(Math.random() * 10001) + 20000;
|
||||||
|
if (await isPortAvailable(port)) {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: let the system assign a port
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the project name from package.json or directory
|
||||||
|
*/
|
||||||
|
export const getProjectName = async (): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const packageJsonPath = plugins.path.join(process.cwd(), 'package.json');
|
||||||
|
if (await plugins.smartfs.file(packageJsonPath).exists()) {
|
||||||
|
const content = (await plugins.smartfs
|
||||||
|
.file(packageJsonPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
const packageJson = JSON.parse(content);
|
||||||
|
if (packageJson.name) {
|
||||||
|
// Sanitize: @fin.cx/skr → fin-cx-skr
|
||||||
|
return packageJson.name.replace(/@/g, '').replace(/[\/\.]/g, '-');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors and fall back to directory name
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins.path.basename(process.cwd());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print a header with decorative lines
|
||||||
|
*/
|
||||||
|
export const printHeader = (title: string) => {
|
||||||
|
console.log();
|
||||||
|
logger.log('info', '═══════════════════════════════════════════════════════════════');
|
||||||
|
logger.log('info', ` ${title}`);
|
||||||
|
logger.log('info', '═══════════════════════════════════════════════════════════════');
|
||||||
|
console.log();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format bytes to human readable string
|
||||||
|
*/
|
||||||
|
export const formatBytes = (bytes: number): string => {
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
let size = bytes;
|
||||||
|
let unitIndex = 0;
|
||||||
|
|
||||||
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
size /= 1024;
|
||||||
|
unitIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the local network IP address
|
||||||
|
*/
|
||||||
|
export const getLocalNetworkIp = async (): Promise<string> => {
|
||||||
|
const smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
|
||||||
|
const gateways = await smartnetworkInstance.getGateways();
|
||||||
|
|
||||||
|
// Find the best local IP from network interfaces
|
||||||
|
for (const interfaceName of Object.keys(gateways)) {
|
||||||
|
const interfaces = gateways[interfaceName];
|
||||||
|
for (const iface of interfaces) {
|
||||||
|
// Skip loopback and internal interfaces
|
||||||
|
if (!iface.internal && iface.family === 'IPv4') {
|
||||||
|
const address = iface.address;
|
||||||
|
// Prefer LAN IPs
|
||||||
|
if (address.startsWith('192.168.') || address.startsWith('10.') || address.startsWith('172.')) {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try to get any non-internal IPv4
|
||||||
|
for (const interfaceName of Object.keys(gateways)) {
|
||||||
|
const interfaces = gateways[interfaceName];
|
||||||
|
for (const iface of interfaces) {
|
||||||
|
if (!iface.internal && iface.family === 'IPv4') {
|
||||||
|
return iface.address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort: localhost
|
||||||
|
return 'localhost';
|
||||||
|
};
|
||||||
@@ -0,0 +1,800 @@
|
|||||||
|
import * as plugins from "./mod.plugins.js";
|
||||||
|
import * as helpers from "./helpers.js";
|
||||||
|
import { ServiceManager } from "./classes.servicemanager.js";
|
||||||
|
import { GlobalRegistry } from "./classes.globalregistry.js";
|
||||||
|
import { logger } from "../gitzone.logging.js";
|
||||||
|
import type { ICliMode } from "../helpers.climode.js";
|
||||||
|
import { getCliMode, printJson } from "../helpers.climode.js";
|
||||||
|
import {
|
||||||
|
getCliConfigValueFromData,
|
||||||
|
readSmartconfigFile,
|
||||||
|
setCliConfigValueInData,
|
||||||
|
writeSmartconfigFile,
|
||||||
|
} from "../helpers.smartconfig.js";
|
||||||
|
|
||||||
|
export const run = async (argvArg: any) => {
|
||||||
|
const mode = await getCliMode(argvArg);
|
||||||
|
const isGlobal = argvArg.g || argvArg.global;
|
||||||
|
const command = argvArg._[1] || "help";
|
||||||
|
|
||||||
|
if (mode.help || command === "help") {
|
||||||
|
showHelp(mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle global commands first
|
||||||
|
if (isGlobal) {
|
||||||
|
await handleGlobalCommand(command);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = argvArg._[2] || "all";
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "config":
|
||||||
|
if (service === "services" || argvArg._[2] === "services") {
|
||||||
|
const serviceManager = new ServiceManager();
|
||||||
|
await serviceManager.init();
|
||||||
|
await handleConfigureServices(serviceManager);
|
||||||
|
} else {
|
||||||
|
await handleShowConfig(mode);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "set":
|
||||||
|
await handleSetServices(argvArg._[2], mode);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "enable":
|
||||||
|
await handleEnableServices(argvArg._.slice(2), mode);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "disable":
|
||||||
|
await handleDisableServices(argvArg._.slice(2), mode);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "start":
|
||||||
|
case "stop":
|
||||||
|
case "restart":
|
||||||
|
case "status":
|
||||||
|
case "compass":
|
||||||
|
case "logs":
|
||||||
|
case "remove":
|
||||||
|
case "clean":
|
||||||
|
case "reconfigure": {
|
||||||
|
const serviceManager = new ServiceManager();
|
||||||
|
await serviceManager.init();
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "start":
|
||||||
|
await handleStart(serviceManager, service);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "stop":
|
||||||
|
await handleStop(serviceManager, service);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "restart":
|
||||||
|
await handleRestart(serviceManager, service);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "status":
|
||||||
|
await serviceManager.showStatus();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "compass":
|
||||||
|
await serviceManager.showCompassConnection();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "logs": {
|
||||||
|
const lines = parseInt(argvArg._[3]) || 20;
|
||||||
|
await serviceManager.showLogs(service, lines);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "remove":
|
||||||
|
await handleRemove(serviceManager);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "clean":
|
||||||
|
await handleClean(serviceManager);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "reconfigure":
|
||||||
|
await serviceManager.reconfigure();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
showHelp(mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const allowedServices = ["mongodb", "minio", "elasticsearch"];
|
||||||
|
|
||||||
|
const normalizeServiceName = (service: string): string => {
|
||||||
|
switch (service) {
|
||||||
|
case "mongo":
|
||||||
|
case "mongodb":
|
||||||
|
return "mongodb";
|
||||||
|
case "minio":
|
||||||
|
case "s3":
|
||||||
|
return "minio";
|
||||||
|
case "elastic":
|
||||||
|
case "elasticsearch":
|
||||||
|
case "es":
|
||||||
|
return "elasticsearch";
|
||||||
|
default:
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function readServicesConfig(): Promise<{
|
||||||
|
enabledServices: string[];
|
||||||
|
environment: Record<string, any> | null;
|
||||||
|
}> {
|
||||||
|
const smartconfigData = await readSmartconfigFile();
|
||||||
|
const enabledServices = getCliConfigValueFromData(
|
||||||
|
smartconfigData,
|
||||||
|
"services",
|
||||||
|
);
|
||||||
|
let environment: Record<string, any> | null = null;
|
||||||
|
const envPath = plugins.path.join(process.cwd(), ".nogit", "env.json");
|
||||||
|
if (await plugins.smartfs.file(envPath).exists()) {
|
||||||
|
const envContent = (await plugins.smartfs
|
||||||
|
.file(envPath)
|
||||||
|
.encoding("utf8")
|
||||||
|
.read()) as string;
|
||||||
|
environment = JSON.parse(envContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabledServices: Array.isArray(enabledServices) ? enabledServices : [],
|
||||||
|
environment,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateEnabledServices(services: string[]): Promise<void> {
|
||||||
|
const smartconfigData = await readSmartconfigFile();
|
||||||
|
setCliConfigValueInData(smartconfigData, "services", services);
|
||||||
|
await writeSmartconfigFile(smartconfigData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleShowConfig(mode: ICliMode) {
|
||||||
|
const configData = await readServicesConfig();
|
||||||
|
|
||||||
|
if (mode.json) {
|
||||||
|
printJson(configData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.printHeader("Current Services Configuration");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
`Enabled Services: ${configData.enabledServices.length > 0 ? configData.enabledServices.join(", ") : "none configured"}`,
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
if (!configData.environment) {
|
||||||
|
logger.log(
|
||||||
|
"note",
|
||||||
|
"No .nogit/env.json found yet. Start a service once to create runtime defaults.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = configData.environment;
|
||||||
|
logger.log("note", "MongoDB:");
|
||||||
|
logger.log("info", ` Host: ${env.MONGODB_HOST}:${env.MONGODB_PORT}`);
|
||||||
|
logger.log("info", ` Database: ${env.MONGODB_NAME}`);
|
||||||
|
logger.log("info", ` User: ${env.MONGODB_USER}`);
|
||||||
|
logger.log("info", ` Container: ${env.PROJECT_NAME}-mongodb`);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
` Data: ${plugins.path.join(process.cwd(), ".nogit", "mongodata")}`,
|
||||||
|
);
|
||||||
|
logger.log("info", ` Connection: ${env.MONGODB_URL}`);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
logger.log("note", "S3/MinIO:");
|
||||||
|
logger.log("info", ` Host: ${env.S3_HOST}`);
|
||||||
|
logger.log("info", ` API Port: ${env.S3_PORT}`);
|
||||||
|
logger.log("info", ` Console Port: ${env.S3_CONSOLE_PORT}`);
|
||||||
|
logger.log("info", ` Bucket: ${env.S3_BUCKET}`);
|
||||||
|
logger.log("info", ` Container: ${env.PROJECT_NAME}-minio`);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
` Data: ${plugins.path.join(process.cwd(), ".nogit", "miniodata")}`,
|
||||||
|
);
|
||||||
|
logger.log("info", ` Endpoint: ${env.S3_ENDPOINT}`);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
logger.log("note", "Elasticsearch:");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
` Host: ${env.ELASTICSEARCH_HOST}:${env.ELASTICSEARCH_PORT}`,
|
||||||
|
);
|
||||||
|
logger.log("info", ` User: ${env.ELASTICSEARCH_USER}`);
|
||||||
|
logger.log("info", ` Container: ${env.PROJECT_NAME}-elasticsearch`);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
` Data: ${plugins.path.join(process.cwd(), ".nogit", "esdata")}`,
|
||||||
|
);
|
||||||
|
logger.log("info", ` Connection: ${env.ELASTICSEARCH_URL}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSetServices(rawValue: string | undefined, mode: ICliMode) {
|
||||||
|
if (!rawValue) {
|
||||||
|
throw new Error("Specify a comma-separated list of services");
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedServices = rawValue
|
||||||
|
.split(",")
|
||||||
|
.map((service) => normalizeServiceName(service.trim()))
|
||||||
|
.filter(Boolean);
|
||||||
|
validateRequestedServices(requestedServices);
|
||||||
|
await updateEnabledServices(requestedServices);
|
||||||
|
|
||||||
|
if (mode.json) {
|
||||||
|
printJson({ ok: true, action: "set", enabledServices: requestedServices });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("ok", `Enabled services set to: ${requestedServices.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEnableServices(
|
||||||
|
requestedServices: string[],
|
||||||
|
mode: ICliMode,
|
||||||
|
) {
|
||||||
|
const normalizedServices = requestedServices.map((service) =>
|
||||||
|
normalizeServiceName(service),
|
||||||
|
);
|
||||||
|
validateRequestedServices(normalizedServices);
|
||||||
|
|
||||||
|
const configData = await readServicesConfig();
|
||||||
|
const nextServices = Array.from(
|
||||||
|
new Set([...configData.enabledServices, ...normalizedServices]),
|
||||||
|
);
|
||||||
|
await updateEnabledServices(nextServices);
|
||||||
|
|
||||||
|
if (mode.json) {
|
||||||
|
printJson({ ok: true, action: "enable", enabledServices: nextServices });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("ok", `Enabled services: ${nextServices.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDisableServices(
|
||||||
|
requestedServices: string[],
|
||||||
|
mode: ICliMode,
|
||||||
|
) {
|
||||||
|
const normalizedServices = requestedServices.map((service) =>
|
||||||
|
normalizeServiceName(service),
|
||||||
|
);
|
||||||
|
validateRequestedServices(normalizedServices);
|
||||||
|
|
||||||
|
const configData = await readServicesConfig();
|
||||||
|
const nextServices = configData.enabledServices.filter(
|
||||||
|
(service) => !normalizedServices.includes(service),
|
||||||
|
);
|
||||||
|
await updateEnabledServices(nextServices);
|
||||||
|
|
||||||
|
if (mode.json) {
|
||||||
|
printJson({ ok: true, action: "disable", enabledServices: nextServices });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("ok", `Enabled services: ${nextServices.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRequestedServices(services: string[]): void {
|
||||||
|
if (services.length === 0) {
|
||||||
|
throw new Error("Specify at least one service");
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidServices = services.filter(
|
||||||
|
(service) => !allowedServices.includes(service),
|
||||||
|
);
|
||||||
|
if (invalidServices.length > 0) {
|
||||||
|
throw new Error(`Unknown service(s): ${invalidServices.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStart(serviceManager: ServiceManager, service: string) {
|
||||||
|
helpers.printHeader("Starting Services");
|
||||||
|
|
||||||
|
switch (service) {
|
||||||
|
case "mongo":
|
||||||
|
case "mongodb":
|
||||||
|
await serviceManager.startMongoDB();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "minio":
|
||||||
|
case "s3":
|
||||||
|
await serviceManager.startMinIO();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "elasticsearch":
|
||||||
|
case "es":
|
||||||
|
await serviceManager.startElasticsearch();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "all":
|
||||||
|
case "":
|
||||||
|
await serviceManager.startAll();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.log("error", `Unknown service: ${service}`);
|
||||||
|
logger.log("note", "Use: mongo, s3, elasticsearch, or all");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStop(serviceManager: ServiceManager, service: string) {
|
||||||
|
helpers.printHeader("Stopping Services");
|
||||||
|
|
||||||
|
switch (service) {
|
||||||
|
case "mongo":
|
||||||
|
case "mongodb":
|
||||||
|
await serviceManager.stopMongoDB();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "minio":
|
||||||
|
case "s3":
|
||||||
|
await serviceManager.stopMinIO();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "elasticsearch":
|
||||||
|
case "es":
|
||||||
|
await serviceManager.stopElasticsearch();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "all":
|
||||||
|
case "":
|
||||||
|
await serviceManager.stopAll();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.log("error", `Unknown service: ${service}`);
|
||||||
|
logger.log("note", "Use: mongo, s3, elasticsearch, or all");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRestart(serviceManager: ServiceManager, service: string) {
|
||||||
|
helpers.printHeader("Restarting Services");
|
||||||
|
|
||||||
|
switch (service) {
|
||||||
|
case "mongo":
|
||||||
|
case "mongodb":
|
||||||
|
await serviceManager.stopMongoDB();
|
||||||
|
await plugins.smartdelay.delayFor(2000);
|
||||||
|
await serviceManager.startMongoDB();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "minio":
|
||||||
|
case "s3":
|
||||||
|
await serviceManager.stopMinIO();
|
||||||
|
await plugins.smartdelay.delayFor(2000);
|
||||||
|
await serviceManager.startMinIO();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "elasticsearch":
|
||||||
|
case "es":
|
||||||
|
await serviceManager.stopElasticsearch();
|
||||||
|
await plugins.smartdelay.delayFor(2000);
|
||||||
|
await serviceManager.startElasticsearch();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "all":
|
||||||
|
case "":
|
||||||
|
await serviceManager.stopAll();
|
||||||
|
await plugins.smartdelay.delayFor(2000);
|
||||||
|
await serviceManager.startAll();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.log("error", `Unknown service: ${service}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRemove(serviceManager: ServiceManager) {
|
||||||
|
helpers.printHeader("Removing Containers");
|
||||||
|
logger.log("note", "⚠️ This will remove containers but preserve data");
|
||||||
|
|
||||||
|
const shouldContinue =
|
||||||
|
await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||||
|
"Continue?",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldContinue) {
|
||||||
|
await serviceManager.removeContainers();
|
||||||
|
} else {
|
||||||
|
logger.log("note", "Cancelled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClean(serviceManager: ServiceManager) {
|
||||||
|
helpers.printHeader("Clean All");
|
||||||
|
logger.log("error", "⚠️ WARNING: This will remove all containers and data!");
|
||||||
|
logger.log("error", "This action cannot be undone!");
|
||||||
|
|
||||||
|
const smartinteraction = new plugins.smartinteract.SmartInteract();
|
||||||
|
const confirmAnswer = await smartinteraction.askQuestion({
|
||||||
|
name: "confirm",
|
||||||
|
type: "input",
|
||||||
|
message: 'Type "yes" to confirm:',
|
||||||
|
default: "no",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (confirmAnswer.value === "yes") {
|
||||||
|
await serviceManager.removeContainers();
|
||||||
|
console.log();
|
||||||
|
await serviceManager.cleanData();
|
||||||
|
logger.log("ok", "All cleaned ✓");
|
||||||
|
} else {
|
||||||
|
logger.log("note", "Cancelled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfigureServices(serviceManager: ServiceManager) {
|
||||||
|
helpers.printHeader("Configure Services");
|
||||||
|
await serviceManager.configureServices();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showHelp(mode?: ICliMode) {
|
||||||
|
if (mode?.json) {
|
||||||
|
printJson({
|
||||||
|
command: "services",
|
||||||
|
usage: "gitzone services <command> [options]",
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
name: "config",
|
||||||
|
description:
|
||||||
|
"Show configured services and any existing runtime env.json data",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set <csv>",
|
||||||
|
description: "Set the enabled service list without prompts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "enable <service...>",
|
||||||
|
description: "Enable one or more services without prompts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disable <service...>",
|
||||||
|
description: "Disable one or more services without prompts",
|
||||||
|
},
|
||||||
|
{ name: "start [service]", description: "Start services" },
|
||||||
|
{ name: "stop [service]", description: "Stop services" },
|
||||||
|
{ name: "status", description: "Show service status" },
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
"gitzone services config --json",
|
||||||
|
"gitzone services set mongodb,minio",
|
||||||
|
"gitzone services enable elasticsearch",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.printHeader("GitZone Services Manager");
|
||||||
|
|
||||||
|
logger.log("ok", "Usage: gitzone services [command] [options]");
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
logger.log("note", "Commands:");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" start [service] Start services (mongo|s3|elasticsearch|all)",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" stop [service] Stop services (mongo|s3|elasticsearch|all)",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" restart [service] Restart services (mongo|s3|elasticsearch|all)",
|
||||||
|
);
|
||||||
|
logger.log("info", " status Show service status");
|
||||||
|
logger.log("info", " config Show current configuration");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" config services Configure which services are enabled",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" set <csv> Set enabled services without prompts",
|
||||||
|
);
|
||||||
|
logger.log("info", " enable <svc...> Enable one or more services");
|
||||||
|
logger.log("info", " disable <svc...> Disable one or more services");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" compass Show MongoDB Compass connection string",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" logs [service] Show logs (mongo|s3|elasticsearch|all) [lines]",
|
||||||
|
);
|
||||||
|
logger.log("info", " reconfigure Reassign ports and restart services");
|
||||||
|
logger.log("info", " remove Remove all containers");
|
||||||
|
logger.log("info", " clean Remove all containers and data ⚠️");
|
||||||
|
logger.log("info", " help Show this help message");
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
logger.log("note", "Available Services:");
|
||||||
|
logger.log("info", " • MongoDB (mongo) - Document database");
|
||||||
|
logger.log("info", " • MinIO (s3) - S3-compatible object storage");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" • Elasticsearch (elasticsearch) - Search and analytics engine",
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
logger.log("note", "Features:");
|
||||||
|
logger.log("info", " • Auto-creates .nogit/env.json with smart defaults");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" • Random ports (20000-30000) for MongoDB/MinIO to avoid conflicts",
|
||||||
|
);
|
||||||
|
logger.log("info", " • Elasticsearch uses standard port 9200");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" • Project-specific containers for multi-project support",
|
||||||
|
);
|
||||||
|
logger.log("info", " • Preserves custom configuration values");
|
||||||
|
logger.log("info", " • MongoDB Compass connection support");
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
logger.log("note", "Examples:");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services start # Start all services",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services start mongo # Start only MongoDB",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services start elasticsearch # Start only Elasticsearch",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services stop # Stop all services",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services status # Check service status",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services config # Show configuration",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services config --json # Show configuration as JSON",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services set mongodb,minio # Configure services without prompts",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services compass # Get MongoDB Compass connection",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services logs elasticsearch # Show Elasticsearch logs",
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
logger.log("note", "Global Commands (-g/--global):");
|
||||||
|
logger.log("info", " list -g List all registered projects");
|
||||||
|
logger.log("info", " status -g Show status across all projects");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" stop -g Stop all containers across all projects",
|
||||||
|
);
|
||||||
|
logger.log("info", " cleanup -g Remove stale registry entries");
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
logger.log("note", "Global Examples:");
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services list -g # List all registered projects",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services status -g # Show global container status",
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
" gitzone services stop -g # Stop all (prompts for confirmation)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Global Command Handlers ====================
|
||||||
|
|
||||||
|
async function handleGlobalCommand(command: string) {
|
||||||
|
const globalRegistry = GlobalRegistry.getInstance();
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "list":
|
||||||
|
await handleGlobalList(globalRegistry);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "status":
|
||||||
|
await handleGlobalStatus(globalRegistry);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "stop":
|
||||||
|
await handleGlobalStop(globalRegistry);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "cleanup":
|
||||||
|
await handleGlobalCleanup(globalRegistry);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "help":
|
||||||
|
default:
|
||||||
|
showHelp();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGlobalList(globalRegistry: GlobalRegistry) {
|
||||||
|
helpers.printHeader("Registered Projects (Global)");
|
||||||
|
|
||||||
|
const projects = await globalRegistry.getAllProjects();
|
||||||
|
const projectPaths = Object.keys(projects);
|
||||||
|
|
||||||
|
if (projectPaths.length === 0) {
|
||||||
|
logger.log("note", "No projects registered");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const path of projectPaths) {
|
||||||
|
const project = projects[path];
|
||||||
|
const lastActive = new Date(project.lastActive).toLocaleString();
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
logger.log("ok", `📁 ${project.projectName}`);
|
||||||
|
logger.log("info", ` Path: ${project.projectPath}`);
|
||||||
|
logger.log("info", ` Services: ${project.enabledServices.join(", ")}`);
|
||||||
|
logger.log("info", ` Last Active: ${lastActive}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGlobalStatus(globalRegistry: GlobalRegistry) {
|
||||||
|
helpers.printHeader("Global Service Status");
|
||||||
|
|
||||||
|
const statuses = await globalRegistry.getGlobalStatus();
|
||||||
|
|
||||||
|
if (statuses.length === 0) {
|
||||||
|
logger.log("note", "No projects registered");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let runningCount = 0;
|
||||||
|
let totalContainers = 0;
|
||||||
|
|
||||||
|
for (const project of statuses) {
|
||||||
|
console.log();
|
||||||
|
logger.log("ok", `📁 ${project.projectName}`);
|
||||||
|
logger.log("info", ` Path: ${project.projectPath}`);
|
||||||
|
|
||||||
|
if (project.containers.length === 0) {
|
||||||
|
logger.log("note", " No containers configured");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const container of project.containers) {
|
||||||
|
totalContainers++;
|
||||||
|
const statusIcon =
|
||||||
|
container.status === "running"
|
||||||
|
? "🟢"
|
||||||
|
: container.status === "exited"
|
||||||
|
? "🟡"
|
||||||
|
: "⚪";
|
||||||
|
if (container.status === "running") runningCount++;
|
||||||
|
logger.log(
|
||||||
|
"info",
|
||||||
|
` ${statusIcon} ${container.name}: ${container.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
logger.log(
|
||||||
|
"note",
|
||||||
|
`Summary: ${runningCount}/${totalContainers} containers running across ${statuses.length} project(s)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGlobalStop(globalRegistry: GlobalRegistry) {
|
||||||
|
helpers.printHeader("Stop All Containers (Global)");
|
||||||
|
|
||||||
|
const statuses = await globalRegistry.getGlobalStatus();
|
||||||
|
|
||||||
|
// Count running containers
|
||||||
|
let runningCount = 0;
|
||||||
|
for (const project of statuses) {
|
||||||
|
for (const container of project.containers) {
|
||||||
|
if (container.status === "running") runningCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runningCount === 0) {
|
||||||
|
logger.log("note", "No running containers found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
"note",
|
||||||
|
`Found ${runningCount} running container(s) across ${statuses.length} project(s)`,
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// Show what will be stopped
|
||||||
|
for (const project of statuses) {
|
||||||
|
const runningContainers = project.containers.filter(
|
||||||
|
(c) => c.status === "running",
|
||||||
|
);
|
||||||
|
if (runningContainers.length > 0) {
|
||||||
|
logger.log("info", `${project.projectName}:`);
|
||||||
|
for (const container of runningContainers) {
|
||||||
|
logger.log("info", ` • ${container.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
const shouldContinue =
|
||||||
|
await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||||
|
"Stop all containers?",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldContinue) {
|
||||||
|
logger.log("note", "Cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("note", "Stopping all containers...");
|
||||||
|
const result = await globalRegistry.stopAll();
|
||||||
|
|
||||||
|
if (result.stopped.length > 0) {
|
||||||
|
logger.log("ok", `Stopped: ${result.stopped.join(", ")}`);
|
||||||
|
}
|
||||||
|
if (result.failed.length > 0) {
|
||||||
|
logger.log("error", `Failed to stop: ${result.failed.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGlobalCleanup(globalRegistry: GlobalRegistry) {
|
||||||
|
helpers.printHeader("Cleanup Registry (Global)");
|
||||||
|
|
||||||
|
logger.log("note", "Checking for stale registry entries...");
|
||||||
|
const removed = await globalRegistry.cleanup();
|
||||||
|
|
||||||
|
if (removed.length === 0) {
|
||||||
|
logger.log("ok", "No stale entries found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
"ok",
|
||||||
|
`Removed ${removed.length} stale entr${removed.length === 1 ? "y" : "ies"}:`,
|
||||||
|
);
|
||||||
|
for (const path of removed) {
|
||||||
|
logger.log("info", ` • ${path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export * from '../plugins.js';
|
||||||
|
|
||||||
|
import * as smartshell from '@push.rocks/smartshell';
|
||||||
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
|
import * as smartinteract from '@push.rocks/smartinteract';
|
||||||
|
import * as smartnetwork from '@push.rocks/smartnetwork';
|
||||||
|
import * as smartdelay from '@push.rocks/smartdelay';
|
||||||
|
|
||||||
|
export { smartshell, smartfile, smartinteract, smartnetwork, smartdelay };
|
||||||
+244
-23
@@ -1,29 +1,250 @@
|
|||||||
/* -----------------------------------------------
|
/* -----------------------------------------------
|
||||||
* executes as standard task
|
* executes as standard task
|
||||||
* ----------------------------------------------- */
|
* ----------------------------------------------- */
|
||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from "./mod.plugins.js";
|
||||||
import * as paths from '../paths.js';
|
import * as paths from "../paths.js";
|
||||||
|
import type { ICliMode } from "../helpers.climode.js";
|
||||||
|
import { getCliMode, printJson } from "../helpers.climode.js";
|
||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
import { logger } from "../gitzone.logging.js";
|
||||||
|
|
||||||
export let run = () => {
|
type ICommandHelpSummary = {
|
||||||
const done = plugins.smartpromise.defer();
|
name: string;
|
||||||
logger.log('warn', 'no action specified');
|
description: string;
|
||||||
logger.log(
|
|
||||||
'info',
|
|
||||||
`
|
|
||||||
You can do one of the following things:
|
|
||||||
* create a new project with 'gitzone template [template]'
|
|
||||||
the following templates exist: ${(() => {
|
|
||||||
let projects = `\n`;
|
|
||||||
for (const template of plugins.smartfile.fs.listFoldersSync(paths.templatesDir)) {
|
|
||||||
projects += ` - ${template}\n`;
|
|
||||||
}
|
|
||||||
return projects;
|
|
||||||
})()}
|
|
||||||
* format a project with 'gitzone format'
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
done.resolve();
|
|
||||||
return done.promise;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const commandSummaries: ICommandHelpSummary[] = [
|
||||||
|
{
|
||||||
|
name: "commit",
|
||||||
|
description:
|
||||||
|
"Analyze changes and create semantic source commits",
|
||||||
|
},
|
||||||
|
{ name: "release", description: "Create versioned releases from pending changelog entries" },
|
||||||
|
{ name: "format", description: "Plan or apply project formatting changes" },
|
||||||
|
{ name: "config", description: "Read and change .smartconfig.json settings" },
|
||||||
|
{ name: "services", description: "Manage or configure development services" },
|
||||||
|
{ name: "tools", description: "Manage the global @git.zone toolchain" },
|
||||||
|
{ name: "template", description: "Create a project from a template" },
|
||||||
|
{ name: "open", description: "Open project assets and CI pages" },
|
||||||
|
{ name: "docker", description: "Run Docker-related maintenance tasks" },
|
||||||
|
{
|
||||||
|
name: "deprecate",
|
||||||
|
description: "Deprecate npm packages across registries",
|
||||||
|
},
|
||||||
|
{ name: "meta", description: "Run meta-repository commands" },
|
||||||
|
{ name: "start", description: "Prepare a project for local work" },
|
||||||
|
{ name: "helpers", description: "Run helper utilities" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export let run = async (argvArg: any = {}) => {
|
||||||
|
const mode = await getCliMode(argvArg);
|
||||||
|
const requestedCommandHelp =
|
||||||
|
argvArg._?.[0] === "help" ? argvArg._?.[1] : undefined;
|
||||||
|
|
||||||
|
if (mode.help || requestedCommandHelp) {
|
||||||
|
await showHelp(mode, requestedCommandHelp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mode.interactive) {
|
||||||
|
await showHelp(mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log(
|
||||||
|
"╭─────────────────────────────────────────────────────────────╮",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"│ gitzone - Development Workflow CLI │",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"╰─────────────────────────────────────────────────────────────╯",
|
||||||
|
);
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||||
|
const response = await interactInstance.askQuestion({
|
||||||
|
type: "list",
|
||||||
|
name: "action",
|
||||||
|
message: "What would you like to do?",
|
||||||
|
default: "commit",
|
||||||
|
choices: [
|
||||||
|
{ name: "Commit changes", value: "commit" },
|
||||||
|
{ name: "Release pending changes", value: "release" },
|
||||||
|
{ name: "Format project files", value: "format" },
|
||||||
|
{ name: "Configure release settings", value: "config" },
|
||||||
|
{ name: "Create from template", value: "template" },
|
||||||
|
{ name: "Manage dev services (MongoDB, S3)", value: "services" },
|
||||||
|
{ name: "Manage global @git.zone tools", value: "tools" },
|
||||||
|
{ name: "Open project assets", value: "open" },
|
||||||
|
{ name: "Show help", value: "help" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const action = (response as any).value;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "commit": {
|
||||||
|
const modCommit = await import("../mod_commit/index.js");
|
||||||
|
await modCommit.run({ _: ["commit"] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "release": {
|
||||||
|
const modRelease = await import("../mod_release/index.js");
|
||||||
|
await modRelease.run({ _: ["release"] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "format": {
|
||||||
|
const modFormat = await import("../mod_format/index.js");
|
||||||
|
await modFormat.run({ interactive: true });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "config": {
|
||||||
|
const modConfig = await import("../mod_config/index.js");
|
||||||
|
await modConfig.run({ _: ["config"] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "template": {
|
||||||
|
const modTemplate = await import("../mod_template/index.js");
|
||||||
|
await modTemplate.run({ _: ["template"] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "services": {
|
||||||
|
const modServices = await import("../mod_services/index.js");
|
||||||
|
await modServices.run({ _: ["services"] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "tools": {
|
||||||
|
const modTools = await import("../mod_tools/index.js");
|
||||||
|
await modTools.run({ _: ["tools"] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "open": {
|
||||||
|
const modOpen = await import("../mod_open/index.js");
|
||||||
|
await modOpen.run({ _: ["open"] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "help":
|
||||||
|
await showHelp(mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function showHelp(
|
||||||
|
mode: ICliMode,
|
||||||
|
commandName?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
if (commandName) {
|
||||||
|
const handled = await showCommandHelp(commandName, mode);
|
||||||
|
if (handled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode.json) {
|
||||||
|
printJson({
|
||||||
|
name: "gitzone",
|
||||||
|
usage: "gitzone <command> [options]",
|
||||||
|
commands: commandSummaries,
|
||||||
|
globalFlags: [
|
||||||
|
{ flag: "--help, -h", description: "Show help output" },
|
||||||
|
{
|
||||||
|
flag: "--json",
|
||||||
|
description: "Emit machine-readable JSON when supported",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--plain",
|
||||||
|
description: "Use plain text output when supported",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--agent",
|
||||||
|
description: "Prefer non-interactive machine-friendly output",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--no-interactive",
|
||||||
|
description: "Disable prompts and interactive menus",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--no-check-updates",
|
||||||
|
description: "Skip the update check banner",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log("Usage: gitzone <command> [options]");
|
||||||
|
console.log("");
|
||||||
|
console.log("Commands:");
|
||||||
|
for (const commandSummary of commandSummaries) {
|
||||||
|
console.log(
|
||||||
|
` ${commandSummary.name.padEnd(11)} ${commandSummary.description}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log("");
|
||||||
|
console.log("Global flags:");
|
||||||
|
console.log(" --help, -h Show help output");
|
||||||
|
console.log(
|
||||||
|
" --json Emit machine-readable JSON when supported",
|
||||||
|
);
|
||||||
|
console.log(" --plain Use plain text output when supported");
|
||||||
|
console.log(
|
||||||
|
" --agent Prefer non-interactive machine-friendly output",
|
||||||
|
);
|
||||||
|
console.log(" --no-interactive Disable prompts and interactive menus");
|
||||||
|
console.log(" --no-check-updates Skip the update check banner");
|
||||||
|
console.log("");
|
||||||
|
console.log("Examples:");
|
||||||
|
console.log(" gitzone help commit");
|
||||||
|
console.log(" gitzone config show --json");
|
||||||
|
console.log(" gitzone commit recommend --json");
|
||||||
|
console.log(" gitzone release --plan");
|
||||||
|
console.log(" gitzone format plan --json");
|
||||||
|
console.log(" gitzone services set mongodb,minio");
|
||||||
|
console.log(" gitzone tools update");
|
||||||
|
console.log("");
|
||||||
|
console.log("Run gitzone <command> --help for command-specific usage.");
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showCommandHelp(
|
||||||
|
commandName: string,
|
||||||
|
mode: ICliMode,
|
||||||
|
): Promise<boolean> {
|
||||||
|
switch (commandName) {
|
||||||
|
case "commit": {
|
||||||
|
const modCommit = await import("../mod_commit/index.js");
|
||||||
|
modCommit.showHelp(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case "release": {
|
||||||
|
const modRelease = await import("../mod_release/index.js");
|
||||||
|
modRelease.showHelp(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case "config": {
|
||||||
|
const modConfig = await import("../mod_config/index.js");
|
||||||
|
modConfig.showHelp(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case "format": {
|
||||||
|
const modFormat = await import("../mod_format/index.js");
|
||||||
|
modFormat.showHelp(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case "services": {
|
||||||
|
const modServices = await import("../mod_services/index.js");
|
||||||
|
modServices.showHelp(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case "tools": {
|
||||||
|
const modTools = await import("../mod_tools/index.js");
|
||||||
|
modTools.showHelp(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export const run = async (argvArg: any) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await smartshellInstance.execStrict(`cd ${paths.cwd} && git checkout master`);
|
await smartshellInstance.execStrict(`cd ${paths.cwd} && git checkout master`);
|
||||||
await smartshellInstance.execStrict(`cd ${paths.cwd} && git pull origin master`);
|
await smartshellInstance.execStrict(
|
||||||
|
`cd ${paths.cwd} && git pull origin master`,
|
||||||
|
);
|
||||||
await smartshellInstance.execStrict(`cd ${paths.cwd} && npm ci`);
|
await smartshellInstance.execStrict(`cd ${paths.cwd} && npm ci`);
|
||||||
|
|
||||||
await provideNoGitFiles();
|
await provideNoGitFiles();
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ export const getTemplatePath = (templateNameArg: string) => {
|
|||||||
* receives a template name and returns wether there is a corresponding template
|
* receives a template name and returns wether there is a corresponding template
|
||||||
*/
|
*/
|
||||||
export const isTemplate = async (templateNameArg: string) => {
|
export const isTemplate = async (templateNameArg: string) => {
|
||||||
return plugins.smartfile.fs.isDirectory(getTemplatePath(templateNameArg));
|
return plugins.smartfs.directory(getTemplatePath(templateNameArg)).exists();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTemplate = async (templateNameArg: string) => {
|
export const getTemplate = async (templateNameArg: string) => {
|
||||||
if (isTemplate(templateNameArg)) {
|
if (isTemplate(templateNameArg)) {
|
||||||
const localScafTemplate = new plugins.smartscaf.ScafTemplate(getTemplatePath(templateNameArg));
|
const localScafTemplate = new plugins.smartscaf.ScafTemplate(
|
||||||
|
getTemplatePath(templateNameArg),
|
||||||
|
);
|
||||||
await localScafTemplate.readTemplateFromDir();
|
await localScafTemplate.readTemplateFromDir();
|
||||||
return localScafTemplate;
|
return localScafTemplate;
|
||||||
} else {
|
} else {
|
||||||
@@ -32,7 +34,8 @@ export const run = async (argvArg: any) => {
|
|||||||
const answerBucket = await smartinteract.askQuestion({
|
const answerBucket = await smartinteract.askQuestion({
|
||||||
type: 'list',
|
type: 'list',
|
||||||
default: 'npm',
|
default: 'npm',
|
||||||
message: 'What template do you want to scaffold? (Only showing mpost common options)',
|
message:
|
||||||
|
'What template do you want to scaffold? (Only showing mpost common options)',
|
||||||
name: 'templateName',
|
name: 'templateName',
|
||||||
choices: ['npm', 'service', 'wcc', 'website'],
|
choices: ['npm', 'service', 'wcc', 'website'],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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";
|
||||||
+16
-2
@@ -1,21 +1,35 @@
|
|||||||
import * as smartlog from '@push.rocks/smartlog';
|
import * as smartlog from '@push.rocks/smartlog';
|
||||||
import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
|
import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
|
||||||
import * as npmextra from '@push.rocks/npmextra';
|
import * as smartconfig from '@push.rocks/smartconfig';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as projectinfo from '@push.rocks/projectinfo';
|
import * as projectinfo from '@push.rocks/projectinfo';
|
||||||
import * as smartcli from '@push.rocks/smartcli';
|
import * as smartcli from '@push.rocks/smartcli';
|
||||||
import * as smartpath from '@push.rocks/smartpath';
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
import * as smartpromise from '@push.rocks/smartpromise';
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
import * as smartupdate from '@push.rocks/smartupdate';
|
import * as smartupdate from '@push.rocks/smartupdate';
|
||||||
|
import * as smartshell from '@push.rocks/smartshell';
|
||||||
|
import * as smartnetwork from '@push.rocks/smartnetwork';
|
||||||
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
|
import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
|
||||||
|
import * as smartinteract from '@push.rocks/smartinteract';
|
||||||
|
import * as smartdelay from '@push.rocks/smartdelay';
|
||||||
|
|
||||||
|
// Create smartfs instance for filesystem operations
|
||||||
|
export const smartfs = new SmartFs(new SmartFsProviderNode());
|
||||||
|
|
||||||
export {
|
export {
|
||||||
smartlog,
|
smartlog,
|
||||||
smartlogDestinationLocal,
|
smartlogDestinationLocal,
|
||||||
npmextra,
|
smartconfig,
|
||||||
path,
|
path,
|
||||||
projectinfo,
|
projectinfo,
|
||||||
smartcli,
|
smartcli,
|
||||||
smartpath,
|
smartpath,
|
||||||
smartpromise,
|
smartpromise,
|
||||||
smartupdate,
|
smartupdate,
|
||||||
|
smartshell,
|
||||||
|
smartnetwork,
|
||||||
|
smartfile,
|
||||||
|
smartinteract,
|
||||||
|
smartdelay,
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-6
@@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
|
||||||
"useDefineForClassFields": false,
|
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
@@ -10,7 +8,5 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {}
|
"paths": {}
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": ["dist_*/**/*.d.ts"]
|
||||||
"dist_*/**/*.d.ts"
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user