Compare commits

..

53 Commits

Author SHA1 Message Date
jkunz 261f7ee6b2 v2.14.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-16 18:54:07 +00:00
jkunz fd7a73398c feat(cli): add machine-readable CLI help, recommendation, and configuration flows 2026-04-16 18:54:07 +00:00
jkunz f43f88a3cb v2.13.16
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-16 13:05:47 +00:00
jkunz 4c86ad62fb fix(mod_format): stop package.json formatter from modifying buildDocs and dependency entries 2026-04-16 13:05:47 +00:00
jkunz 4214a1fdf1 v2.13.15
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 19:59:26 +00:00
jkunz 1c33735799 fix(repo): no changes to commit 2026-03-24 19:59:26 +00:00
jkunz 274405e364 v2.13.14
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 19:59:13 +00:00
jkunz bf858c8650 fix(mod_format): move smartconfig file renaming into the formatter orchestrator 2026-03-24 19:59:13 +00:00
jkunz b257c82bd6 v2.13.13
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 19:42:12 +00:00
jkunz 5a1f6d8c76 fix(vscode-template): update VS Code schema matching to use .smartconfig.json 2026-03-24 19:42:12 +00:00
jkunz d44ad6e4e4 v2.13.12
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 16:56:34 +00:00
jkunz 142adfd396 fix(mod_format): render format templates through smartscaf before comparing generated files 2026-03-24 16:56:34 +00:00
jkunz b55e75d169 2.13.11
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 16:10:56 +00:00
jkunz d0d922e53b update to smartconfig 2026-03-24 16:10:51 +00:00
jkunz eda67395fe v2.13.10
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 15:05:07 +00:00
jkunz 470e87eb79 fix(config): migrate configuration handling from npmextra to smartconfig 2026-03-24 15:05:07 +00:00
jkunz 3358a0eacc v2.13.9
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 10m42s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-03-11 19:10:18 +00:00
jkunz b65fac6257 fix(deps,readme): bump dependencies and update README to prefer pnpm and document semantic commit flags 2026-03-11 19:10:18 +00:00
jkunz 4ab59609e6 v2.13.8
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 12m24s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-03-05 11:30:34 +00:00
jkunz 32f106291f fix(dependencies): move runtime tooling packages from devDependencies to dependencies 2026-03-05 11:30:34 +00:00
jkunz b8aa5d61f6 v2.13.7
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 12m15s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-03-05 10:25:44 +00:00
jkunz 71759c276e fix(deps): bump devDependencies: @git.zone/tsbuild to ^4.1.4 and @push.rocks/smartshell to ^3.3.7 2026-03-05 10:25:44 +00:00
jkunz 7938f12d43 v2.13.6
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 14m42s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-02-01 16:19:37 +00:00
jkunz 3722258d69 fix(templates/npm): use tsbuild tsfolders instead of --web flag in npm template build script 2026-02-01 16:19:37 +00:00
jkunz 68859d0e97 v2.13.5
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 10m44s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-02-01 16:18:37 +00:00
jkunz ecadbc7a86 fix(templates/npm): update npm template: tweak test script, bump devDependencies, add smartpath dependency, and fix ts import path 2026-02-01 16:18:37 +00:00
jkunz 0243bc5ec7 v2.13.4
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 12m1s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-01-12 17:57:00 +00:00
jkunz 92e618104f fix(core): update tsbuild to 4.1.2 with cross-module import path fix 2026-01-12 17:57:00 +00:00
jkunz c089c1f80d v2.13.3
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-18 13:57:13 +00:00
jkunz 10a394c7d8 fix(tsconfig): remove experimentalDecorators and useDefineForClassFields from TypeScript configuration files 2025-12-18 13:57:13 +00:00
jkunz 5980308bb8 v2.13.2
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-16 13:00:30 +00:00
jkunz 398e36bdf7 fix(deps): bump @git.zone/tspublish to ^1.11.0 2025-12-16 13:00:30 +00:00
jkunz 1e78517547 v2.13.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-16 12:58:58 +00:00
jkunz 55700ad87e fix(npmextra): merge old npmextra keys into new keys during migration, preserving existing new values 2025-12-16 12:58:58 +00:00
jkunz 773df5268b v2.13.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-16 10:49:41 +00:00
jkunz b51fa88283 feat(tests): feat(tests): add sandbox test fixture, CI and editor configs; bump deps 2025-12-16 10:49:41 +00:00
jkunz cb9f717d54 v2.12.2
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:46:17 +00:00
jkunz 70be11894c fix(cli): noop: no changes 2025-12-15 17:46:17 +00:00
jkunz 89ab63b153 update 2025-12-15 17:45:28 +00:00
jkunz 44c193d4a8 v2.12.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:44:09 +00:00
jkunz 44d259a0ae fix(cli): No changes detected — no version bump required 2025-12-15 17:44:09 +00:00
jkunz f0adff8784 v2.12.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:34:36 +00:00
jkunz fb453e62c3 feat(ci,test): feat(ci/test): add test scaffold, GitLab CI, update gitea workflows and .gitignore 2025-12-15 17:34:36 +00:00
jkunz 001721a8e9 v2.11.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:30:51 +00:00
jkunz b191464ff9 fix(mod_format/formatters): fix(packagejson.formatter): correctly parse scoped package dependency arguments and default to latest 2025-12-15 17:30:51 +00:00
jkunz 4d7eaa238f v2.11.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:24:17 +00:00
jkunz 601e0d1063 feat(mod_format): feat(mod_format): use unified diff formatter with filenames and context in BaseFormatter.displayDiff 2025-12-15 17:24:17 +00:00
jkunz 4bb1a2f8c7 v2.10.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:07:30 +00:00
jkunz b506bf8785 feat(mod_format): Refactor formatting modules to new BaseFormatter and implement concrete analyze/apply logic 2025-12-15 17:07:30 +00:00
jkunz d5fbeb3fc6 2.9.2
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 15:55:36 +00:00
jkunz 2ecdeff3dc update 2025-12-15 15:55:27 +00:00
jkunz 5a663ae767 2.9.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 15:25:30 +00:00
jkunz 218c84a39b update 2025-12-15 15:25:20 +00:00
64 changed files with 4852 additions and 4460 deletions
+9 -9
View File
@@ -6,19 +6,19 @@ on:
- '**'
env:
IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${-{gitea.repository_owner}-}:${-{secrets.GITEA_TOKEN}-}@{{module.githost}}/${-{gitea.repository}-}.git
NPMCI_TOKEN_NPM: ${-{secrets.NPMCI_TOKEN_NPM}-}
NPMCI_TOKEN_NPM2: ${-{secrets.NPMCI_TOKEN_NPM2}-}
NPMCI_GIT_GITHUBTOKEN: ${-{secrets.NPMCI_GIT_GITHUBTOKEN}-}
NPMCI_URL_CLOUDLY: ${-{secrets.NPMCI_URL_CLOUDLY}-}
jobs:
security:
runs-on: ubuntu-latest
continue-on-error: true
container:
image: ${{ env.IMAGE }}
image: ${-{ env.IMAGE }-}
steps:
- uses: actions/checkout@v3
@@ -44,11 +44,11 @@ jobs:
continue-on-error: true
test:
if: ${{ always() }}
if: ${-{ always() }-}
needs: security
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
image: ${-{ env.IMAGE }-}
steps:
- uses: actions/checkout@v3
+11 -11
View File
@@ -6,19 +6,19 @@ on:
- '*'
env:
IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${-{gitea.repository_owner}-}:${-{secrets.GITEA_TOKEN}-}@{{module.githost}}/${-{gitea.repository}-}.git
NPMCI_TOKEN_NPM: ${-{secrets.NPMCI_TOKEN_NPM}-}
NPMCI_TOKEN_NPM2: ${-{secrets.NPMCI_TOKEN_NPM2}-}
NPMCI_GIT_GITHUBTOKEN: ${-{secrets.NPMCI_GIT_GITHUBTOKEN}-}
NPMCI_URL_CLOUDLY: ${-{secrets.NPMCI_URL_CLOUDLY}-}
jobs:
security:
runs-on: ubuntu-latest
continue-on-error: true
container:
image: ${{ env.IMAGE }}
image: ${-{ env.IMAGE }-}
steps:
- uses: actions/checkout@v3
@@ -42,11 +42,11 @@ jobs:
continue-on-error: true
test:
if: ${{ always() }}
if: ${-{ always() }-}
needs: security
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
image: ${-{ env.IMAGE }-}
steps:
- uses: actions/checkout@v3
@@ -74,7 +74,7 @@ jobs:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
image: ${-{ env.IMAGE }-}
steps:
- uses: actions/checkout@v3
@@ -95,7 +95,7 @@ jobs:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
image: ${-{ env.IMAGE }-}
continue-on-error: true
steps:
+4 -2
View File
@@ -3,8 +3,6 @@
# artifacts
coverage/
public/
test/
test2/
# installs
node_modules/
@@ -18,6 +16,10 @@ node_modules/
dist/
dist_*/
# AI
.claude/
.serena/
#------# custom
.serena
test-output.json
View File
+4
View File
@@ -19,6 +19,10 @@ node_modules/
dist/
dist_*/
# rust
rust/target/
dist_rust/
# AI
.claude/
.serena/
-1
View File
@@ -1,6 +1,5 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"lib": ["ES2022", "DOM"],
"target": "ES2022",
"checkJs": true
+9 -7
View File
@@ -12,15 +12,17 @@ fileName: package.json
"author": "{{module.author}}",
"license": "{{module.license}}",
"scripts": {
"test": "(tstest test/ --web)",
"build": "(tsbuild --web --allowimplicitany)",
"test": "(tstest test/ --verbose --logfile --timeout 60)",
"build": "(tsbuild tsfolders --allowimplicitany)",
"buildDocs": "(tsdoc)"
},
"devDependencies": {
"@git.zone/tsbuild": "^3.1.2",
"@git.zone/tsrun": "^2.0.0",
"@git.zone/tstest": "^3.1.3",
"@types/node": "^24.10.1"
"@git.zone/tsbuild": "^4.1.2",
"@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.8",
"@types/node": "^25.2.0"
},
"dependencies": {}
"dependencies": {
"@push.rocks/smartpath": "^6.0.0"
}
}
+1 -1
View File
@@ -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';
@@ -1,3 +1,6 @@
---
fileName: .smartconfig.json
---
{
"@git.zone/cli": {
"projectType": "{{projectType}}",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"json.schemas": [
{
"fileMatch": ["/npmextra.json"],
"fileMatch": ["/.smartconfig.json"],
"schema": {
"type": "object",
"properties": {
+207 -2
View File
@@ -1,6 +1,168 @@
# Changelog
## 2026-04-16 - 2.14.0 - feat(cli)
add machine-readable CLI help, recommendation, and configuration flows
- introduces shared CLI mode handling for human, plain, and JSON output with configurable interactivity and update checks
- adds read-only JSON support for `commit recommend`, `format plan`, and command help output
- expands `config` and `services` commands with non-interactive config inspection and service enablement flows
- updates format and smartconfig handling to respect non-interactive execution and fail clearly when required metadata is missing
## 2026-04-16 - 2.13.16 - fix(mod_format)
stop package.json formatter from modifying buildDocs and dependency entries
- removes automatic buildDocs script injection from the package.json formatter
- removes dependency include/exclude and latest-version update logic from package.json formatting
- drops the unused smartnpm plugin import after removing registry lookups
## 2026-03-24 - 2.13.15 - fix(repo)
no changes to commit
## 2026-03-24 - 2.13.14 - fix(mod_format)
move smartconfig file renaming into the formatter orchestrator
- Renames smartconfig.json or npmextra.json to .smartconfig.json before formatters run
- Simplifies the smartconfig formatter to only read and modify .smartconfig.json
- Removes create/delete change planning for config renames and applies only content updates within the formatter
## 2026-03-24 - 2.13.13 - fix(vscode-template)
update VS Code schema matching to use .smartconfig.json
- Changes the VS Code template settings so the JSON schema applies to /.smartconfig.json instead of /npmextra.json.
## 2026-03-24 - 2.13.12 - fix(mod_format)
render format templates through smartscaf before comparing generated files
- adds smartscaf-based in-memory template rendering so supplied variables are applied before detecting changes
- supports release.accessLevel as a fallback when selecting public vs private CI templates
- matches rendered output by template or destination path to handle renamed files from template frontmatter
## 2026-03-24 - 2.13.10 - fix(config)
migrate configuration handling from npmextra to smartconfig
- replace @push.rocks/npmextra with @push.rocks/smartconfig across config, commit, format, and service modules
- switch managed project config file references from npmextra.json to smartconfig.json
- update formatting and package metadata checks to include smartconfig.json
- extend the gitignore template with Rust build output directories
## 2026-03-11 - 2.13.9 - fix(deps,readme)
bump dependencies and update README to prefer pnpm and document semantic commit flags
- Dev dependency updates: @git.zone/tsbuild -> ^4.3.0, @git.zone/tstest -> ^3.3.2, @types/node -> ^25.4.0
- Dependency upgrades: @git.zone/tsdoc -> ^2.0.0, @git.zone/tspublish -> ^1.11.2, @push.rocks/lik -> ^6.3.1, @push.rocks/smartfs -> ^1.5.0, @push.rocks/smartlog -> ^3.2.1, @push.rocks/smartstream -> ^3.4.0, prettier -> ^3.8.1 (and other minor/patch bumps)
- README changes: prefer pnpm for global install, clarify format command dry-run behavior and --write flag, add and document gitzone commit flags (-y/--yes, -p/--push, -t/--test, -b/--build, -r/--release) and AI-powered commit workflow
- No source code changes; this is a documentation and dependency refresh, recommend a patch release
## 2026-03-05 - 2.13.8 - fix(dependencies)
move runtime tooling packages from devDependencies to dependencies
- Removed @push.rocks/smartdelay, @push.rocks/smartinteract, @push.rocks/smartnetwork, and @push.rocks/smartshell from devDependencies and added them to dependencies
- No package version numbers were changed; this ensures the moved packages are installed for consumers at runtime
## 2026-03-05 - 2.13.7 - fix(deps)
bump devDependencies: @git.zone/tsbuild to ^4.1.4 and @push.rocks/smartshell to ^3.3.7
- Updated @git.zone/tsbuild from ^4.1.2 to ^4.1.4 (patch)
- Updated @push.rocks/smartshell from ^3.3.0 to ^3.3.7 (patch)
## 2026-02-01 - 2.13.6 - fix(templates/npm)
use tsbuild tsfolders instead of --web flag in npm template build script
- Changed build script in assets/templates/npm/.package.json from "(tsbuild --web --allowimplicitany)" to "(tsbuild tsfolders --allowimplicitany)"
- Replaces --web flag with explicit tsfolders argument to correctly target project folders during build
## 2026-02-01 - 2.13.5 - fix(templates/npm)
update npm template: tweak test script, bump devDependencies, add smartpath dependency, and fix ts import path
- test script updated: '(tstest test/ --web)' -> '(tstest test/ --verbose --logfile --timeout 60)'
- devDependencies bumped: @git.zone/tsbuild ^3.1.2 -> ^4.1.2, @git.zone/tsrun ^2.0.0 -> ^2.0.1, @git.zone/tstest ^3.1.3 -> ^3.1.8, @types/node ^24.10.1 -> ^25.2.0
- dependencies: added @push.rocks/smartpath ^6.0.0
- TypeScript template import fixed: './{{module.name}}.plugins.js' -> './plugins.js'
## 2025-12-18 - 2.13.3 - fix(tsconfig)
remove experimentalDecorators and useDefineForClassFields from TypeScript configuration files
- Removed "experimentalDecorators": true from assets/templates/multienv/deno.json and tsconfig.json
- Removed "useDefineForClassFields": false from tsconfig.json
- This change alters TypeScript/Deno compiler behavior: decorator support and legacy class-field initialization semantics may be affected; code relying on those may need updates
## 2025-12-16 - 2.13.2 - fix(deps)
bump @git.zone/tspublish to ^1.11.0
- Updated dependency @git.zone/tspublish from ^1.10.3 to ^1.11.0 in package.json
## 2025-12-16 - 2.13.1 - fix(npmextra)
merge old npmextra keys into new keys during migration, preserving existing new values
- Changed migration logic to merge data when both old and new keys exist instead of skipping the merge.
- Merge preserves existing new-key values (old values do not overwrite new ones) and still deletes the old key after migration.
- Applied the fix in both ts/mod_format/format.npmextra.ts and ts/mod_format/formatters/npmextra.formatter.ts.
- Adds a console log for successful migrations; behavior for single-key rename remains unchanged.
## 2025-12-16 - 2.13.0 - feat(tests)
feat(tests): add sandbox test fixture, CI and editor configs; bump deps
- Added comprehensive test/ fixture (sandbox-npmts) including package.json, npmextra.json, readme, GitLab CI (.gitlab-ci.yml), .npmrc, VSCode launch/settings and qenv.yml
- Added test sources and helper files under test/test and test/ts (browser and node tests, commitinfo data, simple library code)
- Updated dependencies in package.json: @git.zone/tsdoc -> ^1.11.4, @push.rocks/smartfs -> ^1.3.1
## 2025-12-15 - 2.12.2 - fix(cli)
noop: no changes
- No source or documentation changes detected in the diff; nothing to release.
- Package version remains unchanged at 2.12.1.
## 2025-12-15 - 2.12.1 - fix(cli)
No changes detected — no version bump required
- Current package version: 2.12.0
- No files changed in this commit
- No release or version bump necessary
## 2025-12-15 - 2.12.0 - feat(ci,test)
feat(ci/test): add test scaffold, GitLab CI, update gitea workflows and .gitignore
- Add comprehensive test/ scaffold including sample tests, test package.json, npmextra.json, test fixtures and TypeScript test sources
- Add GitLab CI pipeline (test/.gitlab-ci.yml), test-specific .npmrc, VSCode launch/settings and supporting qenv/readme files for CI/local test runs
- Update .gitea workflow YAML files to use templated placeholders and corrected container image path for CI execution
- Update .gitignore to exclude AI assistant and tooling dirs (.claude/, .serena/) and add test/.gitignore to ignore test artifacts
- Update changelog and documentation files to reflect recent formatter/commit/service changes and the new test/CI additions
## 2025-12-15 - 2.11.1 - fix(mod_format/formatters)
fix(packagejson.formatter): correctly parse scoped package dependency arguments and default to latest
- Handle scoped packages (e.g. @scope/name@version) by detecting the last '@' after the scope slash so package name and version are split correctly.
- Fallback to 'latest' when no version is provided.
- Fixes earlier incorrect splitting on every '@' which broke scoped package names.
## 2025-12-15 - 2.11.0 - feat(mod_format)
feat(mod_format): use unified diff formatter with filenames and context in BaseFormatter.displayDiff
- Replaced plugins.smartdiff.formatLineDiffForConsole(...) with plugins.smartdiff.formatUnifiedDiffForConsole(...) when both before and after are present.
- Passes originalFileName and revisedFileName as diff.path and sets context to 3 to show a unified diff with surrounding lines.
- Improves console output for multi-line diffs by using unified diff format and including file names.
## 2025-12-15 - 2.10.0 - feat(mod_format)
Refactor formatting modules to new BaseFormatter and implement concrete analyze/apply logic
- Replace generic LegacyFormatter with explicit BaseFormatter implementations for formatters: copy, gitignore, license, npmextra, packagejson, prettier, readme, templates, tsconfig (legacy.formatter.ts removed).
- Copy formatter: implemented pattern-based copying, template-preserve path handling, content equality check and planned change generation/apply.
- Gitignore formatter: canonical template with preservation of custom section when updating/creating .gitignore.
- License formatter: added runtime license check against node_modules for incompatible licenses and reporting (no file changes).
- Npmextra formatter: automatic migrations for old namespace keys to package-scoped keys and migration of npmAccessLevel -> @git.zone/cli.release.accessLevel; reformatting and interactive prompting to fill missing repo metadata.
- Package.json formatter: enforces repository/metadata, sets module type/private/license/scripts/files, ensures/updates dependencies (including fetching latest via registry), and applies pnpm overrides from assets.
- Prettier formatter: added check() to compute diffs by running Prettier and returning per-file before/after diffs.
- Readme formatter: create readme.md and readme.hints.md when missing with default content.
- Templates formatter: apply templates from templatesDir based on project type (vscode, CI, docker, website/service/wcc), compare template vs destination and create/modify files as needed; ensures dest directories exist.
- Tsconfig formatter: sets compilerOptions.baseUrl and computes path mappings from @git.zone/tspublish modules.
- General: extensive use of plugins (smartfs, path, smartnpm, smartinteract, smartobject, smartlegal), improved logging and verbose messages.
## 2025-12-15 - 2.9.0 - feat(format)
Add --diff option to format command to display file diffs; pass flag through CLI and show formatter diffs. Bump @git.zone/tsdoc to ^1.11.0.
- Add a diff boolean option to mod_format to enable showing file diffs during format operations.
@@ -9,6 +171,7 @@ Add --diff option to format command to display file diffs; pass flag through CLI
- Update dependency @git.zone/tsdoc from ^1.10.2 to ^1.11.0.
## 2025-12-15 - 2.8.0 - feat(commit)
Add commit configuration and automatic pre-commit tests
- Add CommitConfig class to manage @git.zone/cli.commit settings in npmextra.json (alwaysTest, alwaysBuild).
@@ -20,6 +183,7 @@ Add commit configuration and automatic pre-commit tests
- Add 'gitzone config services' entry to configure services via ServiceManager.
## 2025-12-14 - 2.7.0 - feat(mod_format)
Add check-only formatting with interactive diff preview; make formatting default to dry-run and extend formatting API
- Add BaseFormatter.check(), displayDiff() and displayAllDiffs() to compute and render diffs without applying changes.
@@ -32,6 +196,7 @@ Add check-only formatting with interactive diff preview; make formatting default
- Bump dependency @push.rocks/smartdiff to ^1.1.0.
## 2025-12-14 - 2.6.1 - fix(npmextra)
Normalize npmextra.json: move tsdoc legal entry and reposition @git.zone/cli configuration
- Move TSDoc legal text into a top-level "tsdoc.legal" property in npmextra.json
@@ -40,6 +205,7 @@ Normalize npmextra.json: move tsdoc legal entry and reposition @git.zone/cli con
- Pure configuration change (JSON structure) — no functional code changes
## 2025-12-14 - 2.6.0 - feat(mod_commit)
Add execution plan output to commit command
- Print an execution plan at the start of the commit flow (shows active options and planned steps)
@@ -48,6 +214,7 @@ Add execution plan output to commit command
- Execution plan reflects flags: auto-accept (-y), push (-p), build (-b), release (-r), --format, and target registries
## 2025-12-14 - 2.5.0 - feat(mod_standard)
Add interactive main menu and help to standard CLI module; route commands via dynamic imports
- Introduce interactive CLI menu using @push.rocks/smartinteract to prompt user for actions.
@@ -56,6 +223,7 @@ Add interactive main menu and help to standard CLI module; route commands via dy
- Remove previous static template listing and logger.warn placeholder.
## 2025-12-14 - 2.4.0 - feat(cli)
Add optional build step to release flow and auto-format npmextra config when registries change
- Introduce a --build/-b flag in the commit/release flow to run 'pnpm build' before pushing/releases
@@ -66,6 +234,7 @@ Add optional build step to release flow and auto-format npmextra config when reg
- Add npmextra registry config entry (https://verdaccio.lossless.digital) to npmextra.json
## 2025-12-14 - 2.3.0 - feat(config)
Add interactive menu and help to config command, handle unknown commands, and bump dependencies
- When running the 'config' command with no arguments, show an interactive menu (via SmartInteract) to choose actions (show, add, remove, clear, access, help) instead of defaulting to 'show'.
@@ -74,6 +243,7 @@ Add interactive menu and help to config command, handle unknown commands, and bu
- Update dependency: @push.rocks/smartjson -> ^6.0.0.
## 2025-12-04 - 2.2.1 - fix(commit)
Prevent auto-accept for BREAKING CHANGE commits; require manual confirmation and warn when --yes is used
- Do not auto-accept AI commit recommendations when the suggested change is a BREAKING CHANGE (major bump).
@@ -82,6 +252,7 @@ Prevent auto-accept for BREAKING CHANGE commits; require manual confirmation and
- Introduced isBreakingChange and canAutoAccept flags to centralize the auto-accept logic.
## 2025-12-02 - 2.2.0 - feat(services)
Improve services manager and configuration; switch test templates to @git.zone/tstest; bump dev dependencies and update docs
- services: Add robust ServiceConfiguration (creates .nogit/env.json with sane defaults, syncs ports from existing Docker containers, validates and can reconfigure ports)
@@ -92,6 +263,7 @@ Improve services manager and configuration; switch test templates to @git.zone/t
- docs: README updates — add issue reporting/security section, AI-powered commit recommendation notes, and clarify trademark/legal wording
## 2025-11-29 - 2.1.0 - feat(mod_services)
Add global service registry and global commands for managing project containers
- Introduce GlobalRegistry class to track registered projects, their containers, ports and last activity (ts/mod_services/classes.globalregistry.ts)
@@ -101,30 +273,34 @@ Add global service registry and global commands for managing project containers
- Bump dependency @push.rocks/smartfile to ^13.1.0 in package.json
## 2025-11-27 - 2.0.0 - BREAKING CHANGE(core)
Migrate filesystem to smartfs (async) and add Elasticsearch service support; refactor format/commit/meta modules
- Replace @push.rocks/smartfile usage with @push.rocks/smartfs across the codebase; all filesystem operations are now async (SmartFs.file(...).read()/write(), SmartFs.directory(...).list()/create()/delete(), etc.)
- Convert formerly synchronous helpers and APIs to async (notable: detectProjectType, getProjectName, readCurrentVersion and related version bumping logic). Callers updated accordingly.
- Add Elasticsearch support to services: new config fields (ELASTICSEARCH_*), Docker run/start/stop/logs/status handling, and ELASTICSEARCH_URL in service configuration.
- Add Elasticsearch support to services: new config fields (ELASTICSEARCH\_\*), Docker run/start/stop/logs/status handling, and ELASTICSEARCH_URL in service configuration.
- Refactor formatting subsystem: cache and rollback/backup systems removed/disabled for stability, format planner execution simplified (sequential), diff/stats reporting updated to use smartfs.
- Update package.json dependencies: bump @git.zone/tsbuild, tsrun, tstest; upgrade @push.rocks/smartfile to v13 and add @push.rocks/smartfs dependency; update @types/node.
- Update commit flow and changelog generation to use smartfs for reading/writing files and to await version/branch detection where necessary.
- Expose a SmartFs instance via plugins and adjust all mod.* plugin files to import/use smartfs where required.
- Expose a SmartFs instance via plugins and adjust all mod.\* plugin files to import/use smartfs where required.
- Breaking change: Public and internal APIs that previously used synchronous smartfile APIs are now asynchronous. Consumers and scripts must await these functions and use the new smartfs API.
## 2025-11-17 - 1.21.5 - fix(tsconfig)
Remove emitDecoratorMetadata from tsconfig template
- Removed the "emitDecoratorMetadata" compiler option from assets/templates/tsconfig_update/tsconfig.json
- This updates the tsconfig template to avoid emitting decorator metadata when targeting ES2022
## 2025-11-17 - 1.21.4 - fix(tsconfig template)
Remove experimentalDecorators and useDefineForClassFields from tsconfig template
- Removed experimentalDecorators option from assets/templates/tsconfig_update/tsconfig.json
- Removed useDefineForClassFields option from assets/templates/tsconfig_update/tsconfig.json
## 2025-11-17 - 1.21.3 - fix(assets/templates/multienv)
Remove unused Bun configuration template (assets/templates/multienv/bunfig.toml)
- Deleted assets/templates/multienv/bunfig.toml which previously provided Bun TypeScript decorator configuration
@@ -132,17 +308,20 @@ Remove unused Bun configuration template (assets/templates/multienv/bunfig.toml)
- No functional code changes; removes an unused asset file
## 2025-11-17 - 1.21.2 - fix(templates/multienv)
Disable useDefineForClassFields in multienv TypeScript configs to ensure decorator compatibility
- Set useDefineForClassFields = false in assets/templates/multienv/bunfig.toml to keep Bun's transpiler compatible with decorator usage
- Set "useDefineForClassFields": false in assets/templates/multienv/deno.json to ensure Deno/TypeScript compiler emits class fields compatible with decorators
## 2025-11-17 - 1.21.1 - fix(templates.multienv)
Enable checkJs in multienv Deno template to enable JS type checking
- Added "checkJs": true to compilerOptions in assets/templates/multienv/deno.json to enable JavaScript type checking for the Deno multienv template
## 2025-11-17 - 1.21.0 - feat(multienv)
Add multi-env templates enabling TypeScript decorators for Bun and Deno; rename npmextra config key to szci
- Added assets/templates/multienv/bunfig.toml to enable Bun TypeScript transpiler experimentalDecorators
@@ -150,6 +329,7 @@ Add multi-env templates enabling TypeScript decorators for Bun and Deno; rename
- Updated npmextra.json: renamed top-level config key from "npmci" to "szci" (keeps npmGlobalTools, npmAccessLevel and npmRegistryUrl unchanged)
## 2025-11-06 - 1.20.0 - feat(commit)
Add non-interactive --yes (-y) flag to commit command to auto-accept AI recommendations and optionally push with -p
- Add -y / --yes flag to gitzone commit to auto-accept AI-generated commit recommendations without interactive prompts
@@ -159,6 +339,7 @@ Add non-interactive --yes (-y) flag to commit command to auto-accept AI recommen
- Updated CLI usage and documentation (readme.hints.md) to document the new flags
## 2025-11-05 - 1.19.9 - fix(mod_commit)
Refactor version bumping to a unified implementation for npm and Deno; remove npm-exec based helpers and add file-based version readers/updaters to avoid npm warning pollution
- Removed legacy npm/deno-specific helpers (bumpNpmVersion, syncVersionToDenoJson, bumpDenoVersion) that relied on executing npm and caused warning pollution
@@ -169,39 +350,46 @@ Refactor version bumping to a unified implementation for npm and Deno; remove np
- Benefits: no npm warning pollution in deno.json, simpler git history, consistent behavior across project types
## 2025-11-04 - 1.19.8 - fix(package.json)
Bump @git.zone/tsdoc dependency to ^1.9.2
- Updated dependency @git.zone/tsdoc from ^1.9.1 to ^1.9.2 in package.json
## 2025-11-04 - 1.19.7 - fix(dependencies)
Bump @git.zone/tsdoc to ^1.9.1
- Updated package.json dependency @git.zone/tsdoc from ^1.9.0 to ^1.9.1
## 2025-11-04 - 1.19.6 - fix(cli)
Bump @git.zone/tsdoc dependency to ^1.9.0
- Updated dependency @git.zone/tsdoc from ^1.8.3 to ^1.9.0 in package.json
## 2025-11-04 - 1.19.5 - fix(cli)
Bump @git.zone/tsdoc to ^1.8.3 and add local .claude settings for allowed permissions
- Updated dependency @git.zone/tsdoc from ^1.8.2 to ^1.8.3
- Added .claude/settings.local.json to declare allowed permissions for local tooling (Bash commands, Docker, npm, WebFetch and MCP actions)
## 2025-11-03 - 1.19.3 - fix(tsdoc)
Bump @git.zone/tsdoc to ^1.8.0 and add .claude local settings
- Upgrade dependency @git.zone/tsdoc from ^1.6.1 to ^1.8.0 in package.json
- Add .claude/settings.local.json for local assistant permissions/configuration
## 2025-11-03 - 1.19.2 - fix(tsdoc)
Bump @git.zone/tsdoc to ^1.6.1 and add .claude/settings.local.json
- Update dependency @git.zone/tsdoc from ^1.6.0 to ^1.6.1
- Add .claude/settings.local.json to include local Claude settings/permissions
## 2025-11-02 - 1.19.1 - fix(dependencies)
Bump dependencies and add local Claude settings
- Bump devDependencies: @git.zone/tsbuild -> ^2.7.1, @git.zone/tsrun -> ^1.6.2, @git.zone/tstest -> ^2.7.0
@@ -209,6 +397,7 @@ Bump dependencies and add local Claude settings
- Add .claude/settings.local.json (local project permissions/settings file)
## 2025-10-23 - 1.19.0 - feat(mod_commit)
Add CLI UI helpers and improve commit workflow with progress, recommendations and summary
- Introduce ts/mod_commit/mod.ui.ts: reusable CLI UI helpers (pretty headers, sections, AI recommendation box, step printer, commit summary and helpers for consistent messaging).
@@ -217,6 +406,7 @@ Add CLI UI helpers and improve commit workflow with progress, recommendations an
- Add .claude/settings.local.json: local permissions configuration for development tooling.
## 2025-10-23 - 1.18.9 - fix(mod_commit)
Stage and commit deno.json when bumping/syncing versions and create/update git tags
- bumpDenoVersion now creates a Smartshell instance and runs git add deno.json, git commit -m "v<newVersion>", and git tag v<newVersion> to persist the version bump
@@ -224,6 +414,7 @@ Stage and commit deno.json when bumping/syncing versions and create/update git t
- Added informative logger messages after creating commits and tags
## 2025-10-23 - 1.18.8 - fix(mod_commit)
Improve commit workflow: detect project type and current branch; add robust version bump helpers for npm/deno
- Add mod_commit/mod.helpers.ts with utilities: detectCurrentBranch(), detectProjectType(), bumpProjectVersion(), bumpDenoVersion(), bumpNpmVersion(), syncVersionToDenoJson(), and calculateNewVersion()
@@ -233,12 +424,14 @@ Improve commit workflow: detect project type and current branch; add robust vers
- Add local Claude settings file (.claude/settings.local.json) (editor/CI config) — no code behavior change but included in diff
## 2025-09-07 - 1.18.7 - fix(claude)
Add .claude local settings to whitelist dev tool permissions
- Add .claude/settings.local.json to configure allowed permissions for local AI/tooling helpers (Bash commands, WebFetch, and mcp_serena actions).
- Disable enableAllProjectMcpServers (set to false) to limit automatic project MCP server usage.
## 2025-09-07 - 1.18.6 - fix(deps)
Bump dependency versions and add local Claude settings
- Updated devDependencies: @git.zone/tsbuild ^2.6.4 → ^2.6.8, @git.zone/tstest ^2.3.4 → ^2.3.6, @push.rocks/smartfile ^11.2.5 → ^11.2.7
@@ -246,6 +439,7 @@ Bump dependency versions and add local Claude settings
- Added .claude/settings.local.json to configure local Claude permissions/settings
## 2025-08-17 - 1.18.5 - fix(dependencies)
Bump smartshell and smartscaf versions; add .claude local settings
- Update @push.rocks/smartshell from ^3.2.4 to ^3.3.0 in package.json
@@ -253,6 +447,7 @@ Bump smartshell and smartscaf versions; add .claude local settings
- Add .claude/settings.local.json for local assistant permissions/configuration
## 2025-08-17 - 1.18.4 - fix(cli)
Update dependencies, add local Claude settings, and update gitignore template
- Bump several dependencies: @git.zone/tsbuild -> ^2.6.4, @git.zone/tspublish -> ^1.10.1, @git.zone/tstest -> ^2.3.4, @push.rocks/smartfile -> ^11.2.5, @push.rocks/npmextra -> ^5.3.3, @push.rocks/smartchok -> ^1.1.1, @push.rocks/smartlog -> ^3.1.8, @push.rocks/smartpath -> ^6.0.0, prettier -> ^3.6.2
@@ -261,6 +456,7 @@ Update dependencies, add local Claude settings, and update gitignore template
- Add pnpm onlyBuiltDependencies entries: esbuild and mongodb-memory-server
## 2025-08-16 - 1.18.3 - fix(services)
Simplify S3 endpoint handling in ServiceConfiguration to store host only
- S3_ENDPOINT now stores the raw host (e.g. 'localhost') instead of a full URL with protocol and port.
@@ -269,6 +465,7 @@ Simplify S3 endpoint handling in ServiceConfiguration to store host only
- Consumers that previously relied on S3_ENDPOINT containing protocol and port should now construct the full endpoint URL using S3_USESSL, S3_HOST and S3_PORT.
## 2025-08-16 - 1.18.1 - fix(services)
Improve services and commit flow: stop AiDoc, use silent docker inspect, sync ports with logging, fix config loading, and bump deps
- Ensure AiDoc is stopped after building commit recommendation to avoid resource leaks
@@ -280,6 +477,7 @@ Improve services and commit flow: stop AiDoc, use silent docker inspect, sync po
- Add local Claude settings file (.claude/settings.local.json) with development permissions
## 2025-08-16 - 1.18.0 - feat(services)
Add Docker port mapping sync and reconfigure workflow for local services
- Add getPortMappings to DockerContainer to extract port bindings from docker inspect output
@@ -292,6 +490,7 @@ Add Docker port mapping sync and reconfigure workflow for local services
- Add .claude/settings.local.json (local permissions config) to repository
## 2025-08-15 - 1.17.5 - fix(services)
Update S3 credentials naming and add S3_ENDPOINT/S3_USESSL support for improved MinIO integration
- Replaced S3_USER/S3_PASS with S3_ACCESSKEY/S3_SECRETKEY in ServiceConfiguration
@@ -301,6 +500,7 @@ Update S3 credentials naming and add S3_ENDPOINT/S3_USESSL support for improved
- Added .claude/settings.local.json for local permission settings
## 2025-08-15 - 1.17.4 - fix(services)
Update S3 credentials naming and add S3_ENDPOINT/S3_USESSL support for improved MinIO integration
- Replaced S3_USER/S3_PASS with S3_ACCESSKEY/S3_SECRETKEY in ServiceConfiguration
@@ -309,12 +509,14 @@ Update S3 credentials naming and add S3_ENDPOINT/S3_USESSL support for improved
- Updated ServiceManager to use new credential names in container setup and logging
## 2025-08-15 - 1.17.3 - fix(serviceconfig)
Update service configuration to include dynamic MongoDB connection string and add local permissions settings
- Added .claude/settings.local.json for local permissions configuration
- Updated ServiceConfiguration to compute and update MONGODB_URL based on current config values
## 2025-08-15 - 1.17.2 - fix(ci-test-services)
Update CI/CD configurations, test settings, and Docker service for MongoDB.
- Add .claude/settings.local.json with updated permission settings
@@ -323,6 +525,7 @@ Update CI/CD configurations, test settings, and Docker service for MongoDB.
- Fix MongoDB Docker container command by adding '--bind_ip_all' for proper network binding
## 2025-08-15 - 1.17.1 - fix(services)
Improve services module logging and enhance MongoDB Compass integration
- Refactored services module to use centralized logger from gitzone.logging.ts
@@ -331,6 +534,7 @@ Improve services module logging and enhance MongoDB Compass integration
- Consistent logging across all service commands
## 2025-08-14 - 1.17.0 - feat(services)
Add comprehensive development services management for MongoDB and MinIO containers
- Implemented `gitzone services` command for managing local development services
@@ -344,6 +548,7 @@ Add comprehensive development services management for MongoDB and MinIO containe
- Interactive confirmations for destructive operations
## 2025-08-08 - 1.16.10 - fix(format)
Improve concurrency control in caching and rollback modules, refine gitignore custom section handling, and enhance Prettier file processing.
- Added mutex locking in ChangeCache and RollbackManager to prevent race conditions during manifest updates
+17 -23
View File
@@ -1,7 +1,7 @@
{
"name": "@git.zone/cli",
"private": false,
"version": "2.9.0",
"version": "2.14.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.",
"main": "dist_ts/index.ts",
"typings": "dist_ts/index.d.ts",
@@ -57,46 +57,40 @@
},
"homepage": "https://gitlab.com/gitzone/private/gitzone#readme",
"devDependencies": {
"@git.zone/tsbuild": "^4.0.2",
"@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.3",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartinteract": "^2.0.16",
"@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartshell": "^3.3.0",
"@types/node": "^25.0.2"
"@git.zone/tstest": "^3.3.2",
"@types/node": "^25.4.0"
},
"dependencies": {
"@git.zone/tsdoc": "^1.11.0",
"@git.zone/tspublish": "^1.10.3",
"@git.zone/tsdoc": "^2.0.0",
"@git.zone/tspublish": "^1.11.2",
"@push.rocks/commitinfo": "^1.0.12",
"@push.rocks/early": "^4.0.4",
"@push.rocks/gulp-function": "^3.0.7",
"@push.rocks/lik": "^6.2.2",
"@push.rocks/npmextra": "^5.3.3",
"@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/smartcli": "^4.0.19",
"@push.rocks/smartcli": "^4.0.20",
"@push.rocks/smartconfig": "^6.0.1",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartdiff": "^1.1.0",
"@push.rocks/smartfile": "^13.1.2",
"@push.rocks/smartfs": "^1.2.0",
"@push.rocks/smartgulp": "^3.0.4",
"@push.rocks/smartfs": "^1.5.0",
"@push.rocks/smartinteract": "^2.0.16",
"@push.rocks/smartjson": "^6.0.0",
"@push.rocks/smartlegal": "^1.0.27",
"@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartlog": "^3.2.1",
"@push.rocks/smartlog-destination-local": "^9.0.2",
"@push.rocks/smartmustache": "^3.0.2",
"@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartnpm": "^2.0.6",
"@push.rocks/smartobject": "^1.0.12",
"@push.rocks/smartopen": "^2.0.0",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartscaf": "^4.0.19",
"@push.rocks/smartstream": "^3.2.5",
"@push.rocks/smartscaf": "^4.0.21",
"@push.rocks/smartshell": "^3.3.7",
"@push.rocks/smartunique": "^3.0.9",
"@push.rocks/smartupdate": "^2.0.6",
"@types/through2": "^2.0.41",
"prettier": "^3.7.4",
"through2": "^4.0.2"
"prettier": "^3.8.1"
},
"files": [
"ts/**/*",
@@ -107,7 +101,7 @@
"dist_ts_web/**/*",
"assets/**/*",
"cli.js",
"npmextra.json",
".smartconfig.json",
"readme.md"
],
"browserslist": [
+865 -1616
View File
File diff suppressed because it is too large Load Diff
+39 -38
View File
@@ -23,10 +23,10 @@ Gitzone CLI (`@git.zone/cli`) is a comprehensive toolbelt for streamlining local
### Configuration Management
- Uses `npmextra.json` for all tool configuration
- Configuration stored under `gitzone` key in npmextra
- No separate `.gitzonerc` file - everything in npmextra.json
- Project type and module metadata also stored in npmextra
- Uses `.smartconfig.json` for tool configuration
- CLI settings live under the `@git.zone/cli` namespace
- Agent and non-interactive defaults now belong under `@git.zone/cli.cli`
- Project type, module metadata, release settings, commit defaults, and format settings live in the same file
### Format Module (`mod_format`) - SIGNIFICANTLY ENHANCED
@@ -84,7 +84,7 @@ The format module is responsible for project standardization:
1. **Plan → Action Workflow**: Shows changes before applying them
2. **Rollback Mechanism**: Full backup and restore on failures
3. **Enhanced Configuration**: Granular control via npmextra.json
3. **Enhanced Configuration**: Granular control via `.smartconfig.json`
4. **Better Error Handling**: Detailed errors with recovery options
5. **Performance Optimizations**: Parallel execution and caching
6. **Reporting**: Diff views, statistics, verbose logging
@@ -96,6 +96,7 @@ The format module is responsible for project standardization:
The commit module's version bumping has been refactored to eliminate npm command dependencies:
**Changes:**
- Removed `bumpNpmVersion()` - was causing npm warnings to pollute deno.json
- Removed `syncVersionToDenoJson()` - no longer needed with unified approach
- Removed separate `bumpDenoVersion()` - replaced by unified implementation
@@ -104,6 +105,7 @@ The commit module's version bumping has been refactored to eliminate npm command
- Unified `bumpProjectVersion()` - handles npm/deno/both with single clean code path
**Benefits:**
- No npm warning pollution in version fields
- Full control over version bumping process
- Simpler git history (no amending, no force-tagging)
@@ -115,11 +117,13 @@ The commit module's version bumping has been refactored to eliminate npm command
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
@@ -128,7 +132,7 @@ The commit module now supports `-y/--yes` flag for non-interactive commits:
## Development Tips
- Always check readme.plan.md for ongoing improvement plans
- Use npmextra.json for any new configuration options
- Use `.smartconfig.json` for any new configuration options
- Keep modules focused and single-purpose
- Maintain the existing plugin pattern for dependencies
- Test format operations on sample projects before deploying
@@ -140,30 +144,18 @@ The commit module now supports `-y/--yes` flag for non-interactive commits:
```json
{
"gitzone": {
"@git.zone/cli": {
"cli": {
"interactive": true,
"output": "human",
"checkUpdates": true
},
"format": {
"interactive": true,
"parallel": true,
"showStats": true,
"cache": {
"enabled": true,
"clean": true
},
"rollback": {
"enabled": true,
"autoRollbackOnError": true,
"backupRetentionDays": 7
},
"modules": {
"skip": ["prettier"],
"only": [],
"order": []
},
"licenses": {
"allowed": ["MIT", "Apache-2.0"],
"exceptions": {
"some-package": "GPL-3.0"
}
"only": []
}
}
}
@@ -178,6 +170,9 @@ The commit module now supports `-y/--yes` flag for non-interactive commits:
# Interactive commit (default)
gitzone commit
# Read-only recommendation
gitzone commit recommend --json
# Auto-accept AI recommendations (no prompts)
gitzone commit -y
gitzone commit --yes
@@ -197,11 +192,14 @@ gitzone commit --format
# Basic format
gitzone format
# Read-only JSON plan
gitzone format plan --json
# Dry run to preview changes
gitzone format --dry-run
# Non-interactive mode
gitzone format --yes
# Non-interactive apply
gitzone format --write --yes
# Plan only (no execution)
gitzone format --plan-only
@@ -218,11 +216,10 @@ gitzone format --verbose
# Detailed diff views
gitzone format --detailed
# Rollback operations
gitzone format --rollback
gitzone format --rollback <operation-id>
gitzone format --list-backups
gitzone format --clean-backups
# Inspect config for agents and scripts
gitzone config show --json
gitzone config set cli.output json
gitzone config get release.accessLevel
```
## Common Issues (Now Resolved)
@@ -248,10 +245,12 @@ gitzone format --clean-backups
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
@@ -290,13 +289,15 @@ The project has been fully migrated from @push.rocks/smartfile v11 to v13, which
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/* (version bumping)
- ts/mod_services/* (configuration management)
- ts/mod_meta/* (meta repository management)
- ts/mod_standard/* (template listing)
- ts/mod_template/* (template operations)
- ts/mod_format/\* (largest area - 15+ files)
- ts/mod_commit/\* (version bumping)
- 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()`
- Type imports use `import type` for proper verbatim module syntax
+264 -229
View File
@@ -18,11 +18,11 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
### Installation
```bash
# Install globally via npm
npm install -g @git.zone/cli
# Or with pnpm (recommended)
# Install globally via pnpm (recommended)
pnpm add -g @git.zone/cli
# Or with npm
npm install -g @git.zone/cli
```
Once installed, you can use either `gitzone` or the shorter `gzone` command from anywhere in your terminal.
@@ -33,9 +33,12 @@ Once installed, you can use either `gitzone` or the shorter `gzone` command from
# Create a new TypeScript npm package
gitzone template npm
# Format your entire codebase
# Format your entire codebase (dry-run by default)
gitzone format
# Apply formatting changes
gitzone format --write
# Start local MongoDB and MinIO services
gitzone services start
@@ -45,34 +48,156 @@ gitzone commit
## 🛠️ Core Features
### 🔀 Semantic Commits & Versioning
Create standardized commits with AI-powered suggestions that automatically handle versioning:
```bash
# Interactive commit with AI recommendations
gitzone commit
# Read-only recommendation for agents and scripts
gitzone commit recommend --json
# Auto-accept AI recommendations (skipped for BREAKING CHANGEs)
gitzone commit -y
# Auto-accept, push, build, and release
gitzone commit -ypbr
```
**Flags:**
| Flag | Long Form | Description |
| ---- | ----------- | ---------------------------------------- |
| `-y` | `--yes` | Auto-accept AI recommendations |
| `-p` | `--push` | Push to remote after commit |
| `-t` | `--test` | Run tests before committing |
| `-b` | `--build` | Build after commit, verify clean tree |
| `-r` | `--release` | Publish to configured npm registries |
| | `--format` | Run format before committing |
| | `--json` | Emit JSON for `gitzone commit recommend` |
**Workflow steps:**
1. 🤖 **AI-powered analysis** — analyzes your changes and suggests commit type, scope, and message
2. 📝 Interactive commit message builder (type: `fix`/`feat`/`BREAKING CHANGE`, scope, description)
3. 📜 Automatic changelog generation
4. 🏷️ Automatic version bumping (major/minor/patch) with git tag creation
5. 🔨 Optional build & verification
6. 🚀 Optional push to origin
7. 📦 Optional publish to npm registries
Supports both npm (`package.json`) and Deno (`deno.json`) projects, including dual-type projects.
### 🎨 Intelligent Code Formatting
Automatically format and standardize your entire codebase. **Dry-run by default** — nothing changes until you explicitly use `--write`:
```bash
# Preview what would change (default behavior)
gitzone format
# Emit a machine-readable plan
gitzone format plan --json
# Apply changes
gitzone format --write
# Auto-approve without prompts
gitzone format --yes --write
# Show detailed diffs
gitzone format --diff
# Enable verbose logging
gitzone format --verbose
```
**Flags:**
| Flag | Description |
| -------------------- | --------------------------------------------- |
| `--write` / `-w` | Apply changes (default is dry-run) |
| `--yes` | Auto-approve without interactive confirmation |
| `--plan-only` | Only show what would be done |
| `--save-plan <file>` | Save the format plan to a file |
| `--from-plan <file>` | Load and execute a saved plan |
| `--detailed` | Show detailed stats and save report |
| `--verbose` | Enable verbose logging |
| `--diff` | Show file diffs |
| `--json` | Emit a read-only format plan as JSON |
**Formatters (executed in order):**
1. 🧹 **Cleanup** — removes obsolete files (yarn.lock, package-lock.json, tslint.json, etc.)
2. ⚙️ **Smartconfig** — formats and standardizes `.smartconfig.json`
3. 📜 **License** — ensures proper licensing and checks dependency licenses
4. 📦 **Package.json** — standardizes package configuration
5. 📋 **Templates** — applies project template updates
6. 🙈 **Gitignore** — updates repository ignore rules
7. 🔧 **Tsconfig** — optimizes TypeScript configuration
8.**Prettier** — applies code formatting
9. 📖 **Readme** — ensures readme files exist
10. 📂 **Copy** — copies configured files
### 🐳 Development Services Management
Effortlessly manage local MongoDB and MinIO (S3-compatible) services for your development environment:
Effortlessly manage local development services (MongoDB, MinIO S3, Elasticsearch) with Docker:
```bash
gitzone services [command]
```
**Available commands:**
**Commands:**
- **`start [service]`** - Start services (mongo|s3|all)
- **`stop [service]`** - Stop services (mongo|s3|all)
- **`restart [service]`** - Restart services
- **`status`** - Show current service status
- **`config`** - Display configuration details
- **`compass`** - Get MongoDB Compass connection string with network IP
- **`logs [service] [lines]`** - View service logs
- **`remove`** - Remove containers (preserves data)
- **`clean`** - Remove containers AND data (⚠️ destructive)
| Command | Description |
| ------------------------ | ------------------------------------------------------ |
| `start [service]` | Start services (`mongo`\|`s3`\|`elasticsearch`\|`all`) |
| `stop [service]` | Stop services |
| `restart [service]` | Restart services |
| `status` | Show current service status |
| `config` | Display configuration details |
| `set <csv>` | Set enabled services without prompts |
| `enable <service...>` | Enable one or more services |
| `disable <service...>` | Disable one or more services |
| `compass` | Get MongoDB Compass connection string with network IP |
| `logs [service] [lines]` | View service logs (default: 20 lines) |
| `reconfigure` | Reassign ports and restart all services |
| `remove` | Remove containers (preserves data) |
| `clean` | Remove containers AND data (⚠️ destructive) |
**Service aliases:**
- `mongo` / `mongodb` — MongoDB
- `minio` / `s3` — MinIO (S3-compatible storage)
- `elasticsearch` / `es` — Elasticsearch
- `all` — All services (default)
**Key features:**
- 🎲 **Smart port assignment** - Automatically assigns random ports (20000-30000) to avoid conflicts
- 📦 **Project isolation** - Each project gets its own containers with unique names
- 💾 **Data persistence** - Data stored in `.nogit/` directories survives container restarts
- 🔗 **MongoDB Compass support** - Instantly get connection strings for GUI access
- 🌐 **Network IP detection** - Automatically detects your local network IP for remote connections
- ⚙️ **Auto-configuration** - Creates `.nogit/env.json` with smart defaults
- 🎲 **Smart port assignment** — automatically assigns random ports (2000030000) to avoid conflicts
- 📦 **Project isolation** — each project gets its own containers with unique names
- 💾 **Data persistence** — data stored in `.nogit/` survives container restarts
- 🔗 **MongoDB Compass support** — instantly get connection strings for GUI access
- 🌐 **Network IP detection** detects your local network IP for remote connections
- ⚙️ **Auto-configuration** — creates `.nogit/env.json` with smart defaults
**Global operations (`-g` flag):**
```bash
# List all registered projects
gitzone services list -g
# Show status across all projects
gitzone services status -g
# Stop all containers across all projects
gitzone services stop -g
# Remove stale registry entries
gitzone services cleanup -g
```
**Example workflow:**
@@ -80,6 +205,9 @@ gitzone services [command]
# Start all services for your project
gitzone services start
# Configure enabled services without prompts
gitzone services set mongodb,minio
# Check what's running
gitzone services status
@@ -94,7 +222,29 @@ gitzone services logs mongo 50
gitzone services stop
```
The services are configured via `.nogit/env.json` which is automatically created with secure defaults and random ports for each project.
### ⚙️ Release & Commit Configuration
Manage release registries and commit settings:
```bash
gitzone config [subcommand]
```
| Command | Description |
| ---------------------------------- | ---------------------------------------------------------- |
| `show` | Display current release config (registries, access level) |
| `get <path>` | Read a single value from `@git.zone/cli` |
| `set <path> <value>` | Write a single value to `@git.zone/cli` |
| `unset <path>` | Remove a single value from `@git.zone/cli` |
| `add [url]` | Add a registry URL (default: `https://registry.npmjs.org`) |
| `remove [url]` | Remove a registry URL (interactive selection if no URL) |
| `clear` | Clear all registries (with confirmation) |
| `access [public\|private]` | Set npm access level for publishing |
| `commit alwaysTest [true\|false]` | Always run tests before commit |
| `commit alwaysBuild [true\|false]` | Always build after commit |
| `services` | Configure which services are enabled |
Configuration is stored in `.smartconfig.json` under the `@git.zone/cli` key.
### 📦 Project Templates
@@ -104,12 +254,12 @@ Instantly scaffold production-ready projects with best practices built-in:
gitzone template [template-name]
```
**Available templates:**
**Interactive templates:**
- **`npm`** - TypeScript npm package with testing, CI/CD, and full tooling
- **`service`** - Microservice architecture with Docker support
- **`website`** - Modern web application with LitElement and service workers
- **`wcc`** - Web Component Collection for reusable UI components
- **`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:
@@ -119,94 +269,6 @@ Each template comes pre-configured with:
- ✅ Code formatting and linting
- ✅ Documentation structure
### 🎨 Intelligent Code Formatting
The most powerful feature of gitzone - automatically format and standardize your entire codebase:
```bash
# Preview changes without applying them
gitzone format --dry-run
# Format with automatic approval
gitzone format --yes
# Save formatting plan for later execution
gitzone format --save-plan format-plan.json
# Execute a saved plan
gitzone format --from-plan format-plan.json
# Enable verbose output for debugging
gitzone format --verbose
```
**Format features:**
- 🔄 **Smart caching** - Only processes changed files
- 🛡️ **Rollback support** - Undo formatting changes if needed
- 📊 **Detailed reporting** - See exactly what changed
-**Parallel execution** - Format multiple files simultaneously
- 🎯 **Module-specific formatting** - Target specific formatters
**Rollback capabilities:**
```bash
# List all available backups
gitzone format --list-backups
# Rollback to the last operation
gitzone format --rollback
# Rollback to a specific operation
gitzone format --rollback [operation-id]
# Clean old backups
gitzone format --clean-backups
```
**Formatters included:**
- **Prettier** - JavaScript/TypeScript code formatting
- **License** - Ensure proper licensing
- **Package.json** - Standardize package configurations
- **Tsconfig** - TypeScript configuration optimization
- **Readme** - Documentation formatting
- **Gitignore** - Repository ignore rules
- **Templates** - Project template updates
- **Npmextra** - Extended npm configurations
- **Cleanup** - Removes obsolete files (yarn.lock, package-lock.json, tslint.json, etc.)
### 🔀 Semantic Commits & Versioning
Create standardized commits with AI-powered suggestions that automatically handle versioning:
```bash
# Interactive commit with AI recommendations
gitzone commit
# Auto-accept AI recommendations
gitzone commit -y
# Auto-accept and push
gitzone commit -y -p
```
Features:
- 🤖 **AI-powered analysis** - Analyzes your changes and suggests commit type, scope, and message
- 📝 Interactive commit message builder with smart defaults
- 🏷️ Automatic version bumping (major/minor/patch)
- 📜 Changelog generation
- 🚀 Optional auto-push to origin
- 🎯 Conventional commit compliance
The commit wizard guides you through:
1. **Type selection** (fix/feat/BREAKING CHANGE) with AI recommendation
2. **Scope definition** (component/module affected)
3. **Description crafting**
4. **Version bump determination**
### 🏗️ Meta Repository Management
Manage multiple related repositories as a cohesive unit:
@@ -218,36 +280,22 @@ gitzone meta init
# Add a sub-project
gitzone meta add [name] [git-url]
# Update all sub-projects
# Update all sub-projects (clone missing, clean superfluous)
gitzone meta update
# Remove a sub-project
gitzone meta remove [name]
```
Perfect for:
- Monorepo management
- Multi-package projects
- Coordinated deployments
- Synchronized versioning
### 🐳 Docker Management
Streamline your Docker workflow:
```bash
# Clean up all Docker resources
# Clean up all Docker resources (containers, images, volumes, networks)
gitzone docker prune
```
This command removes:
- Stopped containers
- Unused images
- Dangling volumes
- Unused networks
### 🔗 Quick CI/CD Access
Jump directly to your CI/CD configurations:
@@ -270,12 +318,7 @@ Smoothly transition users from old to new packages:
gitzone deprecate
```
Interactive wizard for:
- Setting deprecation notices
- Guiding users to replacements
- Updating registry metadata
- Coordinating migration paths
Interactive wizard that prompts for registry URLs, old package name, and new package name — then runs `npm deprecate` across all specified registries.
### 🚦 Project Initialization
@@ -285,17 +328,10 @@ Prepare existing projects for development:
gitzone start
```
Automatically:
- Checks out master branch
- Pulls latest changes
- Installs dependencies
- Sets up development environment
Automatically checks out master, pulls latest changes, and installs dependencies.
### 🔧 Helper Utilities
Quick utilities for common tasks:
```bash
# Generate a unique short ID
gitzone helpers shortid
@@ -303,31 +339,33 @@ gitzone helpers shortid
## 📋 Configuration
### npmextra.json Configuration
### .smartconfig.json
Customize gitzone behavior through `npmextra.json`:
Customize gitzone behavior through `.smartconfig.json`:
```json
{
"gitzone": {
"@git.zone/cli": {
"projectType": "npm",
"cli": {
"interactive": true,
"output": "human",
"checkUpdates": true
},
"release": {
"registries": ["https://registry.npmjs.org"],
"accessLevel": "public"
},
"commit": {
"alwaysTest": false,
"alwaysBuild": false
},
"format": {
"interactive": true,
"showDiffs": false,
"autoApprove": false,
"parallel": true,
"rollback": {
"enabled": true,
"autoRollbackOnError": true,
"backupRetentionDays": 7
},
"showStats": true,
"modules": {
"skip": ["prettier"],
"only": [],
"order": []
},
"cache": {
"enabled": true,
"clean": true
"only": []
}
}
}
@@ -336,35 +374,9 @@ Customize gitzone behavior through `npmextra.json`:
### Environment Variables
- `CI` - Detect CI environment for automated workflows
- `DEBUG` - Enable debug output
- `GITZONE_FORMAT_PARALLEL` - Control parallel formatting
## 🏆 Best Practices
### For New Projects
1. Start with a template: `gitzone template npm`
2. Set up local services: `gitzone services start`
3. Customize the generated structure
4. Run initial format: `gitzone format`
5. Set up CI/CD: `gitzone open ci`
### For Existing Projects
1. Initialize: `gitzone start`
2. Format codebase: `gitzone format --dry-run` (preview first!)
3. Apply formatting: `gitzone format --yes`
4. Set up services: `gitzone services start`
5. Commit changes: `gitzone commit`
### For Teams
1. Document format preferences in `npmextra.json`
2. Share `.nogit/env.json` template for consistent service setup
3. Use `--save-plan` for reviewable format changes
4. Enable rollback for safety
5. Standardize commit conventions
- `CI` Detect CI environment for automated workflows
- `DEBUG` Enable debug output
- `GITZONE_FORMAT_PARALLEL` Control parallel formatting
## 🎯 Common Workflows
@@ -383,8 +395,9 @@ gitzone services start
# 4. Check service logs if needed
gitzone services logs mongo
# 5. Format code
# 5. Preview format changes, then apply
gitzone format
gitzone format --write
# 6. Commit with semantic versioning
gitzone commit
@@ -393,6 +406,30 @@ gitzone commit
gitzone services stop
```
### Automated CI/CD Commit
```bash
# Auto-accept, test, build, push, and release in one command
gitzone commit -ytbpr
```
### Agent-Friendly Inspection
```bash
# Top-level machine-readable help
gitzone help config --json
# Read-only commit recommendation
gitzone commit recommend --json
# Read-only format plan
gitzone format plan --json
# Read or change config without prompts
gitzone config get release.accessLevel
gitzone config set cli.interactive false
```
### Multi-Repository Management
```bash
@@ -408,29 +445,26 @@ gitzone meta add shared https://github.com/org/shared.git
gitzone meta update
```
### Safe Formatting with Rollback
### Safe Formatting with Plan Review
```bash
# 1. Preview changes
gitzone format --dry-run
# 1. Preview changes (default)
gitzone format
# 2. Save plan for review
gitzone format --save-plan format-changes.json
# 3. Apply formatting
gitzone format --from-plan format-changes.json
# 4. If something goes wrong, rollback
gitzone format --rollback
# 3. Apply from saved plan
gitzone format --from-plan format-changes.json --write
```
### Database-Driven Development
```bash
# 1. Start MongoDB and MinIO
# 1. Start MongoDB, MinIO, and Elasticsearch
gitzone services start
# 2. Get connection string for your app
# 2. Get connection details
gitzone services config
# 3. Connect with MongoDB Compass
@@ -447,43 +481,44 @@ gitzone services clean # ⚠️ Warning: deletes data
### CI/CD Platforms
- **GitLab CI** - Full pipeline support with templates
- **GitHub Actions** - Automated workflows
- **Docker** - Container-based deployments
- **GitLab CI** — full pipeline support with templates
- **GitHub Actions** — automated workflows
- **Docker** — container-based deployments
### Development Tools
- **TypeScript** - First-class support
- **Prettier** - Code formatting
- **npm/pnpm** - Package management
- **MongoDB** - Local database service
- **MinIO** - S3-compatible object storage
- **MongoDB Compass** - Database GUI integration
- **TypeScript** — first-class support
- **Prettier** — code formatting
- **pnpm** — package management
- **MongoDB** — local database service
- **MinIO** S3-compatible object storage
- **Elasticsearch** — search and analytics
- **MongoDB Compass** — database GUI integration
### Version Control
- **Git** - Deep integration
- **Semantic Versioning** - Automatic version bumping
- **Conventional Commits** - Standardized commit messages
- **Git** — deep integration
- **Semantic Versioning** — automatic version bumping
- **Conventional Commits** — standardized commit messages
- **AI-Powered Analysis** — intelligent commit suggestions via `@git.zone/tsdoc`
## 💡 Pro Tips
1. **Use aliases**: Add `alias gz='gitzone'` to your shell profile
2. **Combine commands**: `gitzone format --yes && gitzone commit`
2. **Combine flags**: `gitzone commit -ypbr` for the full auto workflow
3. **Leverage templates**: Start projects right with proven structures
4. **Enable caching**: Dramatically speeds up formatting operations
5. **Save format plans**: Review changes before applying in production
5. **Save format plans**: Review changes before applying
6. **Port management**: Let services auto-assign ports to avoid conflicts
7. **Use MongoDB Compass**: `gitzone services compass` for visual DB management
8. **Global service management**: `gitzone services status -g` to see all projects' services at once
## 🐛 Troubleshooting
### Format Command Shows "Cancelled"
If the format command shows cancelled even after confirming:
- Check your `npmextra.json` configuration
- Try with `--yes` flag to skip confirmation
- Try with `--yes --write` flags
- Use `--verbose` for detailed output
### Docker Commands Fail
@@ -496,19 +531,20 @@ docker info
### Services Won't Start
Check for port conflicts:
```bash
# Services auto-assign ports, but you can check the config
cat .nogit/env.json
# Verify Docker is running
docker ps
# Reassign ports if there are conflicts
gitzone services reconfigure
```
### Template Creation Issues
Verify npm/pnpm is properly configured:
Verify pnpm/npm is properly configured:
```bash
npm config get registry
@@ -524,12 +560,11 @@ npm config get registry
gitzone is optimized for speed:
- **Parallel processing** for format operations
- **Smart caching** to avoid redundant work
- **Incremental updates** for meta repositories
- **Minimal dependencies** for fast installation
- **Isolated services** prevent resource conflicts
- **Auto port assignment** eliminates manual configuration
- **Parallel processing** for format operations
- 🧠 **Smart caching** to avoid redundant work
- 📊 **Incremental updates** for meta repositories
- 🐳 **Isolated services** prevent resource conflicts
- 🎲 **Auto port assignment** eliminates manual configuration
## License and Legal Information
+11
View File
@@ -1,11 +1,13 @@
# GitZone Services Command Implementation Plan
## Overview
Implement the `gitzone services` command to manage MongoDB and MinIO containers for development projects.
## Tasks
### Module Structure Setup
- [x] Create `ts/mod_services/` directory
- [x] Create `mod.plugins.ts` with required imports
- [x] Create `helpers.ts` with utility functions
@@ -15,6 +17,7 @@ Implement the `gitzone services` command to manage MongoDB and MinIO containers
- [x] Create `index.ts` with main command logic
### Core Functionality
- [x] Implement ServiceConfiguration class
- [x] Load/create `.nogit/env.json` configuration
- [x] Generate random available ports (20000-30000 range)
@@ -37,6 +40,7 @@ Implement the `gitzone services` command to manage MongoDB and MinIO containers
- [x] Generate MongoDB Compass connection strings
### Commands Implementation
- [x] `start` command - Start services (mongo|s3|all)
- [x] `stop` command - Stop services (mongo|s3|all)
- [x] `restart` command - Restart services (mongo|s3|all)
@@ -48,12 +52,14 @@ Implement the `gitzone services` command to manage MongoDB and MinIO containers
- [x] `clean` command - Remove containers and data
### Integration
- [x] Add `@push.rocks/smartshell` to main plugins.ts
- [x] Add `@push.rocks/smartnetwork` to main plugins.ts
- [x] Add `@push.rocks/smartinteraction` to main plugins.ts
- [x] Register services command in `gitzone.cli.ts`
### Features
- [x] Auto-configuration with smart defaults
- [x] Random port assignment to avoid conflicts
- [x] Project isolation with unique container names
@@ -65,6 +71,7 @@ Implement the `gitzone services` command to manage MongoDB and MinIO containers
- [x] MongoDB Compass connection string with network IP
### Testing
- [ ] Test service start/stop operations
- [ ] Test configuration creation and updates
- [ ] Test port collision handling
@@ -73,6 +80,7 @@ Implement the `gitzone services` command to manage MongoDB and MinIO containers
- [ ] Test all command variations
## Configuration Format
```json
{
"PROJECT_NAME": "derived-from-package-name",
@@ -91,6 +99,7 @@ Implement the `gitzone services` command to manage MongoDB and MinIO containers
```
## Command Examples
```bash
gitzone services start # Start all services
gitzone services start mongo # Start only MongoDB
@@ -104,10 +113,12 @@ gitzone services clean # Remove containers and data
```
## Progress Notes
Implementation started: 2025-08-14
Implementation completed: 2025-08-14
## Summary
Successfully implemented the `gitzone services` command in TypeScript, providing a complete replacement for the `services.sh` shell script. The implementation includes:
1. **Complete Docker service management** for MongoDB and MinIO containers
Submodule
+1
Submodule test added at 0b89443584
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/cli',
version: '2.9.0',
version: '2.14.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.'
}
+3 -3
View File
@@ -38,11 +38,11 @@ export class GitzoneConfig {
public data: IGitzoneConfigData;
public async readConfigFromCwd() {
const npmextraInstance = new plugins.npmextra.Npmextra(paths.cwd);
this.data = npmextraInstance.dataFor<IGitzoneConfigData>('@git.zone/cli', {});
const smartconfigInstance = new plugins.smartconfig.Smartconfig(paths.cwd);
this.data = smartconfigInstance.dataFor<IGitzoneConfigData>('@git.zone/cli', {});
// Read szci config for backward compatibility
const szciConfig = npmextraInstance.dataFor<any>('@ship.zone/szci', {});
const szciConfig = smartconfigInstance.dataFor<any>('@ship.zone/szci', {});
// Prefer accessLevel from @git.zone/cli.release, fallback to @ship.zone/szci.npmAccessLevel
const accessLevel =
+46 -51
View File
@@ -1,23 +1,29 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { GitzoneConfig } from './classes.gitzoneconfig.js';
import * as plugins from "./plugins.js";
import * as paths from "./paths.js";
import { GitzoneConfig } from "./classes.gitzoneconfig.js";
import { getRawCliMode } from "./helpers.climode.js";
const gitzoneSmartcli = new plugins.smartcli.Smartcli();
export let run = async () => {
const done = plugins.smartpromise.defer();
const rawCliMode = await getRawCliMode();
// get packageInfo
const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
// check for updates
if (rawCliMode.checkUpdates) {
const smartupdateInstance = new plugins.smartupdate.SmartUpdate();
await smartupdateInstance.check(
'gitzone',
"gitzone",
projectInfo.npm.version,
'http://gitzone.gitlab.io/gitzone/changelog.html',
"http://gitzone.gitlab.io/gitzone/changelog.html",
);
console.log('---------------------------------------------');
}
if (rawCliMode.output === "human") {
console.log("---------------------------------------------");
}
gitzoneSmartcli.addVersion(projectInfo.npm.version);
// ======> Standard task <======
@@ -26,8 +32,13 @@ export let run = async () => {
* standard task
*/
gitzoneSmartcli.standardCommand().subscribe(async (argvArg) => {
const modStandard = await import('./mod_standard/index.js');
await modStandard.run();
const modStandard = await import("./mod_standard/index.js");
await modStandard.run(argvArg);
});
gitzoneSmartcli.addCommand("help").subscribe(async (argvArg) => {
const modStandard = await import("./mod_standard/index.js");
await modStandard.run(argvArg);
});
// ======> Specific tasks <======
@@ -35,62 +46,46 @@ export let run = async () => {
/**
* commit something
*/
gitzoneSmartcli.addCommand('commit').subscribe(async (argvArg) => {
const modCommit = await import('./mod_commit/index.js');
gitzoneSmartcli.addCommand("commit").subscribe(async (argvArg) => {
const modCommit = await import("./mod_commit/index.js");
await modCommit.run(argvArg);
});
/**
* deprecate a package on npm
*/
gitzoneSmartcli.addCommand('deprecate').subscribe(async (argvArg) => {
const modDeprecate = await import('./mod_deprecate/index.js');
gitzoneSmartcli.addCommand("deprecate").subscribe(async (argvArg) => {
const modDeprecate = await import("./mod_deprecate/index.js");
await modDeprecate.run();
});
/**
* docker
*/
gitzoneSmartcli.addCommand('docker').subscribe(async (argvArg) => {
const modDocker = await import('./mod_docker/index.js');
gitzoneSmartcli.addCommand("docker").subscribe(async (argvArg) => {
const modDocker = await import("./mod_docker/index.js");
await modDocker.run(argvArg);
});
/**
* Update all files that comply with the gitzone standard
*/
gitzoneSmartcli.addCommand('format').subscribe(async (argvArg) => {
gitzoneSmartcli.addCommand("format").subscribe(async (argvArg) => {
const config = GitzoneConfig.fromCwd();
const modFormat = await import('./mod_format/index.js');
// Handle rollback commands
if (argvArg.rollback) {
await modFormat.handleRollback(argvArg.rollback);
return;
}
if (argvArg['list-backups']) {
await modFormat.handleListBackups();
return;
}
if (argvArg['clean-backups']) {
await modFormat.handleCleanBackups();
return;
}
const modFormat = await import("./mod_format/index.js");
// Handle format with options
// Default is dry-mode, use --write/-w to apply changes
await modFormat.run({
...argvArg,
write: argvArg.write || argvArg.w,
dryRun: argvArg['dry-run'],
dryRun: argvArg["dry-run"],
yes: argvArg.yes,
planOnly: argvArg['plan-only'],
savePlan: argvArg['save-plan'],
fromPlan: argvArg['from-plan'],
planOnly: argvArg["plan-only"],
savePlan: argvArg["save-plan"],
fromPlan: argvArg["from-plan"],
detailed: argvArg.detailed,
interactive: argvArg.interactive !== false,
parallel: argvArg.parallel !== false,
verbose: argvArg.verbose,
diff: argvArg.diff,
});
@@ -99,54 +94,54 @@ export let run = async () => {
/**
* run meta commands
*/
gitzoneSmartcli.addCommand('meta').subscribe(async (argvArg) => {
gitzoneSmartcli.addCommand("meta").subscribe(async (argvArg) => {
const config = GitzoneConfig.fromCwd();
const modMeta = await import('./mod_meta/index.js');
const modMeta = await import("./mod_meta/index.js");
modMeta.run(argvArg);
});
/**
* open assets
*/
gitzoneSmartcli.addCommand('open').subscribe(async (argvArg) => {
const modOpen = await import('./mod_open/index.js');
gitzoneSmartcli.addCommand("open").subscribe(async (argvArg) => {
const modOpen = await import("./mod_open/index.js");
modOpen.run(argvArg);
});
/**
* add a readme to a project
*/
gitzoneSmartcli.addCommand('template').subscribe(async (argvArg) => {
const modTemplate = await import('./mod_template/index.js');
gitzoneSmartcli.addCommand("template").subscribe(async (argvArg) => {
const modTemplate = await import("./mod_template/index.js");
modTemplate.run(argvArg);
});
/**
* start working on a project
*/
gitzoneSmartcli.addCommand('start').subscribe(async (argvArg) => {
const modTemplate = await import('./mod_start/index.js');
gitzoneSmartcli.addCommand("start").subscribe(async (argvArg) => {
const modTemplate = await import("./mod_start/index.js");
modTemplate.run(argvArg);
});
gitzoneSmartcli.addCommand('helpers').subscribe(async (argvArg) => {
const modHelpers = await import('./mod_helpers/index.js');
gitzoneSmartcli.addCommand("helpers").subscribe(async (argvArg) => {
const modHelpers = await import("./mod_helpers/index.js");
modHelpers.run(argvArg);
});
/**
* manage release configuration
*/
gitzoneSmartcli.addCommand('config').subscribe(async (argvArg) => {
const modConfig = await import('./mod_config/index.js');
gitzoneSmartcli.addCommand("config").subscribe(async (argvArg) => {
const modConfig = await import("./mod_config/index.js");
await modConfig.run(argvArg);
});
/**
* manage development services (MongoDB, S3/MinIO)
*/
gitzoneSmartcli.addCommand('services').subscribe(async (argvArg) => {
const modServices = await import('./mod_services/index.js');
gitzoneSmartcli.addCommand("services").subscribe(async (argvArg) => {
const modServices = await import("./mod_services/index.js");
await modServices.run(argvArg);
});
+212
View File
@@ -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;
}
};
+192
View File
@@ -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;
};
+327 -100
View File
@@ -1,21 +1,49 @@
// this file contains code to create commits in a consistent way
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { logger } from '../gitzone.logging.js';
import * as helpers from './mod.helpers.js';
import * as ui from './mod.ui.js';
import { ReleaseConfig } from '../mod_config/classes.releaseconfig.js';
import * as plugins from "./mod.plugins.js";
import * as paths from "../paths.js";
import { logger } from "../gitzone.logging.js";
import * as helpers from "./mod.helpers.js";
import * as ui from "./mod.ui.js";
import { ReleaseConfig } from "../mod_config/classes.releaseconfig.js";
import type { ICliMode } from "../helpers.climode.js";
import {
getCliMode,
printJson,
runWithSuppressedOutput,
} from "../helpers.climode.js";
export const run = async (argvArg: any) => {
// Read commit config from npmextra.json
const npmextraConfig = new plugins.npmextra.Npmextra();
const gitzoneConfig = npmextraConfig.dataFor<{
const mode = await getCliMode(argvArg);
const subcommand = argvArg._?.[1];
if (mode.help || subcommand === "help") {
showHelp(mode);
return;
}
if (subcommand === "recommend") {
await handleRecommend(mode);
return;
}
if (mode.json) {
printJson({
ok: false,
error:
"JSON output is only supported for the read-only recommendation flow. Use `gitzone commit recommend --json`.",
});
return;
}
// Read commit config from .smartconfig.json
const smartconfigInstance = new plugins.smartconfig.Smartconfig();
const gitzoneConfig = smartconfigInstance.dataFor<{
commit?: {
alwaysTest?: boolean;
alwaysBuild?: boolean;
};
}>('@git.zone/cli', {});
}>("@git.zone/cli", {});
const commitConfig = gitzoneConfig.commit || {};
// Check flags and merge with config options
@@ -27,10 +55,12 @@ export const run = async (argvArg: any) => {
if (wantsRelease) {
releaseConfig = await ReleaseConfig.fromCwd();
if (!releaseConfig.hasRegistries()) {
logger.log('error', 'No release registries configured.');
console.log('');
console.log(' Run `gitzone config add <registry-url>` to add registries.');
console.log('');
logger.log("error", "No release registries configured.");
console.log("");
console.log(
" Run `gitzone config add <registry-url>` to add registries.",
);
console.log("");
process.exit(1);
}
}
@@ -47,26 +77,26 @@ export const run = async (argvArg: any) => {
});
if (argvArg.format) {
const formatMod = await import('../mod_format/index.js');
const formatMod = await import("../mod_format/index.js");
await formatMod.run();
}
// Run tests early to fail fast before analysis
if (wantsTest) {
ui.printHeader('🧪 Running tests...');
ui.printHeader("🧪 Running tests...");
const smartshellForTest = new plugins.smartshell.Smartshell({
executor: 'bash',
executor: "bash",
sourceFilePaths: [],
});
const testResult = await smartshellForTest.exec('pnpm test');
const testResult = await smartshellForTest.exec("pnpm test");
if (testResult.exitCode !== 0) {
logger.log('error', 'Tests failed. Aborting commit.');
logger.log("error", "Tests failed. Aborting commit.");
process.exit(1);
}
logger.log('success', 'All tests passed.');
logger.log("success", "All tests passed.");
}
ui.printHeader('🔍 Analyzing repository changes...');
ui.printHeader("🔍 Analyzing repository changes...");
const aidoc = new plugins.tsdoc.AiDoc();
await aidoc.start();
@@ -79,58 +109,63 @@ export const run = async (argvArg: any) => {
recommendedNextVersion: nextCommitObject.recommendedNextVersion,
recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel,
recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope,
recommendedNextVersionMessage: nextCommitObject.recommendedNextVersionMessage,
recommendedNextVersionMessage:
nextCommitObject.recommendedNextVersionMessage,
});
let answerBucket: plugins.smartinteract.AnswerBucket;
// Check if -y/--yes flag is set AND version is not a breaking change
// Breaking changes (major version bumps) always require manual confirmation
const isBreakingChange = nextCommitObject.recommendedNextVersionLevel === 'BREAKING CHANGE';
const isBreakingChange =
nextCommitObject.recommendedNextVersionLevel === "BREAKING CHANGE";
const canAutoAccept = (argvArg.y || argvArg.yes) && !isBreakingChange;
if (canAutoAccept) {
// Auto-mode: create AnswerBucket programmatically
logger.log('info', '✓ Auto-accepting AI recommendations (--yes flag)');
logger.log("info", "✓ Auto-accepting AI recommendations (--yes flag)");
answerBucket = new plugins.smartinteract.AnswerBucket();
answerBucket.addAnswer({
name: 'commitType',
name: "commitType",
value: nextCommitObject.recommendedNextVersionLevel,
});
answerBucket.addAnswer({
name: 'commitScope',
name: "commitScope",
value: nextCommitObject.recommendedNextVersionScope,
});
answerBucket.addAnswer({
name: 'commitDescription',
name: "commitDescription",
value: nextCommitObject.recommendedNextVersionMessage,
});
answerBucket.addAnswer({
name: 'pushToOrigin',
name: "pushToOrigin",
value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided
});
answerBucket.addAnswer({
name: 'createRelease',
name: "createRelease",
value: wantsRelease,
});
} else {
// Warn if --yes was provided but we're requiring confirmation due to breaking change
if (isBreakingChange && (argvArg.y || argvArg.yes)) {
logger.log('warn', '⚠️ BREAKING CHANGE detected - manual confirmation required');
logger.log(
"warn",
"⚠️ BREAKING CHANGE detected - manual confirmation required",
);
}
// Interactive mode: prompt user for input
const commitInteract = new plugins.smartinteract.SmartInteract();
commitInteract.addQuestions([
{
type: 'list',
type: "list",
name: `commitType`,
message: `Choose TYPE of the commit:`,
choices: [`fix`, `feat`, `BREAKING CHANGE`],
default: nextCommitObject.recommendedNextVersionLevel,
},
{
type: 'input',
type: "input",
name: `commitScope`,
message: `What is the SCOPE of the commit:`,
default: nextCommitObject.recommendedNextVersionScope,
@@ -142,13 +177,13 @@ export const run = async (argvArg: any) => {
default: nextCommitObject.recommendedNextVersionMessage,
},
{
type: 'confirm',
type: "confirm",
name: `pushToOrigin`,
message: `Do you want to push this version now?`,
default: true,
},
{
type: 'confirm',
type: "confirm",
name: `createRelease`,
message: `Do you want to publish to npm registries?`,
default: wantsRelease,
@@ -157,40 +192,50 @@ export const run = async (argvArg: any) => {
answerBucket = await commitInteract.runQueue();
}
const commitString = createCommitStringFromAnswerBucket(answerBucket);
const commitVersionType = (() => {
switch (answerBucket.getAnswerFor('commitType')) {
case 'fix':
return 'patch';
case 'feat':
return 'minor';
case 'BREAKING CHANGE':
return 'major';
const commitType = answerBucket.getAnswerFor("commitType");
let commitVersionType: helpers.VersionType;
switch (commitType) {
case "fix":
commitVersionType = "patch";
break;
case "feat":
commitVersionType = "minor";
break;
case "BREAKING CHANGE":
commitVersionType = "major";
break;
default:
throw new Error(`Unsupported commit type: ${commitType}`);
}
})();
ui.printHeader('✨ Creating Semantic Commit');
ui.printHeader("✨ Creating Semantic Commit");
ui.printCommitMessage(commitString);
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash',
executor: "bash",
sourceFilePaths: [],
});
// Load release config if user wants to release (interactively selected)
if (answerBucket.getAnswerFor('createRelease') && !releaseConfig) {
if (answerBucket.getAnswerFor("createRelease") && !releaseConfig) {
releaseConfig = await ReleaseConfig.fromCwd();
if (!releaseConfig.hasRegistries()) {
logger.log('error', 'No release registries configured.');
console.log('');
console.log(' Run `gitzone config add <registry-url>` to add registries.');
console.log('');
logger.log("error", "No release registries configured.");
console.log("");
console.log(
" Run `gitzone config add <registry-url>` to add registries.",
);
console.log("");
process.exit(1);
}
}
// Determine total steps based on options
// Note: test runs early (like format) so not counted in numbered steps
const willPush = answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true');
const willRelease = answerBucket.getAnswerFor('createRelease') && releaseConfig?.hasRegistries();
const willPush =
answerBucket.getAnswerFor("pushToOrigin") && !(process.env.CI === "true");
const willRelease =
answerBucket.getAnswerFor("createRelease") &&
releaseConfig?.hasRegistries();
let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version
if (wantsBuild) totalSteps += 2; // build step + verification step
if (willPush) totalSteps++;
@@ -199,96 +244,156 @@ export const run = async (argvArg: any) => {
// Step 1: Baking commitinfo
currentStep++;
ui.printStep(currentStep, totalSteps, '🔧 Baking commit info into code', 'in-progress');
ui.printStep(
currentStep,
totalSteps,
"🔧 Baking commit info into code",
"in-progress",
);
const commitInfo = new plugins.commitinfo.CommitInfo(
paths.cwd,
commitVersionType,
);
await commitInfo.writeIntoPotentialDirs();
ui.printStep(currentStep, totalSteps, '🔧 Baking commit info into code', 'done');
ui.printStep(
currentStep,
totalSteps,
"🔧 Baking commit info into code",
"done",
);
// Step 2: Writing changelog
currentStep++;
ui.printStep(currentStep, totalSteps, '📄 Generating changelog.md', 'in-progress');
let changelog = nextCommitObject.changelog;
ui.printStep(
currentStep,
totalSteps,
"📄 Generating changelog.md",
"in-progress",
);
let changelog = nextCommitObject.changelog || "# Changelog\n";
changelog = changelog.replaceAll(
'{{nextVersion}}',
"{{nextVersion}}",
(await commitInfo.getNextPlannedVersion()).versionString,
);
changelog = changelog.replaceAll(
'{{nextVersionScope}}',
`${await answerBucket.getAnswerFor('commitType')}(${await answerBucket.getAnswerFor('commitScope')})`,
"{{nextVersionScope}}",
`${await answerBucket.getAnswerFor("commitType")}(${await answerBucket.getAnswerFor("commitScope")})`,
);
changelog = changelog.replaceAll(
'{{nextVersionMessage}}',
"{{nextVersionMessage}}",
nextCommitObject.recommendedNextVersionMessage,
);
if (nextCommitObject.recommendedNextVersionDetails?.length > 0) {
changelog = changelog.replaceAll(
'{{nextVersionDetails}}',
'- ' + nextCommitObject.recommendedNextVersionDetails.join('\n- '),
"{{nextVersionDetails}}",
"- " + nextCommitObject.recommendedNextVersionDetails.join("\n- "),
);
} else {
changelog = changelog.replaceAll('\n{{nextVersionDetails}}', '');
changelog = changelog.replaceAll("\n{{nextVersionDetails}}", "");
}
await plugins.smartfs
.file(plugins.path.join(paths.cwd, `changelog.md`))
.encoding('utf8')
.encoding("utf8")
.write(changelog);
ui.printStep(currentStep, totalSteps, '📄 Generating changelog.md', 'done');
ui.printStep(currentStep, totalSteps, "📄 Generating changelog.md", "done");
// Step 3: Staging files
currentStep++;
ui.printStep(currentStep, totalSteps, '📦 Staging files', 'in-progress');
ui.printStep(currentStep, totalSteps, "📦 Staging files", "in-progress");
await smartshellInstance.exec(`git add -A`);
ui.printStep(currentStep, totalSteps, '📦 Staging files', 'done');
ui.printStep(currentStep, totalSteps, "📦 Staging files", "done");
// Step 4: Creating commit
currentStep++;
ui.printStep(currentStep, totalSteps, '💾 Creating git commit', 'in-progress');
ui.printStep(
currentStep,
totalSteps,
"💾 Creating git commit",
"in-progress",
);
await smartshellInstance.exec(`git commit -m "${commitString}"`);
ui.printStep(currentStep, totalSteps, '💾 Creating git commit', 'done');
ui.printStep(currentStep, totalSteps, "💾 Creating git commit", "done");
// Step 5: Bumping version
currentStep++;
const projectType = await helpers.detectProjectType();
const newVersion = await helpers.bumpProjectVersion(projectType, commitVersionType, currentStep, totalSteps);
const newVersion = await helpers.bumpProjectVersion(
projectType,
commitVersionType,
currentStep,
totalSteps,
);
// Step 6: Run build (optional)
if (wantsBuild) {
currentStep++;
ui.printStep(currentStep, totalSteps, '🔨 Running build', 'in-progress');
const buildResult = await smartshellInstance.exec('pnpm build');
ui.printStep(currentStep, totalSteps, "🔨 Running build", "in-progress");
const buildResult = await smartshellInstance.exec("pnpm build");
if (buildResult.exitCode !== 0) {
ui.printStep(currentStep, totalSteps, '🔨 Running build', 'error');
logger.log('error', 'Build failed. Aborting release.');
ui.printStep(currentStep, totalSteps, "🔨 Running build", "error");
logger.log("error", "Build failed. Aborting release.");
process.exit(1);
}
ui.printStep(currentStep, totalSteps, '🔨 Running build', 'done');
ui.printStep(currentStep, totalSteps, "🔨 Running build", "done");
// Step 7: Verify no uncommitted changes
currentStep++;
ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'in-progress');
const statusResult = await smartshellInstance.exec('git status --porcelain');
if (statusResult.stdout.trim() !== '') {
ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'error');
logger.log('error', 'Build produced uncommitted changes. This usually means build output is not gitignored.');
logger.log('error', 'Uncommitted files:');
ui.printStep(
currentStep,
totalSteps,
"🔍 Verifying clean working tree",
"in-progress",
);
const statusResult = await smartshellInstance.exec(
"git status --porcelain",
);
if (statusResult.stdout.trim() !== "") {
ui.printStep(
currentStep,
totalSteps,
"🔍 Verifying clean working tree",
"error",
);
logger.log(
"error",
"Build produced uncommitted changes. This usually means build output is not gitignored.",
);
logger.log("error", "Uncommitted files:");
console.log(statusResult.stdout);
logger.log('error', 'Aborting release. Please ensure build artifacts are in .gitignore');
logger.log(
"error",
"Aborting release. Please ensure build artifacts are in .gitignore",
);
process.exit(1);
}
ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'done');
ui.printStep(
currentStep,
totalSteps,
"🔍 Verifying clean working tree",
"done",
);
}
// Step: Push to remote (optional)
const currentBranch = await helpers.detectCurrentBranch();
if (willPush) {
currentStep++;
ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'in-progress');
await smartshellInstance.exec(`git push origin ${currentBranch} --follow-tags`);
ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'done');
ui.printStep(
currentStep,
totalSteps,
`🚀 Pushing to origin/${currentBranch}`,
"in-progress",
);
await smartshellInstance.exec(
`git push origin ${currentBranch} --follow-tags`,
);
ui.printStep(
currentStep,
totalSteps,
`🚀 Pushing to origin/${currentBranch}`,
"done",
);
}
// Step 7: Publish to npm registries (optional)
@@ -296,51 +401,173 @@ export const run = async (argvArg: any) => {
if (willRelease && releaseConfig) {
currentStep++;
const registries = releaseConfig.getRegistries();
ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'in-progress');
ui.printStep(
currentStep,
totalSteps,
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
"in-progress",
);
const accessLevel = releaseConfig.getAccessLevel();
for (const registry of registries) {
try {
await smartshellInstance.exec(`npm publish --registry=${registry} --access=${accessLevel}`);
await smartshellInstance.exec(
`npm publish --registry=${registry} --access=${accessLevel}`,
);
releasedRegistries.push(registry);
} catch (error) {
logger.log('error', `Failed to publish to ${registry}: ${error}`);
logger.log("error", `Failed to publish to ${registry}: ${error}`);
}
}
if (releasedRegistries.length === registries.length) {
ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'done');
ui.printStep(
currentStep,
totalSteps,
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
"done",
);
} else {
ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'error');
ui.printStep(
currentStep,
totalSteps,
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
"error",
);
}
}
console.log(''); // Add spacing before summary
console.log(""); // Add spacing before summary
// Get commit SHA for summary
const commitShaResult = await smartshellInstance.exec('git rev-parse --short HEAD');
const commitShaResult = await smartshellInstance.exec(
"git rev-parse --short HEAD",
);
const commitSha = commitShaResult.stdout.trim();
// Print final summary
ui.printSummary({
projectType,
branch: currentBranch,
commitType: answerBucket.getAnswerFor('commitType'),
commitScope: answerBucket.getAnswerFor('commitScope'),
commitMessage: answerBucket.getAnswerFor('commitDescription'),
commitType: answerBucket.getAnswerFor("commitType"),
commitScope: answerBucket.getAnswerFor("commitScope"),
commitMessage: answerBucket.getAnswerFor("commitDescription"),
newVersion: newVersion,
commitSha: commitSha,
pushed: willPush,
released: releasedRegistries.length > 0,
releasedRegistries: releasedRegistries.length > 0 ? releasedRegistries : undefined,
releasedRegistries:
releasedRegistries.length > 0 ? releasedRegistries : undefined,
});
};
async function handleRecommend(mode: ICliMode): Promise<void> {
const recommendationBuilder = async () => {
const aidoc = new plugins.tsdoc.AiDoc();
await aidoc.start();
try {
return await aidoc.buildNextCommitObject(paths.cwd);
} finally {
await aidoc.stop();
}
};
const recommendation = mode.json
? await runWithSuppressedOutput(recommendationBuilder)
: await recommendationBuilder();
if (mode.json) {
printJson(recommendation);
return;
}
ui.printRecommendation({
recommendedNextVersion: recommendation.recommendedNextVersion,
recommendedNextVersionLevel: recommendation.recommendedNextVersionLevel,
recommendedNextVersionScope: recommendation.recommendedNextVersionScope,
recommendedNextVersionMessage: recommendation.recommendedNextVersionMessage,
});
console.log(
`Suggested commit: ${recommendation.recommendedNextVersionLevel}(${recommendation.recommendedNextVersionScope}): ${recommendation.recommendedNextVersionMessage}`,
);
}
const createCommitStringFromAnswerBucket = (
answerBucket: plugins.smartinteract.AnswerBucket,
) => {
const commitType = answerBucket.getAnswerFor('commitType');
const commitScope = answerBucket.getAnswerFor('commitScope');
const commitDescription = answerBucket.getAnswerFor('commitDescription');
const commitType = answerBucket.getAnswerFor("commitType");
const commitScope = answerBucket.getAnswerFor("commitScope");
const commitDescription = answerBucket.getAnswerFor("commitDescription");
return `${commitType}(${commitScope}): ${commitDescription}`;
};
export function showHelp(mode?: ICliMode): void {
if (mode?.json) {
printJson({
command: "commit",
usage: "gitzone commit [recommend] [options]",
description:
"Creates semantic commits or emits a read-only recommendation.",
commands: [
{
name: "recommend",
description:
"Generate a commit recommendation without mutating the repository",
},
],
flags: [
{ flag: "-y, --yes", description: "Auto-accept AI recommendations" },
{ flag: "-p, --push", description: "Push to origin after commit" },
{ flag: "-t, --test", description: "Run tests before the commit flow" },
{
flag: "-b, --build",
description: "Run the build after the commit flow",
},
{
flag: "-r, --release",
description: "Publish to configured registries after push",
},
{
flag: "--format",
description: "Run gitzone format before committing",
},
{
flag: "--json",
description: "Emit JSON for `commit recommend` only",
},
],
examples: [
"gitzone commit recommend --json",
"gitzone commit -y",
"gitzone commit -ypbr",
],
});
return;
}
console.log("");
console.log("Usage: gitzone commit [recommend] [options]");
console.log("");
console.log("Commands:");
console.log(
" recommend Generate a commit recommendation without mutating the repository",
);
console.log("");
console.log("Flags:");
console.log(" -y, --yes Auto-accept AI recommendations");
console.log(" -p, --push Push to origin after commit");
console.log(" -t, --test Run tests before the commit flow");
console.log(" -b, --build Run the build after the commit flow");
console.log(
" -r, --release Publish to configured registries after push",
);
console.log(" --format Run gitzone format before committing");
console.log(" --json Emit JSON for `commit recommend` only");
console.log("");
console.log("Examples:");
console.log(" gitzone commit recommend --json");
console.log(" gitzone commit -y");
console.log(" gitzone commit -ypbr");
console.log("");
}
+19 -19
View File
@@ -6,7 +6,7 @@ export interface ICommitConfig {
}
/**
* Manages commit configuration stored in npmextra.json
* Manages commit configuration stored in .smartconfig.json
* under @git.zone/cli.commit namespace
*/
export class CommitConfig {
@@ -28,11 +28,11 @@ export class CommitConfig {
}
/**
* Load configuration from npmextra.json
* Load configuration from .smartconfig.json
*/
public async load(): Promise<void> {
const npmextraInstance = new plugins.npmextra.Npmextra(this.cwd);
const gitzoneConfig = npmextraInstance.dataFor<any>('@git.zone/cli', {});
const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
this.config = {
alwaysTest: gitzoneConfig?.commit?.alwaysTest ?? false,
@@ -41,37 +41,37 @@ export class CommitConfig {
}
/**
* Save configuration to npmextra.json
* Save configuration to .smartconfig.json
*/
public async save(): Promise<void> {
const npmextraPath = plugins.path.join(this.cwd, 'npmextra.json');
let npmextraData: any = {};
const smartconfigPath = plugins.path.join(this.cwd, '.smartconfig.json');
let smartconfigData: any = {};
// Read existing npmextra.json
if (await plugins.smartfs.file(npmextraPath).exists()) {
const content = await plugins.smartfs.file(npmextraPath).encoding('utf8').read();
npmextraData = JSON.parse(content as string);
// Read existing .smartconfig.json
if (await plugins.smartfs.file(smartconfigPath).exists()) {
const content = await plugins.smartfs.file(smartconfigPath).encoding('utf8').read();
smartconfigData = JSON.parse(content as string);
}
// Ensure @git.zone/cli namespace exists
if (!npmextraData['@git.zone/cli']) {
npmextraData['@git.zone/cli'] = {};
if (!smartconfigData['@git.zone/cli']) {
smartconfigData['@git.zone/cli'] = {};
}
// Ensure commit object exists
if (!npmextraData['@git.zone/cli'].commit) {
npmextraData['@git.zone/cli'].commit = {};
if (!smartconfigData['@git.zone/cli'].commit) {
smartconfigData['@git.zone/cli'].commit = {};
}
// Update commit settings
npmextraData['@git.zone/cli'].commit.alwaysTest = this.config.alwaysTest;
npmextraData['@git.zone/cli'].commit.alwaysBuild = this.config.alwaysBuild;
smartconfigData['@git.zone/cli'].commit.alwaysTest = this.config.alwaysTest;
smartconfigData['@git.zone/cli'].commit.alwaysBuild = this.config.alwaysBuild;
// Write back to file
await plugins.smartfs
.file(npmextraPath)
.file(smartconfigPath)
.encoding('utf8')
.write(JSON.stringify(npmextraData, null, 2));
.write(JSON.stringify(smartconfigData, null, 2));
}
/**
+20 -20
View File
@@ -8,7 +8,7 @@ export interface IReleaseConfig {
}
/**
* Manages release configuration stored in npmextra.json
* Manages release configuration stored in .smartconfig.json
* under @git.zone/cli.release namespace
*/
export class ReleaseConfig {
@@ -30,14 +30,14 @@ export class ReleaseConfig {
}
/**
* Load configuration from npmextra.json
* Load configuration from .smartconfig.json
*/
public async load(): Promise<void> {
const npmextraInstance = new plugins.npmextra.Npmextra(this.cwd);
const gitzoneConfig = npmextraInstance.dataFor<any>('@git.zone/cli', {});
const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
// Also check szci for backward compatibility
const szciConfig = npmextraInstance.dataFor<any>('@ship.zone/szci', {});
const szciConfig = smartconfigInstance.dataFor<any>('@ship.zone/szci', {});
this.config = {
registries: gitzoneConfig?.release?.registries || [],
@@ -46,37 +46,37 @@ export class ReleaseConfig {
}
/**
* Save configuration to npmextra.json
* Save configuration to .smartconfig.json
*/
public async save(): Promise<void> {
const npmextraPath = plugins.path.join(this.cwd, 'npmextra.json');
let npmextraData: any = {};
const smartconfigPath = plugins.path.join(this.cwd, '.smartconfig.json');
let smartconfigData: any = {};
// Read existing npmextra.json
if (await plugins.smartfs.file(npmextraPath).exists()) {
const content = await plugins.smartfs.file(npmextraPath).encoding('utf8').read();
npmextraData = JSON.parse(content as string);
// Read existing .smartconfig.json
if (await plugins.smartfs.file(smartconfigPath).exists()) {
const content = await plugins.smartfs.file(smartconfigPath).encoding('utf8').read();
smartconfigData = JSON.parse(content as string);
}
// Ensure @git.zone/cli namespace exists
if (!npmextraData['@git.zone/cli']) {
npmextraData['@git.zone/cli'] = {};
if (!smartconfigData['@git.zone/cli']) {
smartconfigData['@git.zone/cli'] = {};
}
// Ensure release object exists
if (!npmextraData['@git.zone/cli'].release) {
npmextraData['@git.zone/cli'].release = {};
if (!smartconfigData['@git.zone/cli'].release) {
smartconfigData['@git.zone/cli'].release = {};
}
// Update registries and accessLevel
npmextraData['@git.zone/cli'].release.registries = this.config.registries;
npmextraData['@git.zone/cli'].release.accessLevel = this.config.accessLevel;
smartconfigData['@git.zone/cli'].release.registries = this.config.registries;
smartconfigData['@git.zone/cli'].release.accessLevel = this.config.accessLevel;
// Write back to file
await plugins.smartfs
.file(npmextraPath)
.file(smartconfigPath)
.encoding('utf8')
.write(JSON.stringify(npmextraData, null, 2));
.write(JSON.stringify(smartconfigData, null, 2));
}
/**
+490 -182
View File
@@ -1,73 +1,116 @@
// gitzone config - manage release registry configuration
import * as plugins from './mod.plugins.js';
import { ReleaseConfig } from './classes.releaseconfig.js';
import { CommitConfig } from './classes.commitconfig.js';
import { runFormatter, type ICheckResult } from '../mod_format/index.js';
import * as plugins from "./mod.plugins.js";
import { ReleaseConfig } from "./classes.releaseconfig.js";
import { CommitConfig } from "./classes.commitconfig.js";
import { runFormatter, type ICheckResult } from "../mod_format/index.js";
import type { ICliMode } from "../helpers.climode.js";
import { getCliMode, printJson } from "../helpers.climode.js";
import {
getCliConfigValueFromData,
readSmartconfigFile,
setCliConfigValueInData,
unsetCliConfigValueInData,
writeSmartconfigFile,
} from "../helpers.smartconfig.js";
export { ReleaseConfig, CommitConfig };
const defaultCliMode: ICliMode = {
output: "human",
interactive: true,
json: false,
plain: false,
quiet: false,
yes: false,
help: false,
agent: false,
checkUpdates: true,
isTty: true,
};
/**
* Format npmextra.json with diff preview
* Format .smartconfig.json with diff preview
* Shows diff first, asks for confirmation, then applies
*/
async function formatNpmextraWithDiff(): Promise<void> {
async function formatSmartconfigWithDiff(mode: ICliMode): Promise<void> {
if (!mode.interactive) {
return;
}
// Check for diffs first
const checkResult = await runFormatter('npmextra', {
const checkResult = (await runFormatter("smartconfig", {
checkOnly: true,
showDiff: true,
}) as ICheckResult | void;
})) as ICheckResult | void;
if (checkResult && checkResult.hasDiff) {
const shouldApply = await plugins.smartinteract.SmartInteract.getCliConfirmation(
'Apply formatting changes to npmextra.json?',
true
const shouldApply =
await plugins.smartinteract.SmartInteract.getCliConfirmation(
"Apply formatting changes to .smartconfig.json?",
true,
);
if (shouldApply) {
await runFormatter('npmextra', { silent: true });
await runFormatter("smartconfig", { silent: true });
}
}
}
export const run = async (argvArg: any) => {
const mode = await getCliMode(argvArg);
const command = argvArg._?.[1];
const value = argvArg._?.[2];
if (mode.help || command === "help") {
showHelp(mode);
return;
}
// If no command provided, show interactive menu
if (!command) {
if (!mode.interactive) {
showHelp(mode);
return;
}
await handleInteractiveMenu();
return;
}
switch (command) {
case 'show':
await handleShow();
case "show":
await handleShow(mode);
break;
case 'add':
await handleAdd(value);
case "add":
await handleAdd(value, mode);
break;
case 'remove':
await handleRemove(value);
case "remove":
await handleRemove(value, mode);
break;
case 'clear':
await handleClear();
case "clear":
await handleClear(mode);
break;
case 'access':
case 'accessLevel':
await handleAccessLevel(value);
case "access":
case "accessLevel":
await handleAccessLevel(value, mode);
break;
case 'commit':
await handleCommit(argvArg._?.[2], argvArg._?.[3]);
case "commit":
await handleCommit(argvArg._?.[2], argvArg._?.[3], mode);
break;
case 'services':
await handleServices();
case "services":
await handleServices(mode);
break;
case 'help':
showHelp();
case "get":
await handleGet(value, mode);
break;
case "set":
await handleSet(value, argvArg._?.[3], mode);
break;
case "unset":
await handleUnset(value, mode);
break;
default:
plugins.logger.log('error', `Unknown command: ${command}`);
showHelp();
plugins.logger.log("error", `Unknown command: ${command}`);
showHelp(mode);
}
};
@@ -75,55 +118,61 @@ export const run = async (argvArg: any) => {
* Interactive menu for config command
*/
async function handleInteractiveMenu(): Promise<void> {
console.log('');
console.log('╭─────────────────────────────────────────────────────────────╮');
console.log('│ gitzone config - Project Configuration │');
console.log('╰─────────────────────────────────────────────────────────────╯');
console.log('');
console.log("");
console.log(
"╭─────────────────────────────────────────────────────────────╮",
);
console.log(
"│ gitzone config - Project Configuration │",
);
console.log(
"╰─────────────────────────────────────────────────────────────╯",
);
console.log("");
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: 'list',
name: 'action',
message: 'What would you like to do?',
default: 'show',
type: "list",
name: "action",
message: "What would you like to do?",
default: "show",
choices: [
{ name: 'Show current configuration', value: 'show' },
{ name: 'Add a registry', value: 'add' },
{ name: 'Remove a registry', value: 'remove' },
{ name: 'Clear all registries', value: 'clear' },
{ name: 'Set access level (public/private)', value: 'access' },
{ name: 'Configure commit options', value: 'commit' },
{ name: 'Configure services', value: 'services' },
{ name: 'Show help', value: 'help' },
{ name: "Show current configuration", value: "show" },
{ name: "Add a registry", value: "add" },
{ name: "Remove a registry", value: "remove" },
{ name: "Clear all registries", value: "clear" },
{ name: "Set access level (public/private)", value: "access" },
{ name: "Configure commit options", value: "commit" },
{ name: "Configure services", value: "services" },
{ name: "Show help", value: "help" },
],
});
const action = (response as any).value;
switch (action) {
case 'show':
await handleShow();
case "show":
await handleShow(defaultCliMode);
break;
case 'add':
await handleAdd();
case "add":
await handleAdd(undefined, defaultCliMode);
break;
case 'remove':
await handleRemove();
case "remove":
await handleRemove(undefined, defaultCliMode);
break;
case 'clear':
await handleClear();
case "clear":
await handleClear(defaultCliMode);
break;
case 'access':
await handleAccessLevel();
case "access":
await handleAccessLevel(undefined, defaultCliMode);
break;
case 'commit':
await handleCommit();
case "commit":
await handleCommit(undefined, undefined, defaultCliMode);
break;
case 'services':
await handleServices();
case "services":
await handleServices(defaultCliMode);
break;
case 'help':
case "help":
showHelp();
break;
}
@@ -132,50 +181,69 @@ async function handleInteractiveMenu(): Promise<void> {
/**
* Show current registry configuration
*/
async function handleShow(): Promise<void> {
async function handleShow(mode: ICliMode): Promise<void> {
if (mode.json) {
const smartconfigData = await readSmartconfigFile();
printJson(getCliConfigValueFromData(smartconfigData, ""));
return;
}
const config = await ReleaseConfig.fromCwd();
const registries = config.getRegistries();
const accessLevel = config.getAccessLevel();
console.log('');
console.log('╭─────────────────────────────────────────────────────────────╮');
console.log('│ Release Configuration │');
console.log('╰─────────────────────────────────────────────────────────────╯');
console.log('');
console.log("");
console.log(
"╭─────────────────────────────────────────────────────────────╮",
);
console.log(
"│ Release Configuration │",
);
console.log(
"╰─────────────────────────────────────────────────────────────╯",
);
console.log("");
// Show access level
plugins.logger.log('info', `Access Level: ${accessLevel}`);
console.log('');
plugins.logger.log("info", `Access Level: ${accessLevel}`);
console.log("");
if (registries.length === 0) {
plugins.logger.log('info', 'No release registries configured.');
console.log('');
console.log(' Run `gitzone config add <registry-url>` to add one.');
console.log('');
plugins.logger.log("info", "No release registries configured.");
console.log("");
console.log(" Run `gitzone config add <registry-url>` to add one.");
console.log("");
} else {
plugins.logger.log('info', `Configured registries (${registries.length}):`);
console.log('');
plugins.logger.log("info", `Configured registries (${registries.length}):`);
console.log("");
registries.forEach((url, index) => {
console.log(` ${index + 1}. ${url}`);
});
console.log('');
console.log("");
}
}
/**
* Add a registry URL
*/
async function handleAdd(url?: string): Promise<void> {
async function handleAdd(
url: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!url) {
if (!mode.interactive) {
throw new Error("Registry URL is required in non-interactive mode");
}
// Interactive mode
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: 'input',
name: 'registryUrl',
message: 'Enter registry URL:',
default: 'https://registry.npmjs.org',
type: "input",
name: "registryUrl",
message: "Enter registry URL:",
default: "https://registry.npmjs.org",
validate: (input: string) => {
return !!(input && input.trim() !== '');
return !!(input && input.trim() !== "");
},
});
url = (response as any).value;
@@ -186,32 +254,48 @@ async function handleAdd(url?: string): Promise<void> {
if (added) {
await config.save();
plugins.logger.log('success', `Added registry: ${url}`);
await formatNpmextraWithDiff();
if (mode.json) {
printJson({
ok: true,
action: "add",
registry: url,
registries: config.getRegistries(),
});
return;
}
plugins.logger.log("success", `Added registry: ${url}`);
await formatSmartconfigWithDiff(mode);
} else {
plugins.logger.log('warn', `Registry already exists: ${url}`);
plugins.logger.log("warn", `Registry already exists: ${url}`);
}
}
/**
* Remove a registry URL
*/
async function handleRemove(url?: string): Promise<void> {
async function handleRemove(
url: string | undefined,
mode: ICliMode,
): Promise<void> {
const config = await ReleaseConfig.fromCwd();
const registries = config.getRegistries();
if (registries.length === 0) {
plugins.logger.log('warn', 'No registries configured to remove.');
plugins.logger.log("warn", "No registries configured to remove.");
return;
}
if (!url) {
if (!mode.interactive) {
throw new Error("Registry URL is required in non-interactive mode");
}
// Interactive mode - show list to select from
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: 'list',
name: 'registryUrl',
message: 'Select registry to remove:',
type: "list",
name: "registryUrl",
message: "Select registry to remove:",
choices: registries,
default: registries[0],
});
@@ -222,99 +306,135 @@ async function handleRemove(url?: string): Promise<void> {
if (removed) {
await config.save();
plugins.logger.log('success', `Removed registry: ${url}`);
await formatNpmextraWithDiff();
if (mode.json) {
printJson({
ok: true,
action: "remove",
registry: url,
registries: config.getRegistries(),
});
return;
}
plugins.logger.log("success", `Removed registry: ${url}`);
await formatSmartconfigWithDiff(mode);
} else {
plugins.logger.log('warn', `Registry not found: ${url}`);
plugins.logger.log("warn", `Registry not found: ${url}`);
}
}
/**
* Clear all registries
*/
async function handleClear(): Promise<void> {
async function handleClear(mode: ICliMode): Promise<void> {
const config = await ReleaseConfig.fromCwd();
if (!config.hasRegistries()) {
plugins.logger.log('info', 'No registries to clear.');
plugins.logger.log("info", "No registries to clear.");
return;
}
// Confirm before clearing
const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
'Clear all configured registries?',
false
);
const confirmed = mode.interactive
? await plugins.smartinteract.SmartInteract.getCliConfirmation(
"Clear all configured registries?",
false,
)
: true;
if (confirmed) {
config.clearRegistries();
await config.save();
plugins.logger.log('success', 'All registries cleared.');
await formatNpmextraWithDiff();
if (mode.json) {
printJson({ ok: true, action: "clear", registries: [] });
return;
}
plugins.logger.log("success", "All registries cleared.");
await formatSmartconfigWithDiff(mode);
} else {
plugins.logger.log('info', 'Operation cancelled.');
plugins.logger.log("info", "Operation cancelled.");
}
}
/**
* Set or toggle access level
*/
async function handleAccessLevel(level?: string): Promise<void> {
async function handleAccessLevel(
level: string | undefined,
mode: ICliMode,
): Promise<void> {
const config = await ReleaseConfig.fromCwd();
const currentLevel = config.getAccessLevel();
if (!level) {
if (!mode.interactive) {
throw new Error("Access level is required in non-interactive mode");
}
// Interactive mode - toggle or ask
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: 'list',
name: 'accessLevel',
message: 'Select npm access level for publishing:',
choices: ['public', 'private'],
type: "list",
name: "accessLevel",
message: "Select npm access level for publishing:",
choices: ["public", "private"],
default: currentLevel,
});
level = (response as any).value;
}
// Validate the level
if (level !== 'public' && level !== 'private') {
plugins.logger.log('error', `Invalid access level: ${level}. Must be 'public' or 'private'.`);
if (level !== "public" && level !== "private") {
plugins.logger.log(
"error",
`Invalid access level: ${level}. Must be 'public' or 'private'.`,
);
return;
}
if (level === currentLevel) {
plugins.logger.log('info', `Access level is already set to: ${level}`);
plugins.logger.log("info", `Access level is already set to: ${level}`);
return;
}
config.setAccessLevel(level as 'public' | 'private');
config.setAccessLevel(level as "public" | "private");
await config.save();
plugins.logger.log('success', `Access level set to: ${level}`);
await formatNpmextraWithDiff();
if (mode.json) {
printJson({ ok: true, action: "access", accessLevel: level });
return;
}
plugins.logger.log("success", `Access level set to: ${level}`);
await formatSmartconfigWithDiff(mode);
}
/**
* Handle commit configuration
*/
async function handleCommit(setting?: string, value?: string): Promise<void> {
async function handleCommit(
setting: string | undefined,
value: string | undefined,
mode: ICliMode,
): Promise<void> {
const config = await CommitConfig.fromCwd();
// No setting = interactive mode
if (!setting) {
if (!mode.interactive) {
throw new Error("Commit setting is required in non-interactive mode");
}
await handleCommitInteractive(config);
return;
}
// Direct setting
switch (setting) {
case 'alwaysTest':
await handleCommitSetting(config, 'alwaysTest', value);
case "alwaysTest":
await handleCommitSetting(config, "alwaysTest", value, mode);
break;
case 'alwaysBuild':
await handleCommitSetting(config, 'alwaysBuild', value);
case "alwaysBuild":
await handleCommitSetting(config, "alwaysBuild", value, mode);
break;
default:
plugins.logger.log('error', `Unknown commit setting: ${setting}`);
plugins.logger.log("error", `Unknown commit setting: ${setting}`);
showCommitHelp();
}
}
@@ -323,109 +443,297 @@ async function handleCommit(setting?: string, value?: string): Promise<void> {
* Interactive commit configuration
*/
async function handleCommitInteractive(config: CommitConfig): Promise<void> {
console.log('');
console.log('╭─────────────────────────────────────────────────────────────╮');
console.log('│ Commit Configuration │');
console.log('╰─────────────────────────────────────────────────────────────╯');
console.log('');
console.log("");
console.log(
"╭─────────────────────────────────────────────────────────────╮",
);
console.log(
"│ Commit Configuration │",
);
console.log(
"╰─────────────────────────────────────────────────────────────╯",
);
console.log("");
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: 'checkbox',
name: 'commitOptions',
message: 'Select commit options to enable:',
type: "checkbox",
name: "commitOptions",
message: "Select commit options to enable:",
choices: [
{ name: 'Always run tests before commit (-t)', value: 'alwaysTest' },
{ name: 'Always build after commit (-b)', value: 'alwaysBuild' },
{ name: "Always run tests before commit (-t)", value: "alwaysTest" },
{ name: "Always build after commit (-b)", value: "alwaysBuild" },
],
default: [
...(config.getAlwaysTest() ? ['alwaysTest'] : []),
...(config.getAlwaysBuild() ? ['alwaysBuild'] : []),
...(config.getAlwaysTest() ? ["alwaysTest"] : []),
...(config.getAlwaysBuild() ? ["alwaysBuild"] : []),
],
});
const selected = (response as any).value || [];
config.setAlwaysTest(selected.includes('alwaysTest'));
config.setAlwaysBuild(selected.includes('alwaysBuild'));
config.setAlwaysTest(selected.includes("alwaysTest"));
config.setAlwaysBuild(selected.includes("alwaysBuild"));
await config.save();
plugins.logger.log('success', 'Commit configuration updated');
await formatNpmextraWithDiff();
plugins.logger.log("success", "Commit configuration updated");
await formatSmartconfigWithDiff(defaultCliMode);
}
/**
* Set a specific commit setting
*/
async function handleCommitSetting(config: CommitConfig, setting: string, value?: string): Promise<void> {
async function handleCommitSetting(
config: CommitConfig,
setting: string,
value: string | undefined,
mode: ICliMode,
): Promise<void> {
// Parse boolean value
const boolValue = value === 'true' || value === '1' || value === 'on';
const boolValue = value === "true" || value === "1" || value === "on";
if (setting === 'alwaysTest') {
if (setting === "alwaysTest") {
config.setAlwaysTest(boolValue);
} else if (setting === 'alwaysBuild') {
} else if (setting === "alwaysBuild") {
config.setAlwaysBuild(boolValue);
}
await config.save();
plugins.logger.log('success', `Set ${setting} to ${boolValue}`);
await formatNpmextraWithDiff();
if (mode.json) {
printJson({ ok: true, action: "commit", setting, value: boolValue });
return;
}
plugins.logger.log("success", `Set ${setting} to ${boolValue}`);
await formatSmartconfigWithDiff(mode);
}
/**
* Show help for commit subcommand
*/
function showCommitHelp(): void {
console.log('');
console.log('Usage: gitzone config commit [setting] [value]');
console.log('');
console.log('Settings:');
console.log(' alwaysTest [true|false] Always run tests before commit');
console.log(' alwaysBuild [true|false] Always build after commit');
console.log('');
console.log('Examples:');
console.log(' gitzone config commit # Interactive mode');
console.log(' gitzone config commit alwaysTest true');
console.log(' gitzone config commit alwaysBuild false');
console.log('');
console.log("");
console.log("Usage: gitzone config commit [setting] [value]");
console.log("");
console.log("Settings:");
console.log(" alwaysTest [true|false] Always run tests before commit");
console.log(" alwaysBuild [true|false] Always build after commit");
console.log("");
console.log("Examples:");
console.log(" gitzone config commit # Interactive mode");
console.log(" gitzone config commit alwaysTest true");
console.log(" gitzone config commit alwaysBuild false");
console.log("");
}
/**
* Handle services configuration
*/
async function handleServices(): Promise<void> {
async function handleServices(mode: ICliMode): Promise<void> {
if (!mode.interactive) {
throw new Error(
"Use `gitzone services config --json` or `gitzone services set ...` in non-interactive mode",
);
}
// Import and use ServiceManager's configureServices
const { ServiceManager } = await import('../mod_services/classes.servicemanager.js');
const { ServiceManager } =
await import("../mod_services/classes.servicemanager.js");
const serviceManager = new ServiceManager();
await serviceManager.init();
await serviceManager.configureServices();
}
async function handleGet(
configPath: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!configPath) {
throw new Error("Configuration path is required");
}
const smartconfigData = await readSmartconfigFile();
const value = getCliConfigValueFromData(smartconfigData, configPath);
if (mode.json) {
printJson({ path: configPath, value, exists: value !== undefined });
return;
}
if (value === undefined) {
plugins.logger.log("warn", `No value set for ${configPath}`);
return;
}
if (typeof value === "string") {
console.log(value);
return;
}
printJson(value);
}
async function handleSet(
configPath: string | undefined,
rawValue: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!configPath) {
throw new Error("Configuration path is required");
}
if (rawValue === undefined) {
throw new Error("Configuration value is required");
}
const smartconfigData = await readSmartconfigFile();
const parsedValue = parseConfigValue(rawValue);
setCliConfigValueInData(smartconfigData, configPath, parsedValue);
await writeSmartconfigFile(smartconfigData);
if (mode.json) {
printJson({
ok: true,
action: "set",
path: configPath,
value: parsedValue,
});
return;
}
plugins.logger.log("success", `Set ${configPath}`);
}
async function handleUnset(
configPath: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!configPath) {
throw new Error("Configuration path is required");
}
const smartconfigData = await readSmartconfigFile();
const removed = unsetCliConfigValueInData(smartconfigData, configPath);
if (!removed) {
if (mode.json) {
printJson({
ok: false,
action: "unset",
path: configPath,
removed: false,
});
return;
}
plugins.logger.log("warn", `No value set for ${configPath}`);
return;
}
await writeSmartconfigFile(smartconfigData);
if (mode.json) {
printJson({ ok: true, action: "unset", path: configPath, removed: true });
return;
}
plugins.logger.log("success", `Unset ${configPath}`);
}
function parseConfigValue(rawValue: string): any {
const trimmedValue = rawValue.trim();
if (trimmedValue === "true") {
return true;
}
if (trimmedValue === "false") {
return false;
}
if (trimmedValue === "null") {
return null;
}
if (/^-?\d+(\.\d+)?$/.test(trimmedValue)) {
return Number(trimmedValue);
}
if (
(trimmedValue.startsWith("{") && trimmedValue.endsWith("}")) ||
(trimmedValue.startsWith("[") && trimmedValue.endsWith("]")) ||
(trimmedValue.startsWith('"') && trimmedValue.endsWith('"'))
) {
return JSON.parse(trimmedValue);
}
return rawValue;
}
/**
* Show help for config command
*/
function showHelp(): void {
console.log('');
console.log('Usage: gitzone config <command> [options]');
console.log('');
console.log('Commands:');
console.log(' show Display current release configuration');
console.log(' add [url] Add a registry URL');
console.log(' remove [url] Remove a registry URL');
console.log(' clear Clear all registries');
console.log(' access [public|private] Set npm access level for publishing');
console.log(' commit [setting] [value] Configure commit options');
console.log(' services Configure which services are enabled');
console.log('');
console.log('Examples:');
console.log(' gitzone config show');
console.log(' gitzone config add https://registry.npmjs.org');
console.log(' gitzone config add https://verdaccio.example.com');
console.log(' gitzone config remove https://registry.npmjs.org');
console.log(' gitzone config clear');
console.log(' gitzone config access public');
console.log(' gitzone config access private');
console.log(' gitzone config commit # Interactive');
console.log(' gitzone config commit alwaysTest true');
console.log(' gitzone config services # Interactive');
console.log('');
export function showHelp(mode?: ICliMode): void {
if (mode?.json) {
printJson({
command: "config",
usage: "gitzone config <command> [options]",
commands: [
{
name: "show",
description: "Display current @git.zone/cli configuration",
},
{ name: "get <path>", description: "Read a single config value" },
{ name: "set <path> <value>", description: "Write a config value" },
{ name: "unset <path>", description: "Delete a config value" },
{ name: "add [url]", description: "Add a release registry" },
{ name: "remove [url]", description: "Remove a release registry" },
{ name: "clear", description: "Clear all release registries" },
{
name: "access [public|private]",
description: "Set npm publish access level",
},
{
name: "commit <setting> <value>",
description: "Set commit defaults",
},
],
examples: [
"gitzone config show --json",
"gitzone config get release.accessLevel",
"gitzone config set cli.interactive false",
"gitzone config set cli.output json",
],
});
return;
}
console.log("");
console.log("Usage: gitzone config <command> [options]");
console.log("");
console.log("Commands:");
console.log(
" show Display current @git.zone/cli configuration",
);
console.log(" get <path> Read a single config value");
console.log(" set <path> <value> Write a config value");
console.log(" unset <path> Delete a config value");
console.log(" add [url] Add a registry URL");
console.log(" remove [url] Remove a registry URL");
console.log(" clear Clear all registries");
console.log(
" access [public|private] Set npm access level for publishing",
);
console.log(" commit [setting] [value] Configure commit options");
console.log(
" services Configure which services are enabled",
);
console.log("");
console.log("Examples:");
console.log(" gitzone config show");
console.log(" gitzone config show --json");
console.log(" gitzone config get release.accessLevel");
console.log(" gitzone config set cli.interactive false");
console.log(" gitzone config set cli.output json");
console.log(" gitzone config unset cli.output");
console.log(" gitzone config add https://registry.npmjs.org");
console.log(" gitzone config add https://verdaccio.example.com");
console.log(" gitzone config remove https://registry.npmjs.org");
console.log(" gitzone config clear");
console.log(" gitzone config access public");
console.log(" gitzone config access private");
console.log(" gitzone config commit # Interactive");
console.log(" gitzone config commit alwaysTest true");
console.log(" gitzone config services # Interactive");
console.log("");
}
+19 -26
View File
@@ -2,11 +2,12 @@ import * as plugins from './mod.plugins.js';
import { FormatContext } from './classes.formatcontext.js';
import type { IPlannedChange, ICheckResult } from './interfaces.format.js';
import { Project } from '../classes.project.js';
import { FormatStats } from './classes.formatstats.js';
export abstract class BaseFormatter {
protected context: FormatContext;
protected project: Project;
protected stats: any; // Will be FormatStats from context
protected stats: FormatStats;
constructor(context: FormatContext, project: Project) {
this.context = context;
@@ -36,9 +37,6 @@ export abstract class BaseFormatter {
}
await this.postExecute();
} catch (error) {
// Don't rollback here - let the FormatPlanner handle it
throw error;
} finally {
this.stats.endModule(this.name, startTime);
}
@@ -53,13 +51,10 @@ export abstract class BaseFormatter {
}
protected async modifyFile(filepath: string, content: string): Promise<void> {
// Validate filepath before writing
if (!filepath || filepath.trim() === '') {
throw new Error(`Invalid empty filepath in modifyFile`);
}
// Ensure we have a proper path with directory component
// If the path has no directory component (e.g., "package.json"), prepend "./"
let normalizedPath = filepath;
if (!plugins.path.parse(filepath).dir) {
normalizedPath = './' + filepath;
@@ -69,44 +64,46 @@ export abstract class BaseFormatter {
}
protected async createFile(filepath: string, content: string): Promise<void> {
await plugins.smartfs.file(filepath).encoding('utf8').write(content);
let normalizedPath = filepath;
if (!plugins.path.parse(filepath).dir) {
normalizedPath = './' + filepath;
}
// Ensure parent directory exists
const dir = plugins.path.dirname(normalizedPath);
if (dir && dir !== '.') {
await plugins.smartfs.directory(dir).recursive().create();
}
await plugins.smartfs.file(normalizedPath).encoding('utf8').write(content);
}
protected async deleteFile(filepath: string): Promise<void> {
await plugins.smartfs.file(filepath).delete();
}
protected async shouldProcessFile(filepath: string): Promise<boolean> {
return true;
}
/**
* Check for diffs without applying changes
* Returns information about what would change
*/
async check(): Promise<ICheckResult> {
const changes = await this.analyze();
const diffs: ICheckResult['diffs'] = [];
for (const change of changes) {
// Skip generic changes that don't have actual content
if (change.path === '<various files>') {
continue;
}
if (change.type === 'modify' || change.type === 'create') {
// Read current content if file exists
let currentContent: string | undefined;
try {
currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
} catch {
// File doesn't exist yet
currentContent = undefined;
}
const newContent = change.content;
// Check if there's an actual diff
if (currentContent !== newContent && newContent !== undefined) {
diffs.push({
path: change.path,
@@ -116,7 +113,6 @@ export abstract class BaseFormatter {
});
}
} else if (change.type === 'delete') {
// Check if file exists before marking for deletion
try {
const currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
diffs.push({
@@ -137,16 +133,16 @@ export abstract class BaseFormatter {
};
}
/**
* Display a single diff using smartdiff
*/
displayDiff(diff: ICheckResult['diffs'][0]): void {
console.log(`\n--- ${diff.path}`);
if (diff.before && diff.after) {
console.log(plugins.smartdiff.formatLineDiffForConsole(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)');
// Show first few lines of new content
const lines = diff.after.split('\n').slice(0, 10);
lines.forEach(line => console.log(` + ${line}`));
if (diff.after.split('\n').length > 10) {
@@ -157,9 +153,6 @@ export abstract class BaseFormatter {
}
}
/**
* Display all diffs from a check result
*/
displayAllDiffs(result: ICheckResult): void {
if (!result.hasDiff) {
console.log(' No changes detected');
-235
View File
@@ -1,235 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
export interface IFileCache {
path: string;
checksum: string;
modified: number;
size: number;
}
export interface ICacheManifest {
version: string;
lastFormat: number;
files: IFileCache[];
}
export class ChangeCache {
private cacheDir: string;
private manifestPath: string;
private cacheVersion = '1.0.0';
constructor() {
this.cacheDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-cache');
this.manifestPath = plugins.path.join(this.cacheDir, 'manifest.json');
}
async initialize(): Promise<void> {
await plugins.smartfs.directory(this.cacheDir).recursive().create();
}
async getManifest(): Promise<ICacheManifest> {
const defaultManifest: ICacheManifest = {
version: this.cacheVersion,
lastFormat: 0,
files: [],
};
const exists = await plugins.smartfs.file(this.manifestPath).exists();
if (!exists) {
return defaultManifest;
}
try {
const content = (await plugins.smartfs
.file(this.manifestPath)
.encoding('utf8')
.read()) as string;
const manifest = JSON.parse(content);
// Validate the manifest structure
if (this.isValidManifest(manifest)) {
return manifest;
} else {
console.warn('Invalid manifest structure, returning default manifest');
return defaultManifest;
}
} catch (error) {
console.warn(
`Failed to read cache manifest: ${error.message}, returning default manifest`,
);
// Try to delete the corrupted file
try {
await plugins.smartfs.file(this.manifestPath).delete();
} catch (removeError) {
// Ignore removal errors
}
return defaultManifest;
}
}
async saveManifest(manifest: ICacheManifest): Promise<void> {
// Validate before saving
if (!this.isValidManifest(manifest)) {
throw new Error('Invalid manifest structure, cannot save');
}
// Ensure directory exists
await plugins.smartfs.directory(this.cacheDir).recursive().create();
// Write directly with proper JSON stringification
const jsonContent = JSON.stringify(manifest, null, 2);
await plugins.smartfs
.file(this.manifestPath)
.encoding('utf8')
.write(jsonContent);
}
async hasFileChanged(filePath: string): Promise<boolean> {
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
: plugins.path.join(paths.cwd, filePath);
// Check if file exists
const exists = await plugins.smartfs.file(absolutePath).exists();
if (!exists) {
return true; // File doesn't exist, so it's "changed" (will be created)
}
// Get current file stats
const stats = await plugins.smartfs.file(absolutePath).stat();
// Skip directories
if (stats.isDirectory) {
return false; // Directories are not processed
}
const content = (await plugins.smartfs
.file(absolutePath)
.encoding('utf8')
.read()) as string;
const currentChecksum = this.calculateChecksum(content);
// Get cached info
const manifest = await this.getManifest();
const cachedFile = manifest.files.find((f) => f.path === filePath);
if (!cachedFile) {
return true; // Not in cache, so it's changed
}
// Compare checksums
return (
cachedFile.checksum !== currentChecksum ||
cachedFile.size !== stats.size ||
cachedFile.modified !== stats.mtime.getTime()
);
}
async updateFileCache(filePath: string): Promise<void> {
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
: plugins.path.join(paths.cwd, filePath);
// Get current file stats
const stats = await plugins.smartfs.file(absolutePath).stat();
// Skip directories
if (stats.isDirectory) {
return; // Don't cache directories
}
const content = (await plugins.smartfs
.file(absolutePath)
.encoding('utf8')
.read()) as string;
const checksum = this.calculateChecksum(content);
// Update manifest
const manifest = await this.getManifest();
const existingIndex = manifest.files.findIndex((f) => f.path === filePath);
const cacheEntry: IFileCache = {
path: filePath,
checksum,
modified: stats.mtime.getTime(),
size: stats.size,
};
if (existingIndex !== -1) {
manifest.files[existingIndex] = cacheEntry;
} else {
manifest.files.push(cacheEntry);
}
manifest.lastFormat = Date.now();
await this.saveManifest(manifest);
}
async getChangedFiles(filePaths: string[]): Promise<string[]> {
const changedFiles: string[] = [];
for (const filePath of filePaths) {
if (await this.hasFileChanged(filePath)) {
changedFiles.push(filePath);
}
}
return changedFiles;
}
async clean(): Promise<void> {
const manifest = await this.getManifest();
const validFiles: IFileCache[] = [];
// Remove entries for files that no longer exist
for (const file of manifest.files) {
const absolutePath = plugins.path.isAbsolute(file.path)
? file.path
: plugins.path.join(paths.cwd, file.path);
if (await plugins.smartfs.file(absolutePath).exists()) {
validFiles.push(file);
}
}
manifest.files = validFiles;
await this.saveManifest(manifest);
}
private calculateChecksum(content: string | Buffer): string {
return plugins.crypto.createHash('sha256').update(content).digest('hex');
}
private isValidManifest(manifest: any): manifest is ICacheManifest {
// Check if manifest has the required structure
if (!manifest || typeof manifest !== 'object') {
return false;
}
// Check required fields
if (
typeof manifest.version !== 'string' ||
typeof manifest.lastFormat !== 'number' ||
!Array.isArray(manifest.files)
) {
return false;
}
// Check each file entry
for (const file of manifest.files) {
if (
!file ||
typeof file !== 'object' ||
typeof file.path !== 'string' ||
typeof file.checksum !== 'string' ||
typeof file.modified !== 'number' ||
typeof file.size !== 'number'
) {
return false;
}
}
return true;
}
}
@@ -1,117 +0,0 @@
import * as plugins from './mod.plugins.js';
import { BaseFormatter } from './classes.baseformatter.js';
export interface IModuleDependency {
module: string;
dependencies: Set<string>;
dependents: Set<string>;
}
export class DependencyAnalyzer {
private moduleDependencies: Map<string, IModuleDependency> = new Map();
constructor() {
this.initializeDependencies();
}
private initializeDependencies(): void {
// Define dependencies between format modules
const dependencies = {
cleanup: [], // No dependencies
npmextra: [], // No dependencies
license: ['npmextra'], // Depends on npmextra for config
packagejson: ['npmextra'], // Depends on npmextra for config
templates: ['npmextra', 'packagejson'], // Depends on both
gitignore: ['templates'], // Depends on templates
tsconfig: ['packagejson'], // Depends on package.json
prettier: [
'cleanup',
'npmextra',
'packagejson',
'templates',
'gitignore',
'tsconfig',
], // Runs after most others
readme: ['npmextra', 'packagejson'], // Depends on project metadata
copy: ['npmextra'], // Depends on config
};
// Initialize all modules
for (const [module, deps] of Object.entries(dependencies)) {
this.moduleDependencies.set(module, {
module,
dependencies: new Set(deps),
dependents: new Set(),
});
}
// Build reverse dependencies (dependents)
for (const [module, deps] of Object.entries(dependencies)) {
for (const dep of deps) {
const depModule = this.moduleDependencies.get(dep);
if (depModule) {
depModule.dependents.add(module);
}
}
}
}
getExecutionGroups(modules: BaseFormatter[]): BaseFormatter[][] {
const modulesMap = new Map(modules.map((m) => [m.name, m]));
const executed = new Set<string>();
const groups: BaseFormatter[][] = [];
while (executed.size < modules.length) {
const currentGroup: BaseFormatter[] = [];
for (const module of modules) {
if (executed.has(module.name)) continue;
const dependency = this.moduleDependencies.get(module.name);
if (!dependency) {
// Unknown module, execute in isolation
currentGroup.push(module);
continue;
}
// Check if all dependencies have been executed
const allDepsExecuted = Array.from(dependency.dependencies).every(
(dep) => executed.has(dep) || !modulesMap.has(dep),
);
if (allDepsExecuted) {
currentGroup.push(module);
}
}
if (currentGroup.length === 0) {
// Circular dependency or error - execute remaining modules
for (const module of modules) {
if (!executed.has(module.name)) {
currentGroup.push(module);
}
}
}
currentGroup.forEach((m) => executed.add(m.name));
groups.push(currentGroup);
}
return groups;
}
canRunInParallel(module1: string, module2: string): boolean {
const dep1 = this.moduleDependencies.get(module1);
const dep2 = this.moduleDependencies.get(module2);
if (!dep1 || !dep2) return false;
// Check if module1 depends on module2 or vice versa
return (
!dep1.dependencies.has(module2) &&
!dep2.dependencies.has(module1) &&
!dep1.dependents.has(module2) &&
!dep2.dependents.has(module1)
);
}
}
+20 -3
View File
@@ -1,14 +1,31 @@
import * as plugins from './mod.plugins.js';
import { FormatStats } from './classes.formatstats.js';
import * as plugins from "./mod.plugins.js";
import { FormatStats } from "./classes.formatstats.js";
interface IFormatContextOptions {
interactive?: boolean;
jsonOutput?: boolean;
}
export class FormatContext {
private formatStats: FormatStats;
private interactive: boolean;
private jsonOutput: boolean;
constructor() {
constructor(options: IFormatContextOptions = {}) {
this.formatStats = new FormatStats();
this.interactive = options.interactive ?? true;
this.jsonOutput = options.jsonOutput ?? false;
}
getFormatStats(): FormatStats {
return this.formatStats;
}
isInteractive(): boolean {
return this.interactive;
}
isJsonOutput(): boolean {
return this.jsonOutput;
}
}
+3 -32
View File
@@ -2,13 +2,12 @@ import * as plugins from './mod.plugins.js';
import { FormatContext } from './classes.formatcontext.js';
import { BaseFormatter } from './classes.baseformatter.js';
import type { IFormatPlan, IPlannedChange } from './interfaces.format.js';
import { getModuleIcon } from './interfaces.format.js';
import { logger } from '../gitzone.logging.js';
import { DependencyAnalyzer } from './classes.dependency-analyzer.js';
import { DiffReporter } from './classes.diffreporter.js';
export class FormatPlanner {
private plannedChanges: Map<string, IPlannedChange[]> = new Map();
private dependencyAnalyzer = new DependencyAnalyzer();
private diffReporter = new DiffReporter();
async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> {
@@ -18,7 +17,6 @@ export class FormatPlanner {
filesAdded: 0,
filesModified: 0,
filesRemoved: 0,
estimatedTime: 0,
},
changes: [],
warnings: [],
@@ -32,7 +30,6 @@ export class FormatPlanner {
for (const change of changes) {
plan.changes.push(change);
// Update summary
switch (change.type) {
case 'create':
plan.summary.filesAdded++;
@@ -58,7 +55,6 @@ export class FormatPlanner {
plan.summary.filesAdded +
plan.summary.filesModified +
plan.summary.filesRemoved;
plan.summary.estimatedTime = plan.summary.totalFiles * 100; // 100ms per file estimate
return plan;
}
@@ -67,12 +63,9 @@ export class FormatPlanner {
plan: IFormatPlan,
modules: BaseFormatter[],
context: FormatContext,
parallel: boolean = false,
): Promise<void> {
const startTime = Date.now();
try {
// Always use sequential execution to avoid race conditions
for (const module of modules) {
const changes = this.plannedChanges.get(module.name) || [];
@@ -82,12 +75,8 @@ export class FormatPlanner {
}
}
const endTime = Date.now();
const duration = endTime - startTime;
const duration = Date.now() - startTime;
logger.log('info', `Format operations completed in ${duration}ms`);
} catch (error) {
throw error;
}
}
async displayPlan(
@@ -103,7 +92,6 @@ export class FormatPlanner {
console.log('');
console.log('Changes by module:');
// Group changes by module
const changesByModule = new Map<string, IPlannedChange[]>();
for (const change of plan.changes) {
const moduleChanges = changesByModule.get(change.module) || [];
@@ -113,14 +101,13 @@ export class FormatPlanner {
for (const [module, changes] of changesByModule) {
console.log(
`\n${this.getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`,
`\n${getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`,
);
for (const change of changes) {
const icon = this.getChangeIcon(change.type);
console.log(` ${icon} ${change.path} - ${change.description}`);
// Show diff for modified files if detailed view is requested
if (detailed && change.type === 'modify') {
const diff = await this.diffReporter.generateDiffForChange(change);
if (diff) {
@@ -141,22 +128,6 @@ export class FormatPlanner {
console.log('\n' + '━'.repeat(50));
}
private getModuleIcon(module: string): string {
const icons: Record<string, string> = {
packagejson: '📦',
license: '📝',
tsconfig: '🔧',
cleanup: '🚮',
gitignore: '🔒',
prettier: '✨',
readme: '📖',
templates: '📄',
npmextra: '⚙️',
copy: '📋',
};
return icons[module] || '📁';
}
private getChangeIcon(type: 'create' | 'modify' | 'delete'): string {
switch (type) {
case 'create':
+2 -42
View File
@@ -1,5 +1,6 @@
import * as plugins from './mod.plugins.js';
import { logger } from '../gitzone.logging.js';
import { getModuleIcon } from './interfaces.format.js';
export interface IModuleStats {
name: string;
@@ -23,8 +24,6 @@ export interface IFormatStats {
totalModified: number;
totalDeleted: number;
totalErrors: number;
cacheHits: number;
cacheMisses: number;
};
}
@@ -43,8 +42,6 @@ export class FormatStats {
totalModified: 0,
totalDeleted: 0,
totalErrors: 0,
cacheHits: 0,
cacheMisses: 0,
},
};
}
@@ -107,14 +104,6 @@ export class FormatStats {
}
}
recordCacheHit(): void {
this.stats.overallStats.cacheHits++;
}
recordCacheMiss(): void {
this.stats.overallStats.cacheMisses++;
}
finish(): void {
this.stats.endTime = Date.now();
this.stats.totalExecutionTime = this.stats.endTime - this.stats.startTime;
@@ -135,20 +124,6 @@ export class FormatStats {
console.log(` • Deleted: ${this.stats.overallStats.totalDeleted}`);
console.log(` Errors: ${this.stats.overallStats.totalErrors}`);
if (
this.stats.overallStats.cacheHits > 0 ||
this.stats.overallStats.cacheMisses > 0
) {
const cacheHitRate =
(this.stats.overallStats.cacheHits /
(this.stats.overallStats.cacheHits +
this.stats.overallStats.cacheMisses)) *
100;
console.log(` Cache Hit Rate: ${cacheHitRate.toFixed(1)}%`);
console.log(` • Hits: ${this.stats.overallStats.cacheHits}`);
console.log(` • Misses: ${this.stats.overallStats.cacheMisses}`);
}
// Module stats
console.log('\nModule Breakdown:');
console.log('─'.repeat(50));
@@ -159,7 +134,7 @@ export class FormatStats {
for (const moduleStats of sortedModules) {
console.log(
`\n${this.getModuleIcon(moduleStats.name)} ${moduleStats.name}:`,
`\n${getModuleIcon(moduleStats.name)} ${moduleStats.name}:`,
);
console.log(
` Execution Time: ${this.formatDuration(moduleStats.executionTime)}`,
@@ -211,19 +186,4 @@ export class FormatStats {
}
}
private getModuleIcon(module: string): string {
const icons: Record<string, string> = {
packagejson: '📦',
license: '📝',
tsconfig: '🔧',
cleanup: '🚮',
gitignore: '🔒',
prettier: '✨',
readme: '📖',
templates: '📄',
npmextra: '⚙️',
copy: '📋',
};
return icons[module] || '📁';
}
}
-340
View File
@@ -1,340 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import type { IFormatOperation } from './interfaces.format.js';
export class RollbackManager {
private backupDir: string;
private manifestPath: string;
constructor() {
this.backupDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-backups');
this.manifestPath = plugins.path.join(this.backupDir, 'manifest.json');
}
async createOperation(): Promise<IFormatOperation> {
await this.ensureBackupDir();
const operation: IFormatOperation = {
id: this.generateOperationId(),
timestamp: Date.now(),
files: [],
status: 'pending',
};
await this.updateManifest(operation);
return operation;
}
async backupFile(filepath: string, operationId: string): Promise<void> {
const operation = await this.getOperation(operationId);
if (!operation) {
throw new Error(`Operation ${operationId} not found`);
}
const absolutePath = plugins.path.isAbsolute(filepath)
? filepath
: plugins.path.join(paths.cwd, filepath);
// Check if file exists
const exists = await plugins.smartfs.file(absolutePath).exists();
if (!exists) {
// File doesn't exist yet (will be created), so we skip backup
return;
}
// Read file content and metadata
const content = (await plugins.smartfs
.file(absolutePath)
.encoding('utf8')
.read()) as string;
const stats = await plugins.smartfs.file(absolutePath).stat();
const checksum = this.calculateChecksum(content);
// Create backup
const backupPath = this.getBackupPath(operationId, filepath);
await plugins.smartfs
.directory(plugins.path.dirname(backupPath))
.recursive()
.create();
await plugins.smartfs.file(backupPath).encoding('utf8').write(content);
// Update operation
operation.files.push({
path: filepath,
originalContent: content,
checksum,
permissions: stats.mode.toString(8),
});
await this.updateManifest(operation);
}
async rollback(operationId: string): Promise<void> {
const operation = await this.getOperation(operationId);
if (!operation) {
// Operation doesn't exist, might have already been rolled back or never created
console.warn(`Operation ${operationId} not found for rollback, skipping`);
return;
}
if (operation.status === 'rolled-back') {
throw new Error(`Operation ${operationId} has already been rolled back`);
}
// Restore files in reverse order
for (let i = operation.files.length - 1; i >= 0; i--) {
const file = operation.files[i];
const absolutePath = plugins.path.isAbsolute(file.path)
? file.path
: plugins.path.join(paths.cwd, file.path);
// Verify backup integrity
const backupPath = this.getBackupPath(operationId, file.path);
const backupContent = await plugins.smartfs
.file(backupPath)
.encoding('utf8')
.read();
const backupChecksum = this.calculateChecksum(backupContent);
if (backupChecksum !== file.checksum) {
throw new Error(`Backup integrity check failed for ${file.path}`);
}
// Restore file
await plugins.smartfs
.file(absolutePath)
.encoding('utf8')
.write(file.originalContent);
// Restore permissions
const mode = parseInt(file.permissions, 8);
// Note: Permissions restoration may not work on all platforms
}
// Update operation status
operation.status = 'rolled-back';
await this.updateManifest(operation);
}
async markComplete(operationId: string): Promise<void> {
const operation = await this.getOperation(operationId);
if (!operation) {
throw new Error(`Operation ${operationId} not found`);
}
operation.status = 'completed';
await this.updateManifest(operation);
}
async cleanOldBackups(retentionDays: number): Promise<void> {
const manifest = await this.getManifest();
const cutoffTime = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
const operationsToDelete = manifest.operations.filter(
(op) => op.timestamp < cutoffTime && op.status === 'completed',
);
for (const operation of operationsToDelete) {
// Remove backup files
const operationDir = plugins.path.join(
this.backupDir,
'operations',
operation.id,
);
await plugins.smartfs.directory(operationDir).recursive().delete();
// Remove from manifest
manifest.operations = manifest.operations.filter(
(op) => op.id !== operation.id,
);
}
await this.saveManifest(manifest);
}
async verifyBackup(operationId: string): Promise<boolean> {
const operation = await this.getOperation(operationId);
if (!operation) {
return false;
}
for (const file of operation.files) {
const backupPath = this.getBackupPath(operationId, file.path);
const exists = await plugins.smartfs.file(backupPath).exists();
if (!exists) {
return false;
}
const content = await plugins.smartfs
.file(backupPath)
.encoding('utf8')
.read();
const checksum = this.calculateChecksum(content);
if (checksum !== file.checksum) {
return false;
}
}
return true;
}
async listBackups(): Promise<IFormatOperation[]> {
const manifest = await this.getManifest();
return manifest.operations;
}
private async ensureBackupDir(): Promise<void> {
await plugins.smartfs.directory(this.backupDir).recursive().create();
await plugins.smartfs
.directory(plugins.path.join(this.backupDir, 'operations'))
.recursive()
.create();
}
private generateOperationId(): string {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const random = Math.random().toString(36).substring(2, 8);
return `${timestamp}-${random}`;
}
private getBackupPath(operationId: string, filepath: string): string {
const filename = plugins.path.basename(filepath);
const dir = plugins.path.dirname(filepath);
const safeDir = dir.replace(/[/\\]/g, '__');
return plugins.path.join(
this.backupDir,
'operations',
operationId,
'files',
safeDir,
`${filename}.backup`,
);
}
private calculateChecksum(content: string | Buffer): string {
return plugins.crypto.createHash('sha256').update(content).digest('hex');
}
private async getManifest(): Promise<{ operations: IFormatOperation[] }> {
const defaultManifest = { operations: [] };
const exists = await plugins.smartfs.file(this.manifestPath).exists();
if (!exists) {
return defaultManifest;
}
try {
const content = (await plugins.smartfs
.file(this.manifestPath)
.encoding('utf8')
.read()) as string;
const manifest = JSON.parse(content);
// Validate the manifest structure
if (this.isValidManifest(manifest)) {
return manifest;
} else {
console.warn(
'Invalid rollback manifest structure, returning default manifest',
);
return defaultManifest;
}
} catch (error) {
console.warn(
`Failed to read rollback manifest: ${error.message}, returning default manifest`,
);
// Try to delete the corrupted file
try {
await plugins.smartfs.file(this.manifestPath).delete();
} catch (removeError) {
// Ignore removal errors
}
return defaultManifest;
}
}
private async saveManifest(manifest: {
operations: IFormatOperation[];
}): Promise<void> {
// Validate before saving
if (!this.isValidManifest(manifest)) {
throw new Error('Invalid rollback manifest structure, cannot save');
}
// Ensure directory exists
await this.ensureBackupDir();
// Write directly with proper JSON stringification
const jsonContent = JSON.stringify(manifest, null, 2);
await plugins.smartfs
.file(this.manifestPath)
.encoding('utf8')
.write(jsonContent);
}
private async getOperation(
operationId: string,
): Promise<IFormatOperation | null> {
const manifest = await this.getManifest();
return manifest.operations.find((op) => op.id === operationId) || null;
}
private async updateManifest(operation: IFormatOperation): Promise<void> {
const manifest = await this.getManifest();
const existingIndex = manifest.operations.findIndex(
(op) => op.id === operation.id,
);
if (existingIndex !== -1) {
manifest.operations[existingIndex] = operation;
} else {
manifest.operations.push(operation);
}
await this.saveManifest(manifest);
}
private isValidManifest(
manifest: any,
): manifest is { operations: IFormatOperation[] } {
// Check if manifest has the required structure
if (!manifest || typeof manifest !== 'object') {
return false;
}
// Check required fields
if (!Array.isArray(manifest.operations)) {
return false;
}
// Check each operation entry
for (const operation of manifest.operations) {
if (
!operation ||
typeof operation !== 'object' ||
typeof operation.id !== 'string' ||
typeof operation.timestamp !== 'number' ||
typeof operation.status !== 'string' ||
!Array.isArray(operation.files)
) {
return false;
}
// Check each file in the operation
for (const file of operation.files) {
if (
!file ||
typeof file !== 'object' ||
typeof file.path !== 'string' ||
typeof file.checksum !== 'string'
) {
return false;
}
}
}
return true;
}
}
-26
View File
@@ -1,26 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { logger } from '../gitzone.logging.js';
import { Project } from '../classes.project.js';
const filesToDelete = [
'defaults.yml',
'yarn.lock',
'package-lock.json',
'tslint.json',
];
export const run = async (projectArg: Project) => {
for (const relativeFilePath of filesToDelete) {
const fileExists = await plugins.smartfs.file(relativeFilePath).exists();
if (fileExists) {
logger.log('info', `Found ${relativeFilePath}! Removing it!`);
await plugins.smartfs
.file(plugins.path.join(paths.cwd, relativeFilePath))
.delete();
} else {
logger.log('info', `Project is free of ${relativeFilePath}`);
}
}
};
-93
View File
@@ -1,93 +0,0 @@
import type { Project } from '../classes.project.js';
import * as plugins from './mod.plugins.js';
import { logger } from '../gitzone.logging.js';
export const run = async (projectArg: Project) => {
const gitzoneConfig = await projectArg.gitzoneConfig;
// Get copy configuration from npmextra.json
const npmextraConfig = new plugins.npmextra.Npmextra();
const copyConfig = npmextraConfig.dataFor<any>('gitzone.format.copy', {
patterns: [],
});
if (!copyConfig.patterns || copyConfig.patterns.length === 0) {
logger.log('info', 'No copy patterns configured in npmextra.json');
return;
}
for (const pattern of copyConfig.patterns) {
if (!pattern.from || !pattern.to) {
logger.log('warn', 'Invalid copy pattern - missing "from" or "to" field');
continue;
}
try {
// Handle glob patterns
const entries = await plugins.smartfs
.directory('.')
.recursive()
.filter(pattern.from)
.list();
const files = entries.map((entry) => entry.path);
for (const file of files) {
const sourcePath = file;
let destPath = pattern.to;
// If destination is a directory, preserve filename
if (pattern.to.endsWith('/')) {
const filename = plugins.path.basename(file);
destPath = plugins.path.join(pattern.to, filename);
}
// Handle template variables in destination path
if (pattern.preservePath) {
const relativePath = plugins.path.relative(
plugins.path.dirname(pattern.from.replace(/\*/g, '')),
file,
);
destPath = plugins.path.join(pattern.to, relativePath);
}
// Ensure destination directory exists
await plugins.smartfs
.directory(plugins.path.dirname(destPath))
.recursive()
.create();
// Copy file
await plugins.smartfs.file(sourcePath).copy(destPath);
logger.log('info', `Copied ${sourcePath} to ${destPath}`);
}
} catch (error) {
logger.log(
'error',
`Failed to copy pattern ${pattern.from}: ${error.message}`,
);
}
}
};
/**
* Example npmextra.json configuration:
* {
* "gitzone": {
* "format": {
* "copy": {
* "patterns": [
* {
* "from": "src/assets/*",
* "to": "dist/assets/",
* "preservePath": true
* },
* {
* "from": "config/*.json",
* "to": "dist/"
* }
* ]
* }
* }
* }
* }
*/
-54
View File
@@ -1,54 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { Project } from '../classes.project.js';
import { logger } from '../gitzone.logging.js';
const gitignorePath = plugins.path.join(paths.cwd, './.gitignore');
export const run = async (projectArg: Project) => {
const gitignoreExists = await plugins.smartfs.file(gitignorePath).exists();
let customContent = '';
if (gitignoreExists) {
// lets get the existing gitignore file
const existingGitIgnoreString = (await plugins.smartfs
.file(gitignorePath)
.encoding('utf8')
.read()) as string;
// Check for different custom section markers
const customMarkers = ['#------# custom', '# custom'];
for (const marker of customMarkers) {
const splitResult = existingGitIgnoreString.split(marker);
if (splitResult.length > 1) {
// Get everything after the marker (excluding the marker itself)
customContent = splitResult[1].trim();
break;
}
}
}
// Write the template
const templateModule = await import('../mod_template/index.js');
const ciTemplate = await templateModule.getTemplate('gitignore');
await ciTemplate.writeToDisk(paths.cwd);
// Append the custom content if it exists
if (customContent) {
const newGitignoreContent = (await plugins.smartfs
.file(gitignorePath)
.encoding('utf8')
.read()) as string;
// The template already ends with "#------# custom", so just append the content
const finalContent =
newGitignoreContent.trimEnd() + '\n' + customContent + '\n';
await plugins.smartfs
.file(gitignorePath)
.encoding('utf8')
.write(finalContent);
logger.log('info', 'Updated .gitignore while preserving custom section!');
} else {
logger.log('info', 'Added a .gitignore!');
}
};
-32
View File
@@ -1,32 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { Project } from '../classes.project.js';
import { logger } from '../gitzone.logging.js';
const incompatibleLicenses: string[] = ['AGPL', 'GPL', 'SSPL'];
export const run = async (projectArg: Project) => {
const nodeModulesInstalled = await plugins.smartfs
.directory(plugins.path.join(paths.cwd, 'node_modules'))
.exists();
if (!nodeModulesInstalled) {
logger.log('warn', 'No node_modules found. Skipping license check');
return;
}
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
const licenseCheckResult = await licenseChecker.excludeLicenseWithinPath(
paths.cwd,
incompatibleLicenses,
);
if (licenseCheckResult.failingModules.length === 0) {
logger.log('info', 'Success -> licenses passed!');
} else {
logger.log('error', 'Error -> licenses failed. Here is why:');
for (const failedModule of licenseCheckResult.failingModules) {
console.log(
`${failedModule.name} fails with license ${failedModule.license}`,
);
}
}
};
-142
View File
@@ -1,142 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import * as gulpFunction from '@push.rocks/gulp-function';
import { Project } from '../classes.project.js';
/**
* Migrates npmextra.json from old namespace keys to new package-scoped keys
*/
const migrateNamespaceKeys = (npmextraJson: any): boolean => {
let migrated = false;
const migrations = [
{ oldKey: 'gitzone', newKey: '@git.zone/cli' },
{ oldKey: 'tsdoc', newKey: '@git.zone/tsdoc' },
{ oldKey: 'npmdocker', newKey: '@git.zone/tsdocker' },
{ oldKey: 'npmci', newKey: '@ship.zone/szci' },
{ oldKey: 'szci', newKey: '@ship.zone/szci' },
];
for (const { oldKey, newKey } of migrations) {
if (npmextraJson[oldKey] && !npmextraJson[newKey]) {
npmextraJson[newKey] = npmextraJson[oldKey];
delete npmextraJson[oldKey];
migrated = true;
console.log(`Migrated npmextra.json: ${oldKey} -> ${newKey}`);
}
}
return migrated;
};
/**
* Migrates npmAccessLevel from @ship.zone/szci to @git.zone/cli.release.accessLevel
* This is a one-time migration for projects using the old location
*/
const migrateAccessLevel = (npmextraJson: any): boolean => {
const szciConfig = npmextraJson['@ship.zone/szci'];
// Check if szci has npmAccessLevel that needs to be migrated
if (!szciConfig?.npmAccessLevel) {
return false;
}
// Check if we already have the new location
const gitzoneConfig = npmextraJson['@git.zone/cli'] || {};
if (gitzoneConfig?.release?.accessLevel) {
// Already migrated, just remove from szci
delete szciConfig.npmAccessLevel;
return true;
}
// Ensure @git.zone/cli and release exist
if (!npmextraJson['@git.zone/cli']) {
npmextraJson['@git.zone/cli'] = {};
}
if (!npmextraJson['@git.zone/cli'].release) {
npmextraJson['@git.zone/cli'].release = {};
}
// Migrate the value
npmextraJson['@git.zone/cli'].release.accessLevel = szciConfig.npmAccessLevel;
delete szciConfig.npmAccessLevel;
console.log(`Migrated npmAccessLevel to @git.zone/cli.release.accessLevel`);
return true;
};
/**
* runs the npmextra file checking
*/
export const run = async (projectArg: Project) => {
const formatSmartstream = new plugins.smartstream.StreamWrapper([
plugins.smartgulp.src([`npmextra.json`]),
gulpFunction.forEach(async (fileArg: plugins.smartfile.SmartFile) => {
const fileString = fileArg.contents.toString();
const npmextraJson = JSON.parse(fileString);
// Migrate old namespace keys to new package-scoped keys
migrateNamespaceKeys(npmextraJson);
// Migrate npmAccessLevel from szci to @git.zone/cli.release.accessLevel
migrateAccessLevel(npmextraJson);
if (!npmextraJson['@git.zone/cli']) {
npmextraJson['@git.zone/cli'] = {};
}
const expectedRepoInformation: string[] = [
'projectType',
'module.githost',
'module.gitscope',
'module.gitrepo',
'module.description',
'module.npmPackagename',
'module.license',
];
const interactInstance = new plugins.smartinteract.SmartInteract();
for (const expectedRepoInformationItem of expectedRepoInformation) {
if (
!plugins.smartobject.smartGet(
npmextraJson['@git.zone/cli'],
expectedRepoInformationItem,
)
) {
interactInstance.addQuestions([
{
message: `What is the value of ${expectedRepoInformationItem}`,
name: expectedRepoInformationItem,
type: 'input',
default: 'undefined variable',
},
]);
}
}
const answerbucket = await interactInstance.runQueue();
for (const expectedRepoInformationItem of expectedRepoInformation) {
const cliProvidedValue = answerbucket.getAnswerFor(
expectedRepoInformationItem,
);
if (cliProvidedValue) {
plugins.smartobject.smartAdd(
npmextraJson['@git.zone/cli'],
expectedRepoInformationItem,
cliProvidedValue,
);
}
}
// delete obsolete
// tbd
if (!npmextraJson['@ship.zone/szci']) {
npmextraJson['@ship.zone/szci'] = {};
}
fileArg.setContentsFromString(JSON.stringify(npmextraJson, null, 2));
}),
plugins.smartgulp.replace(),
]);
await formatSmartstream.run().catch((error) => {
console.log(error);
});
};
-196
View File
@@ -1,196 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import * as gulpFunction from '@push.rocks/gulp-function';
import { Project } from '../classes.project.js';
import { logger } from '../gitzone.logging.js';
/**
* ensures a certain dependency
*/
const ensureDependency = async (
packageJsonObjectArg: any,
position: 'dep' | 'devDep' | 'everywhere',
constraint: 'exclude' | 'include' | 'latest',
dependencyArg: string,
) => {
const [packageName, version] = dependencyArg.includes('@')
? dependencyArg.split('@').filter(Boolean)
: [dependencyArg, 'latest'];
const targetSections: string[] = [];
switch (position) {
case 'dep':
targetSections.push('dependencies');
break;
case 'devDep':
targetSections.push('devDependencies');
break;
case 'everywhere':
targetSections.push('dependencies', 'devDependencies');
break;
}
for (const section of targetSections) {
if (!packageJsonObjectArg[section]) {
packageJsonObjectArg[section] = {};
}
switch (constraint) {
case 'exclude':
delete packageJsonObjectArg[section][packageName];
break;
case 'include':
if (!packageJsonObjectArg[section][packageName]) {
packageJsonObjectArg[section][packageName] =
version === 'latest' ? '^1.0.0' : version;
}
break;
case 'latest':
// Fetch latest version from npm
try {
const registry = new plugins.smartnpm.NpmRegistry();
const packageInfo = await registry.getPackageInfo(packageName);
const latestVersion = packageInfo['dist-tags'].latest;
packageJsonObjectArg[section][packageName] = `^${latestVersion}`;
} catch (error) {
logger.log(
'warn',
`Could not fetch latest version for ${packageName}, using existing or default`,
);
if (!packageJsonObjectArg[section][packageName]) {
packageJsonObjectArg[section][packageName] =
version === 'latest' ? '^1.0.0' : version;
}
}
break;
}
}
};
export const run = async (projectArg: Project) => {
const formatStreamWrapper = new plugins.smartstream.StreamWrapper([
plugins.smartgulp.src([`package.json`]),
gulpFunction.forEach(async (fileArg: plugins.smartfile.SmartFile) => {
const npmextraConfig = new plugins.npmextra.Npmextra(paths.cwd);
const gitzoneData: any = npmextraConfig.dataFor('@git.zone/cli', {});
const fileString = fileArg.contents.toString();
const packageJson = JSON.parse(fileString);
// metadata
packageJson.repository = {
type: 'git',
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}.git`,
};
((packageJson.bugs = {
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}/issues`,
}),
(packageJson.homepage = `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}#readme`));
// Check for module type
if (!packageJson.type) {
logger.log('info', `setting packageJson.type to "module"`);
packageJson.type = 'module';
}
// Check for private or public
if (packageJson.private !== undefined) {
logger.log(
'info',
'Success -> found private/public info in package.json!',
);
} else {
logger.log(
'error',
'found no private boolean! Setting it to private for now!',
);
packageJson.private = true;
}
// Check for license
if (packageJson.license) {
logger.log('info', 'Success -> found license in package.json!');
} else {
logger.log(
'error',
'found no license! Setting it to UNLICENSED for now!',
);
packageJson.license = 'UNLICENSED';
}
// Check for build script
if (packageJson.scripts.build) {
logger.log('info', 'Success -> found build script in package.json!');
} else {
logger.log(
'error',
'found no build script! Putting a placeholder there for now!',
);
packageJson.scripts.build = `echo "Not needed for now"`;
}
// Check for buildDocs script
if (!packageJson.scripts.buildDocs) {
logger.log(
'info',
'found no buildDocs script! Putting tsdoc script there now.',
);
packageJson.scripts.buildDocs = `tsdoc`;
}
// check for files
packageJson.files = [
'ts/**/*',
'ts_web/**/*',
'dist/**/*',
'dist_*/**/*',
'dist_ts/**/*',
'dist_ts_web/**/*',
'assets/**/*',
'cli.js',
'npmextra.json',
'readme.md',
];
// check for dependencies
// Note: @push.rocks/tapbundle is deprecated - use @git.zone/tstest/tapbundle instead
await ensureDependency(
packageJson,
'devDep',
'exclude',
'@push.rocks/tapbundle',
);
await ensureDependency(
packageJson,
'devDep',
'latest',
'@git.zone/tstest',
);
await ensureDependency(
packageJson,
'devDep',
'latest',
'@git.zone/tsbuild',
);
// set overrides
const overridesContent = (await plugins.smartfs
.file(plugins.path.join(paths.assetsDir, 'overrides.json'))
.encoding('utf8')
.read()) as string;
const overrides = JSON.parse(overridesContent);
packageJson.pnpm = packageJson.pnpm || {};
packageJson.pnpm.overrides = overrides;
// exclude
// TODO
fileArg.setContentsFromString(JSON.stringify(packageJson, null, 2));
}),
plugins.smartgulp.replace(),
]);
await formatStreamWrapper.run().catch((error) => {
console.log(error);
});
};
-66
View File
@@ -1,66 +0,0 @@
import * as plugins from './mod.plugins.js';
import prettier from 'prettier';
import { Project } from '../classes.project.js';
import { logger } from '../gitzone.logging.js';
const prettierDefaultTypeScriptConfig: prettier.Options = {
printWidth: 100,
parser: 'typescript',
singleQuote: true,
};
const prettierDefaultMarkdownConfig: prettier.Options = {
singleQuote: true,
printWidth: 100,
parser: 'markdown',
};
const filesToFormat = [
`ts/**/*.ts`,
`test/**/*.ts`,
`readme.md`,
`docs/**/*.md`,
];
const choosePrettierConfig = (fileArg: plugins.smartfile.SmartFile) => {
switch (fileArg.parsedPath.ext) {
case '.ts':
return prettierDefaultTypeScriptConfig;
case '.md':
return prettierDefaultMarkdownConfig;
default:
return {};
}
};
const prettierTypeScriptPipestop = plugins.through2.obj(
async (fileArg: plugins.smartfile.SmartFile, enc, cb) => {
const fileString = fileArg.contentBuffer.toString();
const chosenConfig = choosePrettierConfig(fileArg);
const filePasses = await prettier.check(fileString, chosenConfig);
if (filePasses) {
logger.log('info', `OK! -> ${fileArg.path} passes!`);
cb(null);
} else {
logger.log('info', `${fileArg.path} is being reformated!`);
const formatedFileString = await prettier.format(
fileString,
chosenConfig,
);
fileArg.setContentsFromString(formatedFileString);
cb(null, fileArg);
}
},
);
export const run = async (projectArg: Project) => {
const formatStreamWrapper = new plugins.smartstream.StreamWrapper([
plugins.smartgulp.src(filesToFormat),
prettierTypeScriptPipestop,
plugins.smartgulp.replace(),
]);
await formatStreamWrapper.run().catch((error) => {
console.log(error);
});
};
-29
View File
@@ -1,29 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
export const run = async () => {
const readmePath = plugins.path.join(paths.cwd, 'readme.md');
const readmeHintsPath = plugins.path.join(paths.cwd, 'readme.hints.md');
// Check and initialize readme.md if it doesn't exist
const readmeExists = await plugins.smartfs.file(readmePath).exists();
if (!readmeExists) {
await plugins.smartfs.file(readmePath)
.encoding('utf8')
.write('# Project Readme\n\nThis is the initial readme file.');
console.log('Initialized readme.md');
} else {
console.log('readme.md already exists');
}
// Check and initialize readme.hints.md if it doesn't exist
const readmeHintsExists = await plugins.smartfs.file(readmeHintsPath).exists();
if (!readmeHintsExists) {
await plugins.smartfs.file(readmeHintsPath)
.encoding('utf8')
.write('# Project Readme Hints\n\nThis is the initial readme hints file.');
console.log('Initialized readme.hints.md');
} else {
console.log('readme.hints.md already exists');
}
};
-79
View File
@@ -1,79 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { logger } from '../gitzone.logging.js';
import { Project } from '../classes.project.js';
/**
* takes care of updating files from templates
*/
export const run = async (project: Project) => {
const templateModule = await import('../mod_template/index.js');
// update vscode
const vscodeTemplate = await templateModule.getTemplate('vscode');
await vscodeTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated vscode template!`);
// update gitlab ci and Dockerfile
switch (project.gitzoneConfig.data.projectType) {
case 'npm':
case 'wcc':
if (project.gitzoneConfig.data.npmciOptions.npmAccessLevel === 'public') {
const ciTemplateDefault =
await templateModule.getTemplate('ci_default');
ciTemplateDefault.writeToDisk(paths.cwd);
} else {
const ciTemplateDefault =
await templateModule.getTemplate('ci_default_private');
ciTemplateDefault.writeToDisk(paths.cwd);
}
logger.log('info', 'Updated .gitlabci.yml!');
break;
case 'service':
case 'website':
const ciTemplateDocker = await templateModule.getTemplate('ci_docker');
await ciTemplateDocker.writeToDisk(paths.cwd);
logger.log('info', 'Updated CI/CD config files!');
// lets care about docker
const dockerTemplate =
await templateModule.getTemplate('dockerfile_service');
dockerTemplate.writeToDisk(paths.cwd);
logger.log('info', 'Updated Dockerfile!');
// lets care about cli
const cliTemplate = await templateModule.getTemplate('cli');
await cliTemplate.writeToDisk(paths.cwd);
logger.log('info', 'Updated cli.ts.js and cli.js!');
break;
default:
break;
}
// update html
if (project.gitzoneConfig.data.projectType === 'website') {
const websiteUpdateTemplate =
await templateModule.getTemplate('website_update');
const variables = {
assetbrokerUrl: project.gitzoneConfig.data.module.assetbrokerUrl,
legalUrl: project.gitzoneConfig.data.module.legalUrl,
};
console.log(
'updating website template with variables\n',
JSON.stringify(variables, null, 2),
);
websiteUpdateTemplate.supplyVariables(variables);
await websiteUpdateTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated html for website!`);
} else if (project.gitzoneConfig.data.projectType === 'service') {
const websiteUpdateTemplate =
await templateModule.getTemplate('service_update');
await websiteUpdateTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated html for element template!`);
} else if (project.gitzoneConfig.data.projectType === 'wcc') {
const wccUpdateTemplate = await templateModule.getTemplate('wcc_update');
await wccUpdateTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated html for wcc template!`);
}
};
-31
View File
@@ -1,31 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { logger } from '../gitzone.logging.js';
import { Project } from '../classes.project.js';
export const run = async (projectArg: Project) => {
// lets care about tsconfig.json
logger.log('info', 'Formatting tsconfig.json...');
const factory = plugins.smartfile.SmartFileFactory.nodeFs();
const tsconfigSmartfile = await factory.fromFilePath(
plugins.path.join(paths.cwd, 'tsconfig.json'),
);
const tsconfigObject = JSON.parse(tsconfigSmartfile.parseContentAsString());
tsconfigObject.compilerOptions = tsconfigObject.compilerOptions || {};
tsconfigObject.compilerOptions.baseUrl = '.';
tsconfigObject.compilerOptions.paths = {};
const tsPublishMod = await import('@git.zone/tspublish');
const tsPublishInstance = new tsPublishMod.TsPublish();
const publishModules = await tsPublishInstance.getModuleSubDirs(paths.cwd);
for (const publishModule of Object.keys(publishModules)) {
const publishConfig = publishModules[publishModule];
tsconfigObject.compilerOptions.paths[`${publishConfig.name}`] = [
`./${publishModule}/index.js`,
];
}
await tsconfigSmartfile.editContentAsString(async () => {
return JSON.stringify(tsconfigObject, null, 2);
});
await tsconfigSmartfile.write();
};
@@ -1,7 +1,6 @@
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as cleanupFormatter from '../format.cleanup.js';
export class CleanupFormatter extends BaseFormatter {
get name(): string {
+110 -5
View File
@@ -1,8 +1,113 @@
import { LegacyFormatter } from './legacy.formatter.js';
import * as formatCopy from '../format.copy.js';
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';
export class CopyFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'copy', formatCopy);
interface ICopyPattern {
from: string;
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}`);
}
}
+104 -5
View File
@@ -1,8 +1,107 @@
import { LegacyFormatter } from './legacy.formatter.js';
import * as formatGitignore from '../format.gitignore.js';
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger } from '../../gitzone.logging.js';
export class GitignoreFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'gitignore', formatGitignore);
export class GitignoreFormatter extends BaseFormatter {
get name(): string {
return 'gitignore';
}
/**
* Read the standard gitignore template from the asset file,
* stripping the YAML frontmatter.
*/
private async getStandardTemplate(): Promise<string> {
const templatePath = plugins.path.join(paths.templatesDir, 'gitignore', '_gitignore');
const raw = (await plugins.smartfs
.file(templatePath)
.encoding('utf8')
.read()) as string;
// Strip YAML frontmatter (---\n...\n---)
const frontmatterEnd = raw.indexOf('---', 3);
if (frontmatterEnd !== -1) {
return raw.slice(frontmatterEnd + 3).trimStart();
}
return raw;
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
const gitignorePath = '.gitignore';
const standardTemplate = await this.getStandardTemplate();
// Check if file exists and extract custom content
let customContent = '';
const exists = await plugins.smartfs.file(gitignorePath).exists();
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,43 +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);
}
}
+59 -5
View File
@@ -1,8 +1,62 @@
import { LegacyFormatter } from './legacy.formatter.js';
import * as formatLicense from '../format.license.js';
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger } from '../../gitzone.logging.js';
export class LicenseFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'license', formatLicense);
const INCOMPATIBLE_LICENSES: string[] = ['AGPL', 'GPL', 'SSPL'];
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 * as formatPackageJson from '../format.packagejson.js';
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger, logVerbose } from '../../gitzone.logging.js';
export class PackageJsonFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'packagejson', formatPackageJson);
export class PackageJsonFormatter extends BaseFormatter {
get name(): string {
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');
}
}
+69 -17
View File
@@ -1,5 +1,5 @@
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 { logger, logVerbose } from '../../gitzone.logging.js';
@@ -21,7 +21,7 @@ export class PrettierFormatter extends BaseFormatter {
const rootConfigFiles = [
'package.json',
'tsconfig.json',
'npmextra.json',
'.smartconfig.json',
'.prettierrc',
'.prettierrc.json',
'.prettierrc.js',
@@ -40,6 +40,7 @@ export class PrettierFormatter extends BaseFormatter {
// Add files from TypeScript directories
for (const dir of includeDirs) {
try {
const globPattern = `${dir}/**/*.${extensions}`;
const dirEntries = await plugins.smartfs
.directory('.')
@@ -47,31 +48,40 @@ export class PrettierFormatter extends BaseFormatter {
.filter(globPattern)
.list();
const dirFiles = dirEntries.map((entry) => entry.path);
allFiles.push(...dirFiles);
// 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
// Add root config files (only check root level, no recursive needed)
for (const pattern of rootConfigFiles) {
try {
const rootEntries = await plugins.smartfs
.directory('.')
.recursive()
.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)];
// Get all files that match the pattern
const files = uniqueFiles;
// Ensure we only process actual files (not directories)
const validFiles: string[] = [];
for (const file of files) {
for (const file of uniqueFiles) {
try {
const stats = await plugins.smartfs.file(file).stat();
if (!stats.isDirectory) {
@@ -83,14 +93,7 @@ export class PrettierFormatter extends BaseFormatter {
}
}
// Check which files need formatting
for (const file of validFiles) {
// Skip files that haven't changed
if (!(await this.shouldProcessFile(file))) {
logVerbose(`Skipping ${file} - no changes detected`);
continue;
}
changes.push({
type: 'modify',
path: file,
@@ -219,7 +222,7 @@ export class PrettierFormatter extends BaseFormatter {
private async getPrettierConfig(): Promise<any> {
// Try to load prettier config from the project
const prettierConfig = new plugins.npmextra.Npmextra();
const prettierConfig = new plugins.smartconfig.Smartconfig();
return prettierConfig.dataFor('prettier', {
// Default prettier config
singleQuote: true,
@@ -230,4 +233,53 @@ export class PrettierFormatter extends BaseFormatter {
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,
};
}
}
+39 -8
View File
@@ -1,6 +1,15 @@
import { BaseFormatter } from '../classes.baseformatter.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 {
get name(): string {
@@ -8,17 +17,39 @@ export class ReadmeFormatter extends BaseFormatter {
}
async analyze(): Promise<IPlannedChange[]> {
return [
{
type: 'modify',
const changes: IPlannedChange[] = [];
// Check readme.md
const readmeExists = await plugins.smartfs.file('readme.md').exists();
if (!readmeExists) {
changes.push({
type: 'create',
path: 'readme.md',
module: this.name,
description: 'Ensure readme files exist',
},
];
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> {
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,186 @@
import { BaseFormatter } from "../classes.baseformatter.js";
import type { IPlannedChange } from "../interfaces.format.js";
import * as plugins from "../mod.plugins.js";
import { logger, logVerbose } from "../../gitzone.logging.js";
/**
* Migrates .smartconfig.json from old namespace keys to new package-scoped keys
*/
const migrateNamespaceKeys = (smartconfigJson: any): boolean => {
let migrated = false;
const migrations = [
{ oldKey: "gitzone", newKey: "@git.zone/cli" },
{ oldKey: "tsdoc", newKey: "@git.zone/tsdoc" },
{ oldKey: "npmdocker", newKey: "@git.zone/tsdocker" },
{ oldKey: "npmci", newKey: "@ship.zone/szci" },
{ oldKey: "szci", newKey: "@ship.zone/szci" },
];
for (const { oldKey, newKey } of migrations) {
if (smartconfigJson[oldKey]) {
if (!smartconfigJson[newKey]) {
smartconfigJson[newKey] = smartconfigJson[oldKey];
} else {
smartconfigJson[newKey] = {
...smartconfigJson[oldKey],
...smartconfigJson[newKey],
};
}
delete smartconfigJson[oldKey];
migrated = true;
}
}
return migrated;
};
/**
* Migrates npmAccessLevel from @ship.zone/szci to @git.zone/cli.release.accessLevel
*/
const migrateAccessLevel = (smartconfigJson: any): boolean => {
const szciConfig = smartconfigJson["@ship.zone/szci"];
if (!szciConfig?.npmAccessLevel) {
return false;
}
const gitzoneConfig = smartconfigJson["@git.zone/cli"] || {};
if (gitzoneConfig?.release?.accessLevel) {
delete szciConfig.npmAccessLevel;
return true;
}
if (!smartconfigJson["@git.zone/cli"]) {
smartconfigJson["@git.zone/cli"] = {};
}
if (!smartconfigJson["@git.zone/cli"].release) {
smartconfigJson["@git.zone/cli"].release = {};
}
smartconfigJson["@git.zone/cli"].release.accessLevel =
szciConfig.npmAccessLevel;
delete szciConfig.npmAccessLevel;
return true;
};
const CONFIG_FILE = ".smartconfig.json";
export class SmartconfigFormatter extends BaseFormatter {
get name(): string {
return "smartconfig";
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
// File rename (npmextra.json/smartconfig.json → .smartconfig.json)
// is handled by the orchestrator before analysis.
// This formatter only operates on .smartconfig.json.
const exists = await plugins.smartfs.file(CONFIG_FILE).exists();
if (!exists) {
logVerbose(".smartconfig.json does not exist, skipping");
return changes;
}
const currentContent = (await plugins.smartfs
.file(CONFIG_FILE)
.encoding("utf8")
.read()) as string;
const smartconfigJson = JSON.parse(currentContent);
// Apply key migrations
migrateNamespaceKeys(smartconfigJson);
migrateAccessLevel(smartconfigJson);
// Ensure namespaces exist
if (!smartconfigJson["@git.zone/cli"]) {
smartconfigJson["@git.zone/cli"] = {};
}
if (!smartconfigJson["@ship.zone/szci"]) {
smartconfigJson["@ship.zone/szci"] = {};
}
const newContent = JSON.stringify(smartconfigJson, null, 2);
if (newContent !== currentContent) {
changes.push({
type: "modify",
path: CONFIG_FILE,
module: this.name,
description: "Migrate and format .smartconfig.json",
content: newContent,
});
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
if (change.type !== "modify" || !change.content) return;
const smartconfigJson = JSON.parse(change.content);
// Check for missing required module information
const expectedRepoInformation: string[] = [
"projectType",
"module.githost",
"module.gitscope",
"module.gitrepo",
"module.description",
"module.npmPackagename",
"module.license",
];
const interactInstance = new plugins.smartinteract.SmartInteract();
const missingRepoInformation = expectedRepoInformation.filter(
(expectedRepoInformationItem) => {
return !plugins.smartobject.smartGet(
smartconfigJson["@git.zone/cli"],
expectedRepoInformationItem,
);
},
);
if (missingRepoInformation.length > 0 && !this.context.isInteractive()) {
throw new Error(
`Missing required .smartconfig.json fields: ${missingRepoInformation.join(", ")}`,
);
}
for (const expectedRepoInformationItem of expectedRepoInformation) {
if (
!plugins.smartobject.smartGet(
smartconfigJson["@git.zone/cli"],
expectedRepoInformationItem,
)
) {
interactInstance.addQuestions([
{
message: `What is the value of ${expectedRepoInformationItem}`,
name: expectedRepoInformationItem,
type: "input",
default: "undefined variable",
},
]);
}
}
const answerbucket = await interactInstance.runQueue();
for (const expectedRepoInformationItem of expectedRepoInformation) {
const cliProvidedValue = answerbucket.getAnswerFor(
expectedRepoInformationItem,
);
if (cliProvidedValue) {
plugins.smartobject.smartAdd(
smartconfigJson["@git.zone/cli"],
expectedRepoInformationItem,
cliProvidedValue,
);
}
}
const finalContent = JSON.stringify(smartconfigJson, null, 2);
await this.modifyFile(change.path, finalContent);
logger.log("info", "Updated .smartconfig.json");
}
}
+164 -5
View File
@@ -1,8 +1,167 @@
import { LegacyFormatter } from './legacy.formatter.js';
import * as formatTemplates from '../format.templates.js';
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger, logVerbose } from '../../gitzone.logging.js';
export class TemplatesFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'templates', formatTemplates);
export class TemplatesFormatter extends BaseFormatter {
get name(): string {
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}`);
}
}
+73 -5
View File
@@ -1,8 +1,76 @@
import { LegacyFormatter } from './legacy.formatter.js';
import * as formatTsconfig from '../format.tsconfig.js';
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger, logVerbose } from '../../gitzone.logging.js';
export class TsconfigFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'tsconfig', formatTsconfig);
export class TsconfigFormatter extends BaseFormatter {
get name(): string {
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');
}
}
+394 -244
View File
@@ -1,250 +1,62 @@
import * as plugins from './mod.plugins.js';
import { Project } from '../classes.project.js';
import { FormatContext } from './classes.formatcontext.js';
import { FormatPlanner } from './classes.formatplanner.js';
import { BaseFormatter } from './classes.baseformatter.js';
import { logger, setVerboseMode } from '../gitzone.logging.js';
import * as plugins from "./mod.plugins.js";
import { Project } from "../classes.project.js";
import { FormatContext } from "./classes.formatcontext.js";
import { FormatPlanner } from "./classes.formatplanner.js";
import { BaseFormatter } from "./classes.baseformatter.js";
import { logger, setVerboseMode } from "../gitzone.logging.js";
import type { ICliMode } from "../helpers.climode.js";
import {
getCliMode,
printJson,
runWithSuppressedOutput,
} from "../helpers.climode.js";
import { getCliConfigValue } from "../helpers.smartconfig.js";
// Import wrapper classes for formatters
import { CleanupFormatter } from './formatters/cleanup.formatter.js';
import { NpmextraFormatter } from './formatters/npmextra.formatter.js';
import { LicenseFormatter } from './formatters/license.formatter.js';
import { PackageJsonFormatter } from './formatters/packagejson.formatter.js';
import { TemplatesFormatter } from './formatters/templates.formatter.js';
import { GitignoreFormatter } from './formatters/gitignore.formatter.js';
import { TsconfigFormatter } from './formatters/tsconfig.formatter.js';
import { PrettierFormatter } from './formatters/prettier.formatter.js';
import { ReadmeFormatter } from './formatters/readme.formatter.js';
import { CopyFormatter } from './formatters/copy.formatter.js';
export let run = async (
options: {
write?: boolean; // Explicitly write changes (default: false, dry-mode)
dryRun?: boolean; // Deprecated, kept for compatibility
yes?: boolean;
planOnly?: boolean;
savePlan?: string;
fromPlan?: string;
detailed?: boolean;
interactive?: boolean;
parallel?: boolean;
verbose?: boolean;
diff?: boolean; // Show file diffs
} = {},
): Promise<any> => {
// Set verbose mode if requested
if (options.verbose) {
setVerboseMode(true);
}
// Determine if we should write changes
// Default is dry-mode (no writing) unless --write/-w is specified
const shouldWrite = options.write ?? (options.dryRun === false);
const project = await Project.fromCwd({ requireProjectType: false });
const context = new FormatContext();
// Cache system removed - no longer needed
const planner = new FormatPlanner();
// Get configuration from npmextra
const npmextraConfig = new plugins.npmextra.Npmextra();
const formatConfig = npmextraConfig.dataFor<any>('@git.zone/cli.format', {
interactive: true,
showDiffs: false,
autoApprove: false,
planTimeout: 30000,
rollback: {
enabled: true,
autoRollbackOnError: true,
backupRetentionDays: 7,
maxBackupSize: '100MB',
excludePatterns: ['node_modules/**', '.git/**'],
},
modules: {
skip: [],
only: [],
order: [],
},
parallel: true,
cache: {
enabled: true,
clean: true, // Clean invalid entries from cache
},
});
// Cache cleaning removed - no longer using cache system
// Override config with command options
const interactive = options.interactive ?? formatConfig.interactive;
const autoApprove = options.yes ?? formatConfig.autoApprove;
const parallel = options.parallel ?? formatConfig.parallel;
try {
// Initialize formatters
const formatters = [
new CleanupFormatter(context, project),
new NpmextraFormatter(context, project),
new LicenseFormatter(context, project),
new PackageJsonFormatter(context, project),
new TemplatesFormatter(context, project),
new GitignoreFormatter(context, project),
new TsconfigFormatter(context, project),
new PrettierFormatter(context, project),
new ReadmeFormatter(context, project),
new CopyFormatter(context, project),
];
// 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
logger.log('info', 'Analyzing project for format operations...');
let plan = options.fromPlan
? JSON.parse(
(await plugins.smartfs
.file(options.fromPlan)
.encoding('utf8')
.read()) as string,
)
: await planner.planFormat(activeFormatters);
// Display plan
await planner.displayPlan(plan, options.detailed);
// Save plan if requested
if (options.savePlan) {
await plugins.smartfs
.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) {
return;
}
// Show diffs if requested (works in both dry-run and write modes)
if (options.diff) {
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;
}
// Interactive confirmation
if (interactive && !autoApprove) {
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: 'confirm',
name: 'proceed',
message: 'Proceed with formatting?',
default: true,
});
if (!(response as any).value) {
logger.log('info', 'Format operation cancelled by user');
return;
}
}
// Execute phase
logger.log(
'info',
`Executing format operations${parallel ? ' in parallel' : ' sequentially'}...`,
);
await planner.executePlan(plan, activeFormatters, context, parallel);
// Finish statistics tracking
context.getFormatStats().finish();
// Display statistics
const showStats = npmextraConfig.dataFor('gitzone.format.showStats', true);
if (showStats) {
context.getFormatStats().displayStats();
}
// Save stats if requested
if (options.detailed) {
const statsPath = `.nogit/format-stats-${Date.now()}.json`;
await context.getFormatStats().saveReport(statsPath);
}
logger.log('success', 'Format operations completed successfully!');
} catch (error) {
logger.log('error', `Format operation failed: ${error.message}`);
// Rollback system has been removed for stability
throw error;
}
};
// Export CLI command handlers
export const handleRollback = async (operationId?: string): Promise<void> => {
logger.log('info', 'Rollback system has been disabled for stability');
};
export const handleListBackups = async (): Promise<void> => {
logger.log('info', 'Backup system has been disabled for stability');
};
export const handleCleanBackups = async (): Promise<void> => {
logger.log(
'info',
'Backup cleaning has been disabled - backup system removed',
);
};
// Import the ICheckResult type for external use
import type { ICheckResult } from './interfaces.format.js';
export type { ICheckResult };
// Formatters that don't require projectType to be set
const formattersNotRequiringProjectType = ['npmextra', 'prettier', 'cleanup', 'packagejson'];
import { CleanupFormatter } from "./formatters/cleanup.formatter.js";
import { SmartconfigFormatter } from "./formatters/smartconfig.formatter.js";
import { LicenseFormatter } from "./formatters/license.formatter.js";
import { PackageJsonFormatter } from "./formatters/packagejson.formatter.js";
import { TemplatesFormatter } from "./formatters/templates.formatter.js";
import { GitignoreFormatter } from "./formatters/gitignore.formatter.js";
import { TsconfigFormatter } from "./formatters/tsconfig.formatter.js";
import { PrettierFormatter } from "./formatters/prettier.formatter.js";
import { ReadmeFormatter } from "./formatters/readme.formatter.js";
import { CopyFormatter } from "./formatters/copy.formatter.js";
/**
* Run a single formatter by name (for use by other modules)
* Rename npmextra.json or smartconfig.json to .smartconfig.json
* before any formatter tries to read config.
*/
export const runFormatter = async (
formatterName: string,
options: {
silent?: boolean;
checkOnly?: boolean; // Only check for diffs, don't apply
showDiff?: boolean; // Show the diff output
} = {}
): Promise<ICheckResult | void> => {
// Determine if this formatter requires projectType
const requireProjectType = !formattersNotRequiringProjectType.includes(formatterName);
const project = await Project.fromCwd({ requireProjectType });
const context = new FormatContext();
async function migrateConfigFile(allowWrite: boolean): Promise<void> {
const target = ".smartconfig.json";
const targetExists = await plugins.smartfs.file(target).exists();
if (targetExists) return;
// Map formatter names to classes
const formatterMap: Record<string, new (ctx: FormatContext, proj: Project) => BaseFormatter> = {
for (const oldName of ["smartconfig.json", "npmextra.json"]) {
const exists = await plugins.smartfs.file(oldName).exists();
if (exists) {
if (!allowWrite) {
return;
}
const content = (await plugins.smartfs
.file(oldName)
.encoding("utf8")
.read()) as string;
await plugins.smartfs.file(`./${target}`).encoding("utf8").write(content);
await plugins.smartfs.file(oldName).delete();
logger.log("info", `Migrated ${oldName} to ${target}`);
return;
}
}
}
// Shared formatter class map used by both run() and runFormatter()
const formatterMap: Record<
string,
new (ctx: FormatContext, proj: Project) => BaseFormatter
> = {
cleanup: CleanupFormatter,
npmextra: NpmextraFormatter,
smartconfig: SmartconfigFormatter,
license: LicenseFormatter,
packagejson: PackageJsonFormatter,
templates: TemplatesFormatter,
@@ -255,6 +67,272 @@ export const runFormatter = async (
copy: CopyFormatter,
};
// Formatters that don't require projectType to be set
const formattersNotRequiringProjectType = [
"smartconfig",
"prettier",
"cleanup",
"packagejson",
];
const getFormatConfig = async () => {
const rawFormatConfig = await getCliConfigValue<Record<string, any>>(
"format",
{},
);
return {
interactive: true,
showDiffs: false,
autoApprove: false,
showStats: true,
modules: {
skip: [],
only: [],
...(rawFormatConfig.modules || {}),
},
...rawFormatConfig,
};
};
const createActiveFormatters = async (options: {
interactive: boolean;
jsonOutput: boolean;
}) => {
const project = await Project.fromCwd({ requireProjectType: false });
const context = new FormatContext(options);
const planner = new FormatPlanner();
const formatConfig = await getFormatConfig();
const formatters = Object.entries(formatterMap).map(
([, FormatterClass]) => new FormatterClass(context, project),
);
const activeFormatters = formatters.filter((formatter) => {
if (formatConfig.modules.only.length > 0) {
return formatConfig.modules.only.includes(formatter.name);
}
if (formatConfig.modules.skip.includes(formatter.name)) {
return false;
}
return true;
});
return {
context,
planner,
formatConfig,
activeFormatters,
};
};
const buildFormatPlan = async (options: {
fromPlan?: string;
interactive: boolean;
jsonOutput: boolean;
}) => {
const { context, planner, formatConfig, activeFormatters } =
await createActiveFormatters({
interactive: options.interactive,
jsonOutput: options.jsonOutput,
});
const plan = options.fromPlan
? JSON.parse(
(await plugins.smartfs
.file(options.fromPlan)
.encoding("utf8")
.read()) as string,
)
: await planner.planFormat(activeFormatters);
return {
context,
planner,
formatConfig,
activeFormatters,
plan,
};
};
const serializePlan = (plan: any) => {
return {
summary: plan.summary,
warnings: plan.warnings,
changes: plan.changes.map((change: any) => ({
type: change.type,
path: change.path,
module: change.module,
description: change.description,
})),
};
};
export let run = async (
options: {
write?: boolean;
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
await planner.displayPlan(plan, options.detailed);
// Save plan if requested
if (options.savePlan) {
await plugins.smartfs
.file(options.savePlan)
.encoding("utf8")
.write(JSON.stringify(plan, null, 2));
logger.log("info", `Plan saved to ${options.savePlan}`);
}
if (options.planOnly || treatAsPlan) {
return;
}
// Show diffs if explicitly requested or before interactive write confirmation
const showDiffs =
options.diff || (shouldWrite && interactive && !autoApprove);
if (showDiffs) {
logger.log("info", "Showing file diffs:");
console.log("");
for (const formatter of activeFormatters) {
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;
}
// Interactive confirmation
if (interactive && !autoApprove) {
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: "confirm",
name: "proceed",
message: "Proceed with formatting?",
default: true,
});
if (!(response as any).value) {
logger.log("info", "Format operation cancelled by user");
return;
}
}
// Execute phase
logger.log("info", "Executing format operations...");
await planner.executePlan(plan, activeFormatters, context);
context.getFormatStats().finish();
const showStats = formatConfig.showStats ?? true;
if (showStats) {
context.getFormatStats().displayStats();
}
if (options.detailed) {
const statsPath = `.nogit/format-stats-${Date.now()}.json`;
await context.getFormatStats().saveReport(statsPath);
}
logger.log("success", "Format operations completed successfully!");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.log("error", `Format operation failed: ${errorMessage}`);
throw error;
}
};
import type { ICheckResult } from "./interfaces.format.js";
export type { ICheckResult };
/**
* Run a single formatter by name (for use by other modules)
*/
export const runFormatter = async (
formatterName: string,
options: {
silent?: boolean;
checkOnly?: boolean;
showDiff?: boolean;
} = {},
): Promise<ICheckResult | void> => {
const requireProjectType =
!formattersNotRequiringProjectType.includes(formatterName);
const project = await Project.fromCwd({ requireProjectType });
const context = new FormatContext({ interactive: true, jsonOutput: false });
const FormatterClass = formatterMap[formatterName];
if (!FormatterClass) {
throw new Error(`Unknown formatter: ${formatterName}`);
@@ -262,7 +340,6 @@ export const runFormatter = async (
const formatter = new FormatterClass(context, project);
// Check-only mode: just check for diffs and optionally display them
if (options.checkOnly) {
const result = await formatter.check();
if (result.hasDiff && options.showDiff) {
@@ -271,7 +348,6 @@ export const runFormatter = async (
return result;
}
// Normal mode: analyze and apply changes
const changes = await formatter.analyze();
for (const change of changes) {
@@ -279,6 +355,80 @@ export const runFormatter = async (
}
if (!options.silent) {
logger.log('success', `Formatter '${formatterName}' completed`);
logger.log("success", `Formatter '${formatterName}' completed`);
}
};
export function showHelp(mode?: ICliMode): void {
if (mode?.json) {
printJson({
command: "format",
usage: "gitzone format [plan] [options]",
description:
"Plans formatting changes by default and applies them only with --write.",
flags: [
{ flag: "--write, -w", description: "Apply planned changes" },
{
flag: "--yes",
description: "Skip the interactive confirmation before writing",
},
{
flag: "--plan-only",
description: "Show the plan without applying changes",
},
{
flag: "--save-plan <file>",
description: "Write the format plan to a file",
},
{
flag: "--from-plan <file>",
description: "Load a previously saved plan",
},
{
flag: "--detailed",
description: "Show detailed diffs and save stats",
},
{ flag: "--verbose", description: "Enable verbose logging" },
{
flag: "--diff",
description: "Show per-file diffs before applying changes",
},
{ flag: "--json", description: "Emit a read-only format plan as JSON" },
],
examples: [
"gitzone format",
"gitzone format plan --json",
"gitzone format --write --yes",
],
});
return;
}
console.log("");
console.log("Usage: gitzone format [plan] [options]");
console.log("");
console.log(
"Plans formatting changes by default and applies them only with --write.",
);
console.log("");
console.log("Flags:");
console.log(" --write, -w Apply planned changes");
console.log(
" --yes Skip the interactive confirmation before writing",
);
console.log(" --plan-only Show the plan without applying changes");
console.log(" --save-plan <file> Write the format plan to a file");
console.log(" --from-plan <file> Load a previously saved plan");
console.log(" --detailed Show detailed diffs and save stats");
console.log(" --verbose Enable verbose logging");
console.log(
" --diff Show per-file diffs before applying changes",
);
console.log(" --json Emit a read-only format plan as JSON");
console.log("");
console.log("Examples:");
console.log(" gitzone format");
console.log(" gitzone format plan --json");
console.log(" gitzone format --write --yes");
console.log("");
}
+16 -19
View File
@@ -1,31 +1,15 @@
export type IFormatOperation = {
id: string;
timestamp: number;
files: Array<{
path: string;
originalContent: string;
checksum: string;
permissions: string;
}>;
status: 'pending' | 'in-progress' | 'completed' | 'failed' | 'rolled-back';
error?: Error;
};
export type IFormatPlan = {
summary: {
totalFiles: number;
filesAdded: number;
filesModified: number;
filesRemoved: number;
estimatedTime: number;
};
changes: Array<{
type: 'create' | 'modify' | 'delete';
path: string;
module: string;
description: string;
diff?: string;
size?: number;
}>;
warnings: Array<{
level: 'info' | 'warning' | 'error';
@@ -40,9 +24,6 @@ export type IPlannedChange = {
module: string;
description: string;
content?: string; // New content for create/modify operations
originalContent?: string; // Original content for comparison
diff?: string;
size?: number;
};
export interface ICheckResult {
@@ -54,3 +35,19 @@ export interface ICheckResult {
after?: string;
}>;
}
export function getModuleIcon(module: string): string {
const icons: Record<string, string> = {
packagejson: '📦',
license: '📝',
tsconfig: '🔧',
cleanup: '🚮',
gitignore: '🔒',
prettier: '✨',
readme: '📖',
templates: '📄',
smartconfig: '⚙️',
copy: '📋',
};
return icons[module] || '📁';
}
+4 -14
View File
@@ -1,31 +1,21 @@
export * from '../plugins.js';
import * as crypto from 'crypto';
import * as path from 'path';
import * as lik from '@push.rocks/lik';
import * as smartfile from '@push.rocks/smartfile';
import * as smartgulp from '@push.rocks/smartgulp';
import * as smartinteract from '@push.rocks/smartinteract';
import * as smartlegal from '@push.rocks/smartlegal';
import * as smartobject from '@push.rocks/smartobject';
import * as smartnpm from '@push.rocks/smartnpm';
import * as smartstream from '@push.rocks/smartstream';
import * as through2 from 'through2';
import * as npmextra from '@push.rocks/npmextra';
import * as smartconfig from '@push.rocks/smartconfig';
import * as smartdiff from '@push.rocks/smartdiff';
import * as smartscaf from '@push.rocks/smartscaf';
export {
crypto,
path,
lik,
smartfile,
smartgulp,
smartinteract,
smartlegal,
smartobject,
smartnpm,
smartstream,
through2,
npmextra,
smartconfig,
smartdiff,
smartscaf,
};
+2 -2
View File
@@ -26,11 +26,11 @@ export interface IGlobalRegistryData {
export class GlobalRegistry {
private static instance: GlobalRegistry | null = null;
private kvStore: plugins.npmextra.KeyValueStore<IGlobalRegistryData>;
private kvStore: plugins.smartconfig.KeyValueStore<IGlobalRegistryData>;
private docker: DockerContainer;
private constructor() {
this.kvStore = new plugins.npmextra.KeyValueStore({
this.kvStore = new plugins.smartconfig.KeyValueStore({
typeArg: 'userHomeDir',
identityArg: 'gitzone-services',
});
+20 -20
View File
@@ -31,7 +31,7 @@ export class ServiceManager {
await this.config.loadOrCreate();
logger.log('info', `📋 Project: ${this.config.getConfig().PROJECT_NAME}`);
// Load service selection from npmextra.json
// Load service selection from .smartconfig.json
await this.loadServiceConfiguration();
// Validate and update ports if needed
@@ -39,11 +39,11 @@ export class ServiceManager {
}
/**
* Load service configuration from npmextra.json
* Load service configuration from .smartconfig.json
*/
private async loadServiceConfiguration(): Promise<void> {
const npmextraConfig = new plugins.npmextra.Npmextra(process.cwd());
const gitzoneConfig = npmextraConfig.dataFor<any>('@git.zone/cli', {});
const smartconfigInstance = new plugins.smartconfig.Smartconfig(process.cwd());
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
// Check if services array exists
if (!gitzoneConfig.services || !Array.isArray(gitzoneConfig.services) || gitzoneConfig.services.length === 0) {
@@ -63,7 +63,7 @@ export class ServiceManager {
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
// Save to npmextra.json
// Save to .smartconfig.json
await this.saveServiceConfiguration(this.enabledServices);
} else {
this.enabledServices = gitzoneConfig.services;
@@ -72,31 +72,31 @@ export class ServiceManager {
}
/**
* Save service configuration to npmextra.json
* Save service configuration to .smartconfig.json
*/
private async saveServiceConfiguration(services: string[]): Promise<void> {
const npmextraPath = plugins.path.join(process.cwd(), 'npmextra.json');
let npmextraData: any = {};
const smartconfigPath = plugins.path.join(process.cwd(), '.smartconfig.json');
let smartconfigData: any = {};
// Read existing npmextra.json if it exists
if (await plugins.smartfs.file(npmextraPath).exists()) {
const content = await plugins.smartfs.file(npmextraPath).encoding('utf8').read();
npmextraData = JSON.parse(content as string);
// Read existing .smartconfig.json if it exists
if (await plugins.smartfs.file(smartconfigPath).exists()) {
const content = await plugins.smartfs.file(smartconfigPath).encoding('utf8').read();
smartconfigData = JSON.parse(content as string);
}
// Update @git.zone/cli.services
if (!npmextraData['@git.zone/cli']) {
npmextraData['@git.zone/cli'] = {};
if (!smartconfigData['@git.zone/cli']) {
smartconfigData['@git.zone/cli'] = {};
}
npmextraData['@git.zone/cli'].services = services;
smartconfigData['@git.zone/cli'].services = services;
// Write back to npmextra.json
// Write back to .smartconfig.json
await plugins.smartfs
.file(npmextraPath)
.file(smartconfigPath)
.encoding('utf8')
.write(JSON.stringify(npmextraData, null, 2));
.write(JSON.stringify(smartconfigData, null, 2));
logger.log('ok', `✅ Saved service configuration to npmextra.json`);
logger.log('ok', `✅ Saved service configuration to .smartconfig.json`);
logger.log('info', `🔧 Enabled services: ${services.join(', ')}`);
}
@@ -904,7 +904,7 @@ export class ServiceManager {
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
// Save to npmextra.json
// Save to .smartconfig.json
await this.saveServiceConfiguration(this.enabledServices);
logger.log('ok', '✅ Service configuration updated');
+535 -168
View File
@@ -1,12 +1,26 @@
import * as plugins from './mod.plugins.js';
import * as helpers from './helpers.js';
import { ServiceManager } from './classes.servicemanager.js';
import { GlobalRegistry } from './classes.globalregistry.js';
import { logger } from '../gitzone.logging.js';
import * as plugins from "./mod.plugins.js";
import * as helpers from "./helpers.js";
import { ServiceManager } from "./classes.servicemanager.js";
import { GlobalRegistry } from "./classes.globalregistry.js";
import { logger } from "../gitzone.logging.js";
import type { ICliMode } from "../helpers.climode.js";
import { getCliMode, printJson } from "../helpers.climode.js";
import {
getCliConfigValueFromData,
readSmartconfigFile,
setCliConfigValueInData,
writeSmartconfigFile,
} from "../helpers.smartconfig.js";
export const run = async (argvArg: any) => {
const mode = await getCliMode(argvArg);
const isGlobal = argvArg.g || argvArg.global;
const command = argvArg._[1] || 'help';
const command = argvArg._[1] || "help";
if (mode.help || command === "help") {
showHelp(mode);
return;
}
// Handle global commands first
if (isGlobal) {
@@ -14,264 +28,597 @@ export const run = async (argvArg: any) => {
return;
}
// Local project commands
const serviceManager = new ServiceManager();
await serviceManager.init();
const service = argvArg._[2] || 'all';
const service = argvArg._[2] || "all";
switch (command) {
case 'start':
await handleStart(serviceManager, service);
break;
case 'stop':
await handleStop(serviceManager, service);
break;
case 'restart':
await handleRestart(serviceManager, service);
break;
case 'status':
await serviceManager.showStatus();
break;
case 'config':
if (service === 'services' || argvArg._[2] === 'services') {
case "config":
if (service === "services" || argvArg._[2] === "services") {
const serviceManager = new ServiceManager();
await serviceManager.init();
await handleConfigureServices(serviceManager);
} else {
await serviceManager.showConfig();
await handleShowConfig(mode);
}
break;
case 'compass':
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':
case "logs": {
const lines = parseInt(argvArg._[3]) || 20;
await serviceManager.showLogs(service, lines);
break;
}
case 'remove':
case "remove":
await handleRemove(serviceManager);
break;
case 'clean':
case "clean":
await handleClean(serviceManager);
break;
case 'reconfigure':
case "reconfigure":
await serviceManager.reconfigure();
break;
case 'help':
}
break;
}
default:
showHelp();
showHelp(mode);
break;
}
};
const allowedServices = ["mongodb", "minio", "elasticsearch"];
const normalizeServiceName = (service: string): string => {
switch (service) {
case "mongo":
case "mongodb":
return "mongodb";
case "minio":
case "s3":
return "minio";
case "elastic":
case "elasticsearch":
case "es":
return "elasticsearch";
default:
return service;
}
};
async function readServicesConfig(): Promise<{
enabledServices: string[];
environment: Record<string, any> | null;
}> {
const smartconfigData = await readSmartconfigFile();
const enabledServices = getCliConfigValueFromData(
smartconfigData,
"services",
);
let environment: Record<string, any> | null = null;
const envPath = plugins.path.join(process.cwd(), ".nogit", "env.json");
if (await plugins.smartfs.file(envPath).exists()) {
const envContent = (await plugins.smartfs
.file(envPath)
.encoding("utf8")
.read()) as string;
environment = JSON.parse(envContent);
}
return {
enabledServices: Array.isArray(enabledServices) ? enabledServices : [],
environment,
};
}
async function updateEnabledServices(services: string[]): Promise<void> {
const smartconfigData = await readSmartconfigFile();
setCliConfigValueInData(smartconfigData, "services", services);
await writeSmartconfigFile(smartconfigData);
}
async function handleShowConfig(mode: ICliMode) {
const configData = await readServicesConfig();
if (mode.json) {
printJson(configData);
return;
}
helpers.printHeader("Current Services Configuration");
logger.log(
"info",
`Enabled Services: ${configData.enabledServices.length > 0 ? configData.enabledServices.join(", ") : "none configured"}`,
);
console.log();
if (!configData.environment) {
logger.log(
"note",
"No .nogit/env.json found yet. Start a service once to create runtime defaults.",
);
return;
}
const env = configData.environment;
logger.log("note", "MongoDB:");
logger.log("info", ` Host: ${env.MONGODB_HOST}:${env.MONGODB_PORT}`);
logger.log("info", ` Database: ${env.MONGODB_NAME}`);
logger.log("info", ` User: ${env.MONGODB_USER}`);
logger.log("info", ` Container: ${env.PROJECT_NAME}-mongodb`);
logger.log(
"info",
` Data: ${plugins.path.join(process.cwd(), ".nogit", "mongodata")}`,
);
logger.log("info", ` Connection: ${env.MONGODB_URL}`);
console.log();
logger.log("note", "S3/MinIO:");
logger.log("info", ` Host: ${env.S3_HOST}`);
logger.log("info", ` API Port: ${env.S3_PORT}`);
logger.log("info", ` Console Port: ${env.S3_CONSOLE_PORT}`);
logger.log("info", ` Bucket: ${env.S3_BUCKET}`);
logger.log("info", ` Container: ${env.PROJECT_NAME}-minio`);
logger.log(
"info",
` Data: ${plugins.path.join(process.cwd(), ".nogit", "miniodata")}`,
);
logger.log("info", ` Endpoint: ${env.S3_ENDPOINT}`);
console.log();
logger.log("note", "Elasticsearch:");
logger.log(
"info",
` Host: ${env.ELASTICSEARCH_HOST}:${env.ELASTICSEARCH_PORT}`,
);
logger.log("info", ` User: ${env.ELASTICSEARCH_USER}`);
logger.log("info", ` Container: ${env.PROJECT_NAME}-elasticsearch`);
logger.log(
"info",
` Data: ${plugins.path.join(process.cwd(), ".nogit", "esdata")}`,
);
logger.log("info", ` Connection: ${env.ELASTICSEARCH_URL}`);
}
async function handleSetServices(rawValue: string | undefined, mode: ICliMode) {
if (!rawValue) {
throw new Error("Specify a comma-separated list of services");
}
const requestedServices = rawValue
.split(",")
.map((service) => normalizeServiceName(service.trim()))
.filter(Boolean);
validateRequestedServices(requestedServices);
await updateEnabledServices(requestedServices);
if (mode.json) {
printJson({ ok: true, action: "set", enabledServices: requestedServices });
return;
}
logger.log("ok", `Enabled services set to: ${requestedServices.join(", ")}`);
}
async function handleEnableServices(
requestedServices: string[],
mode: ICliMode,
) {
const normalizedServices = requestedServices.map((service) =>
normalizeServiceName(service),
);
validateRequestedServices(normalizedServices);
const configData = await readServicesConfig();
const nextServices = Array.from(
new Set([...configData.enabledServices, ...normalizedServices]),
);
await updateEnabledServices(nextServices);
if (mode.json) {
printJson({ ok: true, action: "enable", enabledServices: nextServices });
return;
}
logger.log("ok", `Enabled services: ${nextServices.join(", ")}`);
}
async function handleDisableServices(
requestedServices: string[],
mode: ICliMode,
) {
const normalizedServices = requestedServices.map((service) =>
normalizeServiceName(service),
);
validateRequestedServices(normalizedServices);
const configData = await readServicesConfig();
const nextServices = configData.enabledServices.filter(
(service) => !normalizedServices.includes(service),
);
await updateEnabledServices(nextServices);
if (mode.json) {
printJson({ ok: true, action: "disable", enabledServices: nextServices });
return;
}
logger.log("ok", `Enabled services: ${nextServices.join(", ")}`);
}
function validateRequestedServices(services: string[]): void {
if (services.length === 0) {
throw new Error("Specify at least one service");
}
const invalidServices = services.filter(
(service) => !allowedServices.includes(service),
);
if (invalidServices.length > 0) {
throw new Error(`Unknown service(s): ${invalidServices.join(", ")}`);
}
}
async function handleStart(serviceManager: ServiceManager, service: string) {
helpers.printHeader('Starting Services');
helpers.printHeader("Starting Services");
switch (service) {
case 'mongo':
case 'mongodb':
case "mongo":
case "mongodb":
await serviceManager.startMongoDB();
break;
case 'minio':
case 's3':
case "minio":
case "s3":
await serviceManager.startMinIO();
break;
case 'elasticsearch':
case 'es':
case "elasticsearch":
case "es":
await serviceManager.startElasticsearch();
break;
case 'all':
case '':
case "all":
case "":
await serviceManager.startAll();
break;
default:
logger.log('error', `Unknown service: ${service}`);
logger.log('note', 'Use: mongo, s3, elasticsearch, or all');
logger.log("error", `Unknown service: ${service}`);
logger.log("note", "Use: mongo, s3, elasticsearch, or all");
break;
}
}
async function handleStop(serviceManager: ServiceManager, service: string) {
helpers.printHeader('Stopping Services');
helpers.printHeader("Stopping Services");
switch (service) {
case 'mongo':
case 'mongodb':
case "mongo":
case "mongodb":
await serviceManager.stopMongoDB();
break;
case 'minio':
case 's3':
case "minio":
case "s3":
await serviceManager.stopMinIO();
break;
case 'elasticsearch':
case 'es':
case "elasticsearch":
case "es":
await serviceManager.stopElasticsearch();
break;
case 'all':
case '':
case "all":
case "":
await serviceManager.stopAll();
break;
default:
logger.log('error', `Unknown service: ${service}`);
logger.log('note', 'Use: mongo, s3, elasticsearch, or all');
logger.log("error", `Unknown service: ${service}`);
logger.log("note", "Use: mongo, s3, elasticsearch, or all");
break;
}
}
async function handleRestart(serviceManager: ServiceManager, service: string) {
helpers.printHeader('Restarting Services');
helpers.printHeader("Restarting Services");
switch (service) {
case 'mongo':
case 'mongodb':
case "mongo":
case "mongodb":
await serviceManager.stopMongoDB();
await plugins.smartdelay.delayFor(2000);
await serviceManager.startMongoDB();
break;
case 'minio':
case 's3':
case "minio":
case "s3":
await serviceManager.stopMinIO();
await plugins.smartdelay.delayFor(2000);
await serviceManager.startMinIO();
break;
case 'elasticsearch':
case 'es':
case "elasticsearch":
case "es":
await serviceManager.stopElasticsearch();
await plugins.smartdelay.delayFor(2000);
await serviceManager.startElasticsearch();
break;
case 'all':
case '':
case "all":
case "":
await serviceManager.stopAll();
await plugins.smartdelay.delayFor(2000);
await serviceManager.startAll();
break;
default:
logger.log('error', `Unknown service: ${service}`);
logger.log("error", `Unknown service: ${service}`);
break;
}
}
async function handleRemove(serviceManager: ServiceManager) {
helpers.printHeader('Removing Containers');
logger.log('note', '⚠️ This will remove containers but preserve data');
helpers.printHeader("Removing Containers");
logger.log("note", "⚠️ This will remove containers but preserve data");
const shouldContinue = await plugins.smartinteract.SmartInteract.getCliConfirmation('Continue?', false);
const shouldContinue =
await plugins.smartinteract.SmartInteract.getCliConfirmation(
"Continue?",
false,
);
if (shouldContinue) {
await serviceManager.removeContainers();
} else {
logger.log('note', 'Cancelled');
logger.log("note", "Cancelled");
}
}
async function handleClean(serviceManager: ServiceManager) {
helpers.printHeader('Clean All');
logger.log('error', '⚠️ WARNING: This will remove all containers and data!');
logger.log('error', 'This action cannot be undone!');
helpers.printHeader("Clean All");
logger.log("error", "⚠️ WARNING: This will remove all containers and data!");
logger.log("error", "This action cannot be undone!");
const smartinteraction = new plugins.smartinteract.SmartInteract();
const confirmAnswer = await smartinteraction.askQuestion({
name: 'confirm',
type: 'input',
name: "confirm",
type: "input",
message: 'Type "yes" to confirm:',
default: 'no'
default: "no",
});
if (confirmAnswer.value === 'yes') {
if (confirmAnswer.value === "yes") {
await serviceManager.removeContainers();
console.log();
await serviceManager.cleanData();
logger.log('ok', 'All cleaned ✓');
logger.log("ok", "All cleaned ✓");
} else {
logger.log('note', 'Cancelled');
logger.log("note", "Cancelled");
}
}
async function handleConfigureServices(serviceManager: ServiceManager) {
helpers.printHeader('Configure Services');
helpers.printHeader("Configure Services");
await serviceManager.configureServices();
}
function showHelp() {
helpers.printHeader('GitZone Services Manager');
export function showHelp(mode?: ICliMode) {
if (mode?.json) {
printJson({
command: "services",
usage: "gitzone services <command> [options]",
commands: [
{
name: "config",
description:
"Show configured services and any existing runtime env.json data",
},
{
name: "set <csv>",
description: "Set the enabled service list without prompts",
},
{
name: "enable <service...>",
description: "Enable one or more services without prompts",
},
{
name: "disable <service...>",
description: "Disable one or more services without prompts",
},
{ name: "start [service]", description: "Start services" },
{ name: "stop [service]", description: "Stop services" },
{ name: "status", description: "Show service status" },
],
examples: [
"gitzone services config --json",
"gitzone services set mongodb,minio",
"gitzone services enable elasticsearch",
],
});
return;
}
logger.log('ok', 'Usage: gitzone services [command] [options]');
helpers.printHeader("GitZone Services Manager");
logger.log("ok", "Usage: gitzone services [command] [options]");
console.log();
logger.log('note', 'Commands:');
logger.log('info', ' start [service] Start services (mongo|s3|elasticsearch|all)');
logger.log('info', ' stop [service] Stop services (mongo|s3|elasticsearch|all)');
logger.log('info', ' restart [service] Restart services (mongo|s3|elasticsearch|all)');
logger.log('info', ' status Show service status');
logger.log('info', ' config Show current configuration');
logger.log('info', ' config services Configure which services are enabled');
logger.log('info', ' compass Show MongoDB Compass connection string');
logger.log('info', ' logs [service] Show logs (mongo|s3|elasticsearch|all) [lines]');
logger.log('info', ' reconfigure Reassign ports and restart services');
logger.log('info', ' remove Remove all containers');
logger.log('info', ' clean Remove all containers and data ⚠️');
logger.log('info', ' help Show this help message');
logger.log("note", "Commands:");
logger.log(
"info",
" start [service] Start services (mongo|s3|elasticsearch|all)",
);
logger.log(
"info",
" stop [service] Stop services (mongo|s3|elasticsearch|all)",
);
logger.log(
"info",
" restart [service] Restart services (mongo|s3|elasticsearch|all)",
);
logger.log("info", " status Show service status");
logger.log("info", " config Show current configuration");
logger.log(
"info",
" config services Configure which services are enabled",
);
logger.log(
"info",
" set <csv> Set enabled services without prompts",
);
logger.log("info", " enable <svc...> Enable one or more services");
logger.log("info", " disable <svc...> Disable one or more services");
logger.log(
"info",
" compass Show MongoDB Compass connection string",
);
logger.log(
"info",
" logs [service] Show logs (mongo|s3|elasticsearch|all) [lines]",
);
logger.log("info", " reconfigure Reassign ports and restart services");
logger.log("info", " remove Remove all containers");
logger.log("info", " clean Remove all containers and data ⚠️");
logger.log("info", " help Show this help message");
console.log();
logger.log('note', 'Available Services:');
logger.log('info', ' • MongoDB (mongo) - Document database');
logger.log('info', ' • MinIO (s3) - S3-compatible object storage');
logger.log('info', ' • Elasticsearch (elasticsearch) - Search and analytics engine');
logger.log("note", "Available Services:");
logger.log("info", " • MongoDB (mongo) - Document database");
logger.log("info", " • MinIO (s3) - S3-compatible object storage");
logger.log(
"info",
" • Elasticsearch (elasticsearch) - Search and analytics engine",
);
console.log();
logger.log('note', 'Features:');
logger.log('info', ' • Auto-creates .nogit/env.json with smart defaults');
logger.log('info', ' • Random ports (20000-30000) for MongoDB/MinIO to avoid conflicts');
logger.log('info', ' • Elasticsearch uses standard port 9200');
logger.log('info', ' • Project-specific containers for multi-project support');
logger.log('info', ' • Preserves custom configuration values');
logger.log('info', 'MongoDB Compass connection support');
logger.log("note", "Features:");
logger.log("info", " • Auto-creates .nogit/env.json with smart defaults");
logger.log(
"info",
" • Random ports (20000-30000) for MongoDB/MinIO to avoid conflicts",
);
logger.log("info", "Elasticsearch uses standard port 9200");
logger.log(
"info",
" • Project-specific containers for multi-project support",
);
logger.log("info", " • Preserves custom configuration values");
logger.log("info", " • MongoDB Compass connection support");
console.log();
logger.log('note', 'Examples:');
logger.log('info', ' gitzone services start # Start all services');
logger.log('info', ' gitzone services start mongo # Start only MongoDB');
logger.log('info', ' gitzone services start elasticsearch # Start only Elasticsearch');
logger.log('info', ' gitzone services stop # Stop all services');
logger.log('info', ' gitzone services status # Check service status');
logger.log('info', ' gitzone services config # Show configuration');
logger.log('info', ' gitzone services compass # Get MongoDB Compass connection');
logger.log('info', ' gitzone services logs elasticsearch # Show Elasticsearch logs');
logger.log("note", "Examples:");
logger.log(
"info",
" gitzone services start # Start all services",
);
logger.log(
"info",
" gitzone services start mongo # Start only MongoDB",
);
logger.log(
"info",
" gitzone services start elasticsearch # Start only Elasticsearch",
);
logger.log(
"info",
" gitzone services stop # Stop all services",
);
logger.log(
"info",
" gitzone services status # Check service status",
);
logger.log(
"info",
" gitzone services config # Show configuration",
);
logger.log(
"info",
" gitzone services config --json # Show configuration as JSON",
);
logger.log(
"info",
" gitzone services set mongodb,minio # Configure services without prompts",
);
logger.log(
"info",
" gitzone services compass # Get MongoDB Compass connection",
);
logger.log(
"info",
" gitzone services logs elasticsearch # Show Elasticsearch logs",
);
console.log();
logger.log('note', 'Global Commands (-g/--global):');
logger.log('info', ' list -g List all registered projects');
logger.log('info', ' status -g Show status across all projects');
logger.log('info', ' stop -g Stop all containers across all projects');
logger.log('info', ' cleanup -g Remove stale registry entries');
logger.log("note", "Global Commands (-g/--global):");
logger.log("info", " list -g List all registered projects");
logger.log("info", " status -g Show status across all projects");
logger.log(
"info",
" stop -g Stop all containers across all projects",
);
logger.log("info", " cleanup -g Remove stale registry entries");
console.log();
logger.log('note', 'Global Examples:');
logger.log('info', ' gitzone services list -g # List all registered projects');
logger.log('info', ' gitzone services status -g # Show global container status');
logger.log('info', ' gitzone services stop -g # Stop all (prompts for confirmation)');
logger.log("note", "Global Examples:");
logger.log(
"info",
" gitzone services list -g # List all registered projects",
);
logger.log(
"info",
" gitzone services status -g # Show global container status",
);
logger.log(
"info",
" gitzone services stop -g # Stop all (prompts for confirmation)",
);
}
// ==================== Global Command Handlers ====================
@@ -280,23 +627,23 @@ async function handleGlobalCommand(command: string) {
const globalRegistry = GlobalRegistry.getInstance();
switch (command) {
case 'list':
case "list":
await handleGlobalList(globalRegistry);
break;
case 'status':
case "status":
await handleGlobalStatus(globalRegistry);
break;
case 'stop':
case "stop":
await handleGlobalStop(globalRegistry);
break;
case 'cleanup':
case "cleanup":
await handleGlobalCleanup(globalRegistry);
break;
case 'help':
case "help":
default:
showHelp();
break;
@@ -304,13 +651,13 @@ async function handleGlobalCommand(command: string) {
}
async function handleGlobalList(globalRegistry: GlobalRegistry) {
helpers.printHeader('Registered Projects (Global)');
helpers.printHeader("Registered Projects (Global)");
const projects = await globalRegistry.getAllProjects();
const projectPaths = Object.keys(projects);
if (projectPaths.length === 0) {
logger.log('note', 'No projects registered');
logger.log("note", "No projects registered");
return;
}
@@ -319,20 +666,20 @@ async function handleGlobalList(globalRegistry: GlobalRegistry) {
const lastActive = new Date(project.lastActive).toLocaleString();
console.log();
logger.log('ok', `📁 ${project.projectName}`);
logger.log('info', ` Path: ${project.projectPath}`);
logger.log('info', ` Services: ${project.enabledServices.join(', ')}`);
logger.log('info', ` Last Active: ${lastActive}`);
logger.log("ok", `📁 ${project.projectName}`);
logger.log("info", ` Path: ${project.projectPath}`);
logger.log("info", ` Services: ${project.enabledServices.join(", ")}`);
logger.log("info", ` Last Active: ${lastActive}`);
}
}
async function handleGlobalStatus(globalRegistry: GlobalRegistry) {
helpers.printHeader('Global Service Status');
helpers.printHeader("Global Service Status");
const statuses = await globalRegistry.getGlobalStatus();
if (statuses.length === 0) {
logger.log('note', 'No projects registered');
logger.log("note", "No projects registered");
return;
}
@@ -341,28 +688,39 @@ async function handleGlobalStatus(globalRegistry: GlobalRegistry) {
for (const project of statuses) {
console.log();
logger.log('ok', `📁 ${project.projectName}`);
logger.log('info', ` Path: ${project.projectPath}`);
logger.log("ok", `📁 ${project.projectName}`);
logger.log("info", ` Path: ${project.projectPath}`);
if (project.containers.length === 0) {
logger.log('note', ' No containers configured');
logger.log("note", " No containers configured");
continue;
}
for (const container of project.containers) {
totalContainers++;
const statusIcon = container.status === 'running' ? '🟢' : container.status === 'exited' ? '🟡' : '⚪';
if (container.status === 'running') runningCount++;
logger.log('info', ` ${statusIcon} ${container.name}: ${container.status}`);
const statusIcon =
container.status === "running"
? "🟢"
: container.status === "exited"
? "🟡"
: "⚪";
if (container.status === "running") runningCount++;
logger.log(
"info",
` ${statusIcon} ${container.name}: ${container.status}`,
);
}
}
console.log();
logger.log('note', `Summary: ${runningCount}/${totalContainers} containers running across ${statuses.length} project(s)`);
logger.log(
"note",
`Summary: ${runningCount}/${totalContainers} containers running across ${statuses.length} project(s)`,
);
}
async function handleGlobalStop(globalRegistry: GlobalRegistry) {
helpers.printHeader('Stop All Containers (Global)');
helpers.printHeader("Stop All Containers (Global)");
const statuses = await globalRegistry.getGlobalStatus();
@@ -370,64 +728,73 @@ async function handleGlobalStop(globalRegistry: GlobalRegistry) {
let runningCount = 0;
for (const project of statuses) {
for (const container of project.containers) {
if (container.status === 'running') runningCount++;
if (container.status === "running") runningCount++;
}
}
if (runningCount === 0) {
logger.log('note', 'No running containers found');
logger.log("note", "No running containers found");
return;
}
logger.log('note', `Found ${runningCount} running container(s) across ${statuses.length} project(s)`);
logger.log(
"note",
`Found ${runningCount} running container(s) across ${statuses.length} project(s)`,
);
console.log();
// Show what will be stopped
for (const project of statuses) {
const runningContainers = project.containers.filter(c => c.status === 'running');
const runningContainers = project.containers.filter(
(c) => c.status === "running",
);
if (runningContainers.length > 0) {
logger.log('info', `${project.projectName}:`);
logger.log("info", `${project.projectName}:`);
for (const container of runningContainers) {
logger.log('info', `${container.name}`);
logger.log("info", `${container.name}`);
}
}
}
console.log();
const shouldContinue = await plugins.smartinteract.SmartInteract.getCliConfirmation(
'Stop all containers?',
false
const shouldContinue =
await plugins.smartinteract.SmartInteract.getCliConfirmation(
"Stop all containers?",
false,
);
if (!shouldContinue) {
logger.log('note', 'Cancelled');
logger.log("note", "Cancelled");
return;
}
logger.log('note', 'Stopping all containers...');
logger.log("note", "Stopping all containers...");
const result = await globalRegistry.stopAll();
if (result.stopped.length > 0) {
logger.log('ok', `Stopped: ${result.stopped.join(', ')}`);
logger.log("ok", `Stopped: ${result.stopped.join(", ")}`);
}
if (result.failed.length > 0) {
logger.log('error', `Failed to stop: ${result.failed.join(', ')}`);
logger.log("error", `Failed to stop: ${result.failed.join(", ")}`);
}
}
async function handleGlobalCleanup(globalRegistry: GlobalRegistry) {
helpers.printHeader('Cleanup Registry (Global)');
helpers.printHeader("Cleanup Registry (Global)");
logger.log('note', 'Checking for stale registry entries...');
logger.log("note", "Checking for stale registry entries...");
const removed = await globalRegistry.cleanup();
if (removed.length === 0) {
logger.log('ok', 'No stale entries found');
logger.log("ok", "No stale entries found");
return;
}
logger.log('ok', `Removed ${removed.length} stale entr${removed.length === 1 ? 'y' : 'ies'}:`);
logger.log(
"ok",
`Removed ${removed.length} stale entr${removed.length === 1 ? "y" : "ies"}:`,
);
for (const path of removed) {
logger.log('info', `${path}`);
logger.log("info", `${path}`);
}
}
+191 -58
View File
@@ -1,91 +1,224 @@
/* -----------------------------------------------
* executes as standard task
* ----------------------------------------------- */
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import * as plugins from "./mod.plugins.js";
import * as paths from "../paths.js";
import type { ICliMode } from "../helpers.climode.js";
import { getCliMode, printJson } from "../helpers.climode.js";
import { logger } from '../gitzone.logging.js';
import { logger } from "../gitzone.logging.js";
export let run = async () => {
console.log('');
console.log('╭─────────────────────────────────────────────────────────────╮');
console.log('│ gitzone - Development Workflow CLI │');
console.log('╰─────────────────────────────────────────────────────────────╯');
console.log('');
type ICommandHelpSummary = {
name: string;
description: string;
};
const commandSummaries: ICommandHelpSummary[] = [
{
name: "commit",
description:
"Create semantic commits or generate read-only commit recommendations",
},
{ name: "format", description: "Plan or apply project formatting changes" },
{ name: "config", description: "Read and change .smartconfig.json settings" },
{ name: "services", description: "Manage or configure development services" },
{ name: "template", description: "Create a project from a template" },
{ name: "open", description: "Open project assets and CI pages" },
{ name: "docker", description: "Run Docker-related maintenance tasks" },
{
name: "deprecate",
description: "Deprecate npm packages across registries",
},
{ name: "meta", description: "Run meta-repository commands" },
{ name: "start", description: "Prepare a project for local work" },
{ name: "helpers", description: "Run helper utilities" },
];
export let run = async (argvArg: any = {}) => {
const mode = await getCliMode(argvArg);
const requestedCommandHelp =
argvArg._?.[0] === "help" ? argvArg._?.[1] : undefined;
if (mode.help || requestedCommandHelp) {
await showHelp(mode, requestedCommandHelp);
return;
}
if (!mode.interactive) {
await showHelp(mode);
return;
}
console.log("");
console.log(
"╭─────────────────────────────────────────────────────────────╮",
);
console.log(
"│ gitzone - Development Workflow CLI │",
);
console.log(
"╰─────────────────────────────────────────────────────────────╯",
);
console.log("");
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: 'list',
name: 'action',
message: 'What would you like to do?',
default: 'commit',
type: "list",
name: "action",
message: "What would you like to do?",
default: "commit",
choices: [
{ name: 'Commit changes (semantic versioning)', value: 'commit' },
{ name: 'Format project files', value: 'format' },
{ name: 'Configure release settings', value: 'config' },
{ name: 'Create from template', value: 'template' },
{ name: 'Manage dev services (MongoDB, S3)', value: 'services' },
{ name: 'Open project assets', value: 'open' },
{ name: 'Show help', value: 'help' },
{ name: "Commit changes (semantic versioning)", value: "commit" },
{ name: "Format project files", value: "format" },
{ name: "Configure release settings", value: "config" },
{ name: "Create from template", value: "template" },
{ name: "Manage dev services (MongoDB, S3)", value: "services" },
{ name: "Open project assets", value: "open" },
{ name: "Show help", value: "help" },
],
});
const action = (response as any).value;
switch (action) {
case 'commit': {
const modCommit = await import('../mod_commit/index.js');
await modCommit.run({ _: ['commit'] });
case "commit": {
const modCommit = await import("../mod_commit/index.js");
await modCommit.run({ _: ["commit"] });
break;
}
case 'format': {
const modFormat = await import('../mod_format/index.js');
case "format": {
const modFormat = await import("../mod_format/index.js");
await modFormat.run({ interactive: true });
break;
}
case 'config': {
const modConfig = await import('../mod_config/index.js');
await modConfig.run({ _: ['config'] });
case "config": {
const modConfig = await import("../mod_config/index.js");
await modConfig.run({ _: ["config"] });
break;
}
case 'template': {
const modTemplate = await import('../mod_template/index.js');
await modTemplate.run({ _: ['template'] });
case "template": {
const modTemplate = await import("../mod_template/index.js");
await modTemplate.run({ _: ["template"] });
break;
}
case 'services': {
const modServices = await import('../mod_services/index.js');
await modServices.run({ _: ['services'] });
case "services": {
const modServices = await import("../mod_services/index.js");
await modServices.run({ _: ["services"] });
break;
}
case 'open': {
const modOpen = await import('../mod_open/index.js');
await modOpen.run({ _: ['open'] });
case "open": {
const modOpen = await import("../mod_open/index.js");
await modOpen.run({ _: ["open"] });
break;
}
case 'help':
showHelp();
case "help":
await showHelp(mode);
break;
}
};
function showHelp(): void {
console.log('');
console.log('Usage: gitzone <command> [options]');
console.log('');
console.log('Commands:');
console.log(' commit Create a semantic commit with versioning');
console.log(' format Format and standardize project files');
console.log(' config Manage release registry configuration');
console.log(' template Create a new project from template');
console.log(' services Manage dev services (MongoDB, S3/MinIO)');
console.log(' open Open project assets (GitLab, npm, etc.)');
console.log(' docker Docker-related operations');
console.log(' deprecate Deprecate a package on npm');
console.log(' meta Run meta commands');
console.log(' start Start working on a project');
console.log(' helpers Run helper utilities');
console.log('');
console.log('Run gitzone <command> --help for more information on a command.');
console.log('');
export async function showHelp(
mode: ICliMode,
commandName?: string,
): Promise<void> {
if (commandName) {
const handled = await showCommandHelp(commandName, mode);
if (handled) {
return;
}
}
if (mode.json) {
printJson({
name: "gitzone",
usage: "gitzone <command> [options]",
commands: commandSummaries,
globalFlags: [
{ flag: "--help, -h", description: "Show help output" },
{
flag: "--json",
description: "Emit machine-readable JSON when supported",
},
{
flag: "--plain",
description: "Use plain text output when supported",
},
{
flag: "--agent",
description: "Prefer non-interactive machine-friendly output",
},
{
flag: "--no-interactive",
description: "Disable prompts and interactive menus",
},
{
flag: "--no-check-updates",
description: "Skip the update check banner",
},
],
});
return;
}
console.log("");
console.log("Usage: gitzone <command> [options]");
console.log("");
console.log("Commands:");
for (const commandSummary of commandSummaries) {
console.log(
` ${commandSummary.name.padEnd(11)} ${commandSummary.description}`,
);
}
console.log("");
console.log("Global flags:");
console.log(" --help, -h Show help output");
console.log(
" --json Emit machine-readable JSON when supported",
);
console.log(" --plain Use plain text output when supported");
console.log(
" --agent Prefer non-interactive machine-friendly output",
);
console.log(" --no-interactive Disable prompts and interactive menus");
console.log(" --no-check-updates Skip the update check banner");
console.log("");
console.log("Examples:");
console.log(" gitzone help commit");
console.log(" gitzone config show --json");
console.log(" gitzone commit recommend --json");
console.log(" gitzone format plan --json");
console.log(" gitzone services set mongodb,minio");
console.log("");
console.log("Run gitzone <command> --help for command-specific usage.");
console.log("");
}
async function showCommandHelp(
commandName: string,
mode: ICliMode,
): Promise<boolean> {
switch (commandName) {
case "commit": {
const modCommit = await import("../mod_commit/index.js");
modCommit.showHelp(mode);
return true;
}
case "config": {
const modConfig = await import("../mod_config/index.js");
modConfig.showHelp(mode);
return true;
}
case "format": {
const modFormat = await import("../mod_format/index.js");
modFormat.showHelp(mode);
return true;
}
case "services": {
const modServices = await import("../mod_services/index.js");
modServices.showHelp(mode);
return true;
}
default:
return false;
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
import * as smartlog from '@push.rocks/smartlog';
import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
import * as npmextra from '@push.rocks/npmextra';
import * as smartconfig from '@push.rocks/smartconfig';
import * as path from 'path';
import * as projectinfo from '@push.rocks/projectinfo';
import * as smartcli from '@push.rocks/smartcli';
@@ -20,7 +20,7 @@ export const smartfs = new SmartFs(new SmartFsProviderNode());
export {
smartlog,
smartlogDestinationLocal,
npmextra,
smartconfig,
path,
projectinfo,
smartcli,
-2
View File
@@ -1,7 +1,5 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",