Compare commits

...

150 Commits

Author SHA1 Message Date
jkunz 261f7ee6b2 v2.14.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-16 18:54:07 +00:00
jkunz fd7a73398c feat(cli): add machine-readable CLI help, recommendation, and configuration flows 2026-04-16 18:54:07 +00:00
jkunz f43f88a3cb v2.13.16
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-16 13:05:47 +00:00
jkunz 4c86ad62fb fix(mod_format): stop package.json formatter from modifying buildDocs and dependency entries 2026-04-16 13:05:47 +00:00
jkunz 4214a1fdf1 v2.13.15
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 19:59:26 +00:00
jkunz 1c33735799 fix(repo): no changes to commit 2026-03-24 19:59:26 +00:00
jkunz 274405e364 v2.13.14
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 19:59:13 +00:00
jkunz bf858c8650 fix(mod_format): move smartconfig file renaming into the formatter orchestrator 2026-03-24 19:59:13 +00:00
jkunz b257c82bd6 v2.13.13
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 19:42:12 +00:00
jkunz 5a1f6d8c76 fix(vscode-template): update VS Code schema matching to use .smartconfig.json 2026-03-24 19:42:12 +00:00
jkunz d44ad6e4e4 v2.13.12
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 16:56:34 +00:00
jkunz 142adfd396 fix(mod_format): render format templates through smartscaf before comparing generated files 2026-03-24 16:56:34 +00:00
jkunz b55e75d169 2.13.11
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 16:10:56 +00:00
jkunz d0d922e53b update to smartconfig 2026-03-24 16:10:51 +00:00
jkunz eda67395fe v2.13.10
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 15:05:07 +00:00
jkunz 470e87eb79 fix(config): migrate configuration handling from npmextra to smartconfig 2026-03-24 15:05:07 +00:00
jkunz 3358a0eacc v2.13.9
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 10m42s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-03-11 19:10:18 +00:00
jkunz b65fac6257 fix(deps,readme): bump dependencies and update README to prefer pnpm and document semantic commit flags 2026-03-11 19:10:18 +00:00
jkunz 4ab59609e6 v2.13.8
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 12m24s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-03-05 11:30:34 +00:00
jkunz 32f106291f fix(dependencies): move runtime tooling packages from devDependencies to dependencies 2026-03-05 11:30:34 +00:00
jkunz b8aa5d61f6 v2.13.7
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 12m15s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-03-05 10:25:44 +00:00
jkunz 71759c276e fix(deps): bump devDependencies: @git.zone/tsbuild to ^4.1.4 and @push.rocks/smartshell to ^3.3.7 2026-03-05 10:25:44 +00:00
jkunz 7938f12d43 v2.13.6
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 14m42s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-02-01 16:19:37 +00:00
jkunz 3722258d69 fix(templates/npm): use tsbuild tsfolders instead of --web flag in npm template build script 2026-02-01 16:19:37 +00:00
jkunz 68859d0e97 v2.13.5
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 10m44s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-02-01 16:18:37 +00:00
jkunz ecadbc7a86 fix(templates/npm): update npm template: tweak test script, bump devDependencies, add smartpath dependency, and fix ts import path 2026-02-01 16:18:37 +00:00
jkunz 0243bc5ec7 v2.13.4
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 12m1s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2026-01-12 17:57:00 +00:00
jkunz 92e618104f fix(core): update tsbuild to 4.1.2 with cross-module import path fix 2026-01-12 17:57:00 +00:00
jkunz c089c1f80d v2.13.3
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-18 13:57:13 +00:00
jkunz 10a394c7d8 fix(tsconfig): remove experimentalDecorators and useDefineForClassFields from TypeScript configuration files 2025-12-18 13:57:13 +00:00
jkunz 5980308bb8 v2.13.2
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-16 13:00:30 +00:00
jkunz 398e36bdf7 fix(deps): bump @git.zone/tspublish to ^1.11.0 2025-12-16 13:00:30 +00:00
jkunz 1e78517547 v2.13.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-16 12:58:58 +00:00
jkunz 55700ad87e fix(npmextra): merge old npmextra keys into new keys during migration, preserving existing new values 2025-12-16 12:58:58 +00:00
jkunz 773df5268b v2.13.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-16 10:49:41 +00:00
jkunz b51fa88283 feat(tests): feat(tests): add sandbox test fixture, CI and editor configs; bump deps 2025-12-16 10:49:41 +00:00
jkunz cb9f717d54 v2.12.2
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:46:17 +00:00
jkunz 70be11894c fix(cli): noop: no changes 2025-12-15 17:46:17 +00:00
jkunz 89ab63b153 update 2025-12-15 17:45:28 +00:00
jkunz 44c193d4a8 v2.12.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:44:09 +00:00
jkunz 44d259a0ae fix(cli): No changes detected — no version bump required 2025-12-15 17:44:09 +00:00
jkunz f0adff8784 v2.12.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:34:36 +00:00
jkunz fb453e62c3 feat(ci,test): feat(ci/test): add test scaffold, GitLab CI, update gitea workflows and .gitignore 2025-12-15 17:34:36 +00:00
jkunz 001721a8e9 v2.11.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:30:51 +00:00
jkunz b191464ff9 fix(mod_format/formatters): fix(packagejson.formatter): correctly parse scoped package dependency arguments and default to latest 2025-12-15 17:30:51 +00:00
jkunz 4d7eaa238f v2.11.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:24:17 +00:00
jkunz 601e0d1063 feat(mod_format): feat(mod_format): use unified diff formatter with filenames and context in BaseFormatter.displayDiff 2025-12-15 17:24:17 +00:00
jkunz 4bb1a2f8c7 v2.10.0
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 17:07:30 +00:00
jkunz b506bf8785 feat(mod_format): Refactor formatting modules to new BaseFormatter and implement concrete analyze/apply logic 2025-12-15 17:07:30 +00:00
jkunz d5fbeb3fc6 2.9.2
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 15:55:36 +00:00
jkunz 2ecdeff3dc update 2025-12-15 15:55:27 +00:00
jkunz 5a663ae767 2.9.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-15 15:25:30 +00:00
jkunz 218c84a39b update 2025-12-15 15:25:20 +00:00
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
80 changed files with 12847 additions and 8532 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:
+6
View File
@@ -16,4 +16,10 @@ node_modules/
dist/ dist/
dist_*/ dist_*/
# AI
.claude/
.serena/
#------# custom #------# custom
.serena
test-output.json
+12 -5
View File
@@ -1,10 +1,13 @@
{ {
"npmci": { "szci": {
"npmGlobalTools": [], "npmGlobalTools": [],
"npmAccessLevel": "private", "npmAccessLevel": "private",
"npmRegistryUrl": "verdaccio.lossless.one" "npmRegistryUrl": "verdaccio.lossless.one"
}, },
"gitzone": { "tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
},
"@git.zone/cli": {
"projectType": "npm", "projectType": "npm",
"module": { "module": {
"githost": "gitlab.com", "githost": "gitlab.com",
@@ -31,9 +34,13 @@
"Docker", "Docker",
"CI/CD" "CI/CD"
] ]
}
}, },
"tsdoc": { "release": {
"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" "registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
}
} }
} }
+8
View File
@@ -19,4 +19,12 @@ node_modules/
dist/ dist/
dist_*/ dist_*/
# rust
rust/target/
dist_rust/
# AI
.claude/
.serena/
#------# custom #------# custom
+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';
+7 -7
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.5.4" "@types/node": "^24.10.1"
}, },
"dependencies": { "dependencies": {
"@api.global/typedserver": "^3.0.53", "@api.global/typedserver": "^3.0.53",
"@push.rocks/projectinfo": "^5.0.1", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/qenv": "^6.1.0", "@push.rocks/qenv": "^6.1.0",
"@push.rocks/smartdata": "^5.0.7", "@push.rocks/smartdata": "^5.0.7",
"@push.rocks/smartpath": "^5.0.5", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartstate": "^2.0.0" "@push.rocks/smartstate": "^2.0.0"
}, }
} }
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
"gitzone": { "@git.zone/cli": {
"projectType": "{{projectType}}", "projectType": "{{projectType}}",
"module": { "module": {
"githost": "{{module.githost}}", "githost": "{{module.githost}}",
@@ -11,7 +11,7 @@
"projectDomain": "{{module.projectDomain}}" "projectDomain": "{{module.projectDomain}}"
} }
}, },
"npmci": { "@ship.zone/szci": {
"npmGlobalTools": [], "npmGlobalTools": [],
"dockerRegistryRepoMap": { "dockerRegistryRepoMap": {
"registry.gitlab.com": "{{dockerTargetImagePath}}" "registry.gitlab.com": "{{dockerTargetImagePath}}"
@@ -1,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,12 @@
"npmPackagename": "{{module.npmPackagename}}", "npmPackagename": "{{module.npmPackagename}}",
"license": "{{module.license}}", "license": "{{module.license}}",
"projectDomain": "{{module.projectDomain}}" "projectDomain": "{{module.projectDomain}}"
},
"release": {
"accessLevel": "{{module.npmAccessLevel}}"
} }
}, },
"npmci": { "@ship.zone/szci": {
"npmGlobalTools": [], "npmGlobalTools": []
"npmAccessLevel": "{{module.npmAccessLevel}}"
} }
} }
@@ -1,8 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"useDefineForClassFields": false,
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"json.schemas": [ "json.schemas": [
{ {
"fileMatch": ["/npmextra.json"], "fileMatch": ["/.smartconfig.json"],
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
"gitzone": { "@git.zone/cli": {
"projectType": "website", "projectType": "website",
"module": { "module": {
"githost": "{{module.githost}}", "githost": "{{module.githost}}",
@@ -11,7 +11,7 @@
"projectDomain": "{{module.projectDomain}}" "projectDomain": "{{module.projectDomain}}"
} }
}, },
"npmci": { "@ship.zone/szci": {
"npmGlobalTools": [], "npmGlobalTools": [],
"dockerRegistryRepoMap": { "dockerRegistryRepoMap": {
"registry.gitlab.com": "{{docker.registryImageTag}}" "registry.gitlab.com": "{{docker.registryImageTag}}"
+631 -3
View File
@@ -1,6 +1,571 @@
# Changelog # Changelog
## 2026-04-16 - 2.14.0 - feat(cli)
add machine-readable CLI help, recommendation, and configuration flows
- introduces shared CLI mode handling for human, plain, and JSON output with configurable interactivity and update checks
- adds read-only JSON support for `commit recommend`, `format plan`, and command help output
- expands `config` and `services` commands with non-interactive config inspection and service enablement flows
- updates format and smartconfig handling to respect non-interactive execution and fail clearly when required metadata is missing
## 2026-04-16 - 2.13.16 - fix(mod_format)
stop package.json formatter from modifying buildDocs and dependency entries
- removes automatic buildDocs script injection from the package.json formatter
- removes dependency include/exclude and latest-version update logic from package.json formatting
- drops the unused smartnpm plugin import after removing registry lookups
## 2026-03-24 - 2.13.15 - fix(repo)
no changes to commit
## 2026-03-24 - 2.13.14 - fix(mod_format)
move smartconfig file renaming into the formatter orchestrator
- Renames smartconfig.json or npmextra.json to .smartconfig.json before formatters run
- Simplifies the smartconfig formatter to only read and modify .smartconfig.json
- Removes create/delete change planning for config renames and applies only content updates within the formatter
## 2026-03-24 - 2.13.13 - fix(vscode-template)
update VS Code schema matching to use .smartconfig.json
- Changes the VS Code template settings so the JSON schema applies to /.smartconfig.json instead of /npmextra.json.
## 2026-03-24 - 2.13.12 - fix(mod_format)
render format templates through smartscaf before comparing generated files
- adds smartscaf-based in-memory template rendering so supplied variables are applied before detecting changes
- supports release.accessLevel as a fallback when selecting public vs private CI templates
- matches rendered output by template or destination path to handle renamed files from template frontmatter
## 2026-03-24 - 2.13.10 - fix(config)
migrate configuration handling from npmextra to smartconfig
- replace @push.rocks/npmextra with @push.rocks/smartconfig across config, commit, format, and service modules
- switch managed project config file references from npmextra.json to smartconfig.json
- update formatting and package metadata checks to include smartconfig.json
- extend the gitignore template with Rust build output directories
## 2026-03-11 - 2.13.9 - fix(deps,readme)
bump dependencies and update README to prefer pnpm and document semantic commit flags
- Dev dependency updates: @git.zone/tsbuild -> ^4.3.0, @git.zone/tstest -> ^3.3.2, @types/node -> ^25.4.0
- Dependency upgrades: @git.zone/tsdoc -> ^2.0.0, @git.zone/tspublish -> ^1.11.2, @push.rocks/lik -> ^6.3.1, @push.rocks/smartfs -> ^1.5.0, @push.rocks/smartlog -> ^3.2.1, @push.rocks/smartstream -> ^3.4.0, prettier -> ^3.8.1 (and other minor/patch bumps)
- README changes: prefer pnpm for global install, clarify format command dry-run behavior and --write flag, add and document gitzone commit flags (-y/--yes, -p/--push, -t/--test, -b/--build, -r/--release) and AI-powered commit workflow
- No source code changes; this is a documentation and dependency refresh, recommend a patch release
## 2026-03-05 - 2.13.8 - fix(dependencies)
move runtime tooling packages from devDependencies to dependencies
- Removed @push.rocks/smartdelay, @push.rocks/smartinteract, @push.rocks/smartnetwork, and @push.rocks/smartshell from devDependencies and added them to dependencies
- No package version numbers were changed; this ensures the moved packages are installed for consumers at runtime
## 2026-03-05 - 2.13.7 - fix(deps)
bump devDependencies: @git.zone/tsbuild to ^4.1.4 and @push.rocks/smartshell to ^3.3.7
- Updated @git.zone/tsbuild from ^4.1.2 to ^4.1.4 (patch)
- Updated @push.rocks/smartshell from ^3.3.0 to ^3.3.7 (patch)
## 2026-02-01 - 2.13.6 - fix(templates/npm)
use tsbuild tsfolders instead of --web flag in npm template build script
- Changed build script in assets/templates/npm/.package.json from "(tsbuild --web --allowimplicitany)" to "(tsbuild tsfolders --allowimplicitany)"
- Replaces --web flag with explicit tsfolders argument to correctly target project folders during build
## 2026-02-01 - 2.13.5 - fix(templates/npm)
update npm template: tweak test script, bump devDependencies, add smartpath dependency, and fix ts import path
- test script updated: '(tstest test/ --web)' -> '(tstest test/ --verbose --logfile --timeout 60)'
- devDependencies bumped: @git.zone/tsbuild ^3.1.2 -> ^4.1.2, @git.zone/tsrun ^2.0.0 -> ^2.0.1, @git.zone/tstest ^3.1.3 -> ^3.1.8, @types/node ^24.10.1 -> ^25.2.0
- dependencies: added @push.rocks/smartpath ^6.0.0
- TypeScript template import fixed: './{{module.name}}.plugins.js' -> './plugins.js'
## 2025-12-18 - 2.13.3 - fix(tsconfig)
remove experimentalDecorators and useDefineForClassFields from TypeScript configuration files
- Removed "experimentalDecorators": true from assets/templates/multienv/deno.json and tsconfig.json
- Removed "useDefineForClassFields": false from tsconfig.json
- This change alters TypeScript/Deno compiler behavior: decorator support and legacy class-field initialization semantics may be affected; code relying on those may need updates
## 2025-12-16 - 2.13.2 - fix(deps)
bump @git.zone/tspublish to ^1.11.0
- Updated dependency @git.zone/tspublish from ^1.10.3 to ^1.11.0 in package.json
## 2025-12-16 - 2.13.1 - fix(npmextra)
merge old npmextra keys into new keys during migration, preserving existing new values
- Changed migration logic to merge data when both old and new keys exist instead of skipping the merge.
- Merge preserves existing new-key values (old values do not overwrite new ones) and still deletes the old key after migration.
- Applied the fix in both ts/mod_format/format.npmextra.ts and ts/mod_format/formatters/npmextra.formatter.ts.
- Adds a console log for successful migrations; behavior for single-key rename remains unchanged.
## 2025-12-16 - 2.13.0 - feat(tests)
feat(tests): add sandbox test fixture, CI and editor configs; bump deps
- Added comprehensive test/ fixture (sandbox-npmts) including package.json, npmextra.json, readme, GitLab CI (.gitlab-ci.yml), .npmrc, VSCode launch/settings and qenv.yml
- Added test sources and helper files under test/test and test/ts (browser and node tests, commitinfo data, simple library code)
- Updated dependencies in package.json: @git.zone/tsdoc -> ^1.11.4, @push.rocks/smartfs -> ^1.3.1
## 2025-12-15 - 2.12.2 - fix(cli)
noop: no changes
- No source or documentation changes detected in the diff; nothing to release.
- Package version remains unchanged at 2.12.1.
## 2025-12-15 - 2.12.1 - fix(cli)
No changes detected — no version bump required
- Current package version: 2.12.0
- No files changed in this commit
- No release or version bump necessary
## 2025-12-15 - 2.12.0 - feat(ci,test)
feat(ci/test): add test scaffold, GitLab CI, update gitea workflows and .gitignore
- Add comprehensive test/ scaffold including sample tests, test package.json, npmextra.json, test fixtures and TypeScript test sources
- Add GitLab CI pipeline (test/.gitlab-ci.yml), test-specific .npmrc, VSCode launch/settings and supporting qenv/readme files for CI/local test runs
- Update .gitea workflow YAML files to use templated placeholders and corrected container image path for CI execution
- Update .gitignore to exclude AI assistant and tooling dirs (.claude/, .serena/) and add test/.gitignore to ignore test artifacts
- Update changelog and documentation files to reflect recent formatter/commit/service changes and the new test/CI additions
## 2025-12-15 - 2.11.1 - fix(mod_format/formatters)
fix(packagejson.formatter): correctly parse scoped package dependency arguments and default to latest
- Handle scoped packages (e.g. @scope/name@version) by detecting the last '@' after the scope slash so package name and version are split correctly.
- Fallback to 'latest' when no version is provided.
- Fixes earlier incorrect splitting on every '@' which broke scoped package names.
## 2025-12-15 - 2.11.0 - feat(mod_format)
feat(mod_format): use unified diff formatter with filenames and context in BaseFormatter.displayDiff
- Replaced plugins.smartdiff.formatLineDiffForConsole(...) with plugins.smartdiff.formatUnifiedDiffForConsole(...) when both before and after are present.
- Passes originalFileName and revisedFileName as diff.path and sets context to 3 to show a unified diff with surrounding lines.
- Improves console output for multi-line diffs by using unified diff format and including file names.
## 2025-12-15 - 2.10.0 - feat(mod_format)
Refactor formatting modules to new BaseFormatter and implement concrete analyze/apply logic
- Replace generic LegacyFormatter with explicit BaseFormatter implementations for formatters: copy, gitignore, license, npmextra, packagejson, prettier, readme, templates, tsconfig (legacy.formatter.ts removed).
- Copy formatter: implemented pattern-based copying, template-preserve path handling, content equality check and planned change generation/apply.
- Gitignore formatter: canonical template with preservation of custom section when updating/creating .gitignore.
- License formatter: added runtime license check against node_modules for incompatible licenses and reporting (no file changes).
- Npmextra formatter: automatic migrations for old namespace keys to package-scoped keys and migration of npmAccessLevel -> @git.zone/cli.release.accessLevel; reformatting and interactive prompting to fill missing repo metadata.
- Package.json formatter: enforces repository/metadata, sets module type/private/license/scripts/files, ensures/updates dependencies (including fetching latest via registry), and applies pnpm overrides from assets.
- Prettier formatter: added check() to compute diffs by running Prettier and returning per-file before/after diffs.
- Readme formatter: create readme.md and readme.hints.md when missing with default content.
- Templates formatter: apply templates from templatesDir based on project type (vscode, CI, docker, website/service/wcc), compare template vs destination and create/modify files as needed; ensures dest directories exist.
- Tsconfig formatter: sets compilerOptions.baseUrl and computes path mappings from @git.zone/tspublish modules.
- General: extensive use of plugins (smartfs, path, smartnpm, smartinteract, smartobject, smartlegal), improved logging and verbose messages.
## 2025-12-15 - 2.9.0 - feat(format)
Add --diff option to format command to display file diffs; pass flag through CLI and show formatter diffs. Bump @git.zone/tsdoc to ^1.11.0.
- Add a diff boolean option to mod_format to enable showing file diffs during format operations.
- CLI change: pass argvArg.diff into the options so the --diff flag is honored by the format command.
- When diff is enabled, run formatter.check() for each active formatter and call displayAllDiffs() for those with differences, with informational logging.
- Update dependency @git.zone/tsdoc from ^1.10.2 to ^1.11.0.
## 2025-12-15 - 2.8.0 - feat(commit)
Add commit configuration and automatic pre-commit tests
- Add CommitConfig class to manage @git.zone/cli.commit settings in npmextra.json (alwaysTest, alwaysBuild).
- Export CommitConfig from mod_config for use by the CLI.
- Add 'gitzone config commit' subcommand with interactive and direct-setting modes (alwaysTest, alwaysBuild).
- Merge CLI flags and npmextra config: -t/--test and -b/--build now respect commit.alwaysTest and commit.alwaysBuild.
- Run 'pnpm test' early in the commit flow when tests are enabled; abort the commit on failing tests and log results.
- Update commit UI/plan to show the test option and include the test step when enabled.
- Add 'gitzone config services' entry to configure services via ServiceManager.
## 2025-12-14 - 2.7.0 - feat(mod_format)
Add check-only formatting with interactive diff preview; make formatting default to dry-run and extend formatting API
- Add BaseFormatter.check(), displayDiff() and displayAllDiffs() to compute and render diffs without applying changes.
- Extend runFormatter API with new options: write (use to apply changes), checkOnly (only check for diffs), and showDiff (display diffs). When checkOnly is used, runFormatter returns an ICheckResult.
- Change default formatting behavior to dry-run. Use --write / -w to actually apply changes. CLI format command updated to respect --write/-w.
- Add formatNpmextraWithDiff in mod_config to preview diffs for npmextra.json and prompt the user before applying changes; calls to add/remove/clear registries and set access level now use this preview flow.
- Project.fromCwd now accepts an options object ({ requireProjectType?: boolean }) so callers can skip the projectType requirement when appropriate; runFormatter no longer requires projectType for certain formatters.
- Introduce a list of formatters that don't require projectType: npmextra, prettier, cleanup, packagejson.
- Export the ICheckResult type from the formatter module and update mod_format interfaces to include ICheckResult.
- Bump dependency @push.rocks/smartdiff to ^1.1.0.
## 2025-12-14 - 2.6.1 - fix(npmextra)
Normalize npmextra.json: move tsdoc legal entry and reposition @git.zone/cli configuration
- Move TSDoc legal text into a top-level "tsdoc.legal" property in npmextra.json
- Remove the misplaced "gitzone" object that previously contained the TSDoc legal block
- Reorder and properly nest the "@git.zone/cli" configuration to follow the new "tsdoc" section
- Pure configuration change (JSON structure) — no functional code changes
## 2025-12-14 - 2.6.0 - feat(mod_commit)
Add execution plan output to commit command
- Print an execution plan at the start of the commit flow (shows active options and planned steps)
- New printExecutionPlan(options) added to ts/mod_commit/mod.ui.ts
- Commit command now calls ui.printExecutionPlan(...) before running format or other steps
- Execution plan reflects flags: auto-accept (-y), push (-p), build (-b), release (-r), --format, and target registries
## 2025-12-14 - 2.5.0 - feat(mod_standard)
Add interactive main menu and help to standard CLI module; route commands via dynamic imports
- Introduce interactive CLI menu using @push.rocks/smartinteract to prompt user for actions.
- Add routing to commands (commit, format, config, template, services, open, help) using dynamic imports.
- Add showHelp() helper to display usage and available commands.
- Remove previous static template listing and logger.warn placeholder.
## 2025-12-14 - 2.4.0 - feat(cli)
Add optional build step to release flow and auto-format npmextra config when registries change
- Introduce a --build/-b flag in the commit/release flow to run 'pnpm build' before pushing/releases
- Verify the working tree is clean after the build and abort the release if build produces uncommitted changes
- Increase total step counting to include build and verification steps in the UI progress output
- Add a runFormatter utility to the formatting module to execute a single formatter programmatically
- Wire runFormatter('npmextra') into mod_config so npmextra.json is formatted automatically after add/remove/clear/access operations
- Add npmextra registry config entry (https://verdaccio.lossless.digital) to npmextra.json
## 2025-12-14 - 2.3.0 - feat(config)
Add interactive menu and help to config command, handle unknown commands, and bump dependencies
- When running the 'config' command with no arguments, show an interactive menu (via SmartInteract) to choose actions (show, add, remove, clear, access, help) instead of defaulting to 'show'.
- Add explicit 'help' subcommand and log an error for unknown commands before showing help.
- Update devDependencies: @git.zone/tsbuild -> ^4.0.2, @types/node -> ^25.0.2.
- Update dependency: @push.rocks/smartjson -> ^6.0.0.
## 2025-12-04 - 2.2.1 - fix(commit)
Prevent auto-accept for BREAKING CHANGE commits; require manual confirmation and warn when --yes is used
- Do not auto-accept AI commit recommendations when the suggested change is a BREAKING CHANGE (major bump).
- Only auto-accept recommendations when -y/--yes is provided and the change is not breaking.
- When --yes is provided but the recommendation is a breaking change, log a warning and fall back to interactive confirmation.
- Introduced isBreakingChange and canAutoAccept flags to centralize the auto-accept logic.
## 2025-12-02 - 2.2.0 - feat(services)
Improve services manager and configuration; switch test templates to @git.zone/tstest; bump dev dependencies and update docs
- services: Add robust ServiceConfiguration (creates .nogit/env.json with sane defaults, syncs ports from existing Docker containers, validates and can reconfigure ports)
- services CLI: improved start/stop/restart flows, better logging/help output and enhanced global commands (list/status/stop/cleanup)
- templates/tests: replace @push.rocks/tapbundle with @git.zone/tstest and update template test.ts to export default tap.start()
- format: stop auto-updating tslint template and mark @push.rocks/tapbundle as deprecated in package formatting logic
- dependencies: bump @git.zone/tsbuild, @git.zone/tsrun, @git.zone/tstest, @git.zone/tsdoc, @push.rocks/projectinfo, @push.rocks/smartpath, @push.rocks/smartfs, prettier and other dev deps
- docs: README updates — add issue reporting/security section, AI-powered commit recommendation notes, and clarify trademark/legal wording
## 2025-11-29 - 2.1.0 - feat(mod_services)
Add global service registry and global commands for managing project containers
- Introduce GlobalRegistry class to track registered projects, their containers, ports and last activity (ts/mod_services/classes.globalregistry.ts)
- Add global CLI mode for services (use -g/--global) with commands: list, status, stop, cleanup (ts/mod_services/index.ts)
- ServiceManager now registers the current project with the global registry when starting services and unregisters when all containers are removed (ts/mod_services/classes.servicemanager.ts)
- Global handlers to list projects, show aggregated status, stop containers across projects and cleanup stale entries
- Bump dependency @push.rocks/smartfile to ^13.1.0 in package.json
## 2025-11-27 - 2.0.0 - BREAKING CHANGE(core)
Migrate filesystem to smartfs (async) and add Elasticsearch service support; refactor format/commit/meta modules
- Replace @push.rocks/smartfile usage with @push.rocks/smartfs across the codebase; all filesystem operations are now async (SmartFs.file(...).read()/write(), SmartFs.directory(...).list()/create()/delete(), etc.)
- Convert formerly synchronous helpers and APIs to async (notable: detectProjectType, getProjectName, readCurrentVersion and related version bumping logic). Callers updated accordingly.
- Add Elasticsearch support to services: new config fields (ELASTICSEARCH\_\*), Docker run/start/stop/logs/status handling, and ELASTICSEARCH_URL in service configuration.
- Refactor formatting subsystem: cache and rollback/backup systems removed/disabled for stability, format planner execution simplified (sequential), diff/stats reporting updated to use smartfs.
- Update package.json dependencies: bump @git.zone/tsbuild, tsrun, tstest; upgrade @push.rocks/smartfile to v13 and add @push.rocks/smartfs dependency; update @types/node.
- Update commit flow and changelog generation to use smartfs for reading/writing files and to await version/branch detection where necessary.
- Expose a SmartFs instance via plugins and adjust all mod.\* plugin files to import/use smartfs where required.
- Breaking change: Public and internal APIs that previously used synchronous smartfile APIs are now asynchronous. Consumers and scripts must await these functions and use the new smartfs API.
## 2025-11-17 - 1.21.5 - fix(tsconfig)
Remove emitDecoratorMetadata from tsconfig template
- Removed the "emitDecoratorMetadata" compiler option from assets/templates/tsconfig_update/tsconfig.json
- This updates the tsconfig template to avoid emitting decorator metadata when targeting ES2022
## 2025-11-17 - 1.21.4 - fix(tsconfig template)
Remove experimentalDecorators and useDefineForClassFields from tsconfig template
- Removed experimentalDecorators option from assets/templates/tsconfig_update/tsconfig.json
- Removed useDefineForClassFields option from assets/templates/tsconfig_update/tsconfig.json
## 2025-11-17 - 1.21.3 - fix(assets/templates/multienv)
Remove unused Bun configuration template (assets/templates/multienv/bunfig.toml)
- Deleted assets/templates/multienv/bunfig.toml which previously provided Bun TypeScript decorator configuration
- Cleans up stale/unused template to avoid shipping obsolete Bun config
- No functional code changes; removes an unused asset file
## 2025-11-17 - 1.21.2 - fix(templates/multienv)
Disable useDefineForClassFields in multienv TypeScript configs to ensure decorator compatibility
- Set useDefineForClassFields = false in assets/templates/multienv/bunfig.toml to keep Bun's transpiler compatible with decorator usage
- Set "useDefineForClassFields": false in assets/templates/multienv/deno.json to ensure Deno/TypeScript compiler emits class fields compatible with decorators
## 2025-11-17 - 1.21.1 - fix(templates.multienv)
Enable checkJs in multienv Deno template to enable JS type checking
- Added "checkJs": true to compilerOptions in assets/templates/multienv/deno.json to enable JavaScript type checking for the Deno multienv template
## 2025-11-17 - 1.21.0 - feat(multienv)
Add multi-env templates enabling TypeScript decorators for Bun and Deno; rename npmextra config key to szci
- Added assets/templates/multienv/bunfig.toml to enable Bun TypeScript transpiler experimentalDecorators
- Added assets/templates/multienv/deno.json with experimentalDecorators, lib and target set for ES2022
- Updated npmextra.json: renamed top-level config key from "npmci" to "szci" (keeps npmGlobalTools, npmAccessLevel and npmRegistryUrl unchanged)
## 2025-11-06 - 1.20.0 - feat(commit)
Add non-interactive --yes (-y) flag to commit command to auto-accept AI recommendations and optionally push with -p
- Add -y / --yes flag to gitzone commit to auto-accept AI-generated commit recommendations without interactive prompts
- Support -yp or -y -p combinations to auto-accept and push to origin; -p / --push remains the separate control for pushing
- Implementation creates a smartinteract AnswerBucket programmatically when -y is used and populates commitType, commitScope, commitDescription and pushToOrigin
- Preserves existing UI output and interactive flow when -y is not used; fully backward compatible and CI/CD friendly
- Updated CLI usage and documentation (readme.hints.md) to document the new flags
## 2025-11-05 - 1.19.9 - fix(mod_commit)
Refactor version bumping to a unified implementation for npm and Deno; remove npm-exec based helpers and add file-based version readers/updaters to avoid npm warning pollution
- Removed legacy npm/deno-specific helpers (bumpNpmVersion, syncVersionToDenoJson, bumpDenoVersion) that relied on executing npm and caused warning pollution
- Added readCurrentVersion() to read version from package.json or deno.json
- Added updateVersionFile() helper to write version directly into JSON files
- Added unified bumpProjectVersion() that handles npm, deno and both with a single code path; reuses calculateNewVersion()
- Stages updated files, commits v<newVersion> and creates a tag v<newVersion>
- Benefits: no npm warning pollution in deno.json, simpler git history, consistent behavior across project types
## 2025-11-04 - 1.19.8 - fix(package.json)
Bump @git.zone/tsdoc dependency to ^1.9.2
- Updated dependency @git.zone/tsdoc from ^1.9.1 to ^1.9.2 in package.json
## 2025-11-04 - 1.19.7 - fix(dependencies)
Bump @git.zone/tsdoc to ^1.9.1
- Updated package.json dependency @git.zone/tsdoc from ^1.9.0 to ^1.9.1
## 2025-11-04 - 1.19.6 - fix(cli)
Bump @git.zone/tsdoc dependency to ^1.9.0
- Updated dependency @git.zone/tsdoc from ^1.8.3 to ^1.9.0 in package.json
## 2025-11-04 - 1.19.5 - fix(cli)
Bump @git.zone/tsdoc to ^1.8.3 and add local .claude settings for allowed permissions
- Updated dependency @git.zone/tsdoc from ^1.8.2 to ^1.8.3
- Added .claude/settings.local.json to declare allowed permissions for local tooling (Bash commands, Docker, npm, WebFetch and MCP actions)
## 2025-11-03 - 1.19.3 - fix(tsdoc)
Bump @git.zone/tsdoc to ^1.8.0 and add .claude local settings
- Upgrade dependency @git.zone/tsdoc from ^1.6.1 to ^1.8.0 in package.json
- Add .claude/settings.local.json for local assistant permissions/configuration
## 2025-11-03 - 1.19.2 - fix(tsdoc)
Bump @git.zone/tsdoc to ^1.6.1 and add .claude/settings.local.json
- Update dependency @git.zone/tsdoc from ^1.6.0 to ^1.6.1
- Add .claude/settings.local.json to include local Claude settings/permissions
## 2025-11-02 - 1.19.1 - fix(dependencies)
Bump dependencies and add local Claude settings
- Bump devDependencies: @git.zone/tsbuild -> ^2.7.1, @git.zone/tsrun -> ^1.6.2, @git.zone/tstest -> ^2.7.0
- Upgrade runtime dependencies: @git.zone/tsdoc -> ^1.6.0; update @push.rocks packages (smartcli ^4.0.19, smartjson ^5.2.0, smartlog ^3.1.10, smartnetwork ^4.4.0, etc.)
- Add .claude/settings.local.json (local project permissions/settings file)
## 2025-10-23 - 1.19.0 - feat(mod_commit)
Add CLI UI helpers and improve commit workflow with progress, recommendations and summary
- Introduce ts/mod_commit/mod.ui.ts: reusable CLI UI helpers (pretty headers, sections, AI recommendation box, step printer, commit summary and helpers for consistent messaging).
- Refactor ts/mod_commit/index.ts: use new UI functions to display AI recommendations, show step-by-step progress for baking commit info, generating changelog, staging, committing, bumping version and optional push; include commit SHA in final summary.
- Enhance ts/mod_commit/mod.helpers.ts: bumpProjectVersion now accepts currentStep/totalSteps to report progress and returns a consistent newVersion after handling npm/deno/both cases.
- Add .claude/settings.local.json: local permissions configuration for development tooling.
## 2025-10-23 - 1.18.9 - fix(mod_commit)
Stage and commit deno.json when bumping/syncing versions and create/update git tags
- bumpDenoVersion now creates a Smartshell instance and runs git add deno.json, git commit -m "v<newVersion>", and git tag v<newVersion> to persist the version bump
- syncVersionToDenoJson now stages deno.json, amends the npm version commit with --no-edit, and recreates the tag with -fa to keep package.json and deno.json in sync
- Added informative logger messages after creating commits and tags
## 2025-10-23 - 1.18.8 - fix(mod_commit)
Improve commit workflow: detect project type and current branch; add robust version bump helpers for npm/deno
- Add mod_commit/mod.helpers.ts with utilities: detectCurrentBranch(), detectProjectType(), bumpProjectVersion(), bumpDenoVersion(), bumpNpmVersion(), syncVersionToDenoJson(), and calculateNewVersion()
- Refactor ts/mod_commit/index.ts to use the new helpers: bumpProjectVersion(projectType, ... ) instead of a hard npm version call and push the actual current branch instead of hardcoding 'master'
- Support bumping versions for npm-only, deno-only, and hybrid (both) projects and synchronize versions from package.json to deno.json when applicable
- Improve branch detection with a fallback to 'master' and informative logging on detection failures
- Add local Claude settings file (.claude/settings.local.json) (editor/CI config) — no code behavior change but included in diff
## 2025-09-07 - 1.18.7 - fix(claude)
Add .claude local settings to whitelist dev tool permissions
- Add .claude/settings.local.json to configure allowed permissions for local AI/tooling helpers (Bash commands, WebFetch, and mcp_serena actions).
- Disable enableAllProjectMcpServers (set to false) to limit automatic project MCP server usage.
## 2025-09-07 - 1.18.6 - fix(deps)
Bump dependency versions and add local Claude settings
- Updated devDependencies: @git.zone/tsbuild ^2.6.4 → ^2.6.8, @git.zone/tstest ^2.3.4 → ^2.3.6, @push.rocks/smartfile ^11.2.5 → ^11.2.7
- Updated dependencies: @git.zone/tsdoc ^1.5.1 → ^1.5.2, @git.zone/tspublish ^1.10.1 → ^1.10.3, @push.rocks/smartlog ^3.1.8 → ^3.1.9, @push.rocks/smartnpm ^2.0.4 → ^2.0.6, @push.rocks/smartscaf ^4.0.17 → ^4.0.19
- Added .claude/settings.local.json to configure local Claude permissions/settings
## 2025-08-17 - 1.18.5 - fix(dependencies)
Bump smartshell and smartscaf versions; add .claude local settings
- Update @push.rocks/smartshell from ^3.2.4 to ^3.3.0 in package.json
- Update @push.rocks/smartscaf from ^4.0.16 to ^4.0.17 in package.json
- Add .claude/settings.local.json for local assistant permissions/configuration
## 2025-08-17 - 1.18.4 - fix(cli)
Update dependencies, add local Claude settings, and update gitignore template
- Bump several dependencies: @git.zone/tsbuild -> ^2.6.4, @git.zone/tspublish -> ^1.10.1, @git.zone/tstest -> ^2.3.4, @push.rocks/smartfile -> ^11.2.5, @push.rocks/npmextra -> ^5.3.3, @push.rocks/smartchok -> ^1.1.1, @push.rocks/smartlog -> ^3.1.8, @push.rocks/smartpath -> ^6.0.0, prettier -> ^3.6.2
- Add .claude/settings.local.json with local permissions configuration for AI tooling
- Update assets/templates/gitignore to ignore .claude/ and .serena/ directories
- Add pnpm onlyBuiltDependencies entries: esbuild and mongodb-memory-server
## 2025-08-16 - 1.18.3 - fix(services)
Simplify S3 endpoint handling in ServiceConfiguration to store host only
- S3_ENDPOINT now stores the raw host (e.g. 'localhost') instead of a full URL with protocol and port.
- Default .nogit/env.json creation uses the host-only S3_ENDPOINT.
- Sync/update logic (when syncing with Docker or reconfiguring ports) sets S3_ENDPOINT to the host only.
- Consumers that previously relied on S3_ENDPOINT containing protocol and port should now construct the full endpoint URL using S3_USESSL, S3_HOST and S3_PORT.
## 2025-08-16 - 1.18.1 - fix(services)
Improve services and commit flow: stop AiDoc, use silent docker inspect, sync ports with logging, fix config loading, and bump deps
- Ensure AiDoc is stopped after building commit recommendation to avoid resource leaks
- Use execSilent for `docker inspect` in DockerContainer to avoid shell noise and improve JSON parsing
- Sync Docker-exposed ports into service configuration with explicit notes (logs) when MongoDB / S3 ports are updated
- Fix synchronous config loading by removing an unnecessary await in ServiceConfiguration.loadConfig
- Bump dependencies: @push.rocks/smartshell -> ^3.2.4, @git.zone/tsdoc -> ^1.5.1
- Add pnpm.onlyBuiltDependencies for puppeteer and sharp to package.json
- Add local Claude settings file (.claude/settings.local.json) with development permissions
## 2025-08-16 - 1.18.0 - feat(services)
Add Docker port mapping sync and reconfigure workflow for local services
- Add getPortMappings to DockerContainer to extract port bindings from docker inspect output
- Sync existing container port mappings into .nogit/env.json when loading/creating service configuration
- Validate and automatically update ports only when containers are not present; preserve container ports when containers exist
- Recreate containers automatically if detected container port mappings differ from configuration (MongoDB and MinIO)
- Add reconfigure method and new CLI command to reassign ports and optionally restart services
- Improve status output to show configured ports and port availability information
- Minor helpers and imports updated (DockerContainer injected into ServiceConfiguration)
- Add .claude/settings.local.json (local permissions config) to repository
## 2025-08-15 - 1.17.5 - fix(services)
Update S3 credentials naming and add S3_ENDPOINT/S3_USESSL support for improved MinIO integration
- Replaced S3_USER/S3_PASS with S3_ACCESSKEY/S3_SECRETKEY in ServiceConfiguration
- Added S3_ENDPOINT field with automatic protocol selection based on S3_USESSL
- Introduced S3_USESSL boolean field for SSL/TLS configuration
- Updated ServiceManager logging to display new S3_USESSL configuration
- Added .claude/settings.local.json for local permission settings
## 2025-08-15 - 1.17.4 - fix(services)
Update S3 credentials naming and add S3_ENDPOINT/S3_USESSL support for improved MinIO integration
- Replaced S3_USER/S3_PASS with S3_ACCESSKEY/S3_SECRETKEY in ServiceConfiguration
- Added S3_ENDPOINT field with automatic protocol selection based on S3_USESSL
- Added S3_USESSL boolean field for SSL/TLS configuration support
- Updated ServiceManager to use new credential names in container setup and logging
## 2025-08-15 - 1.17.3 - fix(serviceconfig)
Update service configuration to include dynamic MongoDB connection string and add local permissions settings
- Added .claude/settings.local.json for local permissions configuration
- Updated ServiceConfiguration to compute and update MONGODB_URL based on current config values
## 2025-08-15 - 1.17.2 - fix(ci-test-services)
Update CI/CD configurations, test settings, and Docker service for MongoDB.
- Add .claude/settings.local.json with updated permission settings
- Introduce new GitLab CI, VSCode launch and settings, and updated test configuration files (.gitignore, .npmrc, npmextra.json, package.json, qenv.yml, readme.md)
- Update test scripts in test/test and test/ts to improve project validation
- Fix MongoDB Docker container command by adding '--bind_ip_all' for proper network binding
## 2025-08-15 - 1.17.1 - fix(services)
Improve services module logging and enhance MongoDB Compass integration
- Refactored services module to use centralized logger from gitzone.logging.ts
- Automatically display MongoDB Compass connection string when starting services or checking status
- Removed custom printMessage wrapper in favor of standard logger.log() calls
- Consistent logging across all service commands
## 2025-08-14 - 1.17.0 - feat(services)
Add comprehensive development services management for MongoDB and MinIO containers
- Implemented `gitzone services` command for managing local development services
- Added MongoDB and MinIO (S3-compatible) container orchestration
- Smart port assignment (20000-30000 range) to avoid conflicts between projects
- 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
## 2025-08-08 - 1.16.10 - fix(format)
Improve concurrency control in caching and rollback modules, refine gitignore custom section handling, and enhance Prettier file processing.
- Added mutex locking in ChangeCache and RollbackManager to prevent race conditions during manifest updates
- Updated gitignore logic to detect and preserve custom sections
- Enhanced Prettier batching and file formatting for better performance
## 2025-08-08 - 1.16.9 - fix(format)
Improve concurrency control in cache and rollback modules, refine gitignore custom section handling, and enhance Prettier file processing
- Added mutex locking in ChangeCache and RollbackManager to prevent race conditions during manifest updates
- Updated gitignore logic to detect and preserve existing custom sections from various markers
- Simplified Prettier formatter to process files sequentially, skip files without extensions, and log detailed status
- Minor refactoring in base formatter and tsconfig file updates for improved reliability
## 2025-08-08 - 1.16.8 - fix(format) ## 2025-08-08 - 1.16.8 - fix(format)
Improve concurrency control in cache and rollback management with mutex locking and refine formatting details Improve concurrency control in cache and rollback management with mutex locking and refine formatting details
- Added 'withMutex' functions in ChangeCache and RollbackManager to synchronize file I/O operations - Added 'withMutex' functions in ChangeCache and RollbackManager to synchronize file I/O operations
@@ -8,6 +573,7 @@ Improve concurrency control in cache and rollback management with mutex locking
- Fixed minor formatting issues in commit info and package.json - Fixed minor formatting issues in commit info and package.json
## 2025-08-08 - 1.16.7 - fix(core) ## 2025-08-08 - 1.16.7 - fix(core)
Improve formatting, logging, and rollback integrity in core modules Improve formatting, logging, and rollback integrity in core modules
- Add .claude/settings.local.json with defined permissions for allowed commands - Add .claude/settings.local.json with defined permissions for allowed commands
@@ -16,6 +582,7 @@ Improve formatting, logging, and rollback integrity in core modules
- Enhance logging messages and overall code clarity in CLI and commit modules - Enhance logging messages and overall code clarity in CLI and commit modules
## 2025-08-08 - 1.16.6 - fix(changecache) ## 2025-08-08 - 1.16.6 - fix(changecache)
Improve cache manifest validation and atomic file writes; add local settings and overrides Improve cache manifest validation and atomic file writes; add local settings and overrides
- Add manifest structure validation and default fallback in getManifest - Add manifest structure validation and default fallback in getManifest
@@ -25,6 +592,7 @@ Improve cache manifest validation and atomic file writes; add local settings and
- Add an empty assets/overrides.json file for future overrides - Add an empty assets/overrides.json file for future overrides
## 2025-08-08 - 1.16.5 - fix(prettier) ## 2025-08-08 - 1.16.5 - fix(prettier)
Improve file selection in Prettier formatter, remove legacy package overrides, and update CI template indentation Improve file selection in Prettier formatter, remove legacy package overrides, and update CI template indentation
- Added .claude/settings.local.json with updated permission settings for local commands - Added .claude/settings.local.json with updated permission settings for local commands
@@ -33,12 +601,14 @@ Improve file selection in Prettier formatter, remove legacy package overrides, a
- Refined Prettier formatter logic by defining include directories, root config files, and filtering duplicates instead of manual exclusion - Refined Prettier formatter logic by defining include directories, root config files, and filtering duplicates instead of manual exclusion
## 2025-08-08 - 1.16.4 - fix(prettier) ## 2025-08-08 - 1.16.4 - fix(prettier)
Improve file exclusion in the Prettier formatter to skip unnecessary files and directories. Improve file exclusion in the Prettier formatter to skip unnecessary files and directories.
- Added exclusion patterns for node_modules, .git, dist, .nogit, coverage, .nyc_output, vendor, bower_components, jspm_packages, and minified files. - Added exclusion patterns for node_modules, .git, dist, .nogit, coverage, .nyc_output, vendor, bower_components, jspm_packages, and minified files.
- Optimized filtering logic to ensure only valid files are processed. - Optimized filtering logic to ensure only valid files are processed.
## 2025-08-08 - 1.16.3 - fix(changecache/prettier) ## 2025-08-08 - 1.16.3 - fix(changecache/prettier)
Skip directories during file processing to prevent errors in changecache and prettier formatting Skip directories during file processing to prevent errors in changecache and prettier formatting
- Removed unnecessary await on synchronous file reads in changecache - Removed unnecessary await on synchronous file reads in changecache
@@ -46,18 +616,21 @@ Skip directories during file processing to prevent errors in changecache and pre
- Filtered out directories in prettier formatter to avoid processing non-files - Filtered out directories in prettier formatter to avoid processing non-files
## 2025-08-07 - 1.16.2 - fix(format) ## 2025-08-07 - 1.16.2 - fix(format)
Fix format command confirmation prompt to correctly check user response Fix format command confirmation prompt to correctly check user response
- Fixed bug where format command always showed "cancelled" even when user confirmed - Fixed bug where format command always showed "cancelled" even when user confirmed
- Changed response check from `response.proceed` to `response.value` for SmartInteract compatibility - Changed response check from `response.proceed` to `response.value` for SmartInteract compatibility
## 2025-08-04 - 1.16.1 - fix(package/config) ## 2025-08-04 - 1.16.1 - fix(package/config)
Move smartdiff dependency to runtime and add local bash permissions settings Move smartdiff dependency to runtime and add local bash permissions settings
- Moved '@push.rocks/smartdiff' from devDependencies to dependencies in package.json - Moved '@push.rocks/smartdiff' from devDependencies to dependencies in package.json
- Added .claude/settings.local.json with allowed bash commands (grep, mkdir, find, ls) - Added .claude/settings.local.json with allowed bash commands (grep, mkdir, find, ls)
## 2025-05-19 - 1.16.0 - feat(format) ## 2025-05-19 - 1.16.0 - feat(format)
Enhance format module with rollback, diff reporting, and improved parallel execution Enhance format module with rollback, diff reporting, and improved parallel execution
- Implemented rollback functionality with backup management and automatic rollback on error - Implemented rollback functionality with backup management and automatic rollback on error
@@ -68,12 +641,14 @@ Enhance format module with rollback, diff reporting, and improved parallel execu
- Updated package.json to include new dependency '@push.rocks/smartdiff' - Updated package.json to include new dependency '@push.rocks/smartdiff'
## 2025-05-14 - 1.15.5 - fix(dependencies) ## 2025-05-14 - 1.15.5 - fix(dependencies)
Update @git.zone/tsdoc to ^1.5.0 and @types/node to ^22.15.18 Update @git.zone/tsdoc to ^1.5.0 and @types/node to ^22.15.18
- Bumped @git.zone/tsdoc from ^1.4.5 to ^1.5.0 - Bumped @git.zone/tsdoc from ^1.4.5 to ^1.5.0
- Bumped @types/node from ^22.15.17 to ^22.15.18 - Bumped @types/node from ^22.15.17 to ^22.15.18
## 2025-05-13 - 1.15.4 - fix(package.json) ## 2025-05-13 - 1.15.4 - fix(package.json)
Update dependency versions: bump @git.zone/tsdoc, @push.rocks/lik, @push.rocks/smartlog, and @types/node to their latest releases Update dependency versions: bump @git.zone/tsdoc, @push.rocks/lik, @push.rocks/smartlog, and @types/node to their latest releases
- Upgrade @git.zone/tsdoc from ^1.4.4 to ^1.4.5 - Upgrade @git.zone/tsdoc from ^1.4.4 to ^1.4.5
@@ -82,6 +657,7 @@ Update dependency versions: bump @git.zone/tsdoc, @push.rocks/lik, @push.rocks/s
- Upgrade @types/node from ^22.14.1 to ^22.15.17 - Upgrade @types/node from ^22.14.1 to ^22.15.17
## 2025-04-15 - 1.15.3 - fix(deps) ## 2025-04-15 - 1.15.3 - fix(deps)
update dependency versions and improve website template variable handling update dependency versions and improve website template variable handling
- Bumped @git.zone/tsbuild from ^2.2.1 to ^2.3.2 and @types/node to ^22.14.1 - Bumped @git.zone/tsbuild from ^2.2.1 to ^2.3.2 and @types/node to ^22.14.1
@@ -89,56 +665,65 @@ update dependency versions and improve website template variable handling
- Refactored website template update to correctly supply variables with added logging - Refactored website template update to correctly supply variables with added logging
## 2025-04-15 - 1.15.2 - fix(website_update) ## 2025-04-15 - 1.15.2 - fix(website_update)
Await supplyVariables call in website update template Await supplyVariables call in website update template
- Changed website template update to properly await the supplyVariables method - Changed website template update to properly await the supplyVariables method
- Ensured asynchronous consistency in updating website template variables - Ensured asynchronous consistency in updating website template variables
## 2025-04-15 - 1.15.1 - fix(cli) ## 2025-04-15 - 1.15.1 - fix(cli)
Refresh internal CLI tooling and configuration for consistency. Refresh internal CLI tooling and configuration for consistency.
## 2025-04-15 - 1.15.0 - feat(config/template) ## 2025-04-15 - 1.15.0 - feat(config/template)
Add assetbrokerUrl and legalUrl fields to module config and update website template to supply these values Add assetbrokerUrl and legalUrl fields to module config and update website template to supply these values
- Added assetbrokerUrl and legalUrl properties in ts/classes.gitzoneconfig.ts - Added assetbrokerUrl and legalUrl properties in ts/classes.gitzoneconfig.ts
- Updated ts/mod_format/format.templates.ts to pass assetbrokerUrl and legalUrl to website template - Updated ts/mod_format/format.templates.ts to pass assetbrokerUrl and legalUrl to website template
## 2025-04-15 - 1.14.1 - fix(package.json) ## 2025-04-15 - 1.14.1 - fix(package.json)
Add packageManager field to specify pnpm version for consistent package management Add packageManager field to specify pnpm version for consistent package management
- Inserted packageManager property in package.json with pnpm version info to ensure reproducible dependency installs - Inserted packageManager property in package.json with pnpm version info to ensure reproducible dependency installs
## 2025-04-15 - 1.14.0 - feat(tsconfig_update) ## 2025-04-15 - 1.14.0 - feat(tsconfig_update)
Add runafter directive to trigger gitzone format after tsconfig update Add runafter directive to trigger gitzone format after tsconfig update
- Added runafter configuration in assets/templates/tsconfig_update/.smartscaf.yml to automate formatting task - Added runafter configuration in assets/templates/tsconfig_update/.smartscaf.yml to automate formatting task
## 2025-03-07 - 1.13.1 - fix(cli) ## 2025-03-07 - 1.13.1 - fix(cli)
Improve commit message logging Improve commit message logging
- Updated logging to display recommended next commit details. - Updated logging to display recommended next commit details.
- Enabled interactive prompt for choosing commit type and scope. - Enabled interactive prompt for choosing commit type and scope.
## 2025-02-28 - 1.13.0 - feat(templates) ## 2025-02-28 - 1.13.0 - feat(templates)
Updated and added new TypeScript template files for npm projects Updated and added new TypeScript template files for npm projects
- Added new paths.ts and plugins.ts template files for npm projects. - Added new paths.ts and plugins.ts template files for npm projects.
- Removed outdated some.plugins.ts template file. - Removed outdated some.plugins.ts template file.
## 2025-02-25 - 1.12.8 - fix(metadata) ## 2025-02-25 - 1.12.8 - fix(metadata)
Updated package and npmextra json description and keywords for enhanced development workflow clarity Updated package and npmextra json description and keywords for enhanced development workflow clarity
- Updated the description in package.json to focus on project setup and management. - Updated the description in package.json to focus on project setup and management.
- Aligned the keywords in both package.json and npmextra.json to include more relevant terms such as gitzone utilities, template management, and CI/CD. - Aligned the keywords in both package.json and npmextra.json to include more relevant terms such as gitzone utilities, template management, and CI/CD.
## 2025-02-25 - 1.12.7 - fix(meta) ## 2025-02-25 - 1.12.7 - fix(meta)
Fix issues in project metadata and configuration. Fix issues in project metadata and configuration.
- Updated package metadata to ensure accurate project description and licensing. - Updated package metadata to ensure accurate project description and licensing.
- Ensured npm access level configuration consistency within npmextra.json. - Ensured npm access level configuration consistency within npmextra.json.
## 2025-02-25 - 1.12.7 - fix(ci) ## 2025-02-25 - 1.12.7 - fix(ci)
Updated dependencies and added CI/CD workflows. Updated dependencies and added CI/CD workflows.
- Updated several dependencies in package.json for compatibility and security. - Updated several dependencies in package.json for compatibility and security.
@@ -147,6 +732,7 @@ Updated dependencies and added CI/CD workflows.
- Ensured consistent formatting with Prettier and TypeScript configurations. - Ensured consistent formatting with Prettier and TypeScript configurations.
## 2025-01-29 - 1.12.6 - fix(project) ## 2025-01-29 - 1.12.6 - fix(project)
Minor fixes and cleanup Minor fixes and cleanup
- Removed outdated pages/ directory entry in .gitignore. - Removed outdated pages/ directory entry in .gitignore.
@@ -155,6 +741,7 @@ Minor fixes and cleanup
- Fixed formatting issues across various TypeScript files. - Fixed formatting issues across various TypeScript files.
## 2025-01-29 - 1.12.5 - fix(cli) ## 2025-01-29 - 1.12.5 - fix(cli)
Initial implementation of CLI utility with project management features Initial implementation of CLI utility with project management features
- Integration of various plugins for logging, command-line interactions, and project management. - Integration of various plugins for logging, command-line interactions, and project management.
@@ -162,34 +749,40 @@ Initial implementation of CLI utility with project management features
- Implement commands for packaging, versioning, and deprecating npm packages. - Implement commands for packaging, versioning, and deprecating npm packages.
## 2025-01-29 - 1.12.2 - fix(format) ## 2025-01-29 - 1.12.2 - fix(format)
Add overrides for peek-readable in package.json formatting Add overrides for peek-readable in package.json formatting
- Added a URL correction in the packageJson repository information. - Added a URL correction in the packageJson repository information.
- Introduced support for pnpm overrides by including an `overrides.json` file. - Introduced support for pnpm overrides by including an `overrides.json` file.
## 2025-01-18 - 1.12.1 - fix(dependencies) ## 2025-01-18 - 1.12.1 - fix(dependencies)
Update various package dependencies and Dockerfile base image Update various package dependencies and Dockerfile base image
- Updated Dockerfile base image from 'alpinenpmci' to 'alpine_npmci'. - Updated Dockerfile base image from 'alpinenpmci' to 'alpine_npmci'.
- Upgraded @git.zone/tsbuild, @git.zone/tsrun, @git.zone/tsdoc, and other dependencies to their latest versions. - Upgraded @git.zone/tsbuild, @git.zone/tsrun, @git.zone/tsdoc, and other dependencies to their latest versions.
## 2025-01-17 - 1.12.0 - feat(build) ## 2025-01-17 - 1.12.0 - feat(build)
Update TypeScript configuration to support emit decorator metadata Update TypeScript configuration to support emit decorator metadata
- Added emitDecoratorMetadata to the tsconfig.json template in assets/templates/tsconfig_update. - Added emitDecoratorMetadata to the tsconfig.json template in assets/templates/tsconfig_update.
## 2025-01-08 - 1.11.0 - feat(cli) ## 2025-01-08 - 1.11.0 - feat(cli)
Add Docker command for cleaning up Docker system and extend deprecation command for multiple registries Add Docker command for cleaning up Docker system and extend deprecation command for multiple registries
- Added a new command 'docker' to handle Docker system cleanup operations. - Added a new command 'docker' to handle Docker system cleanup operations.
- Improved the 'deprecate' command to support deprecating packages across multiple npm registry URLs. - Improved the 'deprecate' command to support deprecating packages across multiple npm registry URLs.
## 2025-01-01 - 1.10.10 - fix(templates) ## 2025-01-01 - 1.10.10 - fix(templates)
Corrected typo in template file comment Corrected typo in template file comment
- Fixed repeated comment in the template file for services under 'assets/templates/service/ts/some.plugins.ts'. - Fixed repeated comment in the template file for services under 'assets/templates/service/ts/some.plugins.ts'.
## 2025-01-01 - 1.10.9 - fix(templates) ## 2025-01-01 - 1.10.9 - fix(templates)
Correct template file paths and organization for service projects Correct template file paths and organization for service projects
- Moved 'some.classes.some.ts' to 'classes.some.ts' - Moved 'some.classes.some.ts' to 'classes.some.ts'
@@ -197,60 +790,70 @@ Correct template file paths and organization for service projects
- Resolved incorrect import paths in service templates - Resolved incorrect import paths in service templates
## 2025-01-01 - 1.10.8 - fix(assets/templates) ## 2025-01-01 - 1.10.8 - fix(assets/templates)
Update CI template configurations to use module.githost Update CI template configurations to use module.githost
- Replaced occurrences of {{git.host}} with {{module.githost}} in CI workflow files - Replaced occurrences of {{git.host}} with {{module.githost}} in CI workflow files
- Updated package dependencies for service template - Updated package dependencies for service template
## 2024-12-26 - 1.10.7 - fix(assets) ## 2024-12-26 - 1.10.7 - fix(assets)
Correct URLs in templates and fix TypeScript declaration Correct URLs in templates and fix TypeScript declaration
- Updated incorrect URLs in Dockerfile templates to 'host.today'. - Updated incorrect URLs in Dockerfile templates to 'host.today'.
- Fixed type declaration for 'TemplateResult' in header.ts file. - Fixed type declaration for 'TemplateResult' in header.ts file.
## 2024-12-08 - 1.10.6 - fix(ci) ## 2024-12-08 - 1.10.6 - fix(ci)
Corrected Docker image URL in CI templates Corrected Docker image URL in CI templates
- Updated Docker image URL from 'code.foss.global/hosttoday' to 'code.foss.global/host.today' in default_nottags.yaml and default_tags.yaml. - Updated Docker image URL from 'code.foss.global/hosttoday' to 'code.foss.global/host.today' in default_nottags.yaml and default_tags.yaml.
- Adjusted gitignore template to include a custom section delineation. - Adjusted gitignore template to include a custom section delineation.
## 2024-12-02 - 1.10.5 - fix(assets) ## 2024-12-02 - 1.10.5 - fix(assets)
Update .gitignore template to remove pages directory Update .gitignore template to remove pages directory
- Removed 'pages/' from the ignored directories in the .gitignore template. - Removed 'pages/' from the ignored directories in the .gitignore template.
## 2024-11-05 - 1.10.4 - fix(mod_format) ## 2024-11-05 - 1.10.4 - fix(mod_format)
Correct file extension for TypeScript path configuration Correct file extension for TypeScript path configuration
- Fixed the TypeScript configuration to use correct file extensions for module subdirectories. - Fixed the TypeScript configuration to use correct file extensions for module subdirectories.
## 2024-10-27 - 1.10.3 - fix(mod_format) ## 2024-10-27 - 1.10.3 - fix(mod_format)
Reorder TypeScript formatting steps in mod_format module Reorder TypeScript formatting steps in mod_format module
- Moved TypeScript configuration formatting earlier in the sequence for better logical consistency. - Moved TypeScript configuration formatting earlier in the sequence for better logical consistency.
## 2024-10-27 - 1.10.2 - fix(format) ## 2024-10-27 - 1.10.2 - fix(format)
Add logging for tsconfig.json formatting Add logging for tsconfig.json formatting
- Added an info log message for tsconfig.json formatting in format.tsconfig.ts. - Added an info log message for tsconfig.json formatting in format.tsconfig.ts.
## 2024-10-27 - 1.10.1 - fix(format) ## 2024-10-27 - 1.10.1 - fix(format)
Fixed async issue in tsconfig module lookup and corrected property access Fixed async issue in tsconfig module lookup and corrected property access
## 2024-10-27 - 1.10.0 - feat(mod_format) ## 2024-10-27 - 1.10.0 - feat(mod_format)
Add support for tsconfig.json formatting Add support for tsconfig.json formatting
- Added a new script to format tsconfig.json. - Added a new script to format tsconfig.json.
- Updated package.json to include `@git.zone/tspublish` as a dependency. - Updated package.json to include `@git.zone/tspublish` as a dependency.
## 2024-10-23 - 1.9.126 - fix(format) ## 2024-10-23 - 1.9.126 - fix(format)
Remove redundant package.json property checks Remove redundant package.json property checks
- Removed property checks for `main`, `typings`, and `browserslist` from format.packagejson.ts - Removed property checks for `main`, `typings`, and `browserslist` from format.packagejson.ts
- This change streamlines the formatting process by removing unnecessary exits - This change streamlines the formatting process by removing unnecessary exits
## 2024-09-29 - 1.9.125 - fix(cli) ## 2024-09-29 - 1.9.125 - fix(cli)
Fix package version configuration and formatting issues Fix package version configuration and formatting issues
- Updated metadata fields in package.json (repository URL, bugs URL, and homepage). - Updated metadata fields in package.json (repository URL, bugs URL, and homepage).
@@ -258,15 +861,17 @@ Fix package version configuration and formatting issues
- Added missing Prettier default TypeScript and Markdown configurations. - Added missing Prettier default TypeScript and Markdown configurations.
## 2024-09-27 - 1.9.124 - fix(cli) ## 2024-09-27 - 1.9.124 - fix(cli)
Ensured proper existence and initialization of readme files Ensured proper existence and initialization of readme files
- Ensured readme.md and readme.hints.md files are created and initialized if they do not exist. - Ensured readme.md and readme.hints.md files are created and initialized if they do not exist.
## 2024-09-27 - 1.9.123 - fix(core) ## 2024-09-27 - 1.9.123 - fix(core)
No changes detected No changes detected
## 2024-09-27 - 1.9.123 - fix(core) ## 2024-09-27 - 1.9.123 - fix(core)
Update dependencies and improve build configurations Update dependencies and improve build configurations
- Updated several dependencies in package.json for better compatibility - Updated several dependencies in package.json for better compatibility
@@ -277,88 +882,111 @@ Update dependencies and improve build configurations
- Provided initial structure for readme and readme hints - Provided initial structure for readme and readme hints
## 2024-06-24 - 1.9.122 - fix(mod_commit) ## 2024-06-24 - 1.9.122 - fix(mod_commit)
Update package.json dependencies: @git.zone/tsdoc and @push.rocks/smartpromise to latest versions. Update package.json dependencies: @git.zone/tsdoc and @push.rocks/smartpromise to latest versions.
- - Updated @git.zone/tsdoc to ^1.3.12 - - Updated @git.zone/tsdoc to ^1.3.12
- - Updated @push.rocks/smartfile to ^11.0.21 - - Updated @push.rocks/smartfile to ^11.0.21
## 2024-06-23 - 1.9.121 - fix(mod_commit) ## 2024-06-23 - 1.9.121 - fix(mod_commit)
Fix changelog template rendering by removing extra new line when no version details are provided. Fix changelog template rendering by removing extra new line when no version details are provided.
- Update package.json dependencies: @git.zone/tsdoc and @push.rocks/smartpromise to latest versions. - Update package.json dependencies: @git.zone/tsdoc and @push.rocks/smartpromise to latest versions.
## 2024-06-23 - 1.9.120 - fix(mod_commit) ## 2024-06-23 - 1.9.120 - fix(mod_commit)
Handle edge case for empty version details in changelog formatting Handle edge case for empty version details in changelog formatting
- Added check for the length of the recommendedNextVersionDetails array - Added check for the length of the recommendedNextVersionDetails array
- Ensure no extra newline in changelog if there are no version details - Ensure no extra newline in changelog if there are no version details
## 2024-06-23 - 1.9.119 - fix(dependencies) ## 2024-06-23 - 1.9.119 - fix(dependencies)
Update @git.zone/tsdoc to v1.3.8 Update @git.zone/tsdoc to v1.3.8
- Updated @git.zone/tsdoc from v1.3.7 to v1.3.8 in package.json - Updated @git.zone/tsdoc from v1.3.7 to v1.3.8 in package.json
## 2024-06-23 - 1.9.118 - fix(dependencies) ## 2024-06-23 - 1.9.118 - fix(dependencies)
Update @git.zone/tsdoc to version 1.3.7 Update @git.zone/tsdoc to version 1.3.7
- Bump @git.zone/tsdoc from 1.3.6 to 1.3.7 in both package.json and pnpm-lock.yaml - Bump @git.zone/tsdoc from 1.3.6 to 1.3.7 in both package.json and pnpm-lock.yaml
## 2024-06-23 - 1.9.117 - fix(dependencies) ## 2024-06-23 - 1.9.117 - fix(dependencies)
Update @git.zone/tsdoc dependency to v1.3.6 Update @git.zone/tsdoc dependency to v1.3.6
- Updated @git.zone/tsdoc version from 1.3.5 to 1.3.6 in package.json - Updated @git.zone/tsdoc version from 1.3.5 to 1.3.6 in package.json
- Updated pnpm-lock.yaml to reflect the new version of @git.zone/tsdoc - Updated pnpm-lock.yaml to reflect the new version of @git.zone/tsdoc
## 2024-06-23 - 1.9.116 - fix(dependencies) ## 2024-06-23 - 1.9.116 - fix(dependencies)
Update @git.zone/tsdoc to version 1.3.5 Update @git.zone/tsdoc to version 1.3.5
- Updated the @git.zone/tsdoc dependency in package.json and pnpm-lock.yaml from version 1.3.4 to 1.3.5 - Updated the @git.zone/tsdoc dependency in package.json and pnpm-lock.yaml from version 1.3.4 to 1.3.5
- Removed the outdated changelog.md file. - Removed the outdated changelog.md file.
## 2024-06-23 - 1.9.114 - fix(format) ## 2024-06-23 - 1.9.114 - fix(format)
Fixed formatting issues across multiple TypeScript files. Fixed formatting issues across multiple TypeScript files.
## 2024-06-23 - 1.9.113 - fix(mod_commit) ## 2024-06-23 - 1.9.113 - fix(mod_commit)
Remove extra new lines in changelog. Remove extra new lines in changelog.
## 2024-06-23 - 1.9.112 - fix(core) ## 2024-06-23 - 1.9.112 - fix(core)
Update changelog formatting and remove outdated entries. Update changelog formatting and remove outdated entries.
## 2024-06-23 - 1.9.111 - fix(changelog) ## 2024-06-23 - 1.9.111 - fix(changelog)
Remove outdated changelog entries and update formatting. Remove outdated changelog entries and update formatting.
## 2024-06-23 - 1.9.110 - fix(dependencies) ## 2024-06-23 - 1.9.110 - fix(dependencies)
Update @git.zone/tsdoc to version 1.3.4. Update @git.zone/tsdoc to version 1.3.4.
## 2024-06-23 - 1.9.109 - fix(changelog) ## 2024-06-23 - 1.9.109 - fix(changelog)
Remove outdated entries and adjust formatting in changelog. Remove outdated entries and adjust formatting in changelog.
## 2024-06-23 - 1.9.108 - fix(dependencies) ## 2024-06-23 - 1.9.108 - fix(dependencies)
Update @git.zone/tsdoc dependency to version 1.3.2. Update @git.zone/tsdoc dependency to version 1.3.2.
## 2024-06-23 - 1.9.107 - fix(changelog) ## 2024-06-23 - 1.9.107 - fix(changelog)
Remove placeholder entries and adjust formatting in changelog. Remove placeholder entries and adjust formatting in changelog.
## 2024-06-23 - 1.9.106 - fix(dependencies) ## 2024-06-23 - 1.9.106 - fix(dependencies)
Updated @git.zone/tsdoc from version 1.3.0 to 1.3.1. Updated @git.zone/tsdoc from version 1.3.0 to 1.3.1.
## 2024-06-23 - 1.9.105 - fix(dependencies) ## 2024-06-23 - 1.9.105 - fix(dependencies)
Updated @git.zone/tsdoc dependency from 1.2.2 to 1.3.0 in package.json and pnpm-lock.yaml. Updated @git.zone/tsdoc dependency from 1.2.2 to 1.3.0 in package.json and pnpm-lock.yaml.
## 2024-06-23 - 1.9.104 - fix(changelog) ## 2024-06-23 - 1.9.104 - fix(changelog)
Remove placeholder entries and adjust formatting in changelog. Remove placeholder entries and adjust formatting in changelog.
## 2024-06-23 - 1.9.103 - fix(changelog) ## 2024-06-23 - 1.9.103 - fix(changelog)
Fix changelog to remove placeholder entries and adjust formatting. Fix changelog to remove placeholder entries and adjust formatting.
## 2024-06-23 - 1.9.102 - fix(logging) ## 2024-06-23 - 1.9.102 - fix(logging)
Optimize logger instantiation and configuration. Optimize logger instantiation and configuration.
## 2024-06-23 - 1.9.101 - fix(metadata) ## 2024-06-23 - 1.9.101 - fix(metadata)
Ensure accurate project metadata in package.json. Ensure accurate project metadata in package.json.
## 2024-06-23 - 1.9.100 - fix(dependencies) ## 2024-06-23 - 1.9.100 - fix(dependencies)
Updated @git.zone/tsdoc dependency version to ^1.2.2 in package.json and pnpm-lock.yaml. Updated @git.zone/tsdoc dependency version to ^1.2.2 in package.json and pnpm-lock.yaml.
## 2024-06-23 - 1.9.99 - fix(mod_commit) ## 2024-06-23 - 1.9.99 - fix(mod_commit)
Fix variable reassignment issue in changelog writing step. Fix variable reassignment issue in changelog writing step.
+30 -29
View File
@@ -1,7 +1,7 @@
{ {
"name": "@git.zone/cli", "name": "@git.zone/cli",
"private": false, "private": false,
"version": "1.16.8", "version": "2.14.0",
"description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.", "description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.",
"main": "dist_ts/index.ts", "main": "dist_ts/index.ts",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",
@@ -12,7 +12,7 @@
}, },
"scripts": { "scripts": {
"test": "(npm run clean && npm run prepareTest && npm run testCli && npm run testFormat && npm run testCommit && npm run testDeprecate && npm run testVersion && npm run testReadme && npm run testUpdate && npm run testTemplateNpm && npm run testTemplateLit) && rm -rf test", "test": "(npm run clean && npm run prepareTest && npm run testCli && npm run testFormat && npm run testCommit && npm run testDeprecate && npm run testVersion && npm run testReadme && npm run testUpdate && npm run testTemplateNpm && npm run testTemplateLit) && rm -rf test",
"build": "(tsbuild --web --allowimplicitany)", "build": "tsbuild tsfolders",
"clean": "(rm -rf test/)", "clean": "(rm -rf test/)",
"prepareTest": "(git clone https://gitlab.com/sandboxzone/sandbox-npmts.git test/)", "prepareTest": "(git clone https://gitlab.com/sandboxzone/sandbox-npmts.git test/)",
"testBuild": "npm run build && rm -r dist/", "testBuild": "npm run build && rm -r dist/",
@@ -57,45 +57,40 @@
}, },
"homepage": "https://gitlab.com/gitzone/private/gitzone#readme", "homepage": "https://gitlab.com/gitzone/private/gitzone#readme",
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.3.2", "@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsrun": "^1.3.3", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^1.0.96", "@git.zone/tstest": "^3.3.2",
"@types/node": "^22.15.18" "@types/node": "^25.4.0"
}, },
"dependencies": { "dependencies": {
"@git.zone/tsdoc": "^1.5.0", "@git.zone/tsdoc": "^2.0.0",
"@git.zone/tspublish": "^1.9.1", "@git.zone/tspublish": "^1.11.2",
"@push.rocks/commitinfo": "^1.0.12", "@push.rocks/commitinfo": "^1.0.12",
"@push.rocks/early": "^4.0.4", "@push.rocks/early": "^4.0.4",
"@push.rocks/gulp-function": "^3.0.7",
"@push.rocks/lik": "^6.2.2",
"@push.rocks/npmextra": "^5.1.2",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/smartchok": "^1.0.34", "@push.rocks/smartcli": "^4.0.20",
"@push.rocks/smartcli": "^4.0.11", "@push.rocks/smartconfig": "^6.0.1",
"@push.rocks/smartdelay": "^3.0.5", "@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartdiff": "^1.0.3", "@push.rocks/smartdiff": "^1.1.0",
"@push.rocks/smartfile": "^11.2.0", "@push.rocks/smartfile": "^13.1.2",
"@push.rocks/smartgulp": "^3.0.4", "@push.rocks/smartfs": "^1.5.0",
"@push.rocks/smartinteract": "^2.0.15", "@push.rocks/smartinteract": "^2.0.16",
"@push.rocks/smartjson": "^5.0.20", "@push.rocks/smartjson": "^6.0.0",
"@push.rocks/smartlegal": "^1.0.27", "@push.rocks/smartlegal": "^1.0.27",
"@push.rocks/smartlog": "^3.0.9", "@push.rocks/smartlog": "^3.2.1",
"@push.rocks/smartlog-destination-local": "^9.0.2", "@push.rocks/smartlog-destination-local": "^9.0.2",
"@push.rocks/smartmustache": "^3.0.2", "@push.rocks/smartmustache": "^3.0.2",
"@push.rocks/smartnpm": "^2.0.4", "@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartnpm": "^2.0.6",
"@push.rocks/smartobject": "^1.0.12", "@push.rocks/smartobject": "^1.0.12",
"@push.rocks/smartopen": "^2.0.0", "@push.rocks/smartopen": "^2.0.0",
"@push.rocks/smartpath": "^5.0.18", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartscaf": "^4.0.16", "@push.rocks/smartscaf": "^4.0.21",
"@push.rocks/smartshell": "^3.2.3", "@push.rocks/smartshell": "^3.3.7",
"@push.rocks/smartstream": "^3.2.5",
"@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartunique": "^3.0.9",
"@push.rocks/smartupdate": "^2.0.6", "@push.rocks/smartupdate": "^2.0.6",
"@types/through2": "^2.0.41", "prettier": "^3.8.1"
"prettier": "^3.5.3",
"through2": "^4.0.2"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",
@@ -106,14 +101,20 @@
"dist_ts_web/**/*", "dist_ts_web/**/*",
"assets/**/*", "assets/**/*",
"cli.js", "cli.js",
"npmextra.json", ".smartconfig.json",
"readme.md" "readme.md"
], ],
"browserslist": [ "browserslist": [
"last 1 chrome versions" "last 1 chrome versions"
], ],
"pnpm": { "pnpm": {
"overrides": {} "overrides": {},
"onlyBuiltDependencies": [
"esbuild",
"mongodb-memory-server",
"puppeteer",
"sharp"
]
}, },
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6" "packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
} }
+4208 -6129
View File
File diff suppressed because it is too large Load Diff
+150 -35
View File
@@ -1,10 +1,11 @@
# Gitzone CLI - Development Hints # Gitzone CLI - Development Hints
* the cli of the git.zone project. - the cli of the git.zone project.
## Project Overview ## Project Overview
Gitzone CLI (`@git.zone/cli`) is a comprehensive toolbelt for streamlining local development cycles. It provides utilities for: Gitzone CLI (`@git.zone/cli`) is a comprehensive toolbelt for streamlining local development cycles. It provides utilities for:
- Project initialization and templating (via smartscaf) - Project initialization and templating (via smartscaf)
- Code formatting and standardization - Code formatting and standardization
- Version control and commit management - Version control and commit management
@@ -14,22 +15,25 @@ Gitzone CLI (`@git.zone/cli`) is a comprehensive toolbelt for streamlining local
## Architecture ## Architecture
### Core Structure ### Core Structure
- Main CLI entry: `cli.ts` / `cli.child.ts` - Main CLI entry: `cli.ts` / `cli.child.ts`
- Modular architecture with separate modules in `ts/mod_*` directories - Modular architecture with separate modules in `ts/mod_*` directories
- Each module handles specific functionality (format, commit, docker, etc.) - Each module handles specific functionality (format, commit, docker, etc.)
- Extensive use of plugins pattern via `plugins.ts` files - Extensive use of plugins pattern via `plugins.ts` files
### Configuration Management ### Configuration Management
- Uses `npmextra.json` for all tool configuration
- Configuration stored under `gitzone` key in npmextra - Uses `.smartconfig.json` for tool configuration
- No separate `.gitzonerc` file - everything in npmextra.json - CLI settings live under the `@git.zone/cli` namespace
- Project type and module metadata also stored in npmextra - Agent and non-interactive defaults now belong under `@git.zone/cli.cli`
- Project type, module metadata, release settings, commit defaults, and format settings live in the same file
### Format Module (`mod_format`) - SIGNIFICANTLY ENHANCED ### Format Module (`mod_format`) - SIGNIFICANTLY ENHANCED
The format module is responsible for project standardization: The format module is responsible for project standardization:
#### Current Modules: #### Current Modules:
1. **cleanup** - Removes obsolete files (yarn.lock, tslint.json, etc.) 1. **cleanup** - Removes obsolete files (yarn.lock, tslint.json, etc.)
2. **copy** - File copying with glob patterns (fully implemented) 2. **copy** - File copying with glob patterns (fully implemented)
3. **gitignore** - Creates/updates .gitignore from templates 3. **gitignore** - Creates/updates .gitignore from templates
@@ -42,6 +46,7 @@ The format module is responsible for project standardization:
10. **tsconfig** - Formats TypeScript configuration 10. **tsconfig** - Formats TypeScript configuration
#### Execution Order (Dependency-Based): #### Execution Order (Dependency-Based):
- Modules are now executed in parallel groups based on dependencies - Modules are now executed in parallel groups based on dependencies
- Independent modules run concurrently for better performance - Independent modules run concurrently for better performance
- Dependency analyzer ensures correct execution order - Dependency analyzer ensures correct execution order
@@ -79,16 +84,55 @@ The format module is responsible for project standardization:
1. **Plan → Action Workflow**: Shows changes before applying them 1. **Plan → Action Workflow**: Shows changes before applying them
2. **Rollback Mechanism**: Full backup and restore on failures 2. **Rollback Mechanism**: Full backup and restore on failures
3. **Enhanced Configuration**: Granular control via npmextra.json 3. **Enhanced Configuration**: Granular control via `.smartconfig.json`
4. **Better Error Handling**: Detailed errors with recovery options 4. **Better Error Handling**: Detailed errors with recovery options
5. **Performance Optimizations**: Parallel execution and caching 5. **Performance Optimizations**: Parallel execution and caching
6. **Reporting**: Diff views, statistics, verbose logging 6. **Reporting**: Diff views, statistics, verbose logging
7. **Architecture**: Clean separation of concerns with new classes 7. **Architecture**: Clean separation of concerns with new classes
8. **Unified Version Bumping**: Self-managed version updates eliminating npm warning pollution in deno.json
### Version Bumping Refactor (Latest)
The commit module's version bumping has been refactored to eliminate npm command dependencies:
**Changes:**
- Removed `bumpNpmVersion()` - was causing npm warnings to pollute deno.json
- Removed `syncVersionToDenoJson()` - no longer needed with unified approach
- Removed separate `bumpDenoVersion()` - replaced by unified implementation
- Added `readCurrentVersion()` helper - reads from either package.json or deno.json
- Added `updateVersionFile()` helper - updates JSON files directly
- Unified `bumpProjectVersion()` - handles npm/deno/both with single clean code path
**Benefits:**
- No npm warning pollution in version fields
- Full control over version bumping process
- Simpler git history (no amending, no force-tagging)
- Same code path for all project types
- Reuses existing `calculateNewVersion()` function
### Auto-Accept Flag for Commits
The commit module now supports `-y/--yes` flag for non-interactive commits:
**Usage:**
- `gitzone commit -y` - Auto-accepts AI recommendations without prompts
- `gitzone commit -yp` - Auto-accepts and pushes to origin
- Separate `-p/--push` flag controls push behavior
**Implementation:**
- Creates AnswerBucket programmatically when `-y` flag detected
- Preserves all UI output for transparency
- Fully backward compatible with interactive mode
- CI/CD friendly for automated workflows
## Development Tips ## Development Tips
- Always check readme.plan.md for ongoing improvement plans - Always check readme.plan.md for ongoing improvement plans
- Use npmextra.json for any new configuration options - Use `.smartconfig.json` for any new configuration options
- Keep modules focused and single-purpose - Keep modules focused and single-purpose
- Maintain the existing plugin pattern for dependencies - Maintain the existing plugin pattern for dependencies
- Test format operations on sample projects before deploying - Test format operations on sample projects before deploying
@@ -100,30 +144,18 @@ The format module is responsible for project standardization:
```json ```json
{ {
"gitzone": { "@git.zone/cli": {
"cli": {
"interactive": true,
"output": "human",
"checkUpdates": true
},
"format": { "format": {
"interactive": true, "interactive": true,
"parallel": true,
"showStats": true, "showStats": true,
"cache": {
"enabled": true,
"clean": true
},
"rollback": {
"enabled": true,
"autoRollbackOnError": true,
"backupRetentionDays": 7
},
"modules": { "modules": {
"skip": ["prettier"], "skip": ["prettier"],
"only": [], "only": []
"order": []
},
"licenses": {
"allowed": ["MIT", "Apache-2.0"],
"exceptions": {
"some-package": "GPL-3.0"
}
} }
} }
} }
@@ -132,15 +164,42 @@ The format module is responsible for project standardization:
## CLI Usage ## CLI Usage
### Commit Commands
```bash
# Interactive commit (default)
gitzone commit
# Read-only recommendation
gitzone commit recommend --json
# Auto-accept AI recommendations (no prompts)
gitzone commit -y
gitzone commit --yes
# Auto-accept and push to origin
gitzone commit -yp
gitzone commit -y -p
gitzone commit --yes --push
# Run format before commit
gitzone commit --format
```
### Format Commands
```bash ```bash
# Basic format # Basic format
gitzone format gitzone format
# Read-only JSON plan
gitzone format plan --json
# Dry run to preview changes # Dry run to preview changes
gitzone format --dry-run gitzone format --dry-run
# Non-interactive mode # Non-interactive apply
gitzone format --yes gitzone format --write --yes
# Plan only (no execution) # Plan only (no execution)
gitzone format --plan-only gitzone format --plan-only
@@ -157,11 +216,10 @@ gitzone format --verbose
# Detailed diff views # Detailed diff views
gitzone format --detailed gitzone format --detailed
# Rollback operations # Inspect config for agents and scripts
gitzone format --rollback gitzone config show --json
gitzone format --rollback <operation-id> gitzone config set cli.output json
gitzone format --list-backups gitzone config get release.accessLevel
gitzone format --clean-backups
``` ```
## Common Issues (Now Resolved) ## Common Issues (Now Resolved)
@@ -182,7 +240,64 @@ gitzone format --clean-backups
## API Changes ## API Changes
- smartfile API updated to use fs.* and memory.* namespaces ### Smartfile v13 Migration (Latest - Completed)
The project has been fully migrated from @push.rocks/smartfile v11 to v13, which introduced a major breaking change where filesystem operations were split into two separate packages:
**Packages:**
- `@push.rocks/smartfile` v13.0.1 - File representation classes (SmartFile, StreamFile, VirtualDirectory)
- `@push.rocks/smartfs` v1.1.0 - Filesystem operations (read, write, exists, stat, etc.)
**Key API Changes:**
1. **File Reading**:
- Old: `plugins.smartfile.fs.toStringSync(path)` or `plugins.smartfile.fs.toObjectSync(path)`
- New: `await plugins.smartfs.file(path).encoding('utf8').read()` + JSON.parse if needed
- Important: `read()` returns `string | Buffer` - use `as string` type assertion when encoding is set
2. **File Writing**:
- Old: `plugins.smartfile.memory.toFs(content, path)` or `plugins.smartfile.memory.toFsSync(content, path)`
- New: `await plugins.smartfs.file(path).encoding('utf8').write(content)`
3. **File Existence**:
- Old: `plugins.smartfile.fs.fileExists(path)` or `plugins.smartfile.fs.fileExistsSync(path)`
- New: `await plugins.smartfs.file(path).exists()`
4. **Directory Operations**:
- Old: `plugins.smartfile.fs.ensureDir(path)`
- New: `await plugins.smartfs.directory(path).recursive().create()`
- Old: `plugins.smartfile.fs.remove(path)`
- New: `await plugins.smartfs.directory(path).recursive().delete()` or `await plugins.smartfs.file(path).delete()`
5. **Directory Listing**:
- Old: `plugins.smartfile.fs.listFolders(path)` or `plugins.smartfile.fs.listFoldersSync(path)`
- New: `await plugins.smartfs.directory(path).list()` then filter by `stats.isDirectory`
- Note: `list()` returns `IDirectoryEntry[]` with `path` and `name` properties - use `stat()` to check if directory
6. **File Stats**:
- Old: `stats.isDirectory()` (method)
- New: `stats.isDirectory` (boolean property)
- Old: `stats.mtimeMs`
- New: `stats.mtime.getTime()`
7. **SmartFile Factory**:
- Old: Direct SmartFile instantiation
- New: `plugins.smartfile.SmartFileFactory.nodeFs()` then factory methods
**Migration Pattern:**
All sync methods must become async. Functions that were previously synchronous (like `getProjectName()`) now return `Promise<T>` and must be awaited.
**Affected Modules:**
- ts/mod_format/\* (largest area - 15+ files)
- ts/mod_commit/\* (version bumping)
- ts/mod_services/\* (configuration management)
- ts/mod_meta/\* (meta repository management)
- ts/mod_standard/\* (template listing)
- ts/mod_template/\* (template operations)
**Previous API Changes:**
- smartnpm requires instance creation: `new NpmRegistry()` - smartnpm requires instance creation: `new NpmRegistry()`
- All file operations now use updated APIs
- Type imports use `import type` for proper verbatim module syntax - Type imports use `import type` for proper verbatim module syntax
+374 -192
View File
@@ -7,37 +7,245 @@
## 🎯 What is gitzone? ## 🎯 What is gitzone?
gitzone is a powerful command-line interface that supercharges your development workflow with automated project management, intelligent code formatting, and seamless version control. Whether you're bootstrapping a new TypeScript project, maintaining code quality, or managing complex multi-repository setups, gitzone has got you covered. gitzone is a powerful command-line interface that supercharges your development workflow with automated project management, intelligent code formatting, seamless version control, and development service orchestration. Whether you're bootstrapping a new TypeScript project, maintaining code quality, managing complex multi-repository setups, or spinning up local development databases, gitzone has got you covered.
## Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## 🏃‍♂️ Quick Start ## 🏃‍♂️ Quick Start
### Installation ### Installation
```bash ```bash
# Install globally via npm # Install globally via pnpm (recommended)
npm install -g @git.zone/cli
# Or with pnpm (recommended)
pnpm add -g @git.zone/cli pnpm add -g @git.zone/cli
# Or with npm
npm install -g @git.zone/cli
``` ```
Once installed, you can use either `gitzone` or the shorter `gzone` command from anywhere in your terminal. Once installed, you can use either `gitzone` or the shorter `gzone` command from anywhere in your terminal.
### Your First Command ### Your First Commands
```bash ```bash
# Create a new TypeScript npm package # Create a new TypeScript npm package
gitzone template npm gitzone template npm
# Format your entire codebase # Format your entire codebase (dry-run by default)
gitzone format gitzone format
# Create a semantic commit # Apply formatting changes
gitzone format --write
# Start local MongoDB and MinIO services
gitzone services start
# Create a semantic commit with AI-powered suggestions
gitzone commit gitzone commit
``` ```
## 🛠️ Core Features ## 🛠️ Core Features
### 🔀 Semantic Commits & Versioning
Create standardized commits with AI-powered suggestions that automatically handle versioning:
```bash
# Interactive commit with AI recommendations
gitzone commit
# Read-only recommendation for agents and scripts
gitzone commit recommend --json
# Auto-accept AI recommendations (skipped for BREAKING CHANGEs)
gitzone commit -y
# Auto-accept, push, build, and release
gitzone commit -ypbr
```
**Flags:**
| Flag | Long Form | Description |
| ---- | ----------- | ---------------------------------------- |
| `-y` | `--yes` | Auto-accept AI recommendations |
| `-p` | `--push` | Push to remote after commit |
| `-t` | `--test` | Run tests before committing |
| `-b` | `--build` | Build after commit, verify clean tree |
| `-r` | `--release` | Publish to configured npm registries |
| | `--format` | Run format before committing |
| | `--json` | Emit JSON for `gitzone commit recommend` |
**Workflow steps:**
1. 🤖 **AI-powered analysis** — analyzes your changes and suggests commit type, scope, and message
2. 📝 Interactive commit message builder (type: `fix`/`feat`/`BREAKING CHANGE`, scope, description)
3. 📜 Automatic changelog generation
4. 🏷️ Automatic version bumping (major/minor/patch) with git tag creation
5. 🔨 Optional build & verification
6. 🚀 Optional push to origin
7. 📦 Optional publish to npm registries
Supports both npm (`package.json`) and Deno (`deno.json`) projects, including dual-type projects.
### 🎨 Intelligent Code Formatting
Automatically format and standardize your entire codebase. **Dry-run by default** — nothing changes until you explicitly use `--write`:
```bash
# Preview what would change (default behavior)
gitzone format
# Emit a machine-readable plan
gitzone format plan --json
# Apply changes
gitzone format --write
# Auto-approve without prompts
gitzone format --yes --write
# Show detailed diffs
gitzone format --diff
# Enable verbose logging
gitzone format --verbose
```
**Flags:**
| Flag | Description |
| -------------------- | --------------------------------------------- |
| `--write` / `-w` | Apply changes (default is dry-run) |
| `--yes` | Auto-approve without interactive confirmation |
| `--plan-only` | Only show what would be done |
| `--save-plan <file>` | Save the format plan to a file |
| `--from-plan <file>` | Load and execute a saved plan |
| `--detailed` | Show detailed stats and save report |
| `--verbose` | Enable verbose logging |
| `--diff` | Show file diffs |
| `--json` | Emit a read-only format plan as JSON |
**Formatters (executed in order):**
1. 🧹 **Cleanup** — removes obsolete files (yarn.lock, package-lock.json, tslint.json, etc.)
2. ⚙️ **Smartconfig** — formats and standardizes `.smartconfig.json`
3. 📜 **License** — ensures proper licensing and checks dependency licenses
4. 📦 **Package.json** — standardizes package configuration
5. 📋 **Templates** — applies project template updates
6. 🙈 **Gitignore** — updates repository ignore rules
7. 🔧 **Tsconfig** — optimizes TypeScript configuration
8. ✨ **Prettier** — applies code formatting
9. 📖 **Readme** — ensures readme files exist
10. 📂 **Copy** — copies configured files
### 🐳 Development Services Management
Effortlessly manage local development services (MongoDB, MinIO S3, Elasticsearch) with Docker:
```bash
gitzone services [command]
```
**Commands:**
| Command | Description |
| ------------------------ | ------------------------------------------------------ |
| `start [service]` | Start services (`mongo`\|`s3`\|`elasticsearch`\|`all`) |
| `stop [service]` | Stop services |
| `restart [service]` | Restart services |
| `status` | Show current service status |
| `config` | Display configuration details |
| `set <csv>` | Set enabled services without prompts |
| `enable <service...>` | Enable one or more services |
| `disable <service...>` | Disable one or more services |
| `compass` | Get MongoDB Compass connection string with network IP |
| `logs [service] [lines]` | View service logs (default: 20 lines) |
| `reconfigure` | Reassign ports and restart all services |
| `remove` | Remove containers (preserves data) |
| `clean` | Remove containers AND data (⚠️ destructive) |
**Service aliases:**
- `mongo` / `mongodb` — MongoDB
- `minio` / `s3` — MinIO (S3-compatible storage)
- `elasticsearch` / `es` — Elasticsearch
- `all` — All services (default)
**Key features:**
- 🎲 **Smart port assignment** — automatically assigns random ports (2000030000) to avoid conflicts
- 📦 **Project isolation** — each project gets its own containers with unique names
- 💾 **Data persistence** — data stored in `.nogit/` survives container restarts
- 🔗 **MongoDB Compass support** — instantly get connection strings for GUI access
- 🌐 **Network IP detection** — detects your local network IP for remote connections
- ⚙️ **Auto-configuration** — creates `.nogit/env.json` with smart defaults
**Global operations (`-g` flag):**
```bash
# List all registered projects
gitzone services list -g
# Show status across all projects
gitzone services status -g
# Stop all containers across all projects
gitzone services stop -g
# Remove stale registry entries
gitzone services cleanup -g
```
**Example workflow:**
```bash
# Start all services for your project
gitzone services start
# Configure enabled services without prompts
gitzone services set mongodb,minio
# Check what's running
gitzone services status
# Get MongoDB Compass connection string
gitzone services compass
# Output: mongodb://defaultadmin:defaultpass@192.168.1.100:27018/myproject?authSource=admin
# View MongoDB logs
gitzone services logs mongo 50
# Stop services when done
gitzone services stop
```
### ⚙️ Release & Commit Configuration
Manage release registries and commit settings:
```bash
gitzone config [subcommand]
```
| Command | Description |
| ---------------------------------- | ---------------------------------------------------------- |
| `show` | Display current release config (registries, access level) |
| `get <path>` | Read a single value from `@git.zone/cli` |
| `set <path> <value>` | Write a single value to `@git.zone/cli` |
| `unset <path>` | Remove a single value from `@git.zone/cli` |
| `add [url]` | Add a registry URL (default: `https://registry.npmjs.org`) |
| `remove [url]` | Remove a registry URL (interactive selection if no URL) |
| `clear` | Clear all registries (with confirmation) |
| `access [public\|private]` | Set npm access level for publishing |
| `commit alwaysTest [true\|false]` | Always run tests before commit |
| `commit alwaysBuild [true\|false]` | Always build after commit |
| `services` | Configure which services are enabled |
Configuration is stored in `.smartconfig.json` under the `@git.zone/cli` key.
### 📦 Project Templates ### 📦 Project Templates
Instantly scaffold production-ready projects with best practices built-in: Instantly scaffold production-ready projects with best practices built-in:
@@ -46,94 +254,21 @@ Instantly scaffold production-ready projects with best practices built-in:
gitzone template [template-name] gitzone template [template-name]
``` ```
**Available templates:** **Interactive templates:**
- **`npm`** - TypeScript npm package with testing, CI/CD, and full tooling
- **`service`** - Microservice architecture with Docker support - **`npm`** — TypeScript npm package with testing, CI/CD, and full tooling
- **`website`** - Modern web application with LitElement and service workers - **`service`** Microservice architecture with Docker support
- **`wcc`** - Web Component Collection for reusable UI components - **`website`** — Modern web application with LitElement and service workers
- **`wcc`** — Web Component Collection for reusable UI components
Each template comes pre-configured with: Each template comes pre-configured with:
- ✅ TypeScript with modern configurations - ✅ TypeScript with modern configurations
- ✅ Automated testing setup - ✅ Automated testing setup with `@git.zone/tstest`
- ✅ CI/CD pipelines (GitLab/GitHub) - ✅ CI/CD pipelines (GitLab/GitHub)
- ✅ Code formatting and linting - ✅ Code formatting and linting
- ✅ Documentation structure - ✅ Documentation structure
### 🎨 Intelligent Code Formatting
The most powerful feature of gitzone - automatically format and standardize your entire codebase:
```bash
# Preview changes without applying them
gitzone format --dry-run
# Format with automatic approval
gitzone format --yes
# Save formatting plan for later execution
gitzone format --save-plan format-plan.json
# Execute a saved plan
gitzone format --from-plan format-plan.json
# Enable verbose output for debugging
gitzone format --verbose
```
**Format features:**
- 🔄 **Smart caching** - Only processes changed files
- 🛡️ **Rollback support** - Undo formatting changes if needed
- 📊 **Detailed reporting** - See exactly what changed
- ⚡ **Parallel execution** - Format multiple files simultaneously
- 🎯 **Module-specific formatting** - Target specific formatters
**Rollback capabilities:**
```bash
# List all available backups
gitzone format --list-backups
# Rollback to the last operation
gitzone format --rollback
# Rollback to a specific operation
gitzone format --rollback [operation-id]
# Clean old backups
gitzone format --clean-backups
```
**Formatters included:**
- **Prettier** - JavaScript/TypeScript code formatting
- **License** - Ensure proper licensing
- **Package.json** - Standardize package configurations
- **Tsconfig** - TypeScript configuration optimization
- **Readme** - Documentation formatting
- **Gitignore** - Repository ignore rules
- **Templates** - Project template updates
- **Npmextra** - Extended npm configurations
### 🔀 Semantic Commits & Versioning
Create standardized commits that automatically handle versioning:
```bash
gitzone commit
```
Features:
- 📝 Interactive commit message builder
- 🏷️ Automatic version bumping (major/minor/patch)
- 📜 Changelog generation
- 🚀 Optional auto-push to origin
- 🎯 Conventional commit compliance
The commit wizard guides you through:
1. **Type selection** (feat/fix/docs/style/refactor/perf/test/chore)
2. **Scope definition** (component/module affected)
3. **Description crafting**
4. **Breaking change detection**
5. **Version bump determination**
### 🏗️ Meta Repository Management ### 🏗️ Meta Repository Management
Manage multiple related repositories as a cohesive unit: Manage multiple related repositories as a cohesive unit:
@@ -145,34 +280,22 @@ gitzone meta init
# Add a sub-project # Add a sub-project
gitzone meta add [name] [git-url] gitzone meta add [name] [git-url]
# Update all sub-projects # Update all sub-projects (clone missing, clean superfluous)
gitzone meta update gitzone meta update
# Remove a sub-project # Remove a sub-project
gitzone meta remove [name] gitzone meta remove [name]
``` ```
Perfect for:
- Monorepo management
- Multi-package projects
- Coordinated deployments
- Synchronized versioning
### 🐳 Docker Management ### 🐳 Docker Management
Streamline your Docker workflow: Streamline your Docker workflow:
```bash ```bash
# Clean up all Docker resources # Clean up all Docker resources (containers, images, volumes, networks)
gitzone docker prune gitzone docker prune
``` ```
This command removes:
- Stopped containers
- Unused images
- Dangling volumes
- Unused networks
### 🔗 Quick CI/CD Access ### 🔗 Quick CI/CD Access
Jump directly to your CI/CD configurations: Jump directly to your CI/CD configurations:
@@ -195,11 +318,7 @@ Smoothly transition users from old to new packages:
gitzone deprecate gitzone deprecate
``` ```
Interactive wizard for: Interactive wizard that prompts for registry URLs, old package name, and new package name — then runs `npm deprecate` across all specified registries.
- Setting deprecation notices
- Guiding users to replacements
- Updating registry metadata
- Coordinating migration paths
### 🚦 Project Initialization ### 🚦 Project Initialization
@@ -209,16 +328,10 @@ Prepare existing projects for development:
gitzone start gitzone start
``` ```
Automatically: Automatically checks out master, pulls latest changes, and installs dependencies.
- Checks out master branch
- Pulls latest changes
- Installs dependencies
- Sets up development environment
### 🔧 Helper Utilities ### 🔧 Helper Utilities
Quick utilities for common tasks:
```bash ```bash
# Generate a unique short ID # Generate a unique short ID
gitzone helpers shortid gitzone helpers shortid
@@ -226,31 +339,33 @@ gitzone helpers shortid
## 📋 Configuration ## 📋 Configuration
### npmextra.json Configuration ### .smartconfig.json
Customize gitzone behavior through `npmextra.json`: Customize gitzone behavior through `.smartconfig.json`:
```json ```json
{ {
"gitzone": { "@git.zone/cli": {
"projectType": "npm",
"cli": {
"interactive": true,
"output": "human",
"checkUpdates": true
},
"release": {
"registries": ["https://registry.npmjs.org"],
"accessLevel": "public"
},
"commit": {
"alwaysTest": false,
"alwaysBuild": false
},
"format": { "format": {
"interactive": true, "interactive": true,
"showDiffs": false, "showStats": true,
"autoApprove": false,
"parallel": true,
"rollback": {
"enabled": true,
"autoRollbackOnError": true,
"backupRetentionDays": 7
},
"modules": { "modules": {
"skip": ["prettier"], "skip": ["prettier"],
"only": [], "only": []
"order": []
},
"cache": {
"enabled": true,
"clean": true
} }
} }
} }
@@ -259,51 +374,64 @@ Customize gitzone behavior through `npmextra.json`:
### Environment Variables ### Environment Variables
- `CI` - Detect CI environment for automated workflows - `CI` Detect CI environment for automated workflows
- `DEBUG` - Enable debug output - `DEBUG` Enable debug output
- `GITZONE_FORMAT_PARALLEL` - Control parallel formatting - `GITZONE_FORMAT_PARALLEL` Control parallel formatting
## 🏆 Best Practices
### For New Projects
1. Start with a template: `gitzone template npm`
2. Customize the generated structure
3. Run initial format: `gitzone format`
4. Set up CI/CD: `gitzone open ci`
### For Existing Projects
1. Initialize: `gitzone start`
2. Format codebase: `gitzone format --dry-run` (preview first!)
3. Apply formatting: `gitzone format --yes`
4. Commit changes: `gitzone commit`
### For Teams
1. Document format preferences in `npmextra.json`
2. Use `--save-plan` for reviewable format changes
3. Enable rollback for safety
4. Standardize commit conventions
## 🎯 Common Workflows ## 🎯 Common Workflows
### Clean Development Cycle ### Full-Stack Development Cycle
```bash ```bash
# 1. Start fresh # 1. Start fresh
gitzone start gitzone start
# 2. Make changes # 2. Spin up databases and services
gitzone services start
# 3. Make changes
# ... your development work ... # ... your development work ...
# 3. Format code # 4. Check service logs if needed
gitzone format gitzone services logs mongo
# 4. Commit with semantic versioning # 5. Preview format changes, then apply
gitzone format
gitzone format --write
# 6. Commit with semantic versioning
gitzone commit gitzone commit
# 5. Deploy (if CI/CD configured) # 7. Stop services when done
# Automatic via git push gitzone services stop
```
### Automated CI/CD Commit
```bash
# Auto-accept, test, build, push, and release in one command
gitzone commit -ytbpr
```
### Agent-Friendly Inspection
```bash
# Top-level machine-readable help
gitzone help config --json
# Read-only commit recommendation
gitzone commit recommend --json
# Read-only format plan
gitzone format plan --json
# Read or change config without prompts
gitzone config get release.accessLevel
gitzone config set cli.interactive false
``` ```
### Multi-Repository Management ### Multi-Repository Management
```bash ```bash
# 1. Set up meta repository # 1. Set up meta repository
gitzone meta init gitzone meta init
@@ -317,90 +445,144 @@ gitzone meta add shared https://github.com/org/shared.git
gitzone meta update gitzone meta update
``` ```
### Safe Formatting with Rollback ### Safe Formatting with Plan Review
```bash ```bash
# 1. Preview changes # 1. Preview changes (default)
gitzone format --dry-run gitzone format
# 2. Save plan for review # 2. Save plan for review
gitzone format --save-plan format-changes.json gitzone format --save-plan format-changes.json
# 3. Apply formatting # 3. Apply from saved plan
gitzone format --from-plan format-changes.json gitzone format --from-plan format-changes.json --write
```
# 4. If something goes wrong, rollback ### Database-Driven Development
gitzone format --rollback
```bash
# 1. Start MongoDB, MinIO, and Elasticsearch
gitzone services start
# 2. Get connection details
gitzone services config
# 3. Connect with MongoDB Compass
gitzone services compass
# 4. Monitor services
gitzone services status
# 5. Clean everything when done
gitzone services clean # ⚠️ Warning: deletes data
``` ```
## 🔌 Integrations ## 🔌 Integrations
### CI/CD Platforms ### CI/CD Platforms
- **GitLab CI** - Full pipeline support with templates
- **GitHub Actions** - Automated workflows - **GitLab CI** — full pipeline support with templates
- **Docker** - Container-based deployments - **GitHub Actions** — automated workflows
- **Docker** — container-based deployments
### Development Tools ### Development Tools
- **TypeScript** - First-class support
- **Prettier** - Code formatting - **TypeScript** — first-class support
- **ESLint** - Linting (via format modules) - **Prettier** — code formatting
- **npm/pnpm** - Package management - **pnpm** — package management
- **MongoDB** — local database service
- **MinIO** — S3-compatible object storage
- **Elasticsearch** — search and analytics
- **MongoDB Compass** — database GUI integration
### Version Control ### Version Control
- **Git** - Deep integration
- **Semantic Versioning** - Automatic version bumping - **Git** — deep integration
- **Conventional Commits** - Standardized commit messages - **Semantic Versioning** — automatic version bumping
- **Conventional Commits** — standardized commit messages
- **AI-Powered Analysis** — intelligent commit suggestions via `@git.zone/tsdoc`
## 💡 Pro Tips ## 💡 Pro Tips
1. **Use aliases**: Add `alias gz='gitzone'` to your shell profile 1. **Use aliases**: Add `alias gz='gitzone'` to your shell profile
2. **Combine commands**: `gitzone format --yes && gitzone commit` 2. **Combine flags**: `gitzone commit -ypbr` for the full auto workflow
3. **Leverage templates**: Start projects right with proven structures 3. **Leverage templates**: Start projects right with proven structures
4. **Enable caching**: Dramatically speeds up formatting operations 4. **Enable caching**: Dramatically speeds up formatting operations
5. **Save format plans**: Review changes before applying in production 5. **Save format plans**: Review changes before applying
6. **Port management**: Let services auto-assign ports to avoid conflicts
7. **Use MongoDB Compass**: `gitzone services compass` for visual DB management
8. **Global service management**: `gitzone services status -g` to see all projects' services at once
## 🐛 Troubleshooting ## 🐛 Troubleshooting
### Format Command Shows "Cancelled" ### Format Command Shows "Cancelled"
If the format command shows cancelled even after confirming:
- Check your `npmextra.json` configuration - Check your `npmextra.json` configuration
- Try with `--yes` flag to skip confirmation - Try with `--yes --write` flags
- Use `--verbose` for detailed output - Use `--verbose` for detailed output
### Docker Commands Fail ### Docker Commands Fail
Ensure Docker daemon is running: Ensure Docker daemon is running:
```bash ```bash
docker info docker info
``` ```
### Services Won't Start
```bash
# Services auto-assign ports, but you can check the config
cat .nogit/env.json
# Verify Docker is running
docker ps
# Reassign ports if there are conflicts
gitzone services reconfigure
```
### Template Creation Issues ### Template Creation Issues
Verify npm/pnpm is properly configured:
Verify pnpm/npm is properly configured:
```bash ```bash
npm config get registry npm config get registry
``` ```
### MongoDB Connection Issues
- Ensure services are running: `gitzone services status`
- Check firewall settings for the assigned ports
- Use `gitzone services compass` for the correct connection string
## 📈 Performance ## 📈 Performance
gitzone is optimized for speed: gitzone is optimized for speed:
- **Parallel processing** for format operations
- **Smart caching** to avoid redundant work - **Parallel processing** for format operations
- **Incremental updates** for meta repositories - 🧠 **Smart caching** to avoid redundant work
- **Minimal dependencies** for fast installation - 📊 **Incremental updates** for meta repositories
- 🐳 **Isolated services** prevent resource conflicts
- 🎲 **Auto port assignment** eliminates manual configuration
## License and Legal Information ## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file. **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks ### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH. This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information ### Company Information
Task Venture Capital GmbH Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc. For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works. By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
+104 -142
View File
@@ -1,170 +1,132 @@
# Gitzone Format Module Improvement Plan # GitZone Services Command Implementation Plan
Please reread /home/philkunz/.claude/CLAUDE.md before proceeding with any implementation.
## Overview ## Overview
This plan outlines improvements for the gitzone format module to enhance its functionality, reliability, and maintainability.
## Phase 1: Core Improvements (High Priority) - COMPLETED ✅ Implement the `gitzone services` command to manage MongoDB and MinIO containers for development projects.
### 1. Enhanced Error Handling & Recovery ✅ ## Tasks
- [x] Implement rollback mechanism for failed format operations
- [x] Add detailed error messages with recovery suggestions
- [x] Create a `--dry-run` flag to preview changes before applying
- [x] Add transaction-like behavior: all-or-nothing formatting
- [x] Implement plan → action workflow as default behavior
### 2. Complete Missing Functionality ✅ ### Module Structure Setup
- [x] Implement the `ensureDependency` function in format.packagejson.ts
- [x] Develop the copy module for file pattern-based copying
- [x] Add dependency version constraint management
- [x] Support workspace/monorepo configurations (via configuration)
### 3. Configuration & Flexibility ✅ - [x] Create `ts/mod_services/` directory
- [x] Extend npmextra.json gitzone configuration section - [x] Create `mod.plugins.ts` with required imports
- [x] Allow custom license exclusion/inclusion lists - [x] Create `helpers.ts` with utility functions
- [x] Make format steps configurable (skip/include specific modules) - [x] Create `classes.serviceconfiguration.ts` for config handling
- [x] Support custom template directories (via configuration) - [x] Create `classes.dockercontainer.ts` for Docker operations
- [x] Add format profiles for different project types - [x] Create `classes.servicemanager.ts` for service management
- [x] Create `index.ts` with main command logic
### 4. Architecture Changes ✅ ### Core Functionality
- [x] Introduce a `FormatContext` class to manage state across modules
- [x] Create abstract `BaseFormatter` class for consistent module structure
- [x] Implement event system for inter-module communication (via context)
- [x] Add validation layer before format execution
- [x] Implement `FormatPlanner` class for plan → action workflow
## Phase 2: Performance & Reporting (Medium Priority) - COMPLETED ✅ - [x] Implement ServiceConfiguration class
- [x] Load/create `.nogit/env.json` configuration
- [x] Generate random available ports (20000-30000 range)
- [x] Preserve existing custom values
- [x] Provide default values for missing fields
### 5. Performance Optimizations ✅ - [x] Implement DockerContainer class
- [x] Implement parallel execution for independent format modules - [x] Check container status
- [x] Add file change detection to skip unchanged files - [x] Start/stop/restart containers
- [x] Create format cache to track last formatted state - [x] Execute Docker commands
- [x] Optimize Prettier runs by batching files - [x] Handle container logs
- [x] Manage volumes and port bindings
### 6. Enhanced Reporting & Visibility ✅ - [x] Implement ServiceManager class
- [x] Generate comprehensive format report showing all changes - [x] Manage MongoDB containers
- [x] Add diff view for file modifications - [x] Manage MinIO containers
- [x] Create verbose logging option - [x] Handle container lifecycle
- [x] Add format statistics (files changed, time taken, etc.) - [x] Generate project-specific container names
- [x] Manage data directories in `.nogit/`
- [x] Generate MongoDB Compass connection strings
## Phase 3: Advanced Features (Lower Priority) - PARTIALLY COMPLETED ### Commands Implementation
### 7. Better Integration & Extensibility ⏳ - [x] `start` command - Start services (mongo|s3|all)
- [ ] Create plugin system for custom format modules - [x] `stop` command - Stop services (mongo|s3|all)
- [ ] Add hooks for pre/post format operations - [x] `restart` command - Restart services (mongo|s3|all)
- [ ] Support custom validation rules - [x] `status` command - Show service status
- [ ] Integrate with git hooks for pre-commit formatting - [x] `config` command - Show current configuration
- [x] `compass` command - Show MongoDB Compass connection string
- [x] `logs` command - Show service logs with line count
- [x] `remove` command - Remove containers (preserve data)
- [x] `clean` command - Remove containers and data
### 8. Improved Template Integration ### Integration
- [ ] Better error handling when smartscaf operations fail
- [ ] Add pre/post template hooks for custom processing
- [ ] Validate template results before proceeding with format
- [ ] Support skipping template updates via configuration
### 9. Enhanced License Management ⏳ - [x] Add `@push.rocks/smartshell` to main plugins.ts
- [ ] Make license checking configurable (partial) - [x] Add `@push.rocks/smartnetwork` to main plugins.ts
- [ ] Add license compatibility matrix - [x] Add `@push.rocks/smartinteraction` to main plugins.ts
- [x] Support license exceptions for specific packages - [x] Register services command in `gitzone.cli.ts`
- [ ] Generate license report for compliance
### 10. Better Package.json Management ⏳ ### Features
- [ ] Smart dependency sorting and grouping
- [ ] Automated script generation based on project type
- [ ] Support for pnpm workspace configurations
- [ ] Validation of package.json schema
### 11. Quality of Life Improvements ⏳ - [x] Auto-configuration with smart defaults
- [ ] Interactive mode for format configuration - [x] Random port assignment to avoid conflicts
- [ ] Undo/redo capability for format operations - [x] Project isolation with unique container names
- [ ] Format presets for common scenarios - [x] Data persistence in `.nogit/` directories
- [x] Better progress indicators and user feedback - [x] Status display (running/stopped/not installed)
- [x] Interactive confirmations for destructive operations
- [x] Colored console output
- [x] MinIO bucket auto-creation
- [x] MongoDB Compass connection string with network IP
## Implementation Status ### Testing
### ✅ Completed Features - [ ] Test service start/stop operations
- [ ] Test configuration creation and updates
- [ ] Test port collision handling
- [ ] Test data persistence
- [ ] Test MongoDB Compass connection string generation
- [ ] Test all command variations
1. **Rollback Mechanism** ## Configuration Format
- Full backup/restore functionality
- Manifest tracking and integrity checks
- CLI commands for rollback operations
2. **Plan → Action Workflow** ```json
- Two-phase approach (analyze then execute) {
- Interactive confirmation "PROJECT_NAME": "derived-from-package-name",
- Dry-run support "MONGODB_HOST": "localhost",
"MONGODB_NAME": "project-name",
"MONGODB_PORT": "random-port",
"MONGODB_USER": "defaultadmin",
"MONGODB_PASS": "defaultpass",
"S3_HOST": "localhost",
"S3_PORT": "random-port",
"S3_CONSOLE_PORT": "s3-port+1",
"S3_USER": "defaultadmin",
"S3_PASS": "defaultpass",
"S3_BUCKET": "project-name-documents"
}
```
3. **Configuration System** ## Command Examples
- Comprehensive npmextra.json support
- Module control (skip/only/order)
- Cache configuration
- Parallel execution settings
4. **Performance Improvements** ```bash
- Parallel execution by dependency analysis gitzone services start # Start all services
- File change caching gitzone services start mongo # Start only MongoDB
- Prettier batching gitzone services stop # Stop all services
- Execution time tracking gitzone services status # Check service status
gitzone services config # Show configuration
gitzone services compass # Show MongoDB Compass connection string
gitzone services logs mongo 50 # Show last 50 lines of MongoDB logs
gitzone services remove # Remove containers (preserve data)
gitzone services clean # Remove containers and data
```
5. **Reporting & Statistics** ## Progress Notes
- Detailed diff views
- Execution statistics
- Verbose logging mode
- Save reports to file
6. **Architecture Improvements** Implementation started: 2025-08-14
- BaseFormatter abstract class Implementation completed: 2025-08-14
- FormatContext for state management
- DependencyAnalyzer for parallel execution
- Type-safe interfaces
### 🚧 Partially Completed ## Summary
1. **License Management** Successfully implemented the `gitzone services` command in TypeScript, providing a complete replacement for the `services.sh` shell script. The implementation includes:
- Basic configuration support
- Exception handling for specific packages
- Need: compatibility matrix, compliance reports
2. **Package.json Management** 1. **Complete Docker service management** for MongoDB and MinIO containers
- Basic ensureDependency implementation 2. **Smart configuration management** with automatic port assignment and conflict avoidance
- Need: smart sorting, script generation, validation 3. **MongoDB Compass support** with network IP detection for remote connections
4. **Project isolation** using project-specific container names
5. **Data persistence** in `.nogit/` directories
6. **Interactive confirmations** for destructive operations
7. **Comprehensive command set** including start, stop, restart, status, config, compass, logs, remove, and clean commands
### ⏳ Not Started The module is fully integrated into the gitzone CLI and ready for testing.
1. **Plugin System**
- Need to design plugin API
- Hook system for pre/post operations
- Custom validation rules
2. **Git Integration**
- Pre-commit hooks
- Automatic formatting on commit
3. **Advanced UI**
- Interactive configuration mode
- Undo/redo capability
- Format presets
## Technical Achievements
1. **Type Safety**: All new code uses TypeScript interfaces and types
2. **Error Handling**: Comprehensive try-catch blocks with rollback
3. **API Compatibility**: Updated to use latest smartfile/smartnpm APIs
4. **Testing**: Ready for comprehensive test suite
5. **Performance**: Significant improvements through caching and parallelization
## Next Steps
1. Write comprehensive tests for all new functionality
2. Create user documentation for new features
3. Consider plugin API design for extensibility
4. Implement remaining Phase 3 features based on user feedback
5. Performance benchmarking and optimization
## Success Metrics Achieved
- ✅ Reduced error rates through rollback mechanism
- ✅ Faster execution through parallel processing and caching
- ✅ Enhanced user control through configuration
- ✅ Better visibility through reporting and statistics
- ✅ Improved maintainability through better architecture
Submodule
+1
Submodule test added at 0b89443584
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/cli', name: '@git.zone/cli',
version: '1.16.8', version: '2.14.0',
description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.' 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.'
} }
+15 -7
View File
@@ -38,13 +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'] // Read szci config for backward compatibility
>('npmci', { const szciConfig = smartconfigInstance.dataFor<any>('@ship.zone/szci', {});
npmAccessLevel: 'public',
}); // Prefer accessLevel from @git.zone/cli.release, fallback to @ship.zone/szci.npmAccessLevel
const accessLevel =
(this.data as any)?.release?.accessLevel ||
szciConfig?.npmAccessLevel ||
'public';
this.data.npmciOptions = {
npmAccessLevel: accessLevel,
};
} }
constructor() {} constructor() {}
+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;
+61 -47
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,102 +46,105 @@ 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);
}); });
/** /**
* deprecate a package on npm * deprecate a package on npm
*/ */
gitzoneSmartcli.addCommand('deprecate').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("deprecate").subscribe(async (argvArg) => {
const modDeprecate = await import('./mod_deprecate/index.js'); const modDeprecate = await import("./mod_deprecate/index.js");
await modDeprecate.run(); await modDeprecate.run();
}); });
/** /**
* docker * docker
*/ */
gitzoneSmartcli.addCommand('docker').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("docker").subscribe(async (argvArg) => {
const modDocker = await import('./mod_docker/index.js'); const modDocker = await import("./mod_docker/index.js");
await modDocker.run(argvArg); await modDocker.run(argvArg);
}); });
/** /**
* Update all files that comply with the gitzone standard * Update all files that comply with the gitzone standard
*/ */
gitzoneSmartcli.addCommand('format').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("format").subscribe(async (argvArg) => {
const config = GitzoneConfig.fromCwd(); const config = GitzoneConfig.fromCwd();
const modFormat = await import('./mod_format/index.js'); const modFormat = await import("./mod_format/index.js");
// Handle rollback commands
if (argvArg.rollback) {
await modFormat.handleRollback(argvArg.rollback);
return;
}
if (argvArg['list-backups']) {
await modFormat.handleListBackups();
return;
}
if (argvArg['clean-backups']) {
await modFormat.handleCleanBackups();
return;
}
// Handle format with options // Handle format with options
// Default is dry-mode, use --write/-w to apply changes
await modFormat.run({ await modFormat.run({
dryRun: argvArg['dry-run'], ...argvArg,
write: argvArg.write || argvArg.w,
dryRun: argvArg["dry-run"],
yes: argvArg.yes, yes: argvArg.yes,
planOnly: argvArg['plan-only'], planOnly: argvArg["plan-only"],
savePlan: argvArg['save-plan'], savePlan: argvArg["save-plan"],
fromPlan: argvArg['from-plan'], fromPlan: argvArg["from-plan"],
detailed: argvArg.detailed, detailed: argvArg.detailed,
interactive: argvArg.interactive !== false, interactive: argvArg.interactive !== false,
parallel: argvArg.parallel !== false,
verbose: argvArg.verbose, verbose: argvArg.verbose,
diff: argvArg.diff,
}); });
}); });
/** /**
* run meta commands * run meta commands
*/ */
gitzoneSmartcli.addCommand('meta').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("meta").subscribe(async (argvArg) => {
const config = GitzoneConfig.fromCwd(); const config = GitzoneConfig.fromCwd();
const modMeta = await import('./mod_meta/index.js'); const modMeta = await import("./mod_meta/index.js");
modMeta.run(argvArg); modMeta.run(argvArg);
}); });
/** /**
* open assets * open assets
*/ */
gitzoneSmartcli.addCommand('open').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("open").subscribe(async (argvArg) => {
const modOpen = await import('./mod_open/index.js'); const modOpen = await import("./mod_open/index.js");
modOpen.run(argvArg); modOpen.run(argvArg);
}); });
/** /**
* add a readme to a project * add a readme to a project
*/ */
gitzoneSmartcli.addCommand('template').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("template").subscribe(async (argvArg) => {
const modTemplate = await import('./mod_template/index.js'); const modTemplate = await import("./mod_template/index.js");
modTemplate.run(argvArg); modTemplate.run(argvArg);
}); });
/** /**
* start working on a project * start working on a project
*/ */
gitzoneSmartcli.addCommand('start').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("start").subscribe(async (argvArg) => {
const modTemplate = await import('./mod_start/index.js'); const modTemplate = await import("./mod_start/index.js");
modTemplate.run(argvArg); modTemplate.run(argvArg);
}); });
gitzoneSmartcli.addCommand('helpers').subscribe(async (argvArg) => { gitzoneSmartcli.addCommand("helpers").subscribe(async (argvArg) => {
const modHelpers = await import('./mod_helpers/index.js'); const modHelpers = await import("./mod_helpers/index.js");
modHelpers.run(argvArg); modHelpers.run(argvArg);
}); });
/**
* manage 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;
+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;
};
+497 -54
View File
@@ -1,42 +1,171 @@
// 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 helpers from "./mod.helpers.js";
import * as ui from "./mod.ui.js";
import { ReleaseConfig } from "../mod_config/classes.releaseconfig.js";
import type { ICliMode } from "../helpers.climode.js";
import {
getCliMode,
printJson,
runWithSuppressedOutput,
} from "../helpers.climode.js";
export const run = async (argvArg: any) => { export const run = async (argvArg: any) => {
const mode = await getCliMode(argvArg);
const subcommand = argvArg._?.[1];
if (mode.help || subcommand === "help") {
showHelp(mode);
return;
}
if (subcommand === "recommend") {
await handleRecommend(mode);
return;
}
if (mode.json) {
printJson({
ok: false,
error:
"JSON output is only supported for the read-only recommendation flow. Use `gitzone commit recommend --json`.",
});
return;
}
// Read commit config from .smartconfig.json
const smartconfigInstance = new plugins.smartconfig.Smartconfig();
const gitzoneConfig = smartconfigInstance.dataFor<{
commit?: {
alwaysTest?: boolean;
alwaysBuild?: boolean;
};
}>("@git.zone/cli", {});
const commitConfig = gitzoneConfig.commit || {};
// Check flags and merge with config options
const wantsRelease = !!(argvArg.r || argvArg.release);
const wantsTest = !!(argvArg.t || argvArg.test || commitConfig.alwaysTest);
const wantsBuild = !!(argvArg.b || argvArg.build || commitConfig.alwaysBuild);
let releaseConfig: ReleaseConfig | null = null;
if (wantsRelease) {
releaseConfig = await ReleaseConfig.fromCwd();
if (!releaseConfig.hasRegistries()) {
logger.log("error", "No release registries configured.");
console.log("");
console.log(
" Run `gitzone config add <registry-url>` to add registries.",
);
console.log("");
process.exit(1);
}
}
// Print execution plan at the start
ui.printExecutionPlan({
autoAccept: !!(argvArg.y || argvArg.yes),
push: !!(argvArg.p || argvArg.push),
test: wantsTest,
build: wantsBuild,
release: wantsRelease,
format: !!argvArg.format,
registries: releaseConfig?.getRegistries(),
});
if (argvArg.format) { if (argvArg.format) {
const formatMod = await import('../mod_format/index.js'); const formatMod = await import("../mod_format/index.js");
await formatMod.run(); await formatMod.run();
} }
logger.log('info', `gathering facts...`); // Run tests early to fail fast before analysis
if (wantsTest) {
ui.printHeader("🧪 Running tests...");
const smartshellForTest = new plugins.smartshell.Smartshell({
executor: "bash",
sourceFilePaths: [],
});
const testResult = await smartshellForTest.exec("pnpm test");
if (testResult.exitCode !== 0) {
logger.log("error", "Tests failed. Aborting commit.");
process.exit(1);
}
logger.log("success", "All tests passed.");
}
ui.printHeader("🔍 Analyzing repository changes...");
const aidoc = new plugins.tsdoc.AiDoc(); const aidoc = new plugins.tsdoc.AiDoc();
await aidoc.start(); await aidoc.start();
const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd); const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd);
await aidoc.stop();
ui.printRecommendation({
recommendedNextVersion: nextCommitObject.recommendedNextVersion,
recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel,
recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope,
recommendedNextVersionMessage:
nextCommitObject.recommendedNextVersionMessage,
});
let answerBucket: plugins.smartinteract.AnswerBucket;
// Check if -y/--yes flag is set AND version is not a breaking change
// Breaking changes (major version bumps) always require manual confirmation
const isBreakingChange =
nextCommitObject.recommendedNextVersionLevel === "BREAKING CHANGE";
const canAutoAccept = (argvArg.y || argvArg.yes) && !isBreakingChange;
if (canAutoAccept) {
// Auto-mode: create AnswerBucket programmatically
logger.log("info", "✓ Auto-accepting AI recommendations (--yes flag)");
answerBucket = new plugins.smartinteract.AnswerBucket();
answerBucket.addAnswer({
name: "commitType",
value: nextCommitObject.recommendedNextVersionLevel,
});
answerBucket.addAnswer({
name: "commitScope",
value: nextCommitObject.recommendedNextVersionScope,
});
answerBucket.addAnswer({
name: "commitDescription",
value: nextCommitObject.recommendedNextVersionMessage,
});
answerBucket.addAnswer({
name: "pushToOrigin",
value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided
});
answerBucket.addAnswer({
name: "createRelease",
value: wantsRelease,
});
} else {
// Warn if --yes was provided but we're requiring confirmation due to breaking change
if (isBreakingChange && (argvArg.y || argvArg.yes)) {
logger.log( logger.log(
'info', "warn",
`--------- "⚠️ BREAKING CHANGE detected - manual confirmation required",
Next recommended commit would be:
===========
-> ${nextCommitObject.recommendedNextVersion}:
-> ${nextCommitObject.recommendedNextVersionLevel}(${nextCommitObject.recommendedNextVersionScope}): ${nextCommitObject.recommendedNextVersionMessage}
===========
`,
); );
}
// Interactive mode: prompt user for input
const commitInteract = new plugins.smartinteract.SmartInteract(); const commitInteract = new plugins.smartinteract.SmartInteract();
commitInteract.addQuestions([ commitInteract.addQuestions([
{ {
type: 'list', type: "list",
name: `commitType`, name: `commitType`,
message: `Choose TYPE of the commit:`, message: `Choose TYPE of the commit:`,
choices: [`fix`, `feat`, `BREAKING CHANGE`], choices: [`fix`, `feat`, `BREAKING CHANGE`],
default: nextCommitObject.recommendedNextVersionLevel, default: nextCommitObject.recommendedNextVersionLevel,
}, },
{ {
type: 'input', type: "input",
name: `commitScope`, name: `commitScope`,
message: `What is the SCOPE of the commit:`, message: `What is the SCOPE of the commit:`,
default: nextCommitObject.recommendedNextVersionScope, default: nextCommitObject.recommendedNextVersionScope,
@@ -48,83 +177,397 @@ export const run = async (argvArg: any) => {
default: nextCommitObject.recommendedNextVersionMessage, default: nextCommitObject.recommendedNextVersionMessage,
}, },
{ {
type: 'confirm', type: "confirm",
name: `pushToOrigin`, name: `pushToOrigin`,
message: `Do you want to push this version now?`, message: `Do you want to push this version now?`,
default: true, default: true,
}, },
{
type: "confirm",
name: `createRelease`,
message: `Do you want to publish to npm registries?`,
default: wantsRelease,
},
]); ]);
const answerBucket = await commitInteract.runQueue(); answerBucket = await commitInteract.runQueue();
const commitString = createCommitStringFromAnswerBucket(answerBucket); }
const commitVersionType = (() => { const commitString = createCommitStringFromAnswerBucket(answerBucket);
switch (answerBucket.getAnswerFor('commitType')) { const commitType = answerBucket.getAnswerFor("commitType");
case 'fix': let commitVersionType: helpers.VersionType;
return 'patch'; switch (commitType) {
case 'feat': case "fix":
return 'minor'; commitVersionType = "patch";
case 'BREAKING CHANGE': break;
return 'major'; case "feat":
commitVersionType = "minor";
break;
case "BREAKING CHANGE":
commitVersionType = "major";
break;
default:
throw new Error(`Unsupported commit type: ${commitType}`);
} }
})();
logger.log('info', `OK! Creating commit with message '${commitString}'`); ui.printHeader("✨ Creating Semantic Commit");
ui.printCommitMessage(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 ...`); // Load release config if user wants to release (interactively selected)
if (answerBucket.getAnswerFor("createRelease") && !releaseConfig) {
releaseConfig = await ReleaseConfig.fromCwd();
if (!releaseConfig.hasRegistries()) {
logger.log("error", "No release registries configured.");
console.log("");
console.log(
" Run `gitzone config add <registry-url>` to add registries.",
);
console.log("");
process.exit(1);
}
}
// Determine total steps based on options
// Note: test runs early (like format) so not counted in numbered steps
const willPush =
answerBucket.getAnswerFor("pushToOrigin") && !(process.env.CI === "true");
const willRelease =
answerBucket.getAnswerFor("createRelease") &&
releaseConfig?.hasRegistries();
let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version
if (wantsBuild) totalSteps += 2; // build step + verification step
if (willPush) totalSteps++;
if (willRelease) totalSteps++;
let currentStep = 0;
// Step 1: Baking commitinfo
currentStep++;
ui.printStep(
currentStep,
totalSteps,
"🔧 Baking commit info into code",
"in-progress",
);
const commitInfo = new plugins.commitinfo.CommitInfo( const commitInfo = new plugins.commitinfo.CommitInfo(
paths.cwd, paths.cwd,
commitVersionType, commitVersionType,
); );
await commitInfo.writeIntoPotentialDirs(); await commitInfo.writeIntoPotentialDirs();
ui.printStep(
currentStep,
totalSteps,
"🔧 Baking commit info into code",
"done",
);
logger.log('info', `Writing changelog.md ...`); // Step 2: Writing changelog
let changelog = nextCommitObject.changelog; currentStep++;
ui.printStep(
currentStep,
totalSteps,
"📄 Generating changelog.md",
"in-progress",
);
let changelog = nextCommitObject.changelog || "# Changelog\n";
changelog = changelog.replaceAll( changelog = changelog.replaceAll(
'{{nextVersion}}', "{{nextVersion}}",
(await commitInfo.getNextPlannedVersion()).versionString, (await commitInfo.getNextPlannedVersion()).versionString,
); );
changelog = changelog.replaceAll( changelog = changelog.replaceAll(
'{{nextVersionScope}}', "{{nextVersionScope}}",
`${await answerBucket.getAnswerFor('commitType')}(${await answerBucket.getAnswerFor('commitScope')})`, `${await answerBucket.getAnswerFor("commitType")}(${await answerBucket.getAnswerFor("commitScope")})`,
); );
changelog = changelog.replaceAll( changelog = changelog.replaceAll(
'{{nextVersionMessage}}', "{{nextVersionMessage}}",
nextCommitObject.recommendedNextVersionMessage, nextCommitObject.recommendedNextVersionMessage,
); );
if (nextCommitObject.recommendedNextVersionDetails?.length > 0) { if (nextCommitObject.recommendedNextVersionDetails?.length > 0) {
changelog = changelog.replaceAll( changelog = changelog.replaceAll(
'{{nextVersionDetails}}', "{{nextVersionDetails}}",
'- ' + nextCommitObject.recommendedNextVersionDetails.join('\n- '), "- " + nextCommitObject.recommendedNextVersionDetails.join("\n- "),
); );
} else { } else {
changelog = changelog.replaceAll('\n{{nextVersionDetails}}', ''); changelog = changelog.replaceAll("\n{{nextVersionDetails}}", "");
} }
await plugins.smartfile.memory.toFs( await plugins.smartfs
changelog, .file(plugins.path.join(paths.cwd, `changelog.md`))
plugins.path.join(paths.cwd, `changelog.md`), .encoding("utf8")
.write(changelog);
ui.printStep(currentStep, totalSteps, "📄 Generating changelog.md", "done");
// Step 3: Staging files
currentStep++;
ui.printStep(currentStep, totalSteps, "📦 Staging files", "in-progress");
await smartshellInstance.exec(`git add -A`);
ui.printStep(currentStep, totalSteps, "📦 Staging files", "done");
// Step 4: Creating commit
currentStep++;
ui.printStep(
currentStep,
totalSteps,
"💾 Creating git commit",
"in-progress",
);
await smartshellInstance.exec(`git commit -m "${commitString}"`);
ui.printStep(currentStep, totalSteps, "💾 Creating git commit", "done");
// Step 5: Bumping version
currentStep++;
const projectType = await helpers.detectProjectType();
const newVersion = await helpers.bumpProjectVersion(
projectType,
commitVersionType,
currentStep,
totalSteps,
); );
logger.log('info', `Staging files for commit:`); // Step 6: Run build (optional)
await smartshellInstance.exec(`git add -A`); if (wantsBuild) {
await smartshellInstance.exec(`git commit -m "${commitString}"`); currentStep++;
await smartshellInstance.exec(`npm version ${commitVersionType}`); ui.printStep(currentStep, totalSteps, "🔨 Running build", "in-progress");
if ( const buildResult = await smartshellInstance.exec("pnpm build");
answerBucket.getAnswerFor('pushToOrigin') && if (buildResult.exitCode !== 0) {
!(process.env.CI === 'true') ui.printStep(currentStep, totalSteps, "🔨 Running build", "error");
) { logger.log("error", "Build failed. Aborting release.");
await smartshellInstance.exec(`git push origin master --follow-tags`); process.exit(1);
}
ui.printStep(currentStep, totalSteps, "🔨 Running build", "done");
// Step 7: Verify no uncommitted changes
currentStep++;
ui.printStep(
currentStep,
totalSteps,
"🔍 Verifying clean working tree",
"in-progress",
);
const statusResult = await smartshellInstance.exec(
"git status --porcelain",
);
if (statusResult.stdout.trim() !== "") {
ui.printStep(
currentStep,
totalSteps,
"🔍 Verifying clean working tree",
"error",
);
logger.log(
"error",
"Build produced uncommitted changes. This usually means build output is not gitignored.",
);
logger.log("error", "Uncommitted files:");
console.log(statusResult.stdout);
logger.log(
"error",
"Aborting release. Please ensure build artifacts are in .gitignore",
);
process.exit(1);
}
ui.printStep(
currentStep,
totalSteps,
"🔍 Verifying clean working tree",
"done",
);
}
// Step: Push to remote (optional)
const currentBranch = await helpers.detectCurrentBranch();
if (willPush) {
currentStep++;
ui.printStep(
currentStep,
totalSteps,
`🚀 Pushing to origin/${currentBranch}`,
"in-progress",
);
await smartshellInstance.exec(
`git push origin ${currentBranch} --follow-tags`,
);
ui.printStep(
currentStep,
totalSteps,
`🚀 Pushing to origin/${currentBranch}`,
"done",
);
}
// Step 7: Publish to npm registries (optional)
let releasedRegistries: string[] = [];
if (willRelease && releaseConfig) {
currentStep++;
const registries = releaseConfig.getRegistries();
ui.printStep(
currentStep,
totalSteps,
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
"in-progress",
);
const accessLevel = releaseConfig.getAccessLevel();
for (const registry of registries) {
try {
await smartshellInstance.exec(
`npm publish --registry=${registry} --access=${accessLevel}`,
);
releasedRegistries.push(registry);
} catch (error) {
logger.log("error", `Failed to publish to ${registry}: ${error}`);
}
}
if (releasedRegistries.length === registries.length) {
ui.printStep(
currentStep,
totalSteps,
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
"done",
);
} else {
ui.printStep(
currentStep,
totalSteps,
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
"error",
);
}
}
console.log(""); // Add spacing before summary
// Get commit SHA for summary
const commitShaResult = await smartshellInstance.exec(
"git rev-parse --short HEAD",
);
const commitSha = commitShaResult.stdout.trim();
// Print final summary
ui.printSummary({
projectType,
branch: currentBranch,
commitType: answerBucket.getAnswerFor("commitType"),
commitScope: answerBucket.getAnswerFor("commitScope"),
commitMessage: answerBucket.getAnswerFor("commitDescription"),
newVersion: newVersion,
commitSha: commitSha,
pushed: willPush,
released: releasedRegistries.length > 0,
releasedRegistries:
releasedRegistries.length > 0 ? releasedRegistries : undefined,
});
};
async function handleRecommend(mode: ICliMode): Promise<void> {
const recommendationBuilder = async () => {
const aidoc = new plugins.tsdoc.AiDoc();
await aidoc.start();
try {
return await aidoc.buildNextCommitObject(paths.cwd);
} finally {
await aidoc.stop();
} }
}; };
const recommendation = mode.json
? await runWithSuppressedOutput(recommendationBuilder)
: await recommendationBuilder();
if (mode.json) {
printJson(recommendation);
return;
}
ui.printRecommendation({
recommendedNextVersion: recommendation.recommendedNextVersion,
recommendedNextVersionLevel: recommendation.recommendedNextVersionLevel,
recommendedNextVersionScope: recommendation.recommendedNextVersionScope,
recommendedNextVersionMessage: recommendation.recommendedNextVersionMessage,
});
console.log(
`Suggested commit: ${recommendation.recommendedNextVersionLevel}(${recommendation.recommendedNextVersionScope}): ${recommendation.recommendedNextVersionMessage}`,
);
}
const createCommitStringFromAnswerBucket = ( const createCommitStringFromAnswerBucket = (
answerBucket: plugins.smartinteract.AnswerBucket, answerBucket: plugins.smartinteract.AnswerBucket,
) => { ) => {
const commitType = answerBucket.getAnswerFor('commitType'); const commitType = answerBucket.getAnswerFor("commitType");
const commitScope = answerBucket.getAnswerFor('commitScope'); const commitScope = answerBucket.getAnswerFor("commitScope");
const commitDescription = answerBucket.getAnswerFor('commitDescription'); 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:
"Creates semantic commits or emits a read-only recommendation.",
commands: [
{
name: "recommend",
description:
"Generate a commit recommendation without mutating the repository",
},
],
flags: [
{ flag: "-y, --yes", description: "Auto-accept AI recommendations" },
{ flag: "-p, --push", description: "Push to origin after commit" },
{ flag: "-t, --test", description: "Run tests before the commit flow" },
{
flag: "-b, --build",
description: "Run the build after the commit flow",
},
{
flag: "-r, --release",
description: "Publish to configured registries after push",
},
{
flag: "--format",
description: "Run gitzone format before committing",
},
{
flag: "--json",
description: "Emit JSON for `commit recommend` only",
},
],
examples: [
"gitzone commit recommend --json",
"gitzone commit -y",
"gitzone commit -ypbr",
],
});
return;
}
console.log("");
console.log("Usage: gitzone commit [recommend] [options]");
console.log("");
console.log("Commands:");
console.log(
" recommend Generate a commit recommendation without mutating the repository",
);
console.log("");
console.log("Flags:");
console.log(" -y, --yes Auto-accept AI recommendations");
console.log(" -p, --push Push to origin after commit");
console.log(" -t, --test Run tests before the commit flow");
console.log(" -b, --build Run the build after the commit flow");
console.log(
" -r, --release Publish to configured registries after push",
);
console.log(" --format Run gitzone format before committing");
console.log(" --json Emit JSON for `commit recommend` only");
console.log("");
console.log("Examples:");
console.log(" gitzone commit recommend --json");
console.log(" gitzone commit -y");
console.log(" gitzone commit -ypbr");
console.log("");
}
+218
View File
@@ -0,0 +1,218 @@
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
*/
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
*/
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
*/
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');
}
/**
* 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: 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');
}
// 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}`);
}
}
+269
View File
@@ -0,0 +1,269 @@
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}`,
`New Version: 🏷️ v${summary.newVersion}`,
];
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`);
}
+104
View File
@@ -0,0 +1,104 @@
import * as plugins from './mod.plugins.js';
export interface ICommitConfig {
alwaysTest: boolean;
alwaysBuild: boolean;
}
/**
* 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 };
}
/**
* 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', {});
this.config = {
alwaysTest: gitzoneConfig?.commit?.alwaysTest ?? false,
alwaysBuild: gitzoneConfig?.commit?.alwaysBuild ?? false,
};
}
/**
* 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;
// 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;
}
}
+166
View File
@@ -0,0 +1,166 @@
import * as plugins from './mod.plugins.js';
export type TAccessLevel = 'public' | 'private';
export interface IReleaseConfig {
registries: string[];
accessLevel: TAccessLevel;
}
/**
* Manages release configuration stored in .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', {});
// Also check szci for backward compatibility
const szciConfig = smartconfigInstance.dataFor<any>('@ship.zone/szci', {});
this.config = {
registries: gitzoneConfig?.release?.registries || [],
accessLevel: gitzoneConfig?.release?.accessLevel || szciConfig?.npmAccessLevel || '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 = {};
}
// Update registries and accessLevel
smartconfigData['@git.zone/cli'].release.registries = this.config.registries;
smartconfigData['@git.zone/cli'].release.accessLevel = this.config.accessLevel;
// Write back to file
await plugins.smartfs
.file(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;
}
}
+739
View File
@@ -0,0 +1,739 @@
// gitzone config - manage release registry configuration
import * as plugins from "./mod.plugins.js";
import { ReleaseConfig } from "./classes.releaseconfig.js";
import { CommitConfig } from "./classes.commitconfig.js";
import { runFormatter, type ICheckResult } from "../mod_format/index.js";
import type { ICliMode } from "../helpers.climode.js";
import { getCliMode, printJson } from "../helpers.climode.js";
import {
getCliConfigValueFromData,
readSmartconfigFile,
setCliConfigValueInData,
unsetCliConfigValueInData,
writeSmartconfigFile,
} from "../helpers.smartconfig.js";
export { ReleaseConfig, CommitConfig };
const defaultCliMode: ICliMode = {
output: "human",
interactive: true,
json: false,
plain: false,
quiet: false,
yes: false,
help: false,
agent: false,
checkUpdates: true,
isTty: true,
};
/**
* Format .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 "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 a registry", value: "add" },
{ name: "Remove a registry", value: "remove" },
{ name: "Clear all registries", value: "clear" },
{ name: "Set access level (public/private)", value: "access" },
{ name: "Configure commit options", value: "commit" },
{ name: "Configure services", value: "services" },
{ name: "Show help", value: "help" },
],
});
const action = (response as any).value;
switch (action) {
case "show":
await handleShow(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 "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 Configuration │",
);
console.log(
"╰─────────────────────────────────────────────────────────────╯",
);
console.log("");
// Show access level
plugins.logger.log("info", `Access Level: ${accessLevel}`);
console.log("");
if (registries.length === 0) {
plugins.logger.log("info", "No release registries configured.");
console.log("");
console.log(" Run `gitzone config add <registry-url>` to add one.");
console.log("");
} else {
plugins.logger.log("info", `Configured registries (${registries.length}):`);
console.log("");
registries.forEach((url, index) => {
console.log(` ${index + 1}. ${url}`);
});
console.log("");
}
}
/**
* Add a registry URL
*/
async function handleAdd(
url: string | undefined,
mode: ICliMode,
): Promise<void> {
if (!url) {
if (!mode.interactive) {
throw new Error("Registry URL is required in non-interactive mode");
}
// Interactive mode
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: "input",
name: "registryUrl",
message: "Enter registry URL:",
default: "https://registry.npmjs.org",
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 registry: ${url}`);
await formatSmartconfigWithDiff(mode);
} else {
plugins.logger.log("warn", `Registry already exists: ${url}`);
}
}
/**
* Remove a 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 registries configured to remove.");
return;
}
if (!url) {
if (!mode.interactive) {
throw new Error("Registry URL is required in non-interactive mode");
}
// Interactive mode - show list to select from
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: "list",
name: "registryUrl",
message: "Select registry to remove:",
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 registry: ${url}`);
await formatSmartconfigWithDiff(mode);
} else {
plugins.logger.log("warn", `Registry not found: ${url}`);
}
}
/**
* Clear all registries
*/
async function handleClear(mode: ICliMode): Promise<void> {
const config = await ReleaseConfig.fromCwd();
if (!config.hasRegistries()) {
plugins.logger.log("info", "No registries to clear.");
return;
}
// Confirm before clearing
const confirmed = mode.interactive
? await plugins.smartinteract.SmartInteract.getCliConfirmation(
"Clear all configured 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 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"));
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);
}
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);
}
/**
* 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}`);
}
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 a release registry" },
{ name: "remove [url]", description: "Remove a release registry" },
{ name: "clear", description: "Clear all release registries" },
{
name: "access [public|private]",
description: "Set npm publish access level",
},
{
name: "commit <setting> <value>",
description: "Set commit defaults",
},
],
examples: [
"gitzone config show --json",
"gitzone config get release.accessLevel",
"gitzone config set cli.interactive false",
"gitzone config set cli.output json",
],
});
return;
}
console.log("");
console.log("Usage: gitzone config <command> [options]");
console.log("");
console.log("Commands:");
console.log(
" show Display current @git.zone/cli configuration",
);
console.log(" get <path> Read a single config value");
console.log(" set <path> <value> Write a config value");
console.log(" unset <path> Delete a config value");
console.log(" add [url] Add a registry URL");
console.log(" remove [url] Remove a registry URL");
console.log(" clear Clear all registries");
console.log(
" access [public|private] Set npm access level for publishing",
);
console.log(" commit [setting] [value] Configure commit options");
console.log(
" services Configure which services are enabled",
);
console.log("");
console.log("Examples:");
console.log(" gitzone config show");
console.log(" gitzone config show --json");
console.log(" gitzone config get release.accessLevel");
console.log(" gitzone config set cli.interactive false");
console.log(" gitzone config set cli.output json");
console.log(" gitzone config unset cli.output");
console.log(" gitzone config add https://registry.npmjs.org");
console.log(" gitzone config add https://verdaccio.example.com");
console.log(" gitzone config remove https://registry.npmjs.org");
console.log(" gitzone config clear");
console.log(" gitzone config access public");
console.log(" gitzone config access private");
console.log(" gitzone config commit # Interactive");
console.log(" gitzone config commit alwaysTest true");
console.log(" gitzone config services # Interactive");
console.log("");
}
+3
View File
@@ -0,0 +1,3 @@
// mod_config plugins
export * from '../plugins.js';
export { logger } from '../gitzone.logging.js';
+105 -10
View File
@@ -1,12 +1,13 @@
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.js';
import { FormatContext } from './classes.formatcontext.js'; import { FormatContext } from './classes.formatcontext.js';
import type { IPlannedChange } from './interfaces.format.js'; import type { IPlannedChange, ICheckResult } from './interfaces.format.js';
import { Project } from '../classes.project.js'; import { Project } from '../classes.project.js';
import { FormatStats } from './classes.formatstats.js';
export abstract class BaseFormatter { export abstract class BaseFormatter {
protected context: FormatContext; protected context: FormatContext;
protected project: Project; protected project: Project;
protected stats: any; // Will be FormatStats from context protected stats: FormatStats;
constructor(context: FormatContext, project: Project) { constructor(context: FormatContext, project: Project) {
this.context = context; this.context = context;
@@ -36,9 +37,6 @@ export abstract class BaseFormatter {
} }
await this.postExecute(); await this.postExecute();
} catch (error) {
// Don't rollback here - let the FormatPlanner handle it
throw error;
} finally { } finally {
this.stats.endModule(this.name, startTime); this.stats.endModule(this.name, startTime);
} }
@@ -53,18 +51,115 @@ export abstract class BaseFormatter {
} }
protected async modifyFile(filepath: string, content: string): Promise<void> { protected async modifyFile(filepath: string, content: string): Promise<void> {
await plugins.smartfile.memory.toFs(content, filepath); 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> { protected async createFile(filepath: string, content: string): Promise<void> {
await plugins.smartfile.memory.toFs(content, filepath); 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> { protected async deleteFile(filepath: string): Promise<void> {
await plugins.smartfile.fs.remove(filepath); await plugins.smartfs.file(filepath).delete();
} }
protected async shouldProcessFile(filepath: string): Promise<boolean> { /**
return true; * 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);
}
} }
} }
-223
View File
@@ -1,223 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
export interface IFileCache {
path: string;
checksum: string;
modified: number;
size: number;
}
export interface ICacheManifest {
version: string;
lastFormat: number;
files: IFileCache[];
}
export class ChangeCache {
private cacheDir: string;
private manifestPath: string;
private cacheVersion = '1.0.0';
constructor() {
this.cacheDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-cache');
this.manifestPath = plugins.path.join(this.cacheDir, 'manifest.json');
}
async initialize(): Promise<void> {
await plugins.smartfile.fs.ensureDir(this.cacheDir);
}
async getManifest(): Promise<ICacheManifest> {
const defaultManifest: ICacheManifest = {
version: this.cacheVersion,
lastFormat: 0,
files: [],
};
const exists = await plugins.smartfile.fs.fileExists(this.manifestPath);
if (!exists) {
return defaultManifest;
}
try {
const content = plugins.smartfile.fs.toStringSync(this.manifestPath);
const manifest = JSON.parse(content);
// Validate the manifest structure
if (this.isValidManifest(manifest)) {
return manifest;
} else {
console.warn('Invalid manifest structure, returning default manifest');
return defaultManifest;
}
} catch (error) {
console.warn(
`Failed to read cache manifest: ${error.message}, returning default manifest`,
);
// Try to delete the corrupted file
try {
await plugins.smartfile.fs.remove(this.manifestPath);
} catch (removeError) {
// Ignore removal errors
}
return defaultManifest;
}
}
async saveManifest(manifest: ICacheManifest): Promise<void> {
// Validate before saving
if (!this.isValidManifest(manifest)) {
throw new Error('Invalid manifest structure, cannot save');
}
// Ensure directory exists
await plugins.smartfile.fs.ensureDir(this.cacheDir);
// Write directly with proper JSON stringification
const jsonContent = JSON.stringify(manifest, null, 2);
await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath);
}
async hasFileChanged(filePath: string): Promise<boolean> {
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
: plugins.path.join(paths.cwd, filePath);
// Check if file exists
const exists = await plugins.smartfile.fs.fileExists(absolutePath);
if (!exists) {
return true; // File doesn't exist, so it's "changed" (will be created)
}
// Get current file stats
const stats = await plugins.smartfile.fs.stat(absolutePath);
// Skip directories
if (stats.isDirectory()) {
return false; // Directories are not processed
}
const content = plugins.smartfile.fs.toStringSync(absolutePath);
const currentChecksum = this.calculateChecksum(content);
// Get cached info
const manifest = await this.getManifest();
const cachedFile = manifest.files.find((f) => f.path === filePath);
if (!cachedFile) {
return true; // Not in cache, so it's changed
}
// Compare checksums
return (
cachedFile.checksum !== currentChecksum ||
cachedFile.size !== stats.size ||
cachedFile.modified !== stats.mtimeMs
);
}
async updateFileCache(filePath: string): Promise<void> {
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
: plugins.path.join(paths.cwd, filePath);
// Get current file stats
const stats = await plugins.smartfile.fs.stat(absolutePath);
// Skip directories
if (stats.isDirectory()) {
return; // Don't cache directories
}
const content = plugins.smartfile.fs.toStringSync(absolutePath);
const checksum = this.calculateChecksum(content);
// Update manifest
const manifest = await this.getManifest();
const existingIndex = manifest.files.findIndex((f) => f.path === filePath);
const cacheEntry: IFileCache = {
path: filePath,
checksum,
modified: stats.mtimeMs,
size: stats.size,
};
if (existingIndex !== -1) {
manifest.files[existingIndex] = cacheEntry;
} else {
manifest.files.push(cacheEntry);
}
manifest.lastFormat = Date.now();
await this.saveManifest(manifest);
}
async getChangedFiles(filePaths: string[]): Promise<string[]> {
const changedFiles: string[] = [];
for (const filePath of filePaths) {
if (await this.hasFileChanged(filePath)) {
changedFiles.push(filePath);
}
}
return changedFiles;
}
async clean(): Promise<void> {
const manifest = await this.getManifest();
const validFiles: IFileCache[] = [];
// Remove entries for files that no longer exist
for (const file of manifest.files) {
const absolutePath = plugins.path.isAbsolute(file.path)
? file.path
: plugins.path.join(paths.cwd, file.path);
if (await plugins.smartfile.fs.fileExists(absolutePath)) {
validFiles.push(file);
}
}
manifest.files = validFiles;
await this.saveManifest(manifest);
}
private calculateChecksum(content: string | Buffer): string {
return plugins.crypto.createHash('sha256').update(content).digest('hex');
}
private isValidManifest(manifest: any): manifest is ICacheManifest {
// Check if manifest has the required structure
if (!manifest || typeof manifest !== 'object') {
return false;
}
// Check required fields
if (
typeof manifest.version !== 'string' ||
typeof manifest.lastFormat !== 'number' ||
!Array.isArray(manifest.files)
) {
return false;
}
// Check each file entry
for (const file of manifest.files) {
if (
!file ||
typeof file !== 'object' ||
typeof file.path !== 'string' ||
typeof file.checksum !== 'string' ||
typeof file.modified !== 'number' ||
typeof file.size !== 'number'
) {
return false;
}
}
return true;
}
}
@@ -1,117 +0,0 @@
import * as plugins from './mod.plugins.js';
import { BaseFormatter } from './classes.baseformatter.js';
export interface IModuleDependency {
module: string;
dependencies: Set<string>;
dependents: Set<string>;
}
export class DependencyAnalyzer {
private moduleDependencies: Map<string, IModuleDependency> = new Map();
constructor() {
this.initializeDependencies();
}
private initializeDependencies(): void {
// Define dependencies between format modules
const dependencies = {
cleanup: [], // No dependencies
npmextra: [], // No dependencies
license: ['npmextra'], // Depends on npmextra for config
packagejson: ['npmextra'], // Depends on npmextra for config
templates: ['npmextra', 'packagejson'], // Depends on both
gitignore: ['templates'], // Depends on templates
tsconfig: ['packagejson'], // Depends on package.json
prettier: [
'cleanup',
'npmextra',
'packagejson',
'templates',
'gitignore',
'tsconfig',
], // Runs after most others
readme: ['npmextra', 'packagejson'], // Depends on project metadata
copy: ['npmextra'], // Depends on config
};
// Initialize all modules
for (const [module, deps] of Object.entries(dependencies)) {
this.moduleDependencies.set(module, {
module,
dependencies: new Set(deps),
dependents: new Set(),
});
}
// Build reverse dependencies (dependents)
for (const [module, deps] of Object.entries(dependencies)) {
for (const dep of deps) {
const depModule = this.moduleDependencies.get(dep);
if (depModule) {
depModule.dependents.add(module);
}
}
}
}
getExecutionGroups(modules: BaseFormatter[]): BaseFormatter[][] {
const modulesMap = new Map(modules.map((m) => [m.name, m]));
const executed = new Set<string>();
const groups: BaseFormatter[][] = [];
while (executed.size < modules.length) {
const currentGroup: BaseFormatter[] = [];
for (const module of modules) {
if (executed.has(module.name)) continue;
const dependency = this.moduleDependencies.get(module.name);
if (!dependency) {
// Unknown module, execute in isolation
currentGroup.push(module);
continue;
}
// Check if all dependencies have been executed
const allDepsExecuted = Array.from(dependency.dependencies).every(
(dep) => executed.has(dep) || !modulesMap.has(dep),
);
if (allDepsExecuted) {
currentGroup.push(module);
}
}
if (currentGroup.length === 0) {
// Circular dependency or error - execute remaining modules
for (const module of modules) {
if (!executed.has(module.name)) {
currentGroup.push(module);
}
}
}
currentGroup.forEach((m) => executed.add(m.name));
groups.push(currentGroup);
}
return groups;
}
canRunInParallel(module1: string, module2: string): boolean {
const dep1 = this.moduleDependencies.get(module1);
const dep2 = this.moduleDependencies.get(module2);
if (!dep1 || !dep2) return false;
// Check if module1 depends on module2 or vice versa
return (
!dep1.dependencies.has(module2) &&
!dep2.dependencies.has(module1) &&
!dep1.dependents.has(module2) &&
!dep2.dependents.has(module1)
);
}
}
+9 -8
View File
@@ -21,14 +21,15 @@ export class DiffReporter {
} }
try { try {
const exists = await plugins.smartfile.fs.fileExists(change.path); const exists = await plugins.smartfs.file(change.path).exists();
if (!exists) { if (!exists) {
return null; return null;
} }
const currentContent = await plugins.smartfile.fs.toStringSync( const currentContent = (await plugins.smartfs
change.path, .file(change.path)
); .encoding('utf8')
.read()) as string;
// For planned changes, we need the new content // For planned changes, we need the new content
if (!change.content) { if (!change.content) {
@@ -107,10 +108,10 @@ export class DiffReporter {
})), })),
}; };
await plugins.smartfile.memory.toFs( await plugins.smartfs
JSON.stringify(report, null, 2), .file(outputPath)
outputPath, .encoding('utf8')
); .write(JSON.stringify(report, null, 2));
logger.log('info', `Diff report saved to ${outputPath}`); logger.log('info', `Diff report saved to ${outputPath}`);
} }
+20 -3
View File
@@ -1,14 +1,31 @@
import * as plugins from './mod.plugins.js'; import * as plugins from "./mod.plugins.js";
import { FormatStats } from './classes.formatstats.js'; import { FormatStats } from "./classes.formatstats.js";
interface IFormatContextOptions {
interactive?: boolean;
jsonOutput?: boolean;
}
export class FormatContext { export class FormatContext {
private formatStats: FormatStats; private formatStats: FormatStats;
private interactive: boolean;
private jsonOutput: boolean;
constructor() { constructor(options: IFormatContextOptions = {}) {
this.formatStats = new FormatStats(); this.formatStats = new FormatStats();
this.interactive = options.interactive ?? true;
this.jsonOutput = options.jsonOutput ?? false;
} }
getFormatStats(): FormatStats { getFormatStats(): FormatStats {
return this.formatStats; return this.formatStats;
} }
isInteractive(): boolean {
return this.interactive;
}
isJsonOutput(): boolean {
return this.jsonOutput;
}
} }
+3 -32
View File
@@ -2,13 +2,12 @@ import * as plugins from './mod.plugins.js';
import { FormatContext } from './classes.formatcontext.js'; import { FormatContext } from './classes.formatcontext.js';
import { BaseFormatter } from './classes.baseformatter.js'; import { BaseFormatter } from './classes.baseformatter.js';
import type { IFormatPlan, IPlannedChange } from './interfaces.format.js'; import type { IFormatPlan, IPlannedChange } from './interfaces.format.js';
import { getModuleIcon } from './interfaces.format.js';
import { logger } from '../gitzone.logging.js'; import { logger } from '../gitzone.logging.js';
import { DependencyAnalyzer } from './classes.dependency-analyzer.js';
import { DiffReporter } from './classes.diffreporter.js'; import { DiffReporter } from './classes.diffreporter.js';
export class FormatPlanner { export class FormatPlanner {
private plannedChanges: Map<string, IPlannedChange[]> = new Map(); private plannedChanges: Map<string, IPlannedChange[]> = new Map();
private dependencyAnalyzer = new DependencyAnalyzer();
private diffReporter = new DiffReporter(); private diffReporter = new DiffReporter();
async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> { async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> {
@@ -18,7 +17,6 @@ export class FormatPlanner {
filesAdded: 0, filesAdded: 0,
filesModified: 0, filesModified: 0,
filesRemoved: 0, filesRemoved: 0,
estimatedTime: 0,
}, },
changes: [], changes: [],
warnings: [], warnings: [],
@@ -32,7 +30,6 @@ export class FormatPlanner {
for (const change of changes) { for (const change of changes) {
plan.changes.push(change); plan.changes.push(change);
// Update summary
switch (change.type) { switch (change.type) {
case 'create': case 'create':
plan.summary.filesAdded++; plan.summary.filesAdded++;
@@ -58,7 +55,6 @@ export class FormatPlanner {
plan.summary.filesAdded + plan.summary.filesAdded +
plan.summary.filesModified + plan.summary.filesModified +
plan.summary.filesRemoved; plan.summary.filesRemoved;
plan.summary.estimatedTime = plan.summary.totalFiles * 100; // 100ms per file estimate
return plan; return plan;
} }
@@ -67,12 +63,9 @@ export class FormatPlanner {
plan: IFormatPlan, plan: IFormatPlan,
modules: BaseFormatter[], modules: BaseFormatter[],
context: FormatContext, context: FormatContext,
parallel: boolean = false,
): Promise<void> { ): Promise<void> {
const startTime = Date.now(); const startTime = Date.now();
try {
// Always use sequential execution to avoid race conditions
for (const module of modules) { for (const module of modules) {
const changes = this.plannedChanges.get(module.name) || []; const changes = this.plannedChanges.get(module.name) || [];
@@ -82,12 +75,8 @@ export class FormatPlanner {
} }
} }
const endTime = Date.now(); const duration = Date.now() - startTime;
const duration = endTime - startTime;
logger.log('info', `Format operations completed in ${duration}ms`); logger.log('info', `Format operations completed in ${duration}ms`);
} catch (error) {
throw error;
}
} }
async displayPlan( async displayPlan(
@@ -103,7 +92,6 @@ export class FormatPlanner {
console.log(''); console.log('');
console.log('Changes by module:'); console.log('Changes by module:');
// Group changes by module
const changesByModule = new Map<string, IPlannedChange[]>(); const changesByModule = new Map<string, IPlannedChange[]>();
for (const change of plan.changes) { for (const change of plan.changes) {
const moduleChanges = changesByModule.get(change.module) || []; const moduleChanges = changesByModule.get(change.module) || [];
@@ -113,14 +101,13 @@ export class FormatPlanner {
for (const [module, changes] of changesByModule) { for (const [module, changes] of changesByModule) {
console.log( console.log(
`\n${this.getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`, `\n${getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`,
); );
for (const change of changes) { for (const change of changes) {
const icon = this.getChangeIcon(change.type); const icon = this.getChangeIcon(change.type);
console.log(` ${icon} ${change.path} - ${change.description}`); console.log(` ${icon} ${change.path} - ${change.description}`);
// Show diff for modified files if detailed view is requested
if (detailed && change.type === 'modify') { if (detailed && change.type === 'modify') {
const diff = await this.diffReporter.generateDiffForChange(change); const diff = await this.diffReporter.generateDiffForChange(change);
if (diff) { if (diff) {
@@ -141,22 +128,6 @@ export class FormatPlanner {
console.log('\n' + '━'.repeat(50)); console.log('\n' + '━'.repeat(50));
} }
private getModuleIcon(module: string): string {
const icons: Record<string, string> = {
packagejson: '📦',
license: '📝',
tsconfig: '🔧',
cleanup: '🚮',
gitignore: '🔒',
prettier: '✨',
readme: '📖',
templates: '📄',
npmextra: '⚙️',
copy: '📋',
};
return icons[module] || '📁';
}
private getChangeIcon(type: 'create' | 'modify' | 'delete'): string { private getChangeIcon(type: 'create' | 'modify' | 'delete'): string {
switch (type) { switch (type) {
case 'create': case 'create':
+6 -46
View File
@@ -1,5 +1,6 @@
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.js';
import { logger } from '../gitzone.logging.js'; import { logger } from '../gitzone.logging.js';
import { getModuleIcon } from './interfaces.format.js';
export interface IModuleStats { export interface IModuleStats {
name: string; name: string;
@@ -23,8 +24,6 @@ export interface IFormatStats {
totalModified: number; totalModified: number;
totalDeleted: number; totalDeleted: number;
totalErrors: number; totalErrors: number;
cacheHits: number;
cacheMisses: number;
}; };
} }
@@ -43,8 +42,6 @@ export class FormatStats {
totalModified: 0, totalModified: 0,
totalDeleted: 0, totalDeleted: 0,
totalErrors: 0, totalErrors: 0,
cacheHits: 0,
cacheMisses: 0,
}, },
}; };
} }
@@ -107,14 +104,6 @@ export class FormatStats {
} }
} }
recordCacheHit(): void {
this.stats.overallStats.cacheHits++;
}
recordCacheMiss(): void {
this.stats.overallStats.cacheMisses++;
}
finish(): void { finish(): void {
this.stats.endTime = Date.now(); this.stats.endTime = Date.now();
this.stats.totalExecutionTime = this.stats.endTime - this.stats.startTime; this.stats.totalExecutionTime = this.stats.endTime - this.stats.startTime;
@@ -135,20 +124,6 @@ export class FormatStats {
console.log(` • Deleted: ${this.stats.overallStats.totalDeleted}`); console.log(` • Deleted: ${this.stats.overallStats.totalDeleted}`);
console.log(` Errors: ${this.stats.overallStats.totalErrors}`); console.log(` Errors: ${this.stats.overallStats.totalErrors}`);
if (
this.stats.overallStats.cacheHits > 0 ||
this.stats.overallStats.cacheMisses > 0
) {
const cacheHitRate =
(this.stats.overallStats.cacheHits /
(this.stats.overallStats.cacheHits +
this.stats.overallStats.cacheMisses)) *
100;
console.log(` Cache Hit Rate: ${cacheHitRate.toFixed(1)}%`);
console.log(` • Hits: ${this.stats.overallStats.cacheHits}`);
console.log(` • Misses: ${this.stats.overallStats.cacheMisses}`);
}
// Module stats // Module stats
console.log('\nModule Breakdown:'); console.log('\nModule Breakdown:');
console.log('─'.repeat(50)); console.log('─'.repeat(50));
@@ -159,7 +134,7 @@ export class FormatStats {
for (const moduleStats of sortedModules) { for (const moduleStats of sortedModules) {
console.log( console.log(
`\n${this.getModuleIcon(moduleStats.name)} ${moduleStats.name}:`, `\n${getModuleIcon(moduleStats.name)} ${moduleStats.name}:`,
); );
console.log( console.log(
` Execution Time: ${this.formatDuration(moduleStats.executionTime)}`, ` Execution Time: ${this.formatDuration(moduleStats.executionTime)}`,
@@ -192,10 +167,10 @@ export class FormatStats {
moduleStats: Array.from(this.stats.moduleStats.values()), moduleStats: Array.from(this.stats.moduleStats.values()),
}; };
await plugins.smartfile.memory.toFs( await plugins.smartfs
JSON.stringify(report, null, 2), .file(outputPath)
outputPath, .encoding('utf8')
); .write(JSON.stringify(report, null, 2));
logger.log('info', `Statistics report saved to ${outputPath}`); logger.log('info', `Statistics report saved to ${outputPath}`);
} }
@@ -211,19 +186,4 @@ export class FormatStats {
} }
} }
private getModuleIcon(module: string): string {
const icons: Record<string, string> = {
packagejson: '📦',
license: '📝',
tsconfig: '🔧',
cleanup: '🚮',
gitignore: '🔒',
prettier: '✨',
readme: '📖',
templates: '📄',
npmextra: '⚙️',
copy: '📋',
};
return icons[module] || '📁';
}
} }
-318
View File
@@ -1,318 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import type { IFormatOperation } from './interfaces.format.js';
export class RollbackManager {
private backupDir: string;
private manifestPath: string;
constructor() {
this.backupDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-backups');
this.manifestPath = plugins.path.join(this.backupDir, 'manifest.json');
}
async createOperation(): Promise<IFormatOperation> {
await this.ensureBackupDir();
const operation: IFormatOperation = {
id: this.generateOperationId(),
timestamp: Date.now(),
files: [],
status: 'pending',
};
await this.updateManifest(operation);
return operation;
}
async backupFile(filepath: string, operationId: string): Promise<void> {
const operation = await this.getOperation(operationId);
if (!operation) {
throw new Error(`Operation ${operationId} not found`);
}
const absolutePath = plugins.path.isAbsolute(filepath)
? filepath
: plugins.path.join(paths.cwd, filepath);
// Check if file exists
const exists = await plugins.smartfile.fs.fileExists(absolutePath);
if (!exists) {
// File doesn't exist yet (will be created), so we skip backup
return;
}
// Read file content and metadata
const content = plugins.smartfile.fs.toStringSync(absolutePath);
const stats = await plugins.smartfile.fs.stat(absolutePath);
const checksum = this.calculateChecksum(content);
// Create backup
const backupPath = this.getBackupPath(operationId, filepath);
await plugins.smartfile.fs.ensureDir(plugins.path.dirname(backupPath));
await plugins.smartfile.memory.toFs(content, backupPath);
// Update operation
operation.files.push({
path: filepath,
originalContent: content,
checksum,
permissions: stats.mode.toString(8),
});
await this.updateManifest(operation);
}
async rollback(operationId: string): Promise<void> {
const operation = await this.getOperation(operationId);
if (!operation) {
// Operation doesn't exist, might have already been rolled back or never created
console.warn(`Operation ${operationId} not found for rollback, skipping`);
return;
}
if (operation.status === 'rolled-back') {
throw new Error(`Operation ${operationId} has already been rolled back`);
}
// Restore files in reverse order
for (let i = operation.files.length - 1; i >= 0; i--) {
const file = operation.files[i];
const absolutePath = plugins.path.isAbsolute(file.path)
? file.path
: plugins.path.join(paths.cwd, file.path);
// Verify backup integrity
const backupPath = this.getBackupPath(operationId, file.path);
const backupContent = plugins.smartfile.fs.toStringSync(backupPath);
const backupChecksum = this.calculateChecksum(backupContent);
if (backupChecksum !== file.checksum) {
throw new Error(`Backup integrity check failed for ${file.path}`);
}
// Restore file
await plugins.smartfile.memory.toFs(file.originalContent, absolutePath);
// Restore permissions
const mode = parseInt(file.permissions, 8);
// Note: Permissions restoration may not work on all platforms
}
// Update operation status
operation.status = 'rolled-back';
await this.updateManifest(operation);
}
async markComplete(operationId: string): Promise<void> {
const operation = await this.getOperation(operationId);
if (!operation) {
throw new Error(`Operation ${operationId} not found`);
}
operation.status = 'completed';
await this.updateManifest(operation);
}
async cleanOldBackups(retentionDays: number): Promise<void> {
const manifest = await this.getManifest();
const cutoffTime = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
const operationsToDelete = manifest.operations.filter(
(op) => op.timestamp < cutoffTime && op.status === 'completed',
);
for (const operation of operationsToDelete) {
// Remove backup files
const operationDir = plugins.path.join(
this.backupDir,
'operations',
operation.id,
);
await plugins.smartfile.fs.remove(operationDir);
// Remove from manifest
manifest.operations = manifest.operations.filter(
(op) => op.id !== operation.id,
);
}
await this.saveManifest(manifest);
}
async verifyBackup(operationId: string): Promise<boolean> {
const operation = await this.getOperation(operationId);
if (!operation) {
return false;
}
for (const file of operation.files) {
const backupPath = this.getBackupPath(operationId, file.path);
const exists = await plugins.smartfile.fs.fileExists(backupPath);
if (!exists) {
return false;
}
const content = plugins.smartfile.fs.toStringSync(backupPath);
const checksum = this.calculateChecksum(content);
if (checksum !== file.checksum) {
return false;
}
}
return true;
}
async listBackups(): Promise<IFormatOperation[]> {
const manifest = await this.getManifest();
return manifest.operations;
}
private async ensureBackupDir(): Promise<void> {
await plugins.smartfile.fs.ensureDir(this.backupDir);
await plugins.smartfile.fs.ensureDir(
plugins.path.join(this.backupDir, 'operations'),
);
}
private generateOperationId(): string {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const random = Math.random().toString(36).substring(2, 8);
return `${timestamp}-${random}`;
}
private getBackupPath(operationId: string, filepath: string): string {
const filename = plugins.path.basename(filepath);
const dir = plugins.path.dirname(filepath);
const safeDir = dir.replace(/[/\\]/g, '__');
return plugins.path.join(
this.backupDir,
'operations',
operationId,
'files',
safeDir,
`${filename}.backup`,
);
}
private calculateChecksum(content: string | Buffer): string {
return plugins.crypto.createHash('sha256').update(content).digest('hex');
}
private async getManifest(): Promise<{ operations: IFormatOperation[] }> {
const defaultManifest = { operations: [] };
const exists = await plugins.smartfile.fs.fileExists(this.manifestPath);
if (!exists) {
return defaultManifest;
}
try {
const content = plugins.smartfile.fs.toStringSync(this.manifestPath);
const manifest = JSON.parse(content);
// Validate the manifest structure
if (this.isValidManifest(manifest)) {
return manifest;
} else {
console.warn(
'Invalid rollback manifest structure, returning default manifest',
);
return defaultManifest;
}
} catch (error) {
console.warn(
`Failed to read rollback manifest: ${error.message}, returning default manifest`,
);
// Try to delete the corrupted file
try {
await plugins.smartfile.fs.remove(this.manifestPath);
} catch (removeError) {
// Ignore removal errors
}
return defaultManifest;
}
}
private async saveManifest(manifest: {
operations: IFormatOperation[];
}): Promise<void> {
// Validate before saving
if (!this.isValidManifest(manifest)) {
throw new Error('Invalid rollback manifest structure, cannot save');
}
// Ensure directory exists
await this.ensureBackupDir();
// Write directly with proper JSON stringification
const jsonContent = JSON.stringify(manifest, null, 2);
await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath);
}
private async getOperation(
operationId: string,
): Promise<IFormatOperation | null> {
const manifest = await this.getManifest();
return manifest.operations.find((op) => op.id === operationId) || null;
}
private async updateManifest(operation: IFormatOperation): Promise<void> {
const manifest = await this.getManifest();
const existingIndex = manifest.operations.findIndex(
(op) => op.id === operation.id,
);
if (existingIndex !== -1) {
manifest.operations[existingIndex] = operation;
} else {
manifest.operations.push(operation);
}
await this.saveManifest(manifest);
}
private isValidManifest(
manifest: any,
): manifest is { operations: IFormatOperation[] } {
// Check if manifest has the required structure
if (!manifest || typeof manifest !== 'object') {
return false;
}
// Check required fields
if (!Array.isArray(manifest.operations)) {
return false;
}
// Check each operation entry
for (const operation of manifest.operations) {
if (
!operation ||
typeof operation !== 'object' ||
typeof operation.id !== 'string' ||
typeof operation.timestamp !== 'number' ||
typeof operation.status !== 'string' ||
!Array.isArray(operation.files)
) {
return false;
}
// Check each file in the operation
for (const file of operation.files) {
if (
!file ||
typeof file !== 'object' ||
typeof file.path !== 'string' ||
typeof file.checksum !== 'string'
) {
return false;
}
}
}
return true;
}
}
-26
View File
@@ -1,26 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { logger } from '../gitzone.logging.js';
import { Project } from '../classes.project.js';
const filesToDelete = [
'defaults.yml',
'yarn.lock',
'package-lock.json',
'tslint.json',
];
export const run = async (projectArg: Project) => {
for (const relativeFilePath of filesToDelete) {
const fileExists = 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}`);
}
}
};
-85
View File
@@ -1,85 +0,0 @@
import type { Project } from '../classes.project.js';
import * as plugins from './mod.plugins.js';
import { logger } from '../gitzone.logging.js';
export const run = async (projectArg: Project) => {
const gitzoneConfig = await projectArg.gitzoneConfig;
// Get copy configuration from npmextra.json
const npmextraConfig = new plugins.npmextra.Npmextra();
const copyConfig = npmextraConfig.dataFor<any>('gitzone.format.copy', {
patterns: [],
});
if (!copyConfig.patterns || copyConfig.patterns.length === 0) {
logger.log('info', 'No copy patterns configured in npmextra.json');
return;
}
for (const pattern of copyConfig.patterns) {
if (!pattern.from || !pattern.to) {
logger.log('warn', 'Invalid copy pattern - missing "from" or "to" field');
continue;
}
try {
// Handle glob patterns
const files = await plugins.smartfile.fs.listFileTree('.', pattern.from);
for (const file of files) {
const sourcePath = file;
let destPath = pattern.to;
// If destination is a directory, preserve filename
if (pattern.to.endsWith('/')) {
const filename = plugins.path.basename(file);
destPath = plugins.path.join(pattern.to, filename);
}
// Handle template variables in destination path
if (pattern.preservePath) {
const relativePath = plugins.path.relative(
plugins.path.dirname(pattern.from.replace(/\*/g, '')),
file,
);
destPath = plugins.path.join(pattern.to, relativePath);
}
// Ensure destination directory exists
await plugins.smartfile.fs.ensureDir(plugins.path.dirname(destPath));
// Copy file
await plugins.smartfile.fs.copy(sourcePath, destPath);
logger.log('info', `Copied ${sourcePath} to ${destPath}`);
}
} catch (error) {
logger.log(
'error',
`Failed to copy pattern ${pattern.from}: ${error.message}`,
);
}
}
};
/**
* Example npmextra.json configuration:
* {
* "gitzone": {
* "format": {
* "copy": {
* "patterns": [
* {
* "from": "src/assets/*",
* "to": "dist/assets/",
* "preservePath": true
* },
* {
* "from": "config/*.json",
* "to": "dist/"
* }
* ]
* }
* }
* }
* }
*/
-22
View File
@@ -1,22 +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!');
};
-32
View File
@@ -1,32 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { Project } from '../classes.project.js';
import { logger } from '../gitzone.logging.js';
const incompatibleLicenses: string[] = ['AGPL', 'GPL', 'SSPL'];
export const run = async (projectArg: Project) => {
const nodeModulesInstalled = await plugins.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}`,
);
}
}
};
-77
View File
@@ -1,77 +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);
});
};
-193
View File
@@ -1,193 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import * as gulpFunction from '@push.rocks/gulp-function';
import { Project } from '../classes.project.js';
import { logger } from '../gitzone.logging.js';
/**
* ensures a certain dependency
*/
const ensureDependency = async (
packageJsonObjectArg: any,
position: 'dep' | 'devDep' | 'everywhere',
constraint: 'exclude' | 'include' | 'latest',
dependencyArg: string,
) => {
const [packageName, version] = dependencyArg.includes('@')
? dependencyArg.split('@').filter(Boolean)
: [dependencyArg, 'latest'];
const targetSections: string[] = [];
switch (position) {
case 'dep':
targetSections.push('dependencies');
break;
case 'devDep':
targetSections.push('devDependencies');
break;
case 'everywhere':
targetSections.push('dependencies', 'devDependencies');
break;
}
for (const section of targetSections) {
if (!packageJsonObjectArg[section]) {
packageJsonObjectArg[section] = {};
}
switch (constraint) {
case 'exclude':
delete packageJsonObjectArg[section][packageName];
break;
case 'include':
if (!packageJsonObjectArg[section][packageName]) {
packageJsonObjectArg[section][packageName] =
version === 'latest' ? '^1.0.0' : version;
}
break;
case 'latest':
// Fetch latest version from npm
try {
const registry = new plugins.smartnpm.NpmRegistry();
const packageInfo = await registry.getPackageInfo(packageName);
const latestVersion = packageInfo['dist-tags'].latest;
packageJsonObjectArg[section][packageName] = `^${latestVersion}`;
} catch (error) {
logger.log(
'warn',
`Could not fetch latest version for ${packageName}, using existing or default`,
);
if (!packageJsonObjectArg[section][packageName]) {
packageJsonObjectArg[section][packageName] =
version === 'latest' ? '^1.0.0' : version;
}
}
break;
}
}
};
export const run = async (projectArg: Project) => {
const formatStreamWrapper = new plugins.smartstream.StreamWrapper([
plugins.smartgulp.src([`package.json`]),
gulpFunction.forEach(async (fileArg: plugins.smartfile.SmartFile) => {
const npmextraConfig = new plugins.npmextra.Npmextra(paths.cwd);
const gitzoneData: any = npmextraConfig.dataFor('gitzone', {});
const fileString = fileArg.contents.toString();
const packageJson = JSON.parse(fileString);
// metadata
packageJson.repository = {
type: 'git',
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}.git`,
};
(packageJson.bugs = {
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}/issues`,
}),
(packageJson.homepage = `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}#readme`);
// Check for module type
if (!packageJson.type) {
logger.log('info', `setting packageJson.type to "module"`);
packageJson.type = 'module';
}
// Check for private or public
if (packageJson.private !== undefined) {
logger.log(
'info',
'Success -> found private/public info in package.json!',
);
} else {
logger.log(
'error',
'found no private boolean! Setting it to private for now!',
);
packageJson.private = true;
}
// Check for license
if (packageJson.license) {
logger.log('info', 'Success -> found license in package.json!');
} else {
logger.log(
'error',
'found no license! Setting it to UNLICENSED for now!',
);
packageJson.license = 'UNLICENSED';
}
// Check for build script
if (packageJson.scripts.build) {
logger.log('info', 'Success -> found build script in package.json!');
} else {
logger.log(
'error',
'found no build script! Putting a placeholder there for now!',
);
packageJson.scripts.build = `echo "Not needed for now"`;
}
// Check for buildDocs script
if (!packageJson.scripts.buildDocs) {
logger.log(
'info',
'found no buildDocs script! Putting tsdoc script there now.',
);
packageJson.scripts.buildDocs = `tsdoc`;
}
// check for files
packageJson.files = [
'ts/**/*',
'ts_web/**/*',
'dist/**/*',
'dist_*/**/*',
'dist_ts/**/*',
'dist_ts_web/**/*',
'assets/**/*',
'cli.js',
'npmextra.json',
'readme.md',
];
// check for dependencies
await ensureDependency(
packageJson,
'devDep',
'latest',
'@push.rocks/tapbundle',
);
await ensureDependency(
packageJson,
'devDep',
'latest',
'@git.zone/tstest',
);
await ensureDependency(
packageJson,
'devDep',
'latest',
'@git.zone/tsbuild',
);
// set overrides
const overrides = plugins.smartfile.fs.toObjectSync(
plugins.path.join(paths.assetsDir, 'overrides.json'),
);
packageJson.pnpm = packageJson.pnpm || {};
packageJson.pnpm.overrides = overrides;
// exclude
// TODO
fileArg.setContentsFromString(JSON.stringify(packageJson, null, 2));
}),
plugins.smartgulp.replace(),
]);
await formatStreamWrapper.run().catch((error) => {
console.log(error);
});
};
-66
View File
@@ -1,66 +0,0 @@
import * as plugins from './mod.plugins.js';
import prettier from 'prettier';
import { Project } from '../classes.project.js';
import { logger } from '../gitzone.logging.js';
const prettierDefaultTypeScriptConfig: prettier.Options = {
printWidth: 100,
parser: 'typescript',
singleQuote: true,
};
const prettierDefaultMarkdownConfig: prettier.Options = {
singleQuote: true,
printWidth: 100,
parser: 'markdown',
};
const filesToFormat = [
`ts/**/*.ts`,
`test/**/*.ts`,
`readme.md`,
`docs/**/*.md`,
];
const choosePrettierConfig = (fileArg: plugins.smartfile.SmartFile) => {
switch (fileArg.parsedPath.ext) {
case '.ts':
return prettierDefaultTypeScriptConfig;
case '.md':
return prettierDefaultMarkdownConfig;
default:
return {};
}
};
const prettierTypeScriptPipestop = plugins.through2.obj(
async (fileArg: plugins.smartfile.SmartFile, enc, cb) => {
const fileString = fileArg.contentBuffer.toString();
const chosenConfig = choosePrettierConfig(fileArg);
const filePasses = await prettier.check(fileString, chosenConfig);
if (filePasses) {
logger.log('info', `OK! -> ${fileArg.path} passes!`);
cb(null);
} else {
logger.log('info', `${fileArg.path} is being reformated!`);
const formatedFileString = await prettier.format(
fileString,
chosenConfig,
);
fileArg.setContentsFromString(formatedFileString);
cb(null, fileArg);
}
},
);
export const run = async (projectArg: Project) => {
const formatStreamWrapper = new plugins.smartstream.StreamWrapper([
plugins.smartgulp.src(filesToFormat),
prettierTypeScriptPipestop,
plugins.smartgulp.replace(),
]);
await formatStreamWrapper.run().catch((error) => {
console.log(error);
});
};
-32
View File
@@ -1,32 +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');
}
};
-85
View File
@@ -1,85 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { logger } from '../gitzone.logging.js';
import { Project } from '../classes.project.js';
/**
* takes care of updating files from templates
*/
export const run = async (project: Project) => {
const templateModule = await import('../mod_template/index.js');
// update tslint
// getting template
const tslintTemplate = await templateModule.getTemplate('tslint');
await tslintTemplate.writeToDisk(paths.cwd);
logger.log('info', 'Updated tslint.json!');
// update vscode
const vscodeTemplate = await templateModule.getTemplate('vscode');
await vscodeTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated vscode template!`);
// update gitlab ci and Dockerfile
switch (project.gitzoneConfig.data.projectType) {
case 'npm':
case 'wcc':
if (project.gitzoneConfig.data.npmciOptions.npmAccessLevel === 'public') {
const ciTemplateDefault =
await templateModule.getTemplate('ci_default');
ciTemplateDefault.writeToDisk(paths.cwd);
} else {
const ciTemplateDefault =
await templateModule.getTemplate('ci_default_private');
ciTemplateDefault.writeToDisk(paths.cwd);
}
logger.log('info', 'Updated .gitlabci.yml!');
break;
case 'service':
case 'website':
const ciTemplateDocker = await templateModule.getTemplate('ci_docker');
await ciTemplateDocker.writeToDisk(paths.cwd);
logger.log('info', 'Updated CI/CD config files!');
// lets care about docker
const dockerTemplate =
await templateModule.getTemplate('dockerfile_service');
dockerTemplate.writeToDisk(paths.cwd);
logger.log('info', 'Updated Dockerfile!');
// lets care about cli
const cliTemplate = await templateModule.getTemplate('cli');
await cliTemplate.writeToDisk(paths.cwd);
logger.log('info', 'Updated cli.ts.js and cli.js!');
break;
default:
break;
}
// update html
if (project.gitzoneConfig.data.projectType === 'website') {
const websiteUpdateTemplate =
await templateModule.getTemplate('website_update');
const variables = {
assetbrokerUrl: project.gitzoneConfig.data.module.assetbrokerUrl,
legalUrl: project.gitzoneConfig.data.module.legalUrl,
};
console.log(
'updating website template with variables\n',
JSON.stringify(variables, null, 2),
);
websiteUpdateTemplate.supplyVariables(variables);
await websiteUpdateTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated html for website!`);
} else if (project.gitzoneConfig.data.projectType === 'service') {
const websiteUpdateTemplate =
await templateModule.getTemplate('service_update');
await websiteUpdateTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated html for element template!`);
} else if (project.gitzoneConfig.data.projectType === 'wcc') {
const wccUpdateTemplate = await templateModule.getTemplate('wcc_update');
await wccUpdateTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated html for wcc template!`);
}
};
-30
View File
@@ -1,30 +0,0 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
import { logger } from '../gitzone.logging.js';
import { Project } from '../classes.project.js';
export const run = async (projectArg: Project) => {
// lets care about tsconfig.json
logger.log('info', 'Formatting tsconfig.json...');
const tsconfigSmartfile = await plugins.smartfile.SmartFile.fromFilePath(
plugins.path.join(paths.cwd, 'tsconfig.json'),
);
const tsconfigObject = JSON.parse(tsconfigSmartfile.contentBuffer.toString());
tsconfigObject.compilerOptions = tsconfigObject.compilerOptions || {};
tsconfigObject.compilerOptions.baseUrl = '.';
tsconfigObject.compilerOptions.paths = {};
const tsPublishMod = await import('@git.zone/tspublish');
const tsPublishInstance = new tsPublishMod.TsPublish();
const publishModules = await tsPublishInstance.getModuleSubDirs(paths.cwd);
for (const publishModule of Object.keys(publishModules)) {
const publishConfig = publishModules[publishModule];
tsconfigObject.compilerOptions.paths[`${publishConfig.name}`] = [
`./${publishModule}/index.js`,
];
}
tsconfigSmartfile.setContentsFromString(
JSON.stringify(tsconfigObject, null, 2),
);
await tsconfigSmartfile.write();
};
@@ -1,7 +1,6 @@
import { BaseFormatter } from '../classes.baseformatter.js'; import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js'; import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js'; import * as plugins from '../mod.plugins.js';
import * as cleanupFormatter from '../format.cleanup.js';
export class CleanupFormatter extends BaseFormatter { export class CleanupFormatter extends BaseFormatter {
get name(): string { get name(): string {
@@ -20,7 +19,7 @@ export class CleanupFormatter extends BaseFormatter {
]; ];
for (const file of filesToRemove) { for (const file of filesToRemove) {
const exists = await plugins.smartfile.fs.fileExists(file); const exists = await plugins.smartfs.file(file).exists();
if (exists) { if (exists) {
changes.push({ changes.push({
type: 'delete', type: 'delete',
+110 -5
View File
@@ -1,8 +1,113 @@
import { LegacyFormatter } from './legacy.formatter.js'; import { BaseFormatter } from '../classes.baseformatter.js';
import * as formatCopy from '../format.copy.js'; import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import { logger, logVerbose } from '../../gitzone.logging.js';
export class CopyFormatter extends LegacyFormatter { interface ICopyPattern {
constructor(context: any, project: any) { from: string;
super(context, project, 'copy', formatCopy); to: string;
preservePath?: boolean;
}
export class CopyFormatter extends BaseFormatter {
get name(): string {
return 'copy';
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
// Get copy configuration from .smartconfig.json
const smartconfigInstance = new plugins.smartconfig.Smartconfig();
const copyConfig = smartconfigInstance.dataFor<{ patterns: ICopyPattern[] }>(
'gitzone.format.copy',
{ patterns: [] },
);
if (!copyConfig.patterns || copyConfig.patterns.length === 0) {
logVerbose('No copy patterns configured in .smartconfig.json');
return changes;
}
for (const pattern of copyConfig.patterns) {
if (!pattern.from || !pattern.to) {
logVerbose('Invalid copy pattern - missing "from" or "to" field');
continue;
}
try {
// Handle glob patterns
const entries = await plugins.smartfs
.directory('.')
.recursive()
.filter(pattern.from)
.list();
const files = entries.map((entry) => entry.path);
for (const file of files) {
const sourcePath = file;
let destPath = pattern.to;
// If destination is a directory, preserve filename
if (pattern.to.endsWith('/')) {
const filename = plugins.path.basename(file);
destPath = plugins.path.join(pattern.to, filename);
}
// Handle template variables in destination path
if (pattern.preservePath) {
const relativePath = plugins.path.relative(
plugins.path.dirname(pattern.from.replace(/\*/g, '')),
file,
);
destPath = plugins.path.join(pattern.to, relativePath);
}
// Read source content
const content = (await plugins.smartfs
.file(sourcePath)
.encoding('utf8')
.read()) as string;
// Check if destination exists and has same content
let needsCopy = true;
const destExists = await plugins.smartfs.file(destPath).exists();
if (destExists) {
const existingContent = (await plugins.smartfs
.file(destPath)
.encoding('utf8')
.read()) as string;
if (existingContent === content) {
needsCopy = false;
}
}
if (needsCopy) {
changes.push({
type: destExists ? 'modify' : 'create',
path: destPath,
module: this.name,
description: `Copy from ${sourcePath}`,
content: content,
});
}
}
} catch (error) {
logVerbose(`Failed to process pattern ${pattern.from}: ${error.message}`);
}
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
if (!change.content) return;
if (change.type === 'create') {
await this.createFile(change.path, change.content);
} else {
await this.modifyFile(change.path, change.content);
}
logger.log('info', `Copied to ${change.path}`);
} }
} }
+104 -5
View File
@@ -1,8 +1,107 @@
import { LegacyFormatter } from './legacy.formatter.js'; import { BaseFormatter } from '../classes.baseformatter.js';
import * as formatGitignore from '../format.gitignore.js'; import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger } from '../../gitzone.logging.js';
export class GitignoreFormatter extends LegacyFormatter { export class GitignoreFormatter extends BaseFormatter {
constructor(context: any, project: any) { get name(): string {
super(context, project, 'gitignore', formatGitignore); return 'gitignore';
}
/**
* Read the standard gitignore template from the asset file,
* stripping the YAML frontmatter.
*/
private async getStandardTemplate(): Promise<string> {
const templatePath = plugins.path.join(paths.templatesDir, 'gitignore', '_gitignore');
const raw = (await plugins.smartfs
.file(templatePath)
.encoding('utf8')
.read()) as string;
// Strip YAML frontmatter (---\n...\n---)
const frontmatterEnd = raw.indexOf('---', 3);
if (frontmatterEnd !== -1) {
return raw.slice(frontmatterEnd + 3).trimStart();
}
return raw;
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
const gitignorePath = '.gitignore';
const standardTemplate = await this.getStandardTemplate();
// Check if file exists and extract custom content
let customContent = '';
const exists = await plugins.smartfs.file(gitignorePath).exists();
if (exists) {
const existingContent = (await plugins.smartfs
.file(gitignorePath)
.encoding('utf8')
.read()) as string;
// Extract custom section content
const customMarkers = ['#------# custom', '# custom'];
for (const marker of customMarkers) {
const splitResult = existingContent.split(marker);
if (splitResult.length > 1) {
customContent = splitResult[1].trim();
break;
}
}
}
// Compute new content
let newContent = standardTemplate;
if (customContent) {
newContent = standardTemplate + '\n' + customContent + '\n';
} else {
newContent = standardTemplate + '\n';
}
// Read current content to compare
let currentContent = '';
if (exists) {
currentContent = (await plugins.smartfs
.file(gitignorePath)
.encoding('utf8')
.read()) as string;
}
if (!exists) {
changes.push({
type: 'create',
path: gitignorePath,
module: this.name,
description: 'Create .gitignore',
content: newContent,
});
} else if (newContent !== currentContent) {
changes.push({
type: 'modify',
path: gitignorePath,
module: this.name,
description: 'Update .gitignore (preserving custom section)',
content: newContent,
});
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
if (!change.content) return;
if (change.type === 'create') {
await this.createFile(change.path, change.content);
logger.log('info', 'Created .gitignore');
} else if (change.type === 'modify') {
await this.modifyFile(change.path, change.content);
logger.log('info', 'Updated .gitignore (preserved custom section)');
}
} }
} }
@@ -1,43 +0,0 @@
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import { Project } from '../../classes.project.js';
import * as plugins from '../mod.plugins.js';
// This is a wrapper for existing format modules
export class LegacyFormatter extends BaseFormatter {
private moduleName: string;
private formatModule: any;
constructor(
context: any,
project: Project,
moduleName: string,
formatModule: any,
) {
super(context, project);
this.moduleName = moduleName;
this.formatModule = formatModule;
}
get name(): string {
return this.moduleName;
}
async analyze(): Promise<IPlannedChange[]> {
// For legacy modules, we can't easily predict changes
// So we'll return a generic change that indicates the module will run
return [
{
type: 'modify',
path: '<various files>',
module: this.name,
description: `Run ${this.name} formatter`,
},
];
}
async applyChange(change: IPlannedChange): Promise<void> {
// Run the legacy format module
await this.formatModule.run(this.project);
}
}
+59 -5
View File
@@ -1,8 +1,62 @@
import { LegacyFormatter } from './legacy.formatter.js'; import { BaseFormatter } from '../classes.baseformatter.js';
import * as formatLicense from '../format.license.js'; import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger } from '../../gitzone.logging.js';
export class LicenseFormatter extends LegacyFormatter { const INCOMPATIBLE_LICENSES: string[] = ['AGPL', 'GPL', 'SSPL'];
constructor(context: any, project: any) {
super(context, project, 'license', formatLicense); export class LicenseFormatter extends BaseFormatter {
get name(): string {
return 'license';
}
async analyze(): Promise<IPlannedChange[]> {
// License formatter only checks for incompatible licenses
// It does not modify any files, so return empty array
// The actual check happens in execute() for reporting purposes
return [];
}
async execute(changes: IPlannedChange[]): Promise<void> {
const startTime = this.stats.moduleStartTime(this.name);
this.stats.startModule(this.name);
try {
// Check if node_modules exists
const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
const nodeModulesExists = await plugins.smartfs
.directory(nodeModulesPath)
.exists();
if (!nodeModulesExists) {
logger.log('warn', 'No node_modules found. Skipping license check');
return;
}
// Run license check
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
const licenseCheckResult = await licenseChecker.excludeLicenseWithinPath(
paths.cwd,
INCOMPATIBLE_LICENSES,
);
if (licenseCheckResult.failingModules.length === 0) {
logger.log('info', 'License check passed - no incompatible licenses found');
} else {
logger.log('error', 'License check failed - incompatible licenses found:');
for (const failedModule of licenseCheckResult.failingModules) {
console.log(
` ${failedModule.name} has license ${failedModule.license}`,
);
}
}
} finally {
this.stats.endModule(this.name, startTime);
}
}
async applyChange(change: IPlannedChange): Promise<void> {
// No file changes for license formatter
} }
} }
@@ -1,8 +0,0 @@
import { LegacyFormatter } from './legacy.formatter.js';
import * as formatNpmextra from '../format.npmextra.js';
export class NpmextraFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'npmextra', formatNpmextra);
}
}
@@ -1,8 +1,122 @@
import { LegacyFormatter } from './legacy.formatter.js'; import { BaseFormatter } from '../classes.baseformatter.js';
import * as formatPackageJson from '../format.packagejson.js'; import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger, logVerbose } from '../../gitzone.logging.js';
export class PackageJsonFormatter extends LegacyFormatter { export class PackageJsonFormatter extends BaseFormatter {
constructor(context: any, project: any) { get name(): string {
super(context, project, 'packagejson', formatPackageJson); return 'packagejson';
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
const packageJsonPath = 'package.json';
// Check if file exists
const exists = await plugins.smartfs.file(packageJsonPath).exists();
if (!exists) {
logVerbose('package.json does not exist, skipping');
return changes;
}
// Read current content
const currentContent = (await plugins.smartfs
.file(packageJsonPath)
.encoding('utf8')
.read()) as string;
// Parse and compute new content
const packageJson = JSON.parse(currentContent);
// Get gitzone config from smartconfig
const smartconfigInstance = new plugins.smartconfig.Smartconfig(paths.cwd);
const gitzoneData: any = smartconfigInstance.dataFor('@git.zone/cli', {});
// Set metadata from gitzone config
if (gitzoneData.module) {
packageJson.repository = {
type: 'git',
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}.git`,
};
packageJson.bugs = {
url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}/issues`,
};
packageJson.homepage = `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}#readme`;
}
// Ensure module type
if (!packageJson.type) {
packageJson.type = 'module';
}
// Ensure private field exists
if (packageJson.private === undefined) {
packageJson.private = true;
}
// Ensure license field exists
if (!packageJson.license) {
packageJson.license = 'UNLICENSED';
}
// Ensure scripts object exists
if (!packageJson.scripts) {
packageJson.scripts = {};
}
// Ensure build script exists
if (!packageJson.scripts.build) {
packageJson.scripts.build = `echo "Not needed for now"`;
}
// Set files array
packageJson.files = [
'ts/**/*',
'ts_web/**/*',
'dist/**/*',
'dist_*/**/*',
'dist_ts/**/*',
'dist_ts_web/**/*',
'assets/**/*',
'cli.js',
'.smartconfig.json',
'readme.md',
];
// Set pnpm overrides from assets
try {
const overridesContent = (await plugins.smartfs
.file(plugins.path.join(paths.assetsDir, 'overrides.json'))
.encoding('utf8')
.read()) as string;
const overrides = JSON.parse(overridesContent);
packageJson.pnpm = packageJson.pnpm || {};
packageJson.pnpm.overrides = overrides;
} catch (error) {
logVerbose(`Could not read overrides.json: ${error.message}`);
}
const newContent = JSON.stringify(packageJson, null, 2);
// Only add change if content differs
if (newContent !== currentContent) {
changes.push({
type: 'modify',
path: packageJsonPath,
module: this.name,
description: 'Format package.json',
content: newContent,
});
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
if (change.type !== 'modify' || !change.content) return;
await this.modifyFile(change.path, change.content);
logger.log('info', 'Updated package.json');
} }
} }
+140 -45
View File
@@ -1,5 +1,5 @@
import { BaseFormatter } from '../classes.baseformatter.js'; import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js'; import type { IPlannedChange, ICheckResult } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js'; import * as plugins from '../mod.plugins.js';
import { logger, logVerbose } from '../../gitzone.logging.js'; import { logger, logVerbose } from '../../gitzone.logging.js';
@@ -21,7 +21,7 @@ export class PrettierFormatter extends BaseFormatter {
const rootConfigFiles = [ const rootConfigFiles = [
'package.json', 'package.json',
'tsconfig.json', 'tsconfig.json',
'npmextra.json', '.smartconfig.json',
'.prettierrc', '.prettierrc',
'.prettierrc.json', '.prettierrc.json',
'.prettierrc.js', '.prettierrc.js',
@@ -29,8 +29,9 @@ export class PrettierFormatter extends BaseFormatter {
'README.md', 'README.md',
'changelog.md', 'changelog.md',
'CHANGELOG.md', 'CHANGELOG.md',
'license', // Skip files without extensions as prettier can't infer parser
'LICENSE', // 'license',
// 'LICENSE',
'*.md', '*.md',
]; ];
@@ -39,34 +40,51 @@ export class PrettierFormatter extends BaseFormatter {
// Add files from TypeScript directories // Add files from TypeScript directories
for (const dir of includeDirs) { for (const dir of includeDirs) {
try {
const globPattern = `${dir}/**/*.${extensions}`; const globPattern = `${dir}/**/*.${extensions}`;
const dirFiles = await plugins.smartfile.fs.listFileTree( const dirEntries = await plugins.smartfs
'.', .directory('.')
globPattern, .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(...dirFiles); allFiles.push(...filteredFiles);
} catch (error) {
logVerbose(`Skipping directory ${dir}: ${error.message}`);
}
} }
// Add root config files // Add root config files (only check root level, no recursive needed)
for (const pattern of rootConfigFiles) { for (const pattern of rootConfigFiles) {
const rootFiles = await plugins.smartfile.fs.listFileTree('.', pattern); try {
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) // Only include files at root level (no slashes in path)
const rootLevelFiles = rootFiles.filter((f) => !f.includes('/')); const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
allFiles.push(...rootLevelFiles); allFiles.push(...rootLevelFiles);
} catch (error) {
logVerbose(`Skipping pattern ${pattern}: ${error.message}`);
}
} }
// Remove duplicates // Remove duplicates
const uniqueFiles = [...new Set(allFiles)]; const uniqueFiles = [...new Set(allFiles)];
// Get all files that match the pattern
const files = uniqueFiles;
// Ensure we only process actual files (not directories) // Ensure we only process actual files (not directories)
const validFiles: string[] = []; const validFiles: string[] = [];
for (const file of files) { for (const file of uniqueFiles) {
try { try {
const stats = await plugins.smartfile.fs.stat(file); const stats = await plugins.smartfs.file(file).stat();
if (!stats.isDirectory()) { if (!stats.isDirectory) {
validFiles.push(file); validFiles.push(file);
} }
} catch (error) { } catch (error) {
@@ -75,14 +93,7 @@ export class PrettierFormatter extends BaseFormatter {
} }
} }
// Check which files need formatting
for (const file of validFiles) { for (const file of validFiles) {
// Skip files that haven't changed
if (!(await this.shouldProcessFile(file))) {
logVerbose(`Skipping ${file} - no changes detected`);
continue;
}
changes.push({ changes.push({
type: 'modify', type: 'modify',
path: file, path: file,
@@ -102,26 +113,15 @@ export class PrettierFormatter extends BaseFormatter {
try { try {
await this.preExecute(); await this.preExecute();
// Batch process files logVerbose(`Processing ${changes.length} files sequentially`);
const batchSize = 10; // Process 10 files at a time
const batches: IPlannedChange[][] = [];
for (let i = 0; i < changes.length; i += batchSize) {
batches.push(changes.slice(i, i + batchSize));
}
// Process files sequentially to avoid prettier cache/state issues
for (let i = 0; i < changes.length; i++) {
const change = changes[i];
logVerbose( logVerbose(
`Processing ${changes.length} files in ${batches.length} batches`, `Processing file ${i + 1}/${changes.length}: ${change.path}`,
); );
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
logVerbose(
`Processing batch ${i + 1}/${batches.length} (${batch.length} files)`,
);
// Process batch in parallel
const promises = batch.map(async (change) => {
try { try {
await this.applyChange(change); await this.applyChange(change);
this.stats.recordFileOperation(this.name, change.type, true); this.stats.recordFileOperation(this.name, change.type, true);
@@ -133,9 +133,6 @@ export class PrettierFormatter extends BaseFormatter {
); );
// Don't throw - continue with other files // Don't throw - continue with other files
} }
});
await Promise.all(promises);
} }
await this.postExecute(); await this.postExecute();
@@ -151,11 +148,35 @@ export class PrettierFormatter extends BaseFormatter {
if (change.type !== 'modify') return; if (change.type !== 'modify') return;
try { try {
// Validate the path before processing
if (!change.path || change.path.trim() === '') {
logger.log(
'error',
`Invalid empty path in change: ${JSON.stringify(change)}`,
);
throw new Error('Invalid empty path');
}
// Read current content // Read current content
const content = plugins.smartfile.fs.toStringSync(change.path); const content = (await plugins.smartfs
.file(change.path)
.encoding('utf8')
.read()) as string;
// Format with prettier // Format with prettier
const prettier = await import('prettier'); const prettier = await import('prettier');
// 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, { const formatted = await prettier.format(content, {
filepath: change.path, filepath: change.path,
...(await this.getPrettierConfig()), ...(await this.getPrettierConfig()),
@@ -163,20 +184,45 @@ export class PrettierFormatter extends BaseFormatter {
// Only write if content actually changed // Only write if content actually changed
if (formatted !== content) { if (formatted !== content) {
// Debug: log the path being written
logVerbose(`Writing formatted content to: ${change.path}`);
await this.modifyFile(change.path, formatted); await this.modifyFile(change.path, formatted);
logVerbose(`Formatted ${change.path}`); logVerbose(`Formatted ${change.path}`);
} else { } else {
logVerbose(`No formatting changes for ${change.path}`); logVerbose(`No formatting changes for ${change.path}`);
} }
} catch (prettierError) {
// Check if it's a parser error
if (
prettierError.message &&
prettierError.message.includes('No parser could be inferred')
) {
logVerbose(`Skipping ${change.path} - ${prettierError.message}`);
return; // Skip this file silently
}
throw prettierError;
}
} catch (error) { } catch (error) {
logger.log('error', `Failed to format ${change.path}: ${error.message}`); // Log the full error stack for debugging mkdir issues
if (error.message && error.message.includes('mkdir')) {
logger.log(
'error',
`Failed to format ${change.path}: ${error.message}`,
);
logger.log('error', `Error stack: ${error.stack}`);
} else {
logger.log(
'error',
`Failed to format ${change.path}: ${error.message}`,
);
}
throw error; throw error;
} }
} }
private async getPrettierConfig(): Promise<any> { private async getPrettierConfig(): Promise<any> {
// Try to load prettier config from the project // Try to load prettier config from the project
const prettierConfig = new plugins.npmextra.Npmextra(); const prettierConfig = new plugins.smartconfig.Smartconfig();
return prettierConfig.dataFor('prettier', { return prettierConfig.dataFor('prettier', {
// Default prettier config // Default prettier config
singleQuote: true, singleQuote: true,
@@ -187,4 +233,53 @@ export class PrettierFormatter extends BaseFormatter {
arrowParens: 'always', arrowParens: 'always',
}); });
} }
/**
* Override check() to compute diffs on-the-fly by running prettier
*/
async check(): Promise<ICheckResult> {
const changes = await this.analyze();
const diffs: ICheckResult['diffs'] = [];
for (const change of changes) {
if (change.type !== 'modify') continue;
try {
// Read current content
const currentContent = (await plugins.smartfs
.file(change.path)
.encoding('utf8')
.read()) as string;
// Skip files without extension (prettier can't infer parser)
const fileExt = plugins.path.extname(change.path).toLowerCase();
if (!fileExt) continue;
// Format with prettier to get what it would produce
const prettier = await import('prettier');
const formatted = await prettier.format(currentContent, {
filepath: change.path,
...(await this.getPrettierConfig()),
});
// Only add to diffs if content differs
if (formatted !== currentContent) {
diffs.push({
path: change.path,
type: 'modify',
before: currentContent,
after: formatted,
});
}
} catch (error) {
// Skip files that can't be processed
logVerbose(`Skipping diff for ${change.path}: ${error.message}`);
}
}
return {
hasDiff: diffs.length > 0,
diffs,
};
}
} }
+39 -8
View File
@@ -1,6 +1,15 @@
import { BaseFormatter } from '../classes.baseformatter.js'; import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js'; import type { IPlannedChange } from '../interfaces.format.js';
import * as formatReadme from '../format.readme.js'; import * as plugins from '../mod.plugins.js';
import { logger } from '../../gitzone.logging.js';
const DEFAULT_README_CONTENT = `# Project Readme
This is the initial readme file.`;
const DEFAULT_README_HINTS_CONTENT = `# Project Readme Hints
This is the initial readme hints file.`;
export class ReadmeFormatter extends BaseFormatter { export class ReadmeFormatter extends BaseFormatter {
get name(): string { get name(): string {
@@ -8,17 +17,39 @@ export class ReadmeFormatter extends BaseFormatter {
} }
async analyze(): Promise<IPlannedChange[]> { async analyze(): Promise<IPlannedChange[]> {
return [ const changes: IPlannedChange[] = [];
{
type: 'modify', // Check readme.md
const readmeExists = await plugins.smartfs.file('readme.md').exists();
if (!readmeExists) {
changes.push({
type: 'create',
path: 'readme.md', path: 'readme.md',
module: this.name, module: this.name,
description: 'Ensure readme files exist', description: 'Create readme.md',
}, content: DEFAULT_README_CONTENT,
]; });
}
// Check readme.hints.md
const hintsExists = await plugins.smartfs.file('readme.hints.md').exists();
if (!hintsExists) {
changes.push({
type: 'create',
path: 'readme.hints.md',
module: this.name,
description: 'Create readme.hints.md',
content: DEFAULT_README_HINTS_CONTENT,
});
}
return changes;
} }
async applyChange(change: IPlannedChange): Promise<void> { async applyChange(change: IPlannedChange): Promise<void> {
await formatReadme.run(); if (change.type !== 'create' || !change.content) return;
await this.createFile(change.path, change.content);
logger.log('info', `Created ${change.path}`);
} }
} }
@@ -0,0 +1,186 @@
import { BaseFormatter } from "../classes.baseformatter.js";
import type { IPlannedChange } from "../interfaces.format.js";
import * as plugins from "../mod.plugins.js";
import { logger, logVerbose } from "../../gitzone.logging.js";
/**
* Migrates .smartconfig.json from old namespace keys to new package-scoped keys
*/
const migrateNamespaceKeys = (smartconfigJson: any): boolean => {
let migrated = false;
const migrations = [
{ oldKey: "gitzone", newKey: "@git.zone/cli" },
{ oldKey: "tsdoc", newKey: "@git.zone/tsdoc" },
{ oldKey: "npmdocker", newKey: "@git.zone/tsdocker" },
{ oldKey: "npmci", newKey: "@ship.zone/szci" },
{ oldKey: "szci", newKey: "@ship.zone/szci" },
];
for (const { oldKey, newKey } of migrations) {
if (smartconfigJson[oldKey]) {
if (!smartconfigJson[newKey]) {
smartconfigJson[newKey] = smartconfigJson[oldKey];
} else {
smartconfigJson[newKey] = {
...smartconfigJson[oldKey],
...smartconfigJson[newKey],
};
}
delete smartconfigJson[oldKey];
migrated = true;
}
}
return migrated;
};
/**
* Migrates npmAccessLevel from @ship.zone/szci to @git.zone/cli.release.accessLevel
*/
const migrateAccessLevel = (smartconfigJson: any): boolean => {
const szciConfig = smartconfigJson["@ship.zone/szci"];
if (!szciConfig?.npmAccessLevel) {
return false;
}
const gitzoneConfig = smartconfigJson["@git.zone/cli"] || {};
if (gitzoneConfig?.release?.accessLevel) {
delete szciConfig.npmAccessLevel;
return true;
}
if (!smartconfigJson["@git.zone/cli"]) {
smartconfigJson["@git.zone/cli"] = {};
}
if (!smartconfigJson["@git.zone/cli"].release) {
smartconfigJson["@git.zone/cli"].release = {};
}
smartconfigJson["@git.zone/cli"].release.accessLevel =
szciConfig.npmAccessLevel;
delete szciConfig.npmAccessLevel;
return true;
};
const CONFIG_FILE = ".smartconfig.json";
export class SmartconfigFormatter extends BaseFormatter {
get name(): string {
return "smartconfig";
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
// File rename (npmextra.json/smartconfig.json → .smartconfig.json)
// is handled by the orchestrator before analysis.
// This formatter only operates on .smartconfig.json.
const exists = await plugins.smartfs.file(CONFIG_FILE).exists();
if (!exists) {
logVerbose(".smartconfig.json does not exist, skipping");
return changes;
}
const currentContent = (await plugins.smartfs
.file(CONFIG_FILE)
.encoding("utf8")
.read()) as string;
const smartconfigJson = JSON.parse(currentContent);
// Apply key migrations
migrateNamespaceKeys(smartconfigJson);
migrateAccessLevel(smartconfigJson);
// Ensure namespaces exist
if (!smartconfigJson["@git.zone/cli"]) {
smartconfigJson["@git.zone/cli"] = {};
}
if (!smartconfigJson["@ship.zone/szci"]) {
smartconfigJson["@ship.zone/szci"] = {};
}
const newContent = JSON.stringify(smartconfigJson, null, 2);
if (newContent !== currentContent) {
changes.push({
type: "modify",
path: CONFIG_FILE,
module: this.name,
description: "Migrate and format .smartconfig.json",
content: newContent,
});
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
if (change.type !== "modify" || !change.content) return;
const smartconfigJson = JSON.parse(change.content);
// Check for missing required module information
const expectedRepoInformation: string[] = [
"projectType",
"module.githost",
"module.gitscope",
"module.gitrepo",
"module.description",
"module.npmPackagename",
"module.license",
];
const interactInstance = new plugins.smartinteract.SmartInteract();
const missingRepoInformation = expectedRepoInformation.filter(
(expectedRepoInformationItem) => {
return !plugins.smartobject.smartGet(
smartconfigJson["@git.zone/cli"],
expectedRepoInformationItem,
);
},
);
if (missingRepoInformation.length > 0 && !this.context.isInteractive()) {
throw new Error(
`Missing required .smartconfig.json fields: ${missingRepoInformation.join(", ")}`,
);
}
for (const expectedRepoInformationItem of expectedRepoInformation) {
if (
!plugins.smartobject.smartGet(
smartconfigJson["@git.zone/cli"],
expectedRepoInformationItem,
)
) {
interactInstance.addQuestions([
{
message: `What is the value of ${expectedRepoInformationItem}`,
name: expectedRepoInformationItem,
type: "input",
default: "undefined variable",
},
]);
}
}
const answerbucket = await interactInstance.runQueue();
for (const expectedRepoInformationItem of expectedRepoInformation) {
const cliProvidedValue = answerbucket.getAnswerFor(
expectedRepoInformationItem,
);
if (cliProvidedValue) {
plugins.smartobject.smartAdd(
smartconfigJson["@git.zone/cli"],
expectedRepoInformationItem,
cliProvidedValue,
);
}
}
const finalContent = JSON.stringify(smartconfigJson, null, 2);
await this.modifyFile(change.path, finalContent);
logger.log("info", "Updated .smartconfig.json");
}
}
+164 -5
View File
@@ -1,8 +1,167 @@
import { LegacyFormatter } from './legacy.formatter.js'; import { BaseFormatter } from '../classes.baseformatter.js';
import * as formatTemplates from '../format.templates.js'; import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger, logVerbose } from '../../gitzone.logging.js';
export class TemplatesFormatter extends LegacyFormatter { export class TemplatesFormatter extends BaseFormatter {
constructor(context: any, project: any) { get name(): string {
super(context, project, 'templates', formatTemplates); return 'templates';
}
/**
* Render a template directory through smartscaf and return a map of path content.
*/
private async renderTemplate(templateName: string): Promise<Map<string, string>> {
const templateDir = plugins.path.join(paths.templatesDir, templateName);
const scafTemplate = new plugins.smartscaf.ScafTemplate(templateDir);
await scafTemplate.readTemplateFromDir();
const gitzoneData = this.project.gitzoneConfig?.data;
if (gitzoneData) {
await scafTemplate.supplyVariables({
module: gitzoneData.module,
projectType: gitzoneData.projectType,
});
}
const renderedFiles = await scafTemplate.renderToMemory();
const fileMap = new Map<string, string>();
for (const file of renderedFiles) {
fileMap.set(file.path, file.contents.toString());
}
return fileMap;
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
const project = this.project;
const projectType = project.gitzoneConfig?.data?.projectType;
// VSCode template - for all projects
const vscodeChanges = await this.analyzeTemplate('vscode', [
{ templatePath: '.vscode/settings.json', destPath: '.vscode/settings.json' },
{ templatePath: '.vscode/launch.json', destPath: '.vscode/launch.json' },
]);
changes.push(...vscodeChanges);
// CI and other templates based on projectType
switch (projectType) {
case 'npm':
case 'wcc':
const accessLevel = (project.gitzoneConfig?.data as any)?.release?.accessLevel
|| project.gitzoneConfig?.data?.npmciOptions?.npmAccessLevel;
const ciTemplate = accessLevel === 'public' ? 'ci_default' : 'ci_default_private';
const ciChanges = await this.analyzeTemplate(ciTemplate, [
{ templatePath: '.gitea/workflows/default_nottags.yaml', destPath: '.gitea/workflows/default_nottags.yaml' },
{ templatePath: '.gitea/workflows/default_tags.yaml', destPath: '.gitea/workflows/default_tags.yaml' },
]);
changes.push(...ciChanges);
break;
case 'service':
case 'website':
const dockerCiChanges = await this.analyzeTemplate('ci_docker', [
{ templatePath: '.gitea/workflows/docker_nottags.yaml', destPath: '.gitea/workflows/docker_nottags.yaml' },
{ templatePath: '.gitea/workflows/docker_tags.yaml', destPath: '.gitea/workflows/docker_tags.yaml' },
]);
changes.push(...dockerCiChanges);
const dockerfileChanges = await this.analyzeTemplate('dockerfile_service', [
{ templatePath: 'Dockerfile', destPath: 'Dockerfile' },
{ templatePath: 'dockerignore', destPath: '.dockerignore' },
]);
changes.push(...dockerfileChanges);
const cliChanges = await this.analyzeTemplate('cli', [
{ templatePath: 'cli.js', destPath: 'cli.js' },
{ templatePath: 'cli.ts.js', destPath: 'cli.ts.js' },
]);
changes.push(...cliChanges);
break;
}
// Update templates based on projectType
if (projectType === 'website') {
const websiteChanges = await this.analyzeTemplate('website_update', [
{ templatePath: 'html/index.html', destPath: 'html/index.html' },
]);
changes.push(...websiteChanges);
} else if (projectType === 'wcc') {
const wccChanges = await this.analyzeTemplate('wcc_update', [
{ templatePath: 'html/index.html', destPath: 'html/index.html' },
{ templatePath: 'html/index.ts', destPath: 'html/index.ts' },
]);
changes.push(...wccChanges);
}
return changes;
}
private async analyzeTemplate(
templateName: string,
files: Array<{ templatePath: string; destPath: string }>,
): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
const templateDir = plugins.path.join(paths.templatesDir, templateName);
const templateExists = await plugins.smartfs.directory(templateDir).exists();
if (!templateExists) {
logVerbose(`Template ${templateName} not found`);
return changes;
}
let renderedFiles: Map<string, string>;
try {
renderedFiles = await this.renderTemplate(templateName);
} catch (error) {
logVerbose(`Failed to render template ${templateName}: ${error.message}`);
return changes;
}
for (const file of files) {
// Look up by templatePath first, then destPath (frontmatter may rename files)
const processedContent = renderedFiles.get(file.templatePath)
|| renderedFiles.get(file.destPath);
if (!processedContent) {
logVerbose(`Template file ${file.templatePath} not found in rendered output`);
continue;
}
const destExists = await plugins.smartfs.file(file.destPath).exists();
let currentContent = '';
if (destExists) {
currentContent = (await plugins.smartfs
.file(file.destPath)
.encoding('utf8')
.read()) as string;
}
if (processedContent !== currentContent) {
changes.push({
type: destExists ? 'modify' : 'create',
path: file.destPath,
module: this.name,
description: `Apply template ${templateName}/${file.templatePath}`,
content: processedContent,
});
}
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
if (!change.content) return;
if (change.type === 'create') {
await this.createFile(change.path, change.content);
} else {
await this.modifyFile(change.path, change.content);
}
logger.log('info', `Applied template to ${change.path}`);
} }
} }
+73 -5
View File
@@ -1,8 +1,76 @@
import { LegacyFormatter } from './legacy.formatter.js'; import { BaseFormatter } from '../classes.baseformatter.js';
import * as formatTsconfig from '../format.tsconfig.js'; import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger, logVerbose } from '../../gitzone.logging.js';
export class TsconfigFormatter extends LegacyFormatter { export class TsconfigFormatter extends BaseFormatter {
constructor(context: any, project: any) { get name(): string {
super(context, project, 'tsconfig', formatTsconfig); return 'tsconfig';
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
const tsconfigPath = 'tsconfig.json';
// Check if file exists
const exists = await plugins.smartfs.file(tsconfigPath).exists();
if (!exists) {
logVerbose('tsconfig.json does not exist, skipping');
return changes;
}
// Read current content
const currentContent = (await plugins.smartfs
.file(tsconfigPath)
.encoding('utf8')
.read()) as string;
// Parse and compute new content
const tsconfigObject = JSON.parse(currentContent);
tsconfigObject.compilerOptions = tsconfigObject.compilerOptions || {};
tsconfigObject.compilerOptions.baseUrl = '.';
const existingPaths = tsconfigObject.compilerOptions.paths || {};
// Get module paths from tspublish, merging with existing custom paths
const tspublishPaths: Record<string, string[]> = {};
try {
const tsPublishMod = await import('@git.zone/tspublish');
const tsPublishInstance = new tsPublishMod.TsPublish();
const publishModules = await tsPublishInstance.getModuleSubDirs(paths.cwd);
for (const publishModule of Object.keys(publishModules)) {
const publishConfig = publishModules[publishModule];
tspublishPaths[`${publishConfig.name}`] = [
`./${publishModule}/index.js`,
];
}
} catch (error) {
logVerbose(`Could not get tspublish modules: ${error.message}`);
}
tsconfigObject.compilerOptions.paths = { ...existingPaths, ...tspublishPaths };
const newContent = JSON.stringify(tsconfigObject, null, 2);
// Only add change if content differs
if (newContent !== currentContent) {
changes.push({
type: 'modify',
path: tsconfigPath,
module: this.name,
description: 'Format tsconfig.json with path mappings',
content: newContent,
});
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
if (change.type !== 'modify' || !change.content) return;
await this.modifyFile(change.path, change.content);
logger.log('info', 'Updated tsconfig.json');
} }
} }
+361 -116
View File
@@ -1,93 +1,112 @@
import * as plugins from './mod.plugins.js'; import * as plugins from "./mod.plugins.js";
import { Project } from '../classes.project.js'; import { Project } from "../classes.project.js";
import { FormatContext } from './classes.formatcontext.js'; import { FormatContext } from "./classes.formatcontext.js";
import { FormatPlanner } from './classes.formatplanner.js'; import { FormatPlanner } from "./classes.formatplanner.js";
import { logger, setVerboseMode } from '../gitzone.logging.js'; import { BaseFormatter } from "./classes.baseformatter.js";
import { logger, setVerboseMode } from "../gitzone.logging.js";
import type { ICliMode } from "../helpers.climode.js";
import {
getCliMode,
printJson,
runWithSuppressedOutput,
} from "../helpers.climode.js";
import { getCliConfigValue } from "../helpers.smartconfig.js";
// Import wrapper classes for formatters import { CleanupFormatter } from "./formatters/cleanup.formatter.js";
import { CleanupFormatter } from './formatters/cleanup.formatter.js'; import { SmartconfigFormatter } from "./formatters/smartconfig.formatter.js";
import { NpmextraFormatter } from './formatters/npmextra.formatter.js'; import { LicenseFormatter } from "./formatters/license.formatter.js";
import { LicenseFormatter } from './formatters/license.formatter.js'; import { PackageJsonFormatter } from "./formatters/packagejson.formatter.js";
import { PackageJsonFormatter } from './formatters/packagejson.formatter.js'; import { TemplatesFormatter } from "./formatters/templates.formatter.js";
import { TemplatesFormatter } from './formatters/templates.formatter.js'; import { GitignoreFormatter } from "./formatters/gitignore.formatter.js";
import { GitignoreFormatter } from './formatters/gitignore.formatter.js'; import { TsconfigFormatter } from "./formatters/tsconfig.formatter.js";
import { TsconfigFormatter } from './formatters/tsconfig.formatter.js'; import { PrettierFormatter } from "./formatters/prettier.formatter.js";
import { PrettierFormatter } from './formatters/prettier.formatter.js'; import { ReadmeFormatter } from "./formatters/readme.formatter.js";
import { ReadmeFormatter } from './formatters/readme.formatter.js'; import { CopyFormatter } from "./formatters/copy.formatter.js";
import { CopyFormatter } from './formatters/copy.formatter.js';
export let run = async ( /**
options: { * Rename npmextra.json or smartconfig.json to .smartconfig.json
dryRun?: boolean; * before any formatter tries to read config.
yes?: boolean; */
planOnly?: boolean; async function migrateConfigFile(allowWrite: boolean): Promise<void> {
savePlan?: string; const target = ".smartconfig.json";
fromPlan?: string; const targetExists = await plugins.smartfs.file(target).exists();
detailed?: boolean; if (targetExists) return;
interactive?: boolean;
parallel?: boolean; for (const oldName of ["smartconfig.json", "npmextra.json"]) {
verbose?: boolean; const exists = await plugins.smartfs.file(oldName).exists();
} = {}, if (exists) {
): Promise<any> => { if (!allowWrite) {
// Set verbose mode if requested return;
if (options.verbose) { }
setVerboseMode(true); const content = (await plugins.smartfs
.file(oldName)
.encoding("utf8")
.read()) as string;
await plugins.smartfs.file(`./${target}`).encoding("utf8").write(content);
await plugins.smartfs.file(oldName).delete();
logger.log("info", `Migrated ${oldName} to ${target}`);
return;
}
}
} }
const project = await Project.fromCwd(); // Shared formatter class map used by both run() and runFormatter()
const context = new FormatContext(); const formatterMap: Record<
// Cache system removed - no longer needed string,
const planner = new FormatPlanner(); new (ctx: FormatContext, proj: Project) => BaseFormatter
> = {
cleanup: CleanupFormatter,
smartconfig: SmartconfigFormatter,
license: LicenseFormatter,
packagejson: PackageJsonFormatter,
templates: TemplatesFormatter,
gitignore: GitignoreFormatter,
tsconfig: TsconfigFormatter,
prettier: PrettierFormatter,
readme: ReadmeFormatter,
copy: CopyFormatter,
};
// Get configuration from npmextra // Formatters that don't require projectType to be set
const npmextraConfig = new plugins.npmextra.Npmextra(); const formattersNotRequiringProjectType = [
const formatConfig = npmextraConfig.dataFor<any>('gitzone.format', { "smartconfig",
"prettier",
"cleanup",
"packagejson",
];
const getFormatConfig = async () => {
const rawFormatConfig = await getCliConfigValue<Record<string, any>>(
"format",
{},
);
return {
interactive: true, interactive: true,
showDiffs: false, showDiffs: false,
autoApprove: false, autoApprove: false,
planTimeout: 30000, showStats: true,
rollback: {
enabled: true,
autoRollbackOnError: true,
backupRetentionDays: 7,
maxBackupSize: '100MB',
excludePatterns: ['node_modules/**', '.git/**'],
},
modules: { modules: {
skip: [], skip: [],
only: [], only: [],
order: [], ...(rawFormatConfig.modules || {}),
}, },
parallel: true, ...rawFormatConfig,
cache: { };
enabled: true, };
clean: true, // Clean invalid entries from cache
},
});
// Cache cleaning removed - no longer using cache system const createActiveFormatters = async (options: {
interactive: boolean;
jsonOutput: boolean;
}) => {
const project = await Project.fromCwd({ requireProjectType: false });
const context = new FormatContext(options);
const planner = new FormatPlanner();
// Override config with command options const formatConfig = await getFormatConfig();
const interactive = options.interactive ?? formatConfig.interactive; const formatters = Object.entries(formatterMap).map(
const autoApprove = options.yes ?? formatConfig.autoApprove; ([, FormatterClass]) => new FormatterClass(context, project),
const parallel = options.parallel ?? formatConfig.parallel; );
try {
// Initialize formatters
const formatters = [
new CleanupFormatter(context, project),
new NpmextraFormatter(context, project),
new LicenseFormatter(context, project),
new PackageJsonFormatter(context, project),
new TemplatesFormatter(context, project),
new GitignoreFormatter(context, project),
new TsconfigFormatter(context, project),
new PrettierFormatter(context, project),
new ReadmeFormatter(context, project),
new CopyFormatter(context, project),
];
// Filter formatters based on configuration
const activeFormatters = formatters.filter((formatter) => { const activeFormatters = formatters.filter((formatter) => {
if (formatConfig.modules.only.length > 0) { if (formatConfig.modules.only.length > 0) {
return formatConfig.modules.only.includes(formatter.name); return formatConfig.modules.only.includes(formatter.name);
@@ -98,32 +117,160 @@ export let run = async (
return true; return true;
}); });
// Plan phase return {
logger.log('info', 'Analyzing project for format operations...'); context,
let plan = options.fromPlan planner,
? JSON.parse(await plugins.smartfile.fs.toStringSync(options.fromPlan)) 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); : await planner.planFormat(activeFormatters);
return {
context,
planner,
formatConfig,
activeFormatters,
plan,
};
};
const serializePlan = (plan: any) => {
return {
summary: plan.summary,
warnings: plan.warnings,
changes: plan.changes.map((change: any) => ({
type: change.type,
path: change.path,
module: change.module,
description: change.description,
})),
};
};
export let run = async (
options: {
write?: boolean;
dryRun?: boolean; // Deprecated, kept for compatibility
yes?: boolean;
planOnly?: boolean;
savePlan?: string;
fromPlan?: string;
detailed?: boolean;
interactive?: boolean;
verbose?: boolean;
diff?: boolean;
[key: string]: any;
} = {},
): Promise<any> => {
const mode = await getCliMode(options as any);
const subcommand = (options as any)?._?.[1];
if (mode.help || subcommand === "help") {
showHelp(mode);
return;
}
if (options.verbose) {
setVerboseMode(true);
}
const shouldWrite = options.write ?? options.dryRun === false;
const treatAsPlan = subcommand === "plan";
if (mode.json && shouldWrite) {
printJson({
ok: false,
error:
"JSON output is only supported for read-only format planning. Use `gitzone format plan --json` or omit `--json` when applying changes.",
});
return;
}
// Migrate config file before anything reads it
await migrateConfigFile(shouldWrite);
const formatConfig = await getFormatConfig();
const interactive =
options.interactive ?? (mode.interactive && formatConfig.interactive);
const autoApprove = options.yes ?? formatConfig.autoApprove;
try {
const planBuilder = async () => {
return await buildFormatPlan({
fromPlan: options.fromPlan,
interactive,
jsonOutput: mode.json,
});
};
if (!mode.json) {
logger.log("info", "Analyzing project for format operations...");
}
const { context, planner, activeFormatters, plan } = mode.json
? await runWithSuppressedOutput(planBuilder)
: await planBuilder();
if (mode.json) {
printJson(serializePlan(plan));
return;
}
// Display plan // Display plan
await planner.displayPlan(plan, options.detailed); await planner.displayPlan(plan, options.detailed);
// Save plan if requested // Save plan if requested
if (options.savePlan) { if (options.savePlan) {
await plugins.smartfile.memory.toFs( await plugins.smartfs
JSON.stringify(plan, null, 2), .file(options.savePlan)
options.savePlan, .encoding("utf8")
); .write(JSON.stringify(plan, null, 2));
logger.log('info', `Plan saved to ${options.savePlan}`); logger.log("info", `Plan saved to ${options.savePlan}`);
} }
// Exit if plan-only mode if (options.planOnly || treatAsPlan) {
if (options.planOnly) {
return; return;
} }
// Dry-run mode // Show diffs if explicitly requested or before interactive write confirmation
if (options.dryRun) { const showDiffs =
logger.log('info', 'Dry-run mode - no changes will be made'); options.diff || (shouldWrite && interactive && !autoApprove);
if (showDiffs) {
logger.log("info", "Showing file diffs:");
console.log("");
for (const formatter of activeFormatters) {
const checkResult = await formatter.check();
if (checkResult.hasDiff) {
logger.log("info", `[${formatter.name}]`);
formatter.displayAllDiffs(checkResult);
console.log("");
}
}
}
// Dry-run mode (default behavior)
if (!shouldWrite) {
logger.log("info", "Dry-run mode - use --write (-w) to apply changes");
return; return;
} }
@@ -131,59 +278,157 @@ export let run = async (
if (interactive && !autoApprove) { if (interactive && !autoApprove) {
const interactInstance = new plugins.smartinteract.SmartInteract(); const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({ const response = await interactInstance.askQuestion({
type: 'confirm', type: "confirm",
name: 'proceed', name: "proceed",
message: 'Proceed with formatting?', message: "Proceed with formatting?",
default: true, default: true,
}); });
if (!(response as any).value) { if (!(response as any).value) {
logger.log('info', 'Format operation cancelled by user'); logger.log("info", "Format operation cancelled by user");
return; return;
} }
} }
// Execute phase // Execute phase
logger.log( logger.log("info", "Executing format operations...");
'info', await planner.executePlan(plan, activeFormatters, context);
`Executing format operations${parallel ? ' in parallel' : ' sequentially'}...`,
);
await planner.executePlan(plan, activeFormatters, context, parallel);
// Finish statistics tracking
context.getFormatStats().finish(); context.getFormatStats().finish();
// Display statistics const showStats = formatConfig.showStats ?? true;
const showStats = npmextraConfig.dataFor('gitzone.format.showStats', true);
if (showStats) { if (showStats) {
context.getFormatStats().displayStats(); context.getFormatStats().displayStats();
} }
// Save stats if requested
if (options.detailed) { if (options.detailed) {
const statsPath = `.nogit/format-stats-${Date.now()}.json`; const statsPath = `.nogit/format-stats-${Date.now()}.json`;
await context.getFormatStats().saveReport(statsPath); await context.getFormatStats().saveReport(statsPath);
} }
logger.log('success', 'Format operations completed successfully!'); logger.log("success", "Format operations completed successfully!");
} catch (error) { } catch (error) {
logger.log('error', `Format operation failed: ${error.message}`); const errorMessage = error instanceof Error ? error.message : String(error);
logger.log("error", `Format operation failed: ${errorMessage}`);
// Rollback system has been removed for stability
throw error; throw error;
} }
}; };
// Export CLI command handlers import type { ICheckResult } from "./interfaces.format.js";
export const handleRollback = async (operationId?: string): Promise<void> => { export type { ICheckResult };
logger.log('info', 'Rollback system has been disabled for stability');
/**
* 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 const handleListBackups = async (): Promise<void> => { export function showHelp(mode?: ICliMode): void {
logger.log('info', 'Backup system has been disabled for stability'); 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;
}
export const handleCleanBackups = async (): Promise<void> => { console.log("");
logger.log('info', 'Backup cleaning has been disabled - backup system removed'); 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("");
}
+27 -19
View File
@@ -1,31 +1,15 @@
export type IFormatOperation = {
id: string;
timestamp: number;
files: Array<{
path: string;
originalContent: string;
checksum: string;
permissions: string;
}>;
status: 'pending' | 'in-progress' | 'completed' | 'failed' | 'rolled-back';
error?: Error;
};
export type IFormatPlan = { export type IFormatPlan = {
summary: { summary: {
totalFiles: number; totalFiles: number;
filesAdded: number; filesAdded: number;
filesModified: number; filesModified: number;
filesRemoved: number; filesRemoved: number;
estimatedTime: number;
}; };
changes: Array<{ changes: Array<{
type: 'create' | 'modify' | 'delete'; type: 'create' | 'modify' | 'delete';
path: string; path: string;
module: string; module: string;
description: string; description: string;
diff?: string;
size?: number;
}>; }>;
warnings: Array<{ warnings: Array<{
level: 'info' | 'warning' | 'error'; level: 'info' | 'warning' | 'error';
@@ -39,7 +23,31 @@ export type IPlannedChange = {
path: string; path: string;
module: string; module: string;
description: string; description: string;
content?: string; // For create/modify operations content?: string; // New content for create/modify operations
diff?: string;
size?: number;
}; };
export interface ICheckResult {
hasDiff: boolean;
diffs: Array<{
path: string;
type: 'create' | 'modify' | 'delete';
before?: string;
after?: string;
}>;
}
export function getModuleIcon(module: string): string {
const icons: Record<string, string> = {
packagejson: '📦',
license: '📝',
tsconfig: '🔧',
cleanup: '🚮',
gitignore: '🔒',
prettier: '✨',
readme: '📖',
templates: '📄',
smartconfig: '⚙️',
copy: '📋',
};
return icons[module] || '📁';
}
+4 -14
View File
@@ -1,31 +1,21 @@
export * from '../plugins.js'; export * from '../plugins.js';
import * as crypto from 'crypto';
import * as path from 'path'; import * as path from 'path';
import * as lik from '@push.rocks/lik';
import * as smartfile from '@push.rocks/smartfile'; import * as smartfile from '@push.rocks/smartfile';
import * as smartgulp from '@push.rocks/smartgulp';
import * as smartinteract from '@push.rocks/smartinteract'; import * as smartinteract from '@push.rocks/smartinteract';
import * as smartlegal from '@push.rocks/smartlegal'; import * as smartlegal from '@push.rocks/smartlegal';
import * as smartobject from '@push.rocks/smartobject'; import * as smartobject from '@push.rocks/smartobject';
import * as smartnpm from '@push.rocks/smartnpm'; import * as smartconfig from '@push.rocks/smartconfig';
import * as smartstream from '@push.rocks/smartstream';
import * as through2 from 'through2';
import * as npmextra from '@push.rocks/npmextra';
import * as smartdiff from '@push.rocks/smartdiff'; import * as smartdiff from '@push.rocks/smartdiff';
import * as smartscaf from '@push.rocks/smartscaf';
export { export {
crypto,
path, path,
lik,
smartfile, smartfile,
smartgulp,
smartinteract, smartinteract,
smartlegal, smartlegal,
smartobject, smartobject,
smartnpm, smartconfig,
smartstream,
through2,
npmextra,
smartdiff, smartdiff,
smartscaf,
}; };
+75 -32
View File
@@ -48,15 +48,17 @@ export class Meta {
public async readDirectory() { public async readDirectory() {
await this.syncToRemote(true); await this.syncToRemote(true);
logger.log('info', `reading directory`); logger.log('info', `reading directory`);
const metaFileExists = plugins.smartfile.fs.fileExistsSync( const metaFileExists = await plugins.smartfs
this.filePaths.metaJson, .file(this.filePaths.metaJson)
); .exists();
if (!metaFileExists) { if (!metaFileExists) {
throw new Error(`meta file does not exist at ${this.filePaths.metaJson}`); throw new Error(`meta file does not exist at ${this.filePaths.metaJson}`);
} }
this.metaRepoData = plugins.smartfile.fs.toObjectSync( const content = (await plugins.smartfs
this.filePaths.metaJson, .file(this.filePaths.metaJson)
); .encoding('utf8')
.read()) as string;
this.metaRepoData = JSON.parse(content);
} }
/** /**
@@ -78,15 +80,15 @@ export class Meta {
*/ */
public async writeToDisk() { public async writeToDisk() {
// write .meta.json to disk // write .meta.json to disk
plugins.smartfile.memory.toFsSync( await plugins.smartfs
JSON.stringify(this.metaRepoData, null, 2), .file(this.filePaths.metaJson)
this.filePaths.metaJson, .encoding('utf8')
); .write(JSON.stringify(this.metaRepoData, null, 2));
// write .gitignore to disk // write .gitignore to disk
plugins.smartfile.memory.toFsSync( await plugins.smartfs
await this.generateGitignore(), .file(this.filePaths.gitIgnore)
this.filePaths.gitIgnore, .encoding('utf8')
); .write(await this.generateGitignore());
} }
/** /**
@@ -112,10 +114,25 @@ export class Meta {
*/ */
public async updateLocalRepos() { public async updateLocalRepos() {
await this.syncToRemote(); await this.syncToRemote();
const projects = plugins.smartfile.fs.toObjectSync( const metaContent = (await plugins.smartfs
this.filePaths.metaJson, .file(this.filePaths.metaJson)
).projects; .encoding('utf8')
const preExistingFolders = plugins.smartfile.fs.listFoldersSync(this.cwd); .read()) as string;
const projects = JSON.parse(metaContent).projects;
const entries = await plugins.smartfs.directory(this.cwd).list();
const preExistingFolders: string[] = [];
for (const entry of entries) {
try {
const stats = await plugins.smartfs
.file(plugins.path.join(this.cwd, entry.path))
.stat();
if (stats.isDirectory) {
preExistingFolders.push(entry.name);
}
} catch {
// Skip entries that can't be accessed
}
}
for (const preExistingFolderArg of preExistingFolders) { for (const preExistingFolderArg of preExistingFolders) {
if ( if (
preExistingFolderArg !== '.git' && preExistingFolderArg !== '.git' &&
@@ -143,9 +160,17 @@ export class Meta {
await this.sortMetaRepoData(); await this.sortMetaRepoData();
const missingRepos: string[] = []; const missingRepos: string[] = [];
for (const key of Object.keys(this.metaRepoData.projects)) { for (const key of Object.keys(this.metaRepoData.projects)) {
plugins.smartfile.fs.isDirectory(key) const fullPath = plugins.path.join(this.cwd, key);
? logger.log('ok', `${key} -> is already cloned`) try {
: missingRepos.push(key); const stats = await plugins.smartfs.file(fullPath).stat();
if (stats.isDirectory) {
logger.log('ok', `${key} -> is already cloned`);
} else {
missingRepos.push(key);
}
} catch {
missingRepos.push(key);
}
} }
logger.log('info', `found ${missingRepos.length} missing repos`); logger.log('info', `found ${missingRepos.length} missing repos`);
@@ -165,7 +190,20 @@ export class Meta {
await this.syncToRemote(); await this.syncToRemote();
// go recursive // go recursive
const folders = await plugins.smartfile.fs.listFolders(this.cwd); const listEntries = await plugins.smartfs.directory(this.cwd).list();
const folders: string[] = [];
for (const entry of listEntries) {
try {
const stats = await plugins.smartfs
.file(plugins.path.join(this.cwd, entry.path))
.stat();
if (stats.isDirectory) {
folders.push(entry.name);
}
} catch {
// Skip entries that can't be accessed
}
}
const childMetaRepositories: string[] = []; const childMetaRepositories: string[] = [];
for (const folder of folders) { for (const folder of folders) {
logger.log('info', folder); logger.log('info', folder);
@@ -180,26 +218,30 @@ export class Meta {
*/ */
public async initProject() { public async initProject() {
await this.syncToRemote(true); await this.syncToRemote(true);
const fileExists = await plugins.smartfile.fs.fileExists( const fileExists = await plugins.smartfs
this.filePaths.metaJson, .file(this.filePaths.metaJson)
); .exists();
if (!fileExists) { if (!fileExists) {
await plugins.smartfile.memory.toFs( await plugins.smartfs
.file(this.filePaths.metaJson)
.encoding('utf8')
.write(
JSON.stringify({ JSON.stringify({
projects: {}, projects: {},
}), }),
this.filePaths.metaJson,
); );
logger.log( logger.log(
`success`, `success`,
`created a new .meta.json in directory ${this.cwd}`, `created a new .meta.json in directory ${this.cwd}`,
); );
await plugins.smartfile.memory.toFs( await plugins.smartfs
.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( logger.log(
`success`, `success`,
@@ -264,9 +306,10 @@ export class Meta {
await this.writeToDisk(); await this.writeToDisk();
logger.log('info', 'removing directory from cwd'); logger.log('info', 'removing directory from cwd');
await plugins.smartfile.fs.remove( await plugins.smartfs
plugins.path.join(paths.cwd, projectNameArg), .directory(plugins.path.join(paths.cwd, projectNameArg))
); .recursive()
.delete();
await this.updateLocalRepos(); await this.updateLocalRepos();
} }
} }
+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}`);
}
}
+9
View File
@@ -0,0 +1,9 @@
export * from '../plugins.js';
import * as smartshell from '@push.rocks/smartshell';
import * as smartfile from '@push.rocks/smartfile';
import * as smartinteract from '@push.rocks/smartinteract';
import * as smartnetwork from '@push.rocks/smartnetwork';
import * as smartdelay from '@push.rocks/smartdelay';
export { smartshell, smartfile, smartinteract, smartnetwork, smartdelay };
+218 -25
View File
@@ -1,31 +1,224 @@
/* ----------------------------------------------- /* -----------------------------------------------
* executes as standard task * executes as standard task
* ----------------------------------------------- */ * ----------------------------------------------- */
import * as plugins from './mod.plugins.js'; import * as plugins from "./mod.plugins.js";
import * as paths from '../paths.js'; import * as paths from "../paths.js";
import type { ICliMode } from "../helpers.climode.js";
import { getCliMode, printJson } from "../helpers.climode.js";
import { logger } from '../gitzone.logging.js'; import { logger } from "../gitzone.logging.js";
export let run = () => { type ICommandHelpSummary = {
const done = plugins.smartpromise.defer(); name: string;
logger.log('warn', 'no action specified'); description: string;
logger.log(
'info',
`
You can do one of the following things:
* create a new project with 'gitzone template [template]'
the following templates exist: ${(() => {
let projects = `\n`;
for (const template of plugins.smartfile.fs.listFoldersSync(
paths.templatesDir,
)) {
projects += ` - ${template}\n`;
}
return projects;
})()}
* format a project with 'gitzone format'
`,
);
done.resolve();
return done.promise;
}; };
const commandSummaries: ICommandHelpSummary[] = [
{
name: "commit",
description:
"Create semantic commits or generate read-only commit recommendations",
},
{ name: "format", description: "Plan or apply project formatting changes" },
{ name: "config", description: "Read and change .smartconfig.json settings" },
{ name: "services", description: "Manage or configure development services" },
{ name: "template", description: "Create a project from a template" },
{ name: "open", description: "Open project assets and CI pages" },
{ name: "docker", description: "Run Docker-related maintenance tasks" },
{
name: "deprecate",
description: "Deprecate npm packages across registries",
},
{ name: "meta", description: "Run meta-repository commands" },
{ name: "start", description: "Prepare a project for local work" },
{ name: "helpers", description: "Run helper utilities" },
];
export let run = async (argvArg: any = {}) => {
const mode = await getCliMode(argvArg);
const requestedCommandHelp =
argvArg._?.[0] === "help" ? argvArg._?.[1] : undefined;
if (mode.help || requestedCommandHelp) {
await showHelp(mode, requestedCommandHelp);
return;
}
if (!mode.interactive) {
await showHelp(mode);
return;
}
console.log("");
console.log(
"╭─────────────────────────────────────────────────────────────╮",
);
console.log(
"│ gitzone - Development Workflow CLI │",
);
console.log(
"╰─────────────────────────────────────────────────────────────╯",
);
console.log("");
const interactInstance = new plugins.smartinteract.SmartInteract();
const response = await interactInstance.askQuestion({
type: "list",
name: "action",
message: "What would you like to do?",
default: "commit",
choices: [
{ name: "Commit changes (semantic versioning)", value: "commit" },
{ name: "Format project files", value: "format" },
{ name: "Configure release settings", value: "config" },
{ name: "Create from template", value: "template" },
{ name: "Manage dev services (MongoDB, S3)", value: "services" },
{ name: "Open project assets", value: "open" },
{ name: "Show help", value: "help" },
],
});
const action = (response as any).value;
switch (action) {
case "commit": {
const modCommit = await import("../mod_commit/index.js");
await modCommit.run({ _: ["commit"] });
break;
}
case "format": {
const modFormat = await import("../mod_format/index.js");
await modFormat.run({ interactive: true });
break;
}
case "config": {
const modConfig = await import("../mod_config/index.js");
await modConfig.run({ _: ["config"] });
break;
}
case "template": {
const modTemplate = await import("../mod_template/index.js");
await modTemplate.run({ _: ["template"] });
break;
}
case "services": {
const modServices = await import("../mod_services/index.js");
await modServices.run({ _: ["services"] });
break;
}
case "open": {
const modOpen = await import("../mod_open/index.js");
await modOpen.run({ _: ["open"] });
break;
}
case "help":
await showHelp(mode);
break;
}
};
export async function showHelp(
mode: ICliMode,
commandName?: string,
): Promise<void> {
if (commandName) {
const handled = await showCommandHelp(commandName, mode);
if (handled) {
return;
}
}
if (mode.json) {
printJson({
name: "gitzone",
usage: "gitzone <command> [options]",
commands: commandSummaries,
globalFlags: [
{ flag: "--help, -h", description: "Show help output" },
{
flag: "--json",
description: "Emit machine-readable JSON when supported",
},
{
flag: "--plain",
description: "Use plain text output when supported",
},
{
flag: "--agent",
description: "Prefer non-interactive machine-friendly output",
},
{
flag: "--no-interactive",
description: "Disable prompts and interactive menus",
},
{
flag: "--no-check-updates",
description: "Skip the update check banner",
},
],
});
return;
}
console.log("");
console.log("Usage: gitzone <command> [options]");
console.log("");
console.log("Commands:");
for (const commandSummary of commandSummaries) {
console.log(
` ${commandSummary.name.padEnd(11)} ${commandSummary.description}`,
);
}
console.log("");
console.log("Global flags:");
console.log(" --help, -h Show help output");
console.log(
" --json Emit machine-readable JSON when supported",
);
console.log(" --plain Use plain text output when supported");
console.log(
" --agent Prefer non-interactive machine-friendly output",
);
console.log(" --no-interactive Disable prompts and interactive menus");
console.log(" --no-check-updates Skip the update check banner");
console.log("");
console.log("Examples:");
console.log(" gitzone help commit");
console.log(" gitzone config show --json");
console.log(" gitzone commit recommend --json");
console.log(" gitzone format plan --json");
console.log(" gitzone services set mongodb,minio");
console.log("");
console.log("Run gitzone <command> --help for command-specific usage.");
console.log("");
}
async function showCommandHelp(
commandName: string,
mode: ICliMode,
): Promise<boolean> {
switch (commandName) {
case "commit": {
const modCommit = await import("../mod_commit/index.js");
modCommit.showHelp(mode);
return true;
}
case "config": {
const modConfig = await import("../mod_config/index.js");
modConfig.showHelp(mode);
return true;
}
case "format": {
const modFormat = await import("../mod_format/index.js");
modFormat.showHelp(mode);
return true;
}
case "services": {
const modServices = await import("../mod_services/index.js");
modServices.showHelp(mode);
return true;
}
default:
return false;
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ export const getTemplatePath = (templateNameArg: string) => {
* receives a template name and returns wether there is a corresponding template * receives a template name and returns wether there is a corresponding template
*/ */
export const isTemplate = async (templateNameArg: string) => { export const isTemplate = async (templateNameArg: string) => {
return plugins.smartfile.fs.isDirectory(getTemplatePath(templateNameArg)); return plugins.smartfs.directory(getTemplatePath(templateNameArg)).exists();
}; };
export const getTemplate = async (templateNameArg: string) => { export const getTemplate = async (templateNameArg: string) => {
+16 -2
View File
@@ -1,21 +1,35 @@
import * as smartlog from '@push.rocks/smartlog'; import * as smartlog from '@push.rocks/smartlog';
import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local'; import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
import * as npmextra from '@push.rocks/npmextra'; import * as smartconfig from '@push.rocks/smartconfig';
import * as path from 'path'; import * as path from 'path';
import * as projectinfo from '@push.rocks/projectinfo'; import * as projectinfo from '@push.rocks/projectinfo';
import * as smartcli from '@push.rocks/smartcli'; import * as smartcli from '@push.rocks/smartcli';
import * as smartpath from '@push.rocks/smartpath'; import * as smartpath from '@push.rocks/smartpath';
import * as smartpromise from '@push.rocks/smartpromise'; import * as smartpromise from '@push.rocks/smartpromise';
import * as smartupdate from '@push.rocks/smartupdate'; import * as smartupdate from '@push.rocks/smartupdate';
import * as smartshell from '@push.rocks/smartshell';
import * as smartnetwork from '@push.rocks/smartnetwork';
import * as smartfile from '@push.rocks/smartfile';
import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
import * as smartinteract from '@push.rocks/smartinteract';
import * as smartdelay from '@push.rocks/smartdelay';
// Create smartfs instance for filesystem operations
export const smartfs = new SmartFs(new SmartFsProviderNode());
export { export {
smartlog, smartlog,
smartlogDestinationLocal, smartlogDestinationLocal,
npmextra, smartconfig,
path, path,
projectinfo, projectinfo,
smartcli, smartcli,
smartpath, smartpath,
smartpromise, smartpromise,
smartupdate, smartupdate,
smartshell,
smartnetwork,
smartfile,
smartinteract,
smartdelay,
}; };
+1 -5
View File
@@ -1,7 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
@@ -10,7 +8,5 @@
"baseUrl": ".", "baseUrl": ".",
"paths": {} "paths": {}
}, },
"exclude": [ "exclude": ["dist_*/**/*.d.ts"]
"dist_*/**/*.d.ts"
]
} }