Compare commits

...

13 Commits

Author SHA1 Message Date
jkunz ad7f2742ff v2.19.1
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-14 13:26:17 +00:00
jkunz b234ecc12a fix(config): migrate legacy release arrays during config fixes and validate release config shape 2026-05-14 13:25:56 +00:00
jkunz 278df40ba7 feat(format): add check and fix workflows 2026-05-14 13:18:49 +00:00
jkunz 6f0928e7c7 v2.19.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-13 10:21:25 +00:00
jkunz 26effadcc9 feat(release): delegate docker target to tsdocker 2026-05-13 10:19:56 +00:00
jkunz c38e94bcf3 v2.18.1
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-10 14:41:49 +00:00
jkunz b9b51f29d1 fix(config): use inherited stdio for opencode handoff 2026-05-10 14:41:08 +00:00
jkunz a3ad48368d v2.18.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-10 13:43:05 +00:00
jkunz c10b764c0a feat(config): add opencode config fix 2026-05-10 13:42:57 +00:00
jkunz 7686504e4e v2.17.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-10 13:09:36 +00:00
jkunz d96b220703 feat(config): add guided configuration flows 2026-05-10 13:09:28 +00:00
jkunz 06f2de3230 v2.16.1
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-10 11:11:03 +00:00
jkunz cc3128f07b fix(cli): guard startup update check 2026-05-10 11:10:30 +00:00
20 changed files with 2246 additions and 342 deletions
+2 -1
View File
@@ -63,7 +63,8 @@
}, },
"docker": { "docker": {
"enabled": false, "enabled": false,
"images": [] "engine": "tsdocker",
"patterns": []
} }
} }
} }
+43
View File
@@ -3,6 +3,49 @@
## Pending ## Pending
## 2026-05-14 - 2.19.1
### Fixes
- migrate legacy release arrays during config fixes and validate release config shape (config)
- Automatically converts legacy release registry arrays into release.targets.npm.registries during smartconfig migration and config fix runs.
- Re-runs doctor checks after applying known migrations so resolved issues do not require the external fixer.
- Reports an explicit validation error when release config is not an object.
- Updates config fix prompts and help text to use generic configuration repair wording.
## 2026-05-13 - 2.19.0
### Features
- Delegate Docker release targets to `tsdocker`.
- Replace Docker image template publishing with `tsdocker push` execution.
- Move Docker registry behavior to `@git.zone/tsdocker` config and validate removed image config as invalid.
## 2026-05-10 - 2.18.1
### Fixes
- Run `gitzone config fix` opencode handoff with inherited terminal I/O.
## 2026-05-10 - 2.18.0
### Features
- Add `gitzone config fix` to invoke opencode for configuration repair.
## 2026-05-10 - 2.17.0
### Features
- Add guided project, CLI, release, and doctor flows to `gitzone config`.
## 2026-05-10 - 2.16.1
### Fixes
- Prevent startup update checks from crashing when installed package metadata is incomplete.
## 2026-05-10 - 2.16.0 ## 2026-05-10 - 2.16.0
### Features ### Features
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "@git.zone/cli", "name": "@git.zone/cli",
"private": false, "private": false,
"version": "2.16.0", "version": "2.19.1",
"description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.", "description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",
@@ -87,7 +87,7 @@
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartscaf": "^4.0.21", "@push.rocks/smartscaf": "^4.0.21",
"@push.rocks/smartshell": "^3.3.7", "@push.rocks/smartshell": "^3.5.0",
"@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartunique": "^3.0.9",
"@push.rocks/smartupdate": "^2.0.6", "@push.rocks/smartupdate": "^2.0.6",
"prettier": "^3.8.1" "prettier": "^3.8.1"
+35 -23
View File
@@ -81,8 +81,8 @@ importers:
specifier: ^4.0.21 specifier: ^4.0.21
version: 4.0.21 version: 4.0.21
'@push.rocks/smartshell': '@push.rocks/smartshell':
specifier: ^3.3.7 specifier: ^3.5.0
version: 3.3.7 version: 3.5.0
'@push.rocks/smartunique': '@push.rocks/smartunique':
specifier: ^3.0.9 specifier: ^3.0.9
version: 3.0.9 version: 3.0.9
@@ -1063,6 +1063,9 @@ packages:
'@push.rocks/smartdelay@3.0.5': '@push.rocks/smartdelay@3.0.5':
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==} resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
'@push.rocks/smartdelay@3.1.0':
resolution: {integrity: sha512-59xveBMbWmbFhh/rqhQnYG/klg/VONG9hV8+RQ7ftqsNRkcmUT+VM5etAbODgAUvsF4lxK+xVR0tbZOo0kGhRQ==}
'@push.rocks/smartdiff@1.1.0': '@push.rocks/smartdiff@1.1.0':
resolution: {integrity: sha512-AAz/unmko0C+g+60odOoK32PE3Ci3YLoB+zfg1LGLyVRCthcdzjqa1C2Km0MfG7IyJQKPdj8J5HPubtpm3ZeaQ==} resolution: {integrity: sha512-AAz/unmko0C+g+60odOoK32PE3Ci3YLoB+zfg1LGLyVRCthcdzjqa1C2Km0MfG7IyJQKPdj8J5HPubtpm3ZeaQ==}
@@ -1192,6 +1195,9 @@ packages:
'@push.rocks/smartpromise@4.2.3': '@push.rocks/smartpromise@4.2.3':
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==} resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
'@push.rocks/smartpromise@4.2.4':
resolution: {integrity: sha512-8FUyYt94hOIY9mqHjitn4h69u0jbEtTF2RKKw2DpiTVFjpDTk9gXbVHZ/V+xEcBrN4mrzdQES0OiDmkNPoddEQ==}
'@push.rocks/smartpuppeteer@2.0.5': '@push.rocks/smartpuppeteer@2.0.5':
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==} resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
@@ -1222,8 +1228,8 @@ packages:
'@push.rocks/smartserve@2.0.1': '@push.rocks/smartserve@2.0.1':
resolution: {integrity: sha512-YQb2qexfCzCqOlLWBBXKMg6xG4zahCPAxomz/KEKAwHtW6wMTtuHKSTSkRTQ0vl9jssLMAmRz2OyafiL9XGJXQ==} resolution: {integrity: sha512-YQb2qexfCzCqOlLWBBXKMg6xG4zahCPAxomz/KEKAwHtW6wMTtuHKSTSkRTQ0vl9jssLMAmRz2OyafiL9XGJXQ==}
'@push.rocks/smartshell@3.3.7': '@push.rocks/smartshell@3.5.0':
resolution: {integrity: sha512-b3st2+FjHUVhZZRlXfw93+SQA0UMVlURqe55uVpWdjJX7jeGXTTeszuYygtiR99zC5iZ8WZhGDct3N2L1qc/qw==} resolution: {integrity: sha512-Hx9TVvC/AWxZsnm1GDb+W4Fe58nf1FkKbSBABUgkxct4XRYugBI2z9Twnjm3R9vdRry8oy0enfR9NPVhisGaGA==}
'@push.rocks/smartspawn@3.0.3': '@push.rocks/smartspawn@3.0.3':
resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==} resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==}
@@ -3917,9 +3923,9 @@ packages:
engines: {node: '>= 8'} engines: {node: '>= 8'}
hasBin: true hasBin: true
which@6.0.1: which@7.0.0:
resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} resolution: {integrity: sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==}
engines: {node: ^20.17.0 || >=22.9.0} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
hasBin: true hasBin: true
wordwrap@1.0.0: wordwrap@1.0.0:
@@ -4802,7 +4808,7 @@ snapshots:
'@push.rocks/smartlog': 3.2.1 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartlog-destination-local': 9.0.2 '@push.rocks/smartlog-destination-local': 9.0.2
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartshell': 3.3.7 '@push.rocks/smartshell': 3.5.0
'@push.rocks/smarttime': 4.2.3 '@push.rocks/smarttime': 4.2.3
typedoc: 0.28.17(typescript@5.9.3) typedoc: 0.28.17(typescript@5.9.3)
typescript: 5.9.3 typescript: 5.9.3
@@ -4832,7 +4838,7 @@ snapshots:
'@push.rocks/smartnpm': 2.0.6 '@push.rocks/smartnpm': 2.0.6
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartrequest': 5.0.1 '@push.rocks/smartrequest': 5.0.1
'@push.rocks/smartshell': 3.3.7 '@push.rocks/smartshell': 3.5.0
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
- aws-crt - aws-crt
@@ -4845,7 +4851,7 @@ snapshots:
'@git.zone/tsrun@2.0.1': '@git.zone/tsrun@2.0.1':
dependencies: dependencies:
'@push.rocks/smartfile': 13.1.2 '@push.rocks/smartfile': 13.1.2
'@push.rocks/smartshell': 3.3.7 '@push.rocks/smartshell': 3.5.0
tsx: 4.21.0 tsx: 4.21.0
'@git.zone/tstest@3.3.2(socks@2.8.7)(typescript@5.9.3)': '@git.zone/tstest@3.3.2(socks@2.8.7)(typescript@5.9.3)':
@@ -4870,7 +4876,7 @@ snapshots:
'@push.rocks/smartrequest': 5.0.1 '@push.rocks/smartrequest': 5.0.1
'@push.rocks/smarts3': 5.3.0 '@push.rocks/smarts3': 5.3.0
'@push.rocks/smartserve': 2.0.1 '@push.rocks/smartserve': 2.0.1
'@push.rocks/smartshell': 3.3.7 '@push.rocks/smartshell': 3.5.0
'@push.rocks/smarttime': 4.2.3 '@push.rocks/smarttime': 4.2.3
'@push.rocks/smartwatch': 6.3.0 '@push.rocks/smartwatch': 6.3.0
'@types/ws': 8.18.1 '@types/ws': 8.18.1
@@ -5590,7 +5596,7 @@ snapshots:
'@push.rocks/smartai': 2.0.0(typescript@5.9.3)(ws@8.19.0)(zod@3.25.76) '@push.rocks/smartai': 2.0.0(typescript@5.9.3)(ws@8.19.0)(zod@3.25.76)
'@push.rocks/smartfs': 1.5.0 '@push.rocks/smartfs': 1.5.0
'@push.rocks/smartrequest': 5.0.1 '@push.rocks/smartrequest': 5.0.1
'@push.rocks/smartshell': 3.3.7 '@push.rocks/smartshell': 3.5.0
ai: 6.0.116(zod@3.25.76) ai: 6.0.116(zod@3.25.76)
zod: 3.25.76 zod: 3.25.76
transitivePeerDependencies: transitivePeerDependencies:
@@ -5775,6 +5781,10 @@ snapshots:
dependencies: dependencies:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartdelay@3.1.0':
dependencies:
'@push.rocks/smartpromise': 4.2.4
'@push.rocks/smartdiff@1.1.0': '@push.rocks/smartdiff@1.1.0':
dependencies: dependencies:
diff: 8.0.3 diff: 8.0.3
@@ -5818,7 +5828,7 @@ snapshots:
'@push.rocks/smartexit@2.0.3': '@push.rocks/smartexit@2.0.3':
dependencies: dependencies:
'@push.rocks/lik': 6.3.1 '@push.rocks/lik': 6.3.1
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.4
'@push.rocks/smartexpect@2.5.0': '@push.rocks/smartexpect@2.5.0':
dependencies: dependencies:
@@ -5897,7 +5907,7 @@ snapshots:
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartshell': 3.3.7 '@push.rocks/smartshell': 3.5.0
'@push.rocks/smartstring': 4.1.0 '@push.rocks/smartstring': 4.1.0
'@push.rocks/smarttime': 4.2.3 '@push.rocks/smarttime': 4.2.3
'@types/diff': 8.0.0 '@types/diff': 8.0.0
@@ -5968,7 +5978,7 @@ snapshots:
'@push.rocks/smartmustache': 3.0.2 '@push.rocks/smartmustache': 3.0.2
'@push.rocks/smartpnpm': 1.0.6 '@push.rocks/smartpnpm': 1.0.6
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartshell': 3.3.7 '@push.rocks/smartshell': 3.5.0
'@tsclass/tsclass': 4.4.4 '@tsclass/tsclass': 4.4.4
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -6159,14 +6169,16 @@ snapshots:
'@push.rocks/smartpnpm@1.0.6': '@push.rocks/smartpnpm@1.0.6':
dependencies: dependencies:
'@push.rocks/smartshell': 3.3.7 '@push.rocks/smartshell': 3.5.0
'@push.rocks/smartpromise@4.2.3': {} '@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartpromise@4.2.4': {}
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)': '@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartshell': 3.3.7 '@push.rocks/smartshell': 3.5.0
puppeteer: 24.35.0(typescript@5.9.3) puppeteer: 24.35.0(typescript@5.9.3)
tree-kill: 1.2.2 tree-kill: 1.2.2
transitivePeerDependencies: transitivePeerDependencies:
@@ -6233,7 +6245,7 @@ snapshots:
'@push.rocks/smartinteract': 2.0.16 '@push.rocks/smartinteract': 2.0.16
'@push.rocks/smartobject': 1.0.12 '@push.rocks/smartobject': 1.0.12
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartshell': 3.3.7 '@push.rocks/smartshell': 3.5.0
'@push.rocks/smartyaml': 3.0.4 '@push.rocks/smartyaml': 3.0.4
'@push.rocks/smartserve@2.0.1': '@push.rocks/smartserve@2.0.1':
@@ -6249,13 +6261,13 @@ snapshots:
- bufferutil - bufferutil
- utf-8-validate - utf-8-validate
'@push.rocks/smartshell@3.3.7': '@push.rocks/smartshell@3.5.0':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.1.0
'@push.rocks/smartexit': 2.0.3 '@push.rocks/smartexit': 2.0.3
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.4
'@types/which': 3.0.4 '@types/which': 3.0.4
which: 6.0.1 which: 7.0.0
'@push.rocks/smartspawn@3.0.3': '@push.rocks/smartspawn@3.0.3':
dependencies: dependencies:
@@ -9513,7 +9525,7 @@ snapshots:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0
which@6.0.1: which@7.0.0:
dependencies: dependencies:
isexe: 4.0.0 isexe: 4.0.0
+10
View File
@@ -193,12 +193,22 @@ gitzone format
# Read-only JSON plan # Read-only JSON plan
gitzone format plan --json gitzone format plan --json
# CI-friendly check, exits non-zero when changes or validator errors remain
gitzone format check
# Dry run to preview changes # Dry run to preview changes
gitzone format --dry-run gitzone format --dry-run
# Limit formatter modules
gitzone format --only prettier,packagejson
gitzone format --skip license
# Non-interactive apply # Non-interactive apply
gitzone format --write --yes gitzone format --write --yes
# Deterministic format first, opencode for remaining issues
gitzone format fix
# Plan only (no execution) # Plan only (no execution)
gitzone format --plan-only gitzone format --plan-only
+43 -5
View File
@@ -147,7 +147,7 @@ Targets decide what happens after that:
| --- | --- | | --- | --- |
| `git` | Pushes the release commit and tags, often triggering remote CI release builds | | `git` | Pushes the release commit and tags, often triggering remote CI release builds |
| `npm` | Publishes the package to configured npm registries | | `npm` | Publishes the package to configured npm registries |
| `docker` | Builds and pushes configured Docker images | | `docker` | Delegates container builds and pushes to `tsdocker` |
```bash ```bash
# Preview the resolved release plan # Preview the resolved release plan
@@ -205,7 +205,7 @@ The standard buckets are `Breaking Changes`, `Features`, `Fixes`, `Documentation
## Configuration ## Configuration
All CLI config lives under `@git.zone/cli` in `.smartconfig.json`. CLI workflow config lives under `@git.zone/cli` in `.smartconfig.json`. Docker build and registry behavior lives under `@git.zone/tsdocker` and is used by the Docker release target.
```json ```json
{ {
@@ -237,11 +237,21 @@ All CLI config lives under `@git.zone/cli` in `.smartconfig.json`.
"alreadyPublished": "success" "alreadyPublished": "success"
}, },
"docker": { "docker": {
"enabled": false, "enabled": true,
"images": [] "engine": "tsdocker",
"patterns": [],
"cached": true,
"parallel": true
} }
} }
} }
},
"@git.zone/tsdocker": {
"registries": ["registry.gitlab.com"],
"registryRepoMap": {
"registry.gitlab.com": "myorg/myproject"
},
"platforms": ["linux/amd64", "linux/arm64"]
} }
} }
``` ```
@@ -252,12 +262,29 @@ NPM registries belong only here:
@git.zone/cli.release.targets.npm.registries @git.zone/cli.release.targets.npm.registries
``` ```
Docker registries belong only here and should be registry hosts without `http://` or `https://`:
```text
@git.zone/tsdocker.registries
```
Useful config commands: Useful config commands:
```bash ```bash
# Show current @git.zone/cli config # Show current @git.zone/cli config
gitzone config show --json gitzone config show --json
# Configure project basics, CLI behavior, and release targets interactively
gitzone config project
gitzone config cli
gitzone config release
# Validate schema, legacy keys, release targets, registries, and npm auth
gitzone config doctor
# Use opencode to repair configuration issues found by doctor
gitzone config fix
# Read the npm release target registries # Read the npm release target registries
gitzone config get release.targets.npm.registries gitzone config get release.targets.npm.registries
@@ -282,15 +309,26 @@ gitzone format
# Emit a machine-readable plan # Emit a machine-readable plan
gitzone format plan --json gitzone format plan --json
# Fail when formatting changes or validator errors remain
gitzone format check
# Run a subset of formatters
gitzone format --only prettier,packagejson
# Apply changes # Apply changes
gitzone format --write gitzone format --write
# Apply without prompt # Apply without prompt
gitzone format --write --yes gitzone format --write --yes
# Apply deterministic fixes, then use opencode for remaining issues
gitzone format fix
``` ```
Formatters include cleanup, smartconfig normalization, dependency license checks, package metadata normalization, template updates, `.gitignore`, TypeScript config, Prettier, README existence checks, and configured copy operations. Formatters include cleanup, smartconfig normalization, dependency license checks, package metadata normalization, template updates, `.gitignore`, TypeScript config, Prettier, README existence checks, and configured copy operations.
`gitzone format fix` intentionally lives outside the default format path. Normal format runs stay deterministic; the fix command uses opencode only after deterministic formatters have done what they can.
## Development Services ## Development Services
`gitzone services` manages local Docker-backed services for development projects. `gitzone services` manages local Docker-backed services for development projects.
@@ -404,7 +442,7 @@ gitzone config show --json
## License and Legal Information ## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./license) file. This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file. **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/cli', name: '@git.zone/cli',
version: '2.16.0', version: '2.19.1',
description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.' description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
} }
+102 -132
View File
@@ -1,167 +1,137 @@
import * as plugins from "./plugins.js"; import * as plugins from "./plugins.js";
import * as paths from "./paths.js"; import * as paths from "./paths.js";
import { GitzoneConfig } from "./classes.gitzoneconfig.js"; import {
import { getRawCliMode } from "./helpers.climode.js"; getProcessUserArgv,
getRawCliMode,
parseCliArgv,
} from "./helpers.climode.js";
import { commitinfo } from "./00_commitinfo_data.js";
const gitzoneSmartcli = new plugins.smartcli.Smartcli(); const runParsedCommand = async (argvArg: any): Promise<void> => {
const command = argvArg._?.[0];
export let run = async () => { switch (command) {
const done = plugins.smartpromise.defer(); case undefined:
const rawCliMode = await getRawCliMode(); case "help": {
// get packageInfo
const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
// check for updates
if (rawCliMode.checkUpdates) {
const smartupdateInstance = new plugins.smartupdate.SmartUpdate();
await smartupdateInstance.check(
"gitzone",
projectInfo.npm.version,
"http://gitzone.gitlab.io/gitzone/changelog.html",
);
}
if (rawCliMode.output === "human") {
console.log("---------------------------------------------");
}
gitzoneSmartcli.addVersion(projectInfo.npm.version);
// ======> Standard task <======
/**
* standard task
*/
gitzoneSmartcli.standardCommand().subscribe(async (argvArg) => {
const modStandard = await import("./mod_standard/index.js"); const modStandard = await import("./mod_standard/index.js");
await modStandard.run(argvArg); await modStandard.run(argvArg);
}); break;
}
gitzoneSmartcli.addCommand("help").subscribe(async (argvArg) => { case "commit": {
const modStandard = await import("./mod_standard/index.js");
await modStandard.run(argvArg);
});
// ======> Specific tasks <======
/**
* commit something
*/
gitzoneSmartcli.addCommand("commit").subscribe(async (argvArg) => {
const modCommit = await import("./mod_commit/index.js"); const modCommit = await import("./mod_commit/index.js");
await modCommit.run(argvArg); await modCommit.run(argvArg);
}); break;
}
/** case "release": {
* create a release from pending changelog entries
*/
gitzoneSmartcli.addCommand("release").subscribe(async (argvArg) => {
const modRelease = await import("./mod_release/index.js"); const modRelease = await import("./mod_release/index.js");
await modRelease.run(argvArg); await modRelease.run(argvArg);
}); break;
}
/** case "deprecate": {
* deprecate a package on npm
*/
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();
}); break;
}
/** case "docker": {
* docker
*/
gitzoneSmartcli.addCommand("docker").subscribe(async (argvArg) => {
const modDocker = await import("./mod_docker/index.js"); const modDocker = await import("./mod_docker/index.js");
await modDocker.run(argvArg); await modDocker.run(argvArg);
}); break;
}
/** case "format": {
* Update all files that comply with the gitzone standard
*/
gitzoneSmartcli.addCommand("format").subscribe(async (argvArg) => {
const config = GitzoneConfig.fromCwd();
const modFormat = await import("./mod_format/index.js"); const modFormat = await import("./mod_format/index.js");
// Handle format with options
// Default is dry-mode, use --write/-w to apply changes
await modFormat.run({ await modFormat.run({
...argvArg, ...argvArg,
write: argvArg.write || argvArg.w, write: argvArg.write || argvArg.w,
dryRun: argvArg["dry-run"], dryRun: argvArg["dry-run"],
yes: argvArg.yes, yes: argvArg.yes || argvArg.y,
planOnly: argvArg["plan-only"], planOnly: argvArg["plan-only"] || argvArg.planOnly,
savePlan: argvArg["save-plan"], savePlan: argvArg["save-plan"] || argvArg.savePlan,
fromPlan: argvArg["from-plan"], fromPlan: argvArg["from-plan"] || argvArg.fromPlan,
detailed: argvArg.detailed, detailed: argvArg.detailed,
interactive: argvArg.interactive !== false, interactive: argvArg.interactive !== false,
verbose: argvArg.verbose, verbose: argvArg.verbose,
diff: argvArg.diff, diff: argvArg.diff,
}); });
}); break;
}
/** case "meta": {
* run meta commands
*/
gitzoneSmartcli.addCommand("meta").subscribe(async (argvArg) => {
const config = GitzoneConfig.fromCwd();
const modMeta = await import("./mod_meta/index.js"); const modMeta = await import("./mod_meta/index.js");
modMeta.run(argvArg); await modMeta.run(argvArg);
}); break;
}
/** case "open": {
* open assets
*/
gitzoneSmartcli.addCommand("open").subscribe(async (argvArg) => {
const modOpen = await import("./mod_open/index.js"); const modOpen = await import("./mod_open/index.js");
modOpen.run(argvArg); await modOpen.run(argvArg);
}); break;
}
/** case "template": {
* add a readme to a project
*/
gitzoneSmartcli.addCommand("template").subscribe(async (argvArg) => {
const modTemplate = await import("./mod_template/index.js"); const modTemplate = await import("./mod_template/index.js");
modTemplate.run(argvArg); await modTemplate.run(argvArg);
}); break;
}
/** case "start": {
* start working on a project const modStart = await import("./mod_start/index.js");
*/ await modStart.run(argvArg);
gitzoneSmartcli.addCommand("start").subscribe(async (argvArg) => { break;
const modTemplate = await import("./mod_start/index.js"); }
modTemplate.run(argvArg); case "helpers": {
});
gitzoneSmartcli.addCommand("helpers").subscribe(async (argvArg) => {
const modHelpers = await import("./mod_helpers/index.js"); const modHelpers = await import("./mod_helpers/index.js");
modHelpers.run(argvArg); await modHelpers.run(argvArg);
}); break;
}
/** case "tools": {
* manage the global @git.zone toolchain
*/
gitzoneSmartcli.addCommand("tools").subscribe(async (argvArg) => {
const modTools = await import("./mod_tools/index.js"); const modTools = await import("./mod_tools/index.js");
await modTools.run(argvArg); await modTools.run(argvArg);
}); break;
}
/** case "config": {
* manage release configuration
*/
gitzoneSmartcli.addCommand("config").subscribe(async (argvArg) => {
const modConfig = await import("./mod_config/index.js"); const modConfig = await import("./mod_config/index.js");
await modConfig.run(argvArg); await modConfig.run(argvArg);
}); break;
}
/** case "services": {
* manage development services (MongoDB, S3/MinIO)
*/
gitzoneSmartcli.addCommand("services").subscribe(async (argvArg) => {
const modServices = await import("./mod_services/index.js"); const modServices = await import("./mod_services/index.js");
await modServices.run(argvArg); await modServices.run(argvArg);
}); break;
}
// start parsing of the cli default: {
gitzoneSmartcli.startParse(); const modStandard = await import("./mod_standard/index.js");
return await done.promise; await modStandard.run(argvArg);
}
}
};
export let run = async () => {
const rawCliMode = await getRawCliMode();
// get packageInfo
const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
const projectInfoVersion = (projectInfo.npm as any)?.version;
const packageVersion =
typeof projectInfoVersion === "string" && projectInfoVersion.length > 0
? projectInfoVersion
: commitinfo.version;
// check for updates
if (rawCliMode.checkUpdates) {
const smartupdateInstance = new plugins.smartupdate.SmartUpdate();
try {
await smartupdateInstance.check(
"gitzone",
packageVersion,
"http://gitzone.gitlab.io/gitzone/changelog.html",
);
} catch {
// Update checks must never block actual CLI commands.
}
}
if (rawCliMode.output === "human") {
console.log("---------------------------------------------");
}
const argvArg = parseCliArgv(getProcessUserArgv());
if (argvArg.v || argvArg.version) {
console.log(packageVersion);
return;
}
await runParsedCommand(argvArg);
}; };
+36 -1
View File
@@ -88,6 +88,41 @@ const parseRawArgv = (argv: string[]): TArgSource => {
return parsedArgv; return parsedArgv;
}; };
export const parseCliArgv = parseRawArgv;
export const getProcessUserArgv = (): string[] => {
const rawArgv = process.argv;
const argv0Base = (rawArgv[0] || "").split(/[\\/]/).pop()?.toLowerCase();
const runtimeNames = new Set([
"node",
"node.exe",
"nodejs",
"nodejs.exe",
"bun",
"bun.exe",
"deno",
"deno.exe",
"tsx",
"tsx.exe",
"ts-node",
"ts-node.exe",
]);
if (!runtimeNames.has(argv0Base || "")) {
return rawArgv.slice();
}
const firstUserArg = rawArgv[1] || "";
const firstUserArgLooksLikeScript =
firstUserArg.includes("/") ||
firstUserArg.endsWith(".js") ||
firstUserArg.endsWith(".ts") ||
firstUserArg.endsWith(".mjs") ||
firstUserArg.endsWith(".cjs");
return rawArgv.slice(firstUserArgLooksLikeScript ? 2 : 1);
};
const normalizeOutputMode = (value: unknown): TCliOutputMode | undefined => { const normalizeOutputMode = (value: unknown): TCliOutputMode | undefined => {
if (value === "human" || value === "plain" || value === "json") { if (value === "human" || value === "plain" || value === "json") {
return value; return value;
@@ -171,7 +206,7 @@ export const getCliMode = async (
export const getRawCliMode = async (): Promise<ICliMode> => { export const getRawCliMode = async (): Promise<ICliMode> => {
const cliConfig = await getCliModeConfig(); const cliConfig = await getCliModeConfig();
const rawArgv = parseRawArgv(process.argv.slice(2)); const rawArgv = parseRawArgv(getProcessUserArgv());
return resolveCliMode(rawArgv, cliConfig); return resolveCliMode(rawArgv, cliConfig);
}; };
+57 -3
View File
@@ -19,6 +19,38 @@ const ensureObject = (parent: Record<string, any>, key: string): Record<string,
return parent[key]; return parent[key];
}; };
const normalizeRegistryList = (registries: unknown[]): string[] => {
const result: string[] = [];
for (const registry of registries) {
if (typeof registry !== "string" || !registry.trim()) {
continue;
}
const normalizedRegistry = normalizeRegistryUrl(registry);
if (!result.includes(normalizedRegistry)) {
result.push(normalizedRegistry);
}
}
return result;
};
const migrateLegacyReleaseArray = (smartconfigJson: Record<string, any>): boolean => {
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
if (!Array.isArray(cliConfig.release)) {
return false;
}
const registries = normalizeRegistryList(cliConfig.release);
cliConfig.release = {
targets: {
npm: {
enabled: registries.length > 0,
registries,
},
},
};
return true;
};
const migrateNamespaceKeys = (smartconfigJson: Record<string, any>): boolean => { const migrateNamespaceKeys = (smartconfigJson: Record<string, any>): boolean => {
let migrated = false; let migrated = false;
const migrations = [ const migrations = [
@@ -50,9 +82,9 @@ const migrateNamespaceKeys = (smartconfigJson: Record<string, any>): boolean =>
const migrateToV2 = (smartconfigJson: Record<string, any>): boolean => { const migrateToV2 = (smartconfigJson: Record<string, any>): boolean => {
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE); const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
let migrated = migrateLegacyReleaseArray(smartconfigJson);
const releaseConfig = ensureObject(cliConfig, "release"); const releaseConfig = ensureObject(cliConfig, "release");
let migrated = false;
const targets = ensureObject(releaseConfig, "targets"); const targets = ensureObject(releaseConfig, "targets");
const shipzoneConfig = smartconfigJson["@ship.zone/szci"]; const shipzoneConfig = smartconfigJson["@ship.zone/szci"];
@@ -68,8 +100,10 @@ const migrateToV2 = (smartconfigJson: Record<string, any>): boolean => {
migrated = true; migrated = true;
} }
if (isPlainObject(releaseConfig.docker) && !isPlainObject(targets.docker)) { if (isPlainObject(releaseConfig.docker)) {
targets.docker = releaseConfig.docker; targets.docker = isPlainObject(targets.docker)
? { ...releaseConfig.docker, ...targets.docker }
: releaseConfig.docker;
delete releaseConfig.docker; delete releaseConfig.docker;
migrated = true; migrated = true;
} }
@@ -141,11 +175,27 @@ const migrateToV2 = (smartconfigJson: Record<string, any>): boolean => {
if (dockerTarget.enabled === undefined) { if (dockerTarget.enabled === undefined) {
dockerTarget.enabled = true; dockerTarget.enabled = true;
} }
dockerTarget.engine = "tsdocker";
} }
delete releaseConfig.steps; delete releaseConfig.steps;
migrated = true; migrated = true;
} }
if (isPlainObject(targets.docker)) {
if (targets.docker.images) {
delete targets.docker.images;
migrated = true;
}
if (targets.docker.engine !== "tsdocker") {
targets.docker.engine = "tsdocker";
migrated = true;
}
if (!Array.isArray(targets.docker.patterns)) {
targets.docker.patterns = [];
migrated = true;
}
}
if (releaseConfig.changelog) { if (releaseConfig.changelog) {
delete releaseConfig.changelog; delete releaseConfig.changelog;
migrated = true; migrated = true;
@@ -174,6 +224,10 @@ export const migrateSmartconfigData = (
const fromVersion = typeof cliConfig.schemaVersion === "number" ? cliConfig.schemaVersion : 1; const fromVersion = typeof cliConfig.schemaVersion === "number" ? cliConfig.schemaVersion : 1;
let currentVersion = fromVersion; let currentVersion = fromVersion;
if (targetVersion >= 2) {
migrated = migrateLegacyReleaseArray(smartconfigJson) || migrated;
}
if (currentVersion < 2 && targetVersion >= 2) { if (currentVersion < 2 && targetVersion >= 2) {
migrated = migrateToV2(smartconfigJson) || migrated; migrated = migrateToV2(smartconfigJson) || migrated;
currentVersion = 2; currentVersion = 2;
+20 -3
View File
@@ -52,7 +52,12 @@ export interface IReleaseNpmTargetConfig {
export interface IReleaseDockerTargetConfig { export interface IReleaseDockerTargetConfig {
enabled?: boolean; enabled?: boolean;
images?: string[]; engine?: "tsdocker";
patterns?: string[];
cached?: boolean;
parallel?: boolean | number;
context?: string;
noBuild?: boolean;
} }
export interface IReleaseWorkflowConfig { export interface IReleaseWorkflowConfig {
@@ -109,7 +114,12 @@ export interface IResolvedReleaseWorkflow {
npmAccessLevel: "public" | "private"; npmAccessLevel: "public" | "private";
npmAlreadyPublished: "success" | "error"; npmAlreadyPublished: "success" | "error";
dockerEnabled: boolean; dockerEnabled: boolean;
dockerImages: string[]; dockerEngine: "tsdocker";
dockerPatterns: string[];
dockerCached: boolean;
dockerParallel: boolean | number;
dockerContext?: string;
dockerNoBuild: boolean;
} }
interface ICliWorkflowConfig { interface ICliWorkflowConfig {
@@ -382,6 +392,13 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
npmAccessLevel: npmConfig.accessLevel || "public", npmAccessLevel: npmConfig.accessLevel || "public",
npmAlreadyPublished: npmConfig.alreadyPublished || "success", npmAlreadyPublished: npmConfig.alreadyPublished || "success",
dockerEnabled, dockerEnabled,
dockerImages: dockerConfig.images || [], dockerEngine: "tsdocker",
dockerPatterns: Array.isArray(dockerConfig.patterns) ? dockerConfig.patterns : [],
dockerCached: dockerConfig.cached ?? false,
dockerParallel: dockerConfig.parallel ?? false,
dockerContext: typeof dockerConfig.context === "string" && dockerConfig.context.trim()
? dockerConfig.context.trim()
: undefined,
dockerNoBuild: dockerConfig.noBuild ?? false,
}; };
}; };
+1357 -25
View File
File diff suppressed because it is too large Load Diff
+13 -1
View File
@@ -1,6 +1,10 @@
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.js';
import { FormatContext } from './classes.formatcontext.js'; import { FormatContext } from './classes.formatcontext.js';
import type { IPlannedChange, ICheckResult } from './interfaces.format.js'; import type {
IPlannedChange,
ICheckResult,
IFormatWarning,
} from './interfaces.format.js';
import { Project } from '../classes.project.js'; import { Project } from '../classes.project.js';
import { FormatStats } from './classes.formatstats.js'; import { FormatStats } from './classes.formatstats.js';
@@ -19,6 +23,14 @@ export abstract class BaseFormatter {
abstract analyze(): Promise<IPlannedChange[]>; abstract analyze(): Promise<IPlannedChange[]>;
abstract applyChange(change: IPlannedChange): Promise<void>; abstract applyChange(change: IPlannedChange): Promise<void>;
get runsWithoutChanges(): boolean {
return false;
}
async validate(): Promise<IFormatWarning[]> {
return [];
}
async execute(changes: IPlannedChange[]): Promise<void> { async execute(changes: IPlannedChange[]): Promise<void> {
const startTime = this.stats.moduleStartTime(this.name); const startTime = this.stats.moduleStartTime(this.name);
this.stats.startModule(this.name); this.stats.startModule(this.name);
+66 -4
View File
@@ -1,7 +1,11 @@
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.js';
import { FormatContext } from './classes.formatcontext.js'; import { FormatContext } from './classes.formatcontext.js';
import { BaseFormatter } from './classes.baseformatter.js'; import { BaseFormatter } from './classes.baseformatter.js';
import type { IFormatPlan, IPlannedChange } from './interfaces.format.js'; import type {
IFormatPlan,
IPlannedChange,
IFormatWarning,
} from './interfaces.format.js';
import { getModuleIcon } from './interfaces.format.js'; import { getModuleIcon } from './interfaces.format.js';
import { logger } from '../gitzone.logging.js'; import { logger } from '../gitzone.logging.js';
import { DiffReporter } from './classes.diffreporter.js'; import { DiffReporter } from './classes.diffreporter.js';
@@ -42,15 +46,21 @@ export class FormatPlanner {
break; break;
} }
} }
const warnings = await module.validate();
plan.warnings.push(...warnings);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
plan.warnings.push({ plan.warnings.push({
level: 'error', level: 'error',
message: `Failed to analyze module ${module.name}: ${error.message}`, message: `Failed to analyze module ${module.name}: ${errorMessage}`,
module: module.name, module: module.name,
}); });
} }
} }
plan.warnings.push(...this.detectConflictingChanges(plan.changes));
plan.summary.totalFiles = plan.summary.totalFiles =
plan.summary.filesAdded + plan.summary.filesAdded +
plan.summary.filesModified + plan.summary.filesModified +
@@ -65,11 +75,12 @@ export class FormatPlanner {
context: FormatContext, context: FormatContext,
): Promise<void> { ): Promise<void> {
const startTime = Date.now(); const startTime = Date.now();
const changesByModule = this.groupChangesByModule(plan.changes);
for (const module of modules) { for (const module of modules) {
const changes = this.plannedChanges.get(module.name) || []; const changes = changesByModule.get(module.name) || [];
if (changes.length > 0) { if (changes.length > 0 || module.runsWithoutChanges) {
logger.log('info', `Executing ${module.name} formatter...`); logger.log('info', `Executing ${module.name} formatter...`);
await module.execute(changes); await module.execute(changes);
} }
@@ -138,4 +149,55 @@ export class FormatPlanner {
return '❌'; return '❌';
} }
} }
private groupChangesByModule(
changes: IPlannedChange[],
): Map<string, IPlannedChange[]> {
const changesByModule = new Map<string, IPlannedChange[]>();
for (const change of changes) {
const moduleChanges = changesByModule.get(change.module) || [];
moduleChanges.push(change);
changesByModule.set(change.module, moduleChanges);
}
return changesByModule;
}
private detectConflictingChanges(
changes: IPlannedChange[],
): IFormatWarning[] {
const warnings: IFormatWarning[] = [];
const changesByPath = new Map<string, IPlannedChange[]>();
for (const change of changes) {
if (!change.path || change.path === '<various files>') {
continue;
}
const pathChanges = changesByPath.get(change.path) || [];
pathChanges.push(change);
changesByPath.set(change.path, pathChanges);
}
for (const [path, pathChanges] of changesByPath) {
const modules = [...new Set(pathChanges.map((change) => change.module))];
if (modules.length < 2) {
continue;
}
const hasDelete = pathChanges.some((change) => change.type === 'delete');
const plannedContents = pathChanges
.map((change) => change.content)
.filter((content): content is string => content !== undefined);
const uniqueContents = new Set(plannedContents);
const level = hasDelete || uniqueContents.size > 1 ? 'warning' : 'info';
warnings.push({
level,
module: 'planner',
message: `Multiple formatters plan changes for ${path}: ${modules.join(', ')}. They will run in formatter order.`,
});
}
return warnings;
}
} }
+43 -15
View File
@@ -1,5 +1,5 @@
import { BaseFormatter } from '../classes.baseformatter.js'; import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js'; import type { IFormatWarning, IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js'; import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js'; import * as paths from '../../paths.js';
import { logger } from '../../gitzone.logging.js'; import { logger } from '../../gitzone.logging.js';
@@ -11,6 +11,10 @@ export class LicenseFormatter extends BaseFormatter {
return 'license'; return 'license';
} }
get runsWithoutChanges(): boolean {
return true;
}
async analyze(): Promise<IPlannedChange[]> { async analyze(): Promise<IPlannedChange[]> {
// License formatter only checks for incompatible licenses // License formatter only checks for incompatible licenses
// It does not modify any files, so return empty array // It does not modify any files, so return empty array
@@ -18,29 +22,34 @@ export class LicenseFormatter extends BaseFormatter {
return []; return [];
} }
async validate(): Promise<IFormatWarning[]> {
const result = await this.checkLicenses();
if (!result || result.failingModules.length === 0) {
return [];
}
return [
{
level: 'error',
module: this.name,
message: `License check failed for ${result.failingModules.length} module(s): ${result.failingModules
.map((failedModule) => `${failedModule.name} (${failedModule.license})`)
.join(', ')}`,
},
];
}
async execute(changes: IPlannedChange[]): Promise<void> { async execute(changes: IPlannedChange[]): Promise<void> {
const startTime = this.stats.moduleStartTime(this.name); const startTime = this.stats.moduleStartTime(this.name);
this.stats.startModule(this.name); this.stats.startModule(this.name);
try { try {
// Check if node_modules exists const licenseCheckResult = await this.checkLicenses();
const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules'); if (!licenseCheckResult) {
const nodeModulesExists = await plugins.smartfs
.directory(nodeModulesPath)
.exists();
if (!nodeModulesExists) {
logger.log('warn', 'No node_modules found. Skipping license check'); logger.log('warn', 'No node_modules found. Skipping license check');
return; return;
} }
// Run license check
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
const licenseCheckResult = await licenseChecker.excludeLicenseWithinPath(
paths.cwd,
INCOMPATIBLE_LICENSES,
);
if (licenseCheckResult.failingModules.length === 0) { if (licenseCheckResult.failingModules.length === 0) {
logger.log('info', 'License check passed - no incompatible licenses found'); logger.log('info', 'License check passed - no incompatible licenses found');
} else { } else {
@@ -59,4 +68,23 @@ export class LicenseFormatter extends BaseFormatter {
async applyChange(change: IPlannedChange): Promise<void> { async applyChange(change: IPlannedChange): Promise<void> {
// No file changes for license formatter // No file changes for license formatter
} }
private async checkLicenses(): Promise<{
failingModules: Array<{ name: string; license: string }>;
} | undefined> {
const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
const nodeModulesExists = await plugins.smartfs
.directory(nodeModulesPath)
.exists();
if (!nodeModulesExists) {
return undefined;
}
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
return await licenseChecker.excludeLicenseWithinPath(
paths.cwd,
INCOMPATIBLE_LICENSES,
);
}
} }
+48 -60
View File
@@ -56,7 +56,8 @@ export class PrettierFormatter extends BaseFormatter {
); );
allFiles.push(...filteredFiles); allFiles.push(...filteredFiles);
} catch (error) { } catch (error) {
logVerbose(`Skipping directory ${dir}: ${error.message}`); const errorMessage = error instanceof Error ? error.message : String(error);
logVerbose(`Skipping directory ${dir}: ${errorMessage}`);
} }
} }
@@ -72,7 +73,8 @@ export class PrettierFormatter extends BaseFormatter {
const rootLevelFiles = rootFiles.filter((f) => !f.includes('/')); const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
allFiles.push(...rootLevelFiles); allFiles.push(...rootLevelFiles);
} catch (error) { } catch (error) {
logVerbose(`Skipping pattern ${pattern}: ${error.message}`); const errorMessage = error instanceof Error ? error.message : String(error);
logVerbose(`Skipping pattern ${pattern}: ${errorMessage}`);
} }
} }
@@ -89,20 +91,46 @@ export class PrettierFormatter extends BaseFormatter {
} }
} catch (error) { } catch (error) {
// Skip files that can't be accessed // Skip files that can't be accessed
logVerbose(`Skipping ${file} - cannot access: ${error.message}`); const errorMessage = error instanceof Error ? error.message : String(error);
logVerbose(`Skipping ${file} - cannot access: ${errorMessage}`);
} }
} }
const prettier = await import('prettier');
const prettierConfig = await this.getPrettierConfig();
for (const file of validFiles) { for (const file of validFiles) {
try {
const fileExt = plugins.path.extname(file).toLowerCase();
if (!fileExt) {
continue;
}
const content = (await plugins.smartfs
.file(file)
.encoding('utf8')
.read()) as string;
const formatted = await prettier.format(content, {
filepath: file,
...prettierConfig,
});
if (formatted !== content) {
changes.push({ changes.push({
type: 'modify', type: 'modify',
path: file, path: file,
module: this.name, module: this.name,
description: 'Format with Prettier', description: 'Format with Prettier',
content: formatted,
}); });
} }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logVerbose(`Skipping Prettier analysis for ${file}: ${errorMessage}`);
}
}
logger.log('info', `Found ${changes.length} files to format with Prettier`); logger.log('info', `Found ${changes.length} files needing Prettier`);
return changes; return changes;
} }
@@ -127,9 +155,10 @@ export class PrettierFormatter extends BaseFormatter {
this.stats.recordFileOperation(this.name, change.type, true); this.stats.recordFileOperation(this.name, change.type, true);
} catch (error) { } catch (error) {
this.stats.recordFileOperation(this.name, change.type, false); this.stats.recordFileOperation(this.name, change.type, false);
const errorMessage = error instanceof Error ? error.message : String(error);
logger.log( logger.log(
'error', 'error',
`Failed to format ${change.path}: ${error.message}`, `Failed to format ${change.path}: ${errorMessage}`,
); );
// Don't throw - continue with other files // Don't throw - continue with other files
} }
@@ -192,28 +221,32 @@ export class PrettierFormatter extends BaseFormatter {
logVerbose(`No formatting changes for ${change.path}`); logVerbose(`No formatting changes for ${change.path}`);
} }
} catch (prettierError) { } catch (prettierError) {
const prettierErrorMessage = prettierError instanceof Error
? prettierError.message
: String(prettierError);
// Check if it's a parser error // Check if it's a parser error
if ( if (prettierErrorMessage.includes('No parser could be inferred')) {
prettierError.message && logVerbose(`Skipping ${change.path} - ${prettierErrorMessage}`);
prettierError.message.includes('No parser could be inferred')
) {
logVerbose(`Skipping ${change.path} - ${prettierError.message}`);
return; // Skip this file silently return; // Skip this file silently
} }
throw prettierError; throw prettierError;
} }
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
// Log the full error stack for debugging mkdir issues // Log the full error stack for debugging mkdir issues
if (error.message && error.message.includes('mkdir')) { if (errorMessage.includes('mkdir')) {
logger.log( logger.log(
'error', 'error',
`Failed to format ${change.path}: ${error.message}`, `Failed to format ${change.path}: ${errorMessage}`,
); );
logger.log('error', `Error stack: ${error.stack}`); if (errorStack) {
logger.log('error', `Error stack: ${errorStack}`);
}
} else { } else {
logger.log( logger.log(
'error', 'error',
`Failed to format ${change.path}: ${error.message}`, `Failed to format ${change.path}: ${errorMessage}`,
); );
} }
throw error; throw error;
@@ -234,52 +267,7 @@ export class PrettierFormatter extends BaseFormatter {
}); });
} }
/**
* Override check() to compute diffs on-the-fly by running prettier
*/
async check(): Promise<ICheckResult> { async check(): Promise<ICheckResult> {
const changes = await this.analyze(); return await super.check();
const diffs: ICheckResult['diffs'] = [];
for (const change of changes) {
if (change.type !== 'modify') continue;
try {
// Read current content
const currentContent = (await plugins.smartfs
.file(change.path)
.encoding('utf8')
.read()) as string;
// Skip files without extension (prettier can't infer parser)
const fileExt = plugins.path.extname(change.path).toLowerCase();
if (!fileExt) continue;
// Format with prettier to get what it would produce
const prettier = await import('prettier');
const formatted = await prettier.format(currentContent, {
filepath: change.path,
...(await this.getPrettierConfig()),
});
// Only add to diffs if content differs
if (formatted !== currentContent) {
diffs.push({
path: change.path,
type: 'modify',
before: currentContent,
after: formatted,
});
}
} catch (error) {
// Skip files that can't be processed
logVerbose(`Skipping diff for ${change.path}: ${error.message}`);
}
}
return {
hasDiff: diffs.length > 0,
diffs,
};
} }
} }
+289 -8
View File
@@ -22,6 +22,7 @@ import { TsconfigFormatter } from "./formatters/tsconfig.formatter.js";
import { PrettierFormatter } from "./formatters/prettier.formatter.js"; import { PrettierFormatter } from "./formatters/prettier.formatter.js";
import { ReadmeFormatter } from "./formatters/readme.formatter.js"; import { ReadmeFormatter } from "./formatters/readme.formatter.js";
import { CopyFormatter } from "./formatters/copy.formatter.js"; import { CopyFormatter } from "./formatters/copy.formatter.js";
import type { ICheckResult, IFormatPlan } from "./interfaces.format.js";
/** /**
* Rename npmextra.json or smartconfig.json to .smartconfig.json * Rename npmextra.json or smartconfig.json to .smartconfig.json
@@ -94,9 +95,39 @@ const getFormatConfig = async () => {
}; };
}; };
const normalizeModuleList = (value: unknown): string[] => {
if (Array.isArray(value)) {
return value.flatMap((item) => normalizeModuleList(item));
}
if (typeof value !== "string") {
return [];
}
return value
.split(",")
.map((item) => item.trim())
.filter(Boolean);
};
const getPlanStatus = (plan: IFormatPlan) => {
const errorWarnings = plan.warnings.filter(
(warning) => warning.level === "error",
);
const hasChanges = plan.summary.totalFiles > 0;
const hasErrors = errorWarnings.length > 0;
return {
ok: !hasChanges && !hasErrors,
hasChanges,
hasErrors,
errorCount: errorWarnings.length,
};
};
const createActiveFormatters = async (options: { const createActiveFormatters = async (options: {
interactive: boolean; interactive: boolean;
jsonOutput: boolean; jsonOutput: boolean;
only?: string[];
skip?: string[];
}) => { }) => {
const project = await Project.fromCwd({ requireProjectType: false }); const project = await Project.fromCwd({ requireProjectType: false });
const context = new FormatContext(options); const context = new FormatContext(options);
@@ -107,11 +138,19 @@ const createActiveFormatters = async (options: {
([, FormatterClass]) => new FormatterClass(context, project), ([, FormatterClass]) => new FormatterClass(context, project),
); );
const onlyModules = options.only?.length
? options.only
: formatConfig.modules.only;
const skipModules = [
...formatConfig.modules.skip,
...(options.skip || []),
];
const activeFormatters = formatters.filter((formatter) => { const activeFormatters = formatters.filter((formatter) => {
if (formatConfig.modules.only.length > 0) { if (onlyModules.length > 0) {
return formatConfig.modules.only.includes(formatter.name); return onlyModules.includes(formatter.name);
} }
if (formatConfig.modules.skip.includes(formatter.name)) { if (skipModules.includes(formatter.name)) {
return false; return false;
} }
return true; return true;
@@ -129,11 +168,15 @@ const buildFormatPlan = async (options: {
fromPlan?: string; fromPlan?: string;
interactive: boolean; interactive: boolean;
jsonOutput: boolean; jsonOutput: boolean;
only?: string[];
skip?: string[];
}) => { }) => {
const { context, planner, formatConfig, activeFormatters } = const { context, planner, formatConfig, activeFormatters } =
await createActiveFormatters({ await createActiveFormatters({
interactive: options.interactive, interactive: options.interactive,
jsonOutput: options.jsonOutput, jsonOutput: options.jsonOutput,
only: options.only,
skip: options.skip,
}); });
const plan = options.fromPlan const plan = options.fromPlan
@@ -167,6 +210,182 @@ const serializePlan = (plan: any) => {
}; };
}; };
const buildFormatFixPrompt = (
plan: IFormatPlan,
extraInstructions: string,
): string => {
const promptParts = [
"Other /c-* commands can be found at ~/.config/opencode/commands/*",
"# gitzone format fix",
"",
`Working directory: ${process.cwd()}`,
"",
"Repair project formatting so `gitzone format check --json` passes.",
"",
"Rules:",
"- Read `.smartconfig.json`, `package.json`, `tsconfig.json`, and the current format plan before editing.",
"- Prefer deterministic gitzone standards, bundled assets, and existing project conventions.",
"- Keep changes focused on formatting, metadata normalization, templates, and config consistency.",
"- Do not commit, release, install dependencies, or modify unrelated files.",
"- Use pnpm commands only if commands are needed.",
"- Run `gitzone format --write --yes` after changes.",
"- Run `gitzone format check --json` after changes and keep fixing until it passes.",
"- Run `git diff --check` after changes to catch whitespace problems.",
"",
"Current format plan:",
JSON.stringify(serializePlan(plan), null, 2),
];
if (extraInstructions) {
promptParts.push("", "Additional user instructions:", extraInstructions);
}
return promptParts.join("\n");
};
const handleFormatFix = async (
options: Record<string, any>,
mode: ICliMode,
): Promise<void> => {
if (mode.json) {
printJson({
ok: false,
error:
"JSON output is not supported for `gitzone format fix`. Use `gitzone format check --json` for machine-readable diagnostics.",
});
process.exitCode = 1;
return;
}
const extraInstructions = (options._?.slice(2).join(" ") || "").trim();
const force = Boolean(options.force);
const autoApprove = Boolean(options.yes || mode.yes);
const formatConfig = await getFormatConfig();
const interactive =
options.interactive ?? (mode.interactive && formatConfig.interactive);
const only = normalizeModuleList(options.only);
const skip = normalizeModuleList(options.skip);
const buildCurrentPlan = async () => {
return await buildFormatPlan({
interactive,
jsonOutput: false,
only,
skip,
});
};
logger.log("info", "Analyzing project for format fixes...");
let { plan } = await buildCurrentPlan();
let status = getPlanStatus(plan);
if (status.ok && !extraInstructions && !force) {
logger.log(
"success",
"Format check found no issues. Use `gitzone format fix --force` to run opencode anyway.",
);
return;
}
if (!autoApprove) {
if (!mode.interactive) {
throw new Error(
"Format fix requires an interactive terminal or `-y` to run non-interactively.",
);
}
const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
`Run format fixes? (${plan.summary.totalFiles} planned change(s), ${status.errorCount} error warning(s))`,
true,
);
if (!confirmed) {
logger.log("info", "Format fix cancelled.");
return;
}
}
if (status.hasChanges) {
logger.log("info", "Applying deterministic format changes first...");
await run({
_: ["format"],
write: true,
yes: true,
interactive: false,
verbose: options.verbose,
detailed: options.detailed,
only: options.only,
skip: options.skip,
});
({ plan } = await buildCurrentPlan());
status = getPlanStatus(plan);
if (status.ok && !extraInstructions && !force) {
logger.log("success", "Format fix completed successfully.");
return;
}
}
const opencodeArgs = [
"run",
"--title",
"gitzone format fix",
"--dir",
process.cwd(),
];
if (autoApprove) {
opencodeArgs.push("--dangerously-skip-permissions");
}
opencodeArgs.push(buildFormatFixPrompt(plan, extraInstructions));
logger.log("info", "Starting opencode format fix...");
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: "bash",
sourceFilePaths: [],
});
let result: plugins.smartshell.IExecResult;
try {
result = await smartshellInstance.execSpawn("opencode", opencodeArgs, {
stdio: "inherit",
});
} catch (error) {
throw new Error(
`Failed to run opencode: ${error instanceof Error ? error.message : String(error)}`,
);
}
if (result.exitCode !== 0) {
logger.log("error", `opencode exited with code ${result.exitCode}`);
process.exitCode = result.exitCode || 1;
return;
}
logger.log("info", "Running deterministic format pass after opencode...");
await run({
_: ["format"],
write: true,
yes: true,
interactive: false,
verbose: options.verbose,
detailed: options.detailed,
only: options.only,
skip: options.skip,
});
const { planner: finalPlanner, plan: finalPlan } = await buildCurrentPlan();
await finalPlanner.displayPlan(finalPlan, options.detailed);
const finalStatus = getPlanStatus(finalPlan);
if (finalStatus.ok) {
logger.log("success", "Format fix completed successfully.");
return;
}
logger.log(
"error",
`Format fix left ${finalPlan.summary.totalFiles} planned change(s) and ${finalStatus.errorCount} error warning(s).`,
);
process.exitCode = 1;
};
export let run = async ( export let run = async (
options: { options: {
write?: boolean; write?: boolean;
@@ -194,8 +413,25 @@ export let run = async (
setVerboseMode(true); setVerboseMode(true);
} }
if (subcommand === "fix") {
await handleFormatFix(options, mode);
return;
}
const shouldWrite = options.write ?? options.dryRun === false; const shouldWrite = options.write ?? options.dryRun === false;
const treatAsPlan = subcommand === "plan"; const treatAsPlan = subcommand === "plan";
const treatAsCheck = subcommand === "check" || Boolean(options.check);
if (treatAsCheck && shouldWrite) {
const error = "`gitzone format check` is read-only and cannot be combined with --write.";
if (mode.json) {
printJson({ ok: false, error });
} else {
logger.log("error", error);
}
process.exitCode = 1;
return;
}
if (mode.json && shouldWrite) { if (mode.json && shouldWrite) {
printJson({ printJson({
@@ -212,7 +448,9 @@ export let run = async (
const formatConfig = await getFormatConfig(); const formatConfig = await getFormatConfig();
const interactive = const interactive =
options.interactive ?? (mode.interactive && formatConfig.interactive); options.interactive ?? (mode.interactive && formatConfig.interactive);
const autoApprove = options.yes ?? formatConfig.autoApprove; const autoApprove = options.yes ?? (mode.yes || formatConfig.autoApprove);
const only = normalizeModuleList(options.only);
const skip = normalizeModuleList(options.skip);
try { try {
const planBuilder = async () => { const planBuilder = async () => {
@@ -220,6 +458,8 @@ export let run = async (
fromPlan: options.fromPlan, fromPlan: options.fromPlan,
interactive, interactive,
jsonOutput: mode.json, jsonOutput: mode.json,
only,
skip,
}); });
}; };
@@ -231,7 +471,16 @@ export let run = async (
: await planBuilder(); : await planBuilder();
if (mode.json) { if (mode.json) {
printJson(serializePlan(plan)); const serializedPlan = serializePlan(plan);
if (treatAsCheck) {
const status = getPlanStatus(plan);
printJson({ ok: status.ok, ...serializedPlan });
if (!status.ok) {
process.exitCode = 1;
}
return;
}
printJson(serializedPlan);
return; return;
} }
@@ -251,6 +500,20 @@ export let run = async (
return; return;
} }
if (treatAsCheck) {
const status = getPlanStatus(plan);
if (status.ok) {
logger.log("success", "Format check passed");
} else {
logger.log(
"error",
`Format check failed: ${plan.summary.totalFiles} planned change(s), ${status.errorCount} error warning(s)`,
);
process.exitCode = 1;
}
return;
}
// Show diffs if explicitly requested or before interactive write confirmation // Show diffs if explicitly requested or before interactive write confirmation
const showDiffs = const showDiffs =
options.diff || (shouldWrite && interactive && !autoApprove); options.diff || (shouldWrite && interactive && !autoApprove);
@@ -314,7 +577,6 @@ export let run = async (
} }
}; };
import type { ICheckResult } from "./interfaces.format.js";
export type { ICheckResult }; export type { ICheckResult };
/** /**
@@ -363,7 +625,7 @@ export function showHelp(mode?: ICliMode): void {
if (mode?.json) { if (mode?.json) {
printJson({ printJson({
command: "format", command: "format",
usage: "gitzone format [plan] [options]", usage: "gitzone format [plan|check|fix] [options]",
description: description:
"Plans formatting changes by default and applies them only with --write.", "Plans formatting changes by default and applies them only with --write.",
flags: [ flags: [
@@ -393,19 +655,33 @@ export function showHelp(mode?: ICliMode): void {
flag: "--diff", flag: "--diff",
description: "Show per-file diffs before applying changes", description: "Show per-file diffs before applying changes",
}, },
{
flag: "--only <modules>",
description: "Run only the comma-separated formatter modules",
},
{
flag: "--skip <modules>",
description: "Skip the comma-separated formatter modules",
},
{
flag: "--force",
description: "Run `format fix` even when the deterministic plan is clean",
},
{ flag: "--json", description: "Emit a read-only format plan as JSON" }, { flag: "--json", description: "Emit a read-only format plan as JSON" },
], ],
examples: [ examples: [
"gitzone format", "gitzone format",
"gitzone format plan --json", "gitzone format plan --json",
"gitzone format check",
"gitzone format --write --yes", "gitzone format --write --yes",
"gitzone format fix",
], ],
}); });
return; return;
} }
console.log(""); console.log("");
console.log("Usage: gitzone format [plan] [options]"); console.log("Usage: gitzone format [plan|check|fix] [options]");
console.log(""); console.log("");
console.log( console.log(
"Plans formatting changes by default and applies them only with --write.", "Plans formatting changes by default and applies them only with --write.",
@@ -424,11 +700,16 @@ export function showHelp(mode?: ICliMode): void {
console.log( console.log(
" --diff Show per-file diffs before applying changes", " --diff Show per-file diffs before applying changes",
); );
console.log(" --only <modules> Run only comma-separated formatter modules");
console.log(" --skip <modules> Skip comma-separated formatter modules");
console.log(" --force Run format fix even when the plan is clean");
console.log(" --json Emit a read-only format plan as JSON"); console.log(" --json Emit a read-only format plan as JSON");
console.log(""); console.log("");
console.log("Examples:"); console.log("Examples:");
console.log(" gitzone format"); console.log(" gitzone format");
console.log(" gitzone format plan --json"); console.log(" gitzone format plan --json");
console.log(" gitzone format check");
console.log(" gitzone format --write --yes"); console.log(" gitzone format --write --yes");
console.log(" gitzone format fix");
console.log(""); console.log("");
} }
+8 -11
View File
@@ -1,3 +1,9 @@
export type IFormatWarning = {
level: 'info' | 'warning' | 'error';
message: string;
module: string;
};
export type IFormatPlan = { export type IFormatPlan = {
summary: { summary: {
totalFiles: number; totalFiles: number;
@@ -5,17 +11,8 @@ export type IFormatPlan = {
filesModified: number; filesModified: number;
filesRemoved: number; filesRemoved: number;
}; };
changes: Array<{ changes: IPlannedChange[];
type: 'create' | 'modify' | 'delete'; warnings: IFormatWarning[];
path: string;
module: string;
description: string;
}>;
warnings: Array<{
level: 'info' | 'warning' | 'error';
message: string;
module: string;
}>;
}; };
export type IPlannedChange = { export type IPlannedChange = {
+44 -21
View File
@@ -107,7 +107,7 @@ export const run = async (argvArg: any) => {
npmResults.push(...(await runNpmTarget(smartshellInstance, workflow))); npmResults.push(...(await runNpmTarget(smartshellInstance, workflow)));
} }
if (workflow.targets.includes("docker")) { if (workflow.targets.includes("docker")) {
dockerResults.push(...(await runDockerTarget(smartshellInstance, workflow, newVersion))); dockerResults.push(...(await runDockerTarget(smartshellInstance, workflow)));
} }
printReleaseSummary(newVersion, gitResults, npmResults, dockerResults); printReleaseSummary(newVersion, gitResults, npmResults, dockerResults);
@@ -262,31 +262,43 @@ async function runNpmTarget(
async function runDockerTarget( async function runDockerTarget(
smartshellInstance: plugins.smartshell.Smartshell, smartshellInstance: plugins.smartshell.Smartshell,
workflow: IResolvedReleaseWorkflow, workflow: IResolvedReleaseWorkflow,
newVersion: string,
): Promise<ITargetResult[]> { ): Promise<ITargetResult[]> {
if (!workflow.dockerEnabled) { if (!workflow.dockerEnabled) {
return [{ target: "docker", status: "skipped", message: "disabled" }]; return [{ target: "docker", status: "skipped", message: "disabled" }];
} }
if (workflow.dockerImages.length === 0) {
return [{ target: "docker", status: "failed", message: "no images configured" }]; const command = buildTsdockerPushCommand(workflow);
const result = await smartshellInstance.exec(command);
const output = `${result.stdout || ""}\n${(result as any).stderr || ""}\n${(result as any).combinedOutput || ""}`;
return [{
target: workflow.dockerPatterns.length > 0
? `tsdocker:${workflow.dockerPatterns.join(",")}`
: "tsdocker",
status: result.exitCode === 0 ? "success" : "failed",
message: result.exitCode === 0 ? undefined : firstMeaningfulLine(output),
}];
} }
const results: ITargetResult[] = []; function buildTsdockerPushCommand(workflow: IResolvedReleaseWorkflow): string {
for (const imageTemplate of workflow.dockerImages) { const commandParts = ["tsdocker", "push"];
const image = imageTemplate.replaceAll("{{version}}", newVersion); if (workflow.dockerNoBuild) {
const buildResult = await smartshellInstance.exec(`docker build -t ${shellQuote(image)} .`); commandParts.push("--no-build");
if (buildResult.exitCode !== 0) {
results.push({ target: image, status: "failed", message: "docker build failed" });
continue;
} }
const pushResult = await smartshellInstance.exec(`docker push ${shellQuote(image)}`); if (workflow.dockerCached) {
results.push({ commandParts.push("--cached");
target: image,
status: pushResult.exitCode === 0 ? "success" : "failed",
message: pushResult.exitCode === 0 ? undefined : "docker push failed",
});
} }
return results; if (workflow.dockerParallel === true) {
commandParts.push("--parallel");
} else if (typeof workflow.dockerParallel === "number" && Number.isFinite(workflow.dockerParallel) && workflow.dockerParallel > 0) {
commandParts.push(`--parallel=${Math.floor(workflow.dockerParallel)}`);
}
if (workflow.dockerContext) {
commandParts.push(`--context=${shellQuote(workflow.dockerContext)}`);
}
for (const pattern of workflow.dockerPatterns) {
commandParts.push(shellQuote(pattern));
}
return commandParts.join(" ");
} }
function isAlreadyPublishedOutput(output: string): boolean { function isAlreadyPublishedOutput(output: string): boolean {
@@ -315,11 +327,22 @@ function printReleasePlan(workflow: IResolvedReleaseWorkflow): void {
console.log(`npm registries: ${workflow.npmRegistries.length > 0 ? workflow.npmRegistries.join(", ") : "none"}`); console.log(`npm registries: ${workflow.npmRegistries.length > 0 ? workflow.npmRegistries.join(", ") : "none"}`);
} }
if (workflow.targets.includes("docker")) { if (workflow.targets.includes("docker")) {
console.log(`docker images: ${workflow.dockerImages.length > 0 ? workflow.dockerImages.join(", ") : "none"}`); console.log(`docker engine: ${workflow.dockerEngine}`);
console.log(`docker patterns: ${workflow.dockerPatterns.length > 0 ? workflow.dockerPatterns.join(", ") : "all Dockerfiles"}`);
console.log(`docker options: ${formatDockerOptions(workflow)}`);
} }
console.log(""); console.log("");
} }
function formatDockerOptions(workflow: IResolvedReleaseWorkflow): string {
const options: string[] = [];
if (workflow.dockerCached) options.push("cached");
if (workflow.dockerParallel) options.push(`parallel=${workflow.dockerParallel === true ? "true" : workflow.dockerParallel}`);
if (workflow.dockerNoBuild) options.push("no-build");
if (workflow.dockerContext) options.push(`context=${workflow.dockerContext}`);
return options.length > 0 ? options.join(", ") : "default";
}
function printReleaseSummary( function printReleaseSummary(
newVersion: string, newVersion: string,
gitResults: ITargetResult[], gitResults: ITargetResult[],
@@ -365,7 +388,7 @@ export function showHelp(mode?: ICliMode): void {
{ flag: "-p, --push", description: "Enable the git release target" }, { flag: "-p, --push", description: "Enable the git release target" },
{ flag: "--target <names>", description: "Release only selected targets: git,npm,docker" }, { flag: "--target <names>", description: "Release only selected targets: git,npm,docker" },
{ flag: "--npm", description: "Enable the npm release target" }, { flag: "--npm", description: "Enable the npm release target" },
{ flag: "--docker", description: "Enable the Docker release target" }, { flag: "--docker", description: "Enable the tsdocker release target" },
{ flag: "--no-publish", description: "Run release core and git target only" }, { flag: "--no-publish", description: "Run release core and git target only" },
{ flag: "--plan", description: "Show resolved workflow without mutating files" }, { flag: "--plan", description: "Show resolved workflow without mutating files" },
], ],
@@ -385,7 +408,7 @@ export function showHelp(mode?: ICliMode): void {
console.log(" -p, --push Enable the git release target"); console.log(" -p, --push Enable the git release target");
console.log(" --target <names> Release only selected targets: git,npm,docker"); console.log(" --target <names> Release only selected targets: git,npm,docker");
console.log(" --npm Enable the npm release target"); console.log(" --npm Enable the npm release target");
console.log(" --docker Enable the Docker release target"); console.log(" --docker Enable the tsdocker release target");
console.log(" --no-publish Run release core and git target only"); console.log(" --no-publish Run release core and git target only");
console.log(" --major|--minor|--patch Override inferred semver level"); console.log(" --major|--minor|--patch Override inferred semver level");
console.log(" --plan Show resolved workflow without mutating files"); console.log(" --plan Show resolved workflow without mutating files");
+1
View File
@@ -202,6 +202,7 @@ export async function showHelp(
console.log(" gitzone commit recommend --json"); console.log(" gitzone commit recommend --json");
console.log(" gitzone release --plan"); console.log(" gitzone release --plan");
console.log(" gitzone format plan --json"); console.log(" gitzone format plan --json");
console.log(" gitzone format check");
console.log(" gitzone services set mongodb,minio"); console.log(" gitzone services set mongodb,minio");
console.log(" gitzone tools update"); console.log(" gitzone tools update");
console.log(""); console.log("");