Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb9f717d54 | |||
| 70be11894c | |||
| 89ab63b153 | |||
| 44c193d4a8 | |||
| 44d259a0ae | |||
| f0adff8784 | |||
| fb453e62c3 | |||
| 001721a8e9 | |||
| b191464ff9 | |||
| 4d7eaa238f | |||
| 601e0d1063 | |||
| 4bb1a2f8c7 | |||
| b506bf8785 | |||
| d5fbeb3fc6 | |||
| 2ecdeff3dc | |||
| 5a663ae767 | |||
| 218c84a39b | |||
| 27d5cdca35 | |||
| 3ebf072bfb | |||
| 08f56ae0a4 | |||
| b2d2684895 | |||
| 1b328c3045 | |||
| f444a04876 | |||
| 6bd2d35992 | |||
| d038b1d681 | |||
| d60de5cef7 | |||
| e1076a60ad | |||
| 6deba06443 | |||
| 00cd9dc8e7 | |||
| 6b0941eea9 | |||
| 7348567a62 | |||
| ccdca55c9a | |||
| 6c62f80c57 | |||
| 7bb2f65669 | |||
| 48c4b0c9b2 | |||
| 106b72748c | |||
| 03bf82d8ee | |||
| 6a936603bd | |||
| 2a91662e63 | |||
| ea0c026c7e | |||
| d508e1d06c | |||
| 27f2d265de | |||
| af3e15e922 | |||
| b44624f2e7 | |||
| 847e679e92 | |||
| ddf5023ecb | |||
| e1d28bc10a | |||
| 2f3d67f9e3 | |||
| 6304953234 | |||
| 8d84620bc4 | |||
| efd6f04e63 |
@@ -6,19 +6,19 @@ on:
|
|||||||
- '**'
|
- '**'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${-{gitea.repository_owner}-}:${-{secrets.GITEA_TOKEN}-}@{{module.githost}}/${-{gitea.repository}-}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${-{secrets.NPMCI_TOKEN_NPM}-}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${-{secrets.NPMCI_TOKEN_NPM2}-}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
NPMCI_GIT_GITHUBTOKEN: ${-{secrets.NPMCI_GIT_GITHUBTOKEN}-}
|
||||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
NPMCI_URL_CLOUDLY: ${-{secrets.NPMCI_URL_CLOUDLY}-}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
security:
|
security:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
container:
|
container:
|
||||||
image: ${{ env.IMAGE }}
|
image: ${-{ env.IMAGE }-}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -44,11 +44,11 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
test:
|
test:
|
||||||
if: ${{ always() }}
|
if: ${-{ always() }-}
|
||||||
needs: security
|
needs: security
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: ${{ env.IMAGE }}
|
image: ${-{ env.IMAGE }-}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ on:
|
|||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${-{gitea.repository_owner}-}:${-{secrets.GITEA_TOKEN}-}@{{module.githost}}/${-{gitea.repository}-}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${-{secrets.NPMCI_TOKEN_NPM}-}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${-{secrets.NPMCI_TOKEN_NPM2}-}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
NPMCI_GIT_GITHUBTOKEN: ${-{secrets.NPMCI_GIT_GITHUBTOKEN}-}
|
||||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
NPMCI_URL_CLOUDLY: ${-{secrets.NPMCI_URL_CLOUDLY}-}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
security:
|
security:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
container:
|
container:
|
||||||
image: ${{ env.IMAGE }}
|
image: ${-{ env.IMAGE }-}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -42,11 +42,11 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
test:
|
test:
|
||||||
if: ${{ always() }}
|
if: ${-{ always() }-}
|
||||||
needs: security
|
needs: security
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: ${{ env.IMAGE }}
|
image: ${-{ env.IMAGE }-}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: ${{ env.IMAGE }}
|
image: ${-{ env.IMAGE }-}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -95,7 +95,7 @@ jobs:
|
|||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: ${{ env.IMAGE }}
|
image: ${-{ env.IMAGE }-}
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -16,6 +16,10 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
dist_*/
|
dist_*/
|
||||||
|
|
||||||
|
# AI
|
||||||
|
.claude/
|
||||||
|
.serena/
|
||||||
|
|
||||||
#------# custom
|
#------# custom
|
||||||
.serena
|
.serena
|
||||||
test-output.json
|
test-output.json
|
||||||
|
|||||||
@@ -17,12 +17,10 @@ fileName: package.json
|
|||||||
"buildDocs": "(tsdoc)"
|
"buildDocs": "(tsdoc)"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.25",
|
"@git.zone/tsbuild": "^3.1.2",
|
||||||
"@git.zone/tsbundle": "^2.0.5",
|
"@git.zone/tsrun": "^2.0.0",
|
||||||
"@git.zone/tsrun": "^1.2.46",
|
"@git.zone/tstest": "^3.1.3",
|
||||||
"@git.zone/tstest": "^1.0.44",
|
"@types/node": "^24.10.1"
|
||||||
"@push.rocks/tapbundle": "^5.0.15",
|
|
||||||
"@types/node": "^20.8.7"
|
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as {{module.name}} from '../ts/index.js'
|
import * as {{module.name}} from '../ts/index.js'
|
||||||
|
|
||||||
tap.test('first test', async () => {
|
tap.test('first test', async () => {
|
||||||
console.log({{module.name}})
|
console.log({{module.name}})
|
||||||
})
|
})
|
||||||
|
|
||||||
tap.start()
|
export default tap.start()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"gitzone": {
|
"@git.zone/cli": {
|
||||||
"projectType": "{{projectType}}",
|
"projectType": "{{projectType}}",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "{{module.githost}}",
|
"githost": "{{module.githost}}",
|
||||||
@@ -9,10 +9,12 @@
|
|||||||
"npmPackagename": "{{module.npmPackagename}}",
|
"npmPackagename": "{{module.npmPackagename}}",
|
||||||
"license": "{{module.license}}",
|
"license": "{{module.license}}",
|
||||||
"projectDomain": "{{module.projectDomain}}"
|
"projectDomain": "{{module.projectDomain}}"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"accessLevel": "{{module.npmAccessLevel}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npmci": {
|
"@ship.zone/szci": {
|
||||||
"npmGlobalTools": [],
|
"npmGlobalTools": []
|
||||||
"npmAccessLevel": "{{module.npmAccessLevel}}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,18 +17,18 @@ fileName: package.json
|
|||||||
"build": "(tsbuild --web --allowimplicitany)"
|
"build": "(tsbuild --web --allowimplicitany)"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.17",
|
"@git.zone/tsbuild": "^3.1.2",
|
||||||
"@git.zone/tsrun": "^1.2.8",
|
"@git.zone/tsrun": "^2.0.0",
|
||||||
"@git.zone/tstest": "^1.0.28",
|
"@git.zone/tstest": "^3.1.3",
|
||||||
"@git.zone/tswatch": "^2.0.1",
|
"@git.zone/tswatch": "^2.0.1",
|
||||||
"@push.rocks/tapbundle": "^5.5.4"
|
"@types/node": "^24.10.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedserver": "^3.0.53",
|
"@api.global/typedserver": "^3.0.53",
|
||||||
"@push.rocks/projectinfo": "^5.0.1",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/qenv": "^6.1.0",
|
"@push.rocks/qenv": "^6.1.0",
|
||||||
"@push.rocks/smartdata": "^5.0.7",
|
"@push.rocks/smartdata": "^5.0.7",
|
||||||
"@push.rocks/smartpath": "^5.0.5",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartstate": "^2.0.0"
|
"@push.rocks/smartstate": "^2.0.0"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"gitzone": {
|
"@git.zone/cli": {
|
||||||
"projectType": "{{projectType}}",
|
"projectType": "{{projectType}}",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "{{module.githost}}",
|
"githost": "{{module.githost}}",
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"projectDomain": "{{module.projectDomain}}"
|
"projectDomain": "{{module.projectDomain}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npmci": {
|
"@ship.zone/szci": {
|
||||||
"npmGlobalTools": [],
|
"npmGlobalTools": [],
|
||||||
"dockerRegistryRepoMap": {
|
"dockerRegistryRepoMap": {
|
||||||
"registry.gitlab.com": "{{dockerTargetImagePath}}"
|
"registry.gitlab.com": "{{dockerTargetImagePath}}"
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"useDefineForClassFields": false,
|
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"gitzone": {
|
"@git.zone/cli": {
|
||||||
"projectType": "website",
|
"projectType": "website",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "{{module.githost}}",
|
"githost": "{{module.githost}}",
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"projectDomain": "{{module.projectDomain}}"
|
"projectDomain": "{{module.projectDomain}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npmci": {
|
"@ship.zone/szci": {
|
||||||
"npmGlobalTools": [],
|
"npmGlobalTools": [],
|
||||||
"dockerRegistryRepoMap": {
|
"dockerRegistryRepoMap": {
|
||||||
"registry.gitlab.com": "{{docker.registryImageTag}}"
|
"registry.gitlab.com": "{{docker.registryImageTag}}"
|
||||||
|
|||||||
222
changelog.md
222
changelog.md
@@ -1,6 +1,199 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
- CLI change: pass argvArg.diff into the options so the --diff flag is honored by the format command.
|
||||||
|
- When diff is enabled, run formatter.check() for each active formatter and call displayAllDiffs() for those with differences, with informational logging.
|
||||||
|
- 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).
|
||||||
|
- Export CommitConfig from mod_config for use by the CLI.
|
||||||
|
- Add 'gitzone config commit' subcommand with interactive and direct-setting modes (alwaysTest, alwaysBuild).
|
||||||
|
- Merge CLI flags and npmextra config: -t/--test and -b/--build now respect commit.alwaysTest and commit.alwaysBuild.
|
||||||
|
- Run 'pnpm test' early in the commit flow when tests are enabled; abort the commit on failing tests and log results.
|
||||||
|
- Update commit UI/plan to show the test option and include the test step when enabled.
|
||||||
|
- 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.
|
||||||
|
- Extend runFormatter API with new options: write (use to apply changes), checkOnly (only check for diffs), and showDiff (display diffs). When checkOnly is used, runFormatter returns an ICheckResult.
|
||||||
|
- Change default formatting behavior to dry-run. Use --write / -w to actually apply changes. CLI format command updated to respect --write/-w.
|
||||||
|
- Add formatNpmextraWithDiff in mod_config to preview diffs for npmextra.json and prompt the user before applying changes; calls to add/remove/clear registries and set access level now use this preview flow.
|
||||||
|
- Project.fromCwd now accepts an options object ({ requireProjectType?: boolean }) so callers can skip the projectType requirement when appropriate; runFormatter no longer requires projectType for certain formatters.
|
||||||
|
- Introduce a list of formatters that don't require projectType: npmextra, prettier, cleanup, packagejson.
|
||||||
|
- Export the ICheckResult type from the formatter module and update mod_format interfaces to include ICheckResult.
|
||||||
|
- 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
|
||||||
|
- Remove the misplaced "gitzone" object that previously contained the TSDoc legal block
|
||||||
|
- Reorder and properly nest the "@git.zone/cli" configuration to follow the new "tsdoc" section
|
||||||
|
- 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)
|
||||||
|
- New printExecutionPlan(options) added to ts/mod_commit/mod.ui.ts
|
||||||
|
- Commit command now calls ui.printExecutionPlan(...) before running format or other steps
|
||||||
|
- 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.
|
||||||
|
- Add routing to commands (commit, format, config, template, services, open, help) using dynamic imports.
|
||||||
|
- Add showHelp() helper to display usage and available commands.
|
||||||
|
- 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
|
||||||
|
- Verify the working tree is clean after the build and abort the release if build produces uncommitted changes
|
||||||
|
- Increase total step counting to include build and verification steps in the UI progress output
|
||||||
|
- Add a runFormatter utility to the formatting module to execute a single formatter programmatically
|
||||||
|
- Wire runFormatter('npmextra') into mod_config so npmextra.json is formatted automatically after add/remove/clear/access operations
|
||||||
|
- 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'.
|
||||||
|
- Add explicit 'help' subcommand and log an error for unknown commands before showing help.
|
||||||
|
- Update devDependencies: @git.zone/tsbuild -> ^4.0.2, @types/node -> ^25.0.2.
|
||||||
|
- 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).
|
||||||
|
- Only auto-accept recommendations when -y/--yes is provided and the change is not breaking.
|
||||||
|
- When --yes is provided but the recommendation is a breaking change, log a warning and fall back to interactive confirmation.
|
||||||
|
- 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)
|
||||||
|
- services CLI: improved start/stop/restart flows, better logging/help output and enhanced global commands (list/status/stop/cleanup)
|
||||||
|
- templates/tests: replace @push.rocks/tapbundle with @git.zone/tstest and update template test.ts to export default tap.start()
|
||||||
|
- format: stop auto-updating tslint template and mark @push.rocks/tapbundle as deprecated in package formatting logic
|
||||||
|
- dependencies: bump @git.zone/tsbuild, @git.zone/tsrun, @git.zone/tstest, @git.zone/tsdoc, @push.rocks/projectinfo, @push.rocks/smartpath, @push.rocks/smartfs, prettier and other dev deps
|
||||||
|
- 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)
|
||||||
|
- Add global CLI mode for services (use -g/--global) with commands: list, status, stop, cleanup (ts/mod_services/index.ts)
|
||||||
|
- ServiceManager now registers the current project with the global registry when starting services and unregisters when all containers are removed (ts/mod_services/classes.servicemanager.ts)
|
||||||
|
- Global handlers to list projects, show aggregated status, stop containers across projects and cleanup stale entries
|
||||||
|
- 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.
|
||||||
|
- 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.
|
||||||
|
- 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)
|
## 2025-11-17 - 1.21.3 - fix(assets/templates/multienv)
|
||||||
|
|
||||||
Remove unused Bun configuration template (assets/templates/multienv/bunfig.toml)
|
Remove unused Bun configuration template (assets/templates/multienv/bunfig.toml)
|
||||||
|
|
||||||
- Deleted assets/templates/multienv/bunfig.toml which previously provided Bun TypeScript decorator configuration
|
- Deleted assets/templates/multienv/bunfig.toml which previously provided Bun TypeScript decorator configuration
|
||||||
@@ -8,17 +201,20 @@ Remove unused Bun configuration template (assets/templates/multienv/bunfig.toml)
|
|||||||
- No functional code changes; removes an unused asset file
|
- No functional code changes; removes an unused asset file
|
||||||
|
|
||||||
## 2025-11-17 - 1.21.2 - fix(templates/multienv)
|
## 2025-11-17 - 1.21.2 - fix(templates/multienv)
|
||||||
|
|
||||||
Disable useDefineForClassFields in multienv TypeScript configs to ensure decorator compatibility
|
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/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
|
- 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)
|
## 2025-11-17 - 1.21.1 - fix(templates.multienv)
|
||||||
|
|
||||||
Enable checkJs in multienv Deno template to enable JS type checking
|
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
|
- 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)
|
## 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
|
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
|
- Added assets/templates/multienv/bunfig.toml to enable Bun TypeScript transpiler experimentalDecorators
|
||||||
@@ -26,6 +222,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)
|
- 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)
|
## 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 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
|
- Add -y / --yes flag to gitzone commit to auto-accept AI-generated commit recommendations without interactive prompts
|
||||||
@@ -35,6 +232,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
|
- Updated CLI usage and documentation (readme.hints.md) to document the new flags
|
||||||
|
|
||||||
## 2025-11-05 - 1.19.9 - fix(mod_commit)
|
## 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
|
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
|
- Removed legacy npm/deno-specific helpers (bumpNpmVersion, syncVersionToDenoJson, bumpDenoVersion) that relied on executing npm and caused warning pollution
|
||||||
@@ -45,39 +243,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
|
- 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)
|
## 2025-11-04 - 1.19.8 - fix(package.json)
|
||||||
|
|
||||||
Bump @git.zone/tsdoc dependency to ^1.9.2
|
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
|
- Updated dependency @git.zone/tsdoc from ^1.9.1 to ^1.9.2 in package.json
|
||||||
|
|
||||||
## 2025-11-04 - 1.19.7 - fix(dependencies)
|
## 2025-11-04 - 1.19.7 - fix(dependencies)
|
||||||
|
|
||||||
Bump @git.zone/tsdoc to ^1.9.1
|
Bump @git.zone/tsdoc to ^1.9.1
|
||||||
|
|
||||||
- Updated package.json dependency @git.zone/tsdoc from ^1.9.0 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)
|
## 2025-11-04 - 1.19.6 - fix(cli)
|
||||||
|
|
||||||
Bump @git.zone/tsdoc dependency to ^1.9.0
|
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
|
- Updated dependency @git.zone/tsdoc from ^1.8.3 to ^1.9.0 in package.json
|
||||||
|
|
||||||
## 2025-11-04 - 1.19.5 - fix(cli)
|
## 2025-11-04 - 1.19.5 - fix(cli)
|
||||||
|
|
||||||
Bump @git.zone/tsdoc to ^1.8.3 and add local .claude settings for allowed permissions
|
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
|
- 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)
|
- 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)
|
## 2025-11-03 - 1.19.3 - fix(tsdoc)
|
||||||
|
|
||||||
Bump @git.zone/tsdoc to ^1.8.0 and add .claude local settings
|
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
|
- 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
|
- Add .claude/settings.local.json for local assistant permissions/configuration
|
||||||
|
|
||||||
## 2025-11-03 - 1.19.2 - fix(tsdoc)
|
## 2025-11-03 - 1.19.2 - fix(tsdoc)
|
||||||
|
|
||||||
Bump @git.zone/tsdoc to ^1.6.1 and add .claude/settings.local.json
|
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
|
- Update dependency @git.zone/tsdoc from ^1.6.0 to ^1.6.1
|
||||||
- Add .claude/settings.local.json to include local Claude settings/permissions
|
- Add .claude/settings.local.json to include local Claude settings/permissions
|
||||||
|
|
||||||
## 2025-11-02 - 1.19.1 - fix(dependencies)
|
## 2025-11-02 - 1.19.1 - fix(dependencies)
|
||||||
|
|
||||||
Bump dependencies and add local Claude settings
|
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
|
- Bump devDependencies: @git.zone/tsbuild -> ^2.7.1, @git.zone/tsrun -> ^1.6.2, @git.zone/tstest -> ^2.7.0
|
||||||
@@ -85,6 +290,7 @@ Bump dependencies and add local Claude settings
|
|||||||
- Add .claude/settings.local.json (local project permissions/settings file)
|
- Add .claude/settings.local.json (local project permissions/settings file)
|
||||||
|
|
||||||
## 2025-10-23 - 1.19.0 - feat(mod_commit)
|
## 2025-10-23 - 1.19.0 - feat(mod_commit)
|
||||||
|
|
||||||
Add CLI UI helpers and improve commit workflow with progress, recommendations and summary
|
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).
|
- 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).
|
||||||
@@ -93,6 +299,7 @@ Add CLI UI helpers and improve commit workflow with progress, recommendations an
|
|||||||
- Add .claude/settings.local.json: local permissions configuration for development tooling.
|
- Add .claude/settings.local.json: local permissions configuration for development tooling.
|
||||||
|
|
||||||
## 2025-10-23 - 1.18.9 - fix(mod_commit)
|
## 2025-10-23 - 1.18.9 - fix(mod_commit)
|
||||||
|
|
||||||
Stage and commit deno.json when bumping/syncing versions and create/update git tags
|
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
|
- 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
|
||||||
@@ -100,6 +307,7 @@ Stage and commit deno.json when bumping/syncing versions and create/update git t
|
|||||||
- Added informative logger messages after creating commits and tags
|
- Added informative logger messages after creating commits and tags
|
||||||
|
|
||||||
## 2025-10-23 - 1.18.8 - fix(mod_commit)
|
## 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
|
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()
|
- Add mod_commit/mod.helpers.ts with utilities: detectCurrentBranch(), detectProjectType(), bumpProjectVersion(), bumpDenoVersion(), bumpNpmVersion(), syncVersionToDenoJson(), and calculateNewVersion()
|
||||||
@@ -109,12 +317,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
|
- 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)
|
## 2025-09-07 - 1.18.7 - fix(claude)
|
||||||
|
|
||||||
Add .claude local settings to whitelist dev tool permissions
|
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).
|
- 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.
|
- Disable enableAllProjectMcpServers (set to false) to limit automatic project MCP server usage.
|
||||||
|
|
||||||
## 2025-09-07 - 1.18.6 - fix(deps)
|
## 2025-09-07 - 1.18.6 - fix(deps)
|
||||||
|
|
||||||
Bump dependency versions and add local Claude settings
|
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
|
- 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
|
||||||
@@ -122,6 +332,7 @@ Bump dependency versions and add local Claude settings
|
|||||||
- Added .claude/settings.local.json to configure local Claude permissions/settings
|
- Added .claude/settings.local.json to configure local Claude permissions/settings
|
||||||
|
|
||||||
## 2025-08-17 - 1.18.5 - fix(dependencies)
|
## 2025-08-17 - 1.18.5 - fix(dependencies)
|
||||||
|
|
||||||
Bump smartshell and smartscaf versions; add .claude local settings
|
Bump smartshell and smartscaf versions; add .claude local settings
|
||||||
|
|
||||||
- Update @push.rocks/smartshell from ^3.2.4 to ^3.3.0 in package.json
|
- Update @push.rocks/smartshell from ^3.2.4 to ^3.3.0 in package.json
|
||||||
@@ -129,6 +340,7 @@ Bump smartshell and smartscaf versions; add .claude local settings
|
|||||||
- Add .claude/settings.local.json for local assistant permissions/configuration
|
- Add .claude/settings.local.json for local assistant permissions/configuration
|
||||||
|
|
||||||
## 2025-08-17 - 1.18.4 - fix(cli)
|
## 2025-08-17 - 1.18.4 - fix(cli)
|
||||||
|
|
||||||
Update dependencies, add local Claude settings, and update gitignore template
|
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
|
- 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
|
||||||
@@ -137,6 +349,7 @@ Update dependencies, add local Claude settings, and update gitignore template
|
|||||||
- Add pnpm onlyBuiltDependencies entries: esbuild and mongodb-memory-server
|
- Add pnpm onlyBuiltDependencies entries: esbuild and mongodb-memory-server
|
||||||
|
|
||||||
## 2025-08-16 - 1.18.3 - fix(services)
|
## 2025-08-16 - 1.18.3 - fix(services)
|
||||||
|
|
||||||
Simplify S3 endpoint handling in ServiceConfiguration to store host only
|
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.
|
- S3_ENDPOINT now stores the raw host (e.g. 'localhost') instead of a full URL with protocol and port.
|
||||||
@@ -145,6 +358,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.
|
- 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)
|
## 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
|
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
|
- Ensure AiDoc is stopped after building commit recommendation to avoid resource leaks
|
||||||
@@ -156,6 +370,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
|
- Add local Claude settings file (.claude/settings.local.json) with development permissions
|
||||||
|
|
||||||
## 2025-08-16 - 1.18.0 - feat(services)
|
## 2025-08-16 - 1.18.0 - feat(services)
|
||||||
|
|
||||||
Add Docker port mapping sync and reconfigure workflow for local services
|
Add Docker port mapping sync and reconfigure workflow for local services
|
||||||
|
|
||||||
- Add getPortMappings to DockerContainer to extract port bindings from docker inspect output
|
- Add getPortMappings to DockerContainer to extract port bindings from docker inspect output
|
||||||
@@ -168,6 +383,7 @@ Add Docker port mapping sync and reconfigure workflow for local services
|
|||||||
- Add .claude/settings.local.json (local permissions config) to repository
|
- Add .claude/settings.local.json (local permissions config) to repository
|
||||||
|
|
||||||
## 2025-08-15 - 1.17.5 - fix(services)
|
## 2025-08-15 - 1.17.5 - fix(services)
|
||||||
|
|
||||||
Update S3 credentials naming and add S3_ENDPOINT/S3_USESSL support for improved MinIO integration
|
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
|
- Replaced S3_USER/S3_PASS with S3_ACCESSKEY/S3_SECRETKEY in ServiceConfiguration
|
||||||
@@ -177,6 +393,7 @@ Update S3 credentials naming and add S3_ENDPOINT/S3_USESSL support for improved
|
|||||||
- Added .claude/settings.local.json for local permission settings
|
- Added .claude/settings.local.json for local permission settings
|
||||||
|
|
||||||
## 2025-08-15 - 1.17.4 - fix(services)
|
## 2025-08-15 - 1.17.4 - fix(services)
|
||||||
|
|
||||||
Update S3 credentials naming and add S3_ENDPOINT/S3_USESSL support for improved MinIO integration
|
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
|
- Replaced S3_USER/S3_PASS with S3_ACCESSKEY/S3_SECRETKEY in ServiceConfiguration
|
||||||
@@ -185,12 +402,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
|
- Updated ServiceManager to use new credential names in container setup and logging
|
||||||
|
|
||||||
## 2025-08-15 - 1.17.3 - fix(serviceconfig)
|
## 2025-08-15 - 1.17.3 - fix(serviceconfig)
|
||||||
|
|
||||||
Update service configuration to include dynamic MongoDB connection string and add local permissions settings
|
Update service configuration to include dynamic MongoDB connection string and add local permissions settings
|
||||||
|
|
||||||
- Added .claude/settings.local.json for local permissions configuration
|
- Added .claude/settings.local.json for local permissions configuration
|
||||||
- Updated ServiceConfiguration to compute and update MONGODB_URL based on current config values
|
- Updated ServiceConfiguration to compute and update MONGODB_URL based on current config values
|
||||||
|
|
||||||
## 2025-08-15 - 1.17.2 - fix(ci-test-services)
|
## 2025-08-15 - 1.17.2 - fix(ci-test-services)
|
||||||
|
|
||||||
Update CI/CD configurations, test settings, and Docker service for MongoDB.
|
Update CI/CD configurations, test settings, and Docker service for MongoDB.
|
||||||
|
|
||||||
- Add .claude/settings.local.json with updated permission settings
|
- Add .claude/settings.local.json with updated permission settings
|
||||||
@@ -199,6 +418,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
|
- Fix MongoDB Docker container command by adding '--bind_ip_all' for proper network binding
|
||||||
|
|
||||||
## 2025-08-15 - 1.17.1 - fix(services)
|
## 2025-08-15 - 1.17.1 - fix(services)
|
||||||
|
|
||||||
Improve services module logging and enhance MongoDB Compass integration
|
Improve services module logging and enhance MongoDB Compass integration
|
||||||
|
|
||||||
- Refactored services module to use centralized logger from gitzone.logging.ts
|
- Refactored services module to use centralized logger from gitzone.logging.ts
|
||||||
@@ -207,6 +427,7 @@ Improve services module logging and enhance MongoDB Compass integration
|
|||||||
- Consistent logging across all service commands
|
- Consistent logging across all service commands
|
||||||
|
|
||||||
## 2025-08-14 - 1.17.0 - feat(services)
|
## 2025-08-14 - 1.17.0 - feat(services)
|
||||||
|
|
||||||
Add comprehensive development services management for MongoDB and MinIO containers
|
Add comprehensive development services management for MongoDB and MinIO containers
|
||||||
|
|
||||||
- Implemented `gitzone services` command for managing local development services
|
- Implemented `gitzone services` command for managing local development services
|
||||||
@@ -220,6 +441,7 @@ Add comprehensive development services management for MongoDB and MinIO containe
|
|||||||
- Interactive confirmations for destructive operations
|
- Interactive confirmations for destructive operations
|
||||||
|
|
||||||
## 2025-08-08 - 1.16.10 - fix(format)
|
## 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.
|
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
|
- Added mutex locking in ChangeCache and RollbackManager to prevent race conditions during manifest updates
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
"npmAccessLevel": "private",
|
"npmAccessLevel": "private",
|
||||||
"npmRegistryUrl": "verdaccio.lossless.one"
|
"npmRegistryUrl": "verdaccio.lossless.one"
|
||||||
},
|
},
|
||||||
"gitzone": {
|
"tsdoc": {
|
||||||
|
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||||
|
},
|
||||||
|
"@git.zone/cli": {
|
||||||
"projectType": "npm",
|
"projectType": "npm",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "gitlab.com",
|
"githost": "gitlab.com",
|
||||||
@@ -31,9 +34,13 @@
|
|||||||
"Docker",
|
"Docker",
|
||||||
"CI/CD"
|
"CI/CD"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"registries": [
|
||||||
|
"https://verdaccio.lossless.digital",
|
||||||
|
"https://registry.npmjs.org"
|
||||||
|
],
|
||||||
|
"accessLevel": "public"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"tsdoc": {
|
|
||||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
24
package.json
24
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/cli",
|
"name": "@git.zone/cli",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.21.3",
|
"version": "2.12.2",
|
||||||
"description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.",
|
"description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.",
|
||||||
"main": "dist_ts/index.ts",
|
"main": "dist_ts/index.ts",
|
||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(npm run clean && npm run prepareTest && npm run testCli && npm run testFormat && npm run testCommit && npm run testDeprecate && npm run testVersion && npm run testReadme && npm run testUpdate && npm run testTemplateNpm && npm run testTemplateLit) && rm -rf test",
|
"test": "(npm run clean && npm run prepareTest && npm run testCli && npm run testFormat && npm run testCommit && npm run testDeprecate && npm run testVersion && npm run testReadme && npm run testUpdate && npm run testTemplateNpm && npm run testTemplateLit) && rm -rf test",
|
||||||
"build": "(tsbuild --web --allowimplicitany)",
|
"build": "tsbuild tsfolders",
|
||||||
"clean": "(rm -rf test/)",
|
"clean": "(rm -rf test/)",
|
||||||
"prepareTest": "(git clone https://gitlab.com/sandboxzone/sandbox-npmts.git test/)",
|
"prepareTest": "(git clone https://gitlab.com/sandboxzone/sandbox-npmts.git test/)",
|
||||||
"testBuild": "npm run build && rm -r dist/",
|
"testBuild": "npm run build && rm -r dist/",
|
||||||
@@ -57,18 +57,17 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://gitlab.com/gitzone/private/gitzone#readme",
|
"homepage": "https://gitlab.com/gitzone/private/gitzone#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.7.1",
|
"@git.zone/tsbuild": "^4.0.2",
|
||||||
"@git.zone/tsrun": "^1.6.2",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@git.zone/tstest": "^2.7.0",
|
"@git.zone/tstest": "^3.1.3",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"@push.rocks/smartfile": "^11.2.7",
|
|
||||||
"@push.rocks/smartinteract": "^2.0.16",
|
"@push.rocks/smartinteract": "^2.0.16",
|
||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
"@push.rocks/smartnetwork": "^4.4.0",
|
||||||
"@push.rocks/smartshell": "^3.3.0",
|
"@push.rocks/smartshell": "^3.3.0",
|
||||||
"@types/node": "^22.15.18"
|
"@types/node": "^25.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git.zone/tsdoc": "^1.9.2",
|
"@git.zone/tsdoc": "^1.11.3",
|
||||||
"@git.zone/tspublish": "^1.10.3",
|
"@git.zone/tspublish": "^1.10.3",
|
||||||
"@push.rocks/commitinfo": "^1.0.12",
|
"@push.rocks/commitinfo": "^1.0.12",
|
||||||
"@push.rocks/early": "^4.0.4",
|
"@push.rocks/early": "^4.0.4",
|
||||||
@@ -76,11 +75,12 @@
|
|||||||
"@push.rocks/lik": "^6.2.2",
|
"@push.rocks/lik": "^6.2.2",
|
||||||
"@push.rocks/npmextra": "^5.3.3",
|
"@push.rocks/npmextra": "^5.3.3",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/smartchok": "^1.1.1",
|
|
||||||
"@push.rocks/smartcli": "^4.0.19",
|
"@push.rocks/smartcli": "^4.0.19",
|
||||||
"@push.rocks/smartdiff": "^1.0.3",
|
"@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/smartgulp": "^3.0.4",
|
||||||
"@push.rocks/smartjson": "^5.2.0",
|
"@push.rocks/smartjson": "^6.0.0",
|
||||||
"@push.rocks/smartlegal": "^1.0.27",
|
"@push.rocks/smartlegal": "^1.0.27",
|
||||||
"@push.rocks/smartlog": "^3.1.10",
|
"@push.rocks/smartlog": "^3.1.10",
|
||||||
"@push.rocks/smartlog-destination-local": "^9.0.2",
|
"@push.rocks/smartlog-destination-local": "^9.0.2",
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@push.rocks/smartupdate": "^2.0.6",
|
"@push.rocks/smartupdate": "^2.0.6",
|
||||||
"@types/through2": "^2.0.41",
|
"@types/through2": "^2.0.41",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.7.4",
|
||||||
"through2": "^4.0.2"
|
"through2": "^4.0.2"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
4365
pnpm-lock.yaml
generated
4365
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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:
|
The commit module's version bumping has been refactored to eliminate npm command dependencies:
|
||||||
|
|
||||||
**Changes:**
|
**Changes:**
|
||||||
|
|
||||||
- Removed `bumpNpmVersion()` - was causing npm warnings to pollute deno.json
|
- Removed `bumpNpmVersion()` - was causing npm warnings to pollute deno.json
|
||||||
- Removed `syncVersionToDenoJson()` - no longer needed with unified approach
|
- Removed `syncVersionToDenoJson()` - no longer needed with unified approach
|
||||||
- Removed separate `bumpDenoVersion()` - replaced by unified implementation
|
- 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
|
- Unified `bumpProjectVersion()` - handles npm/deno/both with single clean code path
|
||||||
|
|
||||||
**Benefits:**
|
**Benefits:**
|
||||||
|
|
||||||
- No npm warning pollution in version fields
|
- No npm warning pollution in version fields
|
||||||
- Full control over version bumping process
|
- Full control over version bumping process
|
||||||
- Simpler git history (no amending, no force-tagging)
|
- 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:
|
The commit module now supports `-y/--yes` flag for non-interactive commits:
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
- `gitzone commit -y` - Auto-accepts AI recommendations without prompts
|
- `gitzone commit -y` - Auto-accepts AI recommendations without prompts
|
||||||
- `gitzone commit -yp` - Auto-accepts and pushes to origin
|
- `gitzone commit -yp` - Auto-accepts and pushes to origin
|
||||||
- Separate `-p/--push` flag controls push behavior
|
- Separate `-p/--push` flag controls push behavior
|
||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
|
|
||||||
- Creates AnswerBucket programmatically when `-y` flag detected
|
- Creates AnswerBucket programmatically when `-y` flag detected
|
||||||
- Preserves all UI output for transparency
|
- Preserves all UI output for transparency
|
||||||
- Fully backward compatible with interactive mode
|
- Fully backward compatible with interactive mode
|
||||||
@@ -243,7 +247,64 @@ gitzone format --clean-backups
|
|||||||
|
|
||||||
## API Changes
|
## API Changes
|
||||||
|
|
||||||
- smartfile API updated to use fs._ and memory._ namespaces
|
### Smartfile v13 Migration (Latest - Completed)
|
||||||
|
|
||||||
|
The project has been fully migrated from @push.rocks/smartfile v11 to v13, which introduced a major breaking change where filesystem operations were split into two separate packages:
|
||||||
|
|
||||||
|
**Packages:**
|
||||||
|
|
||||||
|
- `@push.rocks/smartfile` v13.0.1 - File representation classes (SmartFile, StreamFile, VirtualDirectory)
|
||||||
|
- `@push.rocks/smartfs` v1.1.0 - Filesystem operations (read, write, exists, stat, etc.)
|
||||||
|
|
||||||
|
**Key API Changes:**
|
||||||
|
|
||||||
|
1. **File Reading**:
|
||||||
|
- Old: `plugins.smartfile.fs.toStringSync(path)` or `plugins.smartfile.fs.toObjectSync(path)`
|
||||||
|
- New: `await plugins.smartfs.file(path).encoding('utf8').read()` + JSON.parse if needed
|
||||||
|
- Important: `read()` returns `string | Buffer` - use `as string` type assertion when encoding is set
|
||||||
|
|
||||||
|
2. **File Writing**:
|
||||||
|
- Old: `plugins.smartfile.memory.toFs(content, path)` or `plugins.smartfile.memory.toFsSync(content, path)`
|
||||||
|
- New: `await plugins.smartfs.file(path).encoding('utf8').write(content)`
|
||||||
|
|
||||||
|
3. **File Existence**:
|
||||||
|
- Old: `plugins.smartfile.fs.fileExists(path)` or `plugins.smartfile.fs.fileExistsSync(path)`
|
||||||
|
- New: `await plugins.smartfs.file(path).exists()`
|
||||||
|
|
||||||
|
4. **Directory Operations**:
|
||||||
|
- Old: `plugins.smartfile.fs.ensureDir(path)`
|
||||||
|
- New: `await plugins.smartfs.directory(path).recursive().create()`
|
||||||
|
- Old: `plugins.smartfile.fs.remove(path)`
|
||||||
|
- New: `await plugins.smartfs.directory(path).recursive().delete()` or `await plugins.smartfs.file(path).delete()`
|
||||||
|
|
||||||
|
5. **Directory Listing**:
|
||||||
|
- Old: `plugins.smartfile.fs.listFolders(path)` or `plugins.smartfile.fs.listFoldersSync(path)`
|
||||||
|
- New: `await plugins.smartfs.directory(path).list()` then filter by `stats.isDirectory`
|
||||||
|
- Note: `list()` returns `IDirectoryEntry[]` with `path` and `name` properties - use `stat()` to check if directory
|
||||||
|
|
||||||
|
6. **File Stats**:
|
||||||
|
- Old: `stats.isDirectory()` (method)
|
||||||
|
- New: `stats.isDirectory` (boolean property)
|
||||||
|
- Old: `stats.mtimeMs`
|
||||||
|
- New: `stats.mtime.getTime()`
|
||||||
|
|
||||||
|
7. **SmartFile Factory**:
|
||||||
|
- Old: Direct SmartFile instantiation
|
||||||
|
- New: `plugins.smartfile.SmartFileFactory.nodeFs()` then factory methods
|
||||||
|
|
||||||
|
**Migration Pattern:**
|
||||||
|
All sync methods must become async. Functions that were previously synchronous (like `getProjectName()`) now return `Promise<T>` and must be awaited.
|
||||||
|
|
||||||
|
**Affected Modules:**
|
||||||
|
|
||||||
|
- ts/mod_format/\* (largest area - 15+ files)
|
||||||
|
- ts/mod_commit/\* (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()`
|
- smartnpm requires instance creation: `new NpmRegistry()`
|
||||||
- All file operations now use updated APIs
|
|
||||||
- Type imports use `import type` for proper verbatim module syntax
|
- Type imports use `import type` for proper verbatim module syntax
|
||||||
|
|||||||
39
readme.md
39
readme.md
@@ -9,6 +9,10 @@
|
|||||||
|
|
||||||
gitzone is a powerful command-line interface that supercharges your development workflow with automated project management, intelligent code formatting, seamless version control, and development service orchestration. Whether you're bootstrapping a new TypeScript project, maintaining code quality, managing complex multi-repository setups, or spinning up local development databases, gitzone has got you covered.
|
gitzone is a powerful command-line interface that supercharges your development workflow with automated project management, intelligent code formatting, seamless version control, and development service orchestration. Whether you're bootstrapping a new TypeScript project, maintaining code quality, managing complex multi-repository setups, or spinning up local development databases, gitzone has got you covered.
|
||||||
|
|
||||||
|
## Issue Reporting and Security
|
||||||
|
|
||||||
|
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||||
|
|
||||||
## 🏃♂️ Quick Start
|
## 🏃♂️ Quick Start
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
@@ -35,13 +39,13 @@ gitzone format
|
|||||||
# Start local MongoDB and MinIO services
|
# Start local MongoDB and MinIO services
|
||||||
gitzone services start
|
gitzone services start
|
||||||
|
|
||||||
# Create a semantic commit
|
# Create a semantic commit with AI-powered suggestions
|
||||||
gitzone commit
|
gitzone commit
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠️ Core Features
|
## 🛠️ Core Features
|
||||||
|
|
||||||
### 🐳 Development Services Management (NEW!)
|
### 🐳 Development Services Management
|
||||||
|
|
||||||
Effortlessly manage local MongoDB and MinIO (S3-compatible) services for your development environment:
|
Effortlessly manage local MongoDB and MinIO (S3-compatible) services for your development environment:
|
||||||
|
|
||||||
@@ -110,7 +114,7 @@ gitzone template [template-name]
|
|||||||
Each template comes pre-configured with:
|
Each template comes pre-configured with:
|
||||||
|
|
||||||
- ✅ TypeScript with modern configurations
|
- ✅ TypeScript with modern configurations
|
||||||
- ✅ Automated testing setup
|
- ✅ Automated testing setup with `@git.zone/tstest`
|
||||||
- ✅ CI/CD pipelines (GitLab/GitHub)
|
- ✅ CI/CD pipelines (GitLab/GitHub)
|
||||||
- ✅ Code formatting and linting
|
- ✅ Code formatting and linting
|
||||||
- ✅ Documentation structure
|
- ✅ Documentation structure
|
||||||
@@ -170,18 +174,27 @@ gitzone format --clean-backups
|
|||||||
- **Gitignore** - Repository ignore rules
|
- **Gitignore** - Repository ignore rules
|
||||||
- **Templates** - Project template updates
|
- **Templates** - Project template updates
|
||||||
- **Npmextra** - Extended npm configurations
|
- **Npmextra** - Extended npm configurations
|
||||||
|
- **Cleanup** - Removes obsolete files (yarn.lock, package-lock.json, tslint.json, etc.)
|
||||||
|
|
||||||
### 🔀 Semantic Commits & Versioning
|
### 🔀 Semantic Commits & Versioning
|
||||||
|
|
||||||
Create standardized commits that automatically handle versioning:
|
Create standardized commits with AI-powered suggestions that automatically handle versioning:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Interactive commit with AI recommendations
|
||||||
gitzone commit
|
gitzone commit
|
||||||
|
|
||||||
|
# Auto-accept AI recommendations
|
||||||
|
gitzone commit -y
|
||||||
|
|
||||||
|
# Auto-accept and push
|
||||||
|
gitzone commit -y -p
|
||||||
```
|
```
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
|
|
||||||
- 📝 Interactive commit message builder
|
- 🤖 **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)
|
- 🏷️ Automatic version bumping (major/minor/patch)
|
||||||
- 📜 Changelog generation
|
- 📜 Changelog generation
|
||||||
- 🚀 Optional auto-push to origin
|
- 🚀 Optional auto-push to origin
|
||||||
@@ -189,11 +202,10 @@ Features:
|
|||||||
|
|
||||||
The commit wizard guides you through:
|
The commit wizard guides you through:
|
||||||
|
|
||||||
1. **Type selection** (feat/fix/docs/style/refactor/perf/test/chore)
|
1. **Type selection** (fix/feat/BREAKING CHANGE) with AI recommendation
|
||||||
2. **Scope definition** (component/module affected)
|
2. **Scope definition** (component/module affected)
|
||||||
3. **Description crafting**
|
3. **Description crafting**
|
||||||
4. **Breaking change detection**
|
4. **Version bump determination**
|
||||||
5. **Version bump determination**
|
|
||||||
|
|
||||||
### 🏗️ Meta Repository Management
|
### 🏗️ Meta Repository Management
|
||||||
|
|
||||||
@@ -443,7 +455,6 @@ gitzone services clean # ⚠️ Warning: deletes data
|
|||||||
|
|
||||||
- **TypeScript** - First-class support
|
- **TypeScript** - First-class support
|
||||||
- **Prettier** - Code formatting
|
- **Prettier** - Code formatting
|
||||||
- **ESLint** - Linting (via format modules)
|
|
||||||
- **npm/pnpm** - Package management
|
- **npm/pnpm** - Package management
|
||||||
- **MongoDB** - Local database service
|
- **MongoDB** - Local database service
|
||||||
- **MinIO** - S3-compatible object storage
|
- **MinIO** - S3-compatible object storage
|
||||||
@@ -522,19 +533,21 @@ gitzone is optimized for speed:
|
|||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
### Trademarks
|
### Trademarks
|
||||||
|
|
||||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||||
|
|
||||||
|
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||||
|
|
||||||
### Company Information
|
### Company Information
|
||||||
|
|
||||||
Task Venture Capital GmbH
|
Task Venture Capital GmbH
|
||||||
Registered at District court Bremen HRB 35230 HB, Germany
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
# GitZone Services Command Implementation Plan
|
# GitZone Services Command Implementation Plan
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Implement the `gitzone services` command to manage MongoDB and MinIO containers for development projects.
|
Implement the `gitzone services` command to manage MongoDB and MinIO containers for development projects.
|
||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
### Module Structure Setup
|
### Module Structure Setup
|
||||||
|
|
||||||
- [x] Create `ts/mod_services/` directory
|
- [x] Create `ts/mod_services/` directory
|
||||||
- [x] Create `mod.plugins.ts` with required imports
|
- [x] Create `mod.plugins.ts` with required imports
|
||||||
- [x] Create `helpers.ts` with utility functions
|
- [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
|
- [x] Create `index.ts` with main command logic
|
||||||
|
|
||||||
### Core Functionality
|
### Core Functionality
|
||||||
|
|
||||||
- [x] Implement ServiceConfiguration class
|
- [x] Implement ServiceConfiguration class
|
||||||
- [x] Load/create `.nogit/env.json` configuration
|
- [x] Load/create `.nogit/env.json` configuration
|
||||||
- [x] Generate random available ports (20000-30000 range)
|
- [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
|
- [x] Generate MongoDB Compass connection strings
|
||||||
|
|
||||||
### Commands Implementation
|
### Commands Implementation
|
||||||
|
|
||||||
- [x] `start` command - Start services (mongo|s3|all)
|
- [x] `start` command - Start services (mongo|s3|all)
|
||||||
- [x] `stop` command - Stop services (mongo|s3|all)
|
- [x] `stop` command - Stop services (mongo|s3|all)
|
||||||
- [x] `restart` command - Restart 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
|
- [x] `clean` command - Remove containers and data
|
||||||
|
|
||||||
### Integration
|
### Integration
|
||||||
|
|
||||||
- [x] Add `@push.rocks/smartshell` to main plugins.ts
|
- [x] Add `@push.rocks/smartshell` to main plugins.ts
|
||||||
- [x] Add `@push.rocks/smartnetwork` to main plugins.ts
|
- [x] Add `@push.rocks/smartnetwork` to main plugins.ts
|
||||||
- [x] Add `@push.rocks/smartinteraction` to main plugins.ts
|
- [x] Add `@push.rocks/smartinteraction` to main plugins.ts
|
||||||
- [x] Register services command in `gitzone.cli.ts`
|
- [x] Register services command in `gitzone.cli.ts`
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- [x] Auto-configuration with smart defaults
|
- [x] Auto-configuration with smart defaults
|
||||||
- [x] Random port assignment to avoid conflicts
|
- [x] Random port assignment to avoid conflicts
|
||||||
- [x] Project isolation with unique container names
|
- [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
|
- [x] MongoDB Compass connection string with network IP
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
- [ ] Test service start/stop operations
|
- [ ] Test service start/stop operations
|
||||||
- [ ] Test configuration creation and updates
|
- [ ] Test configuration creation and updates
|
||||||
- [ ] Test port collision handling
|
- [ ] Test port collision handling
|
||||||
@@ -73,6 +80,7 @@ Implement the `gitzone services` command to manage MongoDB and MinIO containers
|
|||||||
- [ ] Test all command variations
|
- [ ] Test all command variations
|
||||||
|
|
||||||
## Configuration Format
|
## Configuration Format
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"PROJECT_NAME": "derived-from-package-name",
|
"PROJECT_NAME": "derived-from-package-name",
|
||||||
@@ -91,6 +99,7 @@ Implement the `gitzone services` command to manage MongoDB and MinIO containers
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Command Examples
|
## Command Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gitzone services start # Start all services
|
gitzone services start # Start all services
|
||||||
gitzone services start mongo # Start only MongoDB
|
gitzone services start mongo # Start only MongoDB
|
||||||
@@ -104,10 +113,12 @@ gitzone services clean # Remove containers and data
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Progress Notes
|
## Progress Notes
|
||||||
|
|
||||||
Implementation started: 2025-08-14
|
Implementation started: 2025-08-14
|
||||||
Implementation completed: 2025-08-14
|
Implementation completed: 2025-08-14
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
Successfully implemented the `gitzone services` command in TypeScript, providing a complete replacement for the `services.sh` shell script. The implementation includes:
|
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
|
1. **Complete Docker service management** for MongoDB and MinIO containers
|
||||||
|
|||||||
1
test
1
test
Submodule test deleted from 0b89443584
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/cli',
|
name: '@git.zone/cli',
|
||||||
version: '1.21.3',
|
version: '2.12.2',
|
||||||
description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
|
description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,12 +39,20 @@ export class GitzoneConfig {
|
|||||||
|
|
||||||
public async readConfigFromCwd() {
|
public async readConfigFromCwd() {
|
||||||
const npmextraInstance = new plugins.npmextra.Npmextra(paths.cwd);
|
const npmextraInstance = new plugins.npmextra.Npmextra(paths.cwd);
|
||||||
this.data = npmextraInstance.dataFor<IGitzoneConfigData>('gitzone', {});
|
this.data = npmextraInstance.dataFor<IGitzoneConfigData>('@git.zone/cli', {});
|
||||||
this.data.npmciOptions = npmextraInstance.dataFor<
|
|
||||||
IGitzoneConfigData['npmciOptions']
|
// Read szci config for backward compatibility
|
||||||
>('npmci', {
|
const szciConfig = npmextraInstance.dataFor<any>('@ship.zone/szci', {});
|
||||||
npmAccessLevel: 'public',
|
|
||||||
});
|
// Prefer accessLevel from @git.zone/cli.release, fallback to @ship.zone/szci.npmAccessLevel
|
||||||
|
const accessLevel =
|
||||||
|
(this.data as any)?.release?.accessLevel ||
|
||||||
|
szciConfig?.npmAccessLevel ||
|
||||||
|
'public';
|
||||||
|
|
||||||
|
this.data.npmciOptions = {
|
||||||
|
npmAccessLevel: accessLevel,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import type { TGitzoneProjectType } from './classes.gitzoneconfig.js';
|
|||||||
* the Project class is a tool to work with a gitzone project
|
* the Project class is a tool to work with a gitzone project
|
||||||
*/
|
*/
|
||||||
export class Project {
|
export class Project {
|
||||||
public static async fromCwd() {
|
public static async fromCwd(options: { requireProjectType?: boolean } = {}) {
|
||||||
const gitzoneConfig = await GitzoneConfig.fromCwd();
|
const gitzoneConfig = await GitzoneConfig.fromCwd();
|
||||||
const project = new Project(gitzoneConfig);
|
const project = new Project(gitzoneConfig);
|
||||||
if (!project.gitzoneConfig.data.projectType) {
|
const requireProjectType = options.requireProjectType ?? true;
|
||||||
|
if (requireProjectType && !project.gitzoneConfig.data.projectType) {
|
||||||
throw new Error('Please define a project type');
|
throw new Error('Please define a project type');
|
||||||
}
|
}
|
||||||
return project;
|
return project;
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ export let run = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle format with options
|
// Handle format with options
|
||||||
|
// Default is dry-mode, use --write/-w to apply changes
|
||||||
await modFormat.run({
|
await modFormat.run({
|
||||||
|
write: argvArg.write || argvArg.w,
|
||||||
dryRun: argvArg['dry-run'],
|
dryRun: argvArg['dry-run'],
|
||||||
yes: argvArg.yes,
|
yes: argvArg.yes,
|
||||||
planOnly: argvArg['plan-only'],
|
planOnly: argvArg['plan-only'],
|
||||||
@@ -90,6 +92,7 @@ export let run = async () => {
|
|||||||
interactive: argvArg.interactive !== false,
|
interactive: argvArg.interactive !== false,
|
||||||
parallel: argvArg.parallel !== false,
|
parallel: argvArg.parallel !== false,
|
||||||
verbose: argvArg.verbose,
|
verbose: argvArg.verbose,
|
||||||
|
diff: argvArg.diff,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,6 +134,14 @@ export let run = async () => {
|
|||||||
modHelpers.run(argvArg);
|
modHelpers.run(argvArg);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* manage release configuration
|
||||||
|
*/
|
||||||
|
gitzoneSmartcli.addCommand('config').subscribe(async (argvArg) => {
|
||||||
|
const modConfig = await import('./mod_config/index.js');
|
||||||
|
await modConfig.run(argvArg);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* manage development services (MongoDB, S3/MinIO)
|
* manage development services (MongoDB, S3/MinIO)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,13 +5,67 @@ import * as paths from '../paths.js';
|
|||||||
import { logger } from '../gitzone.logging.js';
|
import { logger } from '../gitzone.logging.js';
|
||||||
import * as helpers from './mod.helpers.js';
|
import * as helpers from './mod.helpers.js';
|
||||||
import * as ui from './mod.ui.js';
|
import * as ui from './mod.ui.js';
|
||||||
|
import { ReleaseConfig } from '../mod_config/classes.releaseconfig.js';
|
||||||
|
|
||||||
export const run = async (argvArg: any) => {
|
export const run = async (argvArg: any) => {
|
||||||
|
// Read commit config from npmextra.json
|
||||||
|
const npmextraConfig = new plugins.npmextra.Npmextra();
|
||||||
|
const gitzoneConfig = npmextraConfig.dataFor<{
|
||||||
|
commit?: {
|
||||||
|
alwaysTest?: boolean;
|
||||||
|
alwaysBuild?: boolean;
|
||||||
|
};
|
||||||
|
}>('@git.zone/cli', {});
|
||||||
|
const commitConfig = gitzoneConfig.commit || {};
|
||||||
|
|
||||||
|
// Check flags and merge with config options
|
||||||
|
const wantsRelease = !!(argvArg.r || argvArg.release);
|
||||||
|
const wantsTest = !!(argvArg.t || argvArg.test || commitConfig.alwaysTest);
|
||||||
|
const wantsBuild = !!(argvArg.b || argvArg.build || commitConfig.alwaysBuild);
|
||||||
|
let releaseConfig: ReleaseConfig | null = null;
|
||||||
|
|
||||||
|
if (wantsRelease) {
|
||||||
|
releaseConfig = await ReleaseConfig.fromCwd();
|
||||||
|
if (!releaseConfig.hasRegistries()) {
|
||||||
|
logger.log('error', 'No release registries configured.');
|
||||||
|
console.log('');
|
||||||
|
console.log(' Run `gitzone config add <registry-url>` to add registries.');
|
||||||
|
console.log('');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print execution plan at the start
|
||||||
|
ui.printExecutionPlan({
|
||||||
|
autoAccept: !!(argvArg.y || argvArg.yes),
|
||||||
|
push: !!(argvArg.p || argvArg.push),
|
||||||
|
test: wantsTest,
|
||||||
|
build: wantsBuild,
|
||||||
|
release: wantsRelease,
|
||||||
|
format: !!argvArg.format,
|
||||||
|
registries: releaseConfig?.getRegistries(),
|
||||||
|
});
|
||||||
|
|
||||||
if (argvArg.format) {
|
if (argvArg.format) {
|
||||||
const formatMod = await import('../mod_format/index.js');
|
const formatMod = await import('../mod_format/index.js');
|
||||||
await formatMod.run();
|
await formatMod.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run tests early to fail fast before analysis
|
||||||
|
if (wantsTest) {
|
||||||
|
ui.printHeader('🧪 Running tests...');
|
||||||
|
const smartshellForTest = new plugins.smartshell.Smartshell({
|
||||||
|
executor: 'bash',
|
||||||
|
sourceFilePaths: [],
|
||||||
|
});
|
||||||
|
const testResult = await smartshellForTest.exec('pnpm test');
|
||||||
|
if (testResult.exitCode !== 0) {
|
||||||
|
logger.log('error', 'Tests failed. Aborting commit.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
logger.log('success', 'All tests passed.');
|
||||||
|
}
|
||||||
|
|
||||||
ui.printHeader('🔍 Analyzing repository changes...');
|
ui.printHeader('🔍 Analyzing repository changes...');
|
||||||
|
|
||||||
const aidoc = new plugins.tsdoc.AiDoc();
|
const aidoc = new plugins.tsdoc.AiDoc();
|
||||||
@@ -30,8 +84,12 @@ export const run = async (argvArg: any) => {
|
|||||||
|
|
||||||
let answerBucket: plugins.smartinteract.AnswerBucket;
|
let answerBucket: plugins.smartinteract.AnswerBucket;
|
||||||
|
|
||||||
// Check if -y or --yes flag is set to auto-accept recommendations
|
// Check if -y/--yes flag is set AND version is not a breaking change
|
||||||
if (argvArg.y || argvArg.yes) {
|
// Breaking changes (major version bumps) always require manual confirmation
|
||||||
|
const isBreakingChange = nextCommitObject.recommendedNextVersionLevel === 'BREAKING CHANGE';
|
||||||
|
const canAutoAccept = (argvArg.y || argvArg.yes) && !isBreakingChange;
|
||||||
|
|
||||||
|
if (canAutoAccept) {
|
||||||
// Auto-mode: create AnswerBucket programmatically
|
// Auto-mode: create AnswerBucket programmatically
|
||||||
logger.log('info', '✓ Auto-accepting AI recommendations (--yes flag)');
|
logger.log('info', '✓ Auto-accepting AI recommendations (--yes flag)');
|
||||||
|
|
||||||
@@ -52,7 +110,15 @@ export const run = async (argvArg: any) => {
|
|||||||
name: 'pushToOrigin',
|
name: 'pushToOrigin',
|
||||||
value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided
|
value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided
|
||||||
});
|
});
|
||||||
|
answerBucket.addAnswer({
|
||||||
|
name: 'createRelease',
|
||||||
|
value: wantsRelease,
|
||||||
|
});
|
||||||
} else {
|
} 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');
|
||||||
|
}
|
||||||
// Interactive mode: prompt user for input
|
// Interactive mode: prompt user for input
|
||||||
const commitInteract = new plugins.smartinteract.SmartInteract();
|
const commitInteract = new plugins.smartinteract.SmartInteract();
|
||||||
commitInteract.addQuestions([
|
commitInteract.addQuestions([
|
||||||
@@ -81,6 +147,12 @@ export const run = async (argvArg: any) => {
|
|||||||
message: `Do you want to push this version now?`,
|
message: `Do you want to push this version now?`,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: `createRelease`,
|
||||||
|
message: `Do you want to publish to npm registries?`,
|
||||||
|
default: wantsRelease,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
answerBucket = await commitInteract.runQueue();
|
answerBucket = await commitInteract.runQueue();
|
||||||
}
|
}
|
||||||
@@ -103,8 +175,26 @@ export const run = async (argvArg: any) => {
|
|||||||
sourceFilePaths: [],
|
sourceFilePaths: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Determine total steps (6 if pushing, 5 if not)
|
// Load release config if user wants to release (interactively selected)
|
||||||
const totalSteps = answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true') ? 6 : 5;
|
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('');
|
||||||
|
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();
|
||||||
|
let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version
|
||||||
|
if (wantsBuild) totalSteps += 2; // build step + verification step
|
||||||
|
if (willPush) totalSteps++;
|
||||||
|
if (willRelease) totalSteps++;
|
||||||
let currentStep = 0;
|
let currentStep = 0;
|
||||||
|
|
||||||
// Step 1: Baking commitinfo
|
// Step 1: Baking commitinfo
|
||||||
@@ -142,10 +232,10 @@ export const run = async (argvArg: any) => {
|
|||||||
changelog = changelog.replaceAll('\n{{nextVersionDetails}}', '');
|
changelog = changelog.replaceAll('\n{{nextVersionDetails}}', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
await plugins.smartfile.memory.toFs(
|
await plugins.smartfs
|
||||||
changelog,
|
.file(plugins.path.join(paths.cwd, `changelog.md`))
|
||||||
plugins.path.join(paths.cwd, `changelog.md`),
|
.encoding('utf8')
|
||||||
);
|
.write(changelog);
|
||||||
ui.printStep(currentStep, totalSteps, '📄 Generating changelog.md', 'done');
|
ui.printStep(currentStep, totalSteps, '📄 Generating changelog.md', 'done');
|
||||||
|
|
||||||
// Step 3: Staging files
|
// Step 3: Staging files
|
||||||
@@ -165,18 +255,66 @@ export const run = async (argvArg: any) => {
|
|||||||
const projectType = await helpers.detectProjectType();
|
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: Push to remote (optional)
|
// Step 6: Run build (optional)
|
||||||
|
if (wantsBuild) {
|
||||||
|
currentStep++;
|
||||||
|
ui.printStep(currentStep, totalSteps, '🔨 Running build', 'in-progress');
|
||||||
|
const buildResult = await smartshellInstance.exec('pnpm build');
|
||||||
|
if (buildResult.exitCode !== 0) {
|
||||||
|
ui.printStep(currentStep, totalSteps, '🔨 Running build', 'error');
|
||||||
|
logger.log('error', 'Build failed. Aborting release.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
ui.printStep(currentStep, totalSteps, '🔨 Running build', 'done');
|
||||||
|
|
||||||
|
// Step 7: Verify no uncommitted changes
|
||||||
|
currentStep++;
|
||||||
|
ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'in-progress');
|
||||||
|
const statusResult = await smartshellInstance.exec('git status --porcelain');
|
||||||
|
if (statusResult.stdout.trim() !== '') {
|
||||||
|
ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'error');
|
||||||
|
logger.log('error', 'Build produced uncommitted changes. This usually means build output is not gitignored.');
|
||||||
|
logger.log('error', 'Uncommitted files:');
|
||||||
|
console.log(statusResult.stdout);
|
||||||
|
logger.log('error', 'Aborting release. Please ensure build artifacts are in .gitignore');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'done');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step: Push to remote (optional)
|
||||||
const currentBranch = await helpers.detectCurrentBranch();
|
const currentBranch = await helpers.detectCurrentBranch();
|
||||||
if (
|
if (willPush) {
|
||||||
answerBucket.getAnswerFor('pushToOrigin') &&
|
|
||||||
!(process.env.CI === 'true')
|
|
||||||
) {
|
|
||||||
currentStep++;
|
currentStep++;
|
||||||
ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'in-progress');
|
ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'in-progress');
|
||||||
await smartshellInstance.exec(`git push origin ${currentBranch} --follow-tags`);
|
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}`, 'done');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 7: Publish to npm registries (optional)
|
||||||
|
let releasedRegistries: string[] = [];
|
||||||
|
if (willRelease && releaseConfig) {
|
||||||
|
currentStep++;
|
||||||
|
const registries = releaseConfig.getRegistries();
|
||||||
|
ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'in-progress');
|
||||||
|
|
||||||
|
const accessLevel = releaseConfig.getAccessLevel();
|
||||||
|
for (const registry of registries) {
|
||||||
|
try {
|
||||||
|
await smartshellInstance.exec(`npm publish --registry=${registry} --access=${accessLevel}`);
|
||||||
|
releasedRegistries.push(registry);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log('error', `Failed to publish to ${registry}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releasedRegistries.length === registries.length) {
|
||||||
|
ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'done');
|
||||||
|
} else {
|
||||||
|
ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(''); // Add spacing before summary
|
console.log(''); // Add spacing before summary
|
||||||
|
|
||||||
// Get commit SHA for summary
|
// Get commit SHA for summary
|
||||||
@@ -192,7 +330,9 @@ export const run = async (argvArg: any) => {
|
|||||||
commitMessage: answerBucket.getAnswerFor('commitDescription'),
|
commitMessage: answerBucket.getAnswerFor('commitDescription'),
|
||||||
newVersion: newVersion,
|
newVersion: newVersion,
|
||||||
commitSha: commitSha,
|
commitSha: commitSha,
|
||||||
pushed: answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true'),
|
pushed: willPush,
|
||||||
|
released: releasedRegistries.length > 0,
|
||||||
|
releasedRegistries: releasedRegistries.length > 0 ? releasedRegistries : undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ export async function detectProjectType(): Promise<ProjectType> {
|
|||||||
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
||||||
const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
|
const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
|
||||||
|
|
||||||
const hasPackageJson = await plugins.smartfile.fs.fileExists(packageJsonPath);
|
const hasPackageJson = await plugins.smartfs.file(packageJsonPath).exists();
|
||||||
const hasDenoJson = await plugins.smartfile.fs.fileExists(denoJsonPath);
|
const hasDenoJson = await plugins.smartfs.file(denoJsonPath).exists();
|
||||||
|
|
||||||
if (hasPackageJson && hasDenoJson) {
|
if (hasPackageJson && hasDenoJson) {
|
||||||
logger.log('info', 'Detected dual project (npm + deno)');
|
logger.log('info', 'Detected dual project (npm + deno)');
|
||||||
@@ -95,10 +95,14 @@ function calculateNewVersion(currentVersion: string, versionType: VersionType):
|
|||||||
* @param projectType The project type to determine which file to read
|
* @param projectType The project type to determine which file to read
|
||||||
* @returns The current version string
|
* @returns The current version string
|
||||||
*/
|
*/
|
||||||
function readCurrentVersion(projectType: ProjectType): string {
|
async function readCurrentVersion(projectType: ProjectType): Promise<string> {
|
||||||
if (projectType === 'npm' || projectType === 'both') {
|
if (projectType === 'npm' || projectType === 'both') {
|
||||||
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
||||||
const packageJson = plugins.smartfile.fs.toObjectSync(packageJsonPath) as { version?: string };
|
const content = (await plugins.smartfs
|
||||||
|
.file(packageJsonPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
const packageJson = JSON.parse(content) as { version?: string };
|
||||||
|
|
||||||
if (!packageJson.version) {
|
if (!packageJson.version) {
|
||||||
throw new Error('package.json does not contain a version field');
|
throw new Error('package.json does not contain a version field');
|
||||||
@@ -106,7 +110,11 @@ function readCurrentVersion(projectType: ProjectType): string {
|
|||||||
return packageJson.version;
|
return packageJson.version;
|
||||||
} else {
|
} else {
|
||||||
const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
|
const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
|
||||||
const denoConfig = plugins.smartfile.fs.toObjectSync(denoJsonPath) as { version?: string };
|
const content = (await plugins.smartfs
|
||||||
|
.file(denoJsonPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
const denoConfig = JSON.parse(content) as { version?: string };
|
||||||
|
|
||||||
if (!denoConfig.version) {
|
if (!denoConfig.version) {
|
||||||
throw new Error('deno.json does not contain a version field');
|
throw new Error('deno.json does not contain a version field');
|
||||||
@@ -121,12 +129,16 @@ function readCurrentVersion(projectType: ProjectType): string {
|
|||||||
* @param newVersion The new version to write
|
* @param newVersion The new version to write
|
||||||
*/
|
*/
|
||||||
async function updateVersionFile(filePath: string, newVersion: string): Promise<void> {
|
async function updateVersionFile(filePath: string, newVersion: string): Promise<void> {
|
||||||
const config = plugins.smartfile.fs.toObjectSync(filePath) as { version?: string };
|
const content = (await plugins.smartfs
|
||||||
|
.file(filePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
const config = JSON.parse(content) as { version?: string };
|
||||||
config.version = newVersion;
|
config.version = newVersion;
|
||||||
await plugins.smartfile.memory.toFs(
|
await plugins.smartfs
|
||||||
JSON.stringify(config, null, 2) + '\n',
|
.file(filePath)
|
||||||
filePath
|
.encoding('utf8')
|
||||||
);
|
.write(JSON.stringify(config, null, 2) + '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -162,7 +174,7 @@ export async function bumpProjectVersion(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Read current version
|
// 1. Read current version
|
||||||
const currentVersion = readCurrentVersion(projectType);
|
const currentVersion = await readCurrentVersion(projectType);
|
||||||
|
|
||||||
// 2. Calculate new version (reuse existing function!)
|
// 2. Calculate new version (reuse existing function!)
|
||||||
const newVersion = calculateNewVersion(currentVersion, versionType);
|
const newVersion = calculateNewVersion(currentVersion, versionType);
|
||||||
|
|||||||
@@ -14,6 +14,18 @@ interface ICommitSummary {
|
|||||||
commitSha?: string;
|
commitSha?: string;
|
||||||
pushed: boolean;
|
pushed: boolean;
|
||||||
repoUrl?: string;
|
repoUrl?: string;
|
||||||
|
released?: boolean;
|
||||||
|
releasedRegistries?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IExecutionPlanOptions {
|
||||||
|
autoAccept: boolean;
|
||||||
|
push: boolean;
|
||||||
|
test: boolean;
|
||||||
|
build: boolean;
|
||||||
|
release: boolean;
|
||||||
|
format: boolean;
|
||||||
|
registries?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRecommendation {
|
interface IRecommendation {
|
||||||
@@ -39,6 +51,58 @@ export function printHeader(title: string): void {
|
|||||||
console.log('');
|
console.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print execution plan at the start of commit
|
||||||
|
*/
|
||||||
|
export function printExecutionPlan(options: IExecutionPlanOptions): void {
|
||||||
|
console.log('');
|
||||||
|
console.log('╭───────────────────────────────────────────────────────────────╮');
|
||||||
|
console.log('│ gitzone commit - Execution Plan │');
|
||||||
|
console.log('╰───────────────────────────────────────────────────────────────╯');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Show active options
|
||||||
|
console.log(' Options:');
|
||||||
|
console.log(` Auto-accept ${options.autoAccept ? '✓ enabled (-y)' : '○ interactive mode'}`);
|
||||||
|
console.log(` Push to remote ${options.push ? '✓ enabled (-p)' : '○ disabled'}`);
|
||||||
|
console.log(` Test first ${options.test ? '✓ enabled (-t)' : '○ disabled'}`);
|
||||||
|
console.log(` Build & verify ${options.build ? '✓ enabled (-b)' : '○ disabled'}`);
|
||||||
|
console.log(` Release to npm ${options.release ? '✓ enabled (-r)' : '○ disabled'}`);
|
||||||
|
if (options.format) {
|
||||||
|
console.log(` Format first ✓ enabled (--format)`);
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Show steps
|
||||||
|
console.log(' Steps:');
|
||||||
|
let stepNum = 1;
|
||||||
|
if (options.format) {
|
||||||
|
console.log(` ${stepNum++}. Format project files`);
|
||||||
|
}
|
||||||
|
if (options.test) {
|
||||||
|
console.log(` ${stepNum++}. Run tests`);
|
||||||
|
}
|
||||||
|
console.log(` ${stepNum++}. Analyze repository changes`);
|
||||||
|
console.log(` ${stepNum++}. Bake commit info into code`);
|
||||||
|
console.log(` ${stepNum++}. Generate changelog.md`);
|
||||||
|
console.log(` ${stepNum++}. Stage and commit files`);
|
||||||
|
console.log(` ${stepNum++}. Bump version`);
|
||||||
|
if (options.build) {
|
||||||
|
console.log(` ${stepNum++}. Run build`);
|
||||||
|
console.log(` ${stepNum++}. Verify clean working tree`);
|
||||||
|
}
|
||||||
|
if (options.push) {
|
||||||
|
console.log(` ${stepNum++}. Push to remote`);
|
||||||
|
}
|
||||||
|
if (options.release) {
|
||||||
|
console.log(` ${stepNum++}. Publish to npm registries`);
|
||||||
|
if (options.registries && options.registries.length > 0) {
|
||||||
|
options.registries.forEach(r => console.log(` → ${r}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Print a section with a border
|
* Print a section with a border
|
||||||
*/
|
*/
|
||||||
@@ -146,6 +210,13 @@ export function printSummary(summary: ICommitSummary): void {
|
|||||||
lines.push(`Remote: ⊘ Not pushed (local only)`);
|
lines.push(`Remote: ⊘ Not pushed (local only)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (summary.released && summary.releasedRegistries && summary.releasedRegistries.length > 0) {
|
||||||
|
lines.push(`Published: ✓ Released to ${summary.releasedRegistries.length} registr${summary.releasedRegistries.length === 1 ? 'y' : 'ies'}`);
|
||||||
|
summary.releasedRegistries.forEach((registry) => {
|
||||||
|
lines.push(` → ${registry}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (summary.repoUrl && summary.commitSha) {
|
if (summary.repoUrl && summary.commitSha) {
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push(`View at: ${summary.repoUrl}/commit/${summary.commitSha}`);
|
lines.push(`View at: ${summary.repoUrl}/commit/${summary.commitSha}`);
|
||||||
@@ -153,7 +224,9 @@ export function printSummary(summary: ICommitSummary): void {
|
|||||||
|
|
||||||
printSection('✅ Commit Summary', lines);
|
printSection('✅ Commit Summary', lines);
|
||||||
|
|
||||||
if (summary.pushed) {
|
if (summary.released) {
|
||||||
|
console.log('🎉 All done! Your changes are committed, pushed, and released.\n');
|
||||||
|
} else if (summary.pushed) {
|
||||||
console.log('🎉 All done! Your changes are committed and pushed.\n');
|
console.log('🎉 All done! Your changes are committed and pushed.\n');
|
||||||
} else {
|
} else {
|
||||||
console.log('✓ Commit created successfully.\n');
|
console.log('✓ Commit created successfully.\n');
|
||||||
|
|||||||
104
ts/mod_config/classes.commitconfig.ts
Normal file
104
ts/mod_config/classes.commitconfig.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import * as plugins from './mod.plugins.js';
|
||||||
|
|
||||||
|
export interface ICommitConfig {
|
||||||
|
alwaysTest: boolean;
|
||||||
|
alwaysBuild: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages commit configuration stored in npmextra.json
|
||||||
|
* under @git.zone/cli.commit namespace
|
||||||
|
*/
|
||||||
|
export class CommitConfig {
|
||||||
|
private cwd: string;
|
||||||
|
private config: ICommitConfig;
|
||||||
|
|
||||||
|
constructor(cwd: string = process.cwd()) {
|
||||||
|
this.cwd = cwd;
|
||||||
|
this.config = { alwaysTest: false, alwaysBuild: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a CommitConfig instance from current working directory
|
||||||
|
*/
|
||||||
|
public static async fromCwd(cwd: string = process.cwd()): Promise<CommitConfig> {
|
||||||
|
const instance = new CommitConfig(cwd);
|
||||||
|
await instance.load();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load configuration from npmextra.json
|
||||||
|
*/
|
||||||
|
public async load(): Promise<void> {
|
||||||
|
const npmextraInstance = new plugins.npmextra.Npmextra(this.cwd);
|
||||||
|
const gitzoneConfig = npmextraInstance.dataFor<any>('@git.zone/cli', {});
|
||||||
|
|
||||||
|
this.config = {
|
||||||
|
alwaysTest: gitzoneConfig?.commit?.alwaysTest ?? false,
|
||||||
|
alwaysBuild: gitzoneConfig?.commit?.alwaysBuild ?? false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save configuration to npmextra.json
|
||||||
|
*/
|
||||||
|
public async save(): Promise<void> {
|
||||||
|
const npmextraPath = plugins.path.join(this.cwd, 'npmextra.json');
|
||||||
|
let npmextraData: 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure @git.zone/cli namespace exists
|
||||||
|
if (!npmextraData['@git.zone/cli']) {
|
||||||
|
npmextraData['@git.zone/cli'] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure commit object exists
|
||||||
|
if (!npmextraData['@git.zone/cli'].commit) {
|
||||||
|
npmextraData['@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;
|
||||||
|
|
||||||
|
// Write back to file
|
||||||
|
await plugins.smartfs
|
||||||
|
.file(npmextraPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.write(JSON.stringify(npmextraData, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get alwaysTest setting
|
||||||
|
*/
|
||||||
|
public getAlwaysTest(): boolean {
|
||||||
|
return this.config.alwaysTest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set alwaysTest setting
|
||||||
|
*/
|
||||||
|
public setAlwaysTest(value: boolean): void {
|
||||||
|
this.config.alwaysTest = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get alwaysBuild setting
|
||||||
|
*/
|
||||||
|
public getAlwaysBuild(): boolean {
|
||||||
|
return this.config.alwaysBuild;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set alwaysBuild setting
|
||||||
|
*/
|
||||||
|
public setAlwaysBuild(value: boolean): void {
|
||||||
|
this.config.alwaysBuild = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
166
ts/mod_config/classes.releaseconfig.ts
Normal file
166
ts/mod_config/classes.releaseconfig.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import * as plugins from './mod.plugins.js';
|
||||||
|
|
||||||
|
export type TAccessLevel = 'public' | 'private';
|
||||||
|
|
||||||
|
export interface IReleaseConfig {
|
||||||
|
registries: string[];
|
||||||
|
accessLevel: TAccessLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages release configuration stored in npmextra.json
|
||||||
|
* under @git.zone/cli.release namespace
|
||||||
|
*/
|
||||||
|
export class ReleaseConfig {
|
||||||
|
private cwd: string;
|
||||||
|
private config: IReleaseConfig;
|
||||||
|
|
||||||
|
constructor(cwd: string = process.cwd()) {
|
||||||
|
this.cwd = cwd;
|
||||||
|
this.config = { registries: [], accessLevel: 'public' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a ReleaseConfig instance from current working directory
|
||||||
|
*/
|
||||||
|
public static async fromCwd(cwd: string = process.cwd()): Promise<ReleaseConfig> {
|
||||||
|
const instance = new ReleaseConfig(cwd);
|
||||||
|
await instance.load();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load configuration from npmextra.json
|
||||||
|
*/
|
||||||
|
public async load(): Promise<void> {
|
||||||
|
const npmextraInstance = new plugins.npmextra.Npmextra(this.cwd);
|
||||||
|
const gitzoneConfig = npmextraInstance.dataFor<any>('@git.zone/cli', {});
|
||||||
|
|
||||||
|
// Also check szci for backward compatibility
|
||||||
|
const szciConfig = npmextraInstance.dataFor<any>('@ship.zone/szci', {});
|
||||||
|
|
||||||
|
this.config = {
|
||||||
|
registries: gitzoneConfig?.release?.registries || [],
|
||||||
|
accessLevel: gitzoneConfig?.release?.accessLevel || szciConfig?.npmAccessLevel || 'public',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save configuration to npmextra.json
|
||||||
|
*/
|
||||||
|
public async save(): Promise<void> {
|
||||||
|
const npmextraPath = plugins.path.join(this.cwd, 'npmextra.json');
|
||||||
|
let npmextraData: 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure @git.zone/cli namespace exists
|
||||||
|
if (!npmextraData['@git.zone/cli']) {
|
||||||
|
npmextraData['@git.zone/cli'] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure release object exists
|
||||||
|
if (!npmextraData['@git.zone/cli'].release) {
|
||||||
|
npmextraData['@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;
|
||||||
|
|
||||||
|
// Write back to file
|
||||||
|
await plugins.smartfs
|
||||||
|
.file(npmextraPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.write(JSON.stringify(npmextraData, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all configured registries
|
||||||
|
*/
|
||||||
|
public getRegistries(): string[] {
|
||||||
|
return [...this.config.registries];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any registries are configured
|
||||||
|
*/
|
||||||
|
public hasRegistries(): boolean {
|
||||||
|
return this.config.registries.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a registry URL
|
||||||
|
* @returns true if added, false if already exists
|
||||||
|
*/
|
||||||
|
public addRegistry(url: string): boolean {
|
||||||
|
const normalizedUrl = this.normalizeUrl(url);
|
||||||
|
|
||||||
|
if (this.config.registries.includes(normalizedUrl)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config.registries.push(normalizedUrl);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a registry URL
|
||||||
|
* @returns true if removed, false if not found
|
||||||
|
*/
|
||||||
|
public removeRegistry(url: string): boolean {
|
||||||
|
const normalizedUrl = this.normalizeUrl(url);
|
||||||
|
const index = this.config.registries.indexOf(normalizedUrl);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config.registries.splice(index, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all registries
|
||||||
|
*/
|
||||||
|
public clearRegistries(): void {
|
||||||
|
this.config.registries = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the npm access level
|
||||||
|
*/
|
||||||
|
public getAccessLevel(): TAccessLevel {
|
||||||
|
return this.config.accessLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the npm access level
|
||||||
|
*/
|
||||||
|
public setAccessLevel(level: TAccessLevel): void {
|
||||||
|
this.config.accessLevel = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a registry URL (ensure it has https:// prefix)
|
||||||
|
*/
|
||||||
|
private normalizeUrl(url: string): string {
|
||||||
|
let normalized = url.trim();
|
||||||
|
|
||||||
|
// Add https:// if no protocol specified
|
||||||
|
if (!normalized.startsWith('http://') && !normalized.startsWith('https://')) {
|
||||||
|
normalized = `https://${normalized}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing slash
|
||||||
|
if (normalized.endsWith('/')) {
|
||||||
|
normalized = normalized.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
431
ts/mod_config/index.ts
Normal file
431
ts/mod_config/index.ts
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
// 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';
|
||||||
|
|
||||||
|
export { ReleaseConfig, CommitConfig };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format npmextra.json with diff preview
|
||||||
|
* Shows diff first, asks for confirmation, then applies
|
||||||
|
*/
|
||||||
|
async function formatNpmextraWithDiff(): Promise<void> {
|
||||||
|
// Check for diffs first
|
||||||
|
const checkResult = await runFormatter('npmextra', {
|
||||||
|
checkOnly: true,
|
||||||
|
showDiff: true,
|
||||||
|
}) as ICheckResult | void;
|
||||||
|
|
||||||
|
if (checkResult && checkResult.hasDiff) {
|
||||||
|
const shouldApply = await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||||
|
'Apply formatting changes to npmextra.json?',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (shouldApply) {
|
||||||
|
await runFormatter('npmextra', { silent: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const run = async (argvArg: any) => {
|
||||||
|
const command = argvArg._?.[1];
|
||||||
|
const value = argvArg._?.[2];
|
||||||
|
|
||||||
|
// If no command provided, show interactive menu
|
||||||
|
if (!command) {
|
||||||
|
await handleInteractiveMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'show':
|
||||||
|
await handleShow();
|
||||||
|
break;
|
||||||
|
case 'add':
|
||||||
|
await handleAdd(value);
|
||||||
|
break;
|
||||||
|
case 'remove':
|
||||||
|
await handleRemove(value);
|
||||||
|
break;
|
||||||
|
case 'clear':
|
||||||
|
await handleClear();
|
||||||
|
break;
|
||||||
|
case 'access':
|
||||||
|
case 'accessLevel':
|
||||||
|
await handleAccessLevel(value);
|
||||||
|
break;
|
||||||
|
case 'commit':
|
||||||
|
await handleCommit(argvArg._?.[2], argvArg._?.[3]);
|
||||||
|
break;
|
||||||
|
case 'services':
|
||||||
|
await handleServices();
|
||||||
|
break;
|
||||||
|
case 'help':
|
||||||
|
showHelp();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
plugins.logger.log('error', `Unknown command: ${command}`);
|
||||||
|
showHelp();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interactive menu for config command
|
||||||
|
*/
|
||||||
|
async function handleInteractiveMenu(): Promise<void> {
|
||||||
|
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',
|
||||||
|
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' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const action = (response as any).value;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'show':
|
||||||
|
await handleShow();
|
||||||
|
break;
|
||||||
|
case 'add':
|
||||||
|
await handleAdd();
|
||||||
|
break;
|
||||||
|
case 'remove':
|
||||||
|
await handleRemove();
|
||||||
|
break;
|
||||||
|
case 'clear':
|
||||||
|
await handleClear();
|
||||||
|
break;
|
||||||
|
case 'access':
|
||||||
|
await handleAccessLevel();
|
||||||
|
break;
|
||||||
|
case 'commit':
|
||||||
|
await handleCommit();
|
||||||
|
break;
|
||||||
|
case 'services':
|
||||||
|
await handleServices();
|
||||||
|
break;
|
||||||
|
case 'help':
|
||||||
|
showHelp();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show current registry configuration
|
||||||
|
*/
|
||||||
|
async function handleShow(): Promise<void> {
|
||||||
|
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('');
|
||||||
|
|
||||||
|
// Show access level
|
||||||
|
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('');
|
||||||
|
} else {
|
||||||
|
plugins.logger.log('info', `Configured registries (${registries.length}):`);
|
||||||
|
console.log('');
|
||||||
|
registries.forEach((url, index) => {
|
||||||
|
console.log(` ${index + 1}. ${url}`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a registry URL
|
||||||
|
*/
|
||||||
|
async function handleAdd(url?: string): Promise<void> {
|
||||||
|
if (!url) {
|
||||||
|
// 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',
|
||||||
|
validate: (input: string) => {
|
||||||
|
return !!(input && input.trim() !== '');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
url = (response as any).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await ReleaseConfig.fromCwd();
|
||||||
|
const added = config.addRegistry(url!);
|
||||||
|
|
||||||
|
if (added) {
|
||||||
|
await config.save();
|
||||||
|
plugins.logger.log('success', `Added registry: ${url}`);
|
||||||
|
await formatNpmextraWithDiff();
|
||||||
|
} else {
|
||||||
|
plugins.logger.log('warn', `Registry already exists: ${url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a registry URL
|
||||||
|
*/
|
||||||
|
async function handleRemove(url?: string): Promise<void> {
|
||||||
|
const config = await ReleaseConfig.fromCwd();
|
||||||
|
const registries = config.getRegistries();
|
||||||
|
|
||||||
|
if (registries.length === 0) {
|
||||||
|
plugins.logger.log('warn', 'No registries configured to remove.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
// 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:',
|
||||||
|
choices: registries,
|
||||||
|
default: registries[0],
|
||||||
|
});
|
||||||
|
url = (response as any).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removed = config.removeRegistry(url!);
|
||||||
|
|
||||||
|
if (removed) {
|
||||||
|
await config.save();
|
||||||
|
plugins.logger.log('success', `Removed registry: ${url}`);
|
||||||
|
await formatNpmextraWithDiff();
|
||||||
|
} else {
|
||||||
|
plugins.logger.log('warn', `Registry not found: ${url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all registries
|
||||||
|
*/
|
||||||
|
async function handleClear(): Promise<void> {
|
||||||
|
const config = await ReleaseConfig.fromCwd();
|
||||||
|
|
||||||
|
if (!config.hasRegistries()) {
|
||||||
|
plugins.logger.log('info', 'No registries to clear.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm before clearing
|
||||||
|
const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||||
|
'Clear all configured registries?',
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed) {
|
||||||
|
config.clearRegistries();
|
||||||
|
await config.save();
|
||||||
|
plugins.logger.log('success', 'All registries cleared.');
|
||||||
|
await formatNpmextraWithDiff();
|
||||||
|
} else {
|
||||||
|
plugins.logger.log('info', 'Operation cancelled.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set or toggle access level
|
||||||
|
*/
|
||||||
|
async function handleAccessLevel(level?: string): Promise<void> {
|
||||||
|
const config = await ReleaseConfig.fromCwd();
|
||||||
|
const currentLevel = config.getAccessLevel();
|
||||||
|
|
||||||
|
if (!level) {
|
||||||
|
// 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'],
|
||||||
|
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'.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level === currentLevel) {
|
||||||
|
plugins.logger.log('info', `Access level is already set to: ${level}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.setAccessLevel(level as 'public' | 'private');
|
||||||
|
await config.save();
|
||||||
|
plugins.logger.log('success', `Access level set to: ${level}`);
|
||||||
|
await formatNpmextraWithDiff();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle commit configuration
|
||||||
|
*/
|
||||||
|
async function handleCommit(setting?: string, value?: string): Promise<void> {
|
||||||
|
const config = await CommitConfig.fromCwd();
|
||||||
|
|
||||||
|
// No setting = interactive mode
|
||||||
|
if (!setting) {
|
||||||
|
await handleCommitInteractive(config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct setting
|
||||||
|
switch (setting) {
|
||||||
|
case 'alwaysTest':
|
||||||
|
await handleCommitSetting(config, 'alwaysTest', value);
|
||||||
|
break;
|
||||||
|
case 'alwaysBuild':
|
||||||
|
await handleCommitSetting(config, 'alwaysBuild', value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
plugins.logger.log('error', `Unknown commit setting: ${setting}`);
|
||||||
|
showCommitHelp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interactive commit configuration
|
||||||
|
*/
|
||||||
|
async function handleCommitInteractive(config: CommitConfig): Promise<void> {
|
||||||
|
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:',
|
||||||
|
choices: [
|
||||||
|
{ name: 'Always run tests before commit (-t)', value: 'alwaysTest' },
|
||||||
|
{ name: 'Always build after commit (-b)', value: 'alwaysBuild' },
|
||||||
|
],
|
||||||
|
default: [
|
||||||
|
...(config.getAlwaysTest() ? ['alwaysTest'] : []),
|
||||||
|
...(config.getAlwaysBuild() ? ['alwaysBuild'] : []),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const selected = (response as any).value || [];
|
||||||
|
config.setAlwaysTest(selected.includes('alwaysTest'));
|
||||||
|
config.setAlwaysBuild(selected.includes('alwaysBuild'));
|
||||||
|
await config.save();
|
||||||
|
|
||||||
|
plugins.logger.log('success', 'Commit configuration updated');
|
||||||
|
await formatNpmextraWithDiff();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a specific commit setting
|
||||||
|
*/
|
||||||
|
async function handleCommitSetting(config: CommitConfig, setting: string, value?: string): Promise<void> {
|
||||||
|
// Parse boolean value
|
||||||
|
const boolValue = value === 'true' || value === '1' || value === 'on';
|
||||||
|
|
||||||
|
if (setting === 'alwaysTest') {
|
||||||
|
config.setAlwaysTest(boolValue);
|
||||||
|
} else if (setting === 'alwaysBuild') {
|
||||||
|
config.setAlwaysBuild(boolValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
await config.save();
|
||||||
|
plugins.logger.log('success', `Set ${setting} to ${boolValue}`);
|
||||||
|
await formatNpmextraWithDiff();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle services configuration
|
||||||
|
*/
|
||||||
|
async function handleServices(): Promise<void> {
|
||||||
|
// Import and use ServiceManager's configureServices
|
||||||
|
const { ServiceManager } = await import('../mod_services/classes.servicemanager.js');
|
||||||
|
const serviceManager = new ServiceManager();
|
||||||
|
await serviceManager.init();
|
||||||
|
await serviceManager.configureServices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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('');
|
||||||
|
}
|
||||||
3
ts/mod_config/mod.plugins.ts
Normal file
3
ts/mod_config/mod.plugins.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// mod_config plugins
|
||||||
|
export * from '../plugins.js';
|
||||||
|
export { logger } from '../gitzone.logging.js';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from './mod.plugins.js';
|
||||||
import { FormatContext } from './classes.formatcontext.js';
|
import { FormatContext } from './classes.formatcontext.js';
|
||||||
import type { IPlannedChange } from './interfaces.format.js';
|
import type { IPlannedChange, ICheckResult } from './interfaces.format.js';
|
||||||
import { Project } from '../classes.project.js';
|
import { Project } from '../classes.project.js';
|
||||||
|
|
||||||
export abstract class BaseFormatter {
|
export abstract class BaseFormatter {
|
||||||
@@ -65,18 +65,112 @@ export abstract class BaseFormatter {
|
|||||||
normalizedPath = './' + filepath;
|
normalizedPath = './' + filepath;
|
||||||
}
|
}
|
||||||
|
|
||||||
await plugins.smartfile.memory.toFs(content, normalizedPath);
|
await plugins.smartfs.file(normalizedPath).encoding('utf8').write(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async createFile(filepath: string, content: string): Promise<void> {
|
protected async createFile(filepath: string, content: string): Promise<void> {
|
||||||
await plugins.smartfile.memory.toFs(content, filepath);
|
await plugins.smartfs.file(filepath).encoding('utf8').write(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async deleteFile(filepath: string): Promise<void> {
|
protected async deleteFile(filepath: string): Promise<void> {
|
||||||
await plugins.smartfile.fs.remove(filepath);
|
await plugins.smartfs.file(filepath).delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async shouldProcessFile(filepath: string): Promise<boolean> {
|
protected async shouldProcessFile(filepath: string): Promise<boolean> {
|
||||||
return true;
|
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,
|
||||||
|
type: change.type,
|
||||||
|
before: currentContent,
|
||||||
|
after: newContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} 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({
|
||||||
|
path: change.path,
|
||||||
|
type: 'delete',
|
||||||
|
before: currentContent,
|
||||||
|
after: undefined,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist, nothing to delete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasDiff: diffs.length > 0,
|
||||||
|
diffs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.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) {
|
||||||
|
console.log(' ... (truncated)');
|
||||||
|
}
|
||||||
|
} else if (diff.before && !diff.after) {
|
||||||
|
console.log(' (file will be deleted)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display all diffs from a check result
|
||||||
|
*/
|
||||||
|
displayAllDiffs(result: ICheckResult): void {
|
||||||
|
if (!result.hasDiff) {
|
||||||
|
console.log(' No changes detected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const diff of result.diffs) {
|
||||||
|
this.displayDiff(diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class ChangeCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
await plugins.smartfile.fs.ensureDir(this.cacheDir);
|
await plugins.smartfs.directory(this.cacheDir).recursive().create();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getManifest(): Promise<ICacheManifest> {
|
async getManifest(): Promise<ICacheManifest> {
|
||||||
@@ -35,13 +35,16 @@ export class ChangeCache {
|
|||||||
files: [],
|
files: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const exists = await plugins.smartfile.fs.fileExists(this.manifestPath);
|
const exists = await plugins.smartfs.file(this.manifestPath).exists();
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
return defaultManifest;
|
return defaultManifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = plugins.smartfile.fs.toStringSync(this.manifestPath);
|
const content = (await plugins.smartfs
|
||||||
|
.file(this.manifestPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
const manifest = JSON.parse(content);
|
const manifest = JSON.parse(content);
|
||||||
|
|
||||||
// Validate the manifest structure
|
// Validate the manifest structure
|
||||||
@@ -57,7 +60,7 @@ export class ChangeCache {
|
|||||||
);
|
);
|
||||||
// Try to delete the corrupted file
|
// Try to delete the corrupted file
|
||||||
try {
|
try {
|
||||||
await plugins.smartfile.fs.remove(this.manifestPath);
|
await plugins.smartfs.file(this.manifestPath).delete();
|
||||||
} catch (removeError) {
|
} catch (removeError) {
|
||||||
// Ignore removal errors
|
// Ignore removal errors
|
||||||
}
|
}
|
||||||
@@ -72,11 +75,14 @@ export class ChangeCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
await plugins.smartfile.fs.ensureDir(this.cacheDir);
|
await plugins.smartfs.directory(this.cacheDir).recursive().create();
|
||||||
|
|
||||||
// Write directly with proper JSON stringification
|
// Write directly with proper JSON stringification
|
||||||
const jsonContent = JSON.stringify(manifest, null, 2);
|
const jsonContent = JSON.stringify(manifest, null, 2);
|
||||||
await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath);
|
await plugins.smartfs
|
||||||
|
.file(this.manifestPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.write(jsonContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasFileChanged(filePath: string): Promise<boolean> {
|
async hasFileChanged(filePath: string): Promise<boolean> {
|
||||||
@@ -85,20 +91,23 @@ export class ChangeCache {
|
|||||||
: plugins.path.join(paths.cwd, filePath);
|
: plugins.path.join(paths.cwd, filePath);
|
||||||
|
|
||||||
// Check if file exists
|
// Check if file exists
|
||||||
const exists = await plugins.smartfile.fs.fileExists(absolutePath);
|
const exists = await plugins.smartfs.file(absolutePath).exists();
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
return true; // File doesn't exist, so it's "changed" (will be created)
|
return true; // File doesn't exist, so it's "changed" (will be created)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current file stats
|
// Get current file stats
|
||||||
const stats = await plugins.smartfile.fs.stat(absolutePath);
|
const stats = await plugins.smartfs.file(absolutePath).stat();
|
||||||
|
|
||||||
// Skip directories
|
// Skip directories
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory) {
|
||||||
return false; // Directories are not processed
|
return false; // Directories are not processed
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = plugins.smartfile.fs.toStringSync(absolutePath);
|
const content = (await plugins.smartfs
|
||||||
|
.file(absolutePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
const currentChecksum = this.calculateChecksum(content);
|
const currentChecksum = this.calculateChecksum(content);
|
||||||
|
|
||||||
// Get cached info
|
// Get cached info
|
||||||
@@ -113,7 +122,7 @@ export class ChangeCache {
|
|||||||
return (
|
return (
|
||||||
cachedFile.checksum !== currentChecksum ||
|
cachedFile.checksum !== currentChecksum ||
|
||||||
cachedFile.size !== stats.size ||
|
cachedFile.size !== stats.size ||
|
||||||
cachedFile.modified !== stats.mtimeMs
|
cachedFile.modified !== stats.mtime.getTime()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,14 +132,17 @@ export class ChangeCache {
|
|||||||
: plugins.path.join(paths.cwd, filePath);
|
: plugins.path.join(paths.cwd, filePath);
|
||||||
|
|
||||||
// Get current file stats
|
// Get current file stats
|
||||||
const stats = await plugins.smartfile.fs.stat(absolutePath);
|
const stats = await plugins.smartfs.file(absolutePath).stat();
|
||||||
|
|
||||||
// Skip directories
|
// Skip directories
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory) {
|
||||||
return; // Don't cache directories
|
return; // Don't cache directories
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = plugins.smartfile.fs.toStringSync(absolutePath);
|
const content = (await plugins.smartfs
|
||||||
|
.file(absolutePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
const checksum = this.calculateChecksum(content);
|
const checksum = this.calculateChecksum(content);
|
||||||
|
|
||||||
// Update manifest
|
// Update manifest
|
||||||
@@ -140,7 +152,7 @@ export class ChangeCache {
|
|||||||
const cacheEntry: IFileCache = {
|
const cacheEntry: IFileCache = {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
checksum,
|
checksum,
|
||||||
modified: stats.mtimeMs,
|
modified: stats.mtime.getTime(),
|
||||||
size: stats.size,
|
size: stats.size,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -176,7 +188,7 @@ export class ChangeCache {
|
|||||||
? file.path
|
? file.path
|
||||||
: plugins.path.join(paths.cwd, file.path);
|
: plugins.path.join(paths.cwd, file.path);
|
||||||
|
|
||||||
if (await plugins.smartfile.fs.fileExists(absolutePath)) {
|
if (await plugins.smartfs.file(absolutePath).exists()) {
|
||||||
validFiles.push(file);
|
validFiles.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,15 @@ export class DiffReporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const exists = await plugins.smartfile.fs.fileExists(change.path);
|
const exists = await plugins.smartfs.file(change.path).exists();
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentContent = await plugins.smartfile.fs.toStringSync(
|
const currentContent = (await plugins.smartfs
|
||||||
change.path,
|
.file(change.path)
|
||||||
);
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
// For planned changes, we need the new content
|
// For planned changes, we need the new content
|
||||||
if (!change.content) {
|
if (!change.content) {
|
||||||
@@ -107,10 +108,10 @@ export class DiffReporter {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
await plugins.smartfile.memory.toFs(
|
await plugins.smartfs
|
||||||
JSON.stringify(report, null, 2),
|
.file(outputPath)
|
||||||
outputPath,
|
.encoding('utf8')
|
||||||
);
|
.write(JSON.stringify(report, null, 2));
|
||||||
logger.log('info', `Diff report saved to ${outputPath}`);
|
logger.log('info', `Diff report saved to ${outputPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -192,10 +192,10 @@ export class FormatStats {
|
|||||||
moduleStats: Array.from(this.stats.moduleStats.values()),
|
moduleStats: Array.from(this.stats.moduleStats.values()),
|
||||||
};
|
};
|
||||||
|
|
||||||
await plugins.smartfile.memory.toFs(
|
await plugins.smartfs
|
||||||
JSON.stringify(report, null, 2),
|
.file(outputPath)
|
||||||
outputPath,
|
.encoding('utf8')
|
||||||
);
|
.write(JSON.stringify(report, null, 2));
|
||||||
logger.log('info', `Statistics report saved to ${outputPath}`);
|
logger.log('info', `Statistics report saved to ${outputPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,21 +36,27 @@ export class RollbackManager {
|
|||||||
: plugins.path.join(paths.cwd, filepath);
|
: plugins.path.join(paths.cwd, filepath);
|
||||||
|
|
||||||
// Check if file exists
|
// Check if file exists
|
||||||
const exists = await plugins.smartfile.fs.fileExists(absolutePath);
|
const exists = await plugins.smartfs.file(absolutePath).exists();
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
// File doesn't exist yet (will be created), so we skip backup
|
// File doesn't exist yet (will be created), so we skip backup
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read file content and metadata
|
// Read file content and metadata
|
||||||
const content = plugins.smartfile.fs.toStringSync(absolutePath);
|
const content = (await plugins.smartfs
|
||||||
const stats = await plugins.smartfile.fs.stat(absolutePath);
|
.file(absolutePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
const stats = await plugins.smartfs.file(absolutePath).stat();
|
||||||
const checksum = this.calculateChecksum(content);
|
const checksum = this.calculateChecksum(content);
|
||||||
|
|
||||||
// Create backup
|
// Create backup
|
||||||
const backupPath = this.getBackupPath(operationId, filepath);
|
const backupPath = this.getBackupPath(operationId, filepath);
|
||||||
await plugins.smartfile.fs.ensureDir(plugins.path.dirname(backupPath));
|
await plugins.smartfs
|
||||||
await plugins.smartfile.memory.toFs(content, backupPath);
|
.directory(plugins.path.dirname(backupPath))
|
||||||
|
.recursive()
|
||||||
|
.create();
|
||||||
|
await plugins.smartfs.file(backupPath).encoding('utf8').write(content);
|
||||||
|
|
||||||
// Update operation
|
// Update operation
|
||||||
operation.files.push({
|
operation.files.push({
|
||||||
@@ -84,7 +90,10 @@ export class RollbackManager {
|
|||||||
|
|
||||||
// Verify backup integrity
|
// Verify backup integrity
|
||||||
const backupPath = this.getBackupPath(operationId, file.path);
|
const backupPath = this.getBackupPath(operationId, file.path);
|
||||||
const backupContent = plugins.smartfile.fs.toStringSync(backupPath);
|
const backupContent = await plugins.smartfs
|
||||||
|
.file(backupPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read();
|
||||||
const backupChecksum = this.calculateChecksum(backupContent);
|
const backupChecksum = this.calculateChecksum(backupContent);
|
||||||
|
|
||||||
if (backupChecksum !== file.checksum) {
|
if (backupChecksum !== file.checksum) {
|
||||||
@@ -92,7 +101,10 @@ export class RollbackManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restore file
|
// Restore file
|
||||||
await plugins.smartfile.memory.toFs(file.originalContent, absolutePath);
|
await plugins.smartfs
|
||||||
|
.file(absolutePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.write(file.originalContent);
|
||||||
|
|
||||||
// Restore permissions
|
// Restore permissions
|
||||||
const mode = parseInt(file.permissions, 8);
|
const mode = parseInt(file.permissions, 8);
|
||||||
@@ -129,7 +141,7 @@ export class RollbackManager {
|
|||||||
'operations',
|
'operations',
|
||||||
operation.id,
|
operation.id,
|
||||||
);
|
);
|
||||||
await plugins.smartfile.fs.remove(operationDir);
|
await plugins.smartfs.directory(operationDir).recursive().delete();
|
||||||
|
|
||||||
// Remove from manifest
|
// Remove from manifest
|
||||||
manifest.operations = manifest.operations.filter(
|
manifest.operations = manifest.operations.filter(
|
||||||
@@ -148,13 +160,16 @@ export class RollbackManager {
|
|||||||
|
|
||||||
for (const file of operation.files) {
|
for (const file of operation.files) {
|
||||||
const backupPath = this.getBackupPath(operationId, file.path);
|
const backupPath = this.getBackupPath(operationId, file.path);
|
||||||
const exists = await plugins.smartfile.fs.fileExists(backupPath);
|
const exists = await plugins.smartfs.file(backupPath).exists();
|
||||||
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = plugins.smartfile.fs.toStringSync(backupPath);
|
const content = await plugins.smartfs
|
||||||
|
.file(backupPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read();
|
||||||
const checksum = this.calculateChecksum(content);
|
const checksum = this.calculateChecksum(content);
|
||||||
|
|
||||||
if (checksum !== file.checksum) {
|
if (checksum !== file.checksum) {
|
||||||
@@ -171,10 +186,11 @@ export class RollbackManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async ensureBackupDir(): Promise<void> {
|
private async ensureBackupDir(): Promise<void> {
|
||||||
await plugins.smartfile.fs.ensureDir(this.backupDir);
|
await plugins.smartfs.directory(this.backupDir).recursive().create();
|
||||||
await plugins.smartfile.fs.ensureDir(
|
await plugins.smartfs
|
||||||
plugins.path.join(this.backupDir, 'operations'),
|
.directory(plugins.path.join(this.backupDir, 'operations'))
|
||||||
);
|
.recursive()
|
||||||
|
.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateOperationId(): string {
|
private generateOperationId(): string {
|
||||||
@@ -204,13 +220,16 @@ export class RollbackManager {
|
|||||||
private async getManifest(): Promise<{ operations: IFormatOperation[] }> {
|
private async getManifest(): Promise<{ operations: IFormatOperation[] }> {
|
||||||
const defaultManifest = { operations: [] };
|
const defaultManifest = { operations: [] };
|
||||||
|
|
||||||
const exists = await plugins.smartfile.fs.fileExists(this.manifestPath);
|
const exists = await plugins.smartfs.file(this.manifestPath).exists();
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
return defaultManifest;
|
return defaultManifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = plugins.smartfile.fs.toStringSync(this.manifestPath);
|
const content = (await plugins.smartfs
|
||||||
|
.file(this.manifestPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
const manifest = JSON.parse(content);
|
const manifest = JSON.parse(content);
|
||||||
|
|
||||||
// Validate the manifest structure
|
// Validate the manifest structure
|
||||||
@@ -228,7 +247,7 @@ export class RollbackManager {
|
|||||||
);
|
);
|
||||||
// Try to delete the corrupted file
|
// Try to delete the corrupted file
|
||||||
try {
|
try {
|
||||||
await plugins.smartfile.fs.remove(this.manifestPath);
|
await plugins.smartfs.file(this.manifestPath).delete();
|
||||||
} catch (removeError) {
|
} catch (removeError) {
|
||||||
// Ignore removal errors
|
// Ignore removal errors
|
||||||
}
|
}
|
||||||
@@ -249,7 +268,10 @@ export class RollbackManager {
|
|||||||
|
|
||||||
// Write directly with proper JSON stringification
|
// Write directly with proper JSON stringification
|
||||||
const jsonContent = JSON.stringify(manifest, null, 2);
|
const jsonContent = JSON.stringify(manifest, null, 2);
|
||||||
await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath);
|
await plugins.smartfs
|
||||||
|
.file(this.manifestPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.write(jsonContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getOperation(
|
private async getOperation(
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ const filesToDelete = [
|
|||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
export const run = async (projectArg: Project) => {
|
||||||
for (const relativeFilePath of filesToDelete) {
|
for (const relativeFilePath of filesToDelete) {
|
||||||
const fileExists = plugins.smartfile.fs.fileExistsSync(relativeFilePath);
|
const fileExists = await plugins.smartfs.file(relativeFilePath).exists();
|
||||||
if (fileExists) {
|
if (fileExists) {
|
||||||
logger.log('info', `Found ${relativeFilePath}! Removing it!`);
|
logger.log('info', `Found ${relativeFilePath}! Removing it!`);
|
||||||
plugins.smartfile.fs.removeSync(
|
await plugins.smartfs
|
||||||
plugins.path.join(paths.cwd, relativeFilePath),
|
.file(plugins.path.join(paths.cwd, relativeFilePath))
|
||||||
);
|
.delete();
|
||||||
} else {
|
} else {
|
||||||
logger.log('info', `Project is free of ${relativeFilePath}`);
|
logger.log('info', `Project is free of ${relativeFilePath}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,12 @@ export const run = async (projectArg: Project) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Handle glob patterns
|
// Handle glob patterns
|
||||||
const files = await plugins.smartfile.fs.listFileTree('.', pattern.from);
|
const entries = await plugins.smartfs
|
||||||
|
.directory('.')
|
||||||
|
.recursive()
|
||||||
|
.filter(pattern.from)
|
||||||
|
.list();
|
||||||
|
const files = entries.map((entry) => entry.path);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const sourcePath = file;
|
const sourcePath = file;
|
||||||
@@ -46,10 +51,13 @@ export const run = async (projectArg: Project) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure destination directory exists
|
// Ensure destination directory exists
|
||||||
await plugins.smartfile.fs.ensureDir(plugins.path.dirname(destPath));
|
await plugins.smartfs
|
||||||
|
.directory(plugins.path.dirname(destPath))
|
||||||
|
.recursive()
|
||||||
|
.create();
|
||||||
|
|
||||||
// Copy file
|
// Copy file
|
||||||
await plugins.smartfile.fs.copy(sourcePath, destPath);
|
await plugins.smartfs.file(sourcePath).copy(destPath);
|
||||||
logger.log('info', `Copied ${sourcePath} to ${destPath}`);
|
logger.log('info', `Copied ${sourcePath} to ${destPath}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ import { logger } from '../gitzone.logging.js';
|
|||||||
const gitignorePath = plugins.path.join(paths.cwd, './.gitignore');
|
const gitignorePath = plugins.path.join(paths.cwd, './.gitignore');
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
export const run = async (projectArg: Project) => {
|
||||||
const gitignoreExists = await plugins.smartfile.fs.fileExists(gitignorePath);
|
const gitignoreExists = await plugins.smartfs.file(gitignorePath).exists();
|
||||||
let customContent = '';
|
let customContent = '';
|
||||||
|
|
||||||
if (gitignoreExists) {
|
if (gitignoreExists) {
|
||||||
// lets get the existing gitignore file
|
// lets get the existing gitignore file
|
||||||
const existingGitIgnoreString =
|
const existingGitIgnoreString = (await plugins.smartfs
|
||||||
plugins.smartfile.fs.toStringSync(gitignorePath);
|
.file(gitignorePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
// Check for different custom section markers
|
// Check for different custom section markers
|
||||||
const customMarkers = ['#------# custom', '# custom'];
|
const customMarkers = ['#------# custom', '# custom'];
|
||||||
@@ -34,12 +36,17 @@ export const run = async (projectArg: Project) => {
|
|||||||
|
|
||||||
// Append the custom content if it exists
|
// Append the custom content if it exists
|
||||||
if (customContent) {
|
if (customContent) {
|
||||||
const newGitignoreContent =
|
const newGitignoreContent = (await plugins.smartfs
|
||||||
plugins.smartfile.fs.toStringSync(gitignorePath);
|
.file(gitignorePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
// The template already ends with "#------# custom", so just append the content
|
// The template already ends with "#------# custom", so just append the content
|
||||||
const finalContent =
|
const finalContent =
|
||||||
newGitignoreContent.trimEnd() + '\n' + customContent + '\n';
|
newGitignoreContent.trimEnd() + '\n' + customContent + '\n';
|
||||||
await plugins.smartfile.fs.toFs(finalContent, gitignorePath);
|
await plugins.smartfs
|
||||||
|
.file(gitignorePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.write(finalContent);
|
||||||
logger.log('info', 'Updated .gitignore while preserving custom section!');
|
logger.log('info', 'Updated .gitignore while preserving custom section!');
|
||||||
} else {
|
} else {
|
||||||
logger.log('info', 'Added a .gitignore!');
|
logger.log('info', 'Added a .gitignore!');
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { logger } from '../gitzone.logging.js';
|
|||||||
const incompatibleLicenses: string[] = ['AGPL', 'GPL', 'SSPL'];
|
const incompatibleLicenses: string[] = ['AGPL', 'GPL', 'SSPL'];
|
||||||
|
|
||||||
export const run = async (projectArg: Project) => {
|
export const run = async (projectArg: Project) => {
|
||||||
const nodeModulesInstalled = await plugins.smartfile.fs.isDirectory(
|
const nodeModulesInstalled = await plugins.smartfs
|
||||||
plugins.path.join(paths.cwd, 'node_modules'),
|
.directory(plugins.path.join(paths.cwd, 'node_modules'))
|
||||||
);
|
.exists();
|
||||||
if (!nodeModulesInstalled) {
|
if (!nodeModulesInstalled) {
|
||||||
logger.log('warn', 'No node_modules found. Skipping license check');
|
logger.log('warn', 'No node_modules found. Skipping license check');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -3,6 +3,65 @@ import * as paths from '../paths.js';
|
|||||||
import * as gulpFunction from '@push.rocks/gulp-function';
|
import * as gulpFunction from '@push.rocks/gulp-function';
|
||||||
import { Project } from '../classes.project.js';
|
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
|
* runs the npmextra file checking
|
||||||
*/
|
*/
|
||||||
@@ -13,8 +72,14 @@ export const run = async (projectArg: Project) => {
|
|||||||
const fileString = fileArg.contents.toString();
|
const fileString = fileArg.contents.toString();
|
||||||
const npmextraJson = JSON.parse(fileString);
|
const npmextraJson = JSON.parse(fileString);
|
||||||
|
|
||||||
if (!npmextraJson.gitzone) {
|
// Migrate old namespace keys to new package-scoped keys
|
||||||
npmextraJson.gitzone = {};
|
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[] = [
|
const expectedRepoInformation: string[] = [
|
||||||
@@ -31,7 +96,7 @@ export const run = async (projectArg: Project) => {
|
|||||||
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
||||||
if (
|
if (
|
||||||
!plugins.smartobject.smartGet(
|
!plugins.smartobject.smartGet(
|
||||||
npmextraJson.gitzone,
|
npmextraJson['@git.zone/cli'],
|
||||||
expectedRepoInformationItem,
|
expectedRepoInformationItem,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -53,7 +118,7 @@ export const run = async (projectArg: Project) => {
|
|||||||
);
|
);
|
||||||
if (cliProvidedValue) {
|
if (cliProvidedValue) {
|
||||||
plugins.smartobject.smartAdd(
|
plugins.smartobject.smartAdd(
|
||||||
npmextraJson.gitzone,
|
npmextraJson['@git.zone/cli'],
|
||||||
expectedRepoInformationItem,
|
expectedRepoInformationItem,
|
||||||
cliProvidedValue,
|
cliProvidedValue,
|
||||||
);
|
);
|
||||||
@@ -63,8 +128,8 @@ export const run = async (projectArg: Project) => {
|
|||||||
// delete obsolete
|
// delete obsolete
|
||||||
// tbd
|
// tbd
|
||||||
|
|
||||||
if (!npmextraJson.npmci) {
|
if (!npmextraJson['@ship.zone/szci']) {
|
||||||
npmextraJson.npmci = {};
|
npmextraJson['@ship.zone/szci'] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
fileArg.setContentsFromString(JSON.stringify(npmextraJson, null, 2));
|
fileArg.setContentsFromString(JSON.stringify(npmextraJson, null, 2));
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const run = async (projectArg: Project) => {
|
|||||||
plugins.smartgulp.src([`package.json`]),
|
plugins.smartgulp.src([`package.json`]),
|
||||||
gulpFunction.forEach(async (fileArg: plugins.smartfile.SmartFile) => {
|
gulpFunction.forEach(async (fileArg: plugins.smartfile.SmartFile) => {
|
||||||
const npmextraConfig = new plugins.npmextra.Npmextra(paths.cwd);
|
const npmextraConfig = new plugins.npmextra.Npmextra(paths.cwd);
|
||||||
const gitzoneData: any = npmextraConfig.dataFor('gitzone', {});
|
const gitzoneData: any = npmextraConfig.dataFor('@git.zone/cli', {});
|
||||||
const fileString = fileArg.contents.toString();
|
const fileString = fileArg.contents.toString();
|
||||||
const packageJson = JSON.parse(fileString);
|
const packageJson = JSON.parse(fileString);
|
||||||
|
|
||||||
@@ -154,10 +154,11 @@ export const run = async (projectArg: Project) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// check for dependencies
|
// check for dependencies
|
||||||
|
// Note: @push.rocks/tapbundle is deprecated - use @git.zone/tstest/tapbundle instead
|
||||||
await ensureDependency(
|
await ensureDependency(
|
||||||
packageJson,
|
packageJson,
|
||||||
'devDep',
|
'devDep',
|
||||||
'latest',
|
'exclude',
|
||||||
'@push.rocks/tapbundle',
|
'@push.rocks/tapbundle',
|
||||||
);
|
);
|
||||||
await ensureDependency(
|
await ensureDependency(
|
||||||
@@ -174,9 +175,11 @@ export const run = async (projectArg: Project) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// set overrides
|
// set overrides
|
||||||
const overrides = plugins.smartfile.fs.toObjectSync(
|
const overridesContent = (await plugins.smartfs
|
||||||
plugins.path.join(paths.assetsDir, 'overrides.json'),
|
.file(plugins.path.join(paths.assetsDir, 'overrides.json'))
|
||||||
);
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
const overrides = JSON.parse(overridesContent);
|
||||||
packageJson.pnpm = packageJson.pnpm || {};
|
packageJson.pnpm = packageJson.pnpm || {};
|
||||||
packageJson.pnpm.overrides = overrides;
|
packageJson.pnpm.overrides = overrides;
|
||||||
|
|
||||||
|
|||||||
@@ -6,25 +6,22 @@ export const run = async () => {
|
|||||||
const readmeHintsPath = plugins.path.join(paths.cwd, 'readme.hints.md');
|
const readmeHintsPath = plugins.path.join(paths.cwd, 'readme.hints.md');
|
||||||
|
|
||||||
// Check and initialize readme.md if it doesn't exist
|
// Check and initialize readme.md if it doesn't exist
|
||||||
const readmeExists = await plugins.smartfile.fs.fileExists(readmePath);
|
const readmeExists = await plugins.smartfs.file(readmePath).exists();
|
||||||
if (!readmeExists) {
|
if (!readmeExists) {
|
||||||
await plugins.smartfile.fs.toFs(
|
await plugins.smartfs.file(readmePath)
|
||||||
'# Project Readme\n\nThis is the initial readme file.',
|
.encoding('utf8')
|
||||||
readmePath,
|
.write('# Project Readme\n\nThis is the initial readme file.');
|
||||||
);
|
|
||||||
console.log('Initialized readme.md');
|
console.log('Initialized readme.md');
|
||||||
} else {
|
} else {
|
||||||
console.log('readme.md already exists');
|
console.log('readme.md already exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check and initialize readme.hints.md if it doesn't exist
|
// Check and initialize readme.hints.md if it doesn't exist
|
||||||
const readmeHintsExists =
|
const readmeHintsExists = await plugins.smartfs.file(readmeHintsPath).exists();
|
||||||
await plugins.smartfile.fs.fileExists(readmeHintsPath);
|
|
||||||
if (!readmeHintsExists) {
|
if (!readmeHintsExists) {
|
||||||
await plugins.smartfile.fs.toFs(
|
await plugins.smartfs.file(readmeHintsPath)
|
||||||
'# Project Readme Hints\n\nThis is the initial readme hints file.',
|
.encoding('utf8')
|
||||||
readmeHintsPath,
|
.write('# Project Readme Hints\n\nThis is the initial readme hints file.');
|
||||||
);
|
|
||||||
console.log('Initialized readme.hints.md');
|
console.log('Initialized readme.hints.md');
|
||||||
} else {
|
} else {
|
||||||
console.log('readme.hints.md already exists');
|
console.log('readme.hints.md already exists');
|
||||||
|
|||||||
@@ -10,12 +10,6 @@ import { Project } from '../classes.project.js';
|
|||||||
export const run = async (project: Project) => {
|
export const run = async (project: Project) => {
|
||||||
const templateModule = await import('../mod_template/index.js');
|
const templateModule = await import('../mod_template/index.js');
|
||||||
|
|
||||||
// update tslint
|
|
||||||
// getting template
|
|
||||||
const tslintTemplate = await templateModule.getTemplate('tslint');
|
|
||||||
await tslintTemplate.writeToDisk(paths.cwd);
|
|
||||||
logger.log('info', 'Updated tslint.json!');
|
|
||||||
|
|
||||||
// update vscode
|
// update vscode
|
||||||
const vscodeTemplate = await templateModule.getTemplate('vscode');
|
const vscodeTemplate = await templateModule.getTemplate('vscode');
|
||||||
await vscodeTemplate.writeToDisk(paths.cwd);
|
await vscodeTemplate.writeToDisk(paths.cwd);
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import { Project } from '../classes.project.js';
|
|||||||
export const run = async (projectArg: Project) => {
|
export const run = async (projectArg: Project) => {
|
||||||
// lets care about tsconfig.json
|
// lets care about tsconfig.json
|
||||||
logger.log('info', 'Formatting tsconfig.json...');
|
logger.log('info', 'Formatting tsconfig.json...');
|
||||||
const tsconfigSmartfile = await plugins.smartfile.SmartFile.fromFilePath(
|
const factory = plugins.smartfile.SmartFileFactory.nodeFs();
|
||||||
|
const tsconfigSmartfile = await factory.fromFilePath(
|
||||||
plugins.path.join(paths.cwd, 'tsconfig.json'),
|
plugins.path.join(paths.cwd, 'tsconfig.json'),
|
||||||
);
|
);
|
||||||
const tsconfigObject = JSON.parse(tsconfigSmartfile.contentBuffer.toString());
|
const tsconfigObject = JSON.parse(tsconfigSmartfile.parseContentAsString());
|
||||||
tsconfigObject.compilerOptions = tsconfigObject.compilerOptions || {};
|
tsconfigObject.compilerOptions = tsconfigObject.compilerOptions || {};
|
||||||
tsconfigObject.compilerOptions.baseUrl = '.';
|
tsconfigObject.compilerOptions.baseUrl = '.';
|
||||||
tsconfigObject.compilerOptions.paths = {};
|
tsconfigObject.compilerOptions.paths = {};
|
||||||
@@ -23,8 +24,8 @@ export const run = async (projectArg: Project) => {
|
|||||||
`./${publishModule}/index.js`,
|
`./${publishModule}/index.js`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
tsconfigSmartfile.setContentsFromString(
|
await tsconfigSmartfile.editContentAsString(async () => {
|
||||||
JSON.stringify(tsconfigObject, null, 2),
|
return JSON.stringify(tsconfigObject, null, 2);
|
||||||
);
|
});
|
||||||
await tsconfigSmartfile.write();
|
await tsconfigSmartfile.write();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class CleanupFormatter extends BaseFormatter {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const file of filesToRemove) {
|
for (const file of filesToRemove) {
|
||||||
const exists = await plugins.smartfile.fs.fileExists(file);
|
const exists = await plugins.smartfs.file(file).exists();
|
||||||
if (exists) {
|
if (exists) {
|
||||||
changes.push({
|
changes.push({
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
|
|||||||
@@ -1,8 +1,117 @@
|
|||||||
import { LegacyFormatter } from './legacy.formatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import * as formatCopy from '../format.copy.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
export class CopyFormatter extends LegacyFormatter {
|
interface ICopyPattern {
|
||||||
constructor(context: any, project: any) {
|
from: string;
|
||||||
super(context, project, 'copy', formatCopy);
|
to: string;
|
||||||
|
preservePath?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CopyFormatter extends BaseFormatter {
|
||||||
|
get name(): string {
|
||||||
|
return 'copy';
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
|
const changes: IPlannedChange[] = [];
|
||||||
|
|
||||||
|
// Get copy configuration from npmextra.json
|
||||||
|
const npmextraConfig = new plugins.npmextra.Npmextra();
|
||||||
|
const copyConfig = npmextraConfig.dataFor<{ patterns: ICopyPattern[] }>(
|
||||||
|
'gitzone.format.copy',
|
||||||
|
{ patterns: [] },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!copyConfig.patterns || copyConfig.patterns.length === 0) {
|
||||||
|
logVerbose('No copy patterns configured in npmextra.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;
|
||||||
|
|
||||||
|
// Ensure destination directory exists
|
||||||
|
const destDir = plugins.path.dirname(change.path);
|
||||||
|
await plugins.smartfs.directory(destDir).recursive().create();
|
||||||
|
|
||||||
|
if (change.type === 'create') {
|
||||||
|
await this.createFile(change.path, change.content);
|
||||||
|
} else {
|
||||||
|
await this.modifyFile(change.path, change.content);
|
||||||
|
}
|
||||||
|
logger.log('info', `Copied to ${change.path}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,111 @@
|
|||||||
import { LegacyFormatter } from './legacy.formatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import * as formatGitignore from '../format.gitignore.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import { logger } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
export class GitignoreFormatter extends LegacyFormatter {
|
// Standard gitignore template content (without front-matter)
|
||||||
constructor(context: any, project: any) {
|
const GITIGNORE_TEMPLATE = `.nogit/
|
||||||
super(context, project, 'gitignore', formatGitignore);
|
|
||||||
|
# artifacts
|
||||||
|
coverage/
|
||||||
|
public/
|
||||||
|
|
||||||
|
# installs
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.yarn/
|
||||||
|
.cache/
|
||||||
|
.rpt2_cache
|
||||||
|
|
||||||
|
# builds
|
||||||
|
dist/
|
||||||
|
dist_*/
|
||||||
|
|
||||||
|
# AI
|
||||||
|
.claude/
|
||||||
|
.serena/
|
||||||
|
|
||||||
|
#------# custom`;
|
||||||
|
|
||||||
|
export class GitignoreFormatter extends BaseFormatter {
|
||||||
|
get name(): string {
|
||||||
|
return 'gitignore';
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
|
const changes: IPlannedChange[] = [];
|
||||||
|
const gitignorePath = '.gitignore';
|
||||||
|
|
||||||
|
// 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 = GITIGNORE_TEMPLATE;
|
||||||
|
if (customContent) {
|
||||||
|
newContent = GITIGNORE_TEMPLATE + '\n' + customContent + '\n';
|
||||||
|
} else {
|
||||||
|
newContent = GITIGNORE_TEMPLATE + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read current content to compare
|
||||||
|
let currentContent = '';
|
||||||
|
if (exists) {
|
||||||
|
currentContent = (await plugins.smartfs
|
||||||
|
.file(gitignorePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine change type
|
||||||
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,62 @@
|
|||||||
import { LegacyFormatter } from './legacy.formatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import * as formatLicense from '../format.license.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import * as paths from '../../paths.js';
|
||||||
|
import { logger } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
export class LicenseFormatter extends LegacyFormatter {
|
const INCOMPATIBLE_LICENSES: string[] = ['AGPL', 'GPL', 'SSPL'];
|
||||||
constructor(context: any, project: any) {
|
|
||||||
super(context, project, 'license', formatLicense);
|
export class LicenseFormatter extends BaseFormatter {
|
||||||
|
get name(): string {
|
||||||
|
return 'license';
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
|
// License formatter only checks for incompatible licenses
|
||||||
|
// It does not modify any files, so return empty array
|
||||||
|
// The actual check happens in execute() for reporting purposes
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(changes: IPlannedChange[]): Promise<void> {
|
||||||
|
const startTime = this.stats.moduleStartTime(this.name);
|
||||||
|
this.stats.startModule(this.name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if node_modules exists
|
||||||
|
const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
|
||||||
|
const nodeModulesExists = await plugins.smartfs
|
||||||
|
.directory(nodeModulesPath)
|
||||||
|
.exists();
|
||||||
|
|
||||||
|
if (!nodeModulesExists) {
|
||||||
|
logger.log('warn', 'No node_modules found. Skipping license check');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run license check
|
||||||
|
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
|
||||||
|
const licenseCheckResult = await licenseChecker.excludeLicenseWithinPath(
|
||||||
|
paths.cwd,
|
||||||
|
INCOMPATIBLE_LICENSES,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (licenseCheckResult.failingModules.length === 0) {
|
||||||
|
logger.log('info', 'License check passed - no incompatible licenses found');
|
||||||
|
} else {
|
||||||
|
logger.log('error', 'License check failed - incompatible licenses found:');
|
||||||
|
for (const failedModule of licenseCheckResult.failingModules) {
|
||||||
|
console.log(
|
||||||
|
` ${failedModule.name} has license ${failedModule.license}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.stats.endModule(this.name, startTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
|
// No file changes for license formatter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,165 @@
|
|||||||
import { LegacyFormatter } from './legacy.formatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import * as formatNpmextra from '../format.npmextra.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
export class NpmextraFormatter extends LegacyFormatter {
|
/**
|
||||||
constructor(context: any, project: any) {
|
* Migrates npmextra.json from old namespace keys to new package-scoped keys
|
||||||
super(context, project, 'npmextra', formatNpmextra);
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return migrated;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates npmAccessLevel from @ship.zone/szci to @git.zone/cli.release.accessLevel
|
||||||
|
*/
|
||||||
|
const migrateAccessLevel = (npmextraJson: any): boolean => {
|
||||||
|
const szciConfig = npmextraJson['@ship.zone/szci'];
|
||||||
|
|
||||||
|
if (!szciConfig?.npmAccessLevel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gitzoneConfig = npmextraJson['@git.zone/cli'] || {};
|
||||||
|
if (gitzoneConfig?.release?.accessLevel) {
|
||||||
|
delete szciConfig.npmAccessLevel;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!npmextraJson['@git.zone/cli']) {
|
||||||
|
npmextraJson['@git.zone/cli'] = {};
|
||||||
|
}
|
||||||
|
if (!npmextraJson['@git.zone/cli'].release) {
|
||||||
|
npmextraJson['@git.zone/cli'].release = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
npmextraJson['@git.zone/cli'].release.accessLevel = szciConfig.npmAccessLevel;
|
||||||
|
delete szciConfig.npmAccessLevel;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class NpmextraFormatter extends BaseFormatter {
|
||||||
|
get name(): string {
|
||||||
|
return 'npmextra';
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
|
const changes: IPlannedChange[] = [];
|
||||||
|
const npmextraPath = 'npmextra.json';
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
const exists = await plugins.smartfs.file(npmextraPath).exists();
|
||||||
|
if (!exists) {
|
||||||
|
logVerbose('npmextra.json does not exist, skipping');
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read current content
|
||||||
|
const currentContent = (await plugins.smartfs
|
||||||
|
.file(npmextraPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
|
// Parse and compute new content
|
||||||
|
const npmextraJson = JSON.parse(currentContent);
|
||||||
|
|
||||||
|
// Apply migrations (these are automatic, non-interactive)
|
||||||
|
migrateNamespaceKeys(npmextraJson);
|
||||||
|
migrateAccessLevel(npmextraJson);
|
||||||
|
|
||||||
|
// Ensure namespaces exist
|
||||||
|
if (!npmextraJson['@git.zone/cli']) {
|
||||||
|
npmextraJson['@git.zone/cli'] = {};
|
||||||
|
}
|
||||||
|
if (!npmextraJson['@ship.zone/szci']) {
|
||||||
|
npmextraJson['@ship.zone/szci'] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContent = JSON.stringify(npmextraJson, null, 2);
|
||||||
|
|
||||||
|
// Only add change if content differs
|
||||||
|
if (newContent !== currentContent) {
|
||||||
|
changes.push({
|
||||||
|
type: 'modify',
|
||||||
|
path: npmextraPath,
|
||||||
|
module: this.name,
|
||||||
|
description: 'Migrate and format npmextra.json',
|
||||||
|
content: newContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
|
if (change.type !== 'modify' || !change.content) return;
|
||||||
|
|
||||||
|
// Parse the content to check for missing required fields
|
||||||
|
const npmextraJson = JSON.parse(change.content);
|
||||||
|
|
||||||
|
// Check for missing required module information
|
||||||
|
const expectedRepoInformation: string[] = [
|
||||||
|
'projectType',
|
||||||
|
'module.githost',
|
||||||
|
'module.gitscope',
|
||||||
|
'module.gitrepo',
|
||||||
|
'module.description',
|
||||||
|
'module.npmPackagename',
|
||||||
|
'module.license',
|
||||||
|
];
|
||||||
|
|
||||||
|
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||||
|
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
||||||
|
if (
|
||||||
|
!plugins.smartobject.smartGet(
|
||||||
|
npmextraJson['@git.zone/cli'],
|
||||||
|
expectedRepoInformationItem,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
interactInstance.addQuestions([
|
||||||
|
{
|
||||||
|
message: `What is the value of ${expectedRepoInformationItem}`,
|
||||||
|
name: expectedRepoInformationItem,
|
||||||
|
type: 'input',
|
||||||
|
default: 'undefined variable',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const answerbucket = await interactInstance.runQueue();
|
||||||
|
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
||||||
|
const cliProvidedValue = answerbucket.getAnswerFor(
|
||||||
|
expectedRepoInformationItem,
|
||||||
|
);
|
||||||
|
if (cliProvidedValue) {
|
||||||
|
plugins.smartobject.smartAdd(
|
||||||
|
npmextraJson['@git.zone/cli'],
|
||||||
|
expectedRepoInformationItem,
|
||||||
|
cliProvidedValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the final content
|
||||||
|
const finalContent = JSON.stringify(npmextraJson, null, 2);
|
||||||
|
await this.modifyFile(change.path, finalContent);
|
||||||
|
logger.log('info', 'Updated npmextra.json');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,213 @@
|
|||||||
import { LegacyFormatter } from './legacy.formatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import * as formatPackageJson from '../format.packagejson.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import * as paths from '../../paths.js';
|
||||||
|
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
export class PackageJsonFormatter extends LegacyFormatter {
|
/**
|
||||||
constructor(context: any, project: any) {
|
* Ensures a certain dependency exists or is excluded
|
||||||
super(context, project, 'packagejson', formatPackageJson);
|
*/
|
||||||
|
const ensureDependency = async (
|
||||||
|
packageJsonObject: any,
|
||||||
|
position: 'dep' | 'devDep' | 'everywhere',
|
||||||
|
constraint: 'exclude' | 'include' | 'latest',
|
||||||
|
dependencyArg: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
// Parse package name and version, handling scoped packages like @scope/name@version
|
||||||
|
const isScoped = dependencyArg.startsWith('@');
|
||||||
|
const lastAtIndex = dependencyArg.lastIndexOf('@');
|
||||||
|
|
||||||
|
// For scoped packages, the version @ must come after the /
|
||||||
|
// For unscoped packages, any @ indicates a version
|
||||||
|
const hasVersion = isScoped
|
||||||
|
? lastAtIndex > dependencyArg.indexOf('/')
|
||||||
|
: lastAtIndex >= 0;
|
||||||
|
|
||||||
|
const packageName = hasVersion ? dependencyArg.slice(0, lastAtIndex) : dependencyArg;
|
||||||
|
const version = hasVersion ? dependencyArg.slice(lastAtIndex + 1) : 'latest';
|
||||||
|
|
||||||
|
const targetSections: string[] = [];
|
||||||
|
|
||||||
|
switch (position) {
|
||||||
|
case 'dep':
|
||||||
|
targetSections.push('dependencies');
|
||||||
|
break;
|
||||||
|
case 'devDep':
|
||||||
|
targetSections.push('devDependencies');
|
||||||
|
break;
|
||||||
|
case 'everywhere':
|
||||||
|
targetSections.push('dependencies', 'devDependencies');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const section of targetSections) {
|
||||||
|
if (!packageJsonObject[section]) {
|
||||||
|
packageJsonObject[section] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (constraint) {
|
||||||
|
case 'exclude':
|
||||||
|
delete packageJsonObject[section][packageName];
|
||||||
|
break;
|
||||||
|
case 'include':
|
||||||
|
if (!packageJsonObject[section][packageName]) {
|
||||||
|
packageJsonObject[section][packageName] =
|
||||||
|
version === 'latest' ? '^1.0.0' : version;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'latest':
|
||||||
|
try {
|
||||||
|
const registry = new plugins.smartnpm.NpmRegistry();
|
||||||
|
const packageInfo = await registry.getPackageInfo(packageName);
|
||||||
|
const latestVersion = packageInfo['dist-tags'].latest;
|
||||||
|
packageJsonObject[section][packageName] = `^${latestVersion}`;
|
||||||
|
} catch (error) {
|
||||||
|
logVerbose(
|
||||||
|
`Could not fetch latest version for ${packageName}, using existing or default`,
|
||||||
|
);
|
||||||
|
if (!packageJsonObject[section][packageName]) {
|
||||||
|
packageJsonObject[section][packageName] =
|
||||||
|
version === 'latest' ? '^1.0.0' : version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PackageJsonFormatter extends BaseFormatter {
|
||||||
|
get name(): string {
|
||||||
|
return 'packagejson';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 npmextra
|
||||||
|
const npmextraConfig = new plugins.npmextra.Npmextra(paths.cwd);
|
||||||
|
const gitzoneData: any = npmextraConfig.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"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure buildDocs script exists
|
||||||
|
if (!packageJson.scripts.buildDocs) {
|
||||||
|
packageJson.scripts.buildDocs = `tsdoc`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set files array
|
||||||
|
packageJson.files = [
|
||||||
|
'ts/**/*',
|
||||||
|
'ts_web/**/*',
|
||||||
|
'dist/**/*',
|
||||||
|
'dist_*/**/*',
|
||||||
|
'dist_ts/**/*',
|
||||||
|
'dist_ts_web/**/*',
|
||||||
|
'assets/**/*',
|
||||||
|
'cli.js',
|
||||||
|
'npmextra.json',
|
||||||
|
'readme.md',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Handle dependencies
|
||||||
|
await ensureDependency(
|
||||||
|
packageJson,
|
||||||
|
'devDep',
|
||||||
|
'exclude',
|
||||||
|
'@push.rocks/tapbundle',
|
||||||
|
);
|
||||||
|
await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tstest');
|
||||||
|
await ensureDependency(
|
||||||
|
packageJson,
|
||||||
|
'devDep',
|
||||||
|
'latest',
|
||||||
|
'@git.zone/tsbuild',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set pnpm overrides from assets
|
||||||
|
try {
|
||||||
|
const overridesContent = (await plugins.smartfs
|
||||||
|
.file(plugins.path.join(paths.assetsDir, 'overrides.json'))
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
const overrides = JSON.parse(overridesContent);
|
||||||
|
packageJson.pnpm = packageJson.pnpm || {};
|
||||||
|
packageJson.pnpm.overrides = overrides;
|
||||||
|
} catch (error) {
|
||||||
|
logVerbose(`Could not read overrides.json: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContent = JSON.stringify(packageJson, null, 2);
|
||||||
|
|
||||||
|
// Only add change if content differs
|
||||||
|
if (newContent !== currentContent) {
|
||||||
|
changes.push({
|
||||||
|
type: 'modify',
|
||||||
|
path: packageJsonPath,
|
||||||
|
module: this.name,
|
||||||
|
description: 'Format package.json',
|
||||||
|
content: newContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
|
if (change.type !== 'modify' || !change.content) return;
|
||||||
|
|
||||||
|
await this.modifyFile(change.path, change.content);
|
||||||
|
logger.log('info', 'Updated package.json');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import type { IPlannedChange } from '../interfaces.format.js';
|
import type { IPlannedChange, ICheckResult } from '../interfaces.format.js';
|
||||||
import * as plugins from '../mod.plugins.js';
|
import * as plugins from '../mod.plugins.js';
|
||||||
import { logger, logVerbose } from '../../gitzone.logging.js';
|
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
@@ -40,20 +40,40 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
|
|
||||||
// Add files from TypeScript directories
|
// Add files from TypeScript directories
|
||||||
for (const dir of includeDirs) {
|
for (const dir of includeDirs) {
|
||||||
const globPattern = `${dir}/**/*.${extensions}`;
|
try {
|
||||||
const dirFiles = await plugins.smartfile.fs.listFileTree(
|
const globPattern = `${dir}/**/*.${extensions}`;
|
||||||
'.',
|
const dirEntries = await plugins.smartfs
|
||||||
globPattern,
|
.directory('.')
|
||||||
);
|
.recursive()
|
||||||
allFiles.push(...dirFiles);
|
.filter(globPattern)
|
||||||
|
.list();
|
||||||
|
const dirFiles = dirEntries.map((entry) => entry.path);
|
||||||
|
// Filter out files in excluded directories
|
||||||
|
const filteredFiles = dirFiles.filter((f) =>
|
||||||
|
!f.includes('node_modules/') &&
|
||||||
|
!f.includes('.nogit/') &&
|
||||||
|
!f.includes('.git/')
|
||||||
|
);
|
||||||
|
allFiles.push(...filteredFiles);
|
||||||
|
} catch (error) {
|
||||||
|
logVerbose(`Skipping directory ${dir}: ${error.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add root config files
|
// Add root config files (only check root level, no recursive needed)
|
||||||
for (const pattern of rootConfigFiles) {
|
for (const pattern of rootConfigFiles) {
|
||||||
const rootFiles = await plugins.smartfile.fs.listFileTree('.', pattern);
|
try {
|
||||||
// Only include files at root level (no slashes in path)
|
const rootEntries = await plugins.smartfs
|
||||||
const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
|
.directory('.')
|
||||||
allFiles.push(...rootLevelFiles);
|
.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
|
// Remove duplicates
|
||||||
@@ -66,8 +86,8 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
const validFiles: string[] = [];
|
const validFiles: string[] = [];
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
try {
|
try {
|
||||||
const stats = await plugins.smartfile.fs.stat(file);
|
const stats = await plugins.smartfs.file(file).stat();
|
||||||
if (!stats.isDirectory()) {
|
if (!stats.isDirectory) {
|
||||||
validFiles.push(file);
|
validFiles.push(file);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -148,7 +168,10 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read current content
|
// Read current content
|
||||||
const content = plugins.smartfile.fs.toStringSync(change.path);
|
const content = (await plugins.smartfs
|
||||||
|
.file(change.path)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
// Format with prettier
|
// Format with prettier
|
||||||
const prettier = await import('prettier');
|
const prettier = await import('prettier');
|
||||||
@@ -220,4 +243,53 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
arrowParens: 'always',
|
arrowParens: 'always',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override check() to compute diffs on-the-fly by running prettier
|
||||||
|
*/
|
||||||
|
async check(): Promise<ICheckResult> {
|
||||||
|
const changes = await this.analyze();
|
||||||
|
const diffs: ICheckResult['diffs'] = [];
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
if (change.type !== 'modify') continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read current content
|
||||||
|
const currentContent = (await plugins.smartfs
|
||||||
|
.file(change.path)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
|
// Skip files without extension (prettier can't infer parser)
|
||||||
|
const fileExt = plugins.path.extname(change.path).toLowerCase();
|
||||||
|
if (!fileExt) continue;
|
||||||
|
|
||||||
|
// Format with prettier to get what it would produce
|
||||||
|
const prettier = await import('prettier');
|
||||||
|
const formatted = await prettier.format(currentContent, {
|
||||||
|
filepath: change.path,
|
||||||
|
...(await this.getPrettierConfig()),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only add to diffs if content differs
|
||||||
|
if (formatted !== currentContent) {
|
||||||
|
diffs.push({
|
||||||
|
path: change.path,
|
||||||
|
type: 'modify',
|
||||||
|
before: currentContent,
|
||||||
|
after: formatted,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Skip files that can't be processed
|
||||||
|
logVerbose(`Skipping diff for ${change.path}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasDiff: diffs.length > 0,
|
||||||
|
diffs,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import type { IPlannedChange } from '../interfaces.format.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
import * as formatReadme from '../format.readme.js';
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import { logger } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
|
const DEFAULT_README_CONTENT = `# Project Readme
|
||||||
|
|
||||||
|
This is the initial readme file.`;
|
||||||
|
|
||||||
|
const DEFAULT_README_HINTS_CONTENT = `# Project Readme Hints
|
||||||
|
|
||||||
|
This is the initial readme hints file.`;
|
||||||
|
|
||||||
export class ReadmeFormatter extends BaseFormatter {
|
export class ReadmeFormatter extends BaseFormatter {
|
||||||
get name(): string {
|
get name(): string {
|
||||||
@@ -8,17 +17,39 @@ export class ReadmeFormatter extends BaseFormatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async analyze(): Promise<IPlannedChange[]> {
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
return [
|
const changes: IPlannedChange[] = [];
|
||||||
{
|
|
||||||
type: 'modify',
|
// Check readme.md
|
||||||
|
const readmeExists = await plugins.smartfs.file('readme.md').exists();
|
||||||
|
if (!readmeExists) {
|
||||||
|
changes.push({
|
||||||
|
type: 'create',
|
||||||
path: 'readme.md',
|
path: 'readme.md',
|
||||||
module: this.name,
|
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> {
|
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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,155 @@
|
|||||||
import { LegacyFormatter } from './legacy.formatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import * as formatTemplates from '../format.templates.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import * as paths from '../../paths.js';
|
||||||
|
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
export class TemplatesFormatter extends LegacyFormatter {
|
export class TemplatesFormatter extends BaseFormatter {
|
||||||
constructor(context: any, project: any) {
|
get name(): string {
|
||||||
super(context, project, 'templates', formatTemplates);
|
return 'templates';
|
||||||
|
}
|
||||||
|
|
||||||
|
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?.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 === 'service') {
|
||||||
|
const serviceChanges = await this.analyzeTemplate('service_update', []);
|
||||||
|
changes.push(...serviceChanges);
|
||||||
|
} else if (projectType === 'wcc') {
|
||||||
|
const wccChanges = await this.analyzeTemplate('wcc_update', [
|
||||||
|
{ templatePath: 'html/index.html', destPath: 'html/index.html' },
|
||||||
|
{ 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);
|
||||||
|
|
||||||
|
// Check if template exists
|
||||||
|
const templateExists = await plugins.smartfs.directory(templateDir).exists();
|
||||||
|
if (!templateExists) {
|
||||||
|
logVerbose(`Template ${templateName} not found`);
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const templateFilePath = plugins.path.join(templateDir, file.templatePath);
|
||||||
|
const destFilePath = file.destPath;
|
||||||
|
|
||||||
|
// Check if template file exists
|
||||||
|
const fileExists = await plugins.smartfs.file(templateFilePath).exists();
|
||||||
|
if (!fileExists) {
|
||||||
|
logVerbose(`Template file ${templateFilePath} not found`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read template content
|
||||||
|
const templateContent = (await plugins.smartfs
|
||||||
|
.file(templateFilePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
|
// Check if destination file exists
|
||||||
|
const destExists = await plugins.smartfs.file(destFilePath).exists();
|
||||||
|
let currentContent = '';
|
||||||
|
if (destExists) {
|
||||||
|
currentContent = (await plugins.smartfs
|
||||||
|
.file(destFilePath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add change if content differs
|
||||||
|
if (templateContent !== currentContent) {
|
||||||
|
changes.push({
|
||||||
|
type: destExists ? 'modify' : 'create',
|
||||||
|
path: destFilePath,
|
||||||
|
module: this.name,
|
||||||
|
description: `Apply template ${templateName}/${file.templatePath}`,
|
||||||
|
content: templateContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logVerbose(`Failed to read template ${templateFilePath}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
|
if (!change.content) return;
|
||||||
|
|
||||||
|
// Ensure destination directory exists
|
||||||
|
const destDir = plugins.path.dirname(change.path);
|
||||||
|
if (destDir && destDir !== '.') {
|
||||||
|
await plugins.smartfs.directory(destDir).recursive().create();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.type === 'create') {
|
||||||
|
await this.createFile(change.path, change.content);
|
||||||
|
} else {
|
||||||
|
await this.modifyFile(change.path, change.content);
|
||||||
|
}
|
||||||
|
logger.log('info', `Applied template to ${change.path}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,73 @@
|
|||||||
import { LegacyFormatter } from './legacy.formatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import * as formatTsconfig from '../format.tsconfig.js';
|
import type { IPlannedChange } from '../interfaces.format.js';
|
||||||
|
import * as plugins from '../mod.plugins.js';
|
||||||
|
import * as paths from '../../paths.js';
|
||||||
|
import { logger, logVerbose } from '../../gitzone.logging.js';
|
||||||
|
|
||||||
export class TsconfigFormatter extends LegacyFormatter {
|
export class TsconfigFormatter extends BaseFormatter {
|
||||||
constructor(context: any, project: any) {
|
get name(): string {
|
||||||
super(context, project, 'tsconfig', formatTsconfig);
|
return 'tsconfig';
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
|
const changes: IPlannedChange[] = [];
|
||||||
|
const tsconfigPath = 'tsconfig.json';
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
const exists = await plugins.smartfs.file(tsconfigPath).exists();
|
||||||
|
if (!exists) {
|
||||||
|
logVerbose('tsconfig.json does not exist, skipping');
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read current content
|
||||||
|
const currentContent = (await plugins.smartfs
|
||||||
|
.file(tsconfigPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
|
||||||
|
// Parse and compute new content
|
||||||
|
const tsconfigObject = JSON.parse(currentContent);
|
||||||
|
tsconfigObject.compilerOptions = tsconfigObject.compilerOptions || {};
|
||||||
|
tsconfigObject.compilerOptions.baseUrl = '.';
|
||||||
|
tsconfigObject.compilerOptions.paths = {};
|
||||||
|
|
||||||
|
// Get module paths from tspublish
|
||||||
|
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];
|
||||||
|
tsconfigObject.compilerOptions.paths[`${publishConfig.name}`] = [
|
||||||
|
`./${publishModule}/index.js`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logVerbose(`Could not get tspublish modules: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as plugins from './mod.plugins.js';
|
|||||||
import { Project } from '../classes.project.js';
|
import { Project } from '../classes.project.js';
|
||||||
import { FormatContext } from './classes.formatcontext.js';
|
import { FormatContext } from './classes.formatcontext.js';
|
||||||
import { FormatPlanner } from './classes.formatplanner.js';
|
import { FormatPlanner } from './classes.formatplanner.js';
|
||||||
|
import { BaseFormatter } from './classes.baseformatter.js';
|
||||||
import { logger, setVerboseMode } from '../gitzone.logging.js';
|
import { logger, setVerboseMode } from '../gitzone.logging.js';
|
||||||
|
|
||||||
// Import wrapper classes for formatters
|
// Import wrapper classes for formatters
|
||||||
@@ -18,7 +19,8 @@ import { CopyFormatter } from './formatters/copy.formatter.js';
|
|||||||
|
|
||||||
export let run = async (
|
export let run = async (
|
||||||
options: {
|
options: {
|
||||||
dryRun?: boolean;
|
write?: boolean; // Explicitly write changes (default: false, dry-mode)
|
||||||
|
dryRun?: boolean; // Deprecated, kept for compatibility
|
||||||
yes?: boolean;
|
yes?: boolean;
|
||||||
planOnly?: boolean;
|
planOnly?: boolean;
|
||||||
savePlan?: string;
|
savePlan?: string;
|
||||||
@@ -27,6 +29,7 @@ export let run = async (
|
|||||||
interactive?: boolean;
|
interactive?: boolean;
|
||||||
parallel?: boolean;
|
parallel?: boolean;
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
|
diff?: boolean; // Show file diffs
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<any> => {
|
): Promise<any> => {
|
||||||
// Set verbose mode if requested
|
// Set verbose mode if requested
|
||||||
@@ -34,14 +37,18 @@ export let run = async (
|
|||||||
setVerboseMode(true);
|
setVerboseMode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = await Project.fromCwd();
|
// 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();
|
const context = new FormatContext();
|
||||||
// Cache system removed - no longer needed
|
// Cache system removed - no longer needed
|
||||||
const planner = new FormatPlanner();
|
const planner = new FormatPlanner();
|
||||||
|
|
||||||
// Get configuration from npmextra
|
// Get configuration from npmextra
|
||||||
const npmextraConfig = new plugins.npmextra.Npmextra();
|
const npmextraConfig = new plugins.npmextra.Npmextra();
|
||||||
const formatConfig = npmextraConfig.dataFor<any>('gitzone.format', {
|
const formatConfig = npmextraConfig.dataFor<any>('@git.zone/cli.format', {
|
||||||
interactive: true,
|
interactive: true,
|
||||||
showDiffs: false,
|
showDiffs: false,
|
||||||
autoApprove: false,
|
autoApprove: false,
|
||||||
@@ -101,7 +108,12 @@ export let run = async (
|
|||||||
// Plan phase
|
// Plan phase
|
||||||
logger.log('info', 'Analyzing project for format operations...');
|
logger.log('info', 'Analyzing project for format operations...');
|
||||||
let plan = options.fromPlan
|
let plan = options.fromPlan
|
||||||
? JSON.parse(await plugins.smartfile.fs.toStringSync(options.fromPlan))
|
? JSON.parse(
|
||||||
|
(await plugins.smartfs
|
||||||
|
.file(options.fromPlan)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string,
|
||||||
|
)
|
||||||
: await planner.planFormat(activeFormatters);
|
: await planner.planFormat(activeFormatters);
|
||||||
|
|
||||||
// Display plan
|
// Display plan
|
||||||
@@ -109,10 +121,10 @@ export let run = async (
|
|||||||
|
|
||||||
// Save plan if requested
|
// Save plan if requested
|
||||||
if (options.savePlan) {
|
if (options.savePlan) {
|
||||||
await plugins.smartfile.memory.toFs(
|
await plugins.smartfs
|
||||||
JSON.stringify(plan, null, 2),
|
.file(options.savePlan)
|
||||||
options.savePlan,
|
.encoding('utf8')
|
||||||
);
|
.write(JSON.stringify(plan, null, 2));
|
||||||
logger.log('info', `Plan saved to ${options.savePlan}`);
|
logger.log('info', `Plan saved to ${options.savePlan}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,9 +133,24 @@ export let run = async (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dry-run mode
|
// Show diffs if requested (works in both dry-run and write modes)
|
||||||
if (options.dryRun) {
|
if (options.diff) {
|
||||||
logger.log('info', 'Dry-run mode - no changes will be made');
|
logger.log('info', 'Showing file diffs:');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
for (const formatter of activeFormatters) {
|
||||||
|
const checkResult = await formatter.check();
|
||||||
|
if (checkResult.hasDiff) {
|
||||||
|
logger.log('info', `[${formatter.name}]`);
|
||||||
|
formatter.displayAllDiffs(checkResult);
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dry-run mode (default behavior)
|
||||||
|
if (!shouldWrite) {
|
||||||
|
logger.log('info', 'Dry-run mode - use --write (-w) to apply changes');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,3 +217,68 @@ export const handleCleanBackups = async (): Promise<void> => {
|
|||||||
'Backup cleaning has been disabled - backup system removed',
|
'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'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a single formatter by name (for use by other modules)
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Map formatter names to classes
|
||||||
|
const formatterMap: Record<string, new (ctx: FormatContext, proj: Project) => BaseFormatter> = {
|
||||||
|
cleanup: CleanupFormatter,
|
||||||
|
npmextra: NpmextraFormatter,
|
||||||
|
license: LicenseFormatter,
|
||||||
|
packagejson: PackageJsonFormatter,
|
||||||
|
templates: TemplatesFormatter,
|
||||||
|
gitignore: GitignoreFormatter,
|
||||||
|
tsconfig: TsconfigFormatter,
|
||||||
|
prettier: PrettierFormatter,
|
||||||
|
readme: ReadmeFormatter,
|
||||||
|
copy: CopyFormatter,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormatterClass = formatterMap[formatterName];
|
||||||
|
if (!FormatterClass) {
|
||||||
|
throw new Error(`Unknown formatter: ${formatterName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
formatter.displayAllDiffs(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal mode: analyze and apply changes
|
||||||
|
const changes = await formatter.analyze();
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
await formatter.applyChange(change);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.silent) {
|
||||||
|
logger.log('success', `Formatter '${formatterName}' completed`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -39,7 +39,18 @@ export type IPlannedChange = {
|
|||||||
path: string;
|
path: string;
|
||||||
module: string;
|
module: string;
|
||||||
description: string;
|
description: string;
|
||||||
content?: string; // For create/modify operations
|
content?: string; // New content for create/modify operations
|
||||||
|
originalContent?: string; // Original content for comparison
|
||||||
diff?: string;
|
diff?: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ICheckResult {
|
||||||
|
hasDiff: boolean;
|
||||||
|
diffs: Array<{
|
||||||
|
path: string;
|
||||||
|
type: 'create' | 'modify' | 'delete';
|
||||||
|
before?: string;
|
||||||
|
after?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,15 +48,17 @@ export class Meta {
|
|||||||
public async readDirectory() {
|
public async readDirectory() {
|
||||||
await this.syncToRemote(true);
|
await this.syncToRemote(true);
|
||||||
logger.log('info', `reading directory`);
|
logger.log('info', `reading directory`);
|
||||||
const metaFileExists = plugins.smartfile.fs.fileExistsSync(
|
const metaFileExists = await plugins.smartfs
|
||||||
this.filePaths.metaJson,
|
.file(this.filePaths.metaJson)
|
||||||
);
|
.exists();
|
||||||
if (!metaFileExists) {
|
if (!metaFileExists) {
|
||||||
throw new Error(`meta file does not exist at ${this.filePaths.metaJson}`);
|
throw new Error(`meta file does not exist at ${this.filePaths.metaJson}`);
|
||||||
}
|
}
|
||||||
this.metaRepoData = plugins.smartfile.fs.toObjectSync(
|
const content = (await plugins.smartfs
|
||||||
this.filePaths.metaJson,
|
.file(this.filePaths.metaJson)
|
||||||
);
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
this.metaRepoData = JSON.parse(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,15 +80,15 @@ export class Meta {
|
|||||||
*/
|
*/
|
||||||
public async writeToDisk() {
|
public async writeToDisk() {
|
||||||
// write .meta.json to disk
|
// write .meta.json to disk
|
||||||
plugins.smartfile.memory.toFsSync(
|
await plugins.smartfs
|
||||||
JSON.stringify(this.metaRepoData, null, 2),
|
.file(this.filePaths.metaJson)
|
||||||
this.filePaths.metaJson,
|
.encoding('utf8')
|
||||||
);
|
.write(JSON.stringify(this.metaRepoData, null, 2));
|
||||||
// write .gitignore to disk
|
// write .gitignore to disk
|
||||||
plugins.smartfile.memory.toFsSync(
|
await plugins.smartfs
|
||||||
await this.generateGitignore(),
|
.file(this.filePaths.gitIgnore)
|
||||||
this.filePaths.gitIgnore,
|
.encoding('utf8')
|
||||||
);
|
.write(await this.generateGitignore());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,10 +114,25 @@ export class Meta {
|
|||||||
*/
|
*/
|
||||||
public async updateLocalRepos() {
|
public async updateLocalRepos() {
|
||||||
await this.syncToRemote();
|
await this.syncToRemote();
|
||||||
const projects = plugins.smartfile.fs.toObjectSync(
|
const metaContent = (await plugins.smartfs
|
||||||
this.filePaths.metaJson,
|
.file(this.filePaths.metaJson)
|
||||||
).projects;
|
.encoding('utf8')
|
||||||
const preExistingFolders = plugins.smartfile.fs.listFoldersSync(this.cwd);
|
.read()) as string;
|
||||||
|
const projects = JSON.parse(metaContent).projects;
|
||||||
|
const entries = await plugins.smartfs.directory(this.cwd).list();
|
||||||
|
const preExistingFolders: string[] = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
try {
|
||||||
|
const stats = await plugins.smartfs
|
||||||
|
.file(plugins.path.join(this.cwd, entry.path))
|
||||||
|
.stat();
|
||||||
|
if (stats.isDirectory) {
|
||||||
|
preExistingFolders.push(entry.name);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip entries that can't be accessed
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const preExistingFolderArg of preExistingFolders) {
|
for (const preExistingFolderArg of preExistingFolders) {
|
||||||
if (
|
if (
|
||||||
preExistingFolderArg !== '.git' &&
|
preExistingFolderArg !== '.git' &&
|
||||||
@@ -143,9 +160,17 @@ export class Meta {
|
|||||||
await this.sortMetaRepoData();
|
await this.sortMetaRepoData();
|
||||||
const missingRepos: string[] = [];
|
const missingRepos: string[] = [];
|
||||||
for (const key of Object.keys(this.metaRepoData.projects)) {
|
for (const key of Object.keys(this.metaRepoData.projects)) {
|
||||||
plugins.smartfile.fs.isDirectory(key)
|
const fullPath = plugins.path.join(this.cwd, key);
|
||||||
? logger.log('ok', `${key} -> is already cloned`)
|
try {
|
||||||
: missingRepos.push(key);
|
const stats = await plugins.smartfs.file(fullPath).stat();
|
||||||
|
if (stats.isDirectory) {
|
||||||
|
logger.log('ok', `${key} -> is already cloned`);
|
||||||
|
} else {
|
||||||
|
missingRepos.push(key);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
missingRepos.push(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('info', `found ${missingRepos.length} missing repos`);
|
logger.log('info', `found ${missingRepos.length} missing repos`);
|
||||||
@@ -165,7 +190,20 @@ export class Meta {
|
|||||||
await this.syncToRemote();
|
await this.syncToRemote();
|
||||||
|
|
||||||
// go recursive
|
// go recursive
|
||||||
const folders = await plugins.smartfile.fs.listFolders(this.cwd);
|
const listEntries = await plugins.smartfs.directory(this.cwd).list();
|
||||||
|
const folders: string[] = [];
|
||||||
|
for (const entry of listEntries) {
|
||||||
|
try {
|
||||||
|
const stats = await plugins.smartfs
|
||||||
|
.file(plugins.path.join(this.cwd, entry.path))
|
||||||
|
.stat();
|
||||||
|
if (stats.isDirectory) {
|
||||||
|
folders.push(entry.name);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip entries that can't be accessed
|
||||||
|
}
|
||||||
|
}
|
||||||
const childMetaRepositories: string[] = [];
|
const childMetaRepositories: string[] = [];
|
||||||
for (const folder of folders) {
|
for (const folder of folders) {
|
||||||
logger.log('info', folder);
|
logger.log('info', folder);
|
||||||
@@ -180,27 +218,31 @@ export class Meta {
|
|||||||
*/
|
*/
|
||||||
public async initProject() {
|
public async initProject() {
|
||||||
await this.syncToRemote(true);
|
await this.syncToRemote(true);
|
||||||
const fileExists = await plugins.smartfile.fs.fileExists(
|
const fileExists = await plugins.smartfs
|
||||||
this.filePaths.metaJson,
|
.file(this.filePaths.metaJson)
|
||||||
);
|
.exists();
|
||||||
if (!fileExists) {
|
if (!fileExists) {
|
||||||
await plugins.smartfile.memory.toFs(
|
await plugins.smartfs
|
||||||
JSON.stringify({
|
.file(this.filePaths.metaJson)
|
||||||
projects: {},
|
.encoding('utf8')
|
||||||
}),
|
.write(
|
||||||
this.filePaths.metaJson,
|
JSON.stringify({
|
||||||
);
|
projects: {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
logger.log(
|
logger.log(
|
||||||
`success`,
|
`success`,
|
||||||
`created a new .meta.json in directory ${this.cwd}`,
|
`created a new .meta.json in directory ${this.cwd}`,
|
||||||
);
|
);
|
||||||
await plugins.smartfile.memory.toFs(
|
await plugins.smartfs
|
||||||
JSON.stringify({
|
.file(this.filePaths.packageJson)
|
||||||
name: this.dirName,
|
.encoding('utf8')
|
||||||
version: '1.0.0',
|
.write(
|
||||||
}),
|
JSON.stringify({
|
||||||
this.filePaths.packageJson,
|
name: this.dirName,
|
||||||
);
|
version: '1.0.0',
|
||||||
|
}),
|
||||||
|
);
|
||||||
logger.log(
|
logger.log(
|
||||||
`success`,
|
`success`,
|
||||||
`created a new package.json in directory ${this.cwd}`,
|
`created a new package.json in directory ${this.cwd}`,
|
||||||
@@ -264,9 +306,10 @@ export class Meta {
|
|||||||
await this.writeToDisk();
|
await this.writeToDisk();
|
||||||
|
|
||||||
logger.log('info', 'removing directory from cwd');
|
logger.log('info', 'removing directory from cwd');
|
||||||
await plugins.smartfile.fs.remove(
|
await plugins.smartfs
|
||||||
plugins.path.join(paths.cwd, projectNameArg),
|
.directory(plugins.path.join(paths.cwd, projectNameArg))
|
||||||
);
|
.recursive()
|
||||||
|
.delete();
|
||||||
await this.updateLocalRepos();
|
await this.updateLocalRepos();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
190
ts/mod_services/classes.globalregistry.ts
Normal file
190
ts/mod_services/classes.globalregistry.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { DockerContainer } from './classes.dockercontainer.js';
|
||||||
|
import { logger } from '../gitzone.logging.js';
|
||||||
|
|
||||||
|
export interface IRegisteredProject {
|
||||||
|
projectPath: string;
|
||||||
|
projectName: string;
|
||||||
|
containers: {
|
||||||
|
mongo?: string;
|
||||||
|
minio?: string;
|
||||||
|
elasticsearch?: string;
|
||||||
|
};
|
||||||
|
ports: {
|
||||||
|
mongo?: number;
|
||||||
|
s3?: number;
|
||||||
|
s3Console?: number;
|
||||||
|
elasticsearch?: number;
|
||||||
|
};
|
||||||
|
enabledServices: string[];
|
||||||
|
lastActive: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGlobalRegistryData {
|
||||||
|
projects: { [projectPath: string]: IRegisteredProject };
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GlobalRegistry {
|
||||||
|
private static instance: GlobalRegistry | null = null;
|
||||||
|
private kvStore: plugins.npmextra.KeyValueStore<IGlobalRegistryData>;
|
||||||
|
private docker: DockerContainer;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.kvStore = new plugins.npmextra.KeyValueStore({
|
||||||
|
typeArg: 'userHomeDir',
|
||||||
|
identityArg: 'gitzone-services',
|
||||||
|
});
|
||||||
|
this.docker = new DockerContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the singleton instance
|
||||||
|
*/
|
||||||
|
public static getInstance(): GlobalRegistry {
|
||||||
|
if (!GlobalRegistry.instance) {
|
||||||
|
GlobalRegistry.instance = new GlobalRegistry();
|
||||||
|
}
|
||||||
|
return GlobalRegistry.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register or update a project in the global registry
|
||||||
|
*/
|
||||||
|
public async registerProject(data: Omit<IRegisteredProject, 'lastActive'>): Promise<void> {
|
||||||
|
const allData = await this.kvStore.readAll();
|
||||||
|
const projects = allData.projects || {};
|
||||||
|
|
||||||
|
projects[data.projectPath] = {
|
||||||
|
...data,
|
||||||
|
lastActive: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.kvStore.writeKey('projects', projects);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a project from the registry
|
||||||
|
*/
|
||||||
|
public async unregisterProject(projectPath: string): Promise<void> {
|
||||||
|
const allData = await this.kvStore.readAll();
|
||||||
|
const projects = allData.projects || {};
|
||||||
|
|
||||||
|
if (projects[projectPath]) {
|
||||||
|
delete projects[projectPath];
|
||||||
|
await this.kvStore.writeKey('projects', projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the lastActive timestamp for a project
|
||||||
|
*/
|
||||||
|
public async touchProject(projectPath: string): Promise<void> {
|
||||||
|
const allData = await this.kvStore.readAll();
|
||||||
|
const projects = allData.projects || {};
|
||||||
|
|
||||||
|
if (projects[projectPath]) {
|
||||||
|
projects[projectPath].lastActive = Date.now();
|
||||||
|
await this.kvStore.writeKey('projects', projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered projects
|
||||||
|
*/
|
||||||
|
public async getAllProjects(): Promise<{ [path: string]: IRegisteredProject }> {
|
||||||
|
const allData = await this.kvStore.readAll();
|
||||||
|
return allData.projects || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a project is registered
|
||||||
|
*/
|
||||||
|
public async isRegistered(projectPath: string): Promise<boolean> {
|
||||||
|
const projects = await this.getAllProjects();
|
||||||
|
return !!projects[projectPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status of all containers across all registered projects
|
||||||
|
*/
|
||||||
|
public async getGlobalStatus(): Promise<
|
||||||
|
Array<{
|
||||||
|
projectPath: string;
|
||||||
|
projectName: string;
|
||||||
|
containers: Array<{ name: string; status: string }>;
|
||||||
|
lastActive: number;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const projects = await this.getAllProjects();
|
||||||
|
const result: Array<{
|
||||||
|
projectPath: string;
|
||||||
|
projectName: string;
|
||||||
|
containers: Array<{ name: string; status: string }>;
|
||||||
|
lastActive: number;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const [path, project] of Object.entries(projects)) {
|
||||||
|
const containerStatuses: Array<{ name: string; status: string }> = [];
|
||||||
|
|
||||||
|
for (const containerName of Object.values(project.containers)) {
|
||||||
|
if (containerName) {
|
||||||
|
const status = await this.docker.getStatus(containerName);
|
||||||
|
containerStatuses.push({ name: containerName, status });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
projectPath: path,
|
||||||
|
projectName: project.projectName,
|
||||||
|
containers: containerStatuses,
|
||||||
|
lastActive: project.lastActive,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop all containers across all registered projects
|
||||||
|
*/
|
||||||
|
public async stopAll(): Promise<{ stopped: string[]; failed: string[] }> {
|
||||||
|
const projects = await this.getAllProjects();
|
||||||
|
const stopped: string[] = [];
|
||||||
|
const failed: string[] = [];
|
||||||
|
|
||||||
|
for (const project of Object.values(projects)) {
|
||||||
|
for (const containerName of Object.values(project.containers)) {
|
||||||
|
if (containerName) {
|
||||||
|
const status = await this.docker.getStatus(containerName);
|
||||||
|
if (status === 'running') {
|
||||||
|
if (await this.docker.stop(containerName)) {
|
||||||
|
stopped.push(containerName);
|
||||||
|
} else {
|
||||||
|
failed.push(containerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { stopped, failed };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove stale registry entries (projects that no longer exist on disk)
|
||||||
|
*/
|
||||||
|
public async cleanup(): Promise<string[]> {
|
||||||
|
const projects = await this.getAllProjects();
|
||||||
|
const removed: string[] = [];
|
||||||
|
|
||||||
|
for (const projectPath of Object.keys(projects)) {
|
||||||
|
const exists = await plugins.smartfs.directory(projectPath).exists();
|
||||||
|
if (!exists) {
|
||||||
|
await this.unregisterProject(projectPath);
|
||||||
|
removed.push(projectPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,11 @@ export interface IServiceConfig {
|
|||||||
S3_BUCKET: string;
|
S3_BUCKET: string;
|
||||||
S3_ENDPOINT: string;
|
S3_ENDPOINT: string;
|
||||||
S3_USESSL: boolean;
|
S3_USESSL: boolean;
|
||||||
|
ELASTICSEARCH_HOST: string;
|
||||||
|
ELASTICSEARCH_PORT: string;
|
||||||
|
ELASTICSEARCH_USER: string;
|
||||||
|
ELASTICSEARCH_PASS: string;
|
||||||
|
ELASTICSEARCH_URL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServiceConfiguration {
|
export class ServiceConfiguration {
|
||||||
@@ -61,10 +66,10 @@ export class ServiceConfiguration {
|
|||||||
* Save the configuration to file
|
* Save the configuration to file
|
||||||
*/
|
*/
|
||||||
public async saveConfig(): Promise<void> {
|
public async saveConfig(): Promise<void> {
|
||||||
await plugins.smartfile.memory.toFs(
|
await plugins.smartfs
|
||||||
JSON.stringify(this.config, null, 2),
|
.file(this.configPath)
|
||||||
this.configPath
|
.encoding('utf8')
|
||||||
);
|
.write(JSON.stringify(this.config, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,21 +77,24 @@ export class ServiceConfiguration {
|
|||||||
*/
|
*/
|
||||||
private async ensureNogitDirectory(): Promise<void> {
|
private async ensureNogitDirectory(): Promise<void> {
|
||||||
const nogitPath = plugins.path.join(process.cwd(), '.nogit');
|
const nogitPath = plugins.path.join(process.cwd(), '.nogit');
|
||||||
await plugins.smartfile.fs.ensureDir(nogitPath);
|
await plugins.smartfs.directory(nogitPath).recursive().create();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if configuration file exists
|
* Check if configuration file exists
|
||||||
*/
|
*/
|
||||||
private async configExists(): Promise<boolean> {
|
private async configExists(): Promise<boolean> {
|
||||||
return plugins.smartfile.fs.fileExists(this.configPath);
|
return plugins.smartfs.file(this.configPath).exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load configuration from file
|
* Load configuration from file
|
||||||
*/
|
*/
|
||||||
private async loadConfig(): Promise<void> {
|
private async loadConfig(): Promise<void> {
|
||||||
const configContent = plugins.smartfile.fs.toStringSync(this.configPath);
|
const configContent = (await plugins.smartfs
|
||||||
|
.file(this.configPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
this.config = JSON.parse(configContent);
|
this.config = JSON.parse(configContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +102,7 @@ export class ServiceConfiguration {
|
|||||||
* Create default configuration
|
* Create default configuration
|
||||||
*/
|
*/
|
||||||
private async createDefaultConfig(): Promise<void> {
|
private async createDefaultConfig(): Promise<void> {
|
||||||
const projectName = helpers.getProjectName();
|
const projectName = await helpers.getProjectName();
|
||||||
const mongoPort = await helpers.getRandomAvailablePort();
|
const mongoPort = await helpers.getRandomAvailablePort();
|
||||||
const s3Port = await helpers.getRandomAvailablePort();
|
const s3Port = await helpers.getRandomAvailablePort();
|
||||||
let s3ConsolePort = s3Port + 1;
|
let s3ConsolePort = s3Port + 1;
|
||||||
@@ -111,6 +119,10 @@ export class ServiceConfiguration {
|
|||||||
const mongoPortStr = mongoPort.toString();
|
const mongoPortStr = mongoPort.toString();
|
||||||
const s3Host = 'localhost';
|
const s3Host = 'localhost';
|
||||||
const s3PortStr = s3Port.toString();
|
const s3PortStr = s3Port.toString();
|
||||||
|
const esHost = 'localhost';
|
||||||
|
const esPort = '9200';
|
||||||
|
const esUser = 'elastic';
|
||||||
|
const esPass = 'elastic';
|
||||||
|
|
||||||
this.config = {
|
this.config = {
|
||||||
PROJECT_NAME: projectName,
|
PROJECT_NAME: projectName,
|
||||||
@@ -127,7 +139,12 @@ export class ServiceConfiguration {
|
|||||||
S3_SECRETKEY: 'defaultpass',
|
S3_SECRETKEY: 'defaultpass',
|
||||||
S3_BUCKET: `${projectName}-documents`,
|
S3_BUCKET: `${projectName}-documents`,
|
||||||
S3_ENDPOINT: s3Host,
|
S3_ENDPOINT: s3Host,
|
||||||
S3_USESSL: false
|
S3_USESSL: false,
|
||||||
|
ELASTICSEARCH_HOST: esHost,
|
||||||
|
ELASTICSEARCH_PORT: esPort,
|
||||||
|
ELASTICSEARCH_USER: esUser,
|
||||||
|
ELASTICSEARCH_PASS: esPass,
|
||||||
|
ELASTICSEARCH_URL: `http://${esUser}:${esPass}@${esHost}:${esPort}`
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.saveConfig();
|
await this.saveConfig();
|
||||||
@@ -136,13 +153,14 @@ export class ServiceConfiguration {
|
|||||||
logger.log('info', `📍 MongoDB port: ${mongoPort}`);
|
logger.log('info', `📍 MongoDB port: ${mongoPort}`);
|
||||||
logger.log('info', `📍 S3 API port: ${s3Port}`);
|
logger.log('info', `📍 S3 API port: ${s3Port}`);
|
||||||
logger.log('info', `📍 S3 Console port: ${s3ConsolePort}`);
|
logger.log('info', `📍 S3 Console port: ${s3ConsolePort}`);
|
||||||
|
logger.log('info', `📍 Elasticsearch port: ${esPort}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update missing fields in existing configuration
|
* Update missing fields in existing configuration
|
||||||
*/
|
*/
|
||||||
private async updateMissingFields(): Promise<void> {
|
private async updateMissingFields(): Promise<void> {
|
||||||
const projectName = helpers.getProjectName();
|
const projectName = await helpers.getProjectName();
|
||||||
let updated = false;
|
let updated = false;
|
||||||
const fieldsAdded: string[] = [];
|
const fieldsAdded: string[] = [];
|
||||||
|
|
||||||
@@ -250,6 +268,38 @@ export class ServiceConfiguration {
|
|||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.config.ELASTICSEARCH_HOST) {
|
||||||
|
this.config.ELASTICSEARCH_HOST = 'localhost';
|
||||||
|
fieldsAdded.push('ELASTICSEARCH_HOST');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.ELASTICSEARCH_PORT) {
|
||||||
|
this.config.ELASTICSEARCH_PORT = '9200';
|
||||||
|
fieldsAdded.push('ELASTICSEARCH_PORT');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.ELASTICSEARCH_USER) {
|
||||||
|
this.config.ELASTICSEARCH_USER = 'elastic';
|
||||||
|
fieldsAdded.push('ELASTICSEARCH_USER');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.ELASTICSEARCH_PASS) {
|
||||||
|
this.config.ELASTICSEARCH_PASS = 'elastic';
|
||||||
|
fieldsAdded.push('ELASTICSEARCH_PASS');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always update ELASTICSEARCH_URL based on current settings
|
||||||
|
const oldEsUrl = this.config.ELASTICSEARCH_URL;
|
||||||
|
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
|
||||||
|
if (oldEsUrl !== this.config.ELASTICSEARCH_URL) {
|
||||||
|
fieldsAdded.push('ELASTICSEARCH_URL');
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
await this.saveConfig();
|
await this.saveConfig();
|
||||||
logger.log('ok', `✅ Added missing fields: ${fieldsAdded.join(', ')}`);
|
logger.log('ok', `✅ Added missing fields: ${fieldsAdded.join(', ')}`);
|
||||||
@@ -272,7 +322,8 @@ export class ServiceConfiguration {
|
|||||||
public getContainerNames() {
|
public getContainerNames() {
|
||||||
return {
|
return {
|
||||||
mongo: `${this.config.PROJECT_NAME}-mongodb`,
|
mongo: `${this.config.PROJECT_NAME}-mongodb`,
|
||||||
minio: `${this.config.PROJECT_NAME}-minio`
|
minio: `${this.config.PROJECT_NAME}-minio`,
|
||||||
|
elasticsearch: `${this.config.PROJECT_NAME}-elasticsearch`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +333,8 @@ export class ServiceConfiguration {
|
|||||||
public getDataDirectories() {
|
public getDataDirectories() {
|
||||||
return {
|
return {
|
||||||
mongo: plugins.path.join(process.cwd(), '.nogit', 'mongodata'),
|
mongo: plugins.path.join(process.cwd(), '.nogit', 'mongodata'),
|
||||||
minio: plugins.path.join(process.cwd(), '.nogit', 'miniodata')
|
minio: plugins.path.join(process.cwd(), '.nogit', 'miniodata'),
|
||||||
|
elasticsearch: plugins.path.join(process.cwd(), '.nogit', 'esdata')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,10 +383,25 @@ export class ServiceConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check Elasticsearch container
|
||||||
|
const esStatus = await this.docker.getStatus(containers.elasticsearch);
|
||||||
|
if (esStatus !== 'not_exists') {
|
||||||
|
const portMappings = await this.docker.getPortMappings(containers.elasticsearch);
|
||||||
|
if (portMappings && portMappings['9200']) {
|
||||||
|
const dockerPort = portMappings['9200'];
|
||||||
|
if (this.config.ELASTICSEARCH_PORT !== dockerPort) {
|
||||||
|
logger.log('note', `📍 Syncing Elasticsearch port from Docker: ${dockerPort}`);
|
||||||
|
this.config.ELASTICSEARCH_PORT = dockerPort;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
// Update derived fields
|
// Update derived fields
|
||||||
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
|
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
|
||||||
this.config.S3_ENDPOINT = this.config.S3_HOST;
|
this.config.S3_ENDPOINT = this.config.S3_HOST;
|
||||||
|
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
|
||||||
|
|
||||||
await this.saveConfig();
|
await this.saveConfig();
|
||||||
logger.log('ok', '✅ Configuration synced with Docker containers');
|
logger.log('ok', '✅ Configuration synced with Docker containers');
|
||||||
@@ -351,6 +418,7 @@ export class ServiceConfiguration {
|
|||||||
// Check if containers exist - if they do, ports are fine
|
// Check if containers exist - if they do, ports are fine
|
||||||
const mongoExists = await this.docker.exists(containers.mongo);
|
const mongoExists = await this.docker.exists(containers.mongo);
|
||||||
const minioExists = await this.docker.exists(containers.minio);
|
const minioExists = await this.docker.exists(containers.minio);
|
||||||
|
const esExists = await this.docker.exists(containers.elasticsearch);
|
||||||
|
|
||||||
// Only check port availability if containers don't exist
|
// Only check port availability if containers don't exist
|
||||||
if (!mongoExists) {
|
if (!mongoExists) {
|
||||||
@@ -388,10 +456,22 @@ export class ServiceConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!esExists) {
|
||||||
|
const esPort = parseInt(this.config.ELASTICSEARCH_PORT);
|
||||||
|
if (!(await helpers.isPortAvailable(esPort))) {
|
||||||
|
logger.log('note', `⚠️ Elasticsearch port ${esPort} is in use, finding new port...`);
|
||||||
|
const newPort = await helpers.getRandomAvailablePort();
|
||||||
|
this.config.ELASTICSEARCH_PORT = newPort.toString();
|
||||||
|
logger.log('ok', `✅ New Elasticsearch port: ${newPort}`);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
// Update derived fields
|
// Update derived fields
|
||||||
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
|
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
|
||||||
this.config.S3_ENDPOINT = this.config.S3_HOST;
|
this.config.S3_ENDPOINT = this.config.S3_HOST;
|
||||||
|
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
|
||||||
|
|
||||||
await this.saveConfig();
|
await this.saveConfig();
|
||||||
}
|
}
|
||||||
@@ -414,13 +494,18 @@ export class ServiceConfiguration {
|
|||||||
s3ConsolePort++;
|
s3ConsolePort++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Elasticsearch uses standard port 9200
|
||||||
|
const esPort = '9200';
|
||||||
|
|
||||||
this.config.MONGODB_PORT = mongoPort.toString();
|
this.config.MONGODB_PORT = mongoPort.toString();
|
||||||
this.config.S3_PORT = s3Port.toString();
|
this.config.S3_PORT = s3Port.toString();
|
||||||
this.config.S3_CONSOLE_PORT = s3ConsolePort.toString();
|
this.config.S3_CONSOLE_PORT = s3ConsolePort.toString();
|
||||||
|
this.config.ELASTICSEARCH_PORT = esPort;
|
||||||
|
|
||||||
// Update derived fields
|
// Update derived fields
|
||||||
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
|
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
|
||||||
this.config.S3_ENDPOINT = this.config.S3_HOST;
|
this.config.S3_ENDPOINT = this.config.S3_HOST;
|
||||||
|
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
|
||||||
|
|
||||||
await this.saveConfig();
|
await this.saveConfig();
|
||||||
|
|
||||||
@@ -428,5 +513,6 @@ export class ServiceConfiguration {
|
|||||||
logger.log('info', ` 📍 MongoDB: ${mongoPort}`);
|
logger.log('info', ` 📍 MongoDB: ${mongoPort}`);
|
||||||
logger.log('info', ` 📍 S3 API: ${s3Port}`);
|
logger.log('info', ` 📍 S3 API: ${s3Port}`);
|
||||||
logger.log('info', ` 📍 S3 Console: ${s3ConsolePort}`);
|
logger.log('info', ` 📍 S3 Console: ${s3ConsolePort}`);
|
||||||
|
logger.log('info', ` 📍 Elasticsearch: ${esPort}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,15 +2,19 @@ import * as plugins from './mod.plugins.js';
|
|||||||
import * as helpers from './helpers.js';
|
import * as helpers from './helpers.js';
|
||||||
import { ServiceConfiguration } from './classes.serviceconfiguration.js';
|
import { ServiceConfiguration } from './classes.serviceconfiguration.js';
|
||||||
import { DockerContainer } from './classes.dockercontainer.js';
|
import { DockerContainer } from './classes.dockercontainer.js';
|
||||||
|
import { GlobalRegistry } from './classes.globalregistry.js';
|
||||||
import { logger } from '../gitzone.logging.js';
|
import { logger } from '../gitzone.logging.js';
|
||||||
|
|
||||||
export class ServiceManager {
|
export class ServiceManager {
|
||||||
private config: ServiceConfiguration;
|
private config: ServiceConfiguration;
|
||||||
private docker: DockerContainer;
|
private docker: DockerContainer;
|
||||||
|
private enabledServices: string[] | null = null;
|
||||||
|
private globalRegistry: GlobalRegistry;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.config = new ServiceConfiguration();
|
this.config = new ServiceConfiguration();
|
||||||
this.docker = new DockerContainer();
|
this.docker = new DockerContainer();
|
||||||
|
this.globalRegistry = GlobalRegistry.getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,10 +31,157 @@ export class ServiceManager {
|
|||||||
await this.config.loadOrCreate();
|
await this.config.loadOrCreate();
|
||||||
logger.log('info', `📋 Project: ${this.config.getConfig().PROJECT_NAME}`);
|
logger.log('info', `📋 Project: ${this.config.getConfig().PROJECT_NAME}`);
|
||||||
|
|
||||||
|
// Load service selection from npmextra.json
|
||||||
|
await this.loadServiceConfiguration();
|
||||||
|
|
||||||
// Validate and update ports if needed
|
// Validate and update ports if needed
|
||||||
await this.config.validateAndUpdatePorts();
|
await this.config.validateAndUpdatePorts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load service configuration from npmextra.json
|
||||||
|
*/
|
||||||
|
private async loadServiceConfiguration(): Promise<void> {
|
||||||
|
const npmextraConfig = new plugins.npmextra.Npmextra(process.cwd());
|
||||||
|
const gitzoneConfig = npmextraConfig.dataFor<any>('@git.zone/cli', {});
|
||||||
|
|
||||||
|
// Check if services array exists
|
||||||
|
if (!gitzoneConfig.services || !Array.isArray(gitzoneConfig.services) || gitzoneConfig.services.length === 0) {
|
||||||
|
// Prompt user to select services
|
||||||
|
const smartinteract = new plugins.smartinteract.SmartInteract();
|
||||||
|
const response = await smartinteract.askQuestion({
|
||||||
|
name: 'services',
|
||||||
|
type: 'checkbox',
|
||||||
|
message: 'Which services do you want to enable for this project?',
|
||||||
|
choices: [
|
||||||
|
{ name: 'MongoDB', value: 'mongodb' },
|
||||||
|
{ name: 'MinIO (S3)', value: 'minio' },
|
||||||
|
{ name: 'Elasticsearch', value: 'elasticsearch' }
|
||||||
|
],
|
||||||
|
default: ['mongodb', 'minio', 'elasticsearch']
|
||||||
|
});
|
||||||
|
|
||||||
|
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
|
||||||
|
|
||||||
|
// Save to npmextra.json
|
||||||
|
await this.saveServiceConfiguration(this.enabledServices);
|
||||||
|
} else {
|
||||||
|
this.enabledServices = gitzoneConfig.services;
|
||||||
|
logger.log('info', `🔧 Enabled services: ${this.enabledServices.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save service configuration to npmextra.json
|
||||||
|
*/
|
||||||
|
private async saveServiceConfiguration(services: string[]): Promise<void> {
|
||||||
|
const npmextraPath = plugins.path.join(process.cwd(), 'npmextra.json');
|
||||||
|
let npmextraData: 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update @git.zone/cli.services
|
||||||
|
if (!npmextraData['@git.zone/cli']) {
|
||||||
|
npmextraData['@git.zone/cli'] = {};
|
||||||
|
}
|
||||||
|
npmextraData['@git.zone/cli'].services = services;
|
||||||
|
|
||||||
|
// Write back to npmextra.json
|
||||||
|
await plugins.smartfs
|
||||||
|
.file(npmextraPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.write(JSON.stringify(npmextraData, null, 2));
|
||||||
|
|
||||||
|
logger.log('ok', `✅ Saved service configuration to npmextra.json`);
|
||||||
|
logger.log('info', `🔧 Enabled services: ${services.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a service is enabled
|
||||||
|
*/
|
||||||
|
private isServiceEnabled(service: string): boolean {
|
||||||
|
if (!this.enabledServices) {
|
||||||
|
return true; // If no configuration, enable all
|
||||||
|
}
|
||||||
|
return this.enabledServices.includes(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register this project with the global registry
|
||||||
|
*/
|
||||||
|
private async registerWithGlobalRegistry(): Promise<void> {
|
||||||
|
const config = this.config.getConfig();
|
||||||
|
const containers = this.config.getContainerNames();
|
||||||
|
|
||||||
|
await this.globalRegistry.registerProject({
|
||||||
|
projectPath: process.cwd(),
|
||||||
|
projectName: config.PROJECT_NAME,
|
||||||
|
containers: {
|
||||||
|
mongo: containers.mongo,
|
||||||
|
minio: containers.minio,
|
||||||
|
elasticsearch: containers.elasticsearch,
|
||||||
|
},
|
||||||
|
ports: {
|
||||||
|
mongo: parseInt(config.MONGODB_PORT),
|
||||||
|
s3: parseInt(config.S3_PORT),
|
||||||
|
s3Console: parseInt(config.S3_CONSOLE_PORT),
|
||||||
|
elasticsearch: parseInt(config.ELASTICSEARCH_PORT),
|
||||||
|
},
|
||||||
|
enabledServices: this.enabledServices || ['mongodb', 'minio', 'elasticsearch'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start all enabled services
|
||||||
|
*/
|
||||||
|
public async startAll(): Promise<void> {
|
||||||
|
let first = true;
|
||||||
|
if (this.isServiceEnabled('mongodb')) {
|
||||||
|
if (!first) console.log();
|
||||||
|
await this.startMongoDB();
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
if (this.isServiceEnabled('minio')) {
|
||||||
|
if (!first) console.log();
|
||||||
|
await this.startMinIO();
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
if (this.isServiceEnabled('elasticsearch')) {
|
||||||
|
if (!first) console.log();
|
||||||
|
await this.startElasticsearch();
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register with global registry
|
||||||
|
await this.registerWithGlobalRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop all enabled services
|
||||||
|
*/
|
||||||
|
public async stopAll(): Promise<void> {
|
||||||
|
let first = true;
|
||||||
|
if (this.isServiceEnabled('mongodb')) {
|
||||||
|
if (!first) console.log();
|
||||||
|
await this.stopMongoDB();
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
if (this.isServiceEnabled('minio')) {
|
||||||
|
if (!first) console.log();
|
||||||
|
await this.stopMinIO();
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
if (this.isServiceEnabled('elasticsearch')) {
|
||||||
|
if (!first) console.log();
|
||||||
|
await this.stopElasticsearch();
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start MongoDB service
|
* Start MongoDB service
|
||||||
*/
|
*/
|
||||||
@@ -42,7 +193,7 @@ export class ServiceManager {
|
|||||||
const directories = this.config.getDataDirectories();
|
const directories = this.config.getDataDirectories();
|
||||||
|
|
||||||
// Ensure data directory exists
|
// Ensure data directory exists
|
||||||
await plugins.smartfile.fs.ensureDir(directories.mongo);
|
await plugins.smartfs.directory(directories.mongo).recursive().create();
|
||||||
|
|
||||||
const status = await this.docker.getStatus(containers.mongo);
|
const status = await this.docker.getStatus(containers.mongo);
|
||||||
|
|
||||||
@@ -141,7 +292,7 @@ export class ServiceManager {
|
|||||||
const directories = this.config.getDataDirectories();
|
const directories = this.config.getDataDirectories();
|
||||||
|
|
||||||
// Ensure data directory exists
|
// Ensure data directory exists
|
||||||
await plugins.smartfile.fs.ensureDir(directories.minio);
|
await plugins.smartfs.directory(directories.minio).recursive().create();
|
||||||
|
|
||||||
const status = await this.docker.getStatus(containers.minio);
|
const status = await this.docker.getStatus(containers.minio);
|
||||||
|
|
||||||
@@ -260,6 +411,102 @@ export class ServiceManager {
|
|||||||
logger.log('info', ` Console: http://${config.S3_HOST}:${config.S3_CONSOLE_PORT} (login: ${config.S3_ACCESSKEY}/***)`);
|
logger.log('info', ` Console: http://${config.S3_HOST}:${config.S3_CONSOLE_PORT} (login: ${config.S3_ACCESSKEY}/***)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start Elasticsearch service
|
||||||
|
*/
|
||||||
|
public async startElasticsearch(): Promise<void> {
|
||||||
|
logger.log('note', '📦 Elasticsearch:');
|
||||||
|
|
||||||
|
const config = this.config.getConfig();
|
||||||
|
const containers = this.config.getContainerNames();
|
||||||
|
const directories = this.config.getDataDirectories();
|
||||||
|
|
||||||
|
// Ensure data directory exists
|
||||||
|
await plugins.smartfs.directory(directories.elasticsearch).recursive().create();
|
||||||
|
|
||||||
|
const status = await this.docker.getStatus(containers.elasticsearch);
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'running':
|
||||||
|
logger.log('ok', ' Already running ✓');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'stopped':
|
||||||
|
// Check if port mapping matches config
|
||||||
|
const esPortMappings = await this.docker.getPortMappings(containers.elasticsearch);
|
||||||
|
if (esPortMappings && esPortMappings['9200'] !== config.ELASTICSEARCH_PORT) {
|
||||||
|
logger.log('note', ' Port configuration changed, recreating container...');
|
||||||
|
await this.docker.remove(containers.elasticsearch, true);
|
||||||
|
// Fall through to create new container
|
||||||
|
const success = await this.docker.run({
|
||||||
|
name: containers.elasticsearch,
|
||||||
|
image: 'elasticsearch:8.11.0',
|
||||||
|
ports: {
|
||||||
|
[`0.0.0.0:${config.ELASTICSEARCH_PORT}`]: '9200'
|
||||||
|
},
|
||||||
|
volumes: {
|
||||||
|
[directories.elasticsearch]: '/usr/share/elasticsearch/data'
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
'discovery.type': 'single-node',
|
||||||
|
'xpack.security.enabled': 'true',
|
||||||
|
'ELASTIC_PASSWORD': config.ELASTICSEARCH_PASS,
|
||||||
|
'ES_JAVA_OPTS': '-Xms512m -Xmx512m'
|
||||||
|
},
|
||||||
|
restart: 'unless-stopped'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logger.log('ok', ' Recreated with new port ✓');
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to recreate container');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ports match, just start the container
|
||||||
|
if (await this.docker.start(containers.elasticsearch)) {
|
||||||
|
logger.log('ok', ' Started ✓');
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to start');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'not_exists':
|
||||||
|
logger.log('note', ' Creating container...');
|
||||||
|
|
||||||
|
const success = await this.docker.run({
|
||||||
|
name: containers.elasticsearch,
|
||||||
|
image: 'elasticsearch:8.11.0',
|
||||||
|
ports: {
|
||||||
|
[`0.0.0.0:${config.ELASTICSEARCH_PORT}`]: '9200'
|
||||||
|
},
|
||||||
|
volumes: {
|
||||||
|
[directories.elasticsearch]: '/usr/share/elasticsearch/data'
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
'discovery.type': 'single-node',
|
||||||
|
'xpack.security.enabled': 'true',
|
||||||
|
'ELASTIC_PASSWORD': config.ELASTICSEARCH_PASS,
|
||||||
|
'ES_JAVA_OPTS': '-Xms512m -Xmx512m'
|
||||||
|
},
|
||||||
|
restart: 'unless-stopped'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logger.log('ok', ' Created and started ✓');
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to create container');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', ` Container: ${containers.elasticsearch}`);
|
||||||
|
logger.log('info', ` Port: ${config.ELASTICSEARCH_PORT}`);
|
||||||
|
logger.log('info', ` Connection: ${config.ELASTICSEARCH_URL}`);
|
||||||
|
logger.log('info', ` Username: ${config.ELASTICSEARCH_USER}`);
|
||||||
|
logger.log('info', ` Password: ${config.ELASTICSEARCH_PASS}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop MongoDB service
|
* Stop MongoDB service
|
||||||
*/
|
*/
|
||||||
@@ -300,6 +547,26 @@ export class ServiceManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop Elasticsearch service
|
||||||
|
*/
|
||||||
|
public async stopElasticsearch(): Promise<void> {
|
||||||
|
logger.log('note', '📦 Elasticsearch:');
|
||||||
|
|
||||||
|
const containers = this.config.getContainerNames();
|
||||||
|
const status = await this.docker.getStatus(containers.elasticsearch);
|
||||||
|
|
||||||
|
if (status === 'running') {
|
||||||
|
if (await this.docker.stop(containers.elasticsearch)) {
|
||||||
|
logger.log('ok', ' Stopped ✓');
|
||||||
|
} else {
|
||||||
|
logger.log('error', ' Failed to stop');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.log('note', ' Not running');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show service status
|
* Show service status
|
||||||
*/
|
*/
|
||||||
@@ -385,6 +652,34 @@ export class ServiceManager {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Elasticsearch status
|
||||||
|
const esStatus = await this.docker.getStatus(containers.elasticsearch);
|
||||||
|
switch (esStatus) {
|
||||||
|
case 'running':
|
||||||
|
logger.log('ok', '📦 Elasticsearch: 🟢 Running');
|
||||||
|
logger.log('info', ` ├─ Container: ${containers.elasticsearch}`);
|
||||||
|
logger.log('info', ` ├─ Port: ${config.ELASTICSEARCH_PORT}`);
|
||||||
|
logger.log('info', ` ├─ Connection: ${config.ELASTICSEARCH_URL}`);
|
||||||
|
logger.log('info', ` └─ Credentials: ${config.ELASTICSEARCH_USER}/${config.ELASTICSEARCH_PASS}`);
|
||||||
|
break;
|
||||||
|
case 'stopped':
|
||||||
|
logger.log('note', '📦 Elasticsearch: 🟡 Stopped');
|
||||||
|
logger.log('info', ` ├─ Container: ${containers.elasticsearch}`);
|
||||||
|
logger.log('info', ` └─ Port: ${config.ELASTICSEARCH_PORT}`);
|
||||||
|
break;
|
||||||
|
case 'not_exists':
|
||||||
|
logger.log('info', '📦 Elasticsearch: ⚪ Not installed');
|
||||||
|
// Check port availability
|
||||||
|
const esPort = parseInt(config.ELASTICSEARCH_PORT);
|
||||||
|
const esAvailable = await helpers.isPortAvailable(esPort);
|
||||||
|
if (!esAvailable) {
|
||||||
|
logger.log('error', ` └─ ⚠️ Port ${esPort} is in use by another process`);
|
||||||
|
} else {
|
||||||
|
logger.log('info', ` └─ Port ${esPort} is available`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -420,6 +715,15 @@ export class ServiceManager {
|
|||||||
logger.log('info', ` Data: ${this.config.getDataDirectories().minio}`);
|
logger.log('info', ` Data: ${this.config.getDataDirectories().minio}`);
|
||||||
logger.log('info', ` Endpoint: ${config.S3_ENDPOINT}`);
|
logger.log('info', ` Endpoint: ${config.S3_ENDPOINT}`);
|
||||||
logger.log('info', ` Console URL: http://${config.S3_HOST}:${config.S3_CONSOLE_PORT}`);
|
logger.log('info', ` Console URL: http://${config.S3_HOST}:${config.S3_CONSOLE_PORT}`);
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
logger.log('note', 'Elasticsearch:');
|
||||||
|
logger.log('info', ` Host: ${config.ELASTICSEARCH_HOST}:${config.ELASTICSEARCH_PORT}`);
|
||||||
|
logger.log('info', ` User: ${config.ELASTICSEARCH_USER}`);
|
||||||
|
logger.log('info', ' Password: ***');
|
||||||
|
logger.log('info', ` Container: ${this.config.getContainerNames().elasticsearch}`);
|
||||||
|
logger.log('info', ` Data: ${this.config.getDataDirectories().elasticsearch}`);
|
||||||
|
logger.log('info', ` Connection: ${config.ELASTICSEARCH_URL}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -478,15 +782,28 @@ export class ServiceManager {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'elasticsearch':
|
||||||
|
case 'es':
|
||||||
|
if (await this.docker.isRunning(containers.elasticsearch)) {
|
||||||
|
helpers.printHeader(`Elasticsearch Logs (last ${lines} lines)`);
|
||||||
|
const logs = await this.docker.logs(containers.elasticsearch, lines);
|
||||||
|
console.log(logs);
|
||||||
|
} else {
|
||||||
|
logger.log('note', 'Elasticsearch container is not running');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'all':
|
case 'all':
|
||||||
case '':
|
case '':
|
||||||
await this.showLogs('mongo', lines);
|
await this.showLogs('mongo', lines);
|
||||||
console.log();
|
console.log();
|
||||||
await this.showLogs('minio', lines);
|
await this.showLogs('minio', lines);
|
||||||
|
console.log();
|
||||||
|
await this.showLogs('elasticsearch', lines);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.log('note', 'Usage: gitzone services logs [mongo|s3|all] [lines]');
|
logger.log('note', 'Usage: gitzone services logs [mongo|s3|elasticsearch|all] [lines]');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -512,9 +829,25 @@ export class ServiceManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (await this.docker.exists(containers.elasticsearch)) {
|
||||||
|
if (await this.docker.remove(containers.elasticsearch, true)) {
|
||||||
|
logger.log('ok', ' Elasticsearch container removed ✓');
|
||||||
|
removed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!removed) {
|
if (!removed) {
|
||||||
logger.log('note', ' No containers to remove');
|
logger.log('note', ' No containers to remove');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if all containers are gone, then unregister from global registry
|
||||||
|
const mongoExists = await this.docker.exists(containers.mongo);
|
||||||
|
const minioExists = await this.docker.exists(containers.minio);
|
||||||
|
const esExists = await this.docker.exists(containers.elasticsearch);
|
||||||
|
|
||||||
|
if (!mongoExists && !minioExists && !esExists) {
|
||||||
|
await this.globalRegistry.unregisterProject(process.cwd());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -524,23 +857,59 @@ export class ServiceManager {
|
|||||||
const directories = this.config.getDataDirectories();
|
const directories = this.config.getDataDirectories();
|
||||||
let cleaned = false;
|
let cleaned = false;
|
||||||
|
|
||||||
if (await plugins.smartfile.fs.fileExists(directories.mongo)) {
|
if (await plugins.smartfs.directory(directories.mongo).exists()) {
|
||||||
await plugins.smartfile.fs.remove(directories.mongo);
|
await plugins.smartfs.directory(directories.mongo).recursive().delete();
|
||||||
logger.log('ok', ' MongoDB data removed ✓');
|
logger.log('ok', ' MongoDB data removed ✓');
|
||||||
cleaned = true;
|
cleaned = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await plugins.smartfile.fs.fileExists(directories.minio)) {
|
if (await plugins.smartfs.directory(directories.minio).exists()) {
|
||||||
await plugins.smartfile.fs.remove(directories.minio);
|
await plugins.smartfs.directory(directories.minio).recursive().delete();
|
||||||
logger.log('ok', ' S3/MinIO data removed ✓');
|
logger.log('ok', ' S3/MinIO data removed ✓');
|
||||||
cleaned = true;
|
cleaned = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (await plugins.smartfs.directory(directories.elasticsearch).exists()) {
|
||||||
|
await plugins.smartfs.directory(directories.elasticsearch).recursive().delete();
|
||||||
|
logger.log('ok', ' Elasticsearch data removed ✓');
|
||||||
|
cleaned = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!cleaned) {
|
if (!cleaned) {
|
||||||
logger.log('note', ' No data to clean');
|
logger.log('note', ' No data to clean');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure which services are enabled
|
||||||
|
*/
|
||||||
|
public async configureServices(): Promise<void> {
|
||||||
|
logger.log('note', 'Select which services to enable for this project:');
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
const currentServices = this.enabledServices || ['mongodb', 'minio', 'elasticsearch'];
|
||||||
|
|
||||||
|
const smartinteract = new plugins.smartinteract.SmartInteract();
|
||||||
|
const response = await smartinteract.askQuestion({
|
||||||
|
name: 'services',
|
||||||
|
type: 'checkbox',
|
||||||
|
message: 'Which services do you want to enable?',
|
||||||
|
choices: [
|
||||||
|
{ name: 'MongoDB', value: 'mongodb' },
|
||||||
|
{ name: 'MinIO (S3)', value: 'minio' },
|
||||||
|
{ name: 'Elasticsearch', value: 'elasticsearch' }
|
||||||
|
],
|
||||||
|
default: currentServices
|
||||||
|
});
|
||||||
|
|
||||||
|
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
|
||||||
|
|
||||||
|
// Save to npmextra.json
|
||||||
|
await this.saveServiceConfiguration(this.enabledServices);
|
||||||
|
|
||||||
|
logger.log('ok', '✅ Service configuration updated');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reconfigure services with new ports
|
* Reconfigure services with new ports
|
||||||
*/
|
*/
|
||||||
@@ -562,6 +931,11 @@ export class ServiceManager {
|
|||||||
logger.log('ok', ' S3/MinIO stopped ✓');
|
logger.log('ok', ' S3/MinIO stopped ✓');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (await this.docker.exists(containers.elasticsearch)) {
|
||||||
|
await this.docker.stop(containers.elasticsearch);
|
||||||
|
logger.log('ok', ' Elasticsearch stopped ✓');
|
||||||
|
}
|
||||||
|
|
||||||
// Reconfigure ports
|
// Reconfigure ports
|
||||||
await this.config.reconfigurePorts();
|
await this.config.reconfigurePorts();
|
||||||
|
|
||||||
@@ -576,8 +950,7 @@ export class ServiceManager {
|
|||||||
|
|
||||||
if (response.value) {
|
if (response.value) {
|
||||||
console.log();
|
console.log();
|
||||||
await this.startMongoDB();
|
await this.startAll();
|
||||||
await this.startMinIO();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,11 +42,15 @@ export const getRandomAvailablePort = async (): Promise<number> => {
|
|||||||
/**
|
/**
|
||||||
* Get the project name from package.json or directory
|
* Get the project name from package.json or directory
|
||||||
*/
|
*/
|
||||||
export const getProjectName = (): string => {
|
export const getProjectName = async (): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const packageJsonPath = plugins.path.join(process.cwd(), 'package.json');
|
const packageJsonPath = plugins.path.join(process.cwd(), 'package.json');
|
||||||
if (plugins.smartfile.fs.fileExistsSync(packageJsonPath)) {
|
if (await plugins.smartfs.file(packageJsonPath).exists()) {
|
||||||
const packageJson = plugins.smartfile.fs.toObjectSync(packageJsonPath);
|
const content = (await plugins.smartfs
|
||||||
|
.file(packageJsonPath)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
const packageJson = JSON.parse(content);
|
||||||
if (packageJson.name) {
|
if (packageJson.name) {
|
||||||
// Sanitize: @fin.cx/skr → fin-cx-skr
|
// Sanitize: @fin.cx/skr → fin-cx-skr
|
||||||
return packageJson.name.replace(/@/g, '').replace(/[\/\.]/g, '-');
|
return packageJson.name.replace(/@/g, '').replace(/[\/\.]/g, '-');
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from './mod.plugins.js';
|
||||||
import * as helpers from './helpers.js';
|
import * as helpers from './helpers.js';
|
||||||
import { ServiceManager } from './classes.servicemanager.js';
|
import { ServiceManager } from './classes.servicemanager.js';
|
||||||
|
import { GlobalRegistry } from './classes.globalregistry.js';
|
||||||
import { logger } from '../gitzone.logging.js';
|
import { logger } from '../gitzone.logging.js';
|
||||||
|
|
||||||
export const run = async (argvArg: any) => {
|
export const run = async (argvArg: any) => {
|
||||||
|
const isGlobal = argvArg.g || argvArg.global;
|
||||||
|
const command = argvArg._[1] || 'help';
|
||||||
|
|
||||||
|
// Handle global commands first
|
||||||
|
if (isGlobal) {
|
||||||
|
await handleGlobalCommand(command);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local project commands
|
||||||
const serviceManager = new ServiceManager();
|
const serviceManager = new ServiceManager();
|
||||||
await serviceManager.init();
|
await serviceManager.init();
|
||||||
|
|
||||||
const command = argvArg._[1] || 'help';
|
|
||||||
const service = argvArg._[2] || 'all';
|
const service = argvArg._[2] || 'all';
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
@@ -28,7 +38,11 @@ export const run = async (argvArg: any) => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'config':
|
case 'config':
|
||||||
await serviceManager.showConfig();
|
if (service === 'services' || argvArg._[2] === 'services') {
|
||||||
|
await handleConfigureServices(serviceManager);
|
||||||
|
} else {
|
||||||
|
await serviceManager.showConfig();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'compass':
|
case 'compass':
|
||||||
@@ -73,16 +87,19 @@ async function handleStart(serviceManager: ServiceManager, service: string) {
|
|||||||
await serviceManager.startMinIO();
|
await serviceManager.startMinIO();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'elasticsearch':
|
||||||
|
case 'es':
|
||||||
|
await serviceManager.startElasticsearch();
|
||||||
|
break;
|
||||||
|
|
||||||
case 'all':
|
case 'all':
|
||||||
case '':
|
case '':
|
||||||
await serviceManager.startMongoDB();
|
await serviceManager.startAll();
|
||||||
console.log();
|
|
||||||
await serviceManager.startMinIO();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.log('error', `Unknown service: ${service}`);
|
logger.log('error', `Unknown service: ${service}`);
|
||||||
logger.log('note', 'Use: mongo, s3, or all');
|
logger.log('note', 'Use: mongo, s3, elasticsearch, or all');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,16 +118,19 @@ async function handleStop(serviceManager: ServiceManager, service: string) {
|
|||||||
await serviceManager.stopMinIO();
|
await serviceManager.stopMinIO();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'elasticsearch':
|
||||||
|
case 'es':
|
||||||
|
await serviceManager.stopElasticsearch();
|
||||||
|
break;
|
||||||
|
|
||||||
case 'all':
|
case 'all':
|
||||||
case '':
|
case '':
|
||||||
await serviceManager.stopMongoDB();
|
await serviceManager.stopAll();
|
||||||
console.log();
|
|
||||||
await serviceManager.stopMinIO();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.log('error', `Unknown service: ${service}`);
|
logger.log('error', `Unknown service: ${service}`);
|
||||||
logger.log('note', 'Use: mongo, s3, or all');
|
logger.log('note', 'Use: mongo, s3, elasticsearch, or all');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,14 +153,18 @@ async function handleRestart(serviceManager: ServiceManager, service: string) {
|
|||||||
await serviceManager.startMinIO();
|
await serviceManager.startMinIO();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'elasticsearch':
|
||||||
|
case 'es':
|
||||||
|
await serviceManager.stopElasticsearch();
|
||||||
|
await plugins.smartdelay.delayFor(2000);
|
||||||
|
await serviceManager.startElasticsearch();
|
||||||
|
break;
|
||||||
|
|
||||||
case 'all':
|
case 'all':
|
||||||
case '':
|
case '':
|
||||||
await serviceManager.stopMongoDB();
|
await serviceManager.stopAll();
|
||||||
await serviceManager.stopMinIO();
|
|
||||||
await plugins.smartdelay.delayFor(2000);
|
await plugins.smartdelay.delayFor(2000);
|
||||||
await serviceManager.startMongoDB();
|
await serviceManager.startAll();
|
||||||
console.log();
|
|
||||||
await serviceManager.startMinIO();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -185,6 +209,11 @@ async function handleClean(serviceManager: ServiceManager) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleConfigureServices(serviceManager: ServiceManager) {
|
||||||
|
helpers.printHeader('Configure Services');
|
||||||
|
await serviceManager.configureServices();
|
||||||
|
}
|
||||||
|
|
||||||
function showHelp() {
|
function showHelp() {
|
||||||
helpers.printHeader('GitZone Services Manager');
|
helpers.printHeader('GitZone Services Manager');
|
||||||
|
|
||||||
@@ -192,33 +221,213 @@ function showHelp() {
|
|||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
logger.log('note', 'Commands:');
|
logger.log('note', 'Commands:');
|
||||||
logger.log('info', ' start [service] Start services (mongo|s3|all)');
|
logger.log('info', ' start [service] Start services (mongo|s3|elasticsearch|all)');
|
||||||
logger.log('info', ' stop [service] Stop services (mongo|s3|all)');
|
logger.log('info', ' stop [service] Stop services (mongo|s3|elasticsearch|all)');
|
||||||
logger.log('info', ' restart [service] Restart services (mongo|s3|all)');
|
logger.log('info', ' restart [service] Restart services (mongo|s3|elasticsearch|all)');
|
||||||
logger.log('info', ' status Show service status');
|
logger.log('info', ' status Show service status');
|
||||||
logger.log('info', ' config Show current configuration');
|
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', ' compass Show MongoDB Compass connection string');
|
||||||
logger.log('info', ' logs [service] Show logs (mongo|s3|all) [lines]');
|
logger.log('info', ' logs [service] Show logs (mongo|s3|elasticsearch|all) [lines]');
|
||||||
logger.log('info', ' reconfigure Reassign ports and restart services');
|
logger.log('info', ' reconfigure Reassign ports and restart services');
|
||||||
logger.log('info', ' remove Remove all containers');
|
logger.log('info', ' remove Remove all containers');
|
||||||
logger.log('info', ' clean Remove all containers and data ⚠️');
|
logger.log('info', ' clean Remove all containers and data ⚠️');
|
||||||
logger.log('info', ' help Show this help message');
|
logger.log('info', ' help Show this help message');
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
|
logger.log('note', 'Available Services:');
|
||||||
|
logger.log('info', ' • MongoDB (mongo) - Document database');
|
||||||
|
logger.log('info', ' • MinIO (s3) - S3-compatible object storage');
|
||||||
|
logger.log('info', ' • Elasticsearch (elasticsearch) - Search and analytics engine');
|
||||||
|
console.log();
|
||||||
|
|
||||||
logger.log('note', 'Features:');
|
logger.log('note', 'Features:');
|
||||||
logger.log('info', ' • Auto-creates .nogit/env.json with smart defaults');
|
logger.log('info', ' • Auto-creates .nogit/env.json with smart defaults');
|
||||||
logger.log('info', ' • Random ports (20000-30000) to avoid conflicts');
|
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', ' • Project-specific containers for multi-project support');
|
||||||
logger.log('info', ' • Preserves custom configuration values');
|
logger.log('info', ' • Preserves custom configuration values');
|
||||||
logger.log('info', ' • MongoDB Compass connection support');
|
logger.log('info', ' • MongoDB Compass connection support');
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
logger.log('note', 'Examples:');
|
logger.log('note', 'Examples:');
|
||||||
logger.log('info', ' gitzone services start # Start all services');
|
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 mongo # Start only MongoDB');
|
||||||
logger.log('info', ' gitzone services stop # Stop all services');
|
logger.log('info', ' gitzone services start elasticsearch # Start only Elasticsearch');
|
||||||
logger.log('info', ' gitzone services status # Check service status');
|
logger.log('info', ' gitzone services stop # Stop all services');
|
||||||
logger.log('info', ' gitzone services config # Show configuration');
|
logger.log('info', ' gitzone services status # Check service status');
|
||||||
logger.log('info', ' gitzone services compass # Get MongoDB Compass connection');
|
logger.log('info', ' gitzone services config # Show configuration');
|
||||||
logger.log('info', ' gitzone services logs mongo 50 # Show last 50 lines of MongoDB logs');
|
logger.log('info', ' gitzone services compass # Get MongoDB Compass connection');
|
||||||
|
logger.log('info', ' gitzone services logs elasticsearch # Show Elasticsearch logs');
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
logger.log('note', 'Global Commands (-g/--global):');
|
||||||
|
logger.log('info', ' list -g List all registered projects');
|
||||||
|
logger.log('info', ' status -g Show status across all projects');
|
||||||
|
logger.log('info', ' stop -g Stop all containers across all projects');
|
||||||
|
logger.log('info', ' cleanup -g Remove stale registry entries');
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
logger.log('note', 'Global Examples:');
|
||||||
|
logger.log('info', ' gitzone services list -g # List all registered projects');
|
||||||
|
logger.log('info', ' gitzone services status -g # Show global container status');
|
||||||
|
logger.log('info', ' gitzone services stop -g # Stop all (prompts for confirmation)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Global Command Handlers ====================
|
||||||
|
|
||||||
|
async function handleGlobalCommand(command: string) {
|
||||||
|
const globalRegistry = GlobalRegistry.getInstance();
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'list':
|
||||||
|
await handleGlobalList(globalRegistry);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'status':
|
||||||
|
await handleGlobalStatus(globalRegistry);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'stop':
|
||||||
|
await handleGlobalStop(globalRegistry);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cleanup':
|
||||||
|
await handleGlobalCleanup(globalRegistry);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'help':
|
||||||
|
default:
|
||||||
|
showHelp();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGlobalList(globalRegistry: GlobalRegistry) {
|
||||||
|
helpers.printHeader('Registered Projects (Global)');
|
||||||
|
|
||||||
|
const projects = await globalRegistry.getAllProjects();
|
||||||
|
const projectPaths = Object.keys(projects);
|
||||||
|
|
||||||
|
if (projectPaths.length === 0) {
|
||||||
|
logger.log('note', 'No projects registered');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const path of projectPaths) {
|
||||||
|
const project = projects[path];
|
||||||
|
const lastActive = new Date(project.lastActive).toLocaleString();
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
logger.log('ok', `📁 ${project.projectName}`);
|
||||||
|
logger.log('info', ` Path: ${project.projectPath}`);
|
||||||
|
logger.log('info', ` Services: ${project.enabledServices.join(', ')}`);
|
||||||
|
logger.log('info', ` Last Active: ${lastActive}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGlobalStatus(globalRegistry: GlobalRegistry) {
|
||||||
|
helpers.printHeader('Global Service Status');
|
||||||
|
|
||||||
|
const statuses = await globalRegistry.getGlobalStatus();
|
||||||
|
|
||||||
|
if (statuses.length === 0) {
|
||||||
|
logger.log('note', 'No projects registered');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let runningCount = 0;
|
||||||
|
let totalContainers = 0;
|
||||||
|
|
||||||
|
for (const project of statuses) {
|
||||||
|
console.log();
|
||||||
|
logger.log('ok', `📁 ${project.projectName}`);
|
||||||
|
logger.log('info', ` Path: ${project.projectPath}`);
|
||||||
|
|
||||||
|
if (project.containers.length === 0) {
|
||||||
|
logger.log('note', ' No containers configured');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const container of project.containers) {
|
||||||
|
totalContainers++;
|
||||||
|
const statusIcon = container.status === 'running' ? '🟢' : container.status === 'exited' ? '🟡' : '⚪';
|
||||||
|
if (container.status === 'running') runningCount++;
|
||||||
|
logger.log('info', ` ${statusIcon} ${container.name}: ${container.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
logger.log('note', `Summary: ${runningCount}/${totalContainers} containers running across ${statuses.length} project(s)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGlobalStop(globalRegistry: GlobalRegistry) {
|
||||||
|
helpers.printHeader('Stop All Containers (Global)');
|
||||||
|
|
||||||
|
const statuses = await globalRegistry.getGlobalStatus();
|
||||||
|
|
||||||
|
// Count running containers
|
||||||
|
let runningCount = 0;
|
||||||
|
for (const project of statuses) {
|
||||||
|
for (const container of project.containers) {
|
||||||
|
if (container.status === 'running') runningCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runningCount === 0) {
|
||||||
|
logger.log('note', 'No running containers found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('note', `Found ${runningCount} running container(s) across ${statuses.length} project(s)`);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// Show what will be stopped
|
||||||
|
for (const project of statuses) {
|
||||||
|
const runningContainers = project.containers.filter(c => c.status === 'running');
|
||||||
|
if (runningContainers.length > 0) {
|
||||||
|
logger.log('info', `${project.projectName}:`);
|
||||||
|
for (const container of runningContainers) {
|
||||||
|
logger.log('info', ` • ${container.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
const shouldContinue = await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||||
|
'Stop all containers?',
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldContinue) {
|
||||||
|
logger.log('note', 'Cancelled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('note', 'Stopping all containers...');
|
||||||
|
const result = await globalRegistry.stopAll();
|
||||||
|
|
||||||
|
if (result.stopped.length > 0) {
|
||||||
|
logger.log('ok', `Stopped: ${result.stopped.join(', ')}`);
|
||||||
|
}
|
||||||
|
if (result.failed.length > 0) {
|
||||||
|
logger.log('error', `Failed to stop: ${result.failed.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGlobalCleanup(globalRegistry: GlobalRegistry) {
|
||||||
|
helpers.printHeader('Cleanup Registry (Global)');
|
||||||
|
|
||||||
|
logger.log('note', 'Checking for stale registry entries...');
|
||||||
|
const removed = await globalRegistry.cleanup();
|
||||||
|
|
||||||
|
if (removed.length === 0) {
|
||||||
|
logger.log('ok', 'No stale entries found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('ok', `Removed ${removed.length} stale entr${removed.length === 1 ? 'y' : 'ies'}:`);
|
||||||
|
for (const path of removed) {
|
||||||
|
logger.log('info', ` • ${path}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,26 +6,86 @@ import * as paths from '../paths.js';
|
|||||||
|
|
||||||
import { logger } from '../gitzone.logging.js';
|
import { logger } from '../gitzone.logging.js';
|
||||||
|
|
||||||
export let run = () => {
|
export let run = async () => {
|
||||||
const done = plugins.smartpromise.defer();
|
console.log('');
|
||||||
logger.log('warn', 'no action specified');
|
console.log('╭─────────────────────────────────────────────────────────────╮');
|
||||||
logger.log(
|
console.log('│ gitzone - Development Workflow CLI │');
|
||||||
'info',
|
console.log('╰─────────────────────────────────────────────────────────────╯');
|
||||||
`
|
console.log('');
|
||||||
You can do one of the following things:
|
|
||||||
* create a new project with 'gitzone template [template]'
|
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||||
the following templates exist: ${(() => {
|
const response = await interactInstance.askQuestion({
|
||||||
let projects = `\n`;
|
type: 'list',
|
||||||
for (const template of plugins.smartfile.fs.listFoldersSync(
|
name: 'action',
|
||||||
paths.templatesDir,
|
message: 'What would you like to do?',
|
||||||
)) {
|
default: 'commit',
|
||||||
projects += ` - ${template}\n`;
|
choices: [
|
||||||
}
|
{ name: 'Commit changes (semantic versioning)', value: 'commit' },
|
||||||
return projects;
|
{ name: 'Format project files', value: 'format' },
|
||||||
})()}
|
{ name: 'Configure release settings', value: 'config' },
|
||||||
* format a project with 'gitzone format'
|
{ name: 'Create from template', value: 'template' },
|
||||||
`,
|
{ name: 'Manage dev services (MongoDB, S3)', value: 'services' },
|
||||||
);
|
{ name: 'Open project assets', value: 'open' },
|
||||||
done.resolve();
|
{ name: 'Show help', value: 'help' },
|
||||||
return done.promise;
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const action = (response as any).value;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
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');
|
||||||
|
await modFormat.run({ interactive: true });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'config': {
|
||||||
|
const modConfig = await import('../mod_config/index.js');
|
||||||
|
await modConfig.run({ _: ['config'] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'template': {
|
||||||
|
const modTemplate = await import('../mod_template/index.js');
|
||||||
|
await modTemplate.run({ _: ['template'] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'services': {
|
||||||
|
const modServices = await import('../mod_services/index.js');
|
||||||
|
await modServices.run({ _: ['services'] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'open': {
|
||||||
|
const modOpen = await import('../mod_open/index.js');
|
||||||
|
await modOpen.run({ _: ['open'] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'help':
|
||||||
|
showHelp();
|
||||||
|
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('');
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const getTemplatePath = (templateNameArg: string) => {
|
|||||||
* receives a template name and returns wether there is a corresponding template
|
* receives a template name and returns wether there is a corresponding template
|
||||||
*/
|
*/
|
||||||
export const isTemplate = async (templateNameArg: string) => {
|
export const isTemplate = async (templateNameArg: string) => {
|
||||||
return plugins.smartfile.fs.isDirectory(getTemplatePath(templateNameArg));
|
return plugins.smartfs.directory(getTemplatePath(templateNameArg)).exists();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTemplate = async (templateNameArg: string) => {
|
export const getTemplate = async (templateNameArg: string) => {
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ import * as smartupdate from '@push.rocks/smartupdate';
|
|||||||
import * as smartshell from '@push.rocks/smartshell';
|
import * as smartshell from '@push.rocks/smartshell';
|
||||||
import * as smartnetwork from '@push.rocks/smartnetwork';
|
import * as smartnetwork from '@push.rocks/smartnetwork';
|
||||||
import * as smartfile from '@push.rocks/smartfile';
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
|
import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
|
||||||
import * as smartinteract from '@push.rocks/smartinteract';
|
import * as smartinteract from '@push.rocks/smartinteract';
|
||||||
import * as smartdelay from '@push.rocks/smartdelay';
|
import * as smartdelay from '@push.rocks/smartdelay';
|
||||||
|
|
||||||
|
// Create smartfs instance for filesystem operations
|
||||||
|
export const smartfs = new SmartFs(new SmartFsProviderNode());
|
||||||
|
|
||||||
export {
|
export {
|
||||||
smartlog,
|
smartlog,
|
||||||
smartlogDestinationLocal,
|
smartlogDestinationLocal,
|
||||||
|
|||||||
Reference in New Issue
Block a user