Compare commits

...

228 Commits

Author SHA1 Message Date
jkunz 358d677e72 v2.16.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-10 11:05:17 +00:00
jkunz f421c5851d feat(cli): add toolchain management command 2026-05-10 11:04:57 +00:00
jkunz a420157287 v2.15.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-10 10:01:18 +00:00
jkunz 0e27d54ad2 feat(cli): split commit and release into target-based workflows 2026-05-10 10:01:09 +00:00
jkunz 738fbaa64f v2.14.3
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-30 13:03:04 +00:00
jkunz fe7a9d93d1 fix(test): move test workspace into .nogit and add bundled fixture project files 2026-04-30 13:03:04 +00:00
jkunz 9a4c8795d4 v2.14.2
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-30 12:59:00 +00:00
jkunz faee6a1698 fix(package): correct package entry point extension and align test scripts with pnpm 2026-04-30 12:59:00 +00:00
jkunz 9a1044783d v2.14.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-16 19:44:17 +00:00
jkunz b16eb75d81 fix(repo): no changes to commit 2026-04-16 19:44:17 +00:00
jkunz 261f7ee6b2 v2.14.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-16 18:54:07 +00:00
jkunz fd7a73398c feat(cli): add machine-readable CLI help, recommendation, and configuration flows 2026-04-16 18:54:07 +00:00
jkunz f43f88a3cb v2.13.16
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-16 13:05:47 +00:00
jkunz 4c86ad62fb fix(mod_format): stop package.json formatter from modifying buildDocs and dependency entries 2026-04-16 13:05:47 +00:00
jkunz 4214a1fdf1 v2.13.15
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 19:59:26 +00:00
jkunz 1c33735799 fix(repo): no changes to commit 2026-03-24 19:59:26 +00:00
jkunz 274405e364 v2.13.14
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 19:59:13 +00:00
jkunz bf858c8650 fix(mod_format): move smartconfig file renaming into the formatter orchestrator 2026-03-24 19:59:13 +00:00
jkunz b257c82bd6 v2.13.13
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 19:42:12 +00:00
jkunz 5a1f6d8c76 fix(vscode-template): update VS Code schema matching to use .smartconfig.json 2026-03-24 19:42:12 +00:00
jkunz d44ad6e4e4 v2.13.12
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 16:56:34 +00:00
jkunz 142adfd396 fix(mod_format): render format templates through smartscaf before comparing generated files 2026-03-24 16:56:34 +00:00
jkunz b55e75d169 2.13.11
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 16:10:56 +00:00
jkunz d0d922e53b update to smartconfig 2026-03-24 16:10:51 +00:00
jkunz eda67395fe v2.13.10
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 15:05:07 +00:00
jkunz 470e87eb79 fix(config): migrate configuration handling from npmextra to smartconfig 2026-03-24 15:05:07 +00:00
jkunz 3358a0eacc v2.13.9
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 10m42s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-03-11 19:10:18 +00:00
jkunz b65fac6257 fix(deps,readme): bump dependencies and update README to prefer pnpm and document semantic commit flags 2026-03-11 19:10:18 +00:00
jkunz 4ab59609e6 v2.13.8
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 12m24s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-03-05 11:30:34 +00:00
jkunz 32f106291f fix(dependencies): move runtime tooling packages from devDependencies to dependencies 2026-03-05 11:30:34 +00:00
jkunz b8aa5d61f6 v2.13.7
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 12m15s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-03-05 10:25:44 +00:00
jkunz 71759c276e fix(deps): bump devDependencies: @git.zone/tsbuild to ^4.1.4 and @push.rocks/smartshell to ^3.3.7 2026-03-05 10:25:44 +00:00
jkunz 7938f12d43 v2.13.6
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 14m42s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-02-01 16:19:37 +00:00
jkunz 3722258d69 fix(templates/npm): use tsbuild tsfolders instead of --web flag in npm template build script 2026-02-01 16:19:37 +00:00
jkunz 68859d0e97 v2.13.5
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 10m44s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-02-01 16:18:37 +00:00
jkunz ecadbc7a86 fix(templates/npm): update npm template: tweak test script, bump devDependencies, add smartpath dependency, and fix ts import path 2026-02-01 16:18:37 +00:00
jkunz 0243bc5ec7 v2.13.4
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 12m1s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-01-12 17:57:00 +00:00
jkunz 92e618104f fix(core): update tsbuild to 4.1.2 with cross-module import path fix 2026-01-12 17:57:00 +00:00
jkunz c089c1f80d v2.13.3
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-18 13:57:13 +00:00
jkunz 10a394c7d8 fix(tsconfig): remove experimentalDecorators and useDefineForClassFields from TypeScript configuration files 2025-12-18 13:57:13 +00:00
jkunz 5980308bb8 v2.13.2
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-16 13:00:30 +00:00
jkunz 398e36bdf7 fix(deps): bump @git.zone/tspublish to ^1.11.0 2025-12-16 13:00:30 +00:00
jkunz 1e78517547 v2.13.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-16 12:58:58 +00:00
jkunz 55700ad87e fix(npmextra): merge old npmextra keys into new keys during migration, preserving existing new values 2025-12-16 12:58:58 +00:00
jkunz 773df5268b v2.13.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-16 10:49:41 +00:00
jkunz b51fa88283 feat(tests): feat(tests): add sandbox test fixture, CI and editor configs; bump deps 2025-12-16 10:49:41 +00:00
jkunz cb9f717d54 v2.12.2
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:46:17 +00:00
jkunz 70be11894c fix(cli): noop: no changes 2025-12-15 17:46:17 +00:00
jkunz 89ab63b153 update 2025-12-15 17:45:28 +00:00
jkunz 44c193d4a8 v2.12.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:44:09 +00:00
jkunz 44d259a0ae fix(cli): No changes detected — no version bump required 2025-12-15 17:44:09 +00:00
jkunz f0adff8784 v2.12.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:34:36 +00:00
jkunz fb453e62c3 feat(ci,test): feat(ci/test): add test scaffold, GitLab CI, update gitea workflows and .gitignore 2025-12-15 17:34:36 +00:00
jkunz 001721a8e9 v2.11.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:30:51 +00:00
jkunz b191464ff9 fix(mod_format/formatters): fix(packagejson.formatter): correctly parse scoped package dependency arguments and default to latest 2025-12-15 17:30:51 +00:00
jkunz 4d7eaa238f v2.11.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:24:17 +00:00
jkunz 601e0d1063 feat(mod_format): feat(mod_format): use unified diff formatter with filenames and context in BaseFormatter.displayDiff 2025-12-15 17:24:17 +00:00
jkunz 4bb1a2f8c7 v2.10.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:07:30 +00:00
jkunz b506bf8785 feat(mod_format): Refactor formatting modules to new BaseFormatter and implement concrete analyze/apply logic 2025-12-15 17:07:30 +00:00
jkunz d5fbeb3fc6 2.9.2
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 15:55:36 +00:00
jkunz 2ecdeff3dc update 2025-12-15 15:55:27 +00:00
jkunz 5a663ae767 2.9.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 15:25:30 +00:00
jkunz 218c84a39b update 2025-12-15 15:25:20 +00:00
jkunz 27d5cdca35 v2.9.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 12:00:10 +00:00
jkunz 3ebf072bfb 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. 2025-12-15 12:00:10 +00:00
jkunz 08f56ae0a4 v2.8.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 06:29:32 +00:00
jkunz b2d2684895 feat(commit): Add commit configuration and automatic pre-commit tests 2025-12-15 06:29:32 +00:00
jkunz 1b328c3045 v2.7.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-14 16:53:18 +00:00
jkunz f444a04876 feat(mod_format): Add check-only formatting with interactive diff preview; make formatting default to dry-run and extend formatting API 2025-12-14 16:53:18 +00:00
jkunz 6bd2d35992 v2.6.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-14 11:52:20 +00:00
jkunz d038b1d681 fix(npmextra): Normalize npmextra.json: move tsdoc legal entry and reposition @git.zone/cli configuration 2025-12-14 11:52:20 +00:00
jkunz d60de5cef7 v2.6.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-14 11:46:42 +00:00
jkunz e1076a60ad feat(mod_commit): Add execution plan output to commit command 2025-12-14 11:46:42 +00:00
jkunz 6deba06443 v2.5.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-14 11:03:00 +00:00
jkunz 00cd9dc8e7 feat(mod_standard): Add interactive main menu and help to standard CLI module; route commands via dynamic imports 2025-12-14 11:03:00 +00:00
jkunz 6b0941eea9 v2.4.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-14 10:51:16 +00:00
jkunz 7348567a62 feat(cli): Add optional build step to release flow and auto-format npmextra config when registries change 2025-12-14 10:51:16 +00:00
jkunz ccdca55c9a v2.3.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-14 10:32:58 +00:00
jkunz 6c62f80c57 feat(config): Add interactive menu and help to config command, handle unknown commands, and bump dependencies 2025-12-14 10:32:58 +00:00
jkunz 7bb2f65669 update 2025-12-14 01:42:59 +00:00
jkunz 48c4b0c9b2 update 2025-12-14 01:31:06 +00:00
jkunz 106b72748c 2.2.2
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-13 23:49:15 +00:00
jkunz 03bf82d8ee fix(deps): Update dependencies 2025-12-13 23:48:57 +00:00
jkunz 6a936603bd fix(deps): Update dependencies 2025-12-13 23:47:27 +00:00
jkunz 2a91662e63 fix(deps): Update dependencies 2025-12-13 23:44:25 +00:00
jkunz ea0c026c7e v2.2.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-04 11:13:31 +00:00
jkunz d508e1d06c fix(commit): Prevent auto-accept for BREAKING CHANGE commits; require manual confirmation and warn when --yes is used 2025-12-04 11:13:31 +00:00
jkunz 27f2d265de v2.2.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-02 12:26:35 +00:00
jkunz af3e15e922 feat(services): Improve services manager and configuration; switch test templates to @git.zone/tstest; bump dev dependencies and update docs 2025-12-02 12:26:35 +00:00
jkunz b44624f2e7 v2.1.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-29 17:56:46 +00:00
jkunz 847e679e92 feat(mod_services): Add global service registry and global commands for managing project containers 2025-11-29 17:56:46 +00:00
jkunz ddf5023ecb v2.0.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-27 21:32:34 +00:00
jkunz e1d28bc10a BREAKING CHANGE(core): Migrate filesystem to smartfs (async) and add Elasticsearch service support; refactor format/commit/meta modules 2025-11-27 21:32:34 +00:00
jkunz 2f3d67f9e3 v1.21.5
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-17 11:52:13 +00:00
jkunz 6304953234 fix(tsconfig): Remove emitDecoratorMetadata from tsconfig template 2025-11-17 11:52:13 +00:00
jkunz 8d84620bc4 v1.21.4
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-17 08:11:07 +00:00
jkunz efd6f04e63 fix(tsconfig template): Remove experimentalDecorators and useDefineForClassFields from tsconfig template 2025-11-17 08:11:07 +00:00
jkunz 97ce9db28e v1.21.3
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-17 02:44:57 +00:00
jkunz 362b4c106e fix(assets/templates/multienv): Remove unused Bun configuration template (assets/templates/multienv/bunfig.toml) 2025-11-17 02:44:57 +00:00
jkunz 3efe385952 v1.21.2
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-17 01:49:35 +00:00
jkunz f6886f172d fix(templates/multienv): Disable useDefineForClassFields in multienv TypeScript configs to ensure decorator compatibility 2025-11-17 01:49:35 +00:00
jkunz 81d6273346 v1.21.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-17 01:30:33 +00:00
jkunz 7e6cf5f046 fix(templates.multienv): Enable checkJs in multienv Deno template to enable JS type checking 2025-11-17 01:30:33 +00:00
jkunz 89cf7dca04 v1.21.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-17 01:19:40 +00:00
jkunz 9639a64437 feat(multienv): Add multi-env templates enabling TypeScript decorators for Bun and Deno; rename npmextra config key to szci 2025-11-17 01:19:40 +00:00
jkunz 48305ebb6a v1.20.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-06 00:06:02 +00:00
jkunz 485c0a3855 feat(commit): Add non-interactive --yes (-y) flag to commit command to auto-accept AI recommendations and optionally push with -p 2025-11-06 00:06:02 +00:00
jkunz adc828d9bb 1.19.9
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-05 22:55:29 +00:00
jkunz fff1d39338 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 2025-11-05 22:55:29 +00:00
jkunz 5afbe6ccbc 1.19.8
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-04 03:44:42 +00:00
jkunz 9de17a428d fix(package.json): Bump @git.zone/tsdoc dependency to ^1.9.2 2025-11-04 03:44:42 +00:00
jkunz c9985102c3 1.19.7
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-04 02:31:22 +00:00
jkunz 73f98c1c3f fix(dependencies): Bump @git.zone/tsdoc to ^1.9.1 2025-11-04 02:31:22 +00:00
jkunz ae93e6f146 1.19.6
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-04 02:22:44 +00:00
jkunz 2abaeee500 fix(cli): Bump @git.zone/tsdoc dependency to ^1.9.0 2025-11-04 02:22:44 +00:00
jkunz 0538ba2586 1.19.5
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-04 01:54:04 +00:00
jkunz a451779724 fix(cli): Bump @git.zone/tsdoc to ^1.8.3 2025-11-04 01:54:04 +00:00
jkunz cd3246d659 1.19.4
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-03 17:54:18 +00:00
jkunz d37ffd7177 fix(tsdoc): update tsdoc 2025-11-03 17:54:07 +00:00
jkunz a69b613087 1.19.3
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-03 13:38:17 +00:00
jkunz 1ea186d233 fix(tsdoc): Bump @git.zone/tsdoc to ^1.8.0 and add .claude local settings 2025-11-03 13:38:17 +00:00
jkunz f5e7d43cf3 1.19.2
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-03 11:55:57 +00:00
jkunz d80faa044a fix(tsdoc): Bump @git.zone/tsdoc to ^1.6.1 2025-11-03 11:55:57 +00:00
jkunz 64062e5c43 1.19.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-02 23:11:02 +00:00
jkunz bd22844280 fix(dependencies): Bump dependencies and add local Claude settings 2025-11-02 23:11:02 +00:00
jkunz 366c4a0bc2 1.19.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-10-23 23:44:38 +00:00
jkunz 0d3b10bd00 feat(mod_commit): Add CLI UI helpers and improve commit workflow with progress, recommendations and summary 2025-10-23 23:44:38 +00:00
jkunz a41e3d5d2c 1.18.9
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-10-23 18:24:13 +00:00
jkunz c45cff89de fix(mod_commit): Stage and commit deno.json when bumping/syncing versions and create/update git tags 2025-10-23 18:24:13 +00:00
jkunz 7bb43ad478 1.18.8
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-10-23 18:18:09 +00:00
jkunz 8dcaf1c631 fix(mod_commit): Improve commit workflow: detect project type and current branch; add robust version bump helpers for npm/deno 2025-10-23 18:18:08 +00:00
jkunz 422761806d 1.18.7
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-07 08:27:52 +00:00
jkunz 31360240a9 fix(claude): Add .claude local settings to whitelist dev tool permissions 2025-09-07 08:27:52 +00:00
jkunz e338ee584f 1.18.6
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-07 07:56:47 +00:00
jkunz 31d2e18830 fix(deps): Bump dependency versions and add local Claude settings 2025-09-07 07:56:46 +00:00
jkunz a162ddabbb 1.18.5
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-17 15:50:58 +00:00
jkunz 5dfa1d72aa fix(dependencies): Bump smartshell and smartscaf versions; add .claude local settings 2025-08-17 15:50:58 +00:00
jkunz 7074a19a7f 1.18.4
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-17 13:31:39 +00:00
jkunz 5774fb4da2 fix(cli): Update dependencies, add local Claude settings, and update gitignore template 2025-08-17 13:31:38 +00:00
jkunz be45ce765d 1.18.3
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-16 13:25:57 +00:00
jkunz 2a250b8823 fix(services): Simplify S3 endpoint handling in ServiceConfiguration to store host only 2025-08-16 13:25:57 +00:00
jkunz 9a436cb4be 1.18.2
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-16 11:51:28 +00:00
jkunz 86782c39dd 1.18.1
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-16 11:38:21 +00:00
jkunz fba3e9d2b0 fix(services): Improve services and commit flow: stop AiDoc, use silent docker inspect, sync ports with logging, fix config loading, and bump deps 2025-08-16 11:38:21 +00:00
jkunz cc37f70185 1.18.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-16 08:47:39 +00:00
jkunz dbc1a1ba18 feat(services): Add Docker port mapping sync and reconfigure workflow for local services 2025-08-16 08:47:39 +00:00
jkunz ff57f8a322 1.17.5
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-15 12:17:04 +00:00
jkunz 968e67330d fix(services): Update S3 credentials naming and add S3_ENDPOINT/S3_USESSL support for improved MinIO integration 2025-08-15 12:17:04 +00:00
jkunz 935ee20e83 1.17.4
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-15 11:05:50 +00:00
jkunz c205180991 fix(services): Update S3 credentials naming and add S3_ENDPOINT support for improved MinIO integration 2025-08-15 11:05:50 +00:00
jkunz 4a53bc4abc 1.17.3
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-15 10:12:24 +00:00
jkunz a86fb3bb8e fix(serviceconfig): Update service configuration to include dynamic MongoDB connection string and add local permissions settings 2025-08-15 10:12:24 +00:00
jkunz b187000ae4 1.17.2
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-15 09:48:00 +00:00
jkunz c715adfd6c fix(ci-test-services): Update CI/CD configurations, test settings, and Docker service for MongoDB. 2025-08-15 09:48:00 +00:00
jkunz 7b9ebfdacb fix(services): Improve logging and enhance MongoDB Compass integration
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-15 09:37:54 +00:00
jkunz 05b170cbac feat(services): Add comprehensive development services management (v1.17.0)
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
- Implemented gitzone services command for managing MongoDB and MinIO containers
- Added smart port assignment (20000-30000 range) to avoid conflicts
- Project-specific container names for complete isolation
- Data persistence in .nogit/ directories
- MongoDB Compass connection string generation with network IP detection
- Auto-configuration via .nogit/env.json with secure defaults
- Commands: start, stop, restart, status, config, compass, logs, remove, clean
- Interactive confirmations for destructive operations
- Comprehensive documentation and Task Venture Capital GmbH legal update
2025-08-14 14:38:27 +00:00
jkunz b320af0b61 1.16.10
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-08 09:46:34 +00:00
jkunz 49e1ee1f39 fix(format): Improve concurrency control in caching and rollback modules, refine gitignore custom section handling, and enhance Prettier file processing. 2025-08-08 09:46:34 +00:00
jkunz cef31cf1ff 1.16.9
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-08 06:50:58 +00:00
jkunz 74ecdde1ac fix(format): Improve concurrency control in cache and rollback modules, refine gitignore custom section handling, and enhance Prettier file processing 2025-08-08 06:50:58 +00:00
jkunz 74a8229e43 1.16.8
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-08 06:25:40 +00:00
jkunz 859cbc733d fix(format): Improve concurrency control in cache and rollback management with mutex locking and refine formatting details 2025-08-08 06:25:40 +00:00
jkunz d32d47b706 1.16.7
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-08 05:48:41 +00:00
jkunz fd90cfe895 fix(core): Improve formatting, logging, and rollback integrity in core modules 2025-08-08 05:48:41 +00:00
jkunz c48f48fc8b 1.16.6
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-08 05:43:34 +00:00
jkunz e21e7f0850 fix(changecache): Improve cache manifest validation and atomic file writes; add local settings and overrides 2025-08-08 05:43:34 +00:00
jkunz 5f561527f9 1.16.5
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-08 05:34:54 +00:00
jkunz 9f5f568c3f fix(prettier): Improve file selection in Prettier formatter, remove legacy package overrides, and update CI template indentation 2025-08-08 05:34:54 +00:00
jkunz 39a31a4304 1.16.4
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-08 05:28:02 +00:00
jkunz b629a7d70b fix(prettier): Improve file exclusion in the Prettier formatter to skip unnecessary files and directories. 2025-08-08 05:28:01 +00:00
jkunz 4003944139 1.16.3
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-08 05:18:19 +00:00
jkunz 83d374dffd fix(changecache/prettier): Skip directories during file processing to prevent errors in changecache and prettier formatting 2025-08-08 05:18:19 +00:00
jkunz 49a5a66440 fix(format): Fix format command confirmation prompt to correctly check user response
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-07 21:13:12 +00:00
jkunz 20a53d4d92 1.16.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-04 13:16:56 +00:00
jkunz fe02b990b3 fix(package/config): Move smartdiff dependency to runtime and add local bash permissions settings 2025-08-04 13:16:56 +00:00
philkunz c013fbf42e 1.16.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-19 13:34:23 +00:00
philkunz 949f273317 feat(format): Enhance format module with rollback, diff reporting, and improved parallel execution 2025-05-19 13:34:23 +00:00
philkunz 7b2ae01112 1.15.5
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-14 12:25:01 +00:00
philkunz 53421e79d8 fix(dependencies): Update @git.zone/tsdoc to ^1.5.0 and @types/node to ^22.15.18 2025-05-14 12:25:01 +00:00
philkunz eec803e512 1.15.4
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-13 21:20:54 +00:00
philkunz 6d11515b47 fix(package.json): Update dependency versions: bump @git.zone/tsdoc, @push.rocks/lik, @push.rocks/smartlog, and @types/node to their latest releases 2025-05-13 21:20:54 +00:00
philkunz 6a7b4c8b7e 1.15.3
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-15 22:16:16 +00:00
philkunz 25c0162c39 fix(deps): update dependency versions and improve website template variable handling 2025-04-15 22:16:16 +00:00
philkunz e66d1f05e4 1.15.2
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-15 16:06:28 +00:00
philkunz b1a8a5527e fix(website_update): Await supplyVariables call in website update template 2025-04-15 16:06:27 +00:00
philkunz 0017781516 1.15.1
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-15 15:54:04 +00:00
philkunz da0cd0ed71 fix(cli): Refresh internal CLI tooling and configuration for consistency. 2025-04-15 15:54:03 +00:00
philkunz 4890afb2f0 1.15.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-15 15:53:34 +00:00
philkunz 77bb8bf22e feat(config/template): Add assetbrokerUrl and legalUrl fields to module config and update website template to supply these values 2025-04-15 15:53:34 +00:00
philkunz ff2ebc076d 1.14.1
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-15 15:36:51 +00:00
philkunz d71b082b75 fix(package.json): Add packageManager field to specify pnpm version for consistent package management 2025-04-15 15:36:51 +00:00
philkunz f6680dc45e 1.14.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-15 15:35:33 +00:00
philkunz cb4a02d1ef feat(tsconfig_update): Add runafter directive to trigger gitzone format after tsconfig update 2025-04-15 15:35:33 +00:00
philkunz 9e39444daa 1.13.1
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-07 02:30:14 +00:00
philkunz 6ec5d275bb fix(cli): Improve commit message logging 2025-03-07 02:30:14 +00:00
philkunz f5d78f3b40 1.13.0 2025-02-28 18:00:11 +00:00
philkunz 68546939ca feat(templates): Updated and added new TypeScript template files for npm projects 2025-02-28 18:00:11 +00:00
philkunz 08e9849116 1.12.8
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-02-25 18:47:41 +00:00
philkunz f25ade3e97 fix(metadata): Updated package and npmextra json description and keywords for enhanced development workflow clarity 2025-02-25 18:47:41 +00:00
philkunz 6870c40e57 1.12.7
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-02-25 18:43:56 +00:00
philkunz 5e99a37a55 fix(meta): Fix issues in project metadata and configuration. 2025-02-25 18:43:56 +00:00
philkunz 44d1cefc9f fix(ci): Updated dependencies and added CI/CD workflows. 2025-02-25 18:43:29 +00:00
philkunz 83bafbb6f6 1.12.6
Default (tags) / security (push) Failing after 2s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-01-29 17:56:21 +01:00
philkunz b90234e226 fix(project): Minor fixes and cleanup 2025-01-29 17:56:21 +01:00
philkunz a039a58f8e 1.12.5
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-01-29 15:38:36 +01:00
philkunz d861fcb2d4 fix(cli): Initial implementation of CLI utility with project management features 2025-01-29 15:38:35 +01:00
philkunz 3a1138da83 1.12.4
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-01-29 14:45:43 +01:00
philkunz bdbd2b6e08 update 2025-01-29 14:45:32 +01:00
philkunz 43f5643ab1 1.12.3
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-01-29 14:42:30 +01:00
philkunz 99e4dcb2e8 update 2025-01-29 14:42:24 +01:00
philkunz 52f6afb69a 1.12.2
Default (tags) / security (push) Failing after 2s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-01-29 14:39:31 +01:00
philkunz 75c1e3d8f2 fix(format): Add overrides for peek-readable in package.json formatting 2025-01-29 14:39:31 +01:00
philkunz 2a1ed874b2 1.12.1
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-01-18 12:38:15 +01:00
philkunz 58fb0bc3d5 fix(dependencies): Update various package dependencies and Dockerfile base image 2025-01-18 12:38:14 +01:00
philkunz b1fbd38dba 1.12.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-01-17 17:48:56 +01:00
philkunz fad5e046d3 feat(build): Update TypeScript configuration to support emit decorator metadata 2025-01-17 17:48:56 +01:00
philkunz eb1498c242 1.11.0
Default (tags) / security (push) Failing after 2s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-01-08 22:31:12 +01:00
philkunz 6b3d4d1ecf feat(cli): Add Docker command for cleaning up Docker system and extend deprecation command for multiple registries 2025-01-08 22:31:11 +01:00
philkunz 16ea9bb8bd 1.10.10
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-01-01 05:33:40 +01:00
philkunz 7e7aa098f9 fix(templates): Corrected typo in template file comment 2025-01-01 05:33:40 +01:00
philkunz df4350d29d 1.10.9
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-01-01 05:32:10 +01:00
philkunz 81eb713dee fix(templates): Correct template file paths and organization for service projects 2025-01-01 05:32:10 +01:00
philkunz 90637b1466 1.10.8
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-01-01 05:02:37 +01:00
philkunz 6c3b78d0fe fix(assets/templates): Update CI template configurations to use module.githost 2025-01-01 05:02:36 +01:00
philkunz f0c7cf34e6 1.10.7
Default (tags) / security (push) Failing after 2s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2024-12-26 14:48:07 +01:00
philkunz b9b5736e5e fix(assets): Correct URLs in templates and fix TypeScript declaration 2024-12-26 14:48:07 +01:00
philkunz b73acf904a 1.10.6
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2024-12-08 17:47:45 +01:00
philkunz 6614529122 fix(ci): Corrected Docker image URL in CI templates 2024-12-08 17:47:45 +01:00
112 changed files with 19622 additions and 6114 deletions
+9 -9
View File
@@ -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
+11 -11
View File
@@ -6,19 +6,19 @@ on:
- '*' - '*'
env: env:
IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git NPMCI_COMPUTED_REPOURL: https://${-{gitea.repository_owner}-}:${-{secrets.GITEA_TOKEN}-}@{{module.githost}}/${-{gitea.repository}-}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}} NPMCI_TOKEN_NPM: ${-{secrets.NPMCI_TOKEN_NPM}-}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}} NPMCI_TOKEN_NPM2: ${-{secrets.NPMCI_TOKEN_NPM2}-}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}} NPMCI_GIT_GITHUBTOKEN: ${-{secrets.NPMCI_GIT_GITHUBTOKEN}-}
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}} NPMCI_URL_CLOUDLY: ${-{secrets.NPMCI_URL_CLOUDLY}-}
jobs: jobs:
security: security:
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
container: container:
image: ${{ env.IMAGE }} image: ${-{ env.IMAGE }-}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -42,11 +42,11 @@ jobs:
continue-on-error: true continue-on-error: true
test: test:
if: ${{ always() }} if: ${-{ always() }-}
needs: security needs: security
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: ${{ env.IMAGE }} image: ${-{ env.IMAGE }-}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -74,7 +74,7 @@ jobs:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: ${{ env.IMAGE }} image: ${-{ env.IMAGE }-}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -95,7 +95,7 @@ jobs:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: ${{ env.IMAGE }} image: ${-{ env.IMAGE }-}
continue-on-error: true continue-on-error: true
steps: steps:
+7 -2
View File
@@ -3,7 +3,6 @@
# artifacts # artifacts
coverage/ coverage/
public/ public/
pages/
# installs # installs
node_modules/ node_modules/
@@ -17,4 +16,10 @@ node_modules/
dist/ dist/
dist_*/ dist_*/
# custom # AI
.claude/
.serena/
#------# custom
.serena
test-output.json
+53 -16
View File
@@ -1,34 +1,71 @@
{ {
"npmci": { "@ship.zone/szci": {
"npmGlobalTools": [], "npmGlobalTools": []
"npmAccessLevel": "private",
"npmRegistryUrl": "verdaccio.lossless.one"
}, },
"gitzone": { "@git.zone/tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
},
"@git.zone/cli": {
"schemaVersion": 2,
"projectType": "npm", "projectType": "npm",
"module": { "module": {
"githost": "gitlab.com", "githost": "gitlab.com",
"gitscope": "gitzone/private", "gitscope": "gitzone/private",
"gitrepo": "gitzone", "gitrepo": "gitzone",
"description": "A CLI toolbelt to streamline local development cycles by using various gitzone utilities.", "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.",
"npmPackagename": "@gitzone_private/gitzone", "npmPackagename": "@gitzone_private/gitzone",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [
"CLI", "CLI",
"development", "local development",
"git", "gitzone utilities",
"npm", "project setup",
"version control",
"code formatting",
"template management",
"npm packages",
"TypeScript", "TypeScript",
"automation", "automation",
"project setup", "commit management",
"code formatting",
"template creation", "template creation",
"logging", "meta project management",
"meta project management" "sync",
"Docker",
"CI/CD"
] ]
}
}, },
"tsdoc": { "commit": {
"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" "confirmation": "prompt",
"steps": ["analyze", "changelog", "commit"]
},
"release": {
"confirmation": "prompt",
"preflight": {
"requireCleanTree": true,
"test": false,
"build": true
},
"targets": {
"git": {
"enabled": true,
"remote": "origin",
"pushBranch": true,
"pushTags": true
},
"npm": {
"enabled": true,
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public",
"alreadyPublished": "success"
},
"docker": {
"enabled": false,
"images": []
}
}
}
} }
} }
+1
View File
@@ -0,0 +1 @@
{}
@@ -6,8 +6,8 @@ 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}-}@{{git.host}}/${-{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}-}
@@ -6,8 +6,8 @@ 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}-}@{{git.host}}/${-{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}-}
@@ -7,7 +7,7 @@ on:
env: env:
IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${-{gitea.repository_owner}-}:${-{secrets.GITEA_TOKEN}-}@{{git.host}}/${-{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}-}
@@ -7,7 +7,7 @@ on:
env: env:
IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${-{gitea.repository_owner}-}:${-{secrets.GITEA_TOKEN}-}@{{git.host}}/${-{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}-}
@@ -7,7 +7,7 @@ on:
env: env:
IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${-{gitea.repository_owner}-}:${-{secrets.GITEA_TOKEN}-}@{{gi.host}}/${-{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}-}
@@ -7,7 +7,7 @@ on:
env: env:
IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${-{gitea.repository_owner}-}:${-{secrets.GITEA_TOKEN}-}@{{git.host}}/${-{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}-}
@@ -89,8 +89,8 @@ jobs:
npmci docker login npmci docker login
npmci docker build npmci docker build
npmci docker test npmci docker test
# npmci docker push {{git.host}} # npmci docker push {{module.githost}}
npmci docker push {{git.host}} npmci docker push {{module.githost}}
metadata: metadata:
needs: test needs: test
@@ -1,6 +1,6 @@
# gitzone dockerfile_service # gitzone dockerfile_service
## STAGE 1 // BUILD ## STAGE 1 // BUILD
FROM code.foss.global/hosttoday/ht-docker-node:npmci as node1 FROM code.foss.global/host.today/ht-docker-node:npmci as node1
COPY ./ /app COPY ./ /app
WORKDIR /app WORKDIR /app
ARG NPMCI_TOKEN_NPM2 ARG NPMCI_TOKEN_NPM2
@@ -12,7 +12,7 @@ RUN pnpm run build
# gitzone dockerfile_service # gitzone dockerfile_service
## STAGE 2 // install production ## STAGE 2 // install production
FROM code.foss.global/hosttoday/ht-docker-node:npmci as node2 FROM code.foss.global/host.today/ht-docker-node:npmci as node2
WORKDIR /app WORKDIR /app
COPY --from=node1 /app /app COPY --from=node1 /app /app
RUN rm -rf .pnpm-store RUN rm -rf .pnpm-store
@@ -24,7 +24,7 @@ RUN rm -rf node_modules/ && pnpm install --prod
## STAGE 3 // rebuild dependencies for alpine ## STAGE 3 // rebuild dependencies for alpine
FROM code.foss.global/hosttoday/ht-docker-node:alpinenpmci as node3 FROM code.foss.global/host.today/ht-docker-node:alpine_npmci as node3
WORKDIR /app WORKDIR /app
COPY --from=node2 /app /app COPY --from=node2 /app /app
ARG NPMCI_TOKEN_NPM2 ARG NPMCI_TOKEN_NPM2
@@ -34,7 +34,7 @@ RUN pnpm config set store-dir .pnpm-store
RUN pnpm rebuild -r RUN pnpm rebuild -r
## STAGE 4 // the final production image with all dependencies in place ## STAGE 4 // the final production image with all dependencies in place
FROM code.foss.global/hosttoday/ht-docker-node:alpine as node4 FROM code.foss.global/host.today/ht-docker-node:alpine as node4
WORKDIR /app WORKDIR /app
COPY --from=node3 /app /app COPY --from=node3 /app /app
+9 -1
View File
@@ -19,4 +19,12 @@ node_modules/
dist/ dist/
dist_*/ dist_*/
# custom # rust
rust/target/
dist_rust/
# AI
.claude/
.serena/
#------# custom
-1
View File
@@ -2,4 +2,3 @@ runafter:
- git add -A && git commit -m initial - git add -A && git commit -m initial
- git push origin master - git push origin master
- gitzone meta update - gitzone meta update
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"lib": ["ES2022", "DOM"],
"target": "ES2022",
"checkJs": true
},
"nodeModulesDir": true
}
+9 -9
View File
@@ -12,17 +12,17 @@ fileName: package.json
"author": "{{module.author}}", "author": "{{module.author}}",
"license": "{{module.license}}", "license": "{{module.license}}",
"scripts": { "scripts": {
"test": "(tstest test/ --web)", "test": "(tstest test/ --verbose --logfile --timeout 60)",
"build": "(tsbuild --web --allowimplicitany)", "build": "(tsbuild tsfolders --allowimplicitany)",
"buildDocs": "(tsdoc)" "buildDocs": "(tsdoc)"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.1.25", "@git.zone/tsbuild": "^4.1.2",
"@git.zone/tsbundle": "^2.0.5", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tsrun": "^1.2.46", "@git.zone/tstest": "^3.1.8",
"@git.zone/tstest": "^1.0.44", "@types/node": "^25.2.0"
"@push.rocks/tapbundle": "^5.0.15",
"@types/node": "^20.8.7"
}, },
"dependencies": {} "dependencies": {
"@push.rocks/smartpath": "^6.0.0"
}
} }
+2 -2
View File
@@ -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 -1
View File
@@ -1,3 +1,3 @@
import * as plugins from './{{module.name}}.plugins.js'; import * as plugins from './plugins.js';
export let demoExport = 'Hi there! :) This is an exported string'; export let demoExport = 'Hi there! :) This is an exported string';
+8
View File
@@ -0,0 +1,8 @@
---
fileName: paths.ts
---
import * as plugins from './plugins.js';
export const packageDir = plugins.path.join(
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
'../'
);
+12
View File
@@ -0,0 +1,12 @@
---
fileName: plugins.ts
---
// native scope
import * as path from 'path';
export { path };
// @push.rocks scope
import * as smartpath from '@push.rocks/smartpath';
export { smartpath };
-7
View File
@@ -1,7 +0,0 @@
---
fileName: {{module.name}}.plugins.ts
---
const removeme = {};
export {
removeme
}
+8 -8
View File
@@ -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.0.3" "@types/node": "^24.10.1"
}, },
"dependencies": { "dependencies": {
"@api.global/typedserver": "^1.0.24", "@api.global/typedserver": "^3.0.53",
"@push.rocks/projectinfo": "^5.0.1", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/qenv": "^4.0.10", "@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"
} }
} }
+10 -4
View File
@@ -1,5 +1,5 @@
{ {
"gitzone": { "@git.zone/cli": {
"projectType": "{{projectType}}", "projectType": "{{projectType}}",
"module": { "module": {
"githost": "{{module.githost}}", "githost": "{{module.githost}}",
@@ -9,16 +9,22 @@
"npmPackagename": "{{module.npmPackagename}}", "npmPackagename": "{{module.npmPackagename}}",
"license": "{{module.license}}", "license": "{{module.license}}",
"projectDomain": "{{module.projectDomain}}" "projectDomain": "{{module.projectDomain}}"
},
"release": {
"targets": {
"npm": {
"registries": ["{{npmPrivateRegistry}}"]
}
}
} }
}, },
"npmci": { "@ship.zone/szci": {
"npmGlobalTools": [], "npmGlobalTools": [],
"dockerRegistryRepoMap": { "dockerRegistryRepoMap": {
"registry.gitlab.com": "{{dockerTargetImagePath}}" "registry.gitlab.com": "{{dockerTargetImagePath}}"
}, },
"dockerBuildargEnvMap": { "dockerBuildargEnvMap": {
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2" "NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
}, }
"npmRegistryUrl": "{{npmPrivateRegistry}}"
} }
} }
@@ -1,8 +1,8 @@
--- ---
fileName: {{module.name}}.classes.{{module.name}}db.ts fileName: classes.{{module.name}}db.ts
--- ---
import * as plugins from './{{module.name}}.plugins.js'; import * as plugins from './{{module.name}}.plugins.js';
import { {{module.name}} } from './{{module.name}}.classes.{{module.name}}.js'; import { {{module.name}} } from './classes.{{module.name}}.js';
@@ -1,9 +1,9 @@
--- ---
fileName: {{module.name}}.classes.{{module.name}}.ts fileName: classes.{{module.name}}.ts
--- ---
import * as plugins from './{{module.name}}.plugins.js'; import * as plugins from './{{module.name}}.plugins.js';
import * as paths from './{{module.name}}.paths.js'; import * as paths from './{{module.name}}.paths.js';
import { {{module.name}}Db } from './{{module.name}}.db.js' import { {{module.name}}Db } from './classes.{{module.name}}db.js'
export class {{module.name}} { export class {{module.name}} {
public projectinfo: plugins.projectinfo.ProjectInfo; public projectinfo: plugins.projectinfo.ProjectInfo;
+1 -2
View File
@@ -15,8 +15,7 @@ export {
loleServiceserver loleServiceserver
} }
// pushrocks scope // @push.rocks scope
// pushrocks scope
import * as projectinfo from '@push.rocks/projectinfo'; import * as projectinfo from '@push.rocks/projectinfo';
import * as qenv from '@push.rocks/qenv'; import * as qenv from '@push.rocks/qenv';
import * as smartdata from '@push.rocks/smartdata'; import * as smartdata from '@push.rocks/smartdata';
@@ -1,5 +1,8 @@
---
fileName: .smartconfig.json
---
{ {
"gitzone": { "@git.zone/cli": {
"projectType": "{{projectType}}", "projectType": "{{projectType}}",
"module": { "module": {
"githost": "{{module.githost}}", "githost": "{{module.githost}}",
@@ -9,10 +12,16 @@
"npmPackagename": "{{module.npmPackagename}}", "npmPackagename": "{{module.npmPackagename}}",
"license": "{{module.license}}", "license": "{{module.license}}",
"projectDomain": "{{module.projectDomain}}" "projectDomain": "{{module.projectDomain}}"
},
"release": {
"targets": {
"npm": {
"accessLevel": "{{module.npmAccessLevel}}"
}
}
} }
}, },
"npmci": { "@ship.zone/szci": {
"npmGlobalTools": [], "npmGlobalTools": []
"npmAccessLevel": "{{module.npmAccessLevel}}"
} }
} }
@@ -0,0 +1,2 @@
runafter:
- gitzone format
@@ -1,7 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"json.schemas": [ "json.schemas": [
{ {
"fileMatch": ["/npmextra.json"], "fileMatch": ["/.smartconfig.json"],
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -0,0 +1,2 @@
export * from './page2.js';
export * from './page1.js';
@@ -0,0 +1,5 @@
import { html } from "@design.estate/dees-element";
export const page1 = () => html`
<first-element></first-element>
`;
@@ -0,0 +1,5 @@
import { html } from "@design.estate/dees-element";
export const page2 = () => html`
<first-element></first-element>
`;
+10 -4
View File
@@ -1,5 +1,5 @@
{ {
"gitzone": { "@git.zone/cli": {
"projectType": "website", "projectType": "website",
"module": { "module": {
"githost": "{{module.githost}}", "githost": "{{module.githost}}",
@@ -9,16 +9,22 @@
"npmPackagename": "{{module.npmPackagename}}", "npmPackagename": "{{module.npmPackagename}}",
"license": "{{module.license}}", "license": "{{module.license}}",
"projectDomain": "{{module.projectDomain}}" "projectDomain": "{{module.projectDomain}}"
},
"release": {
"targets": {
"npm": {
"registries": ["{{private.npmRegistryUrl}}"]
}
}
} }
}, },
"npmci": { "@ship.zone/szci": {
"npmGlobalTools": [], "npmGlobalTools": [],
"dockerRegistryRepoMap": { "dockerRegistryRepoMap": {
"registry.gitlab.com": "{{docker.registryImageTag}}" "registry.gitlab.com": "{{docker.registryImageTag}}"
}, },
"dockerBuildargEnvMap": { "dockerBuildargEnvMap": {
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2" "NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
}, }
"npmRegistryUrl": "{{private.npmRegistryUrl}}"
} }
} }
@@ -6,7 +6,7 @@ import {
cssManager, cssManager,
unsafeCSS, unsafeCSS,
css, css,
TemplateResult, type TemplateResult,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
@customElement('default-header') @customElement('default-header')
+877 -2
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2015 Task Venture Capital GmbH (hello@lossless.com) Copyright (c) 2015 Task Venture Capital GmbH (hello@task.vc)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+65 -54
View File
@@ -1,9 +1,9 @@
{ {
"name": "@git.zone/cli", "name": "@git.zone/cli",
"private": false, "private": false,
"version": "1.10.5", "version": "2.16.0",
"description": "A CLI toolbelt to streamline local development cycles by using various gitzone utilities.", "description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.",
"main": "dist_ts/index.ts", "main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",
"type": "module", "type": "module",
"bin": { "bin": {
@@ -11,85 +11,86 @@
"gzone": "./cli.js" "gzone": "./cli.js"
}, },
"scripts": { "scripts": {
"test": "(npm run clean && npm run prepareTest && npm run testCli && npm run testFormat && npm run testCommit && npm run testDeprecate && npm run testVersion && npm run testReadme && npm run testUpdate && npm run testTemplateNpm && npm run testTemplateLit) && rm -rf test", "test": "(pnpm run clean && pnpm run prepareTest && pnpm run testCli && pnpm run testFormat && pnpm run testCommit && pnpm run testDeprecate && pnpm run testVersion && pnpm run testReadme && pnpm run testUpdate && pnpm run testTemplateNpm && pnpm run testTemplateLit) && rm -rf .nogit/test",
"build": "(tsbuild --web --allowimplicitany)", "build": "tsbuild tsfolders",
"clean": "(rm -rf test/)", "clean": "(rm -rf .nogit/test/)",
"prepareTest": "(git clone https://gitlab.com/sandboxzone/sandbox-npmts.git test/)", "prepareTest": "(mkdir -p .nogit && git clone https://gitlab.com/sandboxzone/sandbox-npmts.git .nogit/test/)",
"testBuild": "npm run build && rm -r dist/", "testBuild": "pnpm run build && rm -r dist/",
"testCli": "(cd test && node ../cli.ts.js)", "testCli": "(cd .nogit/test && node ../../cli.ts.js)",
"testCommit": "(cd test && node ../cli.ts.js commit)", "testCommit": "(cd .nogit/test && node ../../cli.ts.js commit)",
"testDeprecate": "(cd test && node ../cli.ts.js deprecate)", "testDeprecate": "(cd .nogit/test && node ../../cli.ts.js deprecate)",
"testOpen": "(cd test && node ../cli.ts.js open ci)", "testOpen": "(cd .nogit/test && node ../../cli.ts.js open ci)",
"testReadme": "(cd test && node ../cli.ts.js readme)", "testReadme": "(cd .nogit/test && node ../../cli.ts.js readme)",
"testFormat": "(cd test && node ../cli.ts.js format)", "testFormat": "(cd .nogit/test && node ../../cli.ts.js format)",
"testTemplateNpm": "(rm -rf test/testtemplate_npm/ && mkdir test/testtemplate_npm && cd test/testtemplate_npm && node ../../cli.ts.js template npm)", "testTemplateNpm": "(rm -rf .nogit/test/testtemplate_npm/ && mkdir -p .nogit/test/testtemplate_npm && cd .nogit/test/testtemplate_npm && node ../../../cli.ts.js template npm)",
"testTemplateLit": "(rm -rf test/testtemplate_lit/ && mkdir test/testtemplate_lit && cd test/testtemplate_lit && node ../../cli.ts.js template lit)", "testTemplateLit": "(rm -rf .nogit/test/testtemplate_lit/ && mkdir -p .nogit/test/testtemplate_lit && cd .nogit/test/testtemplate_lit && node ../../../cli.ts.js template lit)",
"testUpdate": "(cd test && node ../cli.ts.js update)", "testUpdate": "(cd .nogit/test && node ../../cli.ts.js update)",
"testVersion": "(cd test && node ../cli.ts.js -v)", "testVersion": "(cd .nogit/test && node ../../cli.ts.js -v)",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://code.foss.global/git.zone/cli.git" "url": "https://gitlab.com/gitzone/private/gitzone.git"
}, },
"keywords": [ "keywords": [
"CLI", "CLI",
"development", "local development",
"git", "gitzone utilities",
"npm", "project setup",
"version control",
"code formatting",
"template management",
"npm packages",
"TypeScript", "TypeScript",
"automation", "automation",
"project setup", "commit management",
"code formatting",
"template creation", "template creation",
"logging", "meta project management",
"meta project management" "sync",
"Docker",
"CI/CD"
], ],
"author": "Task Venture Capital GmbH", "author": "Task Venture Capital GmbH",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://code.foss.global/git.zone/cli/issues" "url": "https://gitlab.com/gitzone/private/gitzone/issues"
}, },
"homepage": "https://pages.foss.global/git.zone/cli", "homepage": "https://gitlab.com/gitzone/private/gitzone#readme",
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.1.84", "@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsrun": "^1.2.49", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^1.0.90", "@git.zone/tstest": "^3.3.2",
"@types/node": "^22.8.1" "@types/node": "^25.4.0"
}, },
"dependencies": { "dependencies": {
"@git.zone/tsdoc": "^1.3.12", "@git.zone/tsdoc": "^2.0.0",
"@git.zone/tspublish": "^1.4.0", "@git.zone/tspublish": "^1.11.2",
"@push.rocks/commitinfo": "^1.0.12", "@push.rocks/commitinfo": "^1.0.12",
"@push.rocks/early": "^4.0.4", "@push.rocks/early": "^4.0.4",
"@push.rocks/gulp-function": "^3.0.7",
"@push.rocks/lik": "^6.0.15",
"@push.rocks/npmextra": "^5.0.23",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/smartchok": "^1.0.34", "@push.rocks/smartcli": "^4.0.20",
"@push.rocks/smartcli": "^4.0.11", "@push.rocks/smartconfig": "^6.0.1",
"@push.rocks/smartdelay": "^3.0.5", "@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartfile": "^11.0.21", "@push.rocks/smartdiff": "^1.1.0",
"@push.rocks/smartgulp": "^3.0.4", "@push.rocks/smartfile": "^13.1.2",
"@push.rocks/smartinteract": "^2.0.15", "@push.rocks/smartfs": "^1.5.0",
"@push.rocks/smartjson": "^5.0.20", "@push.rocks/smartinteract": "^2.0.16",
"@push.rocks/smartjson": "^6.0.0",
"@push.rocks/smartlegal": "^1.0.27", "@push.rocks/smartlegal": "^1.0.27",
"@push.rocks/smartlog": "^3.0.7", "@push.rocks/smartlog": "^3.2.1",
"@push.rocks/smartlog-destination-local": "^9.0.2", "@push.rocks/smartlog-destination-local": "^9.0.2",
"@push.rocks/smartmustache": "^3.0.2", "@push.rocks/smartmustache": "^3.0.2",
"@push.rocks/smartnpm": "^2.0.4", "@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartnpm": "^2.0.6",
"@push.rocks/smartobject": "^1.0.12", "@push.rocks/smartobject": "^1.0.12",
"@push.rocks/smartopen": "^2.0.0", "@push.rocks/smartopen": "^2.0.0",
"@push.rocks/smartpath": "^5.0.18", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.0.4", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartscaf": "^4.0.15", "@push.rocks/smartscaf": "^4.0.21",
"@push.rocks/smartshell": "^3.0.6", "@push.rocks/smartshell": "^3.3.7",
"@push.rocks/smartstream": "^3.0.44",
"@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartunique": "^3.0.9",
"@push.rocks/smartupdate": "^2.0.6", "@push.rocks/smartupdate": "^2.0.6",
"@types/through2": "^2.0.41", "prettier": "^3.8.1"
"prettier": "^3.3.3",
"through2": "^4.0.2"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",
@@ -100,10 +101,20 @@
"dist_ts_web/**/*", "dist_ts_web/**/*",
"assets/**/*", "assets/**/*",
"cli.js", "cli.js",
"npmextra.json", ".smartconfig.json",
"readme.md" "readme.md"
], ],
"browserslist": [ "browserslist": [
"last 1 chrome versions" "last 1 chrome versions"
],
"pnpm": {
"overrides": {},
"onlyBuiltDependencies": [
"esbuild",
"mongodb-memory-server",
"puppeteer",
"sharp"
] ]
},
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
} }
+7815 -5041
View File
File diff suppressed because it is too large Load Diff
+301 -1
View File
@@ -1 +1,301 @@
* the cli of the git.zone project. # Gitzone CLI - Development Hints
- the cli of the git.zone project.
## Project Overview
Gitzone CLI (`@git.zone/cli`) is a comprehensive toolbelt for streamlining local development cycles. It provides utilities for:
- Project initialization and templating (via smartscaf)
- Code formatting and standardization
- Version control and commit management
- Docker and CI/CD integration
- Meta project management
## Architecture
### Core Structure
- Main CLI entry: `cli.ts` / `cli.child.ts`
- Modular architecture with separate modules in `ts/mod_*` directories
- Each module handles specific functionality (format, commit, docker, etc.)
- Extensive use of plugins pattern via `plugins.ts` files
### Configuration Management
- Uses `.smartconfig.json` for tool configuration
- CLI settings live under the `@git.zone/cli` namespace
- Agent and non-interactive defaults now belong under `@git.zone/cli.cli`
- Project type, module metadata, release settings, commit defaults, and format settings live in the same file
### Format Module (`mod_format`) - SIGNIFICANTLY ENHANCED
The format module is responsible for project standardization:
#### Current Modules:
1. **cleanup** - Removes obsolete files (yarn.lock, tslint.json, etc.)
2. **copy** - File copying with glob patterns (fully implemented)
3. **gitignore** - Creates/updates .gitignore from templates
4. **license** - Checks dependency licenses for compatibility
5. **smartconfig** - Manages project metadata and configuration
6. **packagejson** - Formats and updates package.json
7. **prettier** - Applies code formatting with batching
8. **readme** - Ensures readme files exist
9. **templates** - Updates project templates based on type
10. **tsconfig** - Formats TypeScript configuration
#### Execution Order (Dependency-Based):
- Modules are now executed in parallel groups based on dependencies
- Independent modules run concurrently for better performance
- Dependency analyzer ensures correct execution order
### New Architecture Features
1. **BaseFormatter Pattern**: All formatters extend abstract BaseFormatter class
2. **FormatContext**: Central state management across all modules
3. **FormatPlanner**: Implements plan → action workflow
4. **RollbackManager**: Full backup/restore capabilities
5. **ChangeCache**: Tracks file changes to optimize performance
6. **DependencyAnalyzer**: Manages module execution order
7. **DiffReporter**: Generates diff views for changes
8. **FormatStats**: Comprehensive execution statistics
### Key Patterns
1. **Plugin Architecture**: All dependencies imported through `plugins.ts` files
2. **Streaming**: Uses smartstream for file processing
3. **Interactive Prompts**: smartinteract for user input
4. **Enhanced Error Handling**: Comprehensive try-catch with automatic rollback
5. **Template System**: Templates handled by smartscaf, not directly by gitzone
6. **Type Safety**: Full TypeScript with interfaces and type definitions
### Important Notes
- `.nogit/` directory used for temporary/untracked files, backups, and cache
- `.nogit/gitzone-backups/` stores format operation backups
- `.nogit/gitzone-cache/` stores file change cache
- Templates are managed by smartscaf - improvements should be made there
- License checking configurable with exceptions support
- All features implemented: `ensureDependency`, copy module, etc.
## Recent Improvements (Completed)
1. **Plan → Action Workflow**: Shows changes before applying them
2. **Rollback Mechanism**: Full backup and restore on failures
3. **Enhanced Configuration**: Granular control via `.smartconfig.json`
4. **Better Error Handling**: Detailed errors with recovery options
5. **Performance Optimizations**: Parallel execution and caching
6. **Reporting**: Diff views, statistics, verbose logging
7. **Architecture**: Clean separation of concerns with new classes
8. **Split Commit/Release Workflows**: `commit` creates source commits; `release` owns versioning, tags, and artifact publishing
### Commit/Release Workflow Refactor (Latest)
The commit module no longer bumps versions, creates tags, or publishes packages. Release work now belongs to `gitzone release`:
**Changes:**
- `gitzone commit` analyzes changes, updates `changelog.md` `Pending`, commits, and optionally pushes.
- `gitzone release` reads `Pending`, bumps versions, moves changelog entries into a version section, tags, pushes, and publishes configured artifacts.
- Commit workflow steps are configured in `.smartconfig.json` under `@git.zone/cli.commit.steps`.
- Smartconfig schema versioning lives at `@git.zone/cli.schemaVersion`; run `gitzone config migrate <version>` for targeted migrations.
- Release publishing is target-based under `@git.zone/cli.release.targets`.
- NPM registries only live under `@git.zone/cli.release.targets.npm.registries`.
**Benefits:**
- Commit is safer and has no publishing side effects.
- Multiple source commits can accumulate into one release via `Pending`.
- Per-artifact release results can distinguish published, already-published, skipped, and failed targets.
### Auto-Accept Flag for Commits
The commit module now supports `-y/--yes` flag for non-interactive commits:
**Usage:**
- `gitzone commit -y` - Auto-accepts AI recommendations without prompts
- `gitzone commit -yp` - Auto-accepts and pushes to origin
- Separate `-p/--push` flag controls push behavior
**Implementation:**
- Creates AnswerBucket programmatically when `-y` flag detected
- Preserves all UI output for transparency
- Fully backward compatible with interactive mode
- CI/CD friendly for automated workflows
## Development Tips
- Always check readme.plan.md for ongoing improvement plans
- Use `.smartconfig.json` for any new configuration options
- Keep modules focused and single-purpose
- Maintain the existing plugin pattern for dependencies
- Test format operations on sample projects before deploying
- Consider backward compatibility when changing configuration structure
- Use BaseFormatter pattern for new format modules
- Leverage FormatContext for cross-module state sharing
## Configuration Examples
```json
{
"@git.zone/cli": {
"cli": {
"interactive": true,
"output": "human",
"checkUpdates": true
},
"format": {
"interactive": true,
"showStats": true,
"modules": {
"skip": ["prettier"],
"only": []
}
}
}
}
```
## CLI Usage
### Commit Commands
```bash
# Interactive commit (default)
gitzone commit
# Read-only recommendation
gitzone commit recommend --json
# Auto-accept AI recommendations (no prompts)
gitzone commit -y
gitzone commit --yes
# Auto-accept and push to origin
gitzone commit -yp
gitzone commit -y -p
gitzone commit --yes --push
# Run format before commit
gitzone commit --format
```
### Format Commands
```bash
# Basic format
gitzone format
# Read-only JSON plan
gitzone format plan --json
# Dry run to preview changes
gitzone format --dry-run
# Non-interactive apply
gitzone format --write --yes
# Plan only (no execution)
gitzone format --plan-only
# Save plan for later
gitzone format --save-plan format.json
# Execute saved plan
gitzone format --from-plan format.json
# Verbose mode
gitzone format --verbose
# Detailed diff views
gitzone format --detailed
# Inspect config for agents and scripts
gitzone config show --json
gitzone config set cli.output json
gitzone config get release.targets.npm.accessLevel
```
## Common Issues (Now Resolved)
1. ✅ Format operations are now reversible with rollback
2. ✅ Enhanced error messages with recovery suggestions
3. ✅ All modules fully implemented (including copy)
4. ✅ Dry-run capability available
5. ✅ Extensive configuration options available
## Future Considerations
- Plugin system for custom formatters
- Git hooks integration for pre-commit formatting
- Advanced UI with interactive configuration
- Format presets for common scenarios
- Performance benchmarking tools
## API Changes
### Smartfile v13 Migration (Latest - Completed)
The project has been fully migrated from @push.rocks/smartfile v11 to v13, which introduced a major breaking change where filesystem operations were split into two separate packages:
**Packages:**
- `@push.rocks/smartfile` v13.0.1 - File representation classes (SmartFile, StreamFile, VirtualDirectory)
- `@push.rocks/smartfs` v1.1.0 - Filesystem operations (read, write, exists, stat, etc.)
**Key API Changes:**
1. **File Reading**:
- Old: `plugins.smartfile.fs.toStringSync(path)` or `plugins.smartfile.fs.toObjectSync(path)`
- New: `await plugins.smartfs.file(path).encoding('utf8').read()` + JSON.parse if needed
- Important: `read()` returns `string | Buffer` - use `as string` type assertion when encoding is set
2. **File Writing**:
- Old: `plugins.smartfile.memory.toFs(content, path)` or `plugins.smartfile.memory.toFsSync(content, path)`
- New: `await plugins.smartfs.file(path).encoding('utf8').write(content)`
3. **File Existence**:
- Old: `plugins.smartfile.fs.fileExists(path)` or `plugins.smartfile.fs.fileExistsSync(path)`
- New: `await plugins.smartfs.file(path).exists()`
4. **Directory Operations**:
- Old: `plugins.smartfile.fs.ensureDir(path)`
- New: `await plugins.smartfs.directory(path).recursive().create()`
- Old: `plugins.smartfile.fs.remove(path)`
- New: `await plugins.smartfs.directory(path).recursive().delete()` or `await plugins.smartfs.file(path).delete()`
5. **Directory Listing**:
- Old: `plugins.smartfile.fs.listFolders(path)` or `plugins.smartfile.fs.listFoldersSync(path)`
- New: `await plugins.smartfs.directory(path).list()` then filter by `stats.isDirectory`
- Note: `list()` returns `IDirectoryEntry[]` with `path` and `name` properties - use `stat()` to check if directory
6. **File Stats**:
- Old: `stats.isDirectory()` (method)
- New: `stats.isDirectory` (boolean property)
- Old: `stats.mtimeMs`
- New: `stats.mtime.getTime()`
7. **SmartFile Factory**:
- Old: Direct SmartFile instantiation
- New: `plugins.smartfile.SmartFileFactory.nodeFs()` then factory methods
**Migration Pattern:**
All sync methods must become async. Functions that were previously synchronous (like `getProjectName()`) now return `Promise<T>` and must be awaited.
**Affected Modules:**
- ts/mod_format/\* (largest area - 15+ files)
- ts/mod_commit/\* and ts/mod_release/\* (commit/release workflows)
- ts/mod_services/\* (configuration management)
- ts/mod_meta/\* (meta repository management)
- ts/mod_standard/\* (template listing)
- ts/mod_template/\* (template operations)
**Previous API Changes:**
- smartnpm requires instance creation: `new NpmRegistry()`
- Type imports use `import type` for proper verbatim module syntax
+374 -157
View File
@@ -1,207 +1,424 @@
# @git.zone/cli # @git.zone/cli 🚀
A CLI toolbelt to streamline local development cycles by using various gitzone utilities. `@git.zone/cli` is the development workflow CLI behind the `gitzone` and `gzone` commands. It helps TypeScript-heavy teams keep projects tidy, create semantic source commits, manage local Docker-backed services, scaffold new modules, and release software through explicit, target-based release configuration.
It is opinionated where that saves time: source commits and releases are separate, changelog entries flow through a standard `Pending` section, project config lives in `.smartconfig.json`, and release targets make side effects visible before they happen.
## 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.
## Install ## Install
To install the `@git.zone/cli` tool, you need to have Node.js and npm installed on your machine. Once you have those set up, you can install the CLI tool globally using the following command: ```bash
pnpm add -g @git.zone/cli
```shell
npm install -g @git.zone/cli
``` ```
This will add the `gitzone` or `gzone` command to your PATH, allowing you to use the tool from any directory. After installation, both binaries point to the same CLI:
## Usage ```bash
gitzone --help
The `gitzone` CLI is designed to streamline various aspects of the local development cycle, including project setup, maintenance, and deployment. Below, we'll go through several scenarios that demonstrate the capabilities of `gitzone`. gzone --help
### Getting Started
To start using `gitzone`, you need to initialize a new project or use it with an existing one. This section will guide you through the initial setup and provide examples of the core commands.
#### Initialize a New Project
`gitzone` can create several types of projects, including standard npm modules, websites using LitElement, and custom web components. To create a new project, you can use the following command:
```shell
gitzone template [templatename]
``` ```
Replace `[templatename]` with one of the following: ## The Big Idea
- `npm`: A standard npm module with TypeScript support, testing, and CI/CD setup.
- `website`: A LitElement-based website with e2e testing, bundling, and service worker support.
- `element`: A LitElement standard setup for creating web components.
Example of starting a new npm project: `gitzone commit` handles source history.
1. Open your terminal and navigate to the directory where you want to create your project.
2. Execute the following command: `gitzone release` handles release transactions.
```shell
That split is intentional. A commit should not unexpectedly publish npm packages, push Docker images, or trigger remote release pipelines. A release should clearly show which targets it will publish to.
## Quick Start
```bash
# Preview project standardization work
gitzone format
# Apply formatting changes
gitzone format --write
# Create a semantic source commit
gitzone commit
# Preview the configured release transaction
gitzone release --plan
# Release pending changelog entries to configured targets
gitzone release
```
## Commands
| Command | Purpose |
| --- | --- |
| `commit` | Analyze changes and create one semantic source commit |
| `release` | Turn pending changelog entries into a versioned release and publish targets |
| `format` | Plan or apply project formatting and standardization |
| `config` | Inspect, update, and migrate `.smartconfig.json` |
| `services` | Manage local MongoDB, MinIO, and Elasticsearch containers |
| `tools` | Manage the global `@git.zone` toolchain |
| `template` | Scaffold projects from built-in templates |
| `meta` | Manage multi-repository workspaces |
| `open` | Open repository assets like CI pages |
| `docker` | Run Docker maintenance tasks |
| `deprecate` | Deprecate npm packages across registries |
| `start` | Prepare an existing project for local work |
| `helpers` | Run small helper utilities |
Global flags include `--help`, `--json`, `--plain`, `--agent`, `--no-interactive`, and `--no-check-updates`.
## Toolchain Management
`gitzone tools` replaces the former `gtools` command from `@git.zone/tools`. It manages globally installed `@git.zone` development tools through pnpm.
```bash
# Check installed @git.zone tools and update outdated packages
gitzone tools update
# Update without prompts
gitzone tools update -y
# Install missing managed @git.zone tools
gitzone tools install
```
`gitzone tools update` checks `@git.zone/cli` first. If the CLI itself needs an update, it updates `@git.zone/cli` and asks you to rerun the command before updating the rest of the toolchain.
## Commit Workflow
`gitzone commit` creates one semantic source commit. It does not bump versions, create tags, publish packages, or push Docker images.
```bash
# Interactive semantic commit
gitzone commit
# Read-only AI recommendation
gitzone commit recommend --json
# Auto-accept safe recommendations
gitzone commit -y
# Auto-accept, test, build, and push
gitzone commit -ytbp
# Show the resolved workflow without mutating anything
gitzone commit --plan
```
The commit flow:
1. Analyze the working tree.
2. Suggest commit type, scope, and message.
3. Write a human-readable entry into `changelog.md` under `## Pending`.
4. Stage and create one semantic source commit.
5. Optionally run formatting, tests, build, and push based on flags or config.
Commit flags:
| Flag | Meaning |
| --- | --- |
| `-y`, `--yes` | Auto-accept safe recommendations |
| `-t`, `--test` | Add test step |
| `-b`, `--build` | Add build step |
| `-p`, `--push` | Push after the source commit |
| `-f`, `--format` | Run `gitzone format --write` before commit |
| `--plan` | Show resolved workflow only |
`-r` is intentionally not part of commit anymore. Use `gitzone release`.
## Release Workflow
`gitzone release` performs the release core once, then publishes to configured targets.
The release core is not configurable plumbing. It always follows the same professional release transaction:
1. Run configured preflight checks.
2. Read `changelog.md` `## Pending` entries.
3. Infer or accept a semver bump.
4. Update version files and baked commit info.
5. Move pending changelog entries into the new version section.
6. Create the local release commit.
7. Create the local release tag.
Targets decide what happens after that:
| Target | What it does |
| --- | --- |
| `git` | Pushes the release commit and tags, often triggering remote CI release builds |
| `npm` | Publishes the package to configured npm registries |
| `docker` | Builds and pushes configured Docker images |
```bash
# Preview the resolved release plan
gitzone release --plan
# Release to configured targets
gitzone release
# Release only to npm
gitzone release --target npm
# Release only to git and Docker
gitzone release --target git,docker
# Skip package/container publishing and keep only git target
gitzone release --no-publish
# Override inferred semver level
gitzone release --minor
```
Release flags:
| Flag | Meaning |
| --- | --- |
| `-y`, `--yes` | Run without interactive confirmation |
| `-t`, `--test` | Enable preflight tests |
| `-b`, `--build` | Enable preflight build |
| `-p`, `--push` | Enable the `git` target |
| `--target <csv>` | Use only selected targets, e.g. `git,npm` |
| `--npm` | Enable the `npm` target |
| `--docker` | Enable the `docker` target |
| `--no-publish` | Keep release core and `git` target only |
| `--no-build` | Disable preflight build for this run |
| `--major`, `--minor`, `--patch` | Override inferred semver level |
| `--plan` | Show resolved workflow only |
## Standard Changelog
The changelog is convention-based and intentionally not configured.
`gitzone commit` appends entries to:
```markdown
## Pending
```
`gitzone release` moves those pending entries into a dated version section:
```markdown
## 2026-05-10 - 2.15.0
```
The standard buckets are `Breaking Changes`, `Features`, `Fixes`, `Documentation`, and `Maintenance`.
## Configuration
All CLI config lives under `@git.zone/cli` in `.smartconfig.json`.
```json
{
"@git.zone/cli": {
"schemaVersion": 2,
"projectType": "npm",
"commit": {
"confirmation": "prompt",
"steps": ["analyze", "test", "build", "changelog", "commit", "push"]
},
"release": {
"confirmation": "prompt",
"preflight": {
"requireCleanTree": true,
"test": false,
"build": true
},
"targets": {
"git": {
"enabled": true,
"remote": "origin",
"pushBranch": true,
"pushTags": true
},
"npm": {
"enabled": true,
"registries": ["https://registry.npmjs.org"],
"accessLevel": "public",
"alreadyPublished": "success"
},
"docker": {
"enabled": false,
"images": []
}
}
}
}
}
```
NPM registries belong only here:
```text
@git.zone/cli.release.targets.npm.registries
```
Useful config commands:
```bash
# Show current @git.zone/cli config
gitzone config show --json
# Read the npm release target registries
gitzone config get release.targets.npm.registries
# Add an npm release target registry
gitzone config add https://registry.npmjs.org
# Set npm target access level
gitzone config access public
# Run schema migration to v2
gitzone config migrate 2
```
## Formatting
`gitzone format` is dry-run by default. That makes it safe to run in any repo.
```bash
# Preview changes
gitzone format
# Emit a machine-readable plan
gitzone format plan --json
# Apply changes
gitzone format --write
# Apply without prompt
gitzone format --write --yes
```
Formatters include cleanup, smartconfig normalization, dependency license checks, package metadata normalization, template updates, `.gitignore`, TypeScript config, Prettier, README existence checks, and configured copy operations.
## Development Services
`gitzone services` manages local Docker-backed services for development projects.
Supported services:
| Service | Aliases |
| --- | --- |
| MongoDB | `mongo`, `mongodb` |
| MinIO | `minio`, `s3` |
| Elasticsearch | `elasticsearch`, `es` |
```bash
# Start configured services
gitzone services start
# Enable specific services non-interactively
gitzone services set mongodb,minio
# Check status
gitzone services status
# Print MongoDB Compass connection string
gitzone services compass
# Show logs
gitzone services logs mongo 50
# Stop containers but keep data
gitzone services stop
# Remove containers and data
gitzone services clean
```
Service config is stored in `.nogit/env.json`. Data is stored below `.nogit/`, so it stays out of Git.
## Templates
Start new projects with built-in scaffolds:
```bash
gitzone template npm gitzone template npm
``` gitzone template service
3. Follow the interactive prompts to set up your project. You will be asked to provide information such as the project name, description, GitHub repository, etc. gitzone template website
gitzone template wcc
#### Committing Changes
To standardize commit messages and increment versions based on change types (fix, feat, breaking change):
```shell
gitzone commit
``` ```
This command will prompt you with a series of questions to help construct a standardized commit message and create a new commit. Templates are rendered through SmartScaf and then can be normalized with `gitzone format`.
#### Deprecating a Package ## Meta Repositories
To deprecate an old package in favor of a new one: Use `gitzone meta` when one workspace coordinates multiple repositories.
```shell ```bash
gitzone deprecate
```
This command will prompt you for the old and new package names and will automatically deprecate the old package on npm.
#### Opening CI/CD Settings
Quickly open the CI/CD settings page of your project repository:
```shell
gitzone open ci
```
### Example Commands
#### Project Setup and Initialization
Let's go through how to scaffold a new web component project:
1. Navigate to your desired directory.
2. Run the following command:
```shell
gitzone template element
```
3. Follow the prompts to set up your web component project.
#### Managing Project Lifecycle
Commands to manage your project's lifecycle:
- **Commit Changes:**
```shell
gitzone commit
```
- **Deprecate a Package:**
```shell
gitzone deprecate
```
- **Format Project Files:**
```shell
gitzone format
```
- **Regenerate Readme:**
```shell
gitzone readme
```
- **Update Local Repositories:**
```shell
gitzone update
```
#### Advanced Scenarios
##### Formatting
To ensure consistent formatting using Prettier:
```shell
gitzone format
```
##### Building the Project
To build your project as defined in your `package.json`:
```shell
npm run build
```
##### Git and Version Control
Check the current project version:
```shell
gitzone -v
```
To synchronize local repositories with remotes:
```shell
gitzone update
```
##### Metadata and Configuration
To initialize or update metadata:
```shell
gitzone meta init gitzone meta init
gitzone meta add frontend https://example.com/org/frontend.git
gitzone meta update
gitzone meta remove frontend
``` ```
### Continuous Integration and Delivery (CI/CD) ## Other Utilities
#### Running Tests ```bash
# Docker cleanup
gitzone docker prune
To execute tests defined in your `package.json`: # Open GitLab CI settings or pipelines for the current repo
gitzone open ci
gitzone open pipelines
```shell # Deprecate an old npm package interactively
npm test gitzone deprecate
# Prepare a project for local work
gitzone start
# Generate a short unique ID
gitzone helpers shortid
``` ```
#### Building Documentation ## Troubleshooting
Generate documentation: Format only previews changes:
```shell ```bash
npm run buildDocs gitzone format --write
``` ```
### Troubleshooting and Debugging Release says there is nothing to release:
#### Detailed Logs ```bash
# Make sure commits have populated the Pending changelog section
Enable detailed logging for troubleshooting: gitzone commit
```shell
gitzone --loglevel=debug
``` ```
#### Cleaning Up Docker services fail to start:
To clean up project artifacts: ```bash
docker info
```shell gitzone services status
gitzone clean gitzone services reconfigure
``` ```
### Summary Config looks outdated:
The `gitzone` CLI tool provides a comprehensive suite of commands that streamline project setup, lifecycle management, and deployment, which are indispensable for modern development workflows. By familiarizing yourself with the different commands, you can maximize your productivity and focus on what really matters—writing code. Whether you are starting a new project, maintaining an existing one, or deploying your work, `gitzone` is your toolbelt for efficient development cycles. ```bash
gitzone config migrate 2
gitzone config show --json
```
## 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.
+132
View File
@@ -0,0 +1,132 @@
# GitZone Services Command Implementation Plan
## Overview
Implement the `gitzone services` command to manage MongoDB and MinIO containers for development projects.
## Tasks
### Module Structure Setup
- [x] Create `ts/mod_services/` directory
- [x] Create `mod.plugins.ts` with required imports
- [x] Create `helpers.ts` with utility functions
- [x] Create `classes.serviceconfiguration.ts` for config handling
- [x] Create `classes.dockercontainer.ts` for Docker operations
- [x] Create `classes.servicemanager.ts` for service management
- [x] Create `index.ts` with main command logic
### Core Functionality
- [x] Implement ServiceConfiguration class
- [x] Load/create `.nogit/env.json` configuration
- [x] Generate random available ports (20000-30000 range)
- [x] Preserve existing custom values
- [x] Provide default values for missing fields
- [x] Implement DockerContainer class
- [x] Check container status
- [x] Start/stop/restart containers
- [x] Execute Docker commands
- [x] Handle container logs
- [x] Manage volumes and port bindings
- [x] Implement ServiceManager class
- [x] Manage MongoDB containers
- [x] Manage MinIO containers
- [x] Handle container lifecycle
- [x] Generate project-specific container names
- [x] Manage data directories in `.nogit/`
- [x] Generate MongoDB Compass connection strings
### Commands Implementation
- [x] `start` command - Start services (mongo|s3|all)
- [x] `stop` command - Stop services (mongo|s3|all)
- [x] `restart` command - Restart services (mongo|s3|all)
- [x] `status` command - Show service status
- [x] `config` command - Show current configuration
- [x] `compass` command - Show MongoDB Compass connection string
- [x] `logs` command - Show service logs with line count
- [x] `remove` command - Remove containers (preserve data)
- [x] `clean` command - Remove containers and data
### Integration
- [x] Add `@push.rocks/smartshell` to main plugins.ts
- [x] Add `@push.rocks/smartnetwork` to main plugins.ts
- [x] Add `@push.rocks/smartinteraction` to main plugins.ts
- [x] Register services command in `gitzone.cli.ts`
### Features
- [x] Auto-configuration with smart defaults
- [x] Random port assignment to avoid conflicts
- [x] Project isolation with unique container names
- [x] Data persistence in `.nogit/` directories
- [x] Status display (running/stopped/not installed)
- [x] Interactive confirmations for destructive operations
- [x] Colored console output
- [x] MinIO bucket auto-creation
- [x] MongoDB Compass connection string with network IP
### Testing
- [ ] Test service start/stop operations
- [ ] Test configuration creation and updates
- [ ] Test port collision handling
- [ ] Test data persistence
- [ ] Test MongoDB Compass connection string generation
- [ ] Test all command variations
## Configuration Format
```json
{
"PROJECT_NAME": "derived-from-package-name",
"MONGODB_HOST": "localhost",
"MONGODB_NAME": "project-name",
"MONGODB_PORT": "random-port",
"MONGODB_USER": "defaultadmin",
"MONGODB_PASS": "defaultpass",
"S3_HOST": "localhost",
"S3_PORT": "random-port",
"S3_CONSOLE_PORT": "s3-port+1",
"S3_USER": "defaultadmin",
"S3_PASS": "defaultpass",
"S3_BUCKET": "project-name-documents"
}
```
## Command Examples
```bash
gitzone services start # Start all services
gitzone services start mongo # Start only MongoDB
gitzone services stop # Stop all services
gitzone services status # Check service status
gitzone services config # Show configuration
gitzone services compass # Show MongoDB Compass connection string
gitzone services logs mongo 50 # Show last 50 lines of MongoDB logs
gitzone services remove # Remove containers (preserve data)
gitzone services clean # Remove containers and data
```
## Progress Notes
Implementation started: 2025-08-14
Implementation completed: 2025-08-14
## Summary
Successfully implemented the `gitzone services` command in TypeScript, providing a complete replacement for the `services.sh` shell script. The implementation includes:
1. **Complete Docker service management** for MongoDB and MinIO containers
2. **Smart configuration management** with automatic port assignment and conflict avoidance
3. **MongoDB Compass support** with network IP detection for remote connections
4. **Project isolation** using project-specific container names
5. **Data persistence** in `.nogit/` directories
6. **Interactive confirmations** for destructive operations
7. **Comprehensive command set** including start, stop, restart, status, config, compass, logs, remove, and clean commands
The module is fully integrated into the gitzone CLI and ready for testing.
Submodule
+1
Submodule test added at 0b89443584
+2 -2
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/cli', name: '@git.zone/cli',
version: '1.10.5', version: '2.16.0',
description: 'A CLI toolbelt to streamline local development cycles by using various gitzone utilities.' 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.'
} }
+17 -5
View File
@@ -16,6 +16,8 @@ export interface IGitzoneConfigData {
npmPackageName: string; npmPackageName: string;
license: string; license: string;
projectDomain: string; projectDomain: string;
assetbrokerUrl: string;
legalUrl: string;
}; };
copy: { [key: string]: string }; copy: { [key: string]: string };
npmciOptions: { npmciOptions: {
@@ -36,11 +38,21 @@ export class GitzoneConfig {
public data: IGitzoneConfigData; public data: IGitzoneConfigData;
public async readConfigFromCwd() { public async readConfigFromCwd() {
const npmextraInstance = new plugins.npmextra.Npmextra(paths.cwd); const smartconfigInstance = new plugins.smartconfig.Smartconfig(paths.cwd);
this.data = npmextraInstance.dataFor<IGitzoneConfigData>('gitzone', {}); this.data = smartconfigInstance.dataFor<IGitzoneConfigData>('@git.zone/cli', {});
this.data.npmciOptions = npmextraInstance.dataFor<IGitzoneConfigData['npmciOptions']>('npmci', {
npmAccessLevel: 'public', // Read szci config for backward compatibility
}); const szciConfig = smartconfigInstance.dataFor<any>('@ship.zone/szci', {});
// Prefer accessLevel from @git.zone/cli.release, fallback to @ship.zone/szci.npmAccessLevel
const accessLevel =
(this.data as any)?.release?.accessLevel ||
szciConfig?.npmAccessLevel ||
'public';
this.data.npmciOptions = {
npmAccessLevel: accessLevel,
};
} }
constructor() {} constructor() {}
+3 -2
View File
@@ -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;
+91 -25
View File
@@ -1,23 +1,29 @@
import * as plugins from './plugins.js'; import * as plugins from "./plugins.js";
import * as paths from './paths.js'; import * as paths from "./paths.js";
import { GitzoneConfig } from './classes.gitzoneconfig.js'; import { GitzoneConfig } from "./classes.gitzoneconfig.js";
import { getRawCliMode } from "./helpers.climode.js";
const gitzoneSmartcli = new plugins.smartcli.Smartcli(); const gitzoneSmartcli = new plugins.smartcli.Smartcli();
export let run = async () => { export let run = async () => {
const done = plugins.smartpromise.defer(); const done = plugins.smartpromise.defer();
const rawCliMode = await getRawCliMode();
// get packageInfo // get packageInfo
const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir); const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
// check for updates // check for updates
if (rawCliMode.checkUpdates) {
const smartupdateInstance = new plugins.smartupdate.SmartUpdate(); const smartupdateInstance = new plugins.smartupdate.SmartUpdate();
await smartupdateInstance.check( await smartupdateInstance.check(
'gitzone', "gitzone",
projectInfo.npm.version, projectInfo.npm.version,
'http://gitzone.gitlab.io/gitzone/changelog.html', "http://gitzone.gitlab.io/gitzone/changelog.html",
); );
console.log('---------------------------------------------'); }
if (rawCliMode.output === "human") {
console.log("---------------------------------------------");
}
gitzoneSmartcli.addVersion(projectInfo.npm.version); gitzoneSmartcli.addVersion(projectInfo.npm.version);
// ======> Standard task <====== // ======> Standard task <======
@@ -26,8 +32,13 @@ export let run = async () => {
* standard task * standard task
*/ */
gitzoneSmartcli.standardCommand().subscribe(async (argvArg) => { gitzoneSmartcli.standardCommand().subscribe(async (argvArg) => {
const modStandard = await import('./mod_standard/index.js'); const modStandard = await import("./mod_standard/index.js");
await modStandard.run(); await modStandard.run(argvArg);
});
gitzoneSmartcli.addCommand("help").subscribe(async (argvArg) => {
const modStandard = await import("./mod_standard/index.js");
await modStandard.run(argvArg);
}); });
// ======> Specific tasks <====== // ======> Specific tasks <======
@@ -35,66 +46,121 @@ export let run = async () => {
/** /**
* commit something * commit something
*/ */
gitzoneSmartcli.addCommand('commit').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("commit").subscribe(async (argvArg) => {
const modCommit = await import('./mod_commit/index.js'); const modCommit = await import("./mod_commit/index.js");
await modCommit.run(argvArg); await modCommit.run(argvArg);
}); });
/**
* create a release from pending changelog entries
*/
gitzoneSmartcli.addCommand("release").subscribe(async (argvArg) => {
const modRelease = await import("./mod_release/index.js");
await modRelease.run(argvArg);
});
/** /**
* deprecate a package on npm * deprecate a package on npm
*/ */
gitzoneSmartcli.addCommand('deprecate').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("deprecate").subscribe(async (argvArg) => {
const modDeprecate = await import('./mod_deprecate/index.js'); const modDeprecate = await import("./mod_deprecate/index.js");
await modDeprecate.run(); await modDeprecate.run();
}); });
/**
* docker
*/
gitzoneSmartcli.addCommand("docker").subscribe(async (argvArg) => {
const modDocker = await import("./mod_docker/index.js");
await modDocker.run(argvArg);
});
/** /**
* Update all files that comply with the gitzone standard * Update all files that comply with the gitzone standard
*/ */
gitzoneSmartcli.addCommand('format').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("format").subscribe(async (argvArg) => {
const config = GitzoneConfig.fromCwd(); const config = GitzoneConfig.fromCwd();
const modFormat = await import('./mod_format/index.js'); const modFormat = await import("./mod_format/index.js");
await modFormat.run();
// Handle format with options
// Default is dry-mode, use --write/-w to apply changes
await modFormat.run({
...argvArg,
write: argvArg.write || argvArg.w,
dryRun: argvArg["dry-run"],
yes: argvArg.yes,
planOnly: argvArg["plan-only"],
savePlan: argvArg["save-plan"],
fromPlan: argvArg["from-plan"],
detailed: argvArg.detailed,
interactive: argvArg.interactive !== false,
verbose: argvArg.verbose,
diff: argvArg.diff,
});
}); });
/** /**
* run meta commands * run meta commands
*/ */
gitzoneSmartcli.addCommand('meta').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("meta").subscribe(async (argvArg) => {
const config = GitzoneConfig.fromCwd(); const config = GitzoneConfig.fromCwd();
const modMeta = await import('./mod_meta/index.js'); const modMeta = await import("./mod_meta/index.js");
modMeta.run(argvArg); modMeta.run(argvArg);
}); });
/** /**
* open assets * open assets
*/ */
gitzoneSmartcli.addCommand('open').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("open").subscribe(async (argvArg) => {
const modOpen = await import('./mod_open/index.js'); const modOpen = await import("./mod_open/index.js");
modOpen.run(argvArg); modOpen.run(argvArg);
}); });
/** /**
* add a readme to a project * add a readme to a project
*/ */
gitzoneSmartcli.addCommand('template').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("template").subscribe(async (argvArg) => {
const modTemplate = await import('./mod_template/index.js'); const modTemplate = await import("./mod_template/index.js");
modTemplate.run(argvArg); modTemplate.run(argvArg);
}); });
/** /**
* start working on a project * start working on a project
*/ */
gitzoneSmartcli.addCommand('start').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("start").subscribe(async (argvArg) => {
const modTemplate = await import('./mod_start/index.js'); const modTemplate = await import("./mod_start/index.js");
modTemplate.run(argvArg); modTemplate.run(argvArg);
}); });
gitzoneSmartcli.addCommand('helpers').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("helpers").subscribe(async (argvArg) => {
const modHelpers = await import('./mod_helpers/index.js'); const modHelpers = await import("./mod_helpers/index.js");
modHelpers.run(argvArg); modHelpers.run(argvArg);
}); });
/**
* manage the global @git.zone toolchain
*/
gitzoneSmartcli.addCommand("tools").subscribe(async (argvArg) => {
const modTools = await import("./mod_tools/index.js");
await modTools.run(argvArg);
});
/**
* manage release configuration
*/
gitzoneSmartcli.addCommand("config").subscribe(async (argvArg) => {
const modConfig = await import("./mod_config/index.js");
await modConfig.run(argvArg);
});
/**
* manage development services (MongoDB, S3/MinIO)
*/
gitzoneSmartcli.addCommand("services").subscribe(async (argvArg) => {
const modServices = await import("./mod_services/index.js");
await modServices.run(argvArg);
});
// start parsing of the cli // start parsing of the cli
gitzoneSmartcli.startParse(); gitzoneSmartcli.startParse();
return await done.promise; return await done.promise;
+24 -1
View File
@@ -1,6 +1,29 @@
import { commitinfo } from '@push.rocks/commitinfo'; import { commitinfo } from '@push.rocks/commitinfo';
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
// Create logger instance
export const logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo); export const logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo);
logger.addLogDestination(new plugins.smartlogDestinationLocal.DestinationLocal()); // Add console destination
const consoleDestination =
new plugins.smartlogDestinationLocal.DestinationLocal();
logger.addLogDestination(consoleDestination);
// Verbose logging helper
let verboseMode = false;
export const setVerboseMode = (verbose: boolean): void => {
verboseMode = verbose;
logger.log('info', `Verbose mode ${verbose ? 'enabled' : 'disabled'}`);
};
export const isVerboseMode = (): boolean => {
return verboseMode;
};
// Custom log method with verbose support
export const logVerbose = (message: string): void => {
if (verboseMode) {
logger.log('info', `[VERBOSE] ${message}`);
}
};
+165
View File
@@ -0,0 +1,165 @@
import * as plugins from "./plugins.js";
export type TChangelogBucket =
| "Breaking Changes"
| "Features"
| "Fixes"
| "Documentation"
| "Maintenance";
export interface IChangelogEntry {
type: string;
scope: string;
message: string;
details?: string[];
}
export interface IPendingChangelog {
block: string;
isEmpty: boolean;
}
const bucketForCommitType = (commitType: string): TChangelogBucket => {
switch (commitType) {
case "BREAKING CHANGE":
return "Breaking Changes";
case "feat":
return "Features";
case "fix":
return "Fixes";
case "docs":
return "Documentation";
default:
return "Maintenance";
}
};
const readChangelog = async (filePath: string): Promise<string> => {
if (!(await plugins.smartfs.file(filePath).exists())) {
return "# Changelog\n\n";
}
return (await plugins.smartfs.file(filePath).encoding("utf8").read()) as string;
};
const writeChangelog = async (filePath: string, content: string): Promise<void> => {
await plugins.smartfs.file(filePath).encoding("utf8").write(content.endsWith("\n") ? content : `${content}\n`);
};
const findPendingSection = (
content: string,
sectionName: string,
): { start: number; bodyStart: number; end: number } | null => {
const headingRegex = new RegExp(`^##\\s+${sectionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
const match = headingRegex.exec(content);
if (!match || match.index === undefined) {
return null;
}
const bodyStart = match.index + match[0].length;
const rest = content.slice(bodyStart);
const nextHeadingMatch = /^##\s+/m.exec(rest);
const end = nextHeadingMatch ? bodyStart + nextHeadingMatch.index : content.length;
return { start: match.index, bodyStart, end };
};
export const ensurePendingSection = async (
filePath: string,
sectionName = "Pending",
): Promise<string> => {
let content = await readChangelog(filePath);
if (findPendingSection(content, sectionName)) {
return content;
}
const pendingSection = `## ${sectionName}\n\n`;
const titleMatch = /^#\s+.+$/m.exec(content);
if (titleMatch && titleMatch.index !== undefined) {
const insertAt = titleMatch.index + titleMatch[0].length;
content = `${content.slice(0, insertAt)}\n\n${pendingSection}${content.slice(insertAt).replace(/^\n+/, "")}`;
} else {
content = `# Changelog\n\n${pendingSection}${content}`;
}
await writeChangelog(filePath, content);
return content;
};
export const appendPendingChangelogEntry = async (
filePath: string,
sectionName: string,
entry: IChangelogEntry,
): Promise<void> => {
let content = await ensurePendingSection(filePath, sectionName);
const pendingSection = findPendingSection(content, sectionName)!;
let pendingBody = content.slice(pendingSection.bodyStart, pendingSection.end);
const bucket = bucketForCommitType(entry.type);
const bucketHeading = `### ${bucket}`;
const entryLines = [`- ${entry.message}${entry.scope ? ` (${entry.scope})` : ""}`];
for (const detail of entry.details || []) {
entryLines.push(` - ${detail}`);
}
const renderedEntry = entryLines.join("\n");
const bucketRegex = new RegExp(`^###\\s+${bucket.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
const bucketMatch = bucketRegex.exec(pendingBody);
if (!bucketMatch || bucketMatch.index === undefined) {
pendingBody = `${pendingBody.trimEnd()}\n\n${bucketHeading}\n\n${renderedEntry}\n`;
} else {
const bucketBodyStart = bucketMatch.index + bucketMatch[0].length;
const afterBucket = pendingBody.slice(bucketBodyStart);
const nextBucketMatch = /^###\s+/m.exec(afterBucket);
const insertAt = nextBucketMatch ? bucketBodyStart + nextBucketMatch.index : pendingBody.length;
const beforeInsert = pendingBody.slice(0, insertAt).trimEnd();
const afterInsert = pendingBody.slice(insertAt).replace(/^\n+/, "");
pendingBody = `${beforeInsert}\n${renderedEntry}\n\n${afterInsert}`;
}
content = `${content.slice(0, pendingSection.bodyStart)}\n${pendingBody.trim()}\n\n${content.slice(pendingSection.end).replace(/^\n+/, "")}`;
await writeChangelog(filePath, content);
};
export const readPendingChangelog = async (
filePath: string,
sectionName = "Pending",
): Promise<IPendingChangelog> => {
const content = await ensurePendingSection(filePath, sectionName);
const pendingSection = findPendingSection(content, sectionName)!;
const block = content.slice(pendingSection.bodyStart, pendingSection.end).trim();
return {
block,
isEmpty: block.length === 0,
};
};
export const inferVersionTypeFromPending = (pendingBlock: string): "patch" | "minor" | "major" => {
if (/^###\s+Breaking Changes\s*$/m.test(pendingBlock)) {
return "major";
}
if (/^###\s+Features\s*$/m.test(pendingBlock)) {
return "minor";
}
return "patch";
};
export const movePendingToVersion = async (
filePath: string,
sectionName: string,
versionHeading: string,
version: string,
dateString: string,
): Promise<void> => {
let content = await ensurePendingSection(filePath, sectionName);
const pendingSection = findPendingSection(content, sectionName)!;
const pendingBlock = content.slice(pendingSection.bodyStart, pendingSection.end).trim();
if (!pendingBlock) {
throw new Error("No pending changelog entries. Nothing to release.");
}
const renderedHeading = versionHeading
.replaceAll("{{version}}", version)
.replaceAll("{{date}}", dateString);
const nextContent = content.slice(pendingSection.end).replace(/^\n+/, "");
content = `${content.slice(0, pendingSection.bodyStart)}\n\n${renderedHeading}\n\n${pendingBlock}\n\n${nextContent}`;
await writeChangelog(filePath, content);
};
+212
View File
@@ -0,0 +1,212 @@
import { getCliConfigValue } from "./helpers.smartconfig.js";
export type TCliOutputMode = "human" | "plain" | "json";
export interface ICliMode {
output: TCliOutputMode;
interactive: boolean;
json: boolean;
plain: boolean;
quiet: boolean;
yes: boolean;
help: boolean;
agent: boolean;
checkUpdates: boolean;
isTty: boolean;
command?: string;
}
interface ICliConfigSettings {
interactive?: boolean;
output?: TCliOutputMode;
checkUpdates?: boolean;
}
type TArgSource = Record<string, any> & { _?: string[] };
const camelCase = (value: string): string => {
return value.replace(/-([a-z])/g, (_match, group: string) =>
group.toUpperCase(),
);
};
const getArgValue = (argvArg: TArgSource, key: string): any => {
const keyVariants = [key, camelCase(key), key.replace(/-/g, "")];
for (const keyVariant of keyVariants) {
if (argvArg[keyVariant] !== undefined) {
return argvArg[keyVariant];
}
}
return undefined;
};
const parseRawArgv = (argv: string[]): TArgSource => {
const parsedArgv: TArgSource = { _: [] };
for (let i = 0; i < argv.length; i++) {
const currentArg = argv[i];
if (currentArg.startsWith("--no-")) {
const key = currentArg.slice(5);
parsedArgv[key] = false;
parsedArgv[camelCase(key)] = false;
continue;
}
if (currentArg.startsWith("--")) {
const withoutPrefix = currentArg.slice(2);
const [rawKey, inlineValue] = withoutPrefix.split("=", 2);
if (inlineValue !== undefined) {
parsedArgv[rawKey] = inlineValue;
parsedArgv[camelCase(rawKey)] = inlineValue;
continue;
}
const nextArg = argv[i + 1];
if (nextArg && !nextArg.startsWith("-")) {
parsedArgv[rawKey] = nextArg;
parsedArgv[camelCase(rawKey)] = nextArg;
i++;
} else {
parsedArgv[rawKey] = true;
parsedArgv[camelCase(rawKey)] = true;
}
continue;
}
if (currentArg.startsWith("-") && currentArg.length > 1) {
for (const shortFlag of currentArg.slice(1).split("")) {
parsedArgv[shortFlag] = true;
}
continue;
}
parsedArgv._ = parsedArgv._ || [];
parsedArgv._.push(currentArg);
}
return parsedArgv;
};
const normalizeOutputMode = (value: unknown): TCliOutputMode | undefined => {
if (value === "human" || value === "plain" || value === "json") {
return value;
}
return undefined;
};
const resolveCliMode = (
argvArg: TArgSource,
cliConfig: ICliConfigSettings,
): ICliMode => {
const isTty = Boolean(process.stdout?.isTTY && process.stdin?.isTTY);
const agentMode = Boolean(getArgValue(argvArg, "agent"));
const outputOverride = normalizeOutputMode(getArgValue(argvArg, "output"));
let output: TCliOutputMode =
normalizeOutputMode(cliConfig.output) || (isTty ? "human" : "plain");
if (agentMode || getArgValue(argvArg, "json")) {
output = "json";
} else if (getArgValue(argvArg, "plain")) {
output = "plain";
} else if (outputOverride) {
output = outputOverride;
}
const interactiveSetting = getArgValue(argvArg, "interactive");
let interactive = cliConfig.interactive ?? isTty;
if (interactiveSetting === true) {
interactive = true;
} else if (interactiveSetting === false) {
interactive = false;
}
if (!isTty || output !== "human" || agentMode) {
interactive = false;
}
const checkUpdatesSetting = getArgValue(argvArg, "check-updates");
let checkUpdates = cliConfig.checkUpdates ?? output === "human";
if (checkUpdatesSetting === true) {
checkUpdates = true;
} else if (checkUpdatesSetting === false) {
checkUpdates = false;
}
if (output !== "human" || agentMode) {
checkUpdates = false;
}
return {
output,
interactive,
json: output === "json",
plain: output === "plain",
quiet: Boolean(
getArgValue(argvArg, "quiet") ||
getArgValue(argvArg, "q") ||
output === "json",
),
yes: Boolean(getArgValue(argvArg, "yes") || getArgValue(argvArg, "y")),
help: Boolean(
getArgValue(argvArg, "help") ||
getArgValue(argvArg, "h") ||
argvArg._?.[0] === "help",
),
agent: agentMode,
checkUpdates,
isTty,
command: argvArg._?.[0],
};
};
const getCliModeConfig = async (): Promise<ICliConfigSettings> => {
return await getCliConfigValue<ICliConfigSettings>("cli", {});
};
export const getCliMode = async (
argvArg: TArgSource = {},
): Promise<ICliMode> => {
const cliConfig = await getCliModeConfig();
return resolveCliMode(argvArg, cliConfig);
};
export const getRawCliMode = async (): Promise<ICliMode> => {
const cliConfig = await getCliModeConfig();
const rawArgv = parseRawArgv(process.argv.slice(2));
return resolveCliMode(rawArgv, cliConfig);
};
export const printJson = (data: unknown): void => {
console.log(JSON.stringify(data, null, 2));
};
export const runWithSuppressedOutput = async <T>(
fn: () => Promise<T>,
): Promise<T> => {
const originalConsole = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
};
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
const originalStderrWrite = process.stderr.write.bind(process.stderr);
const noop = () => undefined;
console.log = noop;
console.info = noop;
console.warn = noop;
console.error = noop;
process.stdout.write = (() => true) as typeof process.stdout.write;
process.stderr.write = (() => true) as typeof process.stderr.write;
try {
return await fn();
} finally {
console.log = originalConsole.log;
console.info = originalConsole.info;
console.warn = originalConsole.warn;
console.error = originalConsole.error;
process.stdout.write = originalStdoutWrite;
process.stderr.write = originalStderrWrite;
}
};
+192
View File
@@ -0,0 +1,192 @@
import * as plugins from "./plugins.js";
import { rename, writeFile } from "fs/promises";
export const CLI_NAMESPACE = "@git.zone/cli";
const isPlainObject = (value: unknown): value is Record<string, any> => {
return typeof value === "object" && value !== null && !Array.isArray(value);
};
export const getSmartconfigPath = (cwd: string = process.cwd()): string => {
return plugins.path.join(cwd, ".smartconfig.json");
};
export const readSmartconfigFile = async (
cwd: string = process.cwd(),
): Promise<Record<string, any>> => {
const smartconfigPath = getSmartconfigPath(cwd);
if (!(await plugins.smartfs.file(smartconfigPath).exists())) {
return {};
}
const content = (await plugins.smartfs
.file(smartconfigPath)
.encoding("utf8")
.read()) as string;
if (content.trim() === "") {
return {};
}
return JSON.parse(content);
};
export const writeSmartconfigFile = async (
data: Record<string, any>,
cwd: string = process.cwd(),
): Promise<void> => {
const smartconfigPath = getSmartconfigPath(cwd);
const tempPath = `${smartconfigPath}.tmp-${Date.now()}`;
const content = JSON.stringify(data, null, 2);
await writeFile(tempPath, content, "utf8");
await rename(tempPath, smartconfigPath);
};
export const normalizeCliConfigPath = (configPath: string): string => {
const trimmedPath = configPath.trim();
if (!trimmedPath || trimmedPath === CLI_NAMESPACE) {
return "";
}
if (trimmedPath.startsWith(`${CLI_NAMESPACE}.`)) {
return trimmedPath.slice(`${CLI_NAMESPACE}.`.length);
}
return trimmedPath;
};
export const getCliConfigPathSegments = (configPath: string): string[] => {
const normalizedPath = normalizeCliConfigPath(configPath);
if (!normalizedPath) {
return [];
}
return normalizedPath
.split(".")
.map((segment) => segment.trim())
.filter(Boolean);
};
export const getCliNamespaceConfig = (
smartconfigData: Record<string, any>,
): Record<string, any> => {
const cliConfig = smartconfigData[CLI_NAMESPACE];
if (isPlainObject(cliConfig)) {
return cliConfig;
}
return {};
};
export const getCliConfigValueFromData = (
smartconfigData: Record<string, any>,
configPath: string,
): any => {
const segments = getCliConfigPathSegments(configPath);
let currentValue: any = getCliNamespaceConfig(smartconfigData);
for (const segment of segments) {
if (!isPlainObject(currentValue) && !Array.isArray(currentValue)) {
return undefined;
}
currentValue = (currentValue as any)?.[segment];
}
return currentValue;
};
export const getCliConfigValue = async <T>(
configPath: string,
defaultValue: T,
cwd: string = process.cwd(),
): Promise<T> => {
const smartconfigData = await readSmartconfigFile(cwd);
const configValue = getCliConfigValueFromData(smartconfigData, configPath);
if (configValue === undefined) {
return defaultValue;
}
if (isPlainObject(defaultValue) && isPlainObject(configValue)) {
return {
...defaultValue,
...configValue,
} as T;
}
return configValue as T;
};
export const setCliConfigValueInData = (
smartconfigData: Record<string, any>,
configPath: string,
value: any,
): Record<string, any> => {
const segments = getCliConfigPathSegments(configPath);
if (!isPlainObject(smartconfigData[CLI_NAMESPACE])) {
smartconfigData[CLI_NAMESPACE] = {};
}
if (segments.length === 0) {
smartconfigData[CLI_NAMESPACE] = value;
return smartconfigData;
}
let currentValue = smartconfigData[CLI_NAMESPACE];
for (const segment of segments.slice(0, -1)) {
if (!isPlainObject(currentValue[segment])) {
currentValue[segment] = {};
}
currentValue = currentValue[segment];
}
currentValue[segments[segments.length - 1]] = value;
return smartconfigData;
};
export const unsetCliConfigValueInData = (
smartconfigData: Record<string, any>,
configPath: string,
): boolean => {
const segments = getCliConfigPathSegments(configPath);
if (segments.length === 0) {
if (smartconfigData[CLI_NAMESPACE] !== undefined) {
delete smartconfigData[CLI_NAMESPACE];
return true;
}
return false;
}
const parentSegments = segments.slice(0, -1);
let currentValue: any = getCliNamespaceConfig(smartconfigData);
const objectPath: Array<Record<string, any>> = [currentValue];
for (const segment of parentSegments) {
if (!isPlainObject(currentValue[segment])) {
return false;
}
currentValue = currentValue[segment];
objectPath.push(currentValue);
}
const lastSegment = segments[segments.length - 1];
if (!(lastSegment in currentValue)) {
return false;
}
delete currentValue[lastSegment];
for (let i = objectPath.length - 1; i >= 1; i--) {
if (Object.keys(objectPath[i]).length > 0) {
break;
}
const parentObject = objectPath[i - 1];
const parentKey = parentSegments[i - 1];
delete parentObject[parentKey];
}
if (Object.keys(getCliNamespaceConfig(smartconfigData)).length === 0) {
delete smartconfigData[CLI_NAMESPACE];
}
return true;
};
+192
View File
@@ -0,0 +1,192 @@
export const CURRENT_GITZONE_CLI_SCHEMA_VERSION = 2;
export interface ISmartconfigMigrationResult {
migrated: boolean;
fromVersion: number;
toVersion: number;
}
const CLI_NAMESPACE = "@git.zone/cli";
const isPlainObject = (value: unknown): value is Record<string, any> => {
return typeof value === "object" && value !== null && !Array.isArray(value);
};
const ensureObject = (parent: Record<string, any>, key: string): Record<string, any> => {
if (!isPlainObject(parent[key])) {
parent[key] = {};
}
return parent[key];
};
const migrateNamespaceKeys = (smartconfigJson: Record<string, any>): boolean => {
let migrated = false;
const migrations = [
{ oldKey: "gitzone", newKey: CLI_NAMESPACE },
{ oldKey: "tsdoc", newKey: "@git.zone/tsdoc" },
{ oldKey: "npmdocker", newKey: "@git.zone/tsdocker" },
{ oldKey: "npmci", newKey: "@ship.zone/szci" },
{ oldKey: "szci", newKey: "@ship.zone/szci" },
];
for (const { oldKey, newKey } of migrations) {
if (!isPlainObject(smartconfigJson[oldKey])) {
continue;
}
if (!isPlainObject(smartconfigJson[newKey])) {
smartconfigJson[newKey] = smartconfigJson[oldKey];
} else {
smartconfigJson[newKey] = {
...smartconfigJson[oldKey],
...smartconfigJson[newKey],
};
}
delete smartconfigJson[oldKey];
migrated = true;
}
return migrated;
};
const migrateToV2 = (smartconfigJson: Record<string, any>): boolean => {
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
const releaseConfig = ensureObject(cliConfig, "release");
let migrated = false;
const targets = ensureObject(releaseConfig, "targets");
const shipzoneConfig = smartconfigJson["@ship.zone/szci"];
if (isPlainObject(releaseConfig.git) && !isPlainObject(targets.git)) {
targets.git = releaseConfig.git;
delete releaseConfig.git;
migrated = true;
}
if (isPlainObject(releaseConfig.npm) && !isPlainObject(targets.npm)) {
targets.npm = releaseConfig.npm;
delete releaseConfig.npm;
migrated = true;
}
if (isPlainObject(releaseConfig.docker) && !isPlainObject(targets.docker)) {
targets.docker = releaseConfig.docker;
delete releaseConfig.docker;
migrated = true;
}
if (Array.isArray(releaseConfig.registries)) {
const npmTarget = ensureObject(targets, "npm");
if (!Array.isArray(npmTarget.registries)) {
npmTarget.registries = releaseConfig.registries;
}
delete releaseConfig.registries;
migrated = true;
}
if (releaseConfig.accessLevel) {
const npmTarget = ensureObject(targets, "npm");
if (!npmTarget.accessLevel) {
npmTarget.accessLevel = releaseConfig.accessLevel;
}
delete releaseConfig.accessLevel;
migrated = true;
}
if (isPlainObject(shipzoneConfig)) {
if (shipzoneConfig.npmAccessLevel) {
const npmTarget = ensureObject(targets, "npm");
if (!npmTarget.accessLevel) {
npmTarget.accessLevel = shipzoneConfig.npmAccessLevel;
}
delete shipzoneConfig.npmAccessLevel;
migrated = true;
}
if (shipzoneConfig.npmRegistryUrl) {
const npmTarget = ensureObject(targets, "npm");
const registry = normalizeRegistryUrl(shipzoneConfig.npmRegistryUrl);
const registries = Array.isArray(npmTarget.registries) ? npmTarget.registries : [];
if (!registries.includes(registry)) {
registries.push(registry);
}
npmTarget.registries = registries;
delete shipzoneConfig.npmRegistryUrl;
migrated = true;
}
}
if (Array.isArray(releaseConfig.steps)) {
const steps = releaseConfig.steps as string[];
const preflight = ensureObject(releaseConfig, "preflight");
if (steps.includes("test") && preflight.test === undefined) {
preflight.test = true;
}
if (steps.includes("build") && preflight.build === undefined) {
preflight.build = true;
}
if (steps.includes("push")) {
const gitTarget = ensureObject(targets, "git");
if (gitTarget.enabled === undefined) {
gitTarget.enabled = true;
}
}
if (steps.includes("publishNpm")) {
const npmTarget = ensureObject(targets, "npm");
if (npmTarget.enabled === undefined) {
npmTarget.enabled = true;
}
}
if (steps.includes("publishDocker")) {
const dockerTarget = ensureObject(targets, "docker");
if (dockerTarget.enabled === undefined) {
dockerTarget.enabled = true;
}
}
delete releaseConfig.steps;
migrated = true;
}
if (releaseConfig.changelog) {
delete releaseConfig.changelog;
migrated = true;
}
cliConfig.schemaVersion = 2;
return migrated || true;
};
const normalizeRegistryUrl = (url: string): string => {
let normalizedUrl = url.trim();
if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
normalizedUrl = `https://${normalizedUrl}`;
}
return normalizedUrl.endsWith("/") ? normalizedUrl.slice(0, -1) : normalizedUrl;
};
export const migrateSmartconfigData = (
smartconfigJson: Record<string, any>,
targetVersion = CURRENT_GITZONE_CLI_SCHEMA_VERSION,
): ISmartconfigMigrationResult => {
let migrated = false;
migrated = migrateNamespaceKeys(smartconfigJson) || migrated;
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
const fromVersion = typeof cliConfig.schemaVersion === "number" ? cliConfig.schemaVersion : 1;
let currentVersion = fromVersion;
if (currentVersion < 2 && targetVersion >= 2) {
migrated = migrateToV2(smartconfigJson) || migrated;
currentVersion = 2;
}
if (targetVersion === CURRENT_GITZONE_CLI_SCHEMA_VERSION && cliConfig.schemaVersion !== targetVersion) {
cliConfig.schemaVersion = targetVersion;
migrated = true;
}
return {
migrated,
fromVersion,
toVersion: Math.min(targetVersion, CURRENT_GITZONE_CLI_SCHEMA_VERSION),
};
};
+387
View File
@@ -0,0 +1,387 @@
import { getCliConfigValue } from "./helpers.smartconfig.js";
export type TConfirmationMode = "prompt" | "auto" | "plan";
export type TCommitStep =
| "format"
| "analyze"
| "test"
| "build"
| "changelog"
| "commit"
| "push";
export type TReleaseTarget = "git" | "npm" | "docker";
export interface ICommitWorkflowConfig {
confirmation?: TConfirmationMode;
staging?: "all";
steps?: TCommitStep[];
alwaysTest?: boolean;
alwaysBuild?: boolean;
analyze?: {
provider?: "ai";
requireConfirmationFor?: string[];
};
test?: {
command?: string;
};
build?: {
command?: string;
verifyCleanTree?: boolean;
};
push?: {
remote?: string;
followTags?: boolean;
};
}
export interface IReleaseGitTargetConfig {
enabled?: boolean;
remote?: string;
pushBranch?: boolean;
pushTags?: boolean;
}
export interface IReleaseNpmTargetConfig {
enabled?: boolean;
registries?: string[];
accessLevel?: "public" | "private";
alreadyPublished?: "success" | "error";
}
export interface IReleaseDockerTargetConfig {
enabled?: boolean;
images?: string[];
}
export interface IReleaseWorkflowConfig {
confirmation?: TConfirmationMode;
version?: {
strategy?: "semver";
source?: "pendingChangelog" | "manual";
};
preflight?: {
requireCleanTree?: boolean;
test?: boolean;
build?: boolean;
testCommand?: string;
buildCommand?: string;
};
targets?: {
git?: IReleaseGitTargetConfig;
npm?: IReleaseNpmTargetConfig;
docker?: IReleaseDockerTargetConfig;
};
}
export interface IResolvedCommitWorkflow {
confirmation: TConfirmationMode;
steps: TCommitStep[];
staging: "all";
testCommand: string;
buildCommand: string;
changelogFile: "changelog.md";
changelogSection: "Pending";
pushRemote: string;
pushFollowTags: boolean;
releaseFlagRequested: boolean;
}
export interface IResolvedReleaseWorkflow {
confirmation: TConfirmationMode;
plan: string[];
targets: TReleaseTarget[];
requireCleanTree: boolean;
runTests: boolean;
runBuild: boolean;
testCommand: string;
buildCommand: string;
changelogFile: "changelog.md";
changelogPendingSection: "Pending";
changelogVersionHeading: "## {{date}} - {{version}}";
gitEnabled: boolean;
gitRemote: string;
pushBranch: boolean;
pushTags: boolean;
npmEnabled: boolean;
npmRegistries: string[];
npmAccessLevel: "public" | "private";
npmAlreadyPublished: "success" | "error";
dockerEnabled: boolean;
dockerImages: string[];
}
interface ICliWorkflowConfig {
commit?: ICommitWorkflowConfig;
release?: IReleaseWorkflowConfig;
}
const commitFlagToStep: Record<string, TCommitStep | undefined> = {
f: "format",
t: "test",
b: "build",
p: "push",
};
const unique = <T>(items: T[]): T[] => {
const result: T[] = [];
for (const item of items) {
if (!result.includes(item)) {
result.push(item);
}
}
return result;
};
const normalizeConfirmation = (
value: unknown,
fallback: TConfirmationMode,
): TConfirmationMode => {
if (value === "prompt" || value === "auto" || value === "plan") {
return value;
}
return fallback;
};
const normalizeRegistryUrl = (url: string): string => {
let normalizedUrl = url.trim();
if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
normalizedUrl = `https://${normalizedUrl}`;
}
return normalizedUrl.endsWith("/") ? normalizedUrl.slice(0, -1) : normalizedUrl;
};
const isDisabled = (argvArg: any, ...keys: string[]): boolean => {
return keys.some((key) => argvArg[key] === false || argvArg[`no-${key}`] || argvArg[`no${key[0].toUpperCase()}${key.slice(1)}`]);
};
const readCliWorkflowConfig = async (): Promise<ICliWorkflowConfig> => {
return await getCliConfigValue<ICliWorkflowConfig>("", {});
};
const getOrderedArgsAfterCommand = (commandName: string): string[] => {
const rawArgs = process.argv.slice(2);
const commandIndex = rawArgs.indexOf(commandName);
if (commandIndex === -1) {
return rawArgs;
}
return rawArgs.slice(commandIndex + 1);
};
const getOrderedShortFlags = (commandName: string): string[] => {
const orderedFlags: string[] = [];
for (const arg of getOrderedArgsAfterCommand(commandName)) {
if (arg === "--") {
break;
}
if (arg.startsWith("--")) {
continue;
}
if (arg.startsWith("-") && arg.length > 1) {
orderedFlags.push(...arg.slice(1).split(""));
}
}
return orderedFlags;
};
const hasExplicitCommitWorkflowFlags = (argvArg: any): boolean => {
return Boolean(
argvArg.f ||
argvArg.format ||
argvArg.t ||
argvArg.test ||
argvArg.b ||
argvArg.build ||
argvArg.p ||
argvArg.push,
);
};
const normalizeCommitSteps = (rawSteps: TCommitStep[]): TCommitStep[] => {
const steps = unique(rawSteps.filter(Boolean));
const pushRequested = steps.includes("push");
const prePushSteps = steps.filter((step) => step !== "push");
if (!prePushSteps.includes("analyze")) {
prePushSteps.unshift("analyze");
}
if (!prePushSteps.includes("changelog")) {
const commitIndex = prePushSteps.indexOf("commit");
if (commitIndex === -1) {
prePushSteps.push("changelog");
} else {
prePushSteps.splice(commitIndex, 0, "changelog");
}
}
if (!prePushSteps.includes("commit")) {
prePushSteps.push("commit");
}
const analyzeIndex = prePushSteps.indexOf("analyze");
const commitIndex = prePushSteps.indexOf("commit");
if (analyzeIndex > commitIndex) {
throw new Error("Commit workflow requires analyze before commit.");
}
const changelogIndex = prePushSteps.indexOf("changelog");
if (changelogIndex === -1 || changelogIndex > commitIndex) {
throw new Error("Commit workflow requires changelog before commit.");
}
return pushRequested ? [...prePushSteps, "push"] : prePushSteps;
};
const getTargetOverride = (argvArg: any): TReleaseTarget[] | undefined => {
const validTargets: TReleaseTarget[] = ["git", "npm", "docker"];
const rawTargets = argvArg.target || argvArg.targets;
if (typeof rawTargets === "string") {
return rawTargets
.split(",")
.map((target) => target.trim())
.filter((target): target is TReleaseTarget => validTargets.includes(target as TReleaseTarget));
}
const targets: TReleaseTarget[] = [];
if (argvArg.git || argvArg.p || argvArg.push) targets.push("git");
if (argvArg.npm) targets.push("npm");
if (argvArg.docker) targets.push("docker");
return targets.length > 0 ? targets : undefined;
};
const buildReleasePlan = (options: {
requireCleanTree: boolean;
runTests: boolean;
runBuild: boolean;
targets: TReleaseTarget[];
}): string[] => {
const plan: string[] = [];
if (options.requireCleanTree) plan.push("preflight.cleanTree");
if (options.runTests) plan.push("preflight.test");
plan.push("core.version", "core.changelog", "core.commit", "core.tag");
if (options.runBuild) plan.push("core.build");
for (const target of options.targets) {
plan.push(`target.${target}`);
}
return plan;
};
export const resolveCommitWorkflow = async (argvArg: any): Promise<IResolvedCommitWorkflow> => {
const cliConfig = await readCliWorkflowConfig();
const commitConfig = cliConfig.commit || {};
const releaseFlagRequested = Boolean(argvArg.r || argvArg.release);
let confirmation = normalizeConfirmation(commitConfig.confirmation, "prompt");
if (argvArg.plan) {
confirmation = "plan";
} else if (argvArg.y || argvArg.yes) {
confirmation = "auto";
}
let rawSteps: TCommitStep[];
if (hasExplicitCommitWorkflowFlags(argvArg)) {
const orderedFlags = getOrderedShortFlags("commit");
rawSteps = ["analyze"];
for (const shortFlag of orderedFlags) {
const step = commitFlagToStep[shortFlag];
if (step) {
rawSteps.push(step);
}
}
if (argvArg.format && !rawSteps.includes("format")) rawSteps.push("format");
if (argvArg.test && !rawSteps.includes("test")) rawSteps.push("test");
if (argvArg.build && !rawSteps.includes("build")) rawSteps.push("build");
if (argvArg.push && !rawSteps.includes("push")) rawSteps.push("push");
rawSteps.push("changelog");
rawSteps.push("commit");
} else if (Array.isArray(commitConfig.steps) && commitConfig.steps.length > 0) {
rawSteps = commitConfig.steps;
} else {
rawSteps = ["analyze"];
if (commitConfig.alwaysTest) rawSteps.push("test");
if (commitConfig.alwaysBuild) rawSteps.push("build");
rawSteps.push("changelog");
rawSteps.push("commit");
}
return {
confirmation,
steps: normalizeCommitSteps(rawSteps),
staging: commitConfig.staging || "all",
testCommand: commitConfig.test?.command || "pnpm test",
buildCommand: commitConfig.build?.command || "pnpm build",
changelogFile: "changelog.md",
changelogSection: "Pending",
pushRemote: commitConfig.push?.remote || "origin",
pushFollowTags: commitConfig.push?.followTags || false,
releaseFlagRequested,
};
};
export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedReleaseWorkflow> => {
const cliConfig = await readCliWorkflowConfig();
const releaseConfig = cliConfig.release || {};
const targetConfig = releaseConfig.targets || {};
const gitConfig = targetConfig.git || {};
const npmConfig = targetConfig.npm || {};
const dockerConfig = targetConfig.docker || {};
const npmRegistries = (npmConfig.registries || []).map(normalizeRegistryUrl);
const npmEnabled = npmConfig.enabled ?? npmRegistries.length > 0;
const gitEnabled = gitConfig.enabled ?? true;
const dockerEnabled = dockerConfig.enabled ?? false;
let confirmation = normalizeConfirmation(releaseConfig.confirmation, "prompt");
if (argvArg.plan) {
confirmation = "plan";
} else if (argvArg.y || argvArg.yes) {
confirmation = "auto";
}
let requireCleanTree = releaseConfig.preflight?.requireCleanTree ?? true;
let runTests = releaseConfig.preflight?.test ?? false;
let runBuild = releaseConfig.preflight?.build ?? true;
if (argvArg.t || argvArg.test) runTests = true;
if (argvArg.b || argvArg.build) runBuild = true;
if (isDisabled(argvArg, "test")) runTests = false;
if (isDisabled(argvArg, "build")) runBuild = false;
if (isDisabled(argvArg, "preflight")) requireCleanTree = false;
const configuredTargets: TReleaseTarget[] = [];
if (gitEnabled) configuredTargets.push("git");
if (npmEnabled) configuredTargets.push("npm");
if (dockerEnabled) configuredTargets.push("docker");
let targets = getTargetOverride(argvArg) || configuredTargets;
if (isDisabled(argvArg, "git", "push")) {
targets = targets.filter((target) => target !== "git");
}
if (isDisabled(argvArg, "publish")) {
targets = targets.filter((target) => target === "git");
}
targets = unique(targets);
return {
confirmation,
plan: buildReleasePlan({ requireCleanTree, runTests, runBuild, targets }),
targets,
requireCleanTree,
runTests,
runBuild,
testCommand: releaseConfig.preflight?.testCommand || "pnpm test",
buildCommand: releaseConfig.preflight?.buildCommand || "pnpm build",
changelogFile: "changelog.md",
changelogPendingSection: "Pending",
changelogVersionHeading: "## {{date}} - {{version}}",
gitEnabled,
gitRemote: gitConfig.remote || "origin",
pushBranch: gitConfig.pushBranch ?? true,
pushTags: gitConfig.pushTags ?? true,
npmEnabled,
npmRegistries,
npmAccessLevel: npmConfig.accessLevel || "public",
npmAlreadyPublished: npmConfig.alreadyPublished || "success",
dockerEnabled,
dockerImages: dockerConfig.images || [],
};
};
+350 -84
View File
@@ -1,105 +1,371 @@
// this file contains code to create commits in a consistent way // this file contains code to create commits in a consistent way
import * as plugins from './mod.plugins.js'; import * as plugins from "./mod.plugins.js";
import * as paths from '../paths.js'; import * as paths from "../paths.js";
import { logger } from '../gitzone.logging.js'; import { logger } from "../gitzone.logging.js";
import * as ui from "./mod.ui.js";
import type { ICliMode } from "../helpers.climode.js";
import { getCliMode, printJson, runWithSuppressedOutput } from "../helpers.climode.js";
import { appendPendingChangelogEntry } from "../helpers.changelog.js";
import { resolveCommitWorkflow, type IResolvedCommitWorkflow } from "../helpers.workflow.js";
export const run = async (argvArg: any) => { export const run = async (argvArg: any) => {
if (argvArg.format) { const mode = await getCliMode(argvArg);
const formatMod = await import('../mod_format/index.js'); const subcommand = argvArg._?.[1];
await formatMod.run();
if (mode.help || subcommand === "help") {
showHelp(mode);
return;
} }
if (subcommand === "recommend") {
logger.log('info', `gathering facts...`); await handleRecommend(mode);
const aidoc = new plugins.tsdoc.AiDoc(); return;
await aidoc.start(); }
const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd); if (mode.json) {
printJson({
logger.log('info', `--------- ok: false,
Next recommended commit would be: error:
=========== "JSON output is only supported for the read-only recommendation flow. Use `gitzone commit recommend --json`.",
-> ${nextCommitObject.recommendedNextVersion}: });
-> ${nextCommitObject.recommendedNextVersionLevel}(${nextCommitObject.recommendedNextVersionScope}): ${nextCommitObject.recommendedNextVersionMessage} return;
=========== }
`);
const commitInteract = new plugins.smartinteract.SmartInteract(); const workflow = await resolveCommitWorkflow(argvArg);
commitInteract.addQuestions([ if (workflow.releaseFlagRequested) {
{ logger.log(
type: 'list', "warn",
name: `commitType`, "`gitzone commit -r` is deprecated and no longer releases. Use `gitzone release` after committing.",
message: `Choose TYPE of the commit:`, );
choices: [`fix`, `feat`, `BREAKING CHANGE`], }
default: nextCommitObject.recommendedNextVersionLevel,
}, printCommitExecutionPlan(workflow);
{ if (workflow.confirmation === "plan") {
type: 'input', return;
name: `commitScope`,
message: `What is the SCOPE of the commit:`,
default: nextCommitObject.recommendedNextVersionScope,
},
{
type: `input`,
name: `commitDescription`,
message: `What is the DESCRIPTION of the commit?`,
default: nextCommitObject.recommendedNextVersionMessage,
},
{
type: 'confirm',
name: `pushToOrigin`,
message: `Do you want to push this version now?`,
default: true,
},
]);
const answerBucket = await commitInteract.runQueue();
const commitString = createCommitStringFromAnswerBucket(answerBucket);
const commitVersionType = (() => {
switch (answerBucket.getAnswerFor('commitType')) {
case 'fix':
return 'patch';
case 'feat':
return 'minor';
case 'BREAKING CHANGE':
return 'major';
} }
})();
logger.log('info', `OK! Creating commit with message '${commitString}'`);
const smartshellInstance = new plugins.smartshell.Smartshell({ const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash', executor: "bash",
sourceFilePaths: [], sourceFilePaths: [],
}); });
logger.log('info', `Baking commitinfo into code ...`); let nextCommitObject: any;
const commitInfo = new plugins.commitinfo.CommitInfo(paths.cwd, commitVersionType); let answerBucket: plugins.smartinteract.AnswerBucket | undefined;
await commitInfo.writeIntoPotentialDirs();
logger.log('info', `Writing changelog.md ...`); for (const step of workflow.steps) {
let changelog = nextCommitObject.changelog; switch (step) {
changelog = changelog.replaceAll('{{nextVersion}}', (await commitInfo.getNextPlannedVersion()).versionString); case "format":
changelog = changelog.replaceAll('{{nextVersionScope}}', `${await answerBucket.getAnswerFor('commitType')}(${await answerBucket.getAnswerFor('commitScope')})`); await runFormatStep();
changelog = changelog.replaceAll('{{nextVersionMessage}}', nextCommitObject.recommendedNextVersionMessage); break;
if (nextCommitObject.recommendedNextVersionDetails?.length > 0) { case "test":
changelog = changelog.replaceAll('{{nextVersionDetails}}', '- ' + nextCommitObject.recommendedNextVersionDetails.join('\n- ')); await runCommandStep(smartshellInstance, "Running tests", workflow.testCommand);
} else { break;
changelog = changelog.replaceAll('\n{{nextVersionDetails}}', ''); case "build":
await runCommandStep(smartshellInstance, "Running build", workflow.buildCommand);
break;
case "analyze":
nextCommitObject = await runAnalyzeStep();
answerBucket = await buildAnswerBucket(nextCommitObject, workflow, mode, argvArg);
break;
case "changelog":
assertAnalysisComplete(answerBucket, nextCommitObject);
await runChangelogStep(workflow, answerBucket!, nextCommitObject);
break;
case "commit":
assertAnalysisComplete(answerBucket, nextCommitObject);
await runCommitStep(smartshellInstance, answerBucket!);
break;
case "push":
await runPushStep(smartshellInstance, workflow);
break;
}
} }
await plugins.smartfile.memory.toFs(changelog, plugins.path.join(paths.cwd, `changelog.md`)); const commitShaResult = await smartshellInstance.exec("git rev-parse --short HEAD");
const currentBranch = await detectCurrentBranch(smartshellInstance);
ui.printSummary({
projectType: "source",
branch: currentBranch,
commitType: answerBucket!.getAnswerFor("commitType"),
commitScope: answerBucket!.getAnswerFor("commitScope"),
commitMessage: answerBucket!.getAnswerFor("commitDescription"),
commitSha: commitShaResult.stdout.trim(),
pushed: workflow.steps.includes("push"),
});
};
logger.log('info', `Staging files for commit:`); async function runFormatStep(): Promise<void> {
await smartshellInstance.exec(`git add -A`); ui.printHeader("Formatting project files");
await smartshellInstance.exec(`git commit -m "${commitString}"`); const formatMod = await import("../mod_format/index.js");
await smartshellInstance.exec(`npm version ${commitVersionType}`); await formatMod.run({ write: true, yes: true, interactive: false });
if (answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true')) { }
await smartshellInstance.exec(`git push origin master --follow-tags`);
async function runCommandStep(
smartshellInstance: plugins.smartshell.Smartshell,
label: string,
command: string,
): Promise<void> {
ui.printHeader(label);
const result = await smartshellInstance.exec(command);
if (result.exitCode !== 0) {
logger.log("error", `${label} failed. Aborting commit.`);
process.exit(1);
}
logger.log("success", `${label} passed.`);
}
async function runAnalyzeStep(): Promise<any> {
ui.printHeader("Analyzing repository changes");
const aidoc = new plugins.tsdoc.AiDoc();
await aidoc.start();
try {
const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd);
ui.printRecommendation({
recommendedNextVersion: nextCommitObject.recommendedNextVersion,
recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel,
recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope,
recommendedNextVersionMessage: nextCommitObject.recommendedNextVersionMessage,
});
return nextCommitObject;
} finally {
await aidoc.stop();
}
}
async function buildAnswerBucket(
nextCommitObject: any,
workflow: IResolvedCommitWorkflow,
mode: ICliMode,
argvArg: any,
): Promise<plugins.smartinteract.AnswerBucket> {
const isBreakingChange = nextCommitObject.recommendedNextVersionLevel === "BREAKING CHANGE";
const canAutoAccept = workflow.confirmation === "auto" && !isBreakingChange;
if (canAutoAccept) {
logger.log("info", "Auto-accepting AI recommendations");
return createAnswerBucket({
commitType: nextCommitObject.recommendedNextVersionLevel,
commitScope: nextCommitObject.recommendedNextVersionScope,
commitDescription: nextCommitObject.recommendedNextVersionMessage,
});
}
if (isBreakingChange && (workflow.confirmation === "auto" || argvArg.y || argvArg.yes)) {
logger.log("warn", "BREAKING CHANGE detected - manual confirmation required");
}
if (!mode.interactive) {
throw new Error("Commit confirmation requires an interactive terminal. Use `-y` or set commit.confirmation to `auto`.");
}
const commitInteract = new plugins.smartinteract.SmartInteract();
commitInteract.addQuestions([
{
type: "list",
name: "commitType",
message: "Choose TYPE of the commit:",
choices: ["fix", "feat", "BREAKING CHANGE"],
default: nextCommitObject.recommendedNextVersionLevel,
},
{
type: "input",
name: "commitScope",
message: "What is the SCOPE of the commit:",
default: nextCommitObject.recommendedNextVersionScope,
},
{
type: "input",
name: "commitDescription",
message: "What is the DESCRIPTION of the commit?",
default: nextCommitObject.recommendedNextVersionMessage,
},
]);
return await commitInteract.runQueue();
}
function createAnswerBucket(answers: {
commitType: string;
commitScope: string;
commitDescription: string;
}): plugins.smartinteract.AnswerBucket {
const answerBucket = new plugins.smartinteract.AnswerBucket();
for (const [name, value] of Object.entries(answers)) {
answerBucket.addAnswer({ name, value });
}
return answerBucket;
}
async function runChangelogStep(
workflow: IResolvedCommitWorkflow,
answerBucket: plugins.smartinteract.AnswerBucket,
nextCommitObject: any,
): Promise<void> {
await appendPendingChangelogEntry(
plugins.path.join(paths.cwd, workflow.changelogFile),
workflow.changelogSection,
{
type: answerBucket.getAnswerFor("commitType"),
scope: answerBucket.getAnswerFor("commitScope"),
message: answerBucket.getAnswerFor("commitDescription"),
details: nextCommitObject.recommendedNextVersionDetails || [],
},
);
logger.log("success", `Updated ${workflow.changelogFile} pending section.`);
}
async function runCommitStep(
smartshellInstance: plugins.smartshell.Smartshell,
answerBucket: plugins.smartinteract.AnswerBucket,
): Promise<void> {
ui.printHeader("Creating Semantic Commit");
const commitString = createCommitStringFromAnswerBucket(answerBucket);
ui.printCommitMessage(commitString);
await smartshellInstance.exec("git add -A");
const result = await smartshellInstance.exec(`git commit -m ${shellQuote(commitString)}`);
if (result.exitCode !== 0) {
logger.log("error", "git commit failed.");
process.exit(1);
}
}
async function runPushStep(
smartshellInstance: plugins.smartshell.Smartshell,
workflow: IResolvedCommitWorkflow,
): Promise<void> {
const currentBranch = await detectCurrentBranch(smartshellInstance);
const followTags = workflow.pushFollowTags ? " --follow-tags" : "";
const result = await smartshellInstance.exec(
`git push ${workflow.pushRemote} ${currentBranch}${followTags}`,
);
if (result.exitCode !== 0) {
logger.log("error", "git push failed.");
process.exit(1);
}
}
async function detectCurrentBranch(
smartshellInstance: plugins.smartshell.Smartshell,
): Promise<string> {
const branchResult = await smartshellInstance.exec("git branch --show-current");
return branchResult.stdout.trim() || "master";
}
function assertAnalysisComplete(
answerBucket: plugins.smartinteract.AnswerBucket | undefined,
nextCommitObject: any,
): void {
if (!answerBucket || !nextCommitObject) {
throw new Error("Commit workflow requires analyze before changelog and commit steps.");
}
}
function shellQuote(value: string): string {
return `'${value.replaceAll("'", "'\\''")}'`;
}
function printCommitExecutionPlan(workflow: IResolvedCommitWorkflow): void {
console.log("");
console.log("gitzone commit - resolved workflow");
console.log(`confirmation: ${workflow.confirmation}`);
console.log(`steps: ${workflow.steps.join(" -> ")}`);
console.log(`changelog: ${workflow.changelogFile}#${workflow.changelogSection}`);
console.log("");
}
async function handleRecommend(mode: ICliMode): Promise<void> {
const recommendationBuilder = async () => {
const aidoc = new plugins.tsdoc.AiDoc();
await aidoc.start();
try {
return await aidoc.buildNextCommitObject(paths.cwd);
} finally {
await aidoc.stop();
} }
}; };
const createCommitStringFromAnswerBucket = (answerBucket: plugins.smartinteract.AnswerBucket) => { const recommendation = mode.json
const commitType = answerBucket.getAnswerFor('commitType'); ? await runWithSuppressedOutput(recommendationBuilder)
const commitScope = answerBucket.getAnswerFor('commitScope'); : await recommendationBuilder();
const commitDescription = answerBucket.getAnswerFor('commitDescription');
if (mode.json) {
printJson(recommendation);
return;
}
ui.printRecommendation({
recommendedNextVersion: recommendation.recommendedNextVersion,
recommendedNextVersionLevel: recommendation.recommendedNextVersionLevel,
recommendedNextVersionScope: recommendation.recommendedNextVersionScope,
recommendedNextVersionMessage: recommendation.recommendedNextVersionMessage,
});
console.log(
`Suggested commit: ${recommendation.recommendedNextVersionLevel}(${recommendation.recommendedNextVersionScope}): ${recommendation.recommendedNextVersionMessage}`,
);
}
const createCommitStringFromAnswerBucket = (
answerBucket: plugins.smartinteract.AnswerBucket,
) => {
const commitType = answerBucket.getAnswerFor("commitType");
const commitScope = answerBucket.getAnswerFor("commitScope");
const commitDescription = answerBucket.getAnswerFor("commitDescription");
return `${commitType}(${commitScope}): ${commitDescription}`; return `${commitType}(${commitScope}): ${commitDescription}`;
}; };
export function showHelp(mode?: ICliMode): void {
if (mode?.json) {
printJson({
command: "commit",
usage: "gitzone commit [recommend] [options]",
description: "Analyzes changes and creates one semantic source commit.",
commands: [
{
name: "recommend",
description: "Generate a commit recommendation without mutating the repository",
},
],
flags: [
{ flag: "-y, --yes", description: "Auto-accept safe AI recommendations" },
{ flag: "-p, --push", description: "Push to origin after committing" },
{ flag: "-t, --test", description: "Run tests as part of the commit workflow" },
{ flag: "-b, --build", description: "Run build as part of the commit workflow" },
{ flag: "-f, --format", description: "Run gitzone format before committing" },
{ flag: "--plan", description: "Show resolved workflow without mutating files" },
{ flag: "--json", description: "Emit JSON for `commit recommend` only" },
],
examples: [
"gitzone commit recommend --json",
"gitzone commit -y",
"gitzone commit -ytbp",
"gitzone release",
],
});
return;
}
console.log("");
console.log("Usage: gitzone commit [recommend] [options]");
console.log("");
console.log("Creates one semantic source commit. It does not version, tag, or publish.");
console.log("");
console.log("Commands:");
console.log(" recommend Generate a commit recommendation without mutating the repository");
console.log("");
console.log("Flags:");
console.log(" -y, --yes Auto-accept safe AI recommendations");
console.log(" -p, --push Push after commit");
console.log(" -t, --test Run tests in the configured order");
console.log(" -b, --build Run build in the configured order");
console.log(" -f, --format Run gitzone format before committing");
console.log(" --plan Show resolved workflow without mutating files");
console.log(" --json Emit JSON for `commit recommend` only");
console.log("");
console.log("Examples:");
console.log(" gitzone commit recommend --json");
console.log(" gitzone commit -y");
console.log(" gitzone commit -ytbp");
console.log(" gitzone release");
console.log("");
}
+230
View File
@@ -0,0 +1,230 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { logger } from '../gitzone.logging.js';
import * as ui from './mod.ui.js';
export type ProjectType = 'npm' | 'deno' | 'both' | 'none';
export type VersionType = 'patch' | 'minor' | 'major';
/**
* Detects the current git branch
* @returns The current branch name, defaults to 'master' if detection fails
*/
export async function detectCurrentBranch(): Promise<string> {
try {
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash',
sourceFilePaths: [],
});
const result = await smartshellInstance.exec('git branch --show-current');
const branchName = result.stdout.trim();
if (!branchName) {
logger.log('warn', 'Could not detect current branch, falling back to "master"');
return 'master';
}
logger.log('info', `Detected current branch: ${branchName}`);
return branchName;
} catch (error) {
logger.log('warn', `Failed to detect branch: ${error.message}, falling back to "master"`);
return 'master';
}
}
/**
* Detects the project type based on presence of package.json and/or deno.json
* @returns The project type
*/
export async function detectProjectType(): Promise<ProjectType> {
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
const hasPackageJson = await plugins.smartfs.file(packageJsonPath).exists();
const hasDenoJson = await plugins.smartfs.file(denoJsonPath).exists();
if (hasPackageJson && hasDenoJson) {
logger.log('info', 'Detected dual project (npm + deno)');
return 'both';
} else if (hasPackageJson) {
logger.log('info', 'Detected npm project');
return 'npm';
} else if (hasDenoJson) {
logger.log('info', 'Detected deno project');
return 'deno';
} else {
throw new Error('No package.json or deno.json found in current directory');
}
}
/**
* Parses a semantic version string and bumps it according to the version type
* @param currentVersion Current version string (e.g., "1.2.3")
* @param versionType Type of version bump
* @returns New version string
*/
export function calculateNewVersion(currentVersion: string, versionType: VersionType): string {
const versionMatch = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/);
if (!versionMatch) {
throw new Error(`Invalid version format: ${currentVersion}`);
}
let [, major, minor, patch] = versionMatch.map(Number);
switch (versionType) {
case 'major':
major += 1;
minor = 0;
patch = 0;
break;
case 'minor':
minor += 1;
patch = 0;
break;
case 'patch':
patch += 1;
break;
}
return `${major}.${minor}.${patch}`;
}
/**
* Reads the current version from package.json or deno.json
* @param projectType The project type to determine which file to read
* @returns The current version string
*/
export async function readCurrentVersion(projectType: ProjectType): Promise<string> {
if (projectType === 'npm' || projectType === 'both') {
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
const content = (await plugins.smartfs
.file(packageJsonPath)
.encoding('utf8')
.read()) as string;
const packageJson = JSON.parse(content) as { version?: string };
if (!packageJson.version) {
throw new Error('package.json does not contain a version field');
}
return packageJson.version;
} else {
const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
const content = (await plugins.smartfs
.file(denoJsonPath)
.encoding('utf8')
.read()) as string;
const denoConfig = JSON.parse(content) as { version?: string };
if (!denoConfig.version) {
throw new Error('deno.json does not contain a version field');
}
return denoConfig.version;
}
}
/**
* Updates the version field in a JSON file (package.json or deno.json)
* @param filePath Path to the JSON file
* @param newVersion The new version to write
*/
export async function updateVersionFile(filePath: string, newVersion: string): Promise<void> {
const content = (await plugins.smartfs
.file(filePath)
.encoding('utf8')
.read()) as string;
const config = JSON.parse(content) as { version?: string };
config.version = newVersion;
await plugins.smartfs
.file(filePath)
.encoding('utf8')
.write(JSON.stringify(config, null, 2) + '\n');
}
/**
* Updates project version files without creating commits or tags.
*/
export async function updateProjectVersionFiles(
projectType: ProjectType,
newVersion: string,
): Promise<string[]> {
const filesToUpdate: string[] = [];
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
if (projectType === 'npm' || projectType === 'both') {
await updateVersionFile(packageJsonPath, newVersion);
filesToUpdate.push('package.json');
}
if (projectType === 'deno' || projectType === 'both') {
await updateVersionFile(denoJsonPath, newVersion);
filesToUpdate.push('deno.json');
}
return filesToUpdate;
}
/**
* Bumps the project version based on project type
* Handles npm-only, deno-only, and dual projects with unified logic
* @param projectType The detected project type
* @param versionType The type of version bump
* @param currentStep The current step number for progress display
* @param totalSteps The total number of steps for progress display
* @returns The new version string
*/
export async function bumpProjectVersion(
projectType: ProjectType,
versionType: VersionType,
currentStep?: number,
totalSteps?: number
): Promise<string> {
if (projectType === 'none') {
throw new Error('Cannot bump version: no package.json or deno.json found');
}
const projectEmoji = projectType === 'npm' ? '📦' : projectType === 'deno' ? '🦕' : '🔀';
const description = `🏷️ Bumping version (${projectEmoji} ${projectType})`;
if (currentStep && totalSteps) {
ui.printStep(currentStep, totalSteps, description, 'in-progress');
}
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash',
sourceFilePaths: [],
});
try {
// 1. Read current version
const currentVersion = await readCurrentVersion(projectType);
// 2. Calculate new version (reuse existing function!)
const newVersion = calculateNewVersion(currentVersion, versionType);
logger.log('info', `Bumping version: ${currentVersion}${newVersion}`);
// 3. Determine which files to update
const filesToUpdate = await updateProjectVersionFiles(projectType, newVersion);
// 4. Stage all updated files
await smartshellInstance.exec(`git add ${filesToUpdate.join(' ')}`);
// 5. Create version commit
await smartshellInstance.exec(`git commit -m "v${newVersion}"`);
// 6. Create version tag
await smartshellInstance.exec(`git tag v${newVersion} -m "v${newVersion}"`);
logger.log('info', `Created commit and tag v${newVersion}`);
if (currentStep && totalSteps) {
ui.printStep(currentStep, totalSteps, description, 'done');
}
return newVersion;
} catch (error) {
throw new Error(`Failed to bump project version: ${error.message}`);
}
}
+274
View File
@@ -0,0 +1,274 @@
import { logger } from '../gitzone.logging.js';
/**
* UI helper module for beautiful CLI output
*/
interface ICommitSummary {
projectType: string;
branch: string;
commitType: string;
commitScope: string;
commitMessage: string;
newVersion?: string;
commitSha?: string;
pushed: boolean;
repoUrl?: string;
released?: boolean;
releasedRegistries?: string[];
}
interface IExecutionPlanOptions {
autoAccept: boolean;
push: boolean;
test: boolean;
build: boolean;
release: boolean;
format: boolean;
registries?: string[];
}
interface IRecommendation {
recommendedNextVersion: string;
recommendedNextVersionLevel: string;
recommendedNextVersionScope: string;
recommendedNextVersionMessage: string;
}
/**
* Print a header with a box around it
*/
export function printHeader(title: string): void {
const width = 57;
const padding = Math.max(0, width - title.length - 2);
const leftPad = Math.floor(padding / 2);
const rightPad = padding - leftPad;
console.log('');
console.log('╭─' + '─'.repeat(width) + '─╮');
console.log('│ ' + title + ' '.repeat(rightPad + leftPad) + ' │');
console.log('╰─' + '─'.repeat(width) + '─╯');
console.log('');
}
/**
* Print execution plan at the start of commit
*/
export function printExecutionPlan(options: IExecutionPlanOptions): void {
console.log('');
console.log('╭───────────────────────────────────────────────────────────────╮');
console.log('│ gitzone commit - Execution Plan │');
console.log('╰───────────────────────────────────────────────────────────────╯');
console.log('');
// Show active options
console.log(' Options:');
console.log(` Auto-accept ${options.autoAccept ? '✓ enabled (-y)' : '○ interactive mode'}`);
console.log(` Push to remote ${options.push ? '✓ enabled (-p)' : '○ disabled'}`);
console.log(` Test first ${options.test ? '✓ enabled (-t)' : '○ disabled'}`);
console.log(` Build & verify ${options.build ? '✓ enabled (-b)' : '○ disabled'}`);
console.log(` Release to npm ${options.release ? '✓ enabled (-r)' : '○ disabled'}`);
if (options.format) {
console.log(` Format first ✓ enabled (--format)`);
}
console.log('');
// Show steps
console.log(' Steps:');
let stepNum = 1;
if (options.format) {
console.log(` ${stepNum++}. Format project files`);
}
if (options.test) {
console.log(` ${stepNum++}. Run tests`);
}
console.log(` ${stepNum++}. Analyze repository changes`);
console.log(` ${stepNum++}. Bake commit info into code`);
console.log(` ${stepNum++}. Generate changelog.md`);
console.log(` ${stepNum++}. Stage and commit files`);
console.log(` ${stepNum++}. Bump version`);
if (options.build) {
console.log(` ${stepNum++}. Run build`);
console.log(` ${stepNum++}. Verify clean working tree`);
}
if (options.push) {
console.log(` ${stepNum++}. Push to remote`);
}
if (options.release) {
console.log(` ${stepNum++}. Publish to npm registries`);
if (options.registries && options.registries.length > 0) {
options.registries.forEach(r => console.log(`${r}`));
}
}
console.log('');
}
/**
* Print a section with a border
*/
export function printSection(title: string, lines: string[]): void {
const width = 59;
console.log('┌─ ' + title + ' ' + '─'.repeat(Math.max(0, width - title.length - 3)) + '┐');
console.log('│' + ' '.repeat(width) + '│');
for (const line of lines) {
const padding = width - line.length;
console.log('│ ' + line + ' '.repeat(Math.max(0, padding - 2)) + '│');
}
console.log('│' + ' '.repeat(width) + '│');
console.log('└─' + '─'.repeat(width) + '─┘');
console.log('');
}
/**
* Print AI recommendations in a nice box
*/
export function printRecommendation(recommendation: IRecommendation): void {
const lines = [
`Suggested Version: v${recommendation.recommendedNextVersion}`,
`Suggested Type: ${recommendation.recommendedNextVersionLevel}`,
`Suggested Scope: ${recommendation.recommendedNextVersionScope}`,
`Suggested Message: ${recommendation.recommendedNextVersionMessage}`,
];
printSection('📊 AI Recommendations', lines);
}
/**
* Print a progress step
*/
export function printStep(
current: number,
total: number,
description: string,
status: 'in-progress' | 'done' | 'error'
): void {
const statusIcon = status === 'done' ? '✓' : status === 'error' ? '✗' : '⏳';
const dots = '.'.repeat(Math.max(0, 40 - description.length));
console.log(` [${current}/${total}] ${description}${dots} ${statusIcon}`);
// Clear the line on next update if in progress
if (status === 'in-progress') {
process.stdout.write('\x1b[1A'); // Move cursor up one line
}
}
/**
* Get emoji for project type
*/
function getProjectTypeEmoji(projectType: string): string {
switch (projectType) {
case 'npm':
return '📦 npm';
case 'deno':
return '🦕 Deno';
case 'both':
return '🔀 npm + Deno';
default:
return '❓ Unknown';
}
}
/**
* Get emoji for commit type
*/
function getCommitTypeEmoji(commitType: string): string {
switch (commitType) {
case 'fix':
return '🔧 fix';
case 'feat':
return '✨ feat';
case 'BREAKING CHANGE':
return '💥 BREAKING CHANGE';
default:
return commitType;
}
}
/**
* Print final commit summary
*/
export function printSummary(summary: ICommitSummary): void {
const lines = [
`Project Type: ${getProjectTypeEmoji(summary.projectType)}`,
`Branch: 🌿 ${summary.branch}`,
`Commit Type: ${getCommitTypeEmoji(summary.commitType)}`,
`Scope: 📍 ${summary.commitScope}`,
];
if (summary.newVersion) {
lines.push(`New Version: 🏷️ v${summary.newVersion}`);
} else {
lines.push(`Version: ⊘ Not bumped`);
}
if (summary.commitSha) {
lines.push(`Commit SHA: 📌 ${summary.commitSha}`);
}
if (summary.pushed) {
lines.push(`Remote: ✓ Pushed successfully`);
} else {
lines.push(`Remote: ⊘ Not pushed (local only)`);
}
if (summary.released && summary.releasedRegistries && summary.releasedRegistries.length > 0) {
lines.push(`Published: ✓ Released to ${summary.releasedRegistries.length} registr${summary.releasedRegistries.length === 1 ? 'y' : 'ies'}`);
summary.releasedRegistries.forEach((registry) => {
lines.push(`${registry}`);
});
}
if (summary.repoUrl && summary.commitSha) {
lines.push('');
lines.push(`View at: ${summary.repoUrl}/commit/${summary.commitSha}`);
}
printSection('✅ Commit Summary', lines);
if (summary.released) {
console.log('🎉 All done! Your changes are committed, pushed, and released.\n');
} else if (summary.pushed) {
console.log('🎉 All done! Your changes are committed and pushed.\n');
} else {
console.log('✓ Commit created successfully.\n');
}
}
/**
* Print an info message with consistent formatting
*/
export function printInfo(message: string): void {
console.log(` ${message}`);
}
/**
* Print a success message
*/
export function printSuccess(message: string): void {
console.log(`${message}`);
}
/**
* Print a warning message
*/
export function printWarning(message: string): void {
logger.log('warn', `⚠️ ${message}`);
}
/**
* Print an error message
*/
export function printError(message: string): void {
logger.log('error', `${message}`);
}
/**
* Print commit message being created
*/
export function printCommitMessage(commitString: string): void {
console.log(`\n 📝 Commit: ${commitString}\n`);
}
+134
View File
@@ -0,0 +1,134 @@
import * as plugins from './mod.plugins.js';
export interface ICommitConfig {
alwaysTest: boolean;
alwaysBuild: boolean;
confirmation: 'prompt' | 'auto' | 'plan';
steps: string[];
}
/**
* Manages commit configuration stored in .smartconfig.json
* under @git.zone/cli.commit namespace
*/
export class CommitConfig {
private cwd: string;
private config: ICommitConfig;
constructor(cwd: string = process.cwd()) {
this.cwd = cwd;
this.config = { alwaysTest: false, alwaysBuild: false, confirmation: 'prompt', steps: ['analyze', 'changelog', 'commit'] };
}
/**
* Create a CommitConfig instance from current working directory
*/
public static async fromCwd(cwd: string = process.cwd()): Promise<CommitConfig> {
const instance = new CommitConfig(cwd);
await instance.load();
return instance;
}
/**
* Load configuration from .smartconfig.json
*/
public async load(): Promise<void> {
const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
const alwaysTest = gitzoneConfig?.commit?.alwaysTest ?? false;
const alwaysBuild = gitzoneConfig?.commit?.alwaysBuild ?? false;
this.config = {
alwaysTest,
alwaysBuild,
confirmation: gitzoneConfig?.commit?.confirmation ?? 'prompt',
steps: gitzoneConfig?.commit?.steps || [
'analyze',
...(alwaysTest ? ['test'] : []),
...(alwaysBuild ? ['build'] : []),
'changelog',
'commit',
],
};
}
/**
* Save configuration to .smartconfig.json
*/
public async save(): Promise<void> {
const smartconfigPath = plugins.path.join(this.cwd, '.smartconfig.json');
let smartconfigData: any = {};
// Read existing .smartconfig.json
if (await plugins.smartfs.file(smartconfigPath).exists()) {
const content = await plugins.smartfs.file(smartconfigPath).encoding('utf8').read();
smartconfigData = JSON.parse(content as string);
}
// Ensure @git.zone/cli namespace exists
if (!smartconfigData['@git.zone/cli']) {
smartconfigData['@git.zone/cli'] = {};
}
// Ensure commit object exists
if (!smartconfigData['@git.zone/cli'].commit) {
smartconfigData['@git.zone/cli'].commit = {};
}
// Update commit settings
smartconfigData['@git.zone/cli'].commit.alwaysTest = this.config.alwaysTest;
smartconfigData['@git.zone/cli'].commit.alwaysBuild = this.config.alwaysBuild;
smartconfigData['@git.zone/cli'].commit.confirmation = this.config.confirmation;
smartconfigData['@git.zone/cli'].commit.steps = this.config.steps;
// Write back to file
await plugins.smartfs
.file(smartconfigPath)
.encoding('utf8')
.write(JSON.stringify(smartconfigData, null, 2));
}
/**
* Get alwaysTest setting
*/
public getAlwaysTest(): boolean {
return this.config.alwaysTest;
}
/**
* Set alwaysTest setting
*/
public setAlwaysTest(value: boolean): void {
this.config.alwaysTest = value;
}
/**
* Get alwaysBuild setting
*/
public getAlwaysBuild(): boolean {
return this.config.alwaysBuild;
}
/**
* Set alwaysBuild setting
*/
public setAlwaysBuild(value: boolean): void {
this.config.alwaysBuild = value;
}
public getConfirmation(): 'prompt' | 'auto' | 'plan' {
return this.config.confirmation;
}
public setConfirmation(value: 'prompt' | 'auto' | 'plan'): void {
this.config.confirmation = value;
}
public getSteps(): string[] {
return [...this.config.steps];
}
public setSteps(steps: string[]): void {
this.config.steps = [...steps];
}
}
+173
View File
@@ -0,0 +1,173 @@
import * as plugins from './mod.plugins.js';
export type TAccessLevel = 'public' | 'private';
export interface IReleaseConfig {
registries: string[];
accessLevel: TAccessLevel;
}
/**
* Manages release configuration stored in .smartconfig.json
* under @git.zone/cli.release namespace
*/
export class ReleaseConfig {
private cwd: string;
private config: IReleaseConfig;
constructor(cwd: string = process.cwd()) {
this.cwd = cwd;
this.config = { registries: [], accessLevel: 'public' };
}
/**
* Create a ReleaseConfig instance from current working directory
*/
public static async fromCwd(cwd: string = process.cwd()): Promise<ReleaseConfig> {
const instance = new ReleaseConfig(cwd);
await instance.load();
return instance;
}
/**
* Load configuration from .smartconfig.json
*/
public async load(): Promise<void> {
const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
const npmTarget = gitzoneConfig?.release?.targets?.npm || {};
this.config = {
registries: npmTarget.registries || [],
accessLevel: npmTarget.accessLevel || 'public',
};
}
/**
* Save configuration to .smartconfig.json
*/
public async save(): Promise<void> {
const smartconfigPath = plugins.path.join(this.cwd, '.smartconfig.json');
let smartconfigData: any = {};
// Read existing .smartconfig.json
if (await plugins.smartfs.file(smartconfigPath).exists()) {
const content = await plugins.smartfs.file(smartconfigPath).encoding('utf8').read();
smartconfigData = JSON.parse(content as string);
}
// Ensure @git.zone/cli namespace exists
if (!smartconfigData['@git.zone/cli']) {
smartconfigData['@git.zone/cli'] = {};
}
// Ensure release object exists
if (!smartconfigData['@git.zone/cli'].release) {
smartconfigData['@git.zone/cli'].release = {};
}
if (!smartconfigData['@git.zone/cli'].release.targets) {
smartconfigData['@git.zone/cli'].release.targets = {};
}
if (!smartconfigData['@git.zone/cli'].release.targets.npm) {
smartconfigData['@git.zone/cli'].release.targets.npm = {};
}
// Update registries and accessLevel
smartconfigData['@git.zone/cli'].release.targets.npm.enabled = this.config.registries.length > 0;
smartconfigData['@git.zone/cli'].release.targets.npm.registries = this.config.registries;
smartconfigData['@git.zone/cli'].release.targets.npm.accessLevel = this.config.accessLevel;
// Write back to file
await plugins.smartfs
.file(smartconfigPath)
.encoding('utf8')
.write(JSON.stringify(smartconfigData, null, 2));
}
/**
* Get all configured registries
*/
public getRegistries(): string[] {
return [...this.config.registries];
}
/**
* Check if any registries are configured
*/
public hasRegistries(): boolean {
return this.config.registries.length > 0;
}
/**
* Add a registry URL
* @returns true if added, false if already exists
*/
public addRegistry(url: string): boolean {
const normalizedUrl = this.normalizeUrl(url);
if (this.config.registries.includes(normalizedUrl)) {
return false;
}
this.config.registries.push(normalizedUrl);
return true;
}
/**
* Remove a registry URL
* @returns true if removed, false if not found
*/
public removeRegistry(url: string): boolean {
const normalizedUrl = this.normalizeUrl(url);
const index = this.config.registries.indexOf(normalizedUrl);
if (index === -1) {
return false;
}
this.config.registries.splice(index, 1);
return true;
}
/**
* Clear all registries
*/
public clearRegistries(): void {
this.config.registries = [];
}
/**
* Get the npm access level
*/
public getAccessLevel(): TAccessLevel {
return this.config.accessLevel;
}
/**
* Set the npm access level
*/
public setAccessLevel(level: TAccessLevel): void {
this.config.accessLevel = level;
}
/**
* Normalize a registry URL (ensure it has https:// prefix)
*/
private normalizeUrl(url: string): string {
let normalized = url.trim();
// Add https:// if no protocol specified
if (!normalized.startsWith('http://') && !normalized.startsWith('https://')) {
normalized = `https://${normalized}`;
}
// Remove trailing slash
if (normalized.endsWith('/')) {
normalized = normalized.slice(0, -1);
}
return normalized;
}
}
+800
View File
@@ -0,0 +1,800 @@
// gitzone config - manage CLI smartconfig configuration
import * as plugins from "./mod.plugins.js";
import { ReleaseConfig } from "./classes.releaseconfig.js";
import { CommitConfig } from "./classes.commitconfig.js";
import { runFormatter, type ICheckResult } from "../mod_format/index.js";
import type { ICliMode } from "../helpers.climode.js";
import { getCliMode, printJson } from "../helpers.climode.js";
import {
getCliConfigValueFromData,
readSmartconfigFile,
setCliConfigValueInData,
unsetCliConfigValueInData,
writeSmartconfigFile,
} from "../helpers.smartconfig.js";
import {
CURRENT_GITZONE_CLI_SCHEMA_VERSION,
migrateSmartconfigData,
} from "../helpers.smartconfigmigrations.js";
export { ReleaseConfig, CommitConfig };
const defaultCliMode: ICliMode = {
output: "human",
interactive: true,
json: false,
plain: false,
quiet: false,
yes: false,
help: false,
agent: false,
checkUpdates: true,
isTty: true,
};
/**
* Format .smartconfig.json with diff preview
* Shows diff first, asks for confirmation, then applies
*/
async function formatSmartconfigWithDiff(mode: ICliMode): Promise<void> {
if (!mode.interactive) {
return;
}
// Check for diffs first
const checkResult = (await runFormatter("smartconfig", {
checkOnly: true,
showDiff: true,
})) as ICheckResult | void;
if (checkResult && checkResult.hasDiff) {
const shouldApply =
await plugins.smartinteract.SmartInteract.getCliConfirmation(
"Apply formatting changes to .smartconfig.json?",
true,
);
if (shouldApply) {
await runFormatter("smartconfig", { silent: true });
}
}
}
export const run = async (argvArg: any) => {
const mode = await getCliMode(argvArg);
const command = argvArg._?.[1];
const value = argvArg._?.[2];
if (mode.help || command === "help") {
showHelp(mode);
return;
}
// If no command provided, show interactive menu
if (!command) {
if (!mode.interactive) {
showHelp(mode);
return;
}
await handleInteractiveMenu();
return;
}
switch (command) {
case "show":
await handleShow(mode);
break;
case "add":
await handleAdd(value, mode);
break;
case "remove":
await handleRemove(value, mode);
break;
case "clear":
await handleClear(mode);
break;
case "access":
case "accessLevel":
await handleAccessLevel(value, mode);
break;
case "commit":
await handleCommit(argvArg._?.[2], argvArg._?.[3], mode);
break;
case "services":
await handleServices(mode);
break;
case "migrate":
await handleMigrate(value, mode);
break;
case "get":
await handleGet(value, mode);
break;
case "set":
await handleSet(value, argvArg._?.[3], mode);
break;
case "unset":
await handleUnset(value, mode);
break;
default:
plugins.logger.log("error", `Unknown command: ${command}`);
showHelp(mode);
}
};
/**
* 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 an npm target registry", value: "add" },
{ name: "Remove an npm target registry", value: "remove" },
{ name: "Clear npm target registries", value: "clear" },
{ name: "Set access level (public/private)", value: "access" },
{ name: "Migrate smartconfig schema", value: "migrate" },
{ 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(defaultCliMode);
break;
case "add":
await handleAdd(undefined, defaultCliMode);
break;
case "remove":
await handleRemove(undefined, defaultCliMode);
break;
case "clear":
await handleClear(defaultCliMode);
break;
case "access":
await handleAccessLevel(undefined, defaultCliMode);
break;
case "migrate":
await handleMigrate(undefined, defaultCliMode);
break;
case "commit":
await handleCommit(undefined, undefined, defaultCliMode);
break;
case "services":
await handleServices(defaultCliMode);
break;
case "help":
showHelp();
break;
}
}
/**
* Show current registry configuration
*/
async function handleShow(mode: ICliMode): Promise<void> {
if (mode.json) {
const smartconfigData = await readSmartconfigFile();
printJson(getCliConfigValueFromData(smartconfigData, ""));
return;
}
const config = await ReleaseConfig.fromCwd();
const registries = config.getRegistries();
const accessLevel = config.getAccessLevel();
console.log("");
console.log(
"╭─────────────────────────────────────────────────────────────╮",
);
console.log(
"│ Release NPM Target 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 npm target registries configured.");
console.log("");
console.log(" Run `gitzone config add <registry-url>` to add one.");
console.log("");
} else {
plugins.logger.log("info", `Configured npm target registries (${registries.length}):`);
console.log("");
registries.forEach((url, index) => {
console.log(` ${index + 1}. ${url}`);
});
console.log("");
}
}
/**
* Add an npm target registry URL
*/
async function handleAdd(
url: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!url) {
if (!mode.interactive) {
throw new Error("Registry URL is required in non-interactive mode");
}
// Interactive mode
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: "input",
name: "registryUrl",
message: "Enter npm target 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();
if (mode.json) {
printJson({
ok: true,
action: "add",
registry: url,
registries: config.getRegistries(),
});
return;
}
plugins.logger.log("success", `Added npm target registry: ${url}`);
await formatSmartconfigWithDiff(mode);
} else {
plugins.logger.log("warn", `Registry already exists: ${url}`);
}
}
/**
* Remove an npm target registry URL
*/
async function handleRemove(
url: string | undefined,
mode: ICliMode,
): Promise<void> {
const config = await ReleaseConfig.fromCwd();
const registries = config.getRegistries();
if (registries.length === 0) {
plugins.logger.log("warn", "No npm target registries configured to remove.");
return;
}
if (!url) {
if (!mode.interactive) {
throw new Error("Registry URL is required in non-interactive mode");
}
// Interactive mode - show list to select from
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: "list",
name: "registryUrl",
message: "Select npm target registry to remove:",
choices: registries,
default: registries[0],
});
url = (response as any).value;
}
const removed = config.removeRegistry(url!);
if (removed) {
await config.save();
if (mode.json) {
printJson({
ok: true,
action: "remove",
registry: url,
registries: config.getRegistries(),
});
return;
}
plugins.logger.log("success", `Removed npm target registry: ${url}`);
await formatSmartconfigWithDiff(mode);
} else {
plugins.logger.log("warn", `Registry not found: ${url}`);
}
}
/**
* Clear all npm target registries
*/
async function handleClear(mode: ICliMode): Promise<void> {
const config = await ReleaseConfig.fromCwd();
if (!config.hasRegistries()) {
plugins.logger.log("info", "No npm target registries to clear.");
return;
}
// Confirm before clearing
const confirmed = mode.interactive
? await plugins.smartinteract.SmartInteract.getCliConfirmation(
"Clear all configured npm target registries?",
false,
)
: true;
if (confirmed) {
config.clearRegistries();
await config.save();
if (mode.json) {
printJson({ ok: true, action: "clear", registries: [] });
return;
}
plugins.logger.log("success", "All npm target registries cleared.");
await formatSmartconfigWithDiff(mode);
} else {
plugins.logger.log("info", "Operation cancelled.");
}
}
/**
* Set or toggle access level
*/
async function handleAccessLevel(
level: string | undefined,
mode: ICliMode,
): Promise<void> {
const config = await ReleaseConfig.fromCwd();
const currentLevel = config.getAccessLevel();
if (!level) {
if (!mode.interactive) {
throw new Error("Access level is required in non-interactive mode");
}
// Interactive mode - toggle or ask
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: "list",
name: "accessLevel",
message: "Select npm access level for publishing:",
choices: ["public", "private"],
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();
if (mode.json) {
printJson({ ok: true, action: "access", accessLevel: level });
return;
}
plugins.logger.log("success", `Access level set to: ${level}`);
await formatSmartconfigWithDiff(mode);
}
/**
* Handle commit configuration
*/
async function handleCommit(
setting: string | undefined,
value: string | undefined,
mode: ICliMode,
): Promise<void> {
const config = await CommitConfig.fromCwd();
// No setting = interactive mode
if (!setting) {
if (!mode.interactive) {
throw new Error("Commit setting is required in non-interactive mode");
}
await handleCommitInteractive(config);
return;
}
// Direct setting
switch (setting) {
case "alwaysTest":
await handleCommitSetting(config, "alwaysTest", value, mode);
break;
case "alwaysBuild":
await handleCommitSetting(config, "alwaysBuild", value, mode);
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"));
syncCommitStepsFromBooleans(config);
await config.save();
plugins.logger.log("success", "Commit configuration updated");
await formatSmartconfigWithDiff(defaultCliMode);
}
/**
* Set a specific commit setting
*/
async function handleCommitSetting(
config: CommitConfig,
setting: string,
value: string | undefined,
mode: ICliMode,
): Promise<void> {
// Parse boolean value
const boolValue = value === "true" || value === "1" || value === "on";
if (setting === "alwaysTest") {
config.setAlwaysTest(boolValue);
} else if (setting === "alwaysBuild") {
config.setAlwaysBuild(boolValue);
}
syncCommitStepsFromBooleans(config);
await config.save();
if (mode.json) {
printJson({ ok: true, action: "commit", setting, value: boolValue });
return;
}
plugins.logger.log("success", `Set ${setting} to ${boolValue}`);
await formatSmartconfigWithDiff(mode);
}
function syncCommitStepsFromBooleans(config: CommitConfig): void {
config.setSteps([
"analyze",
...(config.getAlwaysTest() ? ["test"] : []),
...(config.getAlwaysBuild() ? ["build"] : []),
"changelog",
"commit",
]);
}
/**
* 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(mode: ICliMode): Promise<void> {
if (!mode.interactive) {
throw new Error(
"Use `gitzone services config --json` or `gitzone services set ...` in non-interactive mode",
);
}
// Import and use ServiceManager's configureServices
const { ServiceManager } =
await import("../mod_services/classes.servicemanager.js");
const serviceManager = new ServiceManager();
await serviceManager.init();
await serviceManager.configureServices();
}
async function handleGet(
configPath: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!configPath) {
throw new Error("Configuration path is required");
}
const smartconfigData = await readSmartconfigFile();
const value = getCliConfigValueFromData(smartconfigData, configPath);
if (mode.json) {
printJson({ path: configPath, value, exists: value !== undefined });
return;
}
if (value === undefined) {
plugins.logger.log("warn", `No value set for ${configPath}`);
return;
}
if (typeof value === "string") {
console.log(value);
return;
}
printJson(value);
}
async function handleSet(
configPath: string | undefined,
rawValue: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!configPath) {
throw new Error("Configuration path is required");
}
if (rawValue === undefined) {
throw new Error("Configuration value is required");
}
const smartconfigData = await readSmartconfigFile();
const parsedValue = parseConfigValue(rawValue);
setCliConfigValueInData(smartconfigData, configPath, parsedValue);
await writeSmartconfigFile(smartconfigData);
if (mode.json) {
printJson({
ok: true,
action: "set",
path: configPath,
value: parsedValue,
});
return;
}
plugins.logger.log("success", `Set ${configPath}`);
}
async function handleUnset(
configPath: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!configPath) {
throw new Error("Configuration path is required");
}
const smartconfigData = await readSmartconfigFile();
const removed = unsetCliConfigValueInData(smartconfigData, configPath);
if (!removed) {
if (mode.json) {
printJson({
ok: false,
action: "unset",
path: configPath,
removed: false,
});
return;
}
plugins.logger.log("warn", `No value set for ${configPath}`);
return;
}
await writeSmartconfigFile(smartconfigData);
if (mode.json) {
printJson({ ok: true, action: "unset", path: configPath, removed: true });
return;
}
plugins.logger.log("success", `Unset ${configPath}`);
}
async function handleMigrate(
rawTargetVersion: string | undefined,
mode: ICliMode,
): Promise<void> {
const targetVersion = rawTargetVersion
? Number(rawTargetVersion)
: CURRENT_GITZONE_CLI_SCHEMA_VERSION;
if (!Number.isInteger(targetVersion) || targetVersion < 1) {
throw new Error("Migration target version must be a positive integer");
}
const smartconfigData = await readSmartconfigFile();
const result = migrateSmartconfigData(smartconfigData, targetVersion);
if (result.migrated) {
await writeSmartconfigFile(smartconfigData);
}
if (mode.json) {
printJson({ ok: true, action: "migrate", ...result });
return;
}
if (result.migrated) {
plugins.logger.log(
"success",
`Migrated .smartconfig.json from schema v${result.fromVersion} to v${result.toVersion}`,
);
} else {
plugins.logger.log("info", `.smartconfig.json already at schema v${result.toVersion}`);
}
}
function parseConfigValue(rawValue: string): any {
const trimmedValue = rawValue.trim();
if (trimmedValue === "true") {
return true;
}
if (trimmedValue === "false") {
return false;
}
if (trimmedValue === "null") {
return null;
}
if (/^-?\d+(\.\d+)?$/.test(trimmedValue)) {
return Number(trimmedValue);
}
if (
(trimmedValue.startsWith("{") && trimmedValue.endsWith("}")) ||
(trimmedValue.startsWith("[") && trimmedValue.endsWith("]")) ||
(trimmedValue.startsWith('"') && trimmedValue.endsWith('"'))
) {
return JSON.parse(trimmedValue);
}
return rawValue;
}
/**
* Show help for config command
*/
export function showHelp(mode?: ICliMode): void {
if (mode?.json) {
printJson({
command: "config",
usage: "gitzone config <command> [options]",
commands: [
{
name: "show",
description: "Display current @git.zone/cli configuration",
},
{ name: "get <path>", description: "Read a single config value" },
{ name: "set <path> <value>", description: "Write a config value" },
{ name: "unset <path>", description: "Delete a config value" },
{ name: "add [url]", description: "Add an npm release target registry" },
{ name: "remove [url]", description: "Remove an npm release target registry" },
{ name: "clear", description: "Clear npm release target registries" },
{
name: "access [public|private]",
description: "Set npm target publish access level",
},
{
name: "commit <setting> <value>",
description: "Set commit defaults",
},
{
name: "migrate [version]",
description: "Run version-targeted .smartconfig.json migrations",
},
],
examples: [
"gitzone config show --json",
"gitzone config get release.targets.npm.accessLevel",
"gitzone config set cli.interactive false",
"gitzone config set cli.output json",
],
});
return;
}
console.log("");
console.log("Usage: gitzone config <command> [options]");
console.log("");
console.log("Commands:");
console.log(
" show Display current @git.zone/cli configuration",
);
console.log(" get <path> Read a single config value");
console.log(" set <path> <value> Write a config value");
console.log(" unset <path> Delete a config value");
console.log(" add [url] Add an npm target registry URL");
console.log(" remove [url] Remove an npm target registry URL");
console.log(" clear Clear npm target registries");
console.log(
" access [public|private] Set npm target access level for publishing",
);
console.log(" commit [setting] [value] Configure commit options");
console.log(" migrate [version] Run version-targeted smartconfig migrations");
console.log(
" services Configure which services are enabled",
);
console.log("");
console.log("Examples:");
console.log(" gitzone config show");
console.log(" gitzone config show --json");
console.log(" gitzone config get release.targets.npm.accessLevel");
console.log(" gitzone config set cli.interactive false");
console.log(" gitzone config set cli.output json");
console.log(" gitzone config unset cli.output");
console.log(" gitzone config add https://registry.npmjs.org");
console.log(" gitzone config add https://verdaccio.example.com");
console.log(" gitzone config remove https://registry.npmjs.org");
console.log(" gitzone config clear");
console.log(" gitzone config access public");
console.log(" gitzone config access private");
console.log(" gitzone config migrate 2");
console.log(" gitzone config commit # Interactive");
console.log(" gitzone config commit alwaysTest true");
console.log(" gitzone config services # Interactive");
console.log("");
}
+3
View File
@@ -0,0 +1,3 @@
// mod_config plugins
export * from '../plugins.js';
export { logger } from '../gitzone.logging.js';
+17 -2
View File
@@ -4,6 +4,15 @@ import { logger } from '../gitzone.logging.js';
export const run = async () => { export const run = async () => {
const smartInteract = new plugins.smartinteract.SmartInteract([ const smartInteract = new plugins.smartinteract.SmartInteract([
{
name: `registryUrls`,
message: `What are the comma separated registry URLs?`,
type: `input`,
default: `https://registry.npmjs.org`,
validate: (stringInput) => {
return stringInput !== '' && !process.env.CI;
},
},
{ {
name: `oldPackageName`, name: `oldPackageName`,
message: `Whats the name of the OLD package?`, message: `Whats the name of the OLD package?`,
@@ -24,14 +33,20 @@ export const run = async () => {
}, },
]); ]);
const answerBucket = await smartInteract.runQueue(); const answerBucket = await smartInteract.runQueue();
const registryUrls = answerBucket.getAnswerFor(`registryUrls`).split(',');
const oldPackageName = answerBucket.getAnswerFor(`oldPackageName`); const oldPackageName = answerBucket.getAnswerFor(`oldPackageName`);
const newPackageName = answerBucket.getAnswerFor(`newPackageName`); const newPackageName = answerBucket.getAnswerFor(`newPackageName`);
logger.log('info', `Deprecating package ${oldPackageName} in favour of ${newPackageName}`); logger.log(
'info',
`Deprecating package ${oldPackageName} in favour of ${newPackageName}`,
);
const smartshellInstance = new plugins.smartshell.Smartshell({ const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash', executor: 'bash',
}); });
for (const registryUrl of registryUrls) {
await smartshellInstance.exec( await smartshellInstance.exec(
`npm deprecate ${oldPackageName}@* ` + `npm deprecate ${oldPackageName}@* ` +
`"${oldPackageName} has been deprecated in favour of ${newPackageName} - please upgrade asap!!!"` `"${oldPackageName} has been deprecated in favour of ${newPackageName} - please upgrade asap!!!" --registry ${registryUrl}`,
); );
}
}; };
+12
View File
@@ -0,0 +1,12 @@
import * as plugins from './mod.plugins.js';
export const run = async (argvArg) => {
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash',
});
switch (argvArg._[1]) {
case 'prune':
await smartshellInstance.exec(`docker system prune -a -f --volumes`);
break;
}
};
+4
View File
@@ -0,0 +1,4 @@
export * from '../plugins.js';
import * as smartshell from '@push.rocks/smartshell';
export { smartshell };
+165
View File
@@ -0,0 +1,165 @@
import * as plugins from './mod.plugins.js';
import { FormatContext } from './classes.formatcontext.js';
import type { IPlannedChange, ICheckResult } from './interfaces.format.js';
import { Project } from '../classes.project.js';
import { FormatStats } from './classes.formatstats.js';
export abstract class BaseFormatter {
protected context: FormatContext;
protected project: Project;
protected stats: FormatStats;
constructor(context: FormatContext, project: Project) {
this.context = context;
this.project = project;
this.stats = context.getFormatStats();
}
abstract get name(): string;
abstract analyze(): Promise<IPlannedChange[]>;
abstract applyChange(change: IPlannedChange): Promise<void>;
async execute(changes: IPlannedChange[]): Promise<void> {
const startTime = this.stats.moduleStartTime(this.name);
this.stats.startModule(this.name);
try {
await this.preExecute();
for (const change of changes) {
try {
await this.applyChange(change);
this.stats.recordFileOperation(this.name, change.type, true);
} catch (error) {
this.stats.recordFileOperation(this.name, change.type, false);
throw error;
}
}
await this.postExecute();
} finally {
this.stats.endModule(this.name, startTime);
}
}
protected async preExecute(): Promise<void> {
// Override in subclasses if needed
}
protected async postExecute(): Promise<void> {
// Override in subclasses if needed
}
protected async modifyFile(filepath: string, content: string): Promise<void> {
if (!filepath || filepath.trim() === '') {
throw new Error(`Invalid empty filepath in modifyFile`);
}
let normalizedPath = filepath;
if (!plugins.path.parse(filepath).dir) {
normalizedPath = './' + filepath;
}
await plugins.smartfs.file(normalizedPath).encoding('utf8').write(content);
}
protected async createFile(filepath: string, content: string): Promise<void> {
let normalizedPath = filepath;
if (!plugins.path.parse(filepath).dir) {
normalizedPath = './' + filepath;
}
// Ensure parent directory exists
const dir = plugins.path.dirname(normalizedPath);
if (dir && dir !== '.') {
await plugins.smartfs.directory(dir).recursive().create();
}
await plugins.smartfs.file(normalizedPath).encoding('utf8').write(content);
}
protected async deleteFile(filepath: string): Promise<void> {
await plugins.smartfs.file(filepath).delete();
}
/**
* Check for diffs without applying changes
*/
async check(): Promise<ICheckResult> {
const changes = await this.analyze();
const diffs: ICheckResult['diffs'] = [];
for (const change of changes) {
if (change.path === '<various files>') {
continue;
}
if (change.type === 'modify' || change.type === 'create') {
let currentContent: string | undefined;
try {
currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
} catch {
currentContent = undefined;
}
const newContent = change.content;
if (currentContent !== newContent && newContent !== undefined) {
diffs.push({
path: change.path,
type: change.type,
before: currentContent,
after: newContent,
});
}
} else if (change.type === 'delete') {
try {
const currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
diffs.push({
path: change.path,
type: 'delete',
before: currentContent,
after: undefined,
});
} catch {
// File doesn't exist, nothing to delete
}
}
}
return {
hasDiff: diffs.length > 0,
diffs,
};
}
displayDiff(diff: ICheckResult['diffs'][0]): void {
console.log(`\n--- ${diff.path}`);
if (diff.before && diff.after) {
console.log(plugins.smartdiff.formatUnifiedDiffForConsole(diff.before, diff.after, {
originalFileName: diff.path,
revisedFileName: diff.path,
context: 3,
}));
} else if (diff.after && !diff.before) {
console.log(' (new file)');
const lines = diff.after.split('\n').slice(0, 10);
lines.forEach(line => console.log(` + ${line}`));
if (diff.after.split('\n').length > 10) {
console.log(' ... (truncated)');
}
} else if (diff.before && !diff.after) {
console.log(' (file will be deleted)');
}
}
displayAllDiffs(result: ICheckResult): void {
if (!result.hasDiff) {
console.log(' No changes detected');
return;
}
for (const diff of result.diffs) {
this.displayDiff(diff);
}
}
}
+125
View File
@@ -0,0 +1,125 @@
import * as plugins from './mod.plugins.js';
import type { IPlannedChange } from './interfaces.format.js';
import { logger } from '../gitzone.logging.js';
export class DiffReporter {
private diffs: Map<string, string> = new Map();
async generateDiff(
filePath: string,
oldContent: string,
newContent: string,
): Promise<string> {
const diff = plugins.smartdiff.createDiff(oldContent, newContent);
this.diffs.set(filePath, diff);
return diff;
}
async generateDiffForChange(change: IPlannedChange): Promise<string | null> {
if (change.type !== 'modify') {
return null;
}
try {
const exists = await plugins.smartfs.file(change.path).exists();
if (!exists) {
return null;
}
const currentContent = (await plugins.smartfs
.file(change.path)
.encoding('utf8')
.read()) as string;
// For planned changes, we need the new content
if (!change.content) {
return null;
}
return await this.generateDiff(
change.path,
currentContent,
change.content,
);
} catch (error) {
logger.log(
'error',
`Failed to generate diff for ${change.path}: ${error.message}`,
);
return null;
}
}
displayDiff(filePath: string, diff?: string): void {
const diffToShow = diff || this.diffs.get(filePath);
if (!diffToShow) {
logger.log('warn', `No diff available for ${filePath}`);
return;
}
console.log(`\n${this.formatDiffHeader(filePath)}`);
console.log(this.colorDiff(diffToShow));
console.log('━'.repeat(50));
}
displayAllDiffs(): void {
if (this.diffs.size === 0) {
logger.log('info', 'No diffs to display');
return;
}
console.log('\nFile Changes:');
console.log('═'.repeat(50));
for (const [filePath, diff] of this.diffs) {
this.displayDiff(filePath, diff);
}
}
private formatDiffHeader(filePath: string): string {
return `📄 ${filePath}`;
}
private colorDiff(diff: string): string {
const lines = diff.split('\n');
const coloredLines = lines.map((line) => {
if (line.startsWith('+') && !line.startsWith('+++')) {
return `\x1b[32m${line}\x1b[0m`; // Green for additions
} else if (line.startsWith('-') && !line.startsWith('---')) {
return `\x1b[31m${line}\x1b[0m`; // Red for deletions
} else if (line.startsWith('@')) {
return `\x1b[36m${line}\x1b[0m`; // Cyan for line numbers
} else {
return line;
}
});
return coloredLines.join('\n');
}
async saveDiffReport(outputPath: string): Promise<void> {
const report = {
timestamp: new Date().toISOString(),
totalFiles: this.diffs.size,
diffs: Array.from(this.diffs.entries()).map(([path, diff]) => ({
path,
diff,
})),
};
await plugins.smartfs
.file(outputPath)
.encoding('utf8')
.write(JSON.stringify(report, null, 2));
logger.log('info', `Diff report saved to ${outputPath}`);
}
hasAnyDiffs(): boolean {
return this.diffs.size > 0;
}
getDiffCount(): number {
return this.diffs.size;
}
}
+31
View File
@@ -0,0 +1,31 @@
import * as plugins from "./mod.plugins.js";
import { FormatStats } from "./classes.formatstats.js";
interface IFormatContextOptions {
interactive?: boolean;
jsonOutput?: boolean;
}
export class FormatContext {
private formatStats: FormatStats;
private interactive: boolean;
private jsonOutput: boolean;
constructor(options: IFormatContextOptions = {}) {
this.formatStats = new FormatStats();
this.interactive = options.interactive ?? true;
this.jsonOutput = options.jsonOutput ?? false;
}
getFormatStats(): FormatStats {
return this.formatStats;
}
isInteractive(): boolean {
return this.interactive;
}
isJsonOutput(): boolean {
return this.jsonOutput;
}
}
+141
View File
@@ -0,0 +1,141 @@
import * as plugins from './mod.plugins.js';
import { FormatContext } from './classes.formatcontext.js';
import { BaseFormatter } from './classes.baseformatter.js';
import type { IFormatPlan, IPlannedChange } from './interfaces.format.js';
import { getModuleIcon } from './interfaces.format.js';
import { logger } from '../gitzone.logging.js';
import { DiffReporter } from './classes.diffreporter.js';
export class FormatPlanner {
private plannedChanges: Map<string, IPlannedChange[]> = new Map();
private diffReporter = new DiffReporter();
async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> {
const plan: IFormatPlan = {
summary: {
totalFiles: 0,
filesAdded: 0,
filesModified: 0,
filesRemoved: 0,
},
changes: [],
warnings: [],
};
for (const module of modules) {
try {
const changes = await module.analyze();
this.plannedChanges.set(module.name, changes);
for (const change of changes) {
plan.changes.push(change);
switch (change.type) {
case 'create':
plan.summary.filesAdded++;
break;
case 'modify':
plan.summary.filesModified++;
break;
case 'delete':
plan.summary.filesRemoved++;
break;
}
}
} catch (error) {
plan.warnings.push({
level: 'error',
message: `Failed to analyze module ${module.name}: ${error.message}`,
module: module.name,
});
}
}
plan.summary.totalFiles =
plan.summary.filesAdded +
plan.summary.filesModified +
plan.summary.filesRemoved;
return plan;
}
async executePlan(
plan: IFormatPlan,
modules: BaseFormatter[],
context: FormatContext,
): Promise<void> {
const startTime = Date.now();
for (const module of modules) {
const changes = this.plannedChanges.get(module.name) || [];
if (changes.length > 0) {
logger.log('info', `Executing ${module.name} formatter...`);
await module.execute(changes);
}
}
const duration = Date.now() - startTime;
logger.log('info', `Format operations completed in ${duration}ms`);
}
async displayPlan(
plan: IFormatPlan,
detailed: boolean = false,
): Promise<void> {
console.log('\nFormat Plan:');
console.log('━'.repeat(50));
console.log(`Summary: ${plan.summary.totalFiles} files will be changed`);
console.log(`${plan.summary.filesAdded} new files`);
console.log(`${plan.summary.filesModified} modified files`);
console.log(`${plan.summary.filesRemoved} deleted files`);
console.log('');
console.log('Changes by module:');
const changesByModule = new Map<string, IPlannedChange[]>();
for (const change of plan.changes) {
const moduleChanges = changesByModule.get(change.module) || [];
moduleChanges.push(change);
changesByModule.set(change.module, moduleChanges);
}
for (const [module, changes] of changesByModule) {
console.log(
`\n${getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`,
);
for (const change of changes) {
const icon = this.getChangeIcon(change.type);
console.log(` ${icon} ${change.path} - ${change.description}`);
if (detailed && change.type === 'modify') {
const diff = await this.diffReporter.generateDiffForChange(change);
if (diff) {
this.diffReporter.displayDiff(change.path, diff);
}
}
}
}
if (plan.warnings.length > 0) {
console.log('\nWarnings:');
for (const warning of plan.warnings) {
const icon = warning.level === 'error' ? '❌' : '⚠️';
console.log(` ${icon} ${warning.message}`);
}
}
console.log('\n' + '━'.repeat(50));
}
private getChangeIcon(type: 'create' | 'modify' | 'delete'): string {
switch (type) {
case 'create':
return '✅';
case 'modify':
return '✏️';
case 'delete':
return '❌';
}
}
}
+189
View File
@@ -0,0 +1,189 @@
import * as plugins from './mod.plugins.js';
import { logger } from '../gitzone.logging.js';
import { getModuleIcon } from './interfaces.format.js';
export interface IModuleStats {
name: string;
filesProcessed: number;
executionTime: number;
errors: number;
successes: number;
filesCreated: number;
filesModified: number;
filesDeleted: number;
}
export interface IFormatStats {
totalExecutionTime: number;
startTime: number;
endTime: number;
moduleStats: Map<string, IModuleStats>;
overallStats: {
totalFiles: number;
totalCreated: number;
totalModified: number;
totalDeleted: number;
totalErrors: number;
};
}
export class FormatStats {
private stats: IFormatStats;
constructor() {
this.stats = {
totalExecutionTime: 0,
startTime: Date.now(),
endTime: 0,
moduleStats: new Map(),
overallStats: {
totalFiles: 0,
totalCreated: 0,
totalModified: 0,
totalDeleted: 0,
totalErrors: 0,
},
};
}
startModule(moduleName: string): void {
this.stats.moduleStats.set(moduleName, {
name: moduleName,
filesProcessed: 0,
executionTime: 0,
errors: 0,
successes: 0,
filesCreated: 0,
filesModified: 0,
filesDeleted: 0,
});
}
moduleStartTime(moduleName: string): number {
return Date.now();
}
endModule(moduleName: string, startTime: number): void {
const moduleStats = this.stats.moduleStats.get(moduleName);
if (moduleStats) {
moduleStats.executionTime = Date.now() - startTime;
}
}
recordFileOperation(
moduleName: string,
operation: 'create' | 'modify' | 'delete',
success: boolean = true,
): void {
const moduleStats = this.stats.moduleStats.get(moduleName);
if (!moduleStats) return;
moduleStats.filesProcessed++;
if (success) {
moduleStats.successes++;
this.stats.overallStats.totalFiles++;
switch (operation) {
case 'create':
moduleStats.filesCreated++;
this.stats.overallStats.totalCreated++;
break;
case 'modify':
moduleStats.filesModified++;
this.stats.overallStats.totalModified++;
break;
case 'delete':
moduleStats.filesDeleted++;
this.stats.overallStats.totalDeleted++;
break;
}
} else {
moduleStats.errors++;
this.stats.overallStats.totalErrors++;
}
}
finish(): void {
this.stats.endTime = Date.now();
this.stats.totalExecutionTime = this.stats.endTime - this.stats.startTime;
}
displayStats(): void {
console.log('\n📊 Format Operation Statistics:');
console.log('═'.repeat(50));
// Overall stats
console.log('\nOverall Summary:');
console.log(
` Total Execution Time: ${this.formatDuration(this.stats.totalExecutionTime)}`,
);
console.log(` Files Processed: ${this.stats.overallStats.totalFiles}`);
console.log(` • Created: ${this.stats.overallStats.totalCreated}`);
console.log(` • Modified: ${this.stats.overallStats.totalModified}`);
console.log(` • Deleted: ${this.stats.overallStats.totalDeleted}`);
console.log(` Errors: ${this.stats.overallStats.totalErrors}`);
// Module stats
console.log('\nModule Breakdown:');
console.log('─'.repeat(50));
const sortedModules = Array.from(this.stats.moduleStats.values()).sort(
(a, b) => b.filesProcessed - a.filesProcessed,
);
for (const moduleStats of sortedModules) {
console.log(
`\n${getModuleIcon(moduleStats.name)} ${moduleStats.name}:`,
);
console.log(
` Execution Time: ${this.formatDuration(moduleStats.executionTime)}`,
);
console.log(` Files Processed: ${moduleStats.filesProcessed}`);
if (moduleStats.filesCreated > 0) {
console.log(` • Created: ${moduleStats.filesCreated}`);
}
if (moduleStats.filesModified > 0) {
console.log(` • Modified: ${moduleStats.filesModified}`);
}
if (moduleStats.filesDeleted > 0) {
console.log(` • Deleted: ${moduleStats.filesDeleted}`);
}
if (moduleStats.errors > 0) {
console.log(` ❌ Errors: ${moduleStats.errors}`);
}
}
console.log('\n' + '═'.repeat(50));
}
async saveReport(outputPath: string): Promise<void> {
const report = {
timestamp: new Date().toISOString(),
executionTime: this.stats.totalExecutionTime,
overallStats: this.stats.overallStats,
moduleStats: Array.from(this.stats.moduleStats.values()),
};
await plugins.smartfs
.file(outputPath)
.encoding('utf8')
.write(JSON.stringify(report, null, 2));
logger.log('info', `Statistics report saved to ${outputPath}`);
}
private formatDuration(ms: number): string {
if (ms < 1000) {
return `${ms}ms`;
} else if (ms < 60000) {
return `${(ms / 1000).toFixed(1)}s`;
} else {
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
return `${minutes}m ${seconds}s`;
}
}
}
-19
View File
@@ -1,19 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { logger } from '../gitzone.logging.js';
import { Project } from '../classes.project.js';
const filesToDelete = ['defaults.yml', 'yarn.lock', 'package-lock.json', 'tslint.json'];
export const run = async (projectArg: Project) => {
for (const relativeFilePath of filesToDelete) {
const fileExists = plugins.smartfile.fs.fileExistsSync(relativeFilePath);
if (fileExists) {
logger.log('info', `Found ${relativeFilePath}! Removing it!`);
plugins.smartfile.fs.removeSync(plugins.path.join(paths.cwd, relativeFilePath));
} else {
logger.log('info', `Project is free of ${relativeFilePath}`);
}
}
};
-6
View File
@@ -1,6 +0,0 @@
import type { Project } from '../classes.project.js';
import * as plugins from '../plugins.js';
export const run = async (projectArg: Project) => {
const gitzoneConfig = await projectArg.gitzoneConfig;
};
-21
View File
@@ -1,21 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { Project } from '../classes.project.js';
import { logger } from '../gitzone.logging.js';
const gitignorePath = plugins.path.join(paths.cwd, './.gitignore');
export const run = async (projectArg: Project) => {
const gitignoreExists = await plugins.smartfile.fs.fileExists(gitignorePath);
const templateModule = await import('../mod_template/index.js');
const ciTemplate = await templateModule.getTemplate('gitignore');
if (gitignoreExists) {
// lets get the existing gitignore file
const existingGitIgnoreString = plugins.smartfile.fs.toStringSync(gitignorePath);
let customPart = existingGitIgnoreString.split('# custom\n')[1];
customPart ? null : (customPart = '');
}
ciTemplate.writeToDisk(paths.cwd);
logger.log('info', 'Added a .gitignore!');
};
-30
View File
@@ -1,30 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { Project } from '../classes.project.js';
import { logger } from '../gitzone.logging.js';
const incompatibleLicenses: string[] = ['AGPL', 'GPL', 'SSPL'];
export const run = async (projectArg: Project) => {
const nodeModulesInstalled = await plugins.smartfile.fs.isDirectory(
plugins.path.join(paths.cwd, 'node_modules'),
);
if (!nodeModulesInstalled) {
logger.log('warn', 'No node_modules found. Skipping license check');
return;
}
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
const licenseCheckResult = await licenseChecker.excludeLicenseWithinPath(
paths.cwd,
incompatibleLicenses,
);
if (licenseCheckResult.failingModules.length === 0) {
logger.log('info', 'Success -> licenses passed!');
} else {
logger.log('error', 'Error -> licenses failed. Here is why:');
for (const failedModule of licenseCheckResult.failingModules) {
console.log(`${failedModule.name} fails with license ${failedModule.license}`);
}
}
};
-70
View File
@@ -1,70 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import * as gulpFunction from '@push.rocks/gulp-function';
import { Project } from '../classes.project.js';
/**
* runs the npmextra file checking
*/
export const run = async (projectArg: Project) => {
const formatSmartstream = new plugins.smartstream.StreamWrapper([
plugins.smartgulp.src([`npmextra.json`]),
gulpFunction.forEach(async (fileArg: plugins.smartfile.SmartFile) => {
const fileString = fileArg.contents.toString();
const npmextraJson = JSON.parse(fileString);
if (!npmextraJson.gitzone) {
npmextraJson.gitzone = {};
}
const expectedRepoInformation: string[] = [
'projectType',
'module.githost',
'module.gitscope',
'module.gitrepo',
'module.description',
'module.npmPackagename',
'module.license',
];
const interactInstance = new plugins.smartinteract.SmartInteract();
for (const expectedRepoInformationItem of expectedRepoInformation) {
if (!plugins.smartobject.smartGet(npmextraJson.gitzone, expectedRepoInformationItem)) {
interactInstance.addQuestions([
{
message: `What is the value of ${expectedRepoInformationItem}`,
name: expectedRepoInformationItem,
type: 'input',
default: 'undefined variable',
},
]);
}
}
const answerbucket = await interactInstance.runQueue();
for (const expectedRepoInformationItem of expectedRepoInformation) {
const cliProvidedValue = answerbucket.getAnswerFor(expectedRepoInformationItem);
if (cliProvidedValue) {
plugins.smartobject.smartAdd(
npmextraJson.gitzone,
expectedRepoInformationItem,
cliProvidedValue,
);
}
}
// delete obsolete
// tbd
if (!npmextraJson.npmci) {
npmextraJson.npmci = {};
}
fileArg.setContentsFromString(JSON.stringify(npmextraJson, null, 2));
}),
plugins.smartgulp.replace(),
]);
await formatSmartstream.run().catch((error) => {
console.log(error);
});
};
-102
View File
@@ -1,102 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import * as gulpFunction from '@push.rocks/gulp-function';
import { Project } from '../classes.project.js';
import { logger } from '../gitzone.logging.js';
/**
* ensures a certain dependency
*/
const ensureDependency = async (
packageJsonObjectArg: any,
position: 'dep' | 'devDep' | 'everywhere',
constraint: 'exclude' | 'include' | 'latest',
dependencyArg: string,
) => {};
export const run = async (projectArg: Project) => {
const formatStreamWrapper = new plugins.smartstream.StreamWrapper([
plugins.smartgulp.src([`package.json`]),
gulpFunction.forEach(async (fileArg: plugins.smartfile.SmartFile) => {
const npmextraConfig = new plugins.npmextra.Npmextra(paths.cwd);
const gitzoneData: any = npmextraConfig.dataFor('gitzone', {});
const fileString = fileArg.contents.toString();
const packageJson = JSON.parse(fileString);
// metadata
packageJson.repository = {
type: 'git',
url: `git+https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}.git`,
};
(packageJson.bugs = {
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}/issues`,
}),
(packageJson.homepage = `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}#readme`);
// Check for module type
if (!packageJson.type) {
logger.log('info', `setting packageJson.type to "module"`);
packageJson.type = 'module';
}
// Check for private or public
if (packageJson.private !== undefined) {
logger.log('info', 'Success -> found private/public info in package.json!');
} else {
logger.log('error', 'found no private boolean! Setting it to private for now!');
packageJson.private = true;
}
// Check for license
if (packageJson.license) {
logger.log('info', 'Success -> found license in package.json!');
} else {
logger.log('error', 'found no license! Setting it to UNLICENSED for now!');
packageJson.license = 'UNLICENSED';
}
// Check for build script
if (packageJson.scripts.build) {
logger.log('info', 'Success -> found build script in package.json!');
} else {
logger.log('error', 'found no build script! Putting a placeholder there for now!');
packageJson.scripts.build = `echo "Not needed for now"`;
}
// Check for buildDocs script
if (!packageJson.scripts.buildDocs) {
logger.log('info', 'found no buildDocs script! Putting tsdoc script there now.');
packageJson.scripts.buildDocs = `tsdoc`;
}
// check for files
packageJson.files = [
'ts/**/*',
'ts_web/**/*',
'dist/**/*',
'dist_*/**/*',
'dist_ts/**/*',
'dist_ts_web/**/*',
'assets/**/*',
'cli.js',
'npmextra.json',
'readme.md',
];
// check for dependencies
await ensureDependency(packageJson, 'devDep', 'latest', '@push.rocks/tapbundle');
await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tstest');
await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tsbuild');
// exclude
// TODO
fileArg.setContentsFromString(JSON.stringify(packageJson, null, 2));
}),
plugins.smartgulp.replace(),
]);
await formatStreamWrapper.run().catch((error) => {
console.log(error);
});
};
-58
View File
@@ -1,58 +0,0 @@
import * as plugins from './mod.plugins.js';
import prettier from 'prettier';
import { Project } from '../classes.project.js';
import { logger } from '../gitzone.logging.js';
const prettierDefaultTypeScriptConfig: prettier.Options = {
printWidth: 100,
parser: 'typescript',
singleQuote: true,
};
const prettierDefaultMarkdownConfig: prettier.Options = {
singleQuote: true,
printWidth: 100,
parser: 'markdown',
};
const filesToFormat = [`ts/**/*.ts`, `test/**/*.ts`, `readme.md`, `docs/**/*.md`];
const choosePrettierConfig = (fileArg: plugins.smartfile.SmartFile) => {
switch (fileArg.parsedPath.ext) {
case '.ts':
return prettierDefaultTypeScriptConfig;
case '.md':
return prettierDefaultMarkdownConfig;
default:
return {};
}
};
const prettierTypeScriptPipestop = plugins.through2.obj(
async (fileArg: plugins.smartfile.SmartFile, enc, cb) => {
const fileString = fileArg.contentBuffer.toString();
const chosenConfig = choosePrettierConfig(fileArg);
const filePasses = await prettier.check(fileString, chosenConfig);
if (filePasses) {
logger.log('info', `OK! -> ${fileArg.path} passes!`);
cb(null);
} else {
logger.log('info', `${fileArg.path} is being reformated!`);
const formatedFileString = await prettier.format(fileString, chosenConfig);
fileArg.setContentsFromString(formatedFileString);
cb(null, fileArg);
}
},
);
export const run = async (projectArg: Project) => {
const formatStreamWrapper = new plugins.smartstream.StreamWrapper([
plugins.smartgulp.src(filesToFormat),
prettierTypeScriptPipestop,
plugins.smartgulp.replace(),
]);
await formatStreamWrapper.run().catch((error) => {
console.log(error);
});
};
-31
View File
@@ -1,31 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
export const run = async () => {
const readmePath = plugins.path.join(paths.cwd, 'readme.md');
const readmeHintsPath = plugins.path.join(paths.cwd, 'readme.hints.md');
// Check and initialize readme.md if it doesn't exist
const readmeExists = await plugins.smartfile.fs.fileExists(readmePath);
if (!readmeExists) {
await plugins.smartfile.fs.toFs(
'# Project Readme\n\nThis is the initial readme file.',
readmePath,
);
console.log('Initialized readme.md');
} else {
console.log('readme.md already exists');
}
// Check and initialize readme.hints.md if it doesn't exist
const readmeHintsExists = await plugins.smartfile.fs.fileExists(readmeHintsPath);
if (!readmeHintsExists) {
await plugins.smartfile.fs.toFs(
'# Project Readme Hints\n\nThis is the initial readme hints file.',
readmeHintsPath,
);
console.log('Initialized readme.hints.md');
} else {
console.log('readme.hints.md already exists');
}
};
-71
View File
@@ -1,71 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { logger } from '../gitzone.logging.js';
import { Project } from '../classes.project.js';
/**
* takes care of updating files from templates
*/
export const run = async (project: Project) => {
const templateModule = await import('../mod_template/index.js');
// update tslint
// getting template
const tslintTemplate = await templateModule.getTemplate('tslint');
await tslintTemplate.writeToDisk(paths.cwd);
logger.log('info', 'Updated tslint.json!');
// update vscode
const vscodeTemplate = await templateModule.getTemplate('vscode');
await vscodeTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated vscode template!`);
// update gitlab ci and Dockerfile
switch (project.gitzoneConfig.data.projectType) {
case 'npm':
case 'wcc':
if (project.gitzoneConfig.data.npmciOptions.npmAccessLevel === 'public') {
const ciTemplateDefault = await templateModule.getTemplate('ci_default');
ciTemplateDefault.writeToDisk(paths.cwd);
} else {
const ciTemplateDefault = await templateModule.getTemplate('ci_default_private');
ciTemplateDefault.writeToDisk(paths.cwd);
}
logger.log('info', 'Updated .gitlabci.yml!');
break;
case 'service':
case 'website':
const ciTemplateDocker = await templateModule.getTemplate('ci_docker');
await ciTemplateDocker.writeToDisk(paths.cwd);
logger.log('info', 'Updated .gitlabci.yml!');
// lets care about docker
const dockerTemplate = await templateModule.getTemplate('dockerfile_service');
dockerTemplate.writeToDisk(paths.cwd);
logger.log('info', 'Updated Dockerfile!');
// lets care about cli
const cliTemplate = await templateModule.getTemplate('cli');
await cliTemplate.writeToDisk(paths.cwd);
logger.log('info', 'Updated cli.ts.js and cli.js!');
break;
default:
break;
}
// update html
if (project.gitzoneConfig.data.projectType === 'website') {
const websiteUpdateTemplate = await templateModule.getTemplate('website_update');
await websiteUpdateTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated html for website!`);
} else if (project.gitzoneConfig.data.projectType === 'service') {
const websiteUpdateTemplate = await templateModule.getTemplate('service_update');
await websiteUpdateTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated html for element template!`);
} else if (project.gitzoneConfig.data.projectType === 'wcc') {
const wccUpdateTemplate = await templateModule.getTemplate('wcc_update');
await wccUpdateTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated html for wcc template!`);
}
};
-24
View File
@@ -1,24 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { logger } from '../gitzone.logging.js';
import { Project } from '../classes.project.js';
export const run = async (projectArg: Project) => {
// lets care about tsconfig.json
logger.log('info', 'Formatting tsconfig.json...');
const tsconfigSmartfile = await plugins.smartfile.SmartFile.fromFilePath(plugins.path.join(paths.cwd, 'tsconfig.json'));
const tsconfigObject = JSON.parse(tsconfigSmartfile.contentBuffer.toString());
tsconfigObject.compilerOptions = tsconfigObject.compilerOptions || {};
tsconfigObject.compilerOptions.baseUrl = '.';
tsconfigObject.compilerOptions.paths = {};
const tsPublishMod = await import('@git.zone/tspublish');
const tsPublishInstance = new tsPublishMod.TsPublish();
const publishModules = await tsPublishInstance.getModuleSubDirs(paths.cwd);
for (const publishModule of Object.keys(publishModules)) {
const publishConfig = publishModules[publishModule];
tsconfigObject.compilerOptions.paths[`${publishConfig.name}`] = [`./${publishModule}/index.js`];
}
tsconfigSmartfile.setContentsFromString(JSON.stringify(tsconfigObject, null, 2));
await tsconfigSmartfile.write();
};
@@ -0,0 +1,43 @@
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
export class CleanupFormatter extends BaseFormatter {
get name(): string {
return 'cleanup';
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
// List of files to remove
const filesToRemove = [
'yarn.lock',
'package-lock.json',
'tslint.json',
'defaults.yml',
];
for (const file of filesToRemove) {
const exists = await plugins.smartfs.file(file).exists();
if (exists) {
changes.push({
type: 'delete',
path: file,
module: this.name,
description: `Remove obsolete file`,
});
}
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
switch (change.type) {
case 'delete':
await this.deleteFile(change.path);
break;
}
}
}
+113
View File
@@ -0,0 +1,113 @@
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import { logger, logVerbose } from '../../gitzone.logging.js';
interface ICopyPattern {
from: string;
to: string;
preservePath?: boolean;
}
export class CopyFormatter extends BaseFormatter {
get name(): string {
return 'copy';
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
// Get copy configuration from .smartconfig.json
const smartconfigInstance = new plugins.smartconfig.Smartconfig();
const copyConfig = smartconfigInstance.dataFor<{ patterns: ICopyPattern[] }>(
'gitzone.format.copy',
{ patterns: [] },
);
if (!copyConfig.patterns || copyConfig.patterns.length === 0) {
logVerbose('No copy patterns configured in .smartconfig.json');
return changes;
}
for (const pattern of copyConfig.patterns) {
if (!pattern.from || !pattern.to) {
logVerbose('Invalid copy pattern - missing "from" or "to" field');
continue;
}
try {
// Handle glob patterns
const entries = await plugins.smartfs
.directory('.')
.recursive()
.filter(pattern.from)
.list();
const files = entries.map((entry) => entry.path);
for (const file of files) {
const sourcePath = file;
let destPath = pattern.to;
// If destination is a directory, preserve filename
if (pattern.to.endsWith('/')) {
const filename = plugins.path.basename(file);
destPath = plugins.path.join(pattern.to, filename);
}
// Handle template variables in destination path
if (pattern.preservePath) {
const relativePath = plugins.path.relative(
plugins.path.dirname(pattern.from.replace(/\*/g, '')),
file,
);
destPath = plugins.path.join(pattern.to, relativePath);
}
// Read source content
const content = (await plugins.smartfs
.file(sourcePath)
.encoding('utf8')
.read()) as string;
// Check if destination exists and has same content
let needsCopy = true;
const destExists = await plugins.smartfs.file(destPath).exists();
if (destExists) {
const existingContent = (await plugins.smartfs
.file(destPath)
.encoding('utf8')
.read()) as string;
if (existingContent === content) {
needsCopy = false;
}
}
if (needsCopy) {
changes.push({
type: destExists ? 'modify' : 'create',
path: destPath,
module: this.name,
description: `Copy from ${sourcePath}`,
content: content,
});
}
}
} catch (error) {
logVerbose(`Failed to process pattern ${pattern.from}: ${error.message}`);
}
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
if (!change.content) return;
if (change.type === 'create') {
await this.createFile(change.path, change.content);
} else {
await this.modifyFile(change.path, change.content);
}
logger.log('info', `Copied to ${change.path}`);
}
}
@@ -0,0 +1,107 @@
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger } from '../../gitzone.logging.js';
export class GitignoreFormatter extends BaseFormatter {
get name(): string {
return 'gitignore';
}
/**
* Read the standard gitignore template from the asset file,
* stripping the YAML frontmatter.
*/
private async getStandardTemplate(): Promise<string> {
const templatePath = plugins.path.join(paths.templatesDir, 'gitignore', '_gitignore');
const raw = (await plugins.smartfs
.file(templatePath)
.encoding('utf8')
.read()) as string;
// Strip YAML frontmatter (---\n...\n---)
const frontmatterEnd = raw.indexOf('---', 3);
if (frontmatterEnd !== -1) {
return raw.slice(frontmatterEnd + 3).trimStart();
}
return raw;
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
const gitignorePath = '.gitignore';
const standardTemplate = await this.getStandardTemplate();
// Check if file exists and extract custom content
let customContent = '';
const exists = await plugins.smartfs.file(gitignorePath).exists();
if (exists) {
const existingContent = (await plugins.smartfs
.file(gitignorePath)
.encoding('utf8')
.read()) as string;
// Extract custom section content
const customMarkers = ['#------# custom', '# custom'];
for (const marker of customMarkers) {
const splitResult = existingContent.split(marker);
if (splitResult.length > 1) {
customContent = splitResult[1].trim();
break;
}
}
}
// Compute new content
let newContent = standardTemplate;
if (customContent) {
newContent = standardTemplate + '\n' + customContent + '\n';
} else {
newContent = standardTemplate + '\n';
}
// Read current content to compare
let currentContent = '';
if (exists) {
currentContent = (await plugins.smartfs
.file(gitignorePath)
.encoding('utf8')
.read()) as string;
}
if (!exists) {
changes.push({
type: 'create',
path: gitignorePath,
module: this.name,
description: 'Create .gitignore',
content: newContent,
});
} else if (newContent !== currentContent) {
changes.push({
type: 'modify',
path: gitignorePath,
module: this.name,
description: 'Update .gitignore (preserving custom section)',
content: newContent,
});
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
if (!change.content) return;
if (change.type === 'create') {
await this.createFile(change.path, change.content);
logger.log('info', 'Created .gitignore');
} else if (change.type === 'modify') {
await this.modifyFile(change.path, change.content);
logger.log('info', 'Updated .gitignore (preserved custom section)');
}
}
}
@@ -0,0 +1,62 @@
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger } from '../../gitzone.logging.js';
const INCOMPATIBLE_LICENSES: string[] = ['AGPL', 'GPL', 'SSPL'];
export class LicenseFormatter extends BaseFormatter {
get name(): string {
return 'license';
}
async analyze(): Promise<IPlannedChange[]> {
// License formatter only checks for incompatible licenses
// It does not modify any files, so return empty array
// The actual check happens in execute() for reporting purposes
return [];
}
async execute(changes: IPlannedChange[]): Promise<void> {
const startTime = this.stats.moduleStartTime(this.name);
this.stats.startModule(this.name);
try {
// Check if node_modules exists
const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
const nodeModulesExists = await plugins.smartfs
.directory(nodeModulesPath)
.exists();
if (!nodeModulesExists) {
logger.log('warn', 'No node_modules found. Skipping license check');
return;
}
// Run license check
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
const licenseCheckResult = await licenseChecker.excludeLicenseWithinPath(
paths.cwd,
INCOMPATIBLE_LICENSES,
);
if (licenseCheckResult.failingModules.length === 0) {
logger.log('info', 'License check passed - no incompatible licenses found');
} else {
logger.log('error', 'License check failed - incompatible licenses found:');
for (const failedModule of licenseCheckResult.failingModules) {
console.log(
` ${failedModule.name} has license ${failedModule.license}`,
);
}
}
} finally {
this.stats.endModule(this.name, startTime);
}
}
async applyChange(change: IPlannedChange): Promise<void> {
// No file changes for license formatter
}
}
@@ -0,0 +1,122 @@
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger, logVerbose } from '../../gitzone.logging.js';
export class PackageJsonFormatter extends BaseFormatter {
get name(): string {
return 'packagejson';
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
const packageJsonPath = 'package.json';
// Check if file exists
const exists = await plugins.smartfs.file(packageJsonPath).exists();
if (!exists) {
logVerbose('package.json does not exist, skipping');
return changes;
}
// Read current content
const currentContent = (await plugins.smartfs
.file(packageJsonPath)
.encoding('utf8')
.read()) as string;
// Parse and compute new content
const packageJson = JSON.parse(currentContent);
// Get gitzone config from smartconfig
const smartconfigInstance = new plugins.smartconfig.Smartconfig(paths.cwd);
const gitzoneData: any = smartconfigInstance.dataFor('@git.zone/cli', {});
// Set metadata from gitzone config
if (gitzoneData.module) {
packageJson.repository = {
type: 'git',
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}.git`,
};
packageJson.bugs = {
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}/issues`,
};
packageJson.homepage = `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}#readme`;
}
// Ensure module type
if (!packageJson.type) {
packageJson.type = 'module';
}
// Ensure private field exists
if (packageJson.private === undefined) {
packageJson.private = true;
}
// Ensure license field exists
if (!packageJson.license) {
packageJson.license = 'UNLICENSED';
}
// Ensure scripts object exists
if (!packageJson.scripts) {
packageJson.scripts = {};
}
// Ensure build script exists
if (!packageJson.scripts.build) {
packageJson.scripts.build = `echo "Not needed for now"`;
}
// Set files array
packageJson.files = [
'ts/**/*',
'ts_web/**/*',
'dist/**/*',
'dist_*/**/*',
'dist_ts/**/*',
'dist_ts_web/**/*',
'assets/**/*',
'cli.js',
'.smartconfig.json',
'readme.md',
];
// Set pnpm overrides from assets
try {
const overridesContent = (await plugins.smartfs
.file(plugins.path.join(paths.assetsDir, 'overrides.json'))
.encoding('utf8')
.read()) as string;
const overrides = JSON.parse(overridesContent);
packageJson.pnpm = packageJson.pnpm || {};
packageJson.pnpm.overrides = overrides;
} catch (error) {
logVerbose(`Could not read overrides.json: ${error.message}`);
}
const newContent = JSON.stringify(packageJson, null, 2);
// Only add change if content differs
if (newContent !== currentContent) {
changes.push({
type: 'modify',
path: packageJsonPath,
module: this.name,
description: 'Format package.json',
content: newContent,
});
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
if (change.type !== 'modify' || !change.content) return;
await this.modifyFile(change.path, change.content);
logger.log('info', 'Updated package.json');
}
}
@@ -0,0 +1,285 @@
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange, ICheckResult } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import { logger, logVerbose } from '../../gitzone.logging.js';
export class PrettierFormatter extends BaseFormatter {
get name(): string {
return 'prettier';
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
// Define directories to format (TypeScript directories by default)
const includeDirs = ['ts', 'ts_*', 'test', 'tests'];
// File extensions to format
const extensions = '{ts,tsx,js,jsx,json,md,css,scss,html,xml,yaml,yml}';
// Also format root-level config files
const rootConfigFiles = [
'package.json',
'tsconfig.json',
'.smartconfig.json',
'.prettierrc',
'.prettierrc.json',
'.prettierrc.js',
'readme.md',
'README.md',
'changelog.md',
'CHANGELOG.md',
// Skip files without extensions as prettier can't infer parser
// 'license',
// 'LICENSE',
'*.md',
];
// Collect all files to format
const allFiles: string[] = [];
// Add files from TypeScript directories
for (const dir of includeDirs) {
try {
const globPattern = `${dir}/**/*.${extensions}`;
const dirEntries = await plugins.smartfs
.directory('.')
.recursive()
.filter(globPattern)
.list();
const dirFiles = dirEntries.map((entry) => entry.path);
// Filter out files in excluded directories
const filteredFiles = dirFiles.filter((f) =>
!f.includes('node_modules/') &&
!f.includes('.nogit/') &&
!f.includes('.git/')
);
allFiles.push(...filteredFiles);
} catch (error) {
logVerbose(`Skipping directory ${dir}: ${error.message}`);
}
}
// Add root config files (only check root level, no recursive needed)
for (const pattern of rootConfigFiles) {
try {
const rootEntries = await plugins.smartfs
.directory('.')
.filter(pattern)
.list();
const rootFiles = rootEntries.map((entry) => entry.path);
// Only include files at root level (no slashes in path)
const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
allFiles.push(...rootLevelFiles);
} catch (error) {
logVerbose(`Skipping pattern ${pattern}: ${error.message}`);
}
}
// Remove duplicates
const uniqueFiles = [...new Set(allFiles)];
// Ensure we only process actual files (not directories)
const validFiles: string[] = [];
for (const file of uniqueFiles) {
try {
const stats = await plugins.smartfs.file(file).stat();
if (!stats.isDirectory) {
validFiles.push(file);
}
} catch (error) {
// Skip files that can't be accessed
logVerbose(`Skipping ${file} - cannot access: ${error.message}`);
}
}
for (const file of validFiles) {
changes.push({
type: 'modify',
path: file,
module: this.name,
description: 'Format with Prettier',
});
}
logger.log('info', `Found ${changes.length} files to format with Prettier`);
return changes;
}
async execute(changes: IPlannedChange[]): Promise<void> {
const startTime = this.stats.moduleStartTime(this.name);
this.stats.startModule(this.name);
try {
await this.preExecute();
logVerbose(`Processing ${changes.length} files sequentially`);
// Process files sequentially to avoid prettier cache/state issues
for (let i = 0; i < changes.length; i++) {
const change = changes[i];
logVerbose(
`Processing file ${i + 1}/${changes.length}: ${change.path}`,
);
try {
await this.applyChange(change);
this.stats.recordFileOperation(this.name, change.type, true);
} catch (error) {
this.stats.recordFileOperation(this.name, change.type, false);
logger.log(
'error',
`Failed to format ${change.path}: ${error.message}`,
);
// Don't throw - continue with other files
}
}
await this.postExecute();
} catch (error) {
// Rollback removed - no longer tracking operations
throw error;
} finally {
this.stats.endModule(this.name, startTime);
}
}
async applyChange(change: IPlannedChange): Promise<void> {
if (change.type !== 'modify') return;
try {
// Validate the path before processing
if (!change.path || change.path.trim() === '') {
logger.log(
'error',
`Invalid empty path in change: ${JSON.stringify(change)}`,
);
throw new Error('Invalid empty path');
}
// Read current content
const content = (await plugins.smartfs
.file(change.path)
.encoding('utf8')
.read()) as string;
// Format with prettier
const prettier = await import('prettier');
// Skip files that prettier can't parse without explicit parser
const fileExt = plugins.path.extname(change.path).toLowerCase();
if (!fileExt || fileExt === '') {
// Files without extensions need explicit parser
logVerbose(
`Skipping ${change.path} - no file extension for parser inference`,
);
return;
}
try {
const formatted = await prettier.format(content, {
filepath: change.path,
...(await this.getPrettierConfig()),
});
// Only write if content actually changed
if (formatted !== content) {
// Debug: log the path being written
logVerbose(`Writing formatted content to: ${change.path}`);
await this.modifyFile(change.path, formatted);
logVerbose(`Formatted ${change.path}`);
} else {
logVerbose(`No formatting changes for ${change.path}`);
}
} catch (prettierError) {
// Check if it's a parser error
if (
prettierError.message &&
prettierError.message.includes('No parser could be inferred')
) {
logVerbose(`Skipping ${change.path} - ${prettierError.message}`);
return; // Skip this file silently
}
throw prettierError;
}
} catch (error) {
// Log the full error stack for debugging mkdir issues
if (error.message && error.message.includes('mkdir')) {
logger.log(
'error',
`Failed to format ${change.path}: ${error.message}`,
);
logger.log('error', `Error stack: ${error.stack}`);
} else {
logger.log(
'error',
`Failed to format ${change.path}: ${error.message}`,
);
}
throw error;
}
}
private async getPrettierConfig(): Promise<any> {
// Try to load prettier config from the project
const prettierConfig = new plugins.smartconfig.Smartconfig();
return prettierConfig.dataFor('prettier', {
// Default prettier config
singleQuote: true,
trailingComma: 'all',
printWidth: 80,
tabWidth: 2,
semi: true,
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,
};
}
}
@@ -0,0 +1,55 @@
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import { logger } from '../../gitzone.logging.js';
const DEFAULT_README_CONTENT = `# Project Readme
This is the initial readme file.`;
const DEFAULT_README_HINTS_CONTENT = `# Project Readme Hints
This is the initial readme hints file.`;
export class ReadmeFormatter extends BaseFormatter {
get name(): string {
return 'readme';
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
// Check readme.md
const readmeExists = await plugins.smartfs.file('readme.md').exists();
if (!readmeExists) {
changes.push({
type: 'create',
path: 'readme.md',
module: this.name,
description: '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> {
if (change.type !== 'create' || !change.content) return;
await this.createFile(change.path, change.content);
logger.log('info', `Created ${change.path}`);
}
}
@@ -0,0 +1,126 @@
import { BaseFormatter } from "../classes.baseformatter.js";
import type { IPlannedChange } from "../interfaces.format.js";
import * as plugins from "../mod.plugins.js";
import { logger, logVerbose } from "../../gitzone.logging.js";
import { migrateSmartconfigData } from "../../helpers.smartconfigmigrations.js";
const CONFIG_FILE = ".smartconfig.json";
export class SmartconfigFormatter extends BaseFormatter {
get name(): string {
return "smartconfig";
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
// File rename (npmextra.json/smartconfig.json → .smartconfig.json)
// is handled by the orchestrator before analysis.
// This formatter only operates on .smartconfig.json.
const exists = await plugins.smartfs.file(CONFIG_FILE).exists();
if (!exists) {
logVerbose(".smartconfig.json does not exist, skipping");
return changes;
}
const currentContent = (await plugins.smartfs
.file(CONFIG_FILE)
.encoding("utf8")
.read()) as string;
const smartconfigJson = JSON.parse(currentContent);
migrateSmartconfigData(smartconfigJson);
// Ensure namespaces exist
if (!smartconfigJson["@git.zone/cli"]) {
smartconfigJson["@git.zone/cli"] = {};
}
if (!smartconfigJson["@ship.zone/szci"]) {
smartconfigJson["@ship.zone/szci"] = {};
}
const newContent = JSON.stringify(smartconfigJson, null, 2);
if (newContent !== currentContent) {
changes.push({
type: "modify",
path: CONFIG_FILE,
module: this.name,
description: "Migrate and format .smartconfig.json",
content: newContent,
});
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
if (change.type !== "modify" || !change.content) return;
const smartconfigJson = JSON.parse(change.content);
// Check for missing required module information
const expectedRepoInformation: string[] = [
"projectType",
"module.githost",
"module.gitscope",
"module.gitrepo",
"module.description",
"module.npmPackagename",
"module.license",
];
const interactInstance = new plugins.smartinteract.SmartInteract();
const missingRepoInformation = expectedRepoInformation.filter(
(expectedRepoInformationItem) => {
return !plugins.smartobject.smartGet(
smartconfigJson["@git.zone/cli"],
expectedRepoInformationItem,
);
},
);
if (missingRepoInformation.length > 0 && !this.context.isInteractive()) {
throw new Error(
`Missing required .smartconfig.json fields: ${missingRepoInformation.join(", ")}`,
);
}
for (const expectedRepoInformationItem of expectedRepoInformation) {
if (
!plugins.smartobject.smartGet(
smartconfigJson["@git.zone/cli"],
expectedRepoInformationItem,
)
) {
interactInstance.addQuestions([
{
message: `What is the value of ${expectedRepoInformationItem}`,
name: expectedRepoInformationItem,
type: "input",
default: "undefined variable",
},
]);
}
}
const answerbucket = await interactInstance.runQueue();
for (const expectedRepoInformationItem of expectedRepoInformation) {
const cliProvidedValue = answerbucket.getAnswerFor(
expectedRepoInformationItem,
);
if (cliProvidedValue) {
plugins.smartobject.smartAdd(
smartconfigJson["@git.zone/cli"],
expectedRepoInformationItem,
cliProvidedValue,
);
}
}
const finalContent = JSON.stringify(smartconfigJson, null, 2);
await this.modifyFile(change.path, finalContent);
logger.log("info", "Updated .smartconfig.json");
}
}
@@ -0,0 +1,167 @@
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger, logVerbose } from '../../gitzone.logging.js';
export class TemplatesFormatter extends BaseFormatter {
get name(): string {
return 'templates';
}
/**
* Render a template directory through smartscaf and return a map of path → content.
*/
private async renderTemplate(templateName: string): Promise<Map<string, string>> {
const templateDir = plugins.path.join(paths.templatesDir, templateName);
const scafTemplate = new plugins.smartscaf.ScafTemplate(templateDir);
await scafTemplate.readTemplateFromDir();
const gitzoneData = this.project.gitzoneConfig?.data;
if (gitzoneData) {
await scafTemplate.supplyVariables({
module: gitzoneData.module,
projectType: gitzoneData.projectType,
});
}
const renderedFiles = await scafTemplate.renderToMemory();
const fileMap = new Map<string, string>();
for (const file of renderedFiles) {
fileMap.set(file.path, file.contents.toString());
}
return fileMap;
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
const project = this.project;
const projectType = project.gitzoneConfig?.data?.projectType;
// VSCode template - for all projects
const vscodeChanges = await this.analyzeTemplate('vscode', [
{ templatePath: '.vscode/settings.json', destPath: '.vscode/settings.json' },
{ templatePath: '.vscode/launch.json', destPath: '.vscode/launch.json' },
]);
changes.push(...vscodeChanges);
// CI and other templates based on projectType
switch (projectType) {
case 'npm':
case 'wcc':
const accessLevel = (project.gitzoneConfig?.data as any)?.release?.accessLevel
|| project.gitzoneConfig?.data?.npmciOptions?.npmAccessLevel;
const ciTemplate = accessLevel === 'public' ? 'ci_default' : 'ci_default_private';
const ciChanges = await this.analyzeTemplate(ciTemplate, [
{ templatePath: '.gitea/workflows/default_nottags.yaml', destPath: '.gitea/workflows/default_nottags.yaml' },
{ templatePath: '.gitea/workflows/default_tags.yaml', destPath: '.gitea/workflows/default_tags.yaml' },
]);
changes.push(...ciChanges);
break;
case 'service':
case 'website':
const dockerCiChanges = await this.analyzeTemplate('ci_docker', [
{ templatePath: '.gitea/workflows/docker_nottags.yaml', destPath: '.gitea/workflows/docker_nottags.yaml' },
{ templatePath: '.gitea/workflows/docker_tags.yaml', destPath: '.gitea/workflows/docker_tags.yaml' },
]);
changes.push(...dockerCiChanges);
const dockerfileChanges = await this.analyzeTemplate('dockerfile_service', [
{ templatePath: 'Dockerfile', destPath: 'Dockerfile' },
{ templatePath: 'dockerignore', destPath: '.dockerignore' },
]);
changes.push(...dockerfileChanges);
const cliChanges = await this.analyzeTemplate('cli', [
{ templatePath: 'cli.js', destPath: 'cli.js' },
{ templatePath: 'cli.ts.js', destPath: 'cli.ts.js' },
]);
changes.push(...cliChanges);
break;
}
// Update templates based on projectType
if (projectType === 'website') {
const websiteChanges = await this.analyzeTemplate('website_update', [
{ templatePath: 'html/index.html', destPath: 'html/index.html' },
]);
changes.push(...websiteChanges);
} else if (projectType === 'wcc') {
const wccChanges = await this.analyzeTemplate('wcc_update', [
{ templatePath: 'html/index.html', destPath: 'html/index.html' },
{ templatePath: 'html/index.ts', destPath: 'html/index.ts' },
]);
changes.push(...wccChanges);
}
return changes;
}
private async analyzeTemplate(
templateName: string,
files: Array<{ templatePath: string; destPath: string }>,
): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
const templateDir = plugins.path.join(paths.templatesDir, templateName);
const templateExists = await plugins.smartfs.directory(templateDir).exists();
if (!templateExists) {
logVerbose(`Template ${templateName} not found`);
return changes;
}
let renderedFiles: Map<string, string>;
try {
renderedFiles = await this.renderTemplate(templateName);
} catch (error) {
logVerbose(`Failed to render template ${templateName}: ${error.message}`);
return changes;
}
for (const file of files) {
// Look up by templatePath first, then destPath (frontmatter may rename files)
const processedContent = renderedFiles.get(file.templatePath)
|| renderedFiles.get(file.destPath);
if (!processedContent) {
logVerbose(`Template file ${file.templatePath} not found in rendered output`);
continue;
}
const destExists = await plugins.smartfs.file(file.destPath).exists();
let currentContent = '';
if (destExists) {
currentContent = (await plugins.smartfs
.file(file.destPath)
.encoding('utf8')
.read()) as string;
}
if (processedContent !== currentContent) {
changes.push({
type: destExists ? 'modify' : 'create',
path: file.destPath,
module: this.name,
description: `Apply template ${templateName}/${file.templatePath}`,
content: processedContent,
});
}
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
if (!change.content) return;
if (change.type === 'create') {
await this.createFile(change.path, change.content);
} else {
await this.modifyFile(change.path, change.content);
}
logger.log('info', `Applied template to ${change.path}`);
}
}
@@ -0,0 +1,76 @@
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger, logVerbose } from '../../gitzone.logging.js';
export class TsconfigFormatter extends BaseFormatter {
get name(): string {
return 'tsconfig';
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
const tsconfigPath = 'tsconfig.json';
// Check if file exists
const exists = await plugins.smartfs.file(tsconfigPath).exists();
if (!exists) {
logVerbose('tsconfig.json does not exist, skipping');
return changes;
}
// Read current content
const currentContent = (await plugins.smartfs
.file(tsconfigPath)
.encoding('utf8')
.read()) as string;
// Parse and compute new content
const tsconfigObject = JSON.parse(currentContent);
tsconfigObject.compilerOptions = tsconfigObject.compilerOptions || {};
tsconfigObject.compilerOptions.baseUrl = '.';
const existingPaths = tsconfigObject.compilerOptions.paths || {};
// Get module paths from tspublish, merging with existing custom paths
const tspublishPaths: Record<string, string[]> = {};
try {
const tsPublishMod = await import('@git.zone/tspublish');
const tsPublishInstance = new tsPublishMod.TsPublish();
const publishModules = await tsPublishInstance.getModuleSubDirs(paths.cwd);
for (const publishModule of Object.keys(publishModules)) {
const publishConfig = publishModules[publishModule];
tspublishPaths[`${publishConfig.name}`] = [
`./${publishModule}/index.js`,
];
}
} catch (error) {
logVerbose(`Could not get tspublish modules: ${error.message}`);
}
tsconfigObject.compilerOptions.paths = { ...existingPaths, ...tspublishPaths };
const newContent = JSON.stringify(tsconfigObject, null, 2);
// Only add change if content differs
if (newContent !== currentContent) {
changes.push({
type: 'modify',
path: tsconfigPath,
module: this.name,
description: 'Format tsconfig.json with path mappings',
content: newContent,
});
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
if (change.type !== 'modify' || !change.content) return;
await this.modifyFile(change.path, change.content);
logger.log('info', 'Updated tsconfig.json');
}
}
+429 -35
View File
@@ -1,40 +1,434 @@
import * as plugins from './mod.plugins.js'; import * as plugins from "./mod.plugins.js";
import { Project } from '../classes.project.js'; import { Project } from "../classes.project.js";
import { FormatContext } from "./classes.formatcontext.js";
import { FormatPlanner } from "./classes.formatplanner.js";
import { BaseFormatter } from "./classes.baseformatter.js";
import { logger, setVerboseMode } from "../gitzone.logging.js";
import type { ICliMode } from "../helpers.climode.js";
import {
getCliMode,
printJson,
runWithSuppressedOutput,
} from "../helpers.climode.js";
import { getCliConfigValue } from "../helpers.smartconfig.js";
export let run = async (writeArg: boolean = true): Promise<any> => { import { CleanupFormatter } from "./formatters/cleanup.formatter.js";
const project = await Project.fromCwd(); import { SmartconfigFormatter } from "./formatters/smartconfig.formatter.js";
import { LicenseFormatter } from "./formatters/license.formatter.js";
import { PackageJsonFormatter } from "./formatters/packagejson.formatter.js";
import { TemplatesFormatter } from "./formatters/templates.formatter.js";
import { GitignoreFormatter } from "./formatters/gitignore.formatter.js";
import { TsconfigFormatter } from "./formatters/tsconfig.formatter.js";
import { PrettierFormatter } from "./formatters/prettier.formatter.js";
import { ReadmeFormatter } from "./formatters/readme.formatter.js";
import { CopyFormatter } from "./formatters/copy.formatter.js";
// cleanup /**
const formatCleanup = await import('./format.cleanup.js'); * Rename npmextra.json or smartconfig.json to .smartconfig.json
await formatCleanup.run(project); * before any formatter tries to read config.
*/
async function migrateConfigFile(allowWrite: boolean): Promise<void> {
const target = ".smartconfig.json";
const targetExists = await plugins.smartfs.file(target).exists();
if (targetExists) return;
// npmextra for (const oldName of ["smartconfig.json", "npmextra.json"]) {
const formatNpmextra = await import('./format.npmextra.js'); const exists = await plugins.smartfs.file(oldName).exists();
await formatNpmextra.run(project); if (exists) {
if (!allowWrite) {
return;
}
const content = (await plugins.smartfs
.file(oldName)
.encoding("utf8")
.read()) as string;
await plugins.smartfs.file(`./${target}`).encoding("utf8").write(content);
await plugins.smartfs.file(oldName).delete();
logger.log("info", `Migrated ${oldName} to ${target}`);
return;
}
}
}
// license // Shared formatter class map used by both run() and runFormatter()
const formatLicense = await import('./format.license.js'); const formatterMap: Record<
await formatLicense.run(project); string,
new (ctx: FormatContext, proj: Project) => BaseFormatter
// format package.json > = {
const formatPackageJson = await import('./format.packagejson.js'); cleanup: CleanupFormatter,
await formatPackageJson.run(project); smartconfig: SmartconfigFormatter,
license: LicenseFormatter,
// format .gitlab-ci.yml packagejson: PackageJsonFormatter,
const formatTemplates = await import('./format.templates.js'); templates: TemplatesFormatter,
await formatTemplates.run(project); gitignore: GitignoreFormatter,
tsconfig: TsconfigFormatter,
// format .gitignore prettier: PrettierFormatter,
const formatGitignore = await import('./format.gitignore.js'); readme: ReadmeFormatter,
await formatGitignore.run(project); copy: CopyFormatter,
// format TypeScript
const formatTsConfig = await import('./format.tsconfig.js');
await formatTsConfig.run(project);
const formatPrettier = await import('./format.prettier.js');
await formatPrettier.run(project);
// format readme.md
const formatReadme = await import('./format.readme.js');
await formatReadme.run();
}; };
// Formatters that don't require projectType to be set
const formattersNotRequiringProjectType = [
"smartconfig",
"prettier",
"cleanup",
"packagejson",
];
const getFormatConfig = async () => {
const rawFormatConfig = await getCliConfigValue<Record<string, any>>(
"format",
{},
);
return {
interactive: true,
showDiffs: false,
autoApprove: false,
showStats: true,
modules: {
skip: [],
only: [],
...(rawFormatConfig.modules || {}),
},
...rawFormatConfig,
};
};
const createActiveFormatters = async (options: {
interactive: boolean;
jsonOutput: boolean;
}) => {
const project = await Project.fromCwd({ requireProjectType: false });
const context = new FormatContext(options);
const planner = new FormatPlanner();
const formatConfig = await getFormatConfig();
const formatters = Object.entries(formatterMap).map(
([, FormatterClass]) => new FormatterClass(context, project),
);
const activeFormatters = formatters.filter((formatter) => {
if (formatConfig.modules.only.length > 0) {
return formatConfig.modules.only.includes(formatter.name);
}
if (formatConfig.modules.skip.includes(formatter.name)) {
return false;
}
return true;
});
return {
context,
planner,
formatConfig,
activeFormatters,
};
};
const buildFormatPlan = async (options: {
fromPlan?: string;
interactive: boolean;
jsonOutput: boolean;
}) => {
const { context, planner, formatConfig, activeFormatters } =
await createActiveFormatters({
interactive: options.interactive,
jsonOutput: options.jsonOutput,
});
const plan = options.fromPlan
? JSON.parse(
(await plugins.smartfs
.file(options.fromPlan)
.encoding("utf8")
.read()) as string,
)
: await planner.planFormat(activeFormatters);
return {
context,
planner,
formatConfig,
activeFormatters,
plan,
};
};
const serializePlan = (plan: any) => {
return {
summary: plan.summary,
warnings: plan.warnings,
changes: plan.changes.map((change: any) => ({
type: change.type,
path: change.path,
module: change.module,
description: change.description,
})),
};
};
export let run = async (
options: {
write?: boolean;
dryRun?: boolean; // Deprecated, kept for compatibility
yes?: boolean;
planOnly?: boolean;
savePlan?: string;
fromPlan?: string;
detailed?: boolean;
interactive?: boolean;
verbose?: boolean;
diff?: boolean;
[key: string]: any;
} = {},
): Promise<any> => {
const mode = await getCliMode(options as any);
const subcommand = (options as any)?._?.[1];
if (mode.help || subcommand === "help") {
showHelp(mode);
return;
}
if (options.verbose) {
setVerboseMode(true);
}
const shouldWrite = options.write ?? options.dryRun === false;
const treatAsPlan = subcommand === "plan";
if (mode.json && shouldWrite) {
printJson({
ok: false,
error:
"JSON output is only supported for read-only format planning. Use `gitzone format plan --json` or omit `--json` when applying changes.",
});
return;
}
// Migrate config file before anything reads it
await migrateConfigFile(shouldWrite);
const formatConfig = await getFormatConfig();
const interactive =
options.interactive ?? (mode.interactive && formatConfig.interactive);
const autoApprove = options.yes ?? formatConfig.autoApprove;
try {
const planBuilder = async () => {
return await buildFormatPlan({
fromPlan: options.fromPlan,
interactive,
jsonOutput: mode.json,
});
};
if (!mode.json) {
logger.log("info", "Analyzing project for format operations...");
}
const { context, planner, activeFormatters, plan } = mode.json
? await runWithSuppressedOutput(planBuilder)
: await planBuilder();
if (mode.json) {
printJson(serializePlan(plan));
return;
}
// Display plan
await planner.displayPlan(plan, options.detailed);
// Save plan if requested
if (options.savePlan) {
await plugins.smartfs
.file(options.savePlan)
.encoding("utf8")
.write(JSON.stringify(plan, null, 2));
logger.log("info", `Plan saved to ${options.savePlan}`);
}
if (options.planOnly || treatAsPlan) {
return;
}
// Show diffs if explicitly requested or before interactive write confirmation
const showDiffs =
options.diff || (shouldWrite && interactive && !autoApprove);
if (showDiffs) {
logger.log("info", "Showing file diffs:");
console.log("");
for (const formatter of activeFormatters) {
const checkResult = await formatter.check();
if (checkResult.hasDiff) {
logger.log("info", `[${formatter.name}]`);
formatter.displayAllDiffs(checkResult);
console.log("");
}
}
}
// Dry-run mode (default behavior)
if (!shouldWrite) {
logger.log("info", "Dry-run mode - use --write (-w) to apply changes");
return;
}
// Interactive confirmation
if (interactive && !autoApprove) {
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: "confirm",
name: "proceed",
message: "Proceed with formatting?",
default: true,
});
if (!(response as any).value) {
logger.log("info", "Format operation cancelled by user");
return;
}
}
// Execute phase
logger.log("info", "Executing format operations...");
await planner.executePlan(plan, activeFormatters, context);
context.getFormatStats().finish();
const showStats = formatConfig.showStats ?? true;
if (showStats) {
context.getFormatStats().displayStats();
}
if (options.detailed) {
const statsPath = `.nogit/format-stats-${Date.now()}.json`;
await context.getFormatStats().saveReport(statsPath);
}
logger.log("success", "Format operations completed successfully!");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.log("error", `Format operation failed: ${errorMessage}`);
throw error;
}
};
import type { ICheckResult } from "./interfaces.format.js";
export type { ICheckResult };
/**
* Run a single formatter by name (for use by other modules)
*/
export const runFormatter = async (
formatterName: string,
options: {
silent?: boolean;
checkOnly?: boolean;
showDiff?: boolean;
} = {},
): Promise<ICheckResult | void> => {
const requireProjectType =
!formattersNotRequiringProjectType.includes(formatterName);
const project = await Project.fromCwd({ requireProjectType });
const context = new FormatContext({ interactive: true, jsonOutput: false });
const FormatterClass = formatterMap[formatterName];
if (!FormatterClass) {
throw new Error(`Unknown formatter: ${formatterName}`);
}
const formatter = new FormatterClass(context, project);
if (options.checkOnly) {
const result = await formatter.check();
if (result.hasDiff && options.showDiff) {
formatter.displayAllDiffs(result);
}
return result;
}
const changes = await formatter.analyze();
for (const change of changes) {
await formatter.applyChange(change);
}
if (!options.silent) {
logger.log("success", `Formatter '${formatterName}' completed`);
}
};
export function showHelp(mode?: ICliMode): void {
if (mode?.json) {
printJson({
command: "format",
usage: "gitzone format [plan] [options]",
description:
"Plans formatting changes by default and applies them only with --write.",
flags: [
{ flag: "--write, -w", description: "Apply planned changes" },
{
flag: "--yes",
description: "Skip the interactive confirmation before writing",
},
{
flag: "--plan-only",
description: "Show the plan without applying changes",
},
{
flag: "--save-plan <file>",
description: "Write the format plan to a file",
},
{
flag: "--from-plan <file>",
description: "Load a previously saved plan",
},
{
flag: "--detailed",
description: "Show detailed diffs and save stats",
},
{ flag: "--verbose", description: "Enable verbose logging" },
{
flag: "--diff",
description: "Show per-file diffs before applying changes",
},
{ flag: "--json", description: "Emit a read-only format plan as JSON" },
],
examples: [
"gitzone format",
"gitzone format plan --json",
"gitzone format --write --yes",
],
});
return;
}
console.log("");
console.log("Usage: gitzone format [plan] [options]");
console.log("");
console.log(
"Plans formatting changes by default and applies them only with --write.",
);
console.log("");
console.log("Flags:");
console.log(" --write, -w Apply planned changes");
console.log(
" --yes Skip the interactive confirmation before writing",
);
console.log(" --plan-only Show the plan without applying changes");
console.log(" --save-plan <file> Write the format plan to a file");
console.log(" --from-plan <file> Load a previously saved plan");
console.log(" --detailed Show detailed diffs and save stats");
console.log(" --verbose Enable verbose logging");
console.log(
" --diff Show per-file diffs before applying changes",
);
console.log(" --json Emit a read-only format plan as JSON");
console.log("");
console.log("Examples:");
console.log(" gitzone format");
console.log(" gitzone format plan --json");
console.log(" gitzone format --write --yes");
console.log("");
}
+53
View File
@@ -0,0 +1,53 @@
export type IFormatPlan = {
summary: {
totalFiles: number;
filesAdded: number;
filesModified: number;
filesRemoved: number;
};
changes: Array<{
type: 'create' | 'modify' | 'delete';
path: string;
module: string;
description: string;
}>;
warnings: Array<{
level: 'info' | 'warning' | 'error';
message: string;
module: string;
}>;
};
export type IPlannedChange = {
type: 'create' | 'modify' | 'delete';
path: string;
module: string;
description: string;
content?: string; // New content for create/modify operations
};
export interface ICheckResult {
hasDiff: boolean;
diffs: Array<{
path: string;
type: 'create' | 'modify' | 'delete';
before?: string;
after?: string;
}>;
}
export function getModuleIcon(module: string): string {
const icons: Record<string, string> = {
packagejson: '📦',
license: '📝',
tsconfig: '🔧',
cleanup: '🚮',
gitignore: '🔒',
prettier: '✨',
readme: '📖',
templates: '📄',
smartconfig: '⚙️',
copy: '📋',
};
return icons[module] || '📁';
}
+8 -10
View File
@@ -1,23 +1,21 @@
export * from '../plugins.js'; export * from '../plugins.js';
import * as lik from '@push.rocks/lik'; import * as path from 'path';
import * as smartfile from '@push.rocks/smartfile'; import * as smartfile from '@push.rocks/smartfile';
import * as smartgulp from '@push.rocks/smartgulp';
import * as smartinteract from '@push.rocks/smartinteract'; import * as smartinteract from '@push.rocks/smartinteract';
import * as smartlegal from '@push.rocks/smartlegal'; import * as smartlegal from '@push.rocks/smartlegal';
import * as smartobject from '@push.rocks/smartobject'; import * as smartobject from '@push.rocks/smartobject';
import * as smartnpm from '@push.rocks/smartnpm'; import * as smartconfig from '@push.rocks/smartconfig';
import * as smartstream from '@push.rocks/smartstream'; import * as smartdiff from '@push.rocks/smartdiff';
import * as through2 from 'through2'; import * as smartscaf from '@push.rocks/smartscaf';
export { export {
lik, path,
smartfile, smartfile,
smartgulp,
smartinteract, smartinteract,
smartlegal, smartlegal,
smartobject, smartobject,
smartnpm, smartconfig,
smartstream, smartdiff,
through2, smartscaf,
}; };
+110 -29
View File
@@ -35,7 +35,10 @@ export class Meta {
* sorts the metaRepoData * sorts the metaRepoData
*/ */
public async sortMetaRepoData() { public async sortMetaRepoData() {
const stringifiedMetadata = plugins.smartjson.stringify(this.metaRepoData, []); const stringifiedMetadata = plugins.smartjson.stringify(
this.metaRepoData,
[],
);
this.metaRepoData = plugins.smartjson.parse(stringifiedMetadata); this.metaRepoData = plugins.smartjson.parse(stringifiedMetadata);
} }
@@ -45,11 +48,17 @@ export class Meta {
public async readDirectory() { public async readDirectory() {
await this.syncToRemote(true); await this.syncToRemote(true);
logger.log('info', `reading directory`); logger.log('info', `reading directory`);
const metaFileExists = plugins.smartfile.fs.fileExistsSync(this.filePaths.metaJson); const metaFileExists = await plugins.smartfs
.file(this.filePaths.metaJson)
.exists();
if (!metaFileExists) { if (!metaFileExists) {
throw new Error(`meta file does not exist at ${this.filePaths.metaJson}`); throw new Error(`meta file does not exist at ${this.filePaths.metaJson}`);
} }
this.metaRepoData = plugins.smartfile.fs.toObjectSync(this.filePaths.metaJson); const content = (await plugins.smartfs
.file(this.filePaths.metaJson)
.encoding('utf8')
.read()) as string;
this.metaRepoData = JSON.parse(content);
} }
/** /**
@@ -71,12 +80,15 @@ export class Meta {
*/ */
public async writeToDisk() { public async writeToDisk() {
// write .meta.json to disk // write .meta.json to disk
plugins.smartfile.memory.toFsSync( await plugins.smartfs
JSON.stringify(this.metaRepoData, null, 2), .file(this.filePaths.metaJson)
this.filePaths.metaJson, .encoding('utf8')
); .write(JSON.stringify(this.metaRepoData, null, 2));
// write .gitignore to disk // write .gitignore to disk
plugins.smartfile.memory.toFsSync(await this.generateGitignore(), this.filePaths.gitIgnore); await plugins.smartfs
.file(this.filePaths.gitIgnore)
.encoding('utf8')
.write(await this.generateGitignore());
} }
/** /**
@@ -84,13 +96,17 @@ export class Meta {
*/ */
public async syncToRemote(gitCleanArg = false) { public async syncToRemote(gitCleanArg = false) {
logger.log('info', `syncing from origin master`); logger.log('info', `syncing from origin master`);
await this.smartshellInstance.exec(`cd ${this.cwd} && git pull origin master`); await this.smartshellInstance.exec(
`cd ${this.cwd} && git pull origin master`,
);
if (gitCleanArg) { if (gitCleanArg) {
logger.log('info', `cleaning the repository from old directories`); logger.log('info', `cleaning the repository from old directories`);
await this.smartshellInstance.exec(`cd ${this.cwd} && git clean -fd`); await this.smartshellInstance.exec(`cd ${this.cwd} && git clean -fd`);
} }
logger.log('info', `syncing to remote origin master`); logger.log('info', `syncing to remote origin master`);
await this.smartshellInstance.exec(`cd ${this.cwd} && git push origin master`); await this.smartshellInstance.exec(
`cd ${this.cwd} && git push origin master`,
);
} }
/** /**
@@ -98,8 +114,25 @@ export class Meta {
*/ */
public async updateLocalRepos() { public async updateLocalRepos() {
await this.syncToRemote(); await this.syncToRemote();
const projects = plugins.smartfile.fs.toObjectSync(this.filePaths.metaJson).projects; const metaContent = (await plugins.smartfs
const preExistingFolders = plugins.smartfile.fs.listFoldersSync(this.cwd); .file(this.filePaths.metaJson)
.encoding('utf8')
.read()) as string;
const projects = JSON.parse(metaContent).projects;
const entries = await plugins.smartfs.directory(this.cwd).list();
const preExistingFolders: string[] = [];
for (const entry of entries) {
try {
const stats = await plugins.smartfs
.file(plugins.path.join(this.cwd, entry.path))
.stat();
if (stats.isDirectory) {
preExistingFolders.push(entry.name);
}
} catch {
// Skip entries that can't be accessed
}
}
for (const preExistingFolderArg of preExistingFolders) { for (const preExistingFolderArg of preExistingFolders) {
if ( if (
preExistingFolderArg !== '.git' && preExistingFolderArg !== '.git' &&
@@ -107,14 +140,18 @@ export class Meta {
projectFolder.startsWith(preExistingFolderArg), projectFolder.startsWith(preExistingFolderArg),
) )
) { ) {
const response = await plugins.smartinteraction.SmartInteract.getCliConfirmation( const response =
await plugins.smartinteraction.SmartInteract.getCliConfirmation(
`Do you want to delete superfluous directory >>${preExistingFolderArg}<< ?`, `Do you want to delete superfluous directory >>${preExistingFolderArg}<< ?`,
true, true,
); );
if (response) { if (response) {
logger.log('warn', `Deleting >>${preExistingFolderArg}<<!`); logger.log('warn', `Deleting >>${preExistingFolderArg}<<!`);
} else { } else {
logger.log('warn', `Not deleting ${preExistingFolderArg} by request!`); logger.log(
'warn',
`Not deleting ${preExistingFolderArg} by request!`,
);
} }
} }
} }
@@ -123,9 +160,17 @@ export class Meta {
await this.sortMetaRepoData(); await this.sortMetaRepoData();
const missingRepos: string[] = []; const missingRepos: string[] = [];
for (const key of Object.keys(this.metaRepoData.projects)) { for (const key of Object.keys(this.metaRepoData.projects)) {
plugins.smartfile.fs.isDirectory(key) const fullPath = plugins.path.join(this.cwd, key);
? logger.log('ok', `${key} -> is already cloned`) try {
: missingRepos.push(key); const stats = await plugins.smartfs.file(fullPath).stat();
if (stats.isDirectory) {
logger.log('ok', `${key} -> is already cloned`);
} else {
missingRepos.push(key);
}
} catch {
missingRepos.push(key);
}
} }
logger.log('info', `found ${missingRepos.length} missing repos`); logger.log('info', `found ${missingRepos.length} missing repos`);
@@ -145,7 +190,20 @@ export class Meta {
await this.syncToRemote(); await this.syncToRemote();
// go recursive // go recursive
const folders = await plugins.smartfile.fs.listFolders(this.cwd); const listEntries = await plugins.smartfs.directory(this.cwd).list();
const folders: string[] = [];
for (const entry of listEntries) {
try {
const stats = await plugins.smartfs
.file(plugins.path.join(this.cwd, entry.path))
.stat();
if (stats.isDirectory) {
folders.push(entry.name);
}
} catch {
// Skip entries that can't be accessed
}
}
const childMetaRepositories: string[] = []; const childMetaRepositories: string[] = [];
for (const folder of folders) { for (const folder of folders) {
logger.log('info', folder); logger.log('info', folder);
@@ -160,25 +218,40 @@ export class Meta {
*/ */
public async initProject() { public async initProject() {
await this.syncToRemote(true); await this.syncToRemote(true);
const fileExists = await plugins.smartfile.fs.fileExists(this.filePaths.metaJson); const fileExists = await plugins.smartfs
.file(this.filePaths.metaJson)
.exists();
if (!fileExists) { if (!fileExists) {
await plugins.smartfile.memory.toFs( await plugins.smartfs
.file(this.filePaths.metaJson)
.encoding('utf8')
.write(
JSON.stringify({ JSON.stringify({
projects: {}, projects: {},
}), }),
this.filePaths.metaJson,
); );
logger.log(`success`, `created a new .meta.json in directory ${this.cwd}`); logger.log(
await plugins.smartfile.memory.toFs( `success`,
`created a new .meta.json in directory ${this.cwd}`,
);
await plugins.smartfs
.file(this.filePaths.packageJson)
.encoding('utf8')
.write(
JSON.stringify({ JSON.stringify({
name: this.dirName, name: this.dirName,
version: '1.0.0', version: '1.0.0',
}), }),
this.filePaths.packageJson,
); );
logger.log(`success`, `created a new package.json in directory ${this.cwd}`); logger.log(
`success`,
`created a new package.json in directory ${this.cwd}`,
);
} else { } else {
logger.log(`error`, `directory ${this.cwd} already has a .metaJson file. Doing nothing.`); logger.log(
`error`,
`directory ${this.cwd} already has a .metaJson file. Doing nothing.`,
);
} }
await this.smartshellInstance.exec( await this.smartshellInstance.exec(
`cd ${this.cwd} && git add -A && git commit -m "feat(project): init meta project for ${this.dirName}"`, `cd ${this.cwd} && git add -A && git commit -m "feat(project): init meta project for ${this.dirName}"`,
@@ -195,7 +268,9 @@ export class Meta {
const existingProject = this.metaRepoData.projects[projectNameArg]; const existingProject = this.metaRepoData.projects[projectNameArg];
if (existingProject) { if (existingProject) {
throw new Error('Project already exists! Please remove it first before adding it again.'); throw new Error(
'Project already exists! Please remove it first before adding it again.',
);
} }
this.metaRepoData.projects[projectNameArg] = gitUrlArg; this.metaRepoData.projects[projectNameArg] = gitUrlArg;
@@ -217,7 +292,10 @@ export class Meta {
const existingProject = this.metaRepoData.projects[projectNameArg]; const existingProject = this.metaRepoData.projects[projectNameArg];
if (!existingProject) { if (!existingProject) {
logger.log('error', `Project ${projectNameArg} does not exist! So it cannot be removed`); logger.log(
'error',
`Project ${projectNameArg} does not exist! So it cannot be removed`,
);
return; return;
} }
@@ -228,7 +306,10 @@ export class Meta {
await this.writeToDisk(); await this.writeToDisk();
logger.log('info', 'removing directory from cwd'); logger.log('info', 'removing directory from cwd');
await plugins.smartfile.fs.remove(plugins.path.join(paths.cwd, projectNameArg)); await plugins.smartfs
.directory(plugins.path.join(paths.cwd, projectNameArg))
.recursive()
.delete();
await this.updateLocalRepos(); await this.updateLocalRepos();
} }
} }
+393
View File
@@ -0,0 +1,393 @@
import * as plugins from "./mod.plugins.js";
import * as paths from "../paths.js";
import { logger } from "../gitzone.logging.js";
import type { ICliMode } from "../helpers.climode.js";
import { getCliMode, printJson } from "../helpers.climode.js";
import {
inferVersionTypeFromPending,
movePendingToVersion,
readPendingChangelog,
} from "../helpers.changelog.js";
import {
resolveReleaseWorkflow,
type IResolvedReleaseWorkflow,
} from "../helpers.workflow.js";
import * as commitHelpers from "../mod_commit/mod.helpers.js";
type TTargetStatus = "success" | "already-published" | "skipped" | "failed";
interface ITargetResult {
target: string;
status: TTargetStatus;
message?: string;
}
export const run = async (argvArg: any) => {
const mode = await getCliMode(argvArg);
const subcommand = argvArg._?.[1];
if (mode.help || subcommand === "help") {
showHelp(mode);
return;
}
if (mode.json) {
printJson({
ok: false,
error: "JSON output is not supported for mutating release workflows yet. Use `gitzone release --plan` for a human-readable plan.",
});
return;
}
const workflow = await resolveReleaseWorkflow(argvArg);
printReleasePlan(workflow);
if (workflow.confirmation === "plan") {
return;
}
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: "bash",
sourceFilePaths: [],
});
const pending = await readPendingChangelog(
plugins.path.join(paths.cwd, workflow.changelogFile),
workflow.changelogPendingSection,
);
if (pending.isEmpty && !argvArg["allow-empty"] && !argvArg.allowEmpty) {
logger.log("error", "No pending changelog entries. Nothing to release.");
process.exit(1);
}
const versionType = resolveVersionType(argvArg, pending.block);
const projectType = await commitHelpers.detectProjectType();
const currentVersion = await commitHelpers.readCurrentVersion(projectType);
const plannedVersion = commitHelpers.calculateNewVersion(currentVersion, versionType);
if (workflow.confirmation === "prompt") {
if (!mode.interactive) {
throw new Error("Release confirmation requires an interactive terminal. Use `-y` or set release.confirmation to `auto`.");
}
const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
`Release v${plannedVersion} (${versionType}) now?`,
true,
);
if (!confirmed) {
logger.log("info", "Release cancelled.");
return;
}
}
let newVersion = plannedVersion;
const gitResults: ITargetResult[] = [];
const npmResults: ITargetResult[] = [];
const dockerResults: ITargetResult[] = [];
if (workflow.requireCleanTree) {
await verifyCleanTree(smartshellInstance, "Working tree is not clean. Commit or stash changes before releasing.");
}
if (workflow.runTests) {
await runCommandStep(smartshellInstance, "Running tests", workflow.testCommand);
}
newVersion = await runVersionStep(projectType, versionType);
await runChangelogStep(workflow, newVersion);
await runReleaseCommitStep(smartshellInstance, newVersion);
await runTagStep(smartshellInstance, newVersion);
if (workflow.runBuild) {
await runCommandStep(smartshellInstance, "Running release build", workflow.buildCommand);
await verifyCleanTree(smartshellInstance, "Build produced uncommitted changes. Aborting release.");
}
if (workflow.targets.includes("git")) {
gitResults.push(...(await runGitTarget(smartshellInstance, workflow)));
}
if (workflow.targets.includes("npm")) {
npmResults.push(...(await runNpmTarget(smartshellInstance, workflow)));
}
if (workflow.targets.includes("docker")) {
dockerResults.push(...(await runDockerTarget(smartshellInstance, workflow, newVersion)));
}
printReleaseSummary(newVersion, gitResults, npmResults, dockerResults);
if ([...gitResults, ...npmResults, ...dockerResults].some((result) => result.status === "failed")) {
process.exit(1);
}
};
function resolveVersionType(argvArg: any, pendingBlock: string): commitHelpers.VersionType {
if (argvArg.major) return "major";
if (argvArg.minor) return "minor";
if (argvArg.patch) return "patch";
return inferVersionTypeFromPending(pendingBlock);
}
async function runCommandStep(
smartshellInstance: plugins.smartshell.Smartshell,
label: string,
command: string,
): Promise<void> {
console.log(`\n${label}`);
const result = await smartshellInstance.exec(command);
if (result.exitCode !== 0) {
logger.log("error", `${label} failed. Aborting release.`);
process.exit(1);
}
logger.log("success", `${label} passed.`);
}
async function verifyCleanTree(
smartshellInstance: plugins.smartshell.Smartshell,
errorMessage: string,
): Promise<void> {
const statusResult = await smartshellInstance.exec("git status --porcelain");
if (statusResult.stdout.trim() !== "") {
logger.log("error", errorMessage);
console.log(statusResult.stdout);
process.exit(1);
}
}
async function runVersionStep(
projectType: commitHelpers.ProjectType,
versionType: commitHelpers.VersionType,
): Promise<string> {
const currentVersion = await commitHelpers.readCurrentVersion(projectType);
const newVersion = commitHelpers.calculateNewVersion(currentVersion, versionType);
logger.log("info", `Bumping version: ${currentVersion} -> ${newVersion}`);
const commitInfo = new plugins.commitinfo.CommitInfo(paths.cwd, versionType);
await commitInfo.writeIntoPotentialDirs();
await commitHelpers.updateProjectVersionFiles(projectType, newVersion);
return newVersion;
}
async function runChangelogStep(
workflow: IResolvedReleaseWorkflow,
newVersion: string,
): Promise<void> {
const dateString = new Date().toISOString().slice(0, 10);
await movePendingToVersion(
plugins.path.join(paths.cwd, workflow.changelogFile),
workflow.changelogPendingSection,
workflow.changelogVersionHeading,
newVersion,
dateString,
);
}
async function runReleaseCommitStep(
smartshellInstance: plugins.smartshell.Smartshell,
newVersion: string,
): Promise<void> {
await smartshellInstance.exec("git add -A");
const result = await smartshellInstance.exec(`git commit -m ${shellQuote(`v${newVersion}`)}`);
if (result.exitCode !== 0) {
logger.log("error", "Release commit failed.");
process.exit(1);
}
}
async function runTagStep(
smartshellInstance: plugins.smartshell.Smartshell,
newVersion: string,
): Promise<void> {
const result = await smartshellInstance.exec(`git tag v${newVersion} -m ${shellQuote(`v${newVersion}`)}`);
if (result.exitCode !== 0) {
logger.log("error", "Release tag failed.");
process.exit(1);
}
}
async function runGitTarget(
smartshellInstance: plugins.smartshell.Smartshell,
workflow: IResolvedReleaseWorkflow,
): Promise<ITargetResult[]> {
const currentBranchResult = await smartshellInstance.exec("git branch --show-current");
const currentBranch = currentBranchResult.stdout.trim() || "master";
const commands: Array<{ target: string; command: string }> = [];
if (workflow.pushBranch) {
commands.push({
target: `${workflow.gitRemote}/${currentBranch}`,
command: `git push ${workflow.gitRemote} ${currentBranch}`,
});
}
if (workflow.pushTags) {
commands.push({
target: `${workflow.gitRemote}/tags`,
command: `git push ${workflow.gitRemote} --tags`,
});
}
const results: ITargetResult[] = [];
for (const { target, command } of commands) {
const result = await smartshellInstance.exec(command);
results.push({
target,
status: result.exitCode === 0 ? "success" : "failed",
message: result.exitCode === 0 ? undefined : "push failed",
});
}
return results;
}
async function runNpmTarget(
smartshellInstance: plugins.smartshell.Smartshell,
workflow: IResolvedReleaseWorkflow,
): Promise<ITargetResult[]> {
if (!workflow.npmEnabled) {
return [{ target: "npm", status: "skipped", message: "disabled" }];
}
if (workflow.npmRegistries.length === 0) {
return [{ target: "npm", status: "failed", message: "no registries configured" }];
}
const results: ITargetResult[] = [];
for (const registry of workflow.npmRegistries) {
const command = `pnpm publish --registry=${registry} --access=${workflow.npmAccessLevel}`;
const result = await smartshellInstance.exec(command);
const output = `${result.stdout || ""}\n${(result as any).stderr || ""}\n${(result as any).combinedOutput || ""}`;
if (result.exitCode === 0) {
results.push({ target: registry, status: "success" });
} else if (isAlreadyPublishedOutput(output) && workflow.npmAlreadyPublished === "success") {
results.push({ target: registry, status: "already-published" });
} else {
results.push({ target: registry, status: "failed", message: firstMeaningfulLine(output) });
}
}
return results;
}
async function runDockerTarget(
smartshellInstance: plugins.smartshell.Smartshell,
workflow: IResolvedReleaseWorkflow,
newVersion: string,
): Promise<ITargetResult[]> {
if (!workflow.dockerEnabled) {
return [{ target: "docker", status: "skipped", message: "disabled" }];
}
if (workflow.dockerImages.length === 0) {
return [{ target: "docker", status: "failed", message: "no images configured" }];
}
const results: ITargetResult[] = [];
for (const imageTemplate of workflow.dockerImages) {
const image = imageTemplate.replaceAll("{{version}}", newVersion);
const buildResult = await smartshellInstance.exec(`docker build -t ${shellQuote(image)} .`);
if (buildResult.exitCode !== 0) {
results.push({ target: image, status: "failed", message: "docker build failed" });
continue;
}
const pushResult = await smartshellInstance.exec(`docker push ${shellQuote(image)}`);
results.push({
target: image,
status: pushResult.exitCode === 0 ? "success" : "failed",
message: pushResult.exitCode === 0 ? undefined : "docker push failed",
});
}
return results;
}
function isAlreadyPublishedOutput(output: string): boolean {
return /previously published versions|cannot publish over|already exists/i.test(output);
}
function firstMeaningfulLine(output: string): string {
return output
.split("\n")
.map((line) => line.trim())
.find((line) => line.length > 0) || "command failed";
}
function shellQuote(value: string): string {
return `'${value.replaceAll("'", "'\\''")}'`;
}
function printReleasePlan(workflow: IResolvedReleaseWorkflow): void {
console.log("");
console.log("gitzone release - resolved workflow");
console.log(`confirmation: ${workflow.confirmation}`);
console.log(`plan: ${workflow.plan.join(" -> ")}`);
console.log(`targets: ${workflow.targets.length > 0 ? workflow.targets.join(", ") : "none"}`);
console.log(`changelog: ${workflow.changelogFile}#${workflow.changelogPendingSection}`);
if (workflow.targets.includes("npm")) {
console.log(`npm registries: ${workflow.npmRegistries.length > 0 ? workflow.npmRegistries.join(", ") : "none"}`);
}
if (workflow.targets.includes("docker")) {
console.log(`docker images: ${workflow.dockerImages.length > 0 ? workflow.dockerImages.join(", ") : "none"}`);
}
console.log("");
}
function printReleaseSummary(
newVersion: string,
gitResults: ITargetResult[],
npmResults: ITargetResult[],
dockerResults: ITargetResult[],
): void {
console.log("");
console.log(`Release v${newVersion}`);
console.log("");
if (gitResults.length > 0) {
console.log("git:");
for (const result of gitResults) {
console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
}
}
if (npmResults.length > 0) {
console.log("npm:");
for (const result of npmResults) {
console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
}
}
if (dockerResults.length > 0) {
console.log("docker:");
for (const result of dockerResults) {
console.log(` ${result.target} ${result.status}${result.message ? ` (${result.message})` : ""}`);
}
}
}
export function showHelp(mode?: ICliMode): void {
if (mode?.json) {
printJson({
command: "release",
usage: "gitzone release [options]",
description: "Creates a versioned release from pending changelog entries and publishes configured artifacts.",
flags: [
{ flag: "-y, --yes", description: "Run without interactive confirmation" },
{ flag: "-t, --test", description: "Enable release preflight tests" },
{ flag: "-b, --build", description: "Enable release preflight build" },
{ flag: "-p, --push", description: "Enable the git release target" },
{ flag: "--target <names>", description: "Release only selected targets: git,npm,docker" },
{ flag: "--npm", description: "Enable the npm release target" },
{ flag: "--docker", description: "Enable the Docker release target" },
{ flag: "--no-publish", description: "Run release core and git target only" },
{ flag: "--plan", description: "Show resolved workflow without mutating files" },
],
});
return;
}
console.log("");
console.log("Usage: gitzone release [options]");
console.log("");
console.log("Creates a versioned release from changelog Pending entries.");
console.log("");
console.log("Flags:");
console.log(" -y, --yes Run without interactive confirmation");
console.log(" -t, --test Enable release preflight tests");
console.log(" -b, --build Enable release preflight build");
console.log(" -p, --push Enable the git release target");
console.log(" --target <names> Release only selected targets: git,npm,docker");
console.log(" --npm Enable the npm release target");
console.log(" --docker Enable the Docker release target");
console.log(" --no-publish Run release core and git target only");
console.log(" --major|--minor|--patch Override inferred semver level");
console.log(" --plan Show resolved workflow without mutating files");
console.log("");
}
+5
View File
@@ -0,0 +1,5 @@
export * from "../plugins.js";
import * as commitinfo from "@push.rocks/commitinfo";
export { commitinfo };
+261
View File
@@ -0,0 +1,261 @@
import * as plugins from './mod.plugins.js';
import * as helpers from './helpers.js';
import { logger } from '../gitzone.logging.js';
export type ContainerStatus = 'running' | 'stopped' | 'not_exists';
export interface IDockerRunOptions {
name: string;
image: string;
ports?: { [key: string]: string };
volumes?: { [key: string]: string };
environment?: { [key: string]: string };
restart?: string;
command?: string;
}
export class DockerContainer {
private smartshell: plugins.smartshell.Smartshell;
constructor() {
this.smartshell = new plugins.smartshell.Smartshell({
executor: 'bash',
});
}
/**
* Check if Docker is installed and available
*/
public async checkDocker(): Promise<boolean> {
try {
const result = await this.smartshell.exec('docker --version');
return result.exitCode === 0;
} catch (error) {
return false;
}
}
/**
* Get container status
*/
public async getStatus(containerName: string): Promise<ContainerStatus> {
try {
// Check if running
const runningResult = await this.smartshell.exec(
`docker ps --format '{{.Names}}' | grep -q "^${containerName}$"`
);
if (runningResult.exitCode === 0) {
return 'running';
}
// Check if exists but stopped
const existsResult = await this.smartshell.exec(
`docker ps -a --format '{{.Names}}' | grep -q "^${containerName}$"`
);
if (existsResult.exitCode === 0) {
return 'stopped';
}
return 'not_exists';
} catch (error) {
return 'not_exists';
}
}
/**
* Start a container
*/
public async start(containerName: string): Promise<boolean> {
try {
const result = await this.smartshell.exec(`docker start ${containerName}`);
return result.exitCode === 0;
} catch (error) {
return false;
}
}
/**
* Stop a container
*/
public async stop(containerName: string): Promise<boolean> {
try {
const result = await this.smartshell.exec(`docker stop ${containerName}`);
return result.exitCode === 0;
} catch (error) {
return false;
}
}
/**
* Remove a container
*/
public async remove(containerName: string, force: boolean = false): Promise<boolean> {
try {
const forceFlag = force ? '-f' : '';
const result = await this.smartshell.exec(`docker rm ${forceFlag} ${containerName}`);
return result.exitCode === 0;
} catch (error) {
return false;
}
}
/**
* Run a new container
*/
public async run(options: IDockerRunOptions): Promise<boolean> {
let command = 'docker run -d';
// Add name
command += ` --name ${options.name}`;
// Add ports
if (options.ports) {
for (const [hostPort, containerPort] of Object.entries(options.ports)) {
command += ` -p ${hostPort}:${containerPort}`;
}
}
// Add volumes
if (options.volumes) {
for (const [hostPath, containerPath] of Object.entries(options.volumes)) {
command += ` -v "${hostPath}:${containerPath}"`;
}
}
// Add environment variables
if (options.environment) {
for (const [key, value] of Object.entries(options.environment)) {
command += ` -e ${key}="${value}"`;
}
}
// Add restart policy
if (options.restart) {
command += ` --restart ${options.restart}`;
}
// Add image
command += ` ${options.image}`;
// Add command if provided
if (options.command) {
command += ` ${options.command}`;
}
try {
const result = await this.smartshell.exec(command);
return result.exitCode === 0;
} catch (error) {
logger.log('error', `Failed to run container: ${error.message}`);
return false;
}
}
/**
* Execute a command in a running container
*/
public async exec(containerName: string, command: string): Promise<string> {
try {
const result = await this.smartshell.exec(`docker exec ${containerName} ${command}`);
if (result.exitCode === 0) {
return result.stdout;
}
return '';
} catch (error) {
return '';
}
}
/**
* Get container logs
*/
public async logs(containerName: string, lines?: number): Promise<string> {
try {
const tailFlag = lines ? `--tail ${lines}` : '';
const result = await this.smartshell.exec(`docker logs ${tailFlag} ${containerName}`);
return result.stdout;
} catch (error) {
return `Error getting logs: ${error.message}`;
}
}
/**
* Check if a container exists
*/
public async exists(containerName: string): Promise<boolean> {
const status = await this.getStatus(containerName);
return status !== 'not_exists';
}
/**
* Check if a container is running
*/
public async isRunning(containerName: string): Promise<boolean> {
const status = await this.getStatus(containerName);
return status === 'running';
}
/**
* Wait for a container to be ready
*/
public async waitForReady(containerName: string, maxAttempts: number = 30): Promise<boolean> {
for (let i = 0; i < maxAttempts; i++) {
if (await this.isRunning(containerName)) {
return true;
}
await plugins.smartdelay.delayFor(1000);
}
return false;
}
/**
* Get container information
*/
public async inspect(containerName: string): Promise<any> {
try {
const result = await this.smartshell.execSilent(`docker inspect ${containerName}`);
if (result.exitCode === 0) {
return JSON.parse(result.stdout);
}
return null;
} catch (error) {
return null;
}
}
/**
* Get port mappings for a container
*/
public async getPortMappings(containerName: string): Promise<{ [key: string]: string } | null> {
try {
// Use docker inspect without format to get full JSON, then extract PortBindings
const result = await this.smartshell.execSilent(`docker inspect ${containerName}`);
if (result.exitCode === 0 && result.stdout) {
const inspectData = JSON.parse(result.stdout);
if (inspectData && inspectData[0] && inspectData[0].HostConfig && inspectData[0].HostConfig.PortBindings) {
const portBindings = inspectData[0].HostConfig.PortBindings;
const mappings: { [key: string]: string } = {};
// Convert Docker's port binding format to simple host:container mapping
for (const [containerPort, hostBindings] of Object.entries(portBindings)) {
if (Array.isArray(hostBindings) && hostBindings.length > 0) {
const hostPort = (hostBindings[0] as any).HostPort;
if (hostPort) {
mappings[containerPort.replace('/tcp', '').replace('/udp', '')] = hostPort;
}
}
}
return mappings;
}
}
return null;
} catch (error) {
// Silently fail - container might not exist
return null;
}
}
}
+190
View 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.smartconfig.KeyValueStore<IGlobalRegistryData>;
private docker: DockerContainer;
private constructor() {
this.kvStore = new plugins.smartconfig.KeyValueStore({
typeArg: 'userHomeDir',
identityArg: 'gitzone-services',
});
this.docker = new DockerContainer();
}
/**
* Get the singleton instance
*/
public static getInstance(): GlobalRegistry {
if (!GlobalRegistry.instance) {
GlobalRegistry.instance = new GlobalRegistry();
}
return GlobalRegistry.instance;
}
/**
* Register or update a project in the global registry
*/
public async registerProject(data: Omit<IRegisteredProject, 'lastActive'>): Promise<void> {
const allData = await this.kvStore.readAll();
const projects = allData.projects || {};
projects[data.projectPath] = {
...data,
lastActive: Date.now(),
};
await this.kvStore.writeKey('projects', projects);
}
/**
* Remove a project from the registry
*/
public async unregisterProject(projectPath: string): Promise<void> {
const allData = await this.kvStore.readAll();
const projects = allData.projects || {};
if (projects[projectPath]) {
delete projects[projectPath];
await this.kvStore.writeKey('projects', projects);
}
}
/**
* Update the lastActive timestamp for a project
*/
public async touchProject(projectPath: string): Promise<void> {
const allData = await this.kvStore.readAll();
const projects = allData.projects || {};
if (projects[projectPath]) {
projects[projectPath].lastActive = Date.now();
await this.kvStore.writeKey('projects', projects);
}
}
/**
* Get all registered projects
*/
public async getAllProjects(): Promise<{ [path: string]: IRegisteredProject }> {
const allData = await this.kvStore.readAll();
return allData.projects || {};
}
/**
* Check if a project is registered
*/
public async isRegistered(projectPath: string): Promise<boolean> {
const projects = await this.getAllProjects();
return !!projects[projectPath];
}
/**
* Get status of all containers across all registered projects
*/
public async getGlobalStatus(): Promise<
Array<{
projectPath: string;
projectName: string;
containers: Array<{ name: string; status: string }>;
lastActive: number;
}>
> {
const projects = await this.getAllProjects();
const result: Array<{
projectPath: string;
projectName: string;
containers: Array<{ name: string; status: string }>;
lastActive: number;
}> = [];
for (const [path, project] of Object.entries(projects)) {
const containerStatuses: Array<{ name: string; status: string }> = [];
for (const containerName of Object.values(project.containers)) {
if (containerName) {
const status = await this.docker.getStatus(containerName);
containerStatuses.push({ name: containerName, status });
}
}
result.push({
projectPath: path,
projectName: project.projectName,
containers: containerStatuses,
lastActive: project.lastActive,
});
}
return result;
}
/**
* Stop all containers across all registered projects
*/
public async stopAll(): Promise<{ stopped: string[]; failed: string[] }> {
const projects = await this.getAllProjects();
const stopped: string[] = [];
const failed: string[] = [];
for (const project of Object.values(projects)) {
for (const containerName of Object.values(project.containers)) {
if (containerName) {
const status = await this.docker.getStatus(containerName);
if (status === 'running') {
if (await this.docker.stop(containerName)) {
stopped.push(containerName);
} else {
failed.push(containerName);
}
}
}
}
}
return { stopped, failed };
}
/**
* Remove stale registry entries (projects that no longer exist on disk)
*/
public async cleanup(): Promise<string[]> {
const projects = await this.getAllProjects();
const removed: string[] = [];
for (const projectPath of Object.keys(projects)) {
const exists = await plugins.smartfs.directory(projectPath).exists();
if (!exists) {
await this.unregisterProject(projectPath);
removed.push(projectPath);
}
}
return removed;
}
}
@@ -0,0 +1,518 @@
import * as plugins from './mod.plugins.js';
import * as helpers from './helpers.js';
import { logger } from '../gitzone.logging.js';
import { DockerContainer } from './classes.dockercontainer.js';
export interface IServiceConfig {
PROJECT_NAME: string;
MONGODB_HOST: string;
MONGODB_NAME: string;
MONGODB_PORT: string;
MONGODB_USER: string;
MONGODB_PASS: string;
MONGODB_URL: string;
S3_HOST: string;
S3_PORT: string;
S3_CONSOLE_PORT: string;
S3_ACCESSKEY: string;
S3_SECRETKEY: string;
S3_BUCKET: string;
S3_ENDPOINT: string;
S3_USESSL: boolean;
ELASTICSEARCH_HOST: string;
ELASTICSEARCH_PORT: string;
ELASTICSEARCH_USER: string;
ELASTICSEARCH_PASS: string;
ELASTICSEARCH_URL: string;
}
export class ServiceConfiguration {
private configPath: string;
private config: IServiceConfig;
private docker: DockerContainer;
constructor() {
this.configPath = plugins.path.join(process.cwd(), '.nogit', 'env.json');
this.docker = new DockerContainer();
}
/**
* Load or create the configuration
*/
public async loadOrCreate(): Promise<IServiceConfig> {
await this.ensureNogitDirectory();
if (await this.configExists()) {
await this.loadConfig();
await this.updateMissingFields();
} else {
await this.createDefaultConfig();
}
// Sync ports from existing Docker containers if they exist
await this.syncPortsFromDocker();
return this.config;
}
/**
* Get the current configuration
*/
public getConfig(): IServiceConfig {
return this.config;
}
/**
* Save the configuration to file
*/
public async saveConfig(): Promise<void> {
await plugins.smartfs
.file(this.configPath)
.encoding('utf8')
.write(JSON.stringify(this.config, null, 2));
}
/**
* Ensure .nogit directory exists
*/
private async ensureNogitDirectory(): Promise<void> {
const nogitPath = plugins.path.join(process.cwd(), '.nogit');
await plugins.smartfs.directory(nogitPath).recursive().create();
}
/**
* Check if configuration file exists
*/
private async configExists(): Promise<boolean> {
return plugins.smartfs.file(this.configPath).exists();
}
/**
* Load configuration from file
*/
private async loadConfig(): Promise<void> {
const configContent = (await plugins.smartfs
.file(this.configPath)
.encoding('utf8')
.read()) as string;
this.config = JSON.parse(configContent);
}
/**
* Create default configuration
*/
private async createDefaultConfig(): Promise<void> {
const projectName = await helpers.getProjectName();
const mongoPort = await helpers.getRandomAvailablePort();
const s3Port = await helpers.getRandomAvailablePort();
let s3ConsolePort = s3Port + 1;
// Ensure console port is also available
while (!(await helpers.isPortAvailable(s3ConsolePort))) {
s3ConsolePort++;
}
const mongoUser = 'defaultadmin';
const mongoPass = 'defaultpass';
const mongoHost = 'localhost';
const mongoName = projectName;
const mongoPortStr = mongoPort.toString();
const s3Host = 'localhost';
const s3PortStr = s3Port.toString();
const esHost = 'localhost';
const esPort = '9200';
const esUser = 'elastic';
const esPass = 'elastic';
this.config = {
PROJECT_NAME: projectName,
MONGODB_HOST: mongoHost,
MONGODB_NAME: mongoName,
MONGODB_PORT: mongoPortStr,
MONGODB_USER: mongoUser,
MONGODB_PASS: mongoPass,
MONGODB_URL: `mongodb://${mongoUser}:${mongoPass}@${mongoHost}:${mongoPortStr}/${mongoName}?authSource=admin`,
S3_HOST: s3Host,
S3_PORT: s3PortStr,
S3_CONSOLE_PORT: s3ConsolePort.toString(),
S3_ACCESSKEY: 'defaultadmin',
S3_SECRETKEY: 'defaultpass',
S3_BUCKET: `${projectName}-documents`,
S3_ENDPOINT: s3Host,
S3_USESSL: false,
ELASTICSEARCH_HOST: esHost,
ELASTICSEARCH_PORT: esPort,
ELASTICSEARCH_USER: esUser,
ELASTICSEARCH_PASS: esPass,
ELASTICSEARCH_URL: `http://${esUser}:${esPass}@${esHost}:${esPort}`
};
await this.saveConfig();
logger.log('ok', '✅ Created .nogit/env.json with project defaults');
logger.log('info', `📍 MongoDB port: ${mongoPort}`);
logger.log('info', `📍 S3 API port: ${s3Port}`);
logger.log('info', `📍 S3 Console port: ${s3ConsolePort}`);
logger.log('info', `📍 Elasticsearch port: ${esPort}`);
}
/**
* Update missing fields in existing configuration
*/
private async updateMissingFields(): Promise<void> {
const projectName = await helpers.getProjectName();
let updated = false;
const fieldsAdded: string[] = [];
// Check and add missing fields
if (!this.config.PROJECT_NAME) {
this.config.PROJECT_NAME = projectName;
fieldsAdded.push('PROJECT_NAME');
updated = true;
}
if (!this.config.MONGODB_HOST) {
this.config.MONGODB_HOST = 'localhost';
fieldsAdded.push('MONGODB_HOST');
updated = true;
}
if (!this.config.MONGODB_NAME) {
this.config.MONGODB_NAME = projectName;
fieldsAdded.push('MONGODB_NAME');
updated = true;
}
if (!this.config.MONGODB_PORT) {
const port = await helpers.getRandomAvailablePort();
this.config.MONGODB_PORT = port.toString();
fieldsAdded.push(`MONGODB_PORT(${port})`);
updated = true;
}
if (!this.config.MONGODB_USER) {
this.config.MONGODB_USER = 'defaultadmin';
fieldsAdded.push('MONGODB_USER');
updated = true;
}
if (!this.config.MONGODB_PASS) {
this.config.MONGODB_PASS = 'defaultpass';
fieldsAdded.push('MONGODB_PASS');
updated = true;
}
// Always update MONGODB_URL based on current settings
const oldUrl = this.config.MONGODB_URL;
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
if (oldUrl !== this.config.MONGODB_URL) {
fieldsAdded.push('MONGODB_URL');
updated = true;
}
if (!this.config.S3_HOST) {
this.config.S3_HOST = 'localhost';
fieldsAdded.push('S3_HOST');
updated = true;
}
if (!this.config.S3_PORT) {
const port = await helpers.getRandomAvailablePort();
this.config.S3_PORT = port.toString();
fieldsAdded.push(`S3_PORT(${port})`);
updated = true;
}
if (!this.config.S3_CONSOLE_PORT) {
const s3Port = parseInt(this.config.S3_PORT);
let consolePort = s3Port + 1;
while (!(await helpers.isPortAvailable(consolePort))) {
consolePort++;
}
this.config.S3_CONSOLE_PORT = consolePort.toString();
fieldsAdded.push(`S3_CONSOLE_PORT(${consolePort})`);
updated = true;
}
if (!this.config.S3_ACCESSKEY) {
this.config.S3_ACCESSKEY = 'defaultadmin';
fieldsAdded.push('S3_ACCESSKEY');
updated = true;
}
if (!this.config.S3_SECRETKEY) {
this.config.S3_SECRETKEY = 'defaultpass';
fieldsAdded.push('S3_SECRETKEY');
updated = true;
}
if (!this.config.S3_BUCKET) {
this.config.S3_BUCKET = `${projectName}-documents`;
fieldsAdded.push('S3_BUCKET');
updated = true;
}
if (!this.config.S3_USESSL) {
this.config.S3_USESSL = false;
fieldsAdded.push('S3_USESSL');
updated = true;
}
// Always update S3_ENDPOINT based on current settings
const oldEndpoint = this.config.S3_ENDPOINT;
this.config.S3_ENDPOINT = this.config.S3_HOST;
if (oldEndpoint !== this.config.S3_ENDPOINT) {
fieldsAdded.push('S3_ENDPOINT');
updated = true;
}
if (!this.config.ELASTICSEARCH_HOST) {
this.config.ELASTICSEARCH_HOST = 'localhost';
fieldsAdded.push('ELASTICSEARCH_HOST');
updated = true;
}
if (!this.config.ELASTICSEARCH_PORT) {
this.config.ELASTICSEARCH_PORT = '9200';
fieldsAdded.push('ELASTICSEARCH_PORT');
updated = true;
}
if (!this.config.ELASTICSEARCH_USER) {
this.config.ELASTICSEARCH_USER = 'elastic';
fieldsAdded.push('ELASTICSEARCH_USER');
updated = true;
}
if (!this.config.ELASTICSEARCH_PASS) {
this.config.ELASTICSEARCH_PASS = 'elastic';
fieldsAdded.push('ELASTICSEARCH_PASS');
updated = true;
}
// Always update ELASTICSEARCH_URL based on current settings
const oldEsUrl = this.config.ELASTICSEARCH_URL;
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
if (oldEsUrl !== this.config.ELASTICSEARCH_URL) {
fieldsAdded.push('ELASTICSEARCH_URL');
updated = true;
}
if (updated) {
await this.saveConfig();
logger.log('ok', `✅ Added missing fields: ${fieldsAdded.join(', ')}`);
} else {
logger.log('ok', '✅ Configuration complete');
}
}
/**
* Get MongoDB connection string
*/
public getMongoConnectionString(useNetworkIp: boolean = false): string {
const host = useNetworkIp ? '${networkIp}' : this.config.MONGODB_HOST;
return `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${host}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
}
/**
* Get container names
*/
public getContainerNames() {
return {
mongo: `${this.config.PROJECT_NAME}-mongodb`,
minio: `${this.config.PROJECT_NAME}-minio`,
elasticsearch: `${this.config.PROJECT_NAME}-elasticsearch`
};
}
/**
* Get data directories
*/
public getDataDirectories() {
return {
mongo: plugins.path.join(process.cwd(), '.nogit', 'mongodata'),
minio: plugins.path.join(process.cwd(), '.nogit', 'miniodata'),
elasticsearch: plugins.path.join(process.cwd(), '.nogit', 'esdata')
};
}
/**
* Sync port configuration from existing Docker containers
*/
private async syncPortsFromDocker(): Promise<void> {
const containers = this.getContainerNames();
let updated = false;
// Check MongoDB container
const mongoStatus = await this.docker.getStatus(containers.mongo);
if (mongoStatus !== 'not_exists') {
const portMappings = await this.docker.getPortMappings(containers.mongo);
if (portMappings && portMappings['27017']) {
const dockerPort = portMappings['27017'];
if (this.config.MONGODB_PORT !== dockerPort) {
logger.log('note', `📍 Syncing MongoDB port from Docker: ${dockerPort}`);
this.config.MONGODB_PORT = dockerPort;
updated = true;
}
}
}
// Check MinIO container
const minioStatus = await this.docker.getStatus(containers.minio);
if (minioStatus !== 'not_exists') {
const portMappings = await this.docker.getPortMappings(containers.minio);
if (portMappings) {
if (portMappings['9000']) {
const dockerPort = portMappings['9000'];
if (this.config.S3_PORT !== dockerPort) {
logger.log('note', `📍 Syncing S3 API port from Docker: ${dockerPort}`);
this.config.S3_PORT = dockerPort;
updated = true;
}
}
if (portMappings['9001']) {
const dockerPort = portMappings['9001'];
if (this.config.S3_CONSOLE_PORT !== dockerPort) {
logger.log('note', `📍 Syncing S3 Console port from Docker: ${dockerPort}`);
this.config.S3_CONSOLE_PORT = dockerPort;
updated = true;
}
}
}
}
// Check Elasticsearch container
const esStatus = await this.docker.getStatus(containers.elasticsearch);
if (esStatus !== 'not_exists') {
const portMappings = await this.docker.getPortMappings(containers.elasticsearch);
if (portMappings && portMappings['9200']) {
const dockerPort = portMappings['9200'];
if (this.config.ELASTICSEARCH_PORT !== dockerPort) {
logger.log('note', `📍 Syncing Elasticsearch port from Docker: ${dockerPort}`);
this.config.ELASTICSEARCH_PORT = dockerPort;
updated = true;
}
}
}
if (updated) {
// Update derived fields
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
this.config.S3_ENDPOINT = this.config.S3_HOST;
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
await this.saveConfig();
logger.log('ok', '✅ Configuration synced with Docker containers');
}
}
/**
* Validate and update ports if they're not available
*/
public async validateAndUpdatePorts(): Promise<boolean> {
let updated = false;
const containers = this.getContainerNames();
// Check if containers exist - if they do, ports are fine
const mongoExists = await this.docker.exists(containers.mongo);
const minioExists = await this.docker.exists(containers.minio);
const esExists = await this.docker.exists(containers.elasticsearch);
// Only check port availability if containers don't exist
if (!mongoExists) {
const mongoPort = parseInt(this.config.MONGODB_PORT);
if (!(await helpers.isPortAvailable(mongoPort))) {
logger.log('note', `⚠️ MongoDB port ${mongoPort} is in use, finding new port...`);
const newPort = await helpers.getRandomAvailablePort();
this.config.MONGODB_PORT = newPort.toString();
logger.log('ok', `✅ New MongoDB port: ${newPort}`);
updated = true;
}
}
if (!minioExists) {
const s3Port = parseInt(this.config.S3_PORT);
const s3ConsolePort = parseInt(this.config.S3_CONSOLE_PORT);
if (!(await helpers.isPortAvailable(s3Port))) {
logger.log('note', `⚠️ S3 API port ${s3Port} is in use, finding new port...`);
const newPort = await helpers.getRandomAvailablePort();
this.config.S3_PORT = newPort.toString();
logger.log('ok', `✅ New S3 API port: ${newPort}`);
updated = true;
}
if (!(await helpers.isPortAvailable(s3ConsolePort))) {
logger.log('note', `⚠️ S3 Console port ${s3ConsolePort} is in use, finding new port...`);
let newPort = parseInt(this.config.S3_PORT) + 1;
while (!(await helpers.isPortAvailable(newPort))) {
newPort++;
}
this.config.S3_CONSOLE_PORT = newPort.toString();
logger.log('ok', `✅ New S3 Console port: ${newPort}`);
updated = true;
}
}
if (!esExists) {
const esPort = parseInt(this.config.ELASTICSEARCH_PORT);
if (!(await helpers.isPortAvailable(esPort))) {
logger.log('note', `⚠️ Elasticsearch port ${esPort} is in use, finding new port...`);
const newPort = await helpers.getRandomAvailablePort();
this.config.ELASTICSEARCH_PORT = newPort.toString();
logger.log('ok', `✅ New Elasticsearch port: ${newPort}`);
updated = true;
}
}
if (updated) {
// Update derived fields
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
this.config.S3_ENDPOINT = this.config.S3_HOST;
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
await this.saveConfig();
}
return updated;
}
/**
* Force reconfigure all ports with new available ones
*/
public async reconfigurePorts(): Promise<void> {
logger.log('note', '🔄 Finding new available ports...');
const mongoPort = await helpers.getRandomAvailablePort();
const s3Port = await helpers.getRandomAvailablePort();
let s3ConsolePort = s3Port + 1;
// Ensure console port is also available
while (!(await helpers.isPortAvailable(s3ConsolePort))) {
s3ConsolePort++;
}
// Elasticsearch uses standard port 9200
const esPort = '9200';
this.config.MONGODB_PORT = mongoPort.toString();
this.config.S3_PORT = s3Port.toString();
this.config.S3_CONSOLE_PORT = s3ConsolePort.toString();
this.config.ELASTICSEARCH_PORT = esPort;
// Update derived fields
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
this.config.S3_ENDPOINT = this.config.S3_HOST;
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
await this.saveConfig();
logger.log('ok', '✅ New port configuration:');
logger.log('info', ` 📍 MongoDB: ${mongoPort}`);
logger.log('info', ` 📍 S3 API: ${s3Port}`);
logger.log('info', ` 📍 S3 Console: ${s3ConsolePort}`);
logger.log('info', ` 📍 Elasticsearch: ${esPort}`);
}
}
+956
View File
@@ -0,0 +1,956 @@
import * as plugins from './mod.plugins.js';
import * as helpers from './helpers.js';
import { ServiceConfiguration } from './classes.serviceconfiguration.js';
import { DockerContainer } from './classes.dockercontainer.js';
import { GlobalRegistry } from './classes.globalregistry.js';
import { logger } from '../gitzone.logging.js';
export class ServiceManager {
private config: ServiceConfiguration;
private docker: DockerContainer;
private enabledServices: string[] | null = null;
private globalRegistry: GlobalRegistry;
constructor() {
this.config = new ServiceConfiguration();
this.docker = new DockerContainer();
this.globalRegistry = GlobalRegistry.getInstance();
}
/**
* Initialize the service manager
*/
public async init(): Promise<void> {
// Check Docker availability
if (!(await this.docker.checkDocker())) {
logger.log('error', 'Error: Docker is not installed. Please install Docker first.');
process.exit(1);
}
// Load or create configuration
await this.config.loadOrCreate();
logger.log('info', `📋 Project: ${this.config.getConfig().PROJECT_NAME}`);
// Load service selection from .smartconfig.json
await this.loadServiceConfiguration();
// Validate and update ports if needed
await this.config.validateAndUpdatePorts();
}
/**
* Load service configuration from .smartconfig.json
*/
private async loadServiceConfiguration(): Promise<void> {
const smartconfigInstance = new plugins.smartconfig.Smartconfig(process.cwd());
const gitzoneConfig = smartconfigInstance.dataFor<any>('@git.zone/cli', {});
// Check if services array exists
if (!gitzoneConfig.services || !Array.isArray(gitzoneConfig.services) || gitzoneConfig.services.length === 0) {
// Prompt user to select services
const smartinteract = new plugins.smartinteract.SmartInteract();
const response = await smartinteract.askQuestion({
name: 'services',
type: 'checkbox',
message: 'Which services do you want to enable for this project?',
choices: [
{ name: 'MongoDB', value: 'mongodb' },
{ name: 'MinIO (S3)', value: 'minio' },
{ name: 'Elasticsearch', value: 'elasticsearch' }
],
default: ['mongodb', 'minio', 'elasticsearch']
});
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
// Save to .smartconfig.json
await this.saveServiceConfiguration(this.enabledServices);
} else {
this.enabledServices = gitzoneConfig.services;
logger.log('info', `🔧 Enabled services: ${this.enabledServices.join(', ')}`);
}
}
/**
* Save service configuration to .smartconfig.json
*/
private async saveServiceConfiguration(services: string[]): Promise<void> {
const smartconfigPath = plugins.path.join(process.cwd(), '.smartconfig.json');
let smartconfigData: any = {};
// Read existing .smartconfig.json if it exists
if (await plugins.smartfs.file(smartconfigPath).exists()) {
const content = await plugins.smartfs.file(smartconfigPath).encoding('utf8').read();
smartconfigData = JSON.parse(content as string);
}
// Update @git.zone/cli.services
if (!smartconfigData['@git.zone/cli']) {
smartconfigData['@git.zone/cli'] = {};
}
smartconfigData['@git.zone/cli'].services = services;
// Write back to .smartconfig.json
await plugins.smartfs
.file(smartconfigPath)
.encoding('utf8')
.write(JSON.stringify(smartconfigData, null, 2));
logger.log('ok', `✅ Saved service configuration to .smartconfig.json`);
logger.log('info', `🔧 Enabled services: ${services.join(', ')}`);
}
/**
* Check if a service is enabled
*/
private isServiceEnabled(service: string): boolean {
if (!this.enabledServices) {
return true; // If no configuration, enable all
}
return this.enabledServices.includes(service);
}
/**
* Register this project with the global registry
*/
private async registerWithGlobalRegistry(): Promise<void> {
const config = this.config.getConfig();
const containers = this.config.getContainerNames();
await this.globalRegistry.registerProject({
projectPath: process.cwd(),
projectName: config.PROJECT_NAME,
containers: {
mongo: containers.mongo,
minio: containers.minio,
elasticsearch: containers.elasticsearch,
},
ports: {
mongo: parseInt(config.MONGODB_PORT),
s3: parseInt(config.S3_PORT),
s3Console: parseInt(config.S3_CONSOLE_PORT),
elasticsearch: parseInt(config.ELASTICSEARCH_PORT),
},
enabledServices: this.enabledServices || ['mongodb', 'minio', 'elasticsearch'],
});
}
/**
* Start all enabled services
*/
public async startAll(): Promise<void> {
let first = true;
if (this.isServiceEnabled('mongodb')) {
if (!first) console.log();
await this.startMongoDB();
first = false;
}
if (this.isServiceEnabled('minio')) {
if (!first) console.log();
await this.startMinIO();
first = false;
}
if (this.isServiceEnabled('elasticsearch')) {
if (!first) console.log();
await this.startElasticsearch();
first = false;
}
// Register with global registry
await this.registerWithGlobalRegistry();
}
/**
* Stop all enabled services
*/
public async stopAll(): Promise<void> {
let first = true;
if (this.isServiceEnabled('mongodb')) {
if (!first) console.log();
await this.stopMongoDB();
first = false;
}
if (this.isServiceEnabled('minio')) {
if (!first) console.log();
await this.stopMinIO();
first = false;
}
if (this.isServiceEnabled('elasticsearch')) {
if (!first) console.log();
await this.stopElasticsearch();
first = false;
}
}
/**
* Start MongoDB service
*/
public async startMongoDB(): Promise<void> {
logger.log('note', '📦 MongoDB:');
const config = this.config.getConfig();
const containers = this.config.getContainerNames();
const directories = this.config.getDataDirectories();
// Ensure data directory exists
await plugins.smartfs.directory(directories.mongo).recursive().create();
const status = await this.docker.getStatus(containers.mongo);
switch (status) {
case 'running':
logger.log('ok', ' Already running ✓');
break;
case 'stopped':
// Check if port mapping matches config
const mongoPortMappings = await this.docker.getPortMappings(containers.mongo);
if (mongoPortMappings && mongoPortMappings['27017'] !== config.MONGODB_PORT) {
logger.log('note', ' Port configuration changed, recreating container...');
await this.docker.remove(containers.mongo, true);
// Fall through to create new container
const success = await this.docker.run({
name: containers.mongo,
image: 'mongo:7.0',
ports: {
[`0.0.0.0:${config.MONGODB_PORT}`]: '27017'
},
volumes: {
[directories.mongo]: '/data/db'
},
environment: {
MONGO_INITDB_ROOT_USERNAME: config.MONGODB_USER,
MONGO_INITDB_ROOT_PASSWORD: config.MONGODB_PASS,
MONGO_INITDB_DATABASE: config.MONGODB_NAME
},
restart: 'unless-stopped',
command: '--bind_ip_all'
});
if (success) {
logger.log('ok', ' Recreated with new port ✓');
} else {
logger.log('error', ' Failed to recreate container');
}
} else {
// Ports match, just start the container
if (await this.docker.start(containers.mongo)) {
logger.log('ok', ' Started ✓');
} else {
logger.log('error', ' Failed to start');
}
}
break;
case 'not_exists':
logger.log('note', ' Creating container...');
const success = await this.docker.run({
name: containers.mongo,
image: 'mongo:7.0',
ports: {
[`0.0.0.0:${config.MONGODB_PORT}`]: '27017'
},
volumes: {
[directories.mongo]: '/data/db'
},
environment: {
MONGO_INITDB_ROOT_USERNAME: config.MONGODB_USER,
MONGO_INITDB_ROOT_PASSWORD: config.MONGODB_PASS,
MONGO_INITDB_DATABASE: config.MONGODB_NAME
},
restart: 'unless-stopped',
command: '--bind_ip_all'
});
if (success) {
logger.log('ok', ' Created and started ✓');
} else {
logger.log('error', ' Failed to create container');
}
break;
}
logger.log('info', ` Container: ${containers.mongo}`);
logger.log('info', ` Port: ${config.MONGODB_PORT}`);
logger.log('info', ` Connection: ${this.config.getMongoConnectionString()}`);
// Show Compass connection string
const networkIp = await helpers.getLocalNetworkIp();
const compassString = `mongodb://${config.MONGODB_USER}:${config.MONGODB_PASS}@${networkIp}:${config.MONGODB_PORT}/${config.MONGODB_NAME}?authSource=admin`;
logger.log('ok', ` Compass: ${compassString}`);
}
/**
* Start MinIO service
*/
public async startMinIO(): Promise<void> {
logger.log('note', '📦 S3/MinIO:');
const config = this.config.getConfig();
const containers = this.config.getContainerNames();
const directories = this.config.getDataDirectories();
// Ensure data directory exists
await plugins.smartfs.directory(directories.minio).recursive().create();
const status = await this.docker.getStatus(containers.minio);
switch (status) {
case 'running':
logger.log('ok', ' Already running ✓');
break;
case 'stopped':
// Check if port mapping matches config
const minioPortMappings = await this.docker.getPortMappings(containers.minio);
if (minioPortMappings &&
(minioPortMappings['9000'] !== config.S3_PORT ||
minioPortMappings['9001'] !== config.S3_CONSOLE_PORT)) {
logger.log('note', ' Port configuration changed, recreating container...');
await this.docker.remove(containers.minio, true);
// Fall through to create new container
const success = await this.docker.run({
name: containers.minio,
image: 'minio/minio',
ports: {
[config.S3_PORT]: '9000',
[config.S3_CONSOLE_PORT]: '9001'
},
volumes: {
[directories.minio]: '/data'
},
environment: {
MINIO_ROOT_USER: config.S3_ACCESSKEY,
MINIO_ROOT_PASSWORD: config.S3_SECRETKEY
},
restart: 'unless-stopped',
command: 'server /data --console-address ":9001"'
});
if (success) {
logger.log('ok', ' Recreated with new ports ✓');
// Wait for MinIO to be ready
await plugins.smartdelay.delayFor(3000);
// Create default bucket
await this.docker.exec(
containers.minio,
`mc alias set local http://localhost:9000 ${config.S3_ACCESSKEY} ${config.S3_SECRETKEY}`
);
await this.docker.exec(
containers.minio,
`mc mb local/${config.S3_BUCKET}`
);
logger.log('ok', ` Bucket '${config.S3_BUCKET}' created ✓`);
} else {
logger.log('error', ' Failed to recreate container');
}
} else {
// Ports match, just start the container
if (await this.docker.start(containers.minio)) {
logger.log('ok', ' Started ✓');
} else {
logger.log('error', ' Failed to start');
}
}
break;
case 'not_exists':
logger.log('note', ' Creating container...');
const success = await this.docker.run({
name: containers.minio,
image: 'minio/minio',
ports: {
[config.S3_PORT]: '9000',
[config.S3_CONSOLE_PORT]: '9001'
},
volumes: {
[directories.minio]: '/data'
},
environment: {
MINIO_ROOT_USER: config.S3_ACCESSKEY,
MINIO_ROOT_PASSWORD: config.S3_SECRETKEY
},
restart: 'unless-stopped',
command: 'server /data --console-address ":9001"'
});
if (success) {
logger.log('ok', ' Created and started ✓');
// Wait for MinIO to be ready
await plugins.smartdelay.delayFor(3000);
// Create default bucket
await this.docker.exec(
containers.minio,
`mc alias set local http://localhost:9000 ${config.S3_ACCESSKEY} ${config.S3_SECRETKEY}`
);
await this.docker.exec(
containers.minio,
`mc mb local/${config.S3_BUCKET}`
);
logger.log('ok', ` Bucket '${config.S3_BUCKET}' created ✓`);
} else {
logger.log('error', ' Failed to create container');
}
break;
}
logger.log('info', ` Container: ${containers.minio}`);
logger.log('info', ` Port: ${config.S3_PORT}`);
logger.log('info', ` Bucket: ${config.S3_BUCKET}`);
logger.log('info', ` API: http://${config.S3_HOST}:${config.S3_PORT}`);
logger.log('info', ` Console: http://${config.S3_HOST}:${config.S3_CONSOLE_PORT} (login: ${config.S3_ACCESSKEY}/***)`);
}
/**
* Start Elasticsearch service
*/
public async startElasticsearch(): Promise<void> {
logger.log('note', '📦 Elasticsearch:');
const config = this.config.getConfig();
const containers = this.config.getContainerNames();
const directories = this.config.getDataDirectories();
// Ensure data directory exists
await plugins.smartfs.directory(directories.elasticsearch).recursive().create();
const status = await this.docker.getStatus(containers.elasticsearch);
switch (status) {
case 'running':
logger.log('ok', ' Already running ✓');
break;
case 'stopped':
// Check if port mapping matches config
const esPortMappings = await this.docker.getPortMappings(containers.elasticsearch);
if (esPortMappings && esPortMappings['9200'] !== config.ELASTICSEARCH_PORT) {
logger.log('note', ' Port configuration changed, recreating container...');
await this.docker.remove(containers.elasticsearch, true);
// Fall through to create new container
const success = await this.docker.run({
name: containers.elasticsearch,
image: 'elasticsearch:8.11.0',
ports: {
[`0.0.0.0:${config.ELASTICSEARCH_PORT}`]: '9200'
},
volumes: {
[directories.elasticsearch]: '/usr/share/elasticsearch/data'
},
environment: {
'discovery.type': 'single-node',
'xpack.security.enabled': 'true',
'ELASTIC_PASSWORD': config.ELASTICSEARCH_PASS,
'ES_JAVA_OPTS': '-Xms512m -Xmx512m'
},
restart: 'unless-stopped'
});
if (success) {
logger.log('ok', ' Recreated with new port ✓');
} else {
logger.log('error', ' Failed to recreate container');
}
} else {
// Ports match, just start the container
if (await this.docker.start(containers.elasticsearch)) {
logger.log('ok', ' Started ✓');
} else {
logger.log('error', ' Failed to start');
}
}
break;
case 'not_exists':
logger.log('note', ' Creating container...');
const success = await this.docker.run({
name: containers.elasticsearch,
image: 'elasticsearch:8.11.0',
ports: {
[`0.0.0.0:${config.ELASTICSEARCH_PORT}`]: '9200'
},
volumes: {
[directories.elasticsearch]: '/usr/share/elasticsearch/data'
},
environment: {
'discovery.type': 'single-node',
'xpack.security.enabled': 'true',
'ELASTIC_PASSWORD': config.ELASTICSEARCH_PASS,
'ES_JAVA_OPTS': '-Xms512m -Xmx512m'
},
restart: 'unless-stopped'
});
if (success) {
logger.log('ok', ' Created and started ✓');
} else {
logger.log('error', ' Failed to create container');
}
break;
}
logger.log('info', ` Container: ${containers.elasticsearch}`);
logger.log('info', ` Port: ${config.ELASTICSEARCH_PORT}`);
logger.log('info', ` Connection: ${config.ELASTICSEARCH_URL}`);
logger.log('info', ` Username: ${config.ELASTICSEARCH_USER}`);
logger.log('info', ` Password: ${config.ELASTICSEARCH_PASS}`);
}
/**
* Stop MongoDB service
*/
public async stopMongoDB(): Promise<void> {
logger.log('note', '📦 MongoDB:');
const containers = this.config.getContainerNames();
const status = await this.docker.getStatus(containers.mongo);
if (status === 'running') {
if (await this.docker.stop(containers.mongo)) {
logger.log('ok', ' Stopped ✓');
} else {
logger.log('error', ' Failed to stop');
}
} else {
logger.log('note', ' Not running');
}
}
/**
* Stop MinIO service
*/
public async stopMinIO(): Promise<void> {
logger.log('note', '📦 S3/MinIO:');
const containers = this.config.getContainerNames();
const status = await this.docker.getStatus(containers.minio);
if (status === 'running') {
if (await this.docker.stop(containers.minio)) {
logger.log('ok', ' Stopped ✓');
} else {
logger.log('error', ' Failed to stop');
}
} else {
logger.log('note', ' Not running');
}
}
/**
* Stop Elasticsearch service
*/
public async stopElasticsearch(): Promise<void> {
logger.log('note', '📦 Elasticsearch:');
const containers = this.config.getContainerNames();
const status = await this.docker.getStatus(containers.elasticsearch);
if (status === 'running') {
if (await this.docker.stop(containers.elasticsearch)) {
logger.log('ok', ' Stopped ✓');
} else {
logger.log('error', ' Failed to stop');
}
} else {
logger.log('note', ' Not running');
}
}
/**
* Show service status
*/
public async showStatus(): Promise<void> {
helpers.printHeader('Service Status');
const config = this.config.getConfig();
const containers = this.config.getContainerNames();
logger.log('info', `Project: ${config.PROJECT_NAME}`);
console.log();
// MongoDB status
const mongoStatus = await this.docker.getStatus(containers.mongo);
switch (mongoStatus) {
case 'running':
logger.log('ok', '📦 MongoDB: 🟢 Running');
logger.log('info', ` ├─ Container: ${containers.mongo}`);
logger.log('info', ` ├─ Port: ${config.MONGODB_PORT}`);
logger.log('info', ` ├─ Connection: ${this.config.getMongoConnectionString()}`);
// Show Compass connection string
const networkIp = await helpers.getLocalNetworkIp();
const compassString = `mongodb://${config.MONGODB_USER}:${config.MONGODB_PASS}@${networkIp}:${config.MONGODB_PORT}/${config.MONGODB_NAME}?authSource=admin`;
logger.log('ok', ` └─ Compass: ${compassString}`);
break;
case 'stopped':
logger.log('note', '📦 MongoDB: 🟡 Stopped');
logger.log('info', ` ├─ Container: ${containers.mongo}`);
logger.log('info', ` └─ Port: ${config.MONGODB_PORT}`);
break;
case 'not_exists':
logger.log('info', '📦 MongoDB: ⚪ Not installed');
// Check port availability
const mongoPort = parseInt(config.MONGODB_PORT);
const mongoAvailable = await helpers.isPortAvailable(mongoPort);
if (!mongoAvailable) {
logger.log('error', ` └─ ⚠️ Port ${mongoPort} is in use by another process`);
} else {
logger.log('info', ` └─ Port ${mongoPort} is available`);
}
break;
}
// MinIO status
const minioStatus = await this.docker.getStatus(containers.minio);
switch (minioStatus) {
case 'running':
logger.log('ok', '📦 S3/MinIO: 🟢 Running');
logger.log('info', ` ├─ Container: ${containers.minio}`);
logger.log('info', ` ├─ API: http://${config.S3_HOST}:${config.S3_PORT}`);
logger.log('info', ` ├─ Console: http://${config.S3_HOST}:${config.S3_CONSOLE_PORT}`);
logger.log('info', ` └─ Bucket: ${config.S3_BUCKET}`);
break;
case 'stopped':
logger.log('note', '📦 S3/MinIO: 🟡 Stopped');
logger.log('info', ` ├─ Container: ${containers.minio}`);
logger.log('info', ` ├─ API Port: ${config.S3_PORT}`);
logger.log('info', ` └─ Console Port: ${config.S3_CONSOLE_PORT}`);
break;
case 'not_exists':
logger.log('info', '📦 S3/MinIO: ⚪ Not installed');
// Check port availability
const s3Port = parseInt(config.S3_PORT);
const s3ConsolePort = parseInt(config.S3_CONSOLE_PORT);
const s3Available = await helpers.isPortAvailable(s3Port);
const consoleAvailable = await helpers.isPortAvailable(s3ConsolePort);
if (!s3Available || !consoleAvailable) {
if (!s3Available) {
logger.log('error', ` ├─ ⚠️ API Port ${s3Port} is in use`);
} else {
logger.log('info', ` ├─ API Port ${s3Port} is available`);
}
if (!consoleAvailable) {
logger.log('error', ` └─ ⚠️ Console Port ${s3ConsolePort} is in use`);
} else {
logger.log('info', ` └─ Console Port ${s3ConsolePort} is available`);
}
} else {
logger.log('info', ` ├─ API Port ${s3Port} is available`);
logger.log('info', ` └─ Console Port ${s3ConsolePort} is available`);
}
break;
}
// Elasticsearch status
const esStatus = await this.docker.getStatus(containers.elasticsearch);
switch (esStatus) {
case 'running':
logger.log('ok', '📦 Elasticsearch: 🟢 Running');
logger.log('info', ` ├─ Container: ${containers.elasticsearch}`);
logger.log('info', ` ├─ Port: ${config.ELASTICSEARCH_PORT}`);
logger.log('info', ` ├─ Connection: ${config.ELASTICSEARCH_URL}`);
logger.log('info', ` └─ Credentials: ${config.ELASTICSEARCH_USER}/${config.ELASTICSEARCH_PASS}`);
break;
case 'stopped':
logger.log('note', '📦 Elasticsearch: 🟡 Stopped');
logger.log('info', ` ├─ Container: ${containers.elasticsearch}`);
logger.log('info', ` └─ Port: ${config.ELASTICSEARCH_PORT}`);
break;
case 'not_exists':
logger.log('info', '📦 Elasticsearch: ⚪ Not installed');
// Check port availability
const esPort = parseInt(config.ELASTICSEARCH_PORT);
const esAvailable = await helpers.isPortAvailable(esPort);
if (!esAvailable) {
logger.log('error', ` └─ ⚠️ Port ${esPort} is in use by another process`);
} else {
logger.log('info', ` └─ Port ${esPort} is available`);
}
break;
}
}
/**
* Show configuration
*/
public async showConfig(): Promise<void> {
helpers.printHeader('Current Configuration');
const config = this.config.getConfig();
logger.log('info', `Project: ${config.PROJECT_NAME}`);
console.log();
logger.log('note', 'MongoDB:');
logger.log('info', ` Host: ${config.MONGODB_HOST}:${config.MONGODB_PORT}`);
logger.log('info', ` Database: ${config.MONGODB_NAME}`);
logger.log('info', ` User: ${config.MONGODB_USER}`);
logger.log('info', ' Password: ***');
logger.log('info', ` Container: ${this.config.getContainerNames().mongo}`);
logger.log('info', ` Data: ${this.config.getDataDirectories().mongo}`);
logger.log('info', ` Connection: ${this.config.getMongoConnectionString()}`);
console.log();
logger.log('note', 'S3/MinIO:');
logger.log('info', ` Host: ${config.S3_HOST}`);
logger.log('info', ` API Port: ${config.S3_PORT}`);
logger.log('info', ` Console Port: ${config.S3_CONSOLE_PORT}`);
logger.log('info', ` Access Key: ${config.S3_ACCESSKEY}`);
logger.log('info', ' Secret Key: ***');
logger.log('info', ` Bucket: ${config.S3_BUCKET}`);
logger.log('info', ` Use SSL: ${config.S3_USESSL}`);
logger.log('info', ` Container: ${this.config.getContainerNames().minio}`);
logger.log('info', ` Data: ${this.config.getDataDirectories().minio}`);
logger.log('info', ` Endpoint: ${config.S3_ENDPOINT}`);
logger.log('info', ` Console URL: http://${config.S3_HOST}:${config.S3_CONSOLE_PORT}`);
console.log();
logger.log('note', 'Elasticsearch:');
logger.log('info', ` Host: ${config.ELASTICSEARCH_HOST}:${config.ELASTICSEARCH_PORT}`);
logger.log('info', ` User: ${config.ELASTICSEARCH_USER}`);
logger.log('info', ' Password: ***');
logger.log('info', ` Container: ${this.config.getContainerNames().elasticsearch}`);
logger.log('info', ` Data: ${this.config.getDataDirectories().elasticsearch}`);
logger.log('info', ` Connection: ${config.ELASTICSEARCH_URL}`);
}
/**
* Show MongoDB Compass connection string
*/
public async showCompassConnection(): Promise<void> {
helpers.printHeader('MongoDB Compass Connection');
const config = this.config.getConfig();
const networkIp = await helpers.getLocalNetworkIp();
const connectionString = `mongodb://${config.MONGODB_USER}:${config.MONGODB_PASS}@${networkIp}:${config.MONGODB_PORT}/${config.MONGODB_NAME}?authSource=admin`;
logger.log('info', 'MongoDB Compass is a GUI tool for MongoDB. To connect:');
console.log();
logger.log('info', '1. Download MongoDB Compass from:');
logger.log('info', ' https://www.mongodb.com/products/compass');
console.log();
logger.log('info', '2. Open Compass and paste this connection string:');
logger.log('ok', ` ${connectionString}`);
console.log();
logger.log('note', 'Connection Details:');
logger.log('info', ` Network IP: ${networkIp}`);
logger.log('info', ` Port: ${config.MONGODB_PORT}`);
logger.log('info', ` Database: ${config.MONGODB_NAME}`);
logger.log('info', ` Username: ${config.MONGODB_USER}`);
logger.log('info', ` Auth Source: admin`);
}
/**
* Show logs for a service
*/
public async showLogs(service: string, lines: number = 20): Promise<void> {
const containers = this.config.getContainerNames();
switch (service) {
case 'mongo':
case 'mongodb':
if (await this.docker.isRunning(containers.mongo)) {
helpers.printHeader(`MongoDB Logs (last ${lines} lines)`);
const logs = await this.docker.logs(containers.mongo, lines);
console.log(logs);
} else {
logger.log('note', 'MongoDB container is not running');
}
break;
case 'minio':
case 's3':
if (await this.docker.isRunning(containers.minio)) {
helpers.printHeader(`S3/MinIO Logs (last ${lines} lines)`);
const logs = await this.docker.logs(containers.minio, lines);
console.log(logs);
} else {
logger.log('note', 'S3/MinIO container is not running');
}
break;
case 'elasticsearch':
case 'es':
if (await this.docker.isRunning(containers.elasticsearch)) {
helpers.printHeader(`Elasticsearch Logs (last ${lines} lines)`);
const logs = await this.docker.logs(containers.elasticsearch, lines);
console.log(logs);
} else {
logger.log('note', 'Elasticsearch container is not running');
}
break;
case 'all':
case '':
await this.showLogs('mongo', lines);
console.log();
await this.showLogs('minio', lines);
console.log();
await this.showLogs('elasticsearch', lines);
break;
default:
logger.log('note', 'Usage: gitzone services logs [mongo|s3|elasticsearch|all] [lines]');
break;
}
}
/**
* Remove containers
*/
public async removeContainers(): Promise<void> {
const containers = this.config.getContainerNames();
let removed = false;
if (await this.docker.exists(containers.mongo)) {
if (await this.docker.remove(containers.mongo, true)) {
logger.log('ok', ' MongoDB container removed ✓');
removed = true;
}
}
if (await this.docker.exists(containers.minio)) {
if (await this.docker.remove(containers.minio, true)) {
logger.log('ok', ' S3/MinIO container removed ✓');
removed = true;
}
}
if (await this.docker.exists(containers.elasticsearch)) {
if (await this.docker.remove(containers.elasticsearch, true)) {
logger.log('ok', ' Elasticsearch container removed ✓');
removed = true;
}
}
if (!removed) {
logger.log('note', ' No containers to remove');
}
// Check if all containers are gone, then unregister from global registry
const mongoExists = await this.docker.exists(containers.mongo);
const minioExists = await this.docker.exists(containers.minio);
const esExists = await this.docker.exists(containers.elasticsearch);
if (!mongoExists && !minioExists && !esExists) {
await this.globalRegistry.unregisterProject(process.cwd());
}
}
/**
* Clean data directories
*/
public async cleanData(): Promise<void> {
const directories = this.config.getDataDirectories();
let cleaned = false;
if (await plugins.smartfs.directory(directories.mongo).exists()) {
await plugins.smartfs.directory(directories.mongo).recursive().delete();
logger.log('ok', ' MongoDB data removed ✓');
cleaned = true;
}
if (await plugins.smartfs.directory(directories.minio).exists()) {
await plugins.smartfs.directory(directories.minio).recursive().delete();
logger.log('ok', ' S3/MinIO data removed ✓');
cleaned = true;
}
if (await plugins.smartfs.directory(directories.elasticsearch).exists()) {
await plugins.smartfs.directory(directories.elasticsearch).recursive().delete();
logger.log('ok', ' Elasticsearch data removed ✓');
cleaned = true;
}
if (!cleaned) {
logger.log('note', ' No data to clean');
}
}
/**
* Configure which services are enabled
*/
public async configureServices(): Promise<void> {
logger.log('note', 'Select which services to enable for this project:');
console.log();
const currentServices = this.enabledServices || ['mongodb', 'minio', 'elasticsearch'];
const smartinteract = new plugins.smartinteract.SmartInteract();
const response = await smartinteract.askQuestion({
name: 'services',
type: 'checkbox',
message: 'Which services do you want to enable?',
choices: [
{ name: 'MongoDB', value: 'mongodb' },
{ name: 'MinIO (S3)', value: 'minio' },
{ name: 'Elasticsearch', value: 'elasticsearch' }
],
default: currentServices
});
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
// Save to .smartconfig.json
await this.saveServiceConfiguration(this.enabledServices);
logger.log('ok', '✅ Service configuration updated');
}
/**
* Reconfigure services with new ports
*/
public async reconfigure(): Promise<void> {
helpers.printHeader('Reconfiguring Services');
const containers = this.config.getContainerNames();
// Stop existing containers
logger.log('note', '🛑 Stopping existing containers...');
if (await this.docker.exists(containers.mongo)) {
await this.docker.stop(containers.mongo);
logger.log('ok', ' MongoDB stopped ✓');
}
if (await this.docker.exists(containers.minio)) {
await this.docker.stop(containers.minio);
logger.log('ok', ' S3/MinIO stopped ✓');
}
if (await this.docker.exists(containers.elasticsearch)) {
await this.docker.stop(containers.elasticsearch);
logger.log('ok', ' Elasticsearch stopped ✓');
}
// Reconfigure ports
await this.config.reconfigurePorts();
// Ask if user wants to restart services
const smartinteract = new plugins.smartinteract.SmartInteract();
const response = await smartinteract.askQuestion({
name: 'restart',
type: 'confirm',
message: 'Do you want to start services with new ports?',
default: true
});
if (response.value) {
console.log();
await this.startAll();
}
}
}
+127
View File
@@ -0,0 +1,127 @@
import * as plugins from './mod.plugins.js';
import * as net from 'net';
import { logger } from '../gitzone.logging.js';
/**
* Check if a port is available
*/
export const isPortAvailable = async (port: number): Promise<boolean> => {
return new Promise((resolve) => {
const server = net.createServer();
server.once('error', () => {
resolve(false);
});
server.once('listening', () => {
server.close();
resolve(true);
});
server.listen(port, '0.0.0.0');
});
};
/**
* Get a random available port between 20000 and 30000
*/
export const getRandomAvailablePort = async (): Promise<number> => {
const maxAttempts = 100;
for (let i = 0; i < maxAttempts; i++) {
const port = Math.floor(Math.random() * 10001) + 20000;
if (await isPortAvailable(port)) {
return port;
}
}
// Fallback: let the system assign a port
return 0;
};
/**
* Get the project name from package.json or directory
*/
export const getProjectName = async (): Promise<string> => {
try {
const packageJsonPath = plugins.path.join(process.cwd(), 'package.json');
if (await plugins.smartfs.file(packageJsonPath).exists()) {
const content = (await plugins.smartfs
.file(packageJsonPath)
.encoding('utf8')
.read()) as string;
const packageJson = JSON.parse(content);
if (packageJson.name) {
// Sanitize: @fin.cx/skr → fin-cx-skr
return packageJson.name.replace(/@/g, '').replace(/[\/\.]/g, '-');
}
}
} catch (error) {
// Ignore errors and fall back to directory name
}
return plugins.path.basename(process.cwd());
};
/**
* Print a header with decorative lines
*/
export const printHeader = (title: string) => {
console.log();
logger.log('info', '═══════════════════════════════════════════════════════════════');
logger.log('info', ` ${title}`);
logger.log('info', '═══════════════════════════════════════════════════════════════');
console.log();
};
/**
* Format bytes to human readable string
*/
export const formatBytes = (bytes: number): string => {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
};
/**
* Get the local network IP address
*/
export const getLocalNetworkIp = async (): Promise<string> => {
const smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
const gateways = await smartnetworkInstance.getGateways();
// Find the best local IP from network interfaces
for (const interfaceName of Object.keys(gateways)) {
const interfaces = gateways[interfaceName];
for (const iface of interfaces) {
// Skip loopback and internal interfaces
if (!iface.internal && iface.family === 'IPv4') {
const address = iface.address;
// Prefer LAN IPs
if (address.startsWith('192.168.') || address.startsWith('10.') || address.startsWith('172.')) {
return address;
}
}
}
}
// Fallback: try to get any non-internal IPv4
for (const interfaceName of Object.keys(gateways)) {
const interfaces = gateways[interfaceName];
for (const iface of interfaces) {
if (!iface.internal && iface.family === 'IPv4') {
return iface.address;
}
}
}
// Last resort: localhost
return 'localhost';
};
+800
View File
@@ -0,0 +1,800 @@
import * as plugins from "./mod.plugins.js";
import * as helpers from "./helpers.js";
import { ServiceManager } from "./classes.servicemanager.js";
import { GlobalRegistry } from "./classes.globalregistry.js";
import { logger } from "../gitzone.logging.js";
import type { ICliMode } from "../helpers.climode.js";
import { getCliMode, printJson } from "../helpers.climode.js";
import {
getCliConfigValueFromData,
readSmartconfigFile,
setCliConfigValueInData,
writeSmartconfigFile,
} from "../helpers.smartconfig.js";
export const run = async (argvArg: any) => {
const mode = await getCliMode(argvArg);
const isGlobal = argvArg.g || argvArg.global;
const command = argvArg._[1] || "help";
if (mode.help || command === "help") {
showHelp(mode);
return;
}
// Handle global commands first
if (isGlobal) {
await handleGlobalCommand(command);
return;
}
const service = argvArg._[2] || "all";
switch (command) {
case "config":
if (service === "services" || argvArg._[2] === "services") {
const serviceManager = new ServiceManager();
await serviceManager.init();
await handleConfigureServices(serviceManager);
} else {
await handleShowConfig(mode);
}
break;
case "set":
await handleSetServices(argvArg._[2], mode);
break;
case "enable":
await handleEnableServices(argvArg._.slice(2), mode);
break;
case "disable":
await handleDisableServices(argvArg._.slice(2), mode);
break;
case "start":
case "stop":
case "restart":
case "status":
case "compass":
case "logs":
case "remove":
case "clean":
case "reconfigure": {
const serviceManager = new ServiceManager();
await serviceManager.init();
switch (command) {
case "start":
await handleStart(serviceManager, service);
break;
case "stop":
await handleStop(serviceManager, service);
break;
case "restart":
await handleRestart(serviceManager, service);
break;
case "status":
await serviceManager.showStatus();
break;
case "compass":
await serviceManager.showCompassConnection();
break;
case "logs": {
const lines = parseInt(argvArg._[3]) || 20;
await serviceManager.showLogs(service, lines);
break;
}
case "remove":
await handleRemove(serviceManager);
break;
case "clean":
await handleClean(serviceManager);
break;
case "reconfigure":
await serviceManager.reconfigure();
break;
}
break;
}
default:
showHelp(mode);
break;
}
};
const allowedServices = ["mongodb", "minio", "elasticsearch"];
const normalizeServiceName = (service: string): string => {
switch (service) {
case "mongo":
case "mongodb":
return "mongodb";
case "minio":
case "s3":
return "minio";
case "elastic":
case "elasticsearch":
case "es":
return "elasticsearch";
default:
return service;
}
};
async function readServicesConfig(): Promise<{
enabledServices: string[];
environment: Record<string, any> | null;
}> {
const smartconfigData = await readSmartconfigFile();
const enabledServices = getCliConfigValueFromData(
smartconfigData,
"services",
);
let environment: Record<string, any> | null = null;
const envPath = plugins.path.join(process.cwd(), ".nogit", "env.json");
if (await plugins.smartfs.file(envPath).exists()) {
const envContent = (await plugins.smartfs
.file(envPath)
.encoding("utf8")
.read()) as string;
environment = JSON.parse(envContent);
}
return {
enabledServices: Array.isArray(enabledServices) ? enabledServices : [],
environment,
};
}
async function updateEnabledServices(services: string[]): Promise<void> {
const smartconfigData = await readSmartconfigFile();
setCliConfigValueInData(smartconfigData, "services", services);
await writeSmartconfigFile(smartconfigData);
}
async function handleShowConfig(mode: ICliMode) {
const configData = await readServicesConfig();
if (mode.json) {
printJson(configData);
return;
}
helpers.printHeader("Current Services Configuration");
logger.log(
"info",
`Enabled Services: ${configData.enabledServices.length > 0 ? configData.enabledServices.join(", ") : "none configured"}`,
);
console.log();
if (!configData.environment) {
logger.log(
"note",
"No .nogit/env.json found yet. Start a service once to create runtime defaults.",
);
return;
}
const env = configData.environment;
logger.log("note", "MongoDB:");
logger.log("info", ` Host: ${env.MONGODB_HOST}:${env.MONGODB_PORT}`);
logger.log("info", ` Database: ${env.MONGODB_NAME}`);
logger.log("info", ` User: ${env.MONGODB_USER}`);
logger.log("info", ` Container: ${env.PROJECT_NAME}-mongodb`);
logger.log(
"info",
` Data: ${plugins.path.join(process.cwd(), ".nogit", "mongodata")}`,
);
logger.log("info", ` Connection: ${env.MONGODB_URL}`);
console.log();
logger.log("note", "S3/MinIO:");
logger.log("info", ` Host: ${env.S3_HOST}`);
logger.log("info", ` API Port: ${env.S3_PORT}`);
logger.log("info", ` Console Port: ${env.S3_CONSOLE_PORT}`);
logger.log("info", ` Bucket: ${env.S3_BUCKET}`);
logger.log("info", ` Container: ${env.PROJECT_NAME}-minio`);
logger.log(
"info",
` Data: ${plugins.path.join(process.cwd(), ".nogit", "miniodata")}`,
);
logger.log("info", ` Endpoint: ${env.S3_ENDPOINT}`);
console.log();
logger.log("note", "Elasticsearch:");
logger.log(
"info",
` Host: ${env.ELASTICSEARCH_HOST}:${env.ELASTICSEARCH_PORT}`,
);
logger.log("info", ` User: ${env.ELASTICSEARCH_USER}`);
logger.log("info", ` Container: ${env.PROJECT_NAME}-elasticsearch`);
logger.log(
"info",
` Data: ${plugins.path.join(process.cwd(), ".nogit", "esdata")}`,
);
logger.log("info", ` Connection: ${env.ELASTICSEARCH_URL}`);
}
async function handleSetServices(rawValue: string | undefined, mode: ICliMode) {
if (!rawValue) {
throw new Error("Specify a comma-separated list of services");
}
const requestedServices = rawValue
.split(",")
.map((service) => normalizeServiceName(service.trim()))
.filter(Boolean);
validateRequestedServices(requestedServices);
await updateEnabledServices(requestedServices);
if (mode.json) {
printJson({ ok: true, action: "set", enabledServices: requestedServices });
return;
}
logger.log("ok", `Enabled services set to: ${requestedServices.join(", ")}`);
}
async function handleEnableServices(
requestedServices: string[],
mode: ICliMode,
) {
const normalizedServices = requestedServices.map((service) =>
normalizeServiceName(service),
);
validateRequestedServices(normalizedServices);
const configData = await readServicesConfig();
const nextServices = Array.from(
new Set([...configData.enabledServices, ...normalizedServices]),
);
await updateEnabledServices(nextServices);
if (mode.json) {
printJson({ ok: true, action: "enable", enabledServices: nextServices });
return;
}
logger.log("ok", `Enabled services: ${nextServices.join(", ")}`);
}
async function handleDisableServices(
requestedServices: string[],
mode: ICliMode,
) {
const normalizedServices = requestedServices.map((service) =>
normalizeServiceName(service),
);
validateRequestedServices(normalizedServices);
const configData = await readServicesConfig();
const nextServices = configData.enabledServices.filter(
(service) => !normalizedServices.includes(service),
);
await updateEnabledServices(nextServices);
if (mode.json) {
printJson({ ok: true, action: "disable", enabledServices: nextServices });
return;
}
logger.log("ok", `Enabled services: ${nextServices.join(", ")}`);
}
function validateRequestedServices(services: string[]): void {
if (services.length === 0) {
throw new Error("Specify at least one service");
}
const invalidServices = services.filter(
(service) => !allowedServices.includes(service),
);
if (invalidServices.length > 0) {
throw new Error(`Unknown service(s): ${invalidServices.join(", ")}`);
}
}
async function handleStart(serviceManager: ServiceManager, service: string) {
helpers.printHeader("Starting Services");
switch (service) {
case "mongo":
case "mongodb":
await serviceManager.startMongoDB();
break;
case "minio":
case "s3":
await serviceManager.startMinIO();
break;
case "elasticsearch":
case "es":
await serviceManager.startElasticsearch();
break;
case "all":
case "":
await serviceManager.startAll();
break;
default:
logger.log("error", `Unknown service: ${service}`);
logger.log("note", "Use: mongo, s3, elasticsearch, or all");
break;
}
}
async function handleStop(serviceManager: ServiceManager, service: string) {
helpers.printHeader("Stopping Services");
switch (service) {
case "mongo":
case "mongodb":
await serviceManager.stopMongoDB();
break;
case "minio":
case "s3":
await serviceManager.stopMinIO();
break;
case "elasticsearch":
case "es":
await serviceManager.stopElasticsearch();
break;
case "all":
case "":
await serviceManager.stopAll();
break;
default:
logger.log("error", `Unknown service: ${service}`);
logger.log("note", "Use: mongo, s3, elasticsearch, or all");
break;
}
}
async function handleRestart(serviceManager: ServiceManager, service: string) {
helpers.printHeader("Restarting Services");
switch (service) {
case "mongo":
case "mongodb":
await serviceManager.stopMongoDB();
await plugins.smartdelay.delayFor(2000);
await serviceManager.startMongoDB();
break;
case "minio":
case "s3":
await serviceManager.stopMinIO();
await plugins.smartdelay.delayFor(2000);
await serviceManager.startMinIO();
break;
case "elasticsearch":
case "es":
await serviceManager.stopElasticsearch();
await plugins.smartdelay.delayFor(2000);
await serviceManager.startElasticsearch();
break;
case "all":
case "":
await serviceManager.stopAll();
await plugins.smartdelay.delayFor(2000);
await serviceManager.startAll();
break;
default:
logger.log("error", `Unknown service: ${service}`);
break;
}
}
async function handleRemove(serviceManager: ServiceManager) {
helpers.printHeader("Removing Containers");
logger.log("note", "⚠️ This will remove containers but preserve data");
const shouldContinue =
await plugins.smartinteract.SmartInteract.getCliConfirmation(
"Continue?",
false,
);
if (shouldContinue) {
await serviceManager.removeContainers();
} else {
logger.log("note", "Cancelled");
}
}
async function handleClean(serviceManager: ServiceManager) {
helpers.printHeader("Clean All");
logger.log("error", "⚠️ WARNING: This will remove all containers and data!");
logger.log("error", "This action cannot be undone!");
const smartinteraction = new plugins.smartinteract.SmartInteract();
const confirmAnswer = await smartinteraction.askQuestion({
name: "confirm",
type: "input",
message: 'Type "yes" to confirm:',
default: "no",
});
if (confirmAnswer.value === "yes") {
await serviceManager.removeContainers();
console.log();
await serviceManager.cleanData();
logger.log("ok", "All cleaned ✓");
} else {
logger.log("note", "Cancelled");
}
}
async function handleConfigureServices(serviceManager: ServiceManager) {
helpers.printHeader("Configure Services");
await serviceManager.configureServices();
}
export function showHelp(mode?: ICliMode) {
if (mode?.json) {
printJson({
command: "services",
usage: "gitzone services <command> [options]",
commands: [
{
name: "config",
description:
"Show configured services and any existing runtime env.json data",
},
{
name: "set <csv>",
description: "Set the enabled service list without prompts",
},
{
name: "enable <service...>",
description: "Enable one or more services without prompts",
},
{
name: "disable <service...>",
description: "Disable one or more services without prompts",
},
{ name: "start [service]", description: "Start services" },
{ name: "stop [service]", description: "Stop services" },
{ name: "status", description: "Show service status" },
],
examples: [
"gitzone services config --json",
"gitzone services set mongodb,minio",
"gitzone services enable elasticsearch",
],
});
return;
}
helpers.printHeader("GitZone Services Manager");
logger.log("ok", "Usage: gitzone services [command] [options]");
console.log();
logger.log("note", "Commands:");
logger.log(
"info",
" start [service] Start services (mongo|s3|elasticsearch|all)",
);
logger.log(
"info",
" stop [service] Stop services (mongo|s3|elasticsearch|all)",
);
logger.log(
"info",
" restart [service] Restart services (mongo|s3|elasticsearch|all)",
);
logger.log("info", " status Show service status");
logger.log("info", " config Show current configuration");
logger.log(
"info",
" config services Configure which services are enabled",
);
logger.log(
"info",
" set <csv> Set enabled services without prompts",
);
logger.log("info", " enable <svc...> Enable one or more services");
logger.log("info", " disable <svc...> Disable one or more services");
logger.log(
"info",
" compass Show MongoDB Compass connection string",
);
logger.log(
"info",
" logs [service] Show logs (mongo|s3|elasticsearch|all) [lines]",
);
logger.log("info", " reconfigure Reassign ports and restart services");
logger.log("info", " remove Remove all containers");
logger.log("info", " clean Remove all containers and data ⚠️");
logger.log("info", " help Show this help message");
console.log();
logger.log("note", "Available Services:");
logger.log("info", " • MongoDB (mongo) - Document database");
logger.log("info", " • MinIO (s3) - S3-compatible object storage");
logger.log(
"info",
" • Elasticsearch (elasticsearch) - Search and analytics engine",
);
console.log();
logger.log("note", "Features:");
logger.log("info", " • Auto-creates .nogit/env.json with smart defaults");
logger.log(
"info",
" • Random ports (20000-30000) for MongoDB/MinIO to avoid conflicts",
);
logger.log("info", " • Elasticsearch uses standard port 9200");
logger.log(
"info",
" • Project-specific containers for multi-project support",
);
logger.log("info", " • Preserves custom configuration values");
logger.log("info", " • MongoDB Compass connection support");
console.log();
logger.log("note", "Examples:");
logger.log(
"info",
" gitzone services start # Start all services",
);
logger.log(
"info",
" gitzone services start mongo # Start only MongoDB",
);
logger.log(
"info",
" gitzone services start elasticsearch # Start only Elasticsearch",
);
logger.log(
"info",
" gitzone services stop # Stop all services",
);
logger.log(
"info",
" gitzone services status # Check service status",
);
logger.log(
"info",
" gitzone services config # Show configuration",
);
logger.log(
"info",
" gitzone services config --json # Show configuration as JSON",
);
logger.log(
"info",
" gitzone services set mongodb,minio # Configure services without prompts",
);
logger.log(
"info",
" gitzone services compass # Get MongoDB Compass connection",
);
logger.log(
"info",
" gitzone services logs elasticsearch # Show Elasticsearch logs",
);
console.log();
logger.log("note", "Global Commands (-g/--global):");
logger.log("info", " list -g List all registered projects");
logger.log("info", " status -g Show status across all projects");
logger.log(
"info",
" stop -g Stop all containers across all projects",
);
logger.log("info", " cleanup -g Remove stale registry entries");
console.log();
logger.log("note", "Global Examples:");
logger.log(
"info",
" gitzone services list -g # List all registered projects",
);
logger.log(
"info",
" gitzone services status -g # Show global container status",
);
logger.log(
"info",
" gitzone services stop -g # Stop all (prompts for confirmation)",
);
}
// ==================== Global Command Handlers ====================
async function handleGlobalCommand(command: string) {
const globalRegistry = GlobalRegistry.getInstance();
switch (command) {
case "list":
await handleGlobalList(globalRegistry);
break;
case "status":
await handleGlobalStatus(globalRegistry);
break;
case "stop":
await handleGlobalStop(globalRegistry);
break;
case "cleanup":
await handleGlobalCleanup(globalRegistry);
break;
case "help":
default:
showHelp();
break;
}
}
async function handleGlobalList(globalRegistry: GlobalRegistry) {
helpers.printHeader("Registered Projects (Global)");
const projects = await globalRegistry.getAllProjects();
const projectPaths = Object.keys(projects);
if (projectPaths.length === 0) {
logger.log("note", "No projects registered");
return;
}
for (const path of projectPaths) {
const project = projects[path];
const lastActive = new Date(project.lastActive).toLocaleString();
console.log();
logger.log("ok", `📁 ${project.projectName}`);
logger.log("info", ` Path: ${project.projectPath}`);
logger.log("info", ` Services: ${project.enabledServices.join(", ")}`);
logger.log("info", ` Last Active: ${lastActive}`);
}
}
async function handleGlobalStatus(globalRegistry: GlobalRegistry) {
helpers.printHeader("Global Service Status");
const statuses = await globalRegistry.getGlobalStatus();
if (statuses.length === 0) {
logger.log("note", "No projects registered");
return;
}
let runningCount = 0;
let totalContainers = 0;
for (const project of statuses) {
console.log();
logger.log("ok", `📁 ${project.projectName}`);
logger.log("info", ` Path: ${project.projectPath}`);
if (project.containers.length === 0) {
logger.log("note", " No containers configured");
continue;
}
for (const container of project.containers) {
totalContainers++;
const statusIcon =
container.status === "running"
? "🟢"
: container.status === "exited"
? "🟡"
: "⚪";
if (container.status === "running") runningCount++;
logger.log(
"info",
` ${statusIcon} ${container.name}: ${container.status}`,
);
}
}
console.log();
logger.log(
"note",
`Summary: ${runningCount}/${totalContainers} containers running across ${statuses.length} project(s)`,
);
}
async function handleGlobalStop(globalRegistry: GlobalRegistry) {
helpers.printHeader("Stop All Containers (Global)");
const statuses = await globalRegistry.getGlobalStatus();
// Count running containers
let runningCount = 0;
for (const project of statuses) {
for (const container of project.containers) {
if (container.status === "running") runningCount++;
}
}
if (runningCount === 0) {
logger.log("note", "No running containers found");
return;
}
logger.log(
"note",
`Found ${runningCount} running container(s) across ${statuses.length} project(s)`,
);
console.log();
// Show what will be stopped
for (const project of statuses) {
const runningContainers = project.containers.filter(
(c) => c.status === "running",
);
if (runningContainers.length > 0) {
logger.log("info", `${project.projectName}:`);
for (const container of runningContainers) {
logger.log("info", `${container.name}`);
}
}
}
console.log();
const shouldContinue =
await plugins.smartinteract.SmartInteract.getCliConfirmation(
"Stop all containers?",
false,
);
if (!shouldContinue) {
logger.log("note", "Cancelled");
return;
}
logger.log("note", "Stopping all containers...");
const result = await globalRegistry.stopAll();
if (result.stopped.length > 0) {
logger.log("ok", `Stopped: ${result.stopped.join(", ")}`);
}
if (result.failed.length > 0) {
logger.log("error", `Failed to stop: ${result.failed.join(", ")}`);
}
}
async function handleGlobalCleanup(globalRegistry: GlobalRegistry) {
helpers.printHeader("Cleanup Registry (Global)");
logger.log("note", "Checking for stale registry entries...");
const removed = await globalRegistry.cleanup();
if (removed.length === 0) {
logger.log("ok", "No stale entries found");
return;
}
logger.log(
"ok",
`Removed ${removed.length} stale entr${removed.length === 1 ? "y" : "ies"}:`,
);
for (const path of removed) {
logger.log("info", `${path}`);
}
}

Some files were not shown because too many files have changed in this diff Show More