├── .changeset └── config.json ├── .commitlintrc ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── LICENSE ├── README.md ├── docs ├── readme │ └── pt-BR │ │ └── README.md └── static │ └── demo.gif ├── package.json ├── packages ├── ai-models │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── scripts │ │ ├── config │ │ │ └── constants.mjs │ │ ├── generateAIModels.mjs │ │ ├── generateDynamicFiles.mjs │ │ ├── generateSchemes.mjs │ │ ├── utils │ │ │ ├── getAIModelsOutput.mjs │ │ │ ├── getModelsFiles.mjs │ │ │ └── getSchemeOutput.mjs │ │ └── validateAIModels.mjs │ ├── src │ │ ├── AIModel.ts │ │ ├── AjvSchemeValidator.ts │ │ ├── factories │ │ │ └── createAIModel.ts │ │ ├── index.ts │ │ ├── models │ │ │ ├── OllamaModel.ts │ │ │ └── OpenAIModel.ts │ │ └── types │ │ │ ├── IAIModel.ts │ │ │ ├── IAIModelsParams.ts │ │ │ └── ISchemeValidator.ts │ ├── tests │ │ └── unit │ │ │ ├── AIModel.spec.ts │ │ │ ├── AjvSchemeValidator.spec.ts │ │ │ └── models │ │ │ ├── OllamaModel.spec.ts │ │ │ └── OpenAIModel.spec.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsup.config.ts ├── commit-history │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── src │ │ ├── CommitHistory.ts │ │ └── index.ts │ ├── tests │ │ └── CommitHistory.spec.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsup.config.ts ├── config │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── src │ │ ├── ConfigManager.ts │ │ ├── ConfigSourceManager.ts │ │ ├── ConfigValidator.ts │ │ ├── factories │ │ │ └── createConfigManager.ts │ │ ├── formatConfigValue.ts │ │ ├── index.ts │ │ ├── loaders │ │ │ ├── ArgConfigLoader.ts │ │ │ ├── EnvConfigLoader.ts │ │ │ └── FileConfigLoader.ts │ │ └── types │ │ │ ├── IConfig.ts │ │ │ ├── IConfigDefinitions.ts │ │ │ ├── IConfigLoader.ts │ │ │ ├── IConfigValue.ts │ │ │ └── ISource.ts │ ├── tests │ │ └── unit │ │ │ ├── ConfigManager.spec.ts │ │ │ ├── ConfigSourceManager.spec.ts │ │ │ ├── ConfigValidator.spec.ts │ │ │ ├── formatConfigValue.spec.ts │ │ │ └── loaders │ │ │ ├── ArgConfigLoader.spec.ts │ │ │ ├── EnvConfigLoader.spec.ts │ │ │ └── FileConfigLoader.spec.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsup.config.ts ├── eslint-config │ ├── CHANGELOG.md │ ├── README.md │ ├── node.js │ └── package.json ├── git │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── src │ │ ├── Git.ts │ │ ├── buildDiffArgs.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ └── types │ │ │ ├── IDiffOptions.ts │ │ │ └── IGit.ts │ ├── tests │ │ └── unit │ │ │ └── buildDiffArgs.spec.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsup.config.ts ├── prompt-parser │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── src │ │ ├── Prompt.ts │ │ ├── PromptBuilder.ts │ │ ├── PromptTemplateParser.ts │ │ └── index.ts │ ├── tests │ │ └── unit │ │ │ ├── Prompt.spec.ts │ │ │ ├── PromptBuilder.spec.ts │ │ │ └── PromptTemplateParser.spec.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsup.config.ts └── typescript-config │ ├── CHANGELOG.md │ ├── README.md │ ├── base.json │ └── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── projects ├── cli │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── src │ │ ├── commands │ │ │ ├── amend.ts │ │ │ ├── commit.ts │ │ │ ├── config │ │ │ │ ├── init.ts │ │ │ │ ├── list.ts │ │ │ │ ├── set.ts │ │ │ │ └── unset.ts │ │ │ ├── edit.ts │ │ │ ├── generate.ts │ │ │ ├── generateAndCommit.ts │ │ │ └── validate.ts │ │ ├── config.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── parsers │ │ │ └── keyValueParser.ts │ │ ├── prompts │ │ │ ├── promptCommitContext.ts │ │ │ ├── promptCommitType.ts │ │ │ ├── promptConfirmConfigOverwrite.ts │ │ │ ├── promptConfirmStage.ts │ │ │ ├── promptInterativeGeneration.ts │ │ │ ├── promptProvider.ts │ │ │ └── promptProviderParams.ts │ │ └── utils │ │ │ ├── checkStagedFiles.ts │ │ │ ├── editLine.ts │ │ │ ├── errorHandler.ts │ │ │ ├── maskSecret.ts │ │ │ └── wrapText.ts │ ├── tests │ │ └── unit │ │ │ ├── parsers │ │ │ └── keyValueParser.spec.ts │ │ │ └── utils │ │ │ ├── editLine.spec.ts │ │ │ └── maskSecret.spec.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsup.config.ts └── core │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── src │ ├── application │ │ ├── AmendGenerated.ts │ │ ├── CommitGenerated.ts │ │ ├── EditLastGenerated.ts │ │ ├── GenerateCommit.ts │ │ └── ValidateCommit.ts │ ├── factories │ │ ├── createAmendGenerated.ts │ │ ├── createCommitGenerated.ts │ │ ├── createEditLastGenerated.ts │ │ ├── createGenerateCommit.ts │ │ └── createValidateCommit.ts │ ├── index.ts │ ├── prompts │ │ ├── generatePrompt.ts │ │ └── validatePrompt.ts │ ├── sanitizers │ │ ├── normalizeJson.ts │ │ └── sanitize.ts │ ├── schemes.ts │ ├── services │ │ ├── CommitGenerator.ts │ │ └── CommitValidator.ts │ └── types │ │ ├── IAIModel.ts │ │ ├── ICommitGenerator.ts │ │ ├── ICommitHistory.ts │ │ ├── ICommitInfo.ts │ │ └── ICommitValidator.ts │ ├── tests │ └── unit │ │ ├── application │ │ ├── AmendGenerated.spec.ts │ │ ├── CommitGenerated.spec.ts │ │ ├── EditLastGenerated.spec.ts │ │ ├── GenerateCommit.spec.ts │ │ └── ValidateCommit.spec.ts │ │ ├── prompts │ │ ├── generatePrompt.spec.ts │ │ └── validatePrompt.spec.ts │ │ ├── sanitizers │ │ ├── normalizeJson.spec.ts │ │ └── sanitize.spec.ts │ │ └── services │ │ ├── CommitGenerator.spec.ts │ │ └── CommitValidator.spec.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsup.config.ts ├── turbo.json └── vitest.config.ts /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "access": "public", 6 | "baseBranch": "main", 7 | "updateInternalDependencies": "patch" 8 | } 9 | -------------------------------------------------------------------------------- /.commitlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .env 5 | .commitgen.json 6 | history 7 | .turbo -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run precommit 2 | npm run lint 3 | npm run typecheck 4 | npm run test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 JulioC090 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Commit Generator

2 | 3 | > Generate commits with AI 4 | 5 |

6 | 7 |

8 | 9 |

10 | English 11 | | 12 | Português 13 |

14 | 15 | **Commit Generator** is a tool that leverages AI to automatically generate commit messages based on changes in your code. 16 | 17 | ## 📌 Table of Contents 18 | - [🔹 What is Commit Generator?](#-what-is-commit-generator) 19 | - [🚀 Getting Started](#-getting-started) 20 | - [⚙️ Features](#️-features) 21 | - [📂 Project Structure](#-project-structure) 22 | - [📜 License](#-license) 23 | 24 | ## 🔹 What is Commit Generator? 25 | 26 | Writing meaningful commit messages can be tedious, and inconsistent messages make version history difficult to navigate. 27 | 28 | **Commit Generator** eliminates this hassle by analyzing your code changes and generating **clear, structured, and relevant commit messages** automatically. 29 | 30 | ## 🚀 Getting started 31 | 32 | Currently, the primary way to interact with **Commit Generator** is through the **CLI**. 33 | 34 | ### 📋 Requirements 35 | 36 | Before installing, ensure you have the following dependencies: 37 | - [Node.js](https://nodejs.org/en) (Required for running the CLI) 38 | - [Git](https://git-scm.com/) (Used for repository management) 39 | 40 | ### 🔧 Installation 41 | 42 | Follow these steps to install and set up **Commit Generator**: 43 | 44 | 1. Install the package 45 | ```bash 46 | npm install --global @commit-generator/cli 47 | ``` 48 | 49 | 2. Initialize AI configuration 50 | ```bash 51 | commitgen config init 52 | ``` 53 | 54 | ### 🎯 Usage 55 | 56 | Once installed, follow these steps to generate commit messages using AI. 57 | 58 | 1. Stage your modified files 59 | ```bash 60 | git add . 61 | ``` 62 | 63 | 2. Generate a commit message 64 | ```bash 65 | commitgen 66 | ``` 67 | 68 | 🎉 Done! Commit Generator will analyze your staged changes and suggest a meaningful commit message. 69 | 70 | For more details, check out the [CLI documentation](./projects/cli). 71 | 72 | ## ⚙️ Features 73 | 74 | ✅ **AI-powered commit message generation** – Uses AI to analyze code changes and generate meaningful commit messages. 75 | 76 | ✅ **Seamless Git integration** – Analyzes Git diffs and staged files for precise commit suggestions. 77 | 78 | ✅ **Standardized commit types** – Supports `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `style`, `build`, `ci`, `perf`, and `revert`. 79 | 80 | ✅ **Context-aware commits** – Add extra context (e.g., issue numbers, scope, or additional details) for more clarity. 81 | 82 | ✅ **Automated commits** – Use `--force` to commit changes instantly without manual confirmation. 83 | 84 | ✅ **Commit editing & history management** – Modify, amend, and validate commit messages easily with commands like `commitgen edit`, `commitgen amend`, and `commitgen validate`. 85 | 86 | ✅ **Commit message validation** – Ensures messages follow best practices and provides recommendations. 87 | 88 | ✅ **Configurable AI provider** – Customize AI settings via `commitgen config`, with options to set, unset, and list configurations. 89 | 90 | ## 📂 Project Structure 91 | 92 | ``` 93 | commit-generator/ 94 | │── docs/ # Documentation files 95 | │── packages/ 96 | │ ├── git/ # Git integration 97 | │ ├── ai-models/ # Handles AI model interactions 98 | │ ├── prompt-parser/ # Parses text templates into structured prompts 99 | │ ├── commit-history/ # Tracks previously generated commit messages 100 | │ ├── config/ # Configuration manager 101 | │ ├── eslint-config/ # Pre-configured ESLint settings 102 | │ ├── typescript-config/ # TypeScript configurations 103 | │── projects 104 | │ ├── core/ # The core logic for commit generation 105 | │ ├── cli/ # CLI interface for commit generator 106 | │── .gitignore 107 | │── package.json 108 | │── README.md 109 | ``` 110 | 111 | Each package is documented separately. See: 112 | - [Core](./projects/core) 113 | - [Cli](./projects/cli) 114 | - [Git](./packages/git) 115 | - [AI Models](./packages/ai-models/) 116 | - [Prompt Parser](./packages/prompt-parser/) 117 | - [Commit History](./packages/commit-history) 118 | - [Config Manager](./packages/config) 119 | 120 | ## 📜 License 121 | 122 | Commit Generator is open-source and released under the MIT License. 123 | Feel free to use, modify, and contribute! -------------------------------------------------------------------------------- /docs/readme/pt-BR/README.md: -------------------------------------------------------------------------------- 1 |

Commit Generator

2 | 3 | > Gere commits com IA 4 | 5 |

6 | 7 |

8 | 9 |

10 | English 11 | | 12 | Português 13 |

14 | 15 | **Commit Generator** é uma ferramenta que utiliza IA para gerar automaticamente mensagens de commit com base nas alterações no seu código. 16 | 17 | ## 📌 Sumário 18 | - [🔹 O Que é o Gerador de Commits?](#-o-que-é-o-gerador-de-commits) 19 | - [🚀 Começando](#-começando) 20 | - [⚙️ Funcionalidades](#️-funcionalidades) 21 | - [📂 Estrutura do Projeto](#-estrutura-do-projeto) 22 | - [📜 Licença](#-licença) 23 | 24 | ## 🔹 O que é o Gerador de Commits? 25 | 26 | Escrever mensagens de commit significativas pode ser tedioso, e mensagens inconsistentes tornam o histórico de versões difícil de navegar. 27 | 28 | **Gerador de Commits** elimina essa dificuldade ao analisar suas mudanças de código e gerar **mensagens de commit claras, estruturadas e relevantes** automaticamente. 29 | 30 | ## 🚀 Começando 31 | 32 | Atualmente, a principal forma de interação com **Commit Generator** é por meio da **CLI**. 33 | 34 | ### 📋 Requisitos 35 | 36 | Antes de instalar, certifique-se de que você possui as seguintes dependências: 37 | - [Node.js](https://nodejs.org/en) (Necessário para rodar a CLI) 38 | - [Git](https://git-scm.com/) (Usado para gerenciamento de repositórios) 39 | 40 | ### 🔧 Instalação 41 | 42 | Siga os passos abaixo para instalar e configurar o **Commit Generator**: 43 | 44 | 1. Instale o pacote 45 | ```bash 46 | npm install --global @commit-generator/cli 47 | ``` 48 | 49 | 2. Inicialize a configuração da IA 50 | ```bash 51 | commitgen config init 52 | ``` 53 | 54 | ### 🎯 Uso 55 | 56 | Após a instalação, siga esses passos para gerar mensagens de commit usando IA. 57 | 58 | 1. Adicione seus arquivos modificados ao staging 59 | ```bash 60 | git add . 61 | ``` 62 | 63 | 2. Gere uma mensagem de commit 64 | ```bash 65 | commitgen 66 | ``` 67 | 68 | 🎉 Pronto! O Gerador de Commits irá analisar suas mudanças no staging e sugerir uma mensagem de commit significativa. 69 | 70 | Para mais detalhes, consulte a [documentação da CLI ](../../../projects/cli). 71 | 72 | ## Funcionalidades 73 | 74 | ✅ **Geração de mensagens de commit com IA** – Usa IA para analisar mudanças no código e gerar mensagens de commit significativas. 75 | 76 | ✅ **Integração sem falhas com Git** – Analisa diffs do Git e arquivos em staging para sugestões precisas de commits. 77 | 78 | ✅ **Tipos de commit padronizados** – Suporta `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `style`, `build`, `ci`, `perf`, e `revert`. 79 | 80 | ✅ **Commits com contexto** – Adiciona contexto extra (por exemplo, números de issue, escopo ou detalhes adicionais) para mais clareza. 81 | 82 | ✅ **Commits automáticos** – Use `--force` para realizar commits instantâneos sem confirmação manual. 83 | 84 | ✅ **Edição e gerenciamento de histórico de commits** – Modifique, emende e valide mensagens de commit facilmente com comandos como `commitgen edit`, `commitgen amend` e `commitgen validate`. 85 | 86 | ✅ **Validação de mensagens de commit** – Garante que as mensagens sigam as melhores práticas e fornece recomendações. 87 | 88 | ✅ **Provedor de IA configurável** – Personalize as configurações da IA via `commitgen config`, com opções para definir, remover e listar configurações. 89 | 90 | ## 📂 Estrutura do Projeto 91 | 92 | ``` 93 | commit-generator/ 94 | │── docs/ # Arquivos de documentação 95 | │── packages/ 96 | │ ├── git/ # Integração com Git 97 | │ ├── ai-models/ # Gerencia interações com modelos de IA 98 | │ ├── prompt-parser/ # Converte templates de texto em prompts estruturados 99 | │ ├── commit-history/ # Registra mensagens de commit geradas anteriormente 100 | │ ├── config/ # Gerenciador de configurações 101 | │ ├── eslint-config/ # Configurações do ESLint pré-configuradas 102 | │ ├── typescript-config/ # Configurações do TypeScript 103 | │── projects 104 | │ ├── core/ # Lógica principal para geração de commits 105 | │ ├── cli/ # Interface CLI para o gerador de commits 106 | │── .gitignore 107 | │── package.json 108 | │── README.md 109 | ``` 110 | 111 | Cada pacote é documentado separadamente. Veja: 112 | - [Core](../../../projects/core) 113 | - [Cli](../../../projects/cli) 114 | - [Git](../../../packages/git) 115 | - [AI Models](../../../packages/ai-models/) 116 | - [Prompt Parser](../../../packages/prompt-parser/) 117 | - [Commit History](../../../packages/commit-history) 118 | - [Config Manager](../../../packages/config) 119 | 120 | ## 📜 Licença 121 | 122 | O Gerador de Commits é um projeto open-source e está licenciado sob a Licença MIT. 123 | Fique à vontade para usar, modificar e contribuir! -------------------------------------------------------------------------------- /docs/static/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulioC090/commit-generator/9e2f1fa0285fc133d3d437a504d748ab19898b26/docs/static/demo.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commit-generator", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "prepare": "husky", 7 | "precommit": "turbo run precommit", 8 | "build": "turbo run build", 9 | "clean": "turbo run clean", 10 | "lint": "turbo run lint", 11 | "lint:fix": "turbo run lint:fix --filter=[HEAD]", 12 | "test": "turbo run test", 13 | "test:watch": "vitest watch", 14 | "test:coverage": "turbo run test:coverage", 15 | "typecheck": "turbo run typecheck", 16 | "publish-packages": "turbo run build typecheck test && changeset publish" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@changesets/cli": "^2.27.12", 23 | "@commitlint/cli": "^19.3.0", 24 | "@commitlint/config-conventional": "^19.2.2", 25 | "@vitest/coverage-v8": "^1.6.0", 26 | "husky": "^9.0.11", 27 | "tsup": "^8.3.6", 28 | "tsx": "^4.15.7", 29 | "typescript": "^5.5.2", 30 | "vite-tsconfig-paths": "^4.3.2", 31 | "vitest": "^1.6.0" 32 | }, 33 | "dependencies": { 34 | "turbo": "^2.4.0" 35 | }, 36 | "packageManager": "pnpm@10.5.2" 37 | } -------------------------------------------------------------------------------- /packages/ai-models/.gitignore: -------------------------------------------------------------------------------- 1 | # Dynamic files 2 | src/schemes.ts 3 | src/models/index.ts -------------------------------------------------------------------------------- /packages/ai-models/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/ai-models 2 | 3 | ## 1.1.1 4 | 5 | ### Patch Changes 6 | 7 | - update local package to use workspace 8 | 9 | ## 1.1.0 10 | 11 | ### Minor Changes 12 | 13 | - add new parameters to ai models 14 | 15 | ## 1.0.1 16 | 17 | ### Patch Changes 18 | 19 | - - Unused and redundant files have been removed to optimize the package. 20 | 21 | ## 1.0.0 22 | 23 | ### Major Changes 24 | 25 | - - Initial release of `@commit-generator/ai-models` package. 26 | - Added `createAIModel` function to initialize models from either provider. 27 | - Added `aiModelSchemes` and `IAIModelSchemes` for working with OpenAI schemas. 28 | -------------------------------------------------------------------------------- /packages/ai-models/README.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/ai-models 2 | 3 | This package provides AI models and schemas for the **Commit Generator** project. Currently, it supports models from **OpenAI** and **Ollama**. 4 | 5 | ## Installation 6 | To use the AI models and schemas in your project, install the package: 7 | 8 | ```bash 9 | pnpm install @commit-generator/ai-models 10 | ``` 11 | 12 | ## Usage 13 | After installation, you can use the AI models and schemas in your project. 14 | 15 | 1. Using AI Models 16 | The available models are OpenAI and Ollama. 17 | To create an AI model, use the createAIModel function, specifying the provider and parameters: 18 | 19 | ```javascript 20 | import { createAIModel } from '@commit-generator/ai-models'; 21 | 22 | const provider = "openai"; // Available models: "openai" or "ollama" 23 | 24 | const params = { 25 | key: "some_key", 26 | }; 27 | 28 | const model = createAIModel(provider, params); 29 | 30 | const result = await model.complete("Hello"); 31 | console.log(result); 32 | ``` 33 | 34 | 2. Extending Schemas 35 | 36 | ```javascript 37 | import { aiModelSchemes, IAIModelSchemes } from '@commit-generator/ai-models/schemes'; 38 | 39 | console.log(aiModelSchemes.openai); 40 | 41 | export type IType = { 42 | myProperties: Array 43 | } & Partial; 44 | ``` 45 | 46 | ## License 47 | This package is licensed under the MIT License. -------------------------------------------------------------------------------- /packages/ai-models/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslintNode from '@commit-generator/eslint-config/node.js'; 2 | 3 | export default [...eslintNode]; 4 | -------------------------------------------------------------------------------- /packages/ai-models/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@commit-generator/ai-models", 3 | "version": "1.1.1", 4 | "description": "AI models factory and schemas for the Commit Generator project", 5 | "author": "JulioC090", 6 | "files": [ 7 | "dist" 8 | ], 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "import": "./dist/index.mjs", 13 | "require": "./dist/index.js", 14 | "default": "./dist/index.js" 15 | }, 16 | "./schemes": { 17 | "types": "./dist/schemes.d.ts", 18 | "import": "./dist/schemes.mjs", 19 | "require": "./dist/schemes.js", 20 | "default": "./dist/schemes.js" 21 | } 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/JulioC090/commit-generator.git", 26 | "directory": "packages/ai-models" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/JulioC090/commit-generator/issues" 30 | }, 31 | "license": "MIT", 32 | "scripts": { 33 | "prepare": "node scripts/generateDynamicFiles.mjs", 34 | "precommit": "node scripts/validateAIModels.mjs", 35 | "prebuild": "node scripts/generateDynamicFiles.mjs", 36 | "build": "tsup --format cjs,esm", 37 | "lint": "eslint . --report-unused-disable-directives --max-warnings 0", 38 | "lint:fix": "eslint . --fix", 39 | "test": "vitest run", 40 | "test:watch": "vitest watch", 41 | "test:coverage": "vitest run --coverage", 42 | "typecheck": "tsc --noEmit -p ." 43 | }, 44 | "dependencies": { 45 | "ajv": "^8.17.1", 46 | "ollama": "^0.5.13", 47 | "openai": "^4.77.0" 48 | }, 49 | "devDependencies": { 50 | "@commit-generator/eslint-config": "workspace:^", 51 | "@commit-generator/typescript-config": "workspace:^", 52 | "@types/node": "^22.10.2", 53 | "eslint": "9.19.0", 54 | "ts-morph": "^25.0.1" 55 | } 56 | } -------------------------------------------------------------------------------- /packages/ai-models/scripts/config/constants.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | 4 | export const constantsPath = fileURLToPath(import.meta.url); 5 | 6 | export const __dirname = path.resolve(path.dirname(constantsPath), '../..'); 7 | 8 | export const excludeModelsFiles = []; 9 | 10 | export const relativeModelsDir = 'src/models'; 11 | export const relativeSchemeOutput = 'src/schemes.ts'; 12 | export const relativeAIModelsOutput = 'src/models/index.ts'; 13 | 14 | export const modelsDir = path.resolve(__dirname, relativeModelsDir); 15 | export const schemeOutput = path.resolve(__dirname, relativeSchemeOutput); 16 | export const aiModelsOutput = path.resolve(__dirname, relativeAIModelsOutput); 17 | -------------------------------------------------------------------------------- /packages/ai-models/scripts/generateAIModels.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { relativeModelsDir } from './config/constants.mjs'; 3 | import { getAIModelsOutput } from './utils/getAIModelsOutput.mjs'; 4 | import { getModelsFiles } from './utils/getModelsFiles.mjs'; 5 | 6 | console.log('📝 Generating AI Models...'); 7 | 8 | const files = getModelsFiles(); 9 | 10 | const imports = files 11 | .map((file) => { 12 | const modelName = file.replace('.ts', ''); 13 | return `import ${modelName} from '${relativeModelsDir.replace('src', '@')}/${modelName}';`; 14 | }) 15 | .join('\n'); 16 | 17 | const modelsObject = files 18 | .map((file) => { 19 | const modelName = file.replace('.ts', ''); 20 | return `${modelName.replace('Model', '').toLowerCase()}: ${modelName},`; 21 | }) 22 | .join('\n '); 23 | 24 | const content = `// AUTO-GENERATED FILE. DO NOT EDIT MANUALLY. 25 | ${imports} 26 | 27 | const aiModels = { 28 | ${modelsObject} 29 | }; 30 | 31 | export default aiModels; 32 | `; 33 | 34 | fs.writeFileSync(getAIModelsOutput(), content, 'utf8'); 35 | 36 | console.log('✅ AI Models generated successfully!'); 37 | -------------------------------------------------------------------------------- /packages/ai-models/scripts/generateDynamicFiles.mjs: -------------------------------------------------------------------------------- 1 | await import('./validateAIModels.mjs'); 2 | import('./generateSchemes.mjs'); 3 | import('./generateAIModels.mjs'); 4 | -------------------------------------------------------------------------------- /packages/ai-models/scripts/generateSchemes.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { relativeModelsDir } from './config/constants.mjs'; 3 | import { getModelsFiles } from './utils/getModelsFiles.mjs'; 4 | import { getSchemeOutput } from './utils/getSchemeOutput.mjs'; 5 | 6 | console.log('📝 Generating schemes...'); 7 | 8 | const files = getModelsFiles(); 9 | 10 | const imports = files 11 | .map((file) => { 12 | const modelName = file.replace('Model.ts', ''); 13 | return `import { ${modelName}Schema, I${modelName}Params } from '${relativeModelsDir.replace('src', '@')}/${file.replace('.ts', '')}';`; 14 | }) 15 | .join('\n'); 16 | 17 | const registry = `export const aiModelSchemes = {\n ${files 18 | .map((file) => { 19 | const modelName = file.replace('Model.ts', ''); 20 | return `${modelName.toLowerCase()}: ${modelName}Schema,`; 21 | }) 22 | .join('\n ')}\n};\n`; 23 | 24 | const types = `export type IAIModelSchemes = {\n ${files 25 | .map((file) => { 26 | const modelName = file.replace('Model.ts', ''); 27 | return `${modelName.toLowerCase()}: I${modelName}Params;`; 28 | }) 29 | .join('\n ')}\n};\n`; 30 | 31 | const content = `// AUTO-GENERATED FILE. DO NOT EDIT MANUALLY. 32 | ${imports} 33 | 34 | ${types} 35 | ${registry}`; 36 | 37 | fs.writeFileSync(getSchemeOutput(), content); 38 | 39 | console.log('✅ Schemes generated successfully!'); 40 | -------------------------------------------------------------------------------- /packages/ai-models/scripts/utils/getAIModelsOutput.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import { aiModelsOutput, constantsPath } from '../config/constants.mjs'; 4 | 5 | export const getAIModelsOutput = () => { 6 | if (!fs.existsSync(path.dirname(aiModelsOutput))) { 7 | console.error( 8 | '❌ AI Models output directory not found. You may have refactored and forgot to update the generation scripts.', 9 | ); 10 | console.error( 11 | `❌ Please update relativeAIModelsOutput in ${constantsPath}`, 12 | ); 13 | process.exit(1); 14 | } 15 | 16 | return aiModelsOutput; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/ai-models/scripts/utils/getModelsFiles.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import { 3 | constantsPath, 4 | excludeModelsFiles, 5 | modelsDir, 6 | } from '../config/constants.mjs'; 7 | 8 | export const getModelsFiles = () => { 9 | if (!fs.existsSync(modelsDir)) { 10 | console.error( 11 | '❌ AI Models Dir not found. You may have refactored and forgot to update the generation scripts.', 12 | ); 13 | console.error(`❌ Please update relativeModelsDir in ${constantsPath}`); 14 | process.exit(1); 15 | } 16 | 17 | return fs 18 | .readdirSync(modelsDir) 19 | .filter( 20 | (file) => file.endsWith('Model.ts') && !excludeModelsFiles.includes(file), 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/ai-models/scripts/utils/getSchemeOutput.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import { constantsPath, schemeOutput } from '../config/constants.mjs'; 4 | 5 | export const getSchemeOutput = () => { 6 | if (!fs.existsSync(path.dirname(schemeOutput))) { 7 | console.error( 8 | '❌ Scheme output directory not found. You may have refactored and forgot to update the generation scripts.', 9 | ); 10 | console.error(`❌ Please update relativeSchemeOutput in ${constantsPath}`); 11 | process.exit(1); 12 | } 13 | 14 | return schemeOutput; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/ai-models/scripts/validateAIModels.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { Project } from 'ts-morph'; 3 | import { __dirname, modelsDir } from './config/constants.mjs'; 4 | import { getModelsFiles } from './utils/getModelsFiles.mjs'; 5 | 6 | console.log('🔍 Starting AI model validation...'); 7 | 8 | const files = getModelsFiles(); 9 | 10 | const project = new Project({ 11 | tsConfigFilePath: path.resolve(__dirname, 'tsconfig.json'), 12 | }); 13 | 14 | const invalidModels = new Set(); 15 | 16 | files.forEach((file) => { 17 | const filePath = path.join(modelsDir, file); 18 | const sourceFile = project.addSourceFileAtPath(filePath); 19 | 20 | const modelName = file.replace('Model.ts', ''); 21 | const expectedSchema = `${modelName}Schema`; 22 | const expectedParams = `I${modelName}Params`; 23 | 24 | const exportedSchemas = sourceFile.getExportedDeclarations(); 25 | const hasSchema = exportedSchemas.has(expectedSchema); 26 | const hasParams = exportedSchemas.has(expectedParams); 27 | 28 | if (!hasSchema) { 29 | console.error( 30 | `❌ ${path.resolve(modelsDir, file)} should export ${expectedSchema}`, 31 | ); 32 | invalidModels.add(file); 33 | } 34 | 35 | if (!hasParams) { 36 | console.error( 37 | `❌ ${path.resolve(modelsDir, file)}should export ${expectedParams}`, 38 | ); 39 | invalidModels.add(file); 40 | } 41 | }); 42 | 43 | if (invalidModels.size > 0) { 44 | console.error( 45 | '🚨 The following models need maintenance. Process interrupted:', 46 | ); 47 | invalidModels.forEach((model) => console.error(` - ${model}`)); 48 | process.exit(1); 49 | } 50 | 51 | console.log('✅ AI model validation completed successfully!'); 52 | -------------------------------------------------------------------------------- /packages/ai-models/src/AIModel.ts: -------------------------------------------------------------------------------- 1 | import AjvSchemeValidator from '@/AjvSchemeValidator'; 2 | import IAIModel from '@/types/IAIModel'; 3 | import { IAIModelParams } from '@/types/IAIModelsParams'; 4 | import ISchemeValidator from '@/types/ISchemeValidator'; 5 | 6 | export default abstract class AIModel implements IAIModel { 7 | protected parameters: ParamsType; 8 | private schemeValidator: ISchemeValidator; 9 | 10 | constructor(parameters: IAIModelParams, scheme: unknown) { 11 | this.parameters = parameters as ParamsType; 12 | this.schemeValidator = new AjvSchemeValidator(); 13 | if (!this.validateParameters(parameters, scheme)) { 14 | throw new Error(`Invalid parameters`); 15 | } 16 | } 17 | 18 | private validateParameters( 19 | parameters: IAIModelParams, 20 | schema: unknown, 21 | ): boolean { 22 | return this.schemeValidator.validate(parameters, schema); 23 | } 24 | 25 | public abstract complete(prompt: string): Promise; 26 | } 27 | -------------------------------------------------------------------------------- /packages/ai-models/src/AjvSchemeValidator.ts: -------------------------------------------------------------------------------- 1 | import ISchemeValidator from '@/types/ISchemeValidator'; 2 | import Ajv, { JSONSchemaType } from 'ajv'; 3 | 4 | export default class AjvSchemeValidator implements ISchemeValidator { 5 | private ajv: Ajv; 6 | 7 | constructor() { 8 | this.ajv = new Ajv(); 9 | } 10 | 11 | validate(values: unknown, scheme: JSONSchemaType): boolean { 12 | const validate = this.ajv.compile(scheme); 13 | return validate(values); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/ai-models/src/factories/createAIModel.ts: -------------------------------------------------------------------------------- 1 | import aiModels from '@/models'; 2 | import { IAIModelParams } from '@/types/IAIModelsParams'; 3 | 4 | export default function createAIModel( 5 | provider: string, 6 | parameters: IAIModelParams, 7 | ) { 8 | const modelClass = 9 | aiModels[provider.toLowerCase() as keyof typeof aiModels] ?? ''; 10 | 11 | if (!modelClass) { 12 | throw new Error(`Unsupported AI model provider: ${provider}`); 13 | } 14 | 15 | return new modelClass(parameters); 16 | } 17 | -------------------------------------------------------------------------------- /packages/ai-models/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as createAIModel } from '@/factories/createAIModel'; 2 | -------------------------------------------------------------------------------- /packages/ai-models/src/models/OllamaModel.ts: -------------------------------------------------------------------------------- 1 | import AIModel from '@/AIModel'; 2 | import { IAIModelParams } from '@/types/IAIModelsParams'; 3 | import { Ollama } from 'ollama'; 4 | 5 | export type IOllamaParams = { 6 | model: string; 7 | temperature?: string; 8 | url?: string; 9 | }; 10 | 11 | export const OllamaSchema = { 12 | type: 'object', 13 | properties: { 14 | model: { type: 'string' }, 15 | temperature: { type: 'string' }, 16 | url: { type: 'string' }, 17 | }, 18 | required: ['model'], 19 | additionalProperties: false, 20 | }; 21 | 22 | export default class OllamaModel extends AIModel { 23 | private _model: Ollama; 24 | 25 | constructor(parameters: IAIModelParams) { 26 | super(parameters, OllamaSchema); 27 | this._model = new Ollama({ 28 | host: this.parameters.url ?? 'http://127.0.0.1:11434', 29 | }); 30 | } 31 | 32 | public async complete(prompt: string): Promise { 33 | const completion = await this._model.chat({ 34 | options: { 35 | temperature: Number(this.parameters.temperature), 36 | }, 37 | model: this.parameters.model, 38 | messages: [{ role: 'user', content: prompt }], 39 | }); 40 | 41 | return completion.message.content; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/ai-models/src/models/OpenAIModel.ts: -------------------------------------------------------------------------------- 1 | import AIModel from '@/AIModel'; 2 | import { IAIModelParams } from '@/types/IAIModelsParams'; 3 | import OpenAI from 'openai'; 4 | 5 | export type IOpenAIParams = { 6 | key: string; 7 | model?: string; 8 | }; 9 | 10 | export const OpenAISchema = { 11 | type: 'object', 12 | properties: { 13 | key: { type: 'string' }, 14 | model: { type: 'string' }, 15 | }, 16 | required: ['key'], 17 | additionalProperties: false, 18 | }; 19 | 20 | export default class OpenAIModel extends AIModel { 21 | private model: OpenAI; 22 | 23 | constructor(private params: IAIModelParams) { 24 | super(params, OpenAISchema); 25 | this.model = new OpenAI({ apiKey: this.parameters.key }); 26 | } 27 | 28 | async complete(prompt: string): Promise { 29 | const completion = await this.model.chat.completions.create({ 30 | model: this.parameters.model ?? 'gpt-4o-mini', 31 | messages: [ 32 | { 33 | role: 'user', 34 | content: prompt, 35 | }, 36 | ], 37 | }); 38 | 39 | return completion.choices[0].message.content || ''; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/ai-models/src/types/IAIModel.ts: -------------------------------------------------------------------------------- 1 | export default interface IAIModel { 2 | complete(prompt: string): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /packages/ai-models/src/types/IAIModelsParams.ts: -------------------------------------------------------------------------------- 1 | export type IAIModelParams = { [key: string]: unknown }; 2 | -------------------------------------------------------------------------------- /packages/ai-models/src/types/ISchemeValidator.ts: -------------------------------------------------------------------------------- 1 | export default interface ISchemeValidator { 2 | validate(values: unknown, scheme: unknown): boolean; 3 | } 4 | -------------------------------------------------------------------------------- /packages/ai-models/tests/unit/AIModel.spec.ts: -------------------------------------------------------------------------------- 1 | import AIModel from '@/AIModel'; 2 | import { IAIModelParams } from '@/types/IAIModelsParams'; 3 | import { JSONSchemaType } from 'ajv'; 4 | import { describe, expect, it } from 'vitest'; 5 | 6 | class TestModel extends AIModel { 7 | constructor(parameters: IAIModelParams, schema: unknown) { 8 | super(parameters, schema); 9 | } 10 | 11 | public async complete(): Promise { 12 | // Implementation not relevant for this test 13 | return ''; 14 | } 15 | } 16 | 17 | describe('AIModel', () => { 18 | const schema: JSONSchemaType<{ temperature: number; maxTokens: number }> = { 19 | type: 'object', 20 | properties: { 21 | temperature: { type: 'number', minimum: 0, maximum: 1 }, 22 | maxTokens: { type: 'number', minimum: 1 }, 23 | }, 24 | required: ['temperature', 'maxTokens'], 25 | additionalProperties: false, 26 | }; 27 | 28 | it('should create an AIModel with valid parameters', () => { 29 | const validParams = { temperature: 0.7, maxTokens: 100 }; 30 | expect(() => new TestModel(validParams, schema)).not.toThrow(); 31 | }); 32 | 33 | it('should throw an error for invalid parameters', () => { 34 | const invalidParams = { temperature: 1.5, maxTokens: -10 }; 35 | expect(() => new TestModel(invalidParams, schema)).toThrow(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/ai-models/tests/unit/AjvSchemeValidator.spec.ts: -------------------------------------------------------------------------------- 1 | import AjvSchemeValidator from '@/AjvSchemeValidator'; 2 | import { JSONSchemaType } from 'ajv'; 3 | import { describe, expect, it } from 'vitest'; 4 | 5 | describe('AjvSchemeValidator', () => { 6 | const sut = new AjvSchemeValidator(); 7 | 8 | it('should validate a correct object', () => { 9 | const schema: JSONSchemaType<{ name: string; age: number }> = { 10 | type: 'object', 11 | properties: { 12 | name: { type: 'string' }, 13 | age: { type: 'number' }, 14 | }, 15 | required: ['name', 'age'], 16 | additionalProperties: false, 17 | }; 18 | 19 | const validObject = { name: 'John', age: 30 }; 20 | expect(sut.validate(validObject, schema)).toBe(true); 21 | }); 22 | 23 | it('should invalidate an incorrect object', () => { 24 | const schema: JSONSchemaType<{ name: string; age: number }> = { 25 | type: 'object', 26 | properties: { 27 | name: { type: 'string' }, 28 | age: { type: 'number' }, 29 | }, 30 | required: ['name', 'age'], 31 | additionalProperties: false, 32 | }; 33 | 34 | const invalidObject = { name: 'John' }; 35 | expect(sut.validate(invalidObject, schema)).toBe(false); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/ai-models/tests/unit/models/OllamaModel.spec.ts: -------------------------------------------------------------------------------- 1 | import OllamaModel from '@/models/OllamaModel'; 2 | import { Ollama } from 'ollama'; 3 | import { beforeEach, describe, expect, it, vi } from 'vitest'; 4 | 5 | vi.mock('ollama', () => ({ 6 | Ollama: vi.fn().mockImplementation(() => ({ 7 | chat: vi.fn().mockResolvedValue({ 8 | message: { content: 'Mocked response' }, 9 | }), 10 | })), 11 | })); 12 | 13 | describe('OllamaModel', () => { 14 | beforeEach(() => { 15 | vi.resetModules(); 16 | }); 17 | 18 | describe('constructor', () => { 19 | it('should validate parameters', () => { 20 | const validParameters = [ 21 | { 22 | model: 'gemma2:2b', 23 | }, 24 | { 25 | model: 'gemma2:2b', 26 | temperature: '0.8', 27 | }, 28 | { 29 | model: 'gemma2:2b', 30 | temperature: '0.8', 31 | url: 'https://example.com/model', 32 | }, 33 | { 34 | model: 'gemma2:2b', 35 | url: 'https://example.com/model', 36 | }, 37 | ]; 38 | 39 | const invalidParameters = [ 40 | {}, 41 | { temperature: '0.8' }, 42 | { url: 'https://example.com' }, 43 | ]; 44 | 45 | validParameters.forEach((params) => { 46 | expect(() => new OllamaModel(params)).not.toThrow(); 47 | }); 48 | 49 | invalidParameters.forEach((params) => { 50 | expect(() => new OllamaModel(params)).toThrow(); 51 | }); 52 | }); 53 | 54 | it('should create an instance of Ollama with the correct host', () => { 55 | new OllamaModel({ 56 | model: 'gemma2:2b', 57 | }); 58 | 59 | expect(Ollama).toHaveBeenCalledWith({ host: 'http://127.0.0.1:11434' }); 60 | 61 | new OllamaModel({ 62 | model: 'gemma2:2b', 63 | url: 'https://example', 64 | }); 65 | 66 | expect(Ollama).toHaveBeenCalledWith({ host: 'https://example' }); 67 | }); 68 | }); 69 | 70 | describe('complete', () => { 71 | it('should return a response without making a real request', async () => { 72 | const mockParams = { 73 | model: 'gemma2:2b', 74 | temperature: '0.7', 75 | url: 'http://127.0.0.1:11434', 76 | }; 77 | 78 | const sut = new OllamaModel(mockParams); 79 | const response = await sut.complete('Test prompt'); 80 | 81 | expect(response).toBe('Mocked response'); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /packages/ai-models/tests/unit/models/OpenAIModel.spec.ts: -------------------------------------------------------------------------------- 1 | import OpenAIModel from '@/models/OpenAIModel'; 2 | import { beforeEach, describe, expect, it, vi } from 'vitest'; 3 | 4 | vi.mock('openai', () => ({ 5 | default: vi.fn().mockImplementation(() => ({ 6 | chat: { 7 | completions: { 8 | create: vi.fn().mockResolvedValue({ 9 | choices: [{ message: { content: 'Mocked response' } }], 10 | }), 11 | }, 12 | }, 13 | })), 14 | })); 15 | 16 | describe('OpenAIModel', () => { 17 | beforeEach(() => { 18 | vi.resetModules(); 19 | }); 20 | 21 | describe('constructor', () => { 22 | it('should validate parameters', () => { 23 | const validParameters = [ 24 | { 25 | key: 'my-key', 26 | }, 27 | { 28 | key: 'my-key', 29 | model: 'gpt-4o-mini', 30 | }, 31 | ]; 32 | 33 | const invalidParameters = [{}]; 34 | 35 | validParameters.forEach((params) => { 36 | expect(() => new OpenAIModel(params)).not.toThrow(); 37 | }); 38 | 39 | invalidParameters.forEach((params) => { 40 | expect(() => new OpenAIModel(params)).toThrow(); 41 | }); 42 | }); 43 | }); 44 | 45 | describe('complete', () => { 46 | it('should return a response without making a real request', async () => { 47 | const mockParams = { 48 | key: 'my-key', 49 | }; 50 | 51 | const sut = new OpenAIModel(mockParams); 52 | const response = await sut.complete('Test prompt'); 53 | 54 | expect(response).toBe('Mocked response'); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/ai-models/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["tests"] 4 | } -------------------------------------------------------------------------------- /packages/ai-models/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@commit-generator/typescript-config/base.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "rootDirs": ["src", "tests"], 6 | "outDir": "dist", 7 | "paths": { 8 | "@/tests/*": ["../tests/*"], 9 | "@/*": ["*"] 10 | }, 11 | }, 12 | "include": ["src", "tests"], 13 | "exclude": [] 14 | } -------------------------------------------------------------------------------- /packages/ai-models/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/schemes.ts'], 5 | clean: true, 6 | tsconfig: 'tsconfig.build.json', 7 | treeshake: true, 8 | minify: true, 9 | shims: true, 10 | dts: true, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/commit-history/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/commit-history 2 | 3 | ## 1.0.1 4 | 5 | ### Patch Changes 6 | 7 | - update local package to use workspace 8 | 9 | ## 1.0.0 10 | 11 | ### Major Changes 12 | 13 | - - Initial release of `@commit-generator/commit-history` package. 14 | - Implemented `add` method for adding commit messages to history. 15 | - Implemented `get` method for retrieving the last _N_ commit messages. 16 | -------------------------------------------------------------------------------- /packages/commit-history/README.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/commit-history 2 | 3 | This package provides a utility for **managing commit message history** in the **Commit Generator** project. It allows storing and retrieving generated commit messages. 4 | 5 | ## Installation 6 | To use the commit history utility in your project, install the package: 7 | 8 | ```bash 9 | pnpm install @commit-generator/commit-history 10 | ``` 11 | 12 | ## Usage 13 | After installation, you can use the commit history utility in your project. 14 | 15 | 1. Adding a Commit to History. 16 | To store a generated commit message, use the `add` method: 17 | 18 | ```javascript 19 | import { CommitHistory } from '@commit-generator/commit-history'; 20 | 21 | const history = new CommitHistory("./commit-history.log"); 22 | 23 | await history.add("feat: add new feature"); 24 | ``` 25 | 26 | 2. Retrieving Commit History. 27 | To retrieve the last *N* commit messages, use the `get` method: 28 | 29 | ```javascript 30 | const lastCommits = await history.get(5); 31 | console.log(lastCommits); 32 | ``` 33 | 34 | ## License 35 | This package is licensed under the MIT License. -------------------------------------------------------------------------------- /packages/commit-history/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslintNode from '@commit-generator/eslint-config/node.js'; 2 | 3 | export default [...eslintNode]; 4 | -------------------------------------------------------------------------------- /packages/commit-history/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@commit-generator/commit-history", 3 | "version": "1.0.1", 4 | "description": "A utility for managing commit message history.", 5 | "author": "JulioC090", 6 | "files": [ 7 | "dist" 8 | ], 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "import": "./dist/index.mjs", 13 | "require": "./dist/index.js", 14 | "default": "./dist/index.js" 15 | } 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/JulioC090/commit-generator.git", 20 | "directory": "packages/commit-history" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/JulioC090/commit-generator/issues" 24 | }, 25 | "license": "MIT", 26 | "scripts": { 27 | "build": "tsup --format cjs,esm", 28 | "lint": "eslint . --report-unused-disable-directives --max-warnings 0", 29 | "lint:fix": "eslint . --fix", 30 | "test": "vitest run", 31 | "test:watch": "vitest watch", 32 | "test:coverage": "vitest run --coverage", 33 | "typecheck": "tsc --noEmit -p ." 34 | }, 35 | "devDependencies": { 36 | "@commit-generator/eslint-config": "workspace:^", 37 | "@commit-generator/typescript-config": "workspace:^", 38 | "@types/node": "^22.10.2", 39 | "eslint": "9.19.0" 40 | } 41 | } -------------------------------------------------------------------------------- /packages/commit-history/src/CommitHistory.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | 3 | export default class CommitHistory { 4 | constructor(private historyPath: string) {} 5 | 6 | public async add(generatedCommit: string): Promise { 7 | await fs.appendFile(this.historyPath, `${generatedCommit}\n`, 'utf8'); 8 | } 9 | 10 | public async get(numberOfLines: number): Promise> { 11 | try { 12 | const historyContent = await fs.readFile(this.historyPath, 'utf8'); 13 | 14 | if (numberOfLines < 0) { 15 | numberOfLines = 1; 16 | } 17 | 18 | const reversedHistory = historyContent 19 | .split('\n') 20 | .filter((line) => line.trim() !== '') 21 | .reverse() 22 | .slice(0, numberOfLines); 23 | 24 | return reversedHistory; 25 | } catch { 26 | return []; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/commit-history/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CommitHistory } from '@/CommitHistory'; 2 | -------------------------------------------------------------------------------- /packages/commit-history/tests/CommitHistory.spec.ts: -------------------------------------------------------------------------------- 1 | import CommitHistory from '@/CommitHistory'; 2 | import fs from 'node:fs/promises'; 3 | import path from 'node:path'; 4 | import { beforeEach, describe, expect, it, vi } from 'vitest'; 5 | 6 | vi.mock('node:fs/promises'); 7 | 8 | const mockHistoryPath = path.join(__dirname, 'history'); 9 | 10 | describe('CommitHistory', () => { 11 | let sut: CommitHistory; 12 | 13 | beforeEach(async () => { 14 | vi.resetAllMocks(); 15 | sut = new CommitHistory(mockHistoryPath); 16 | }); 17 | 18 | describe('add', () => { 19 | it('should append a line to the history file', async () => { 20 | const commitMessage = 'Test commit'; 21 | 22 | await sut.add(commitMessage); 23 | 24 | expect(fs.appendFile).toHaveBeenCalledWith( 25 | mockHistoryPath, 26 | commitMessage + '\n', 27 | 'utf8', 28 | ); 29 | }); 30 | 31 | it('should append multiple lines to the history file', async () => { 32 | const commitMessages = ['First commit', 'Second commit']; 33 | 34 | for (const msg of commitMessages) { 35 | await sut.add(msg); 36 | } 37 | 38 | expect(fs.appendFile).toHaveBeenCalledTimes(2); 39 | }); 40 | }); 41 | 42 | describe('get', () => { 43 | it('should return reversed lines from the file', async () => { 44 | const fileContent = `First line\nSecond line\nThird line`; 45 | vi.mocked(fs.readFile).mockResolvedValue(fileContent); 46 | 47 | const result = await sut.get(2); 48 | 49 | expect(fs.readFile).toHaveBeenCalledWith(mockHistoryPath, 'utf8'); 50 | expect(result).toEqual(['Third line', 'Second line']); 51 | }); 52 | 53 | it('should return only 1 line if numberOfLines is negative', async () => { 54 | const fileContent = `First line\nSecond line\nThird line`; 55 | vi.mocked(fs.readFile).mockResolvedValue(fileContent); 56 | 57 | const result = await sut.get(-5); 58 | 59 | expect(result).toEqual(['Third line']); 60 | }); 61 | 62 | it('should return all lines if numberOfLines is greater than the number of lines in the file', async () => { 63 | const fileContent = `First line\nSecond line\nThird line`; 64 | vi.mocked(fs.readFile).mockResolvedValue(fileContent); 65 | 66 | const result = await sut.get(10); 67 | 68 | expect(result).toEqual(['Third line', 'Second line', 'First line']); 69 | }); 70 | 71 | it('should return an empty array if the file is empty', async () => { 72 | vi.mocked(fs.readFile).mockResolvedValue(''); 73 | 74 | const result = await sut.get(5); 75 | 76 | expect(result).toEqual([]); 77 | }); 78 | 79 | it('should return an empty array if reading the file fails', async () => { 80 | vi.mocked(fs.readFile).mockRejectedValue(new Error('File not found')); 81 | 82 | const result = await sut.get(3); 83 | 84 | expect(result).toEqual([]); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /packages/commit-history/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["tests"] 4 | } -------------------------------------------------------------------------------- /packages/commit-history/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@commit-generator/typescript-config/base.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "rootDirs": ["src", "tests"], 6 | "outDir": "dist", 7 | "paths": { 8 | "@/tests/*": ["../tests/*"], 9 | "@/*": ["*"] 10 | }, 11 | }, 12 | "include": ["src", "tests"], 13 | "exclude": [] 14 | } -------------------------------------------------------------------------------- /packages/commit-history/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | clean: true, 6 | tsconfig: 'tsconfig.build.json', 7 | treeshake: true, 8 | minify: true, 9 | shims: true, 10 | dts: true, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/config/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/config 2 | 3 | ## 1.0.1 4 | 5 | ### Patch Changes 6 | 7 | - update local package to use workspace 8 | 9 | ## 1.0.0 10 | 11 | ### Major Changes 12 | 13 | - - Initial release of `@commit-generator/config` package. 14 | - Added utility for managing configurations from multiple sources. 15 | - Supports file, environment variables, and command-line arguments as configuration sources. 16 | - Configurations are defined using JSON schema for flexible structure and validation. 17 | -------------------------------------------------------------------------------- /packages/config/README.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/config 2 | 3 | This package provides a flexible and extensible configuration management utility for the **Commit Generator project**. It supports multiple configuration sources, including files, environment variables, and command-line arguments. 4 | 5 | ## Installation 6 | 7 | To use the configuration management utility in your project, install the package as a dependency: 8 | 9 | ```bash 10 | pnpm install @commit-generator/config 11 | ``` 12 | 13 | ## Usage 14 | 15 | 1. Define Configuration Sources 16 | First, define where the configuration values should be loaded from. The example below sets up three sources: 17 | - **File** (`.myConfigFile.json`): Stores persistent configurations. 18 | - **Environment variables** (`MY_PREFIX_*`): Loads configurations from environment variables. 19 | - **Command-line arguments** (`--key=value`): Allows overriding configurations via CLI.` 20 | 21 | ```javascript 22 | import createConfigManager, { 23 | IConfigDefinitions, 24 | IConfigSource, 25 | } from '@commit-generator/config'; 26 | 27 | const sources: Array = [ 28 | { name: 'local', type: 'file', path: '.myConfigFile.json' }, 29 | { name: 'env', type: 'env', prefix: 'MY_PREFIX' }, 30 | { name: 'arg', type: 'arg' }, 31 | ]; 32 | 33 | export type IConfigType = { 34 | provider: string; 35 | }; 36 | 37 | const configDefinitions: IConfigDefinitions = { 38 | type: 'object', 39 | properties: { 40 | provider: { type: 'string' }, 41 | }, 42 | required: ['provider'], 43 | additionalProperties: false, 44 | }; 45 | 46 | const configManager = createConfigManager({ 47 | sources, 48 | definitions: configDefinitions, 49 | }); 50 | 51 | export default configManager; 52 | ``` 53 | 54 | 2. Load Configuration 55 | Once the sources are defined, you can load the configuration dynamically: 56 | 57 | ```javascript 58 | import configManager from 'path/to/myConfigManager'; 59 | 60 | const config = await configManager.loadConfig(); 61 | 62 | console.log(config); 63 | ``` 64 | 65 | 3. Set a Configuration Value 66 | You can modify a configuration setting and store it in a specific source (e.g., `local` file): 67 | 68 | ```javascript 69 | import configManager from 'path/to/myConfigManager'; 70 | 71 | await configManager.set('provider', 'some_provider', 'local'); 72 | ``` 73 | 74 | 4. Unset a Configuration Value 75 | To remove a configuration key from a specific source: 76 | 77 | ```javascript 78 | import configManager from 'path/to/myConfigManager'; 79 | 80 | await configManager.unset('provider', 'local'); 81 | ``` 82 | 83 | ## License 84 | This package is licensed under the MIT License. -------------------------------------------------------------------------------- /packages/config/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslintNode from '@commit-generator/eslint-config/node.js'; 2 | 3 | export default [...eslintNode]; 4 | -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@commit-generator/config", 3 | "version": "1.0.1", 4 | "description": "A configuration management utility.", 5 | "author": "JulioC090", 6 | "files": [ 7 | "dist" 8 | ], 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "import": "./dist/index.mjs", 13 | "require": "./dist/index.js", 14 | "default": "./dist/index.js" 15 | } 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/JulioC090/commit-generator.git", 20 | "directory": "packages/config" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/JulioC090/commit-generator/issues" 24 | }, 25 | "license": "MIT", 26 | "scripts": { 27 | "build": "tsup --format cjs,esm", 28 | "lint": "eslint . --report-unused-disable-directives --max-warnings 0", 29 | "lint:fix": "eslint . --fix", 30 | "test": "vitest run", 31 | "test:watch": "vitest watch", 32 | "test:coverage": "vitest run --coverage", 33 | "typecheck": "tsc --noEmit -p ." 34 | }, 35 | "devDependencies": { 36 | "@commit-generator/eslint-config": "workspace:^", 37 | "@commit-generator/typescript-config": "workspace:^", 38 | "@types/lodash": "^4.17.15", 39 | "@types/node": "^22.10.2", 40 | "eslint": "9.19.0" 41 | }, 42 | "dependencies": { 43 | "ajv": "^8.17.1", 44 | "lodash": "^4.17.21" 45 | } 46 | } -------------------------------------------------------------------------------- /packages/config/src/ConfigManager.ts: -------------------------------------------------------------------------------- 1 | import ConfigSourceManager from '@/ConfigSourceManager'; 2 | import ConfigValidator from '@/ConfigValidator'; 3 | import IConfig from '@/types/IConfig'; 4 | import { IConfigValue } from '@/types/IConfigValue'; 5 | import _ from 'lodash'; 6 | 7 | interface ConfigManagerProps { 8 | configSourceManager: ConfigSourceManager; 9 | configValidator: ConfigValidator; 10 | } 11 | 12 | export default class ConfigManager 13 | implements IConfig 14 | { 15 | private allConfigsLoaded = new Map(); 16 | private config: IConfigValue = {}; 17 | private isLoaded = false; 18 | 19 | private configSourceManager: ConfigSourceManager; 20 | private configValidator: ConfigValidator; 21 | 22 | constructor({ 23 | configSourceManager, 24 | configValidator, 25 | }: ConfigManagerProps) { 26 | this.configSourceManager = configSourceManager; 27 | this.configValidator = configValidator; 28 | } 29 | 30 | async loadConfig(): Promise { 31 | if (this.isLoaded) return this.config as IConfigType; 32 | 33 | const sources = this.configSourceManager.getSources(); 34 | 35 | for (const source of sources) { 36 | const config = await this.configSourceManager.load(source.name); 37 | this.config = { ...this.config, ...config }; 38 | } 39 | 40 | this.isLoaded = true; 41 | 42 | const validation = this.configValidator.validate(this.config); 43 | 44 | if (!validation.valid) { 45 | console.error('Config loaded with errors:'); 46 | validation.errors!.forEach((error) => console.error(error.message)); 47 | } 48 | 49 | return this.config as IConfigType; 50 | } 51 | 52 | async get(key: string) { 53 | if (this.isLoaded) { 54 | return _.get(this.config, key) ?? undefined; 55 | } 56 | 57 | const sources = this.configSourceManager.getSources(); 58 | 59 | for (let i = sources.length - 1; i >= 0; i--) { 60 | const source = sources[i]; 61 | 62 | if (!this.allConfigsLoaded.has(source.name)) { 63 | const config = await this.configSourceManager.load(source.name); 64 | this.allConfigsLoaded.set(source.name, config); 65 | } 66 | 67 | const cachedConfig = this.allConfigsLoaded.get(source.name)!; 68 | const value = _.get(cachedConfig, key, undefined); 69 | 70 | if (value !== undefined) { 71 | return value; 72 | } 73 | } 74 | 75 | return undefined; 76 | } 77 | 78 | async set(key: string, value: unknown, sourceName: string) { 79 | const validation = this.configValidator.validateKey(key as string, value); 80 | 81 | if (!validation.valid) { 82 | console.error('Config loaded with errors:'); 83 | validation.errors!.forEach((error) => console.error(error.message)); 84 | return; 85 | } 86 | 87 | const fileConfig = await this.configSourceManager.load(sourceName); 88 | 89 | _.set(fileConfig, key, value); 90 | 91 | await this.configSourceManager.write(sourceName, fileConfig); 92 | } 93 | 94 | async unset(key: string, sourceName: string) { 95 | const fileConfig = await this.configSourceManager.load(sourceName); 96 | 97 | _.unset(fileConfig, key); 98 | 99 | await this.configSourceManager.write(sourceName, fileConfig); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /packages/config/src/ConfigSourceManager.ts: -------------------------------------------------------------------------------- 1 | import IConfigLoader from '@/types/IConfigLoader'; 2 | import { IConfigValue } from '@/types/IConfigValue'; 3 | import { ISource } from '@/types/ISource'; 4 | 5 | interface ConfigSourceManagerProps { 6 | sources: Array; 7 | loaders: Record; 8 | } 9 | 10 | const defaultSources: Array = [{ name: 'env', type: 'env' }]; 11 | 12 | export default class ConfigSourceManager { 13 | private sources: Array; 14 | private loaders: Record; 15 | 16 | constructor({ sources = defaultSources, loaders }: ConfigSourceManagerProps) { 17 | this.sources = sources; 18 | this.loaders = loaders; 19 | 20 | this.validateSources(sources); 21 | } 22 | 23 | public getSource(sourceName: string): ISource | undefined { 24 | return this.sources.find((source) => source.name === sourceName); 25 | } 26 | 27 | public getSources(): Array { 28 | return this.sources; 29 | } 30 | 31 | public setSources(sources: Array): void { 32 | this.validateSources(sources); 33 | this.sources = sources; 34 | } 35 | 36 | private validateSources(sources: Array) { 37 | if (sources.length === 0) 38 | throw new Error('Config Error: No sources specified'); 39 | 40 | sources.forEach((source) => { 41 | const loader = this.loaders[source.type]; 42 | 43 | if (!loader) 44 | throw new Error(`No loader defined for source type "${source.type}". `); 45 | 46 | loader.validate(source); 47 | }); 48 | } 49 | 50 | private ensureSourceExists(sourceName: string): ISource { 51 | const source = this.getSource(sourceName); 52 | if (!source) { 53 | throw new Error( 54 | `Config Error: No source with name "${sourceName}" found`, 55 | ); 56 | } 57 | return source; 58 | } 59 | 60 | public isWritableSource(sourceName: string): boolean { 61 | const source = this.getSource(sourceName); 62 | 63 | if (!source) return false; 64 | 65 | const loader = this.loaders[source.type]; 66 | 67 | return loader.isWritable; 68 | } 69 | 70 | public async load(sourceName: string): Promise { 71 | const source = this.ensureSourceExists(sourceName); 72 | 73 | const loader = this.loaders[source.type]; 74 | 75 | return await loader.load(source); 76 | } 77 | 78 | public async write(sourceName: string, config: IConfigValue): Promise { 79 | const source = this.ensureSourceExists(sourceName); 80 | 81 | if (!this.isWritableSource(sourceName)) { 82 | throw new Error( 83 | `Config Error: The source "${sourceName}" is not writable`, 84 | ); 85 | } 86 | 87 | const loader = this.loaders[source.type]; 88 | 89 | await loader.write!(source, config); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/config/src/ConfigValidator.ts: -------------------------------------------------------------------------------- 1 | import Ajv, { ErrorObject, JSONSchemaType, ValidateFunction } from 'ajv'; 2 | 3 | interface ConfigValidatorProps { 4 | definitions: JSONSchemaType; 5 | } 6 | 7 | interface ValidateOutput { 8 | valid: boolean; 9 | errors: Array; 10 | } 11 | 12 | export default class ConfigValidator { 13 | private definitions: JSONSchemaType; 14 | private ajv: Ajv; 15 | private validateFunction: ValidateFunction; 16 | 17 | constructor({ definitions }: ConfigValidatorProps) { 18 | this.definitions = definitions; 19 | this.ajv = new Ajv(); 20 | this.validateFunction = this.ajv.compile(definitions); 21 | } 22 | 23 | public validate(config: unknown): ValidateOutput { 24 | const isValid = this.validateFunction(config); 25 | return { 26 | valid: isValid, 27 | errors: this.validateFunction.errors ? this.validateFunction.errors : [], 28 | }; 29 | } 30 | 31 | public validateKey(key: string, value: unknown): ValidateOutput { 32 | const path = key.toString().split('.'); 33 | 34 | let keySchema: JSONSchemaType | undefined = this.definitions; 35 | 36 | for (const segment of path) { 37 | if ( 38 | keySchema && 39 | 'properties' in keySchema && 40 | keySchema.properties && 41 | segment in keySchema.properties 42 | ) { 43 | keySchema = keySchema.properties[segment] as JSONSchemaType; 44 | } else { 45 | throw new Error( 46 | `The key "${String(key)}" is not defined in the schema.`, 47 | ); 48 | } 49 | } 50 | 51 | const validate = this.ajv.compile(keySchema); 52 | 53 | const isValid = validate(value); 54 | return { 55 | valid: isValid, 56 | errors: validate.errors ? validate.errors : [], 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/config/src/factories/createConfigManager.ts: -------------------------------------------------------------------------------- 1 | import ConfigManager from '@/ConfigManager'; 2 | import ConfigSourceManager from '@/ConfigSourceManager'; 3 | import ConfigValidator from '@/ConfigValidator'; 4 | import ArgConfigLoader from '@/loaders/ArgConfigLoader'; 5 | import EnvConfigLoader from '@/loaders/EnvConfigLoader'; 6 | import FileConfigLoader from '@/loaders/FileConfigLoader'; 7 | import { IConfigDefinitions } from '@/types/IConfigDefinitions'; 8 | import { ISource } from '@/types/ISource'; 9 | 10 | interface createConfigManagerProps { 11 | sources: Array; 12 | definitions: IConfigDefinitions; 13 | } 14 | 15 | export default function createConfigManager({ 16 | sources, 17 | definitions, 18 | }: createConfigManagerProps): ConfigManager { 19 | const fileConfigLoader = new FileConfigLoader(); 20 | const envConfigLoader = new EnvConfigLoader(); 21 | const argConfigLoader = new ArgConfigLoader(); 22 | 23 | const configSourceManager = new ConfigSourceManager({ 24 | sources, 25 | loaders: { 26 | file: fileConfigLoader, 27 | env: envConfigLoader, 28 | arg: argConfigLoader, 29 | }, 30 | }); 31 | 32 | const configValidator = new ConfigValidator({ definitions }); 33 | 34 | const configManager = new ConfigManager({ 35 | configSourceManager, 36 | configValidator, 37 | }); 38 | 39 | return configManager; 40 | } 41 | -------------------------------------------------------------------------------- /packages/config/src/formatConfigValue.ts: -------------------------------------------------------------------------------- 1 | export default function formatConfigValue(value: string) { 2 | return value.includes(',') ? value.split(',') : value; 3 | } 4 | -------------------------------------------------------------------------------- /packages/config/src/index.ts: -------------------------------------------------------------------------------- 1 | import createConfigManager from '@/factories/createConfigManager'; 2 | 3 | export default createConfigManager; 4 | 5 | export { default as ConfigManager } from '@/ConfigManager'; 6 | export { default as formatConfigValue } from '@/formatConfigValue'; 7 | export { IConfigDefinitions } from '@/types/IConfigDefinitions'; 8 | export { ISource as IConfigSource } from '@/types/ISource'; 9 | -------------------------------------------------------------------------------- /packages/config/src/loaders/ArgConfigLoader.ts: -------------------------------------------------------------------------------- 1 | import formatConfigValue from '@/formatConfigValue'; 2 | import IConfigLoader from '@/types/IConfigLoader'; 3 | import { IConfigValue } from '@/types/IConfigValue'; 4 | import _ from 'lodash'; 5 | 6 | interface ArgConfigLoaderProps { 7 | arg?: Array; 8 | } 9 | 10 | export default class ArgConfigLoader implements IConfigLoader { 11 | public readonly isWritable: boolean = false; 12 | private arg: Array; 13 | 14 | constructor(props?: ArgConfigLoaderProps) { 15 | this.arg = props?.arg || process.argv.slice(2); 16 | } 17 | 18 | public validate(): void { 19 | return; 20 | } 21 | 22 | async load(): Promise { 23 | const config = Object.create(null); 24 | 25 | this.arg.forEach((arg) => { 26 | const match = arg.match(/^--([^=]+)=(.+)$/); 27 | if (match) { 28 | const [, key, value] = match; 29 | _.set(config, key, formatConfigValue(value)); 30 | } 31 | }); 32 | 33 | return config; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/config/src/loaders/EnvConfigLoader.ts: -------------------------------------------------------------------------------- 1 | import formatConfigValue from '@/formatConfigValue'; 2 | import IConfigLoader from '@/types/IConfigLoader'; 3 | import { IConfigValue } from '@/types/IConfigValue'; 4 | import { ISource } from '@/types/ISource'; 5 | import _ from 'lodash'; 6 | 7 | interface EnvConfigLoaderProps { 8 | env?: NodeJS.ProcessEnv; 9 | prefix?: string; 10 | } 11 | 12 | export default class EnvConfigLoader implements IConfigLoader { 13 | public readonly isWritable: boolean = false; 14 | private env: NodeJS.ProcessEnv; 15 | 16 | constructor(props?: EnvConfigLoaderProps) { 17 | this.env = props?.env || process.env; 18 | } 19 | 20 | public validate(): void { 21 | return; 22 | } 23 | 24 | public async load(source: ISource): Promise { 25 | const conf = Object.create(null); 26 | 27 | const prefixRegex = source.prefix 28 | ? new RegExp(`^${source.prefix}`, 'i') 29 | : null; 30 | 31 | for (const [envKey, envValue] of Object.entries(this.env)) { 32 | if (!envValue) continue; 33 | 34 | if (source.prefix && !prefixRegex?.test(envKey)) { 35 | continue; 36 | } 37 | 38 | let key = source.prefix ? envKey.slice(source.prefix.length) : envKey; 39 | key = key.toLowerCase().replace(/_/g, '.'); 40 | 41 | _.set(conf, key, formatConfigValue(envValue)); 42 | } 43 | 44 | return conf; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/config/src/loaders/FileConfigLoader.ts: -------------------------------------------------------------------------------- 1 | import IConfigLoader from '@/types/IConfigLoader'; 2 | import { IConfigValue } from '@/types/IConfigValue'; 3 | import { ISource } from '@/types/ISource'; 4 | import fs from 'node:fs/promises'; 5 | 6 | export default class FileConfigLoader implements IConfigLoader { 7 | public readonly isWritable: boolean = true; 8 | 9 | public validate(source: ISource): void { 10 | if (!source.path) 11 | throw new Error( 12 | `Config Error: Source of type "file" must have a "path": ${source.name}`, 13 | ); 14 | } 15 | 16 | public async load(source: ISource): Promise { 17 | try { 18 | const fileContent = await fs.readFile(source.path!, { encoding: 'utf8' }); 19 | return JSON.parse(fileContent) as IConfigValue; 20 | } catch (error) { 21 | console.error( 22 | `Failed to load or parse config file at ${source.path!}`, 23 | error, 24 | ); 25 | return {}; 26 | } 27 | } 28 | 29 | public async write(source: ISource, config: IConfigValue): Promise { 30 | await fs.writeFile(source.path!, JSON.stringify(config, null, 2), { 31 | encoding: 'utf8', 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/config/src/types/IConfig.ts: -------------------------------------------------------------------------------- 1 | export default interface IConfig { 2 | loadConfig(): Promise; 3 | get(key: string): Promise; 4 | set(key: string, value: unknown, sourceName: string): Promise; 5 | unset(key: string, sourceName: string): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /packages/config/src/types/IConfigDefinitions.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchemaType } from 'ajv'; 2 | 3 | export type IConfigDefinitions = JSONSchemaType; 4 | -------------------------------------------------------------------------------- /packages/config/src/types/IConfigLoader.ts: -------------------------------------------------------------------------------- 1 | import { IConfigValue } from '@/types/IConfigValue'; 2 | import { ISource } from '@/types/ISource'; 3 | 4 | export default interface IConfigLoader { 5 | isWritable: boolean; 6 | validate(source: ISource): void; 7 | load(source: ISource): Promise; 8 | write?(source: ISource, config: IConfigValue): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /packages/config/src/types/IConfigValue.ts: -------------------------------------------------------------------------------- 1 | export type IConfigValue = { [key: string]: unknown }; 2 | -------------------------------------------------------------------------------- /packages/config/src/types/ISource.ts: -------------------------------------------------------------------------------- 1 | export type ISource = { 2 | name: string; 3 | type: string; 4 | path?: string; 5 | prefix?: string; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/config/tests/unit/ConfigSourceManager.spec.ts: -------------------------------------------------------------------------------- 1 | import ConfigSourceManager from '@/ConfigSourceManager'; 2 | import ArgConfigLoader from '@/loaders/ArgConfigLoader'; 3 | import EnvConfigLoader from '@/loaders/EnvConfigLoader'; 4 | import FileConfigLoader from '@/loaders/FileConfigLoader'; 5 | import { ISource } from '@/types/ISource'; 6 | import { beforeEach, describe, expect, it, vi } from 'vitest'; 7 | 8 | const mockFileConfigLoader = { 9 | isWritable: true, 10 | validate: vi.fn(), 11 | load: vi.fn(), 12 | write: vi.fn(), 13 | } as unknown as FileConfigLoader; 14 | 15 | const mockEnvConfigLoader = { 16 | isWritable: false, 17 | validate: vi.fn(), 18 | load: vi.fn(), 19 | } as unknown as EnvConfigLoader; 20 | 21 | const mockArgConfigLoader = { 22 | isWritable: false, 23 | validate: vi.fn(), 24 | load: vi.fn(), 25 | } as unknown as ArgConfigLoader; 26 | 27 | const mockSources: Array = [ 28 | { name: 'fileSource', type: 'file', path: '/path/to/config.json' }, 29 | { name: 'envSource', type: 'env' }, 30 | { name: 'argSource', type: 'arg' }, 31 | ]; 32 | 33 | describe('ConfigSourceManager', () => { 34 | let sut: ConfigSourceManager; 35 | 36 | beforeEach(() => { 37 | sut = new ConfigSourceManager({ 38 | sources: mockSources, 39 | loaders: { 40 | file: mockFileConfigLoader, 41 | env: mockEnvConfigLoader, 42 | arg: mockArgConfigLoader, 43 | }, 44 | }); 45 | 46 | vi.resetAllMocks(); 47 | }); 48 | 49 | describe('constructor', () => { 50 | it('should validate and set sources in constructor', () => { 51 | expect(sut.getSources()).toEqual(mockSources); 52 | }); 53 | 54 | it('should throw an error if no sources are specified', () => { 55 | expect( 56 | () => 57 | new ConfigSourceManager({ 58 | sources: [], 59 | loaders: { 60 | file: mockFileConfigLoader, 61 | env: mockEnvConfigLoader, 62 | arg: mockArgConfigLoader, 63 | }, 64 | }), 65 | ).toThrow('Config Error: No sources specified'); 66 | }); 67 | 68 | it('should throw an error if no loader is defined for source type', () => { 69 | expect( 70 | () => 71 | new ConfigSourceManager({ 72 | sources: [{ name: 'invalidSource', type: 'invalidType' }], 73 | loaders: { 74 | file: mockFileConfigLoader, 75 | env: mockEnvConfigLoader, 76 | arg: mockArgConfigLoader, 77 | }, 78 | }), 79 | ).toThrow('No loader defined for source type "invalidType".'); 80 | }); 81 | }); 82 | 83 | describe('getSource', () => { 84 | it('should return a source by name', () => { 85 | const source = sut.getSource('fileSource'); 86 | expect(source).toEqual(mockSources[0]); 87 | }); 88 | 89 | it('should return undefined for a non-existent source', () => { 90 | const source = sut.getSource('nonExistentSource'); 91 | expect(source).toBeUndefined(); 92 | }); 93 | }); 94 | 95 | describe('getSources', () => { 96 | it('should return the list of sources', () => { 97 | const sources = sut.getSources(); 98 | expect(sources).toEqual(mockSources); 99 | }); 100 | }); 101 | 102 | describe('setSources', () => { 103 | it('should update the list of sources', () => { 104 | const newSources: Array = [ 105 | { name: 'newFileSource', type: 'file', path: '/new/path/config.json' }, 106 | ]; 107 | 108 | sut.setSources(newSources); 109 | const sources = sut.getSources(); 110 | 111 | expect(sources).toEqual(newSources); 112 | }); 113 | 114 | it('should throw an error if no sources are provided', () => { 115 | expect(() => sut.setSources([])).toThrowError( 116 | 'Config Error: No sources specified', 117 | ); 118 | }); 119 | }); 120 | 121 | describe('isWritableSource', () => { 122 | it('should correctly identify writable sources', () => { 123 | expect(sut.isWritableSource('fileSource')).toBe(true); 124 | expect(sut.isWritableSource('envSource')).toBe(false); 125 | }); 126 | 127 | it('should return false if source does not exist', () => { 128 | expect(sut.isWritableSource('invalidSourceName')).toBe(false); 129 | }); 130 | }); 131 | 132 | describe('load', () => { 133 | it('should throw when source does not exist', async () => { 134 | const nonExistentSourceName = 'nonExistentSource'; 135 | 136 | await expect(sut.load(nonExistentSourceName)).rejects.toThrowError( 137 | `Config Error: No source with name "${nonExistentSourceName}" found`, 138 | ); 139 | }); 140 | 141 | it('should load config from a file source', async () => { 142 | const expectedConfig = { openaiKey: 'fileValue' }; 143 | vi.mocked(mockFileConfigLoader.load).mockResolvedValue(expectedConfig); 144 | 145 | const config = await sut.load('fileSource'); 146 | expect(config).toEqual(expectedConfig); 147 | expect(mockFileConfigLoader.load).toHaveBeenCalledWith({ 148 | name: 'fileSource', 149 | path: '/path/to/config.json', 150 | type: 'file', 151 | }); 152 | }); 153 | 154 | it('should load config from an env source', async () => { 155 | const expectedConfig = { openaiKey: 'fileValue' }; 156 | vi.mocked(mockEnvConfigLoader.load).mockResolvedValue(expectedConfig); 157 | 158 | const config = await sut.load('envSource'); 159 | expect(config).toEqual(expectedConfig); 160 | expect(mockEnvConfigLoader.load).toHaveBeenCalled(); 161 | }); 162 | 163 | it('should load config from an arg source', async () => { 164 | const expectedConfig = { openaiKey: 'fileValue' }; 165 | vi.mocked(mockArgConfigLoader.load).mockResolvedValue(expectedConfig); 166 | 167 | const config = await sut.load('argSource'); 168 | expect(config).toEqual(expectedConfig); 169 | expect(mockArgConfigLoader.load).toHaveBeenCalled(); 170 | }); 171 | }); 172 | 173 | describe('write', () => { 174 | it('should throw when source does not exist', async () => { 175 | const configToWrite = { openaiKey: 'value' }; 176 | const nonExistentSourceName = 'nonExistentSource'; 177 | 178 | await expect( 179 | sut.write(nonExistentSourceName, configToWrite), 180 | ).rejects.toThrowError( 181 | `Config Error: No source with name "${nonExistentSourceName}" found`, 182 | ); 183 | }); 184 | 185 | it('should throw an error if source is not writable on write', async () => { 186 | const configToWrite = { openaiKey: 'value' }; 187 | 188 | await expect(sut.write('envSource', configToWrite)).rejects.toThrow( 189 | 'Config Error: The source "envSource" is not writable', 190 | ); 191 | }); 192 | 193 | it('should write config to a file source', async () => { 194 | const configToWrite = { openaiKey: 'value' }; 195 | await sut.write('fileSource', configToWrite); 196 | 197 | expect(mockFileConfigLoader.write).toHaveBeenCalledWith( 198 | { 199 | name: 'fileSource', 200 | path: '/path/to/config.json', 201 | type: 'file', 202 | }, 203 | configToWrite, 204 | ); 205 | }); 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /packages/config/tests/unit/ConfigValidator.spec.ts: -------------------------------------------------------------------------------- 1 | import ConfigValidator from '@/ConfigValidator'; 2 | import { JSONSchemaType } from 'ajv'; 3 | import { describe, expect, it } from 'vitest'; 4 | 5 | interface MyConfig { 6 | port: number; 7 | debug: boolean; 8 | name: string; 9 | job: { 10 | title: string; 11 | }; 12 | } 13 | 14 | const schema: JSONSchemaType = { 15 | type: 'object', 16 | properties: { 17 | port: { type: 'integer' }, 18 | debug: { type: 'boolean' }, 19 | name: { type: 'string' }, 20 | job: { 21 | type: 'object', 22 | properties: { 23 | title: { type: 'string' }, 24 | }, 25 | required: ['title'], 26 | }, 27 | }, 28 | required: ['port', 'debug', 'name'], 29 | additionalProperties: false, 30 | }; 31 | 32 | describe('ConfigValidator', () => { 33 | const sut = new ConfigValidator({ definitions: schema }); 34 | 35 | describe('validate', () => { 36 | it('should validate a correct config object', () => { 37 | const config = { port: 8080, debug: true, name: 'MyApp' }; 38 | const result = sut.validate(config); 39 | expect(result).toEqual({ valid: true, errors: [] }); 40 | }); 41 | 42 | it('should return an error when validating an object with invalid values', () => { 43 | const config = { port: 'wrong', debug: true, name: 'MyApp' }; 44 | const result = sut.validate(config); 45 | expect(result.valid).toBe(false); 46 | expect(result.errors).toBeInstanceOf(Array); 47 | expect(result.errors.length).toBeGreaterThan(0); 48 | }); 49 | 50 | it('should return an error when validating an object with missing properties', () => { 51 | const config = { port: 8080, debug: true }; 52 | const result = sut.validate(config); 53 | expect(result.valid).toBe(false); 54 | expect(result.errors).toBeInstanceOf(Array); 55 | expect(result.errors.length).toBeGreaterThan(0); 56 | }); 57 | }); 58 | 59 | describe('validateKey', () => { 60 | it('should validate a correct key-value pair', () => { 61 | expect(sut.validateKey('port', 3000)).toEqual({ 62 | valid: true, 63 | errors: [], 64 | }); 65 | expect(sut.validateKey('debug', false)).toEqual({ 66 | valid: true, 67 | errors: [], 68 | }); 69 | expect(sut.validateKey('name', 'TestApp')).toEqual({ 70 | valid: true, 71 | errors: [], 72 | }); 73 | expect(sut.validateKey('job.title', 'TestApp')).toEqual({ 74 | valid: true, 75 | errors: [], 76 | }); 77 | }); 78 | 79 | it('should return an error when validating an invalid key-value pair', () => { 80 | expect(sut.validateKey('port', 'wrong')).toMatchObject({ valid: false }); 81 | expect(sut.validateKey('debug', 'true')).toMatchObject({ valid: false }); 82 | expect(sut.validateKey('name', 123)).toMatchObject({ valid: false }); 83 | expect(sut.validateKey('job', {})).toMatchObject({ valid: false }); 84 | }); 85 | 86 | it('should throw an error when trying to validate a non-existent key', () => { 87 | expect(() => sut.validateKey('nonexistent', 'value')).toThrow( 88 | 'The key "nonexistent" is not defined in the schema.', 89 | ); 90 | 91 | expect(() => sut.validateKey('nonexistent.nonexistent', 'value')).toThrow( 92 | 'The key "nonexistent.nonexistent" is not defined in the schema.', 93 | ); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /packages/config/tests/unit/formatConfigValue.spec.ts: -------------------------------------------------------------------------------- 1 | import formatConfigValue from '@/formatConfigValue'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('formatConfigValue', () => { 5 | it('should return an array when value contains a comma', () => { 6 | expect(formatConfigValue('a,b,c')).toEqual(['a', 'b', 'c']); 7 | }); 8 | 9 | it('should return the original string when no comma is present', () => { 10 | expect(formatConfigValue('abc')).toBe('abc'); 11 | }); 12 | 13 | it('should handle empty string', () => { 14 | expect(formatConfigValue('')).toBe(''); 15 | }); 16 | 17 | it('should handle single character string without comma', () => { 18 | expect(formatConfigValue('a')).toBe('a'); 19 | }); 20 | 21 | it('should handle string with only commas', () => { 22 | expect(formatConfigValue(',,,')).toEqual(['', '', '', '']); 23 | }); 24 | 25 | it('should handle leading and trailing commas', () => { 26 | expect(formatConfigValue(',a,b,')).toEqual(['', 'a', 'b', '']); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/config/tests/unit/loaders/ArgConfigLoader.spec.ts: -------------------------------------------------------------------------------- 1 | import ArgConfigLoader from '@/loaders/ArgConfigLoader'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('ArgConfigLoader', () => { 5 | describe('constructor', () => { 6 | it('should parse arguments from process.argv correctly', async () => { 7 | const originalArgv = process.argv; 8 | process.argv = ['node', 'script.js', '--openaiKey=arg_openai_key']; 9 | 10 | const sut = new ArgConfigLoader(); 11 | const config = await sut.load(); 12 | 13 | expect(config).toEqual({ openaiKey: 'arg_openai_key' }); 14 | 15 | process.argv = originalArgv; 16 | }); 17 | }); 18 | 19 | describe('validate', () => { 20 | it('should validate', async () => { 21 | const sut = new ArgConfigLoader(); 22 | await sut.validate(); 23 | }); 24 | }); 25 | 26 | describe('load', () => { 27 | it('should parse single argument correctly', async () => { 28 | const args = ['--openaiKey=arg_openai_key']; 29 | const sut = new ArgConfigLoader({ arg: args }); 30 | 31 | const config = await sut.load(); 32 | expect(config).toEqual({ openaiKey: 'arg_openai_key' }); 33 | }); 34 | 35 | it('should parse multiple arguments correctly', async () => { 36 | const args = ['--openaiKey=arg_openai_key', '--otherKey=12345,12345']; 37 | const sut = new ArgConfigLoader({ arg: args }); 38 | 39 | const config = await sut.load(); 40 | expect(config).toEqual({ 41 | openaiKey: 'arg_openai_key', 42 | otherKey: ['12345', '12345'], 43 | }); 44 | }); 45 | 46 | it('should return an empty object when no arguments match', async () => { 47 | const args = ['randomArg', 'anotherArg']; 48 | const sut = new ArgConfigLoader({ arg: args }); 49 | 50 | const config = await sut.load(); 51 | expect(config).toEqual({}); 52 | }); 53 | 54 | it('should ignore malformed arguments', async () => { 55 | const args = [ 56 | '--openaiKey=arg_openai_key', 57 | 'malformedArg', 58 | '--validKey=value', 59 | ]; 60 | const loader = new ArgConfigLoader({ arg: args }); 61 | 62 | const config = await loader.load(); 63 | expect(config).toEqual({ 64 | openaiKey: 'arg_openai_key', 65 | validKey: 'value', 66 | }); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /packages/config/tests/unit/loaders/EnvConfigLoader.spec.ts: -------------------------------------------------------------------------------- 1 | import EnvConfigLoader from '@/loaders/EnvConfigLoader'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('EnvConfigLoader', async () => { 5 | describe('constructor', () => { 6 | it('should parse arguments from process.env correctly', async () => { 7 | const originalEnv = process.env; 8 | process.env = { OPENAI_KEY: 'env_openai_key' }; 9 | 10 | const sut = new EnvConfigLoader(); 11 | const config = await sut.load({ name: 'env-source', type: 'env' }); 12 | 13 | expect(config).toEqual({ openai: { key: 'env_openai_key' } }); 14 | 15 | process.env = originalEnv; 16 | }); 17 | }); 18 | 19 | describe('validate', () => { 20 | it('should validate', async () => { 21 | const sut = new EnvConfigLoader(); 22 | await sut.validate(); 23 | }); 24 | }); 25 | 26 | describe('load', async () => { 27 | it('should load variables with the correct prefix', async () => { 28 | const sut = new EnvConfigLoader({ 29 | env: { 30 | COMMIT_GEN_CONFIG_OPENAI_KEY: 'env_openai_key', 31 | COMMIT_GEN_CONFIG_EXCLUDE_FILES: 'env_file1,env_file2', 32 | }, 33 | }); 34 | 35 | const result = await sut.load({ 36 | name: 'env-source', 37 | type: 'env', 38 | prefix: 'commit_gen_config_', 39 | }); 40 | 41 | expect(result).toEqual({ 42 | openai: { key: 'env_openai_key' }, 43 | exclude: { files: ['env_file1', 'env_file2'] }, 44 | }); 45 | }); 46 | 47 | it('should load all variables when no prefix is set', async () => { 48 | const mockEnv = { 49 | commit_gen_config_openaiKey: 'openai-key-value', 50 | commit_gen_config_excludeFiles: 'node_modules,.git', 51 | APPSECRET: 'supersecretvalue', 52 | }; 53 | 54 | const sut = new EnvConfigLoader({ env: mockEnv }); 55 | 56 | const result = (await sut.load({ name: 'env-source', type: 'env' })) as { 57 | [key: string]: unknown; 58 | }; 59 | 60 | expect(result.openaiKey).toBeUndefined(); 61 | expect(result.appsecret).toBe('supersecretvalue'); 62 | }); 63 | 64 | it('should ignore irrelevant environment variables', async () => { 65 | const sut = new EnvConfigLoader({ 66 | env: { UNRELATED_ENV_VAR: 'value' }, 67 | }); 68 | 69 | const result = await sut.load({ 70 | name: 'env-source', 71 | type: 'env', 72 | prefix: 'commit_gen_config_', 73 | }); 74 | 75 | expect(result).toEqual({}); 76 | }); 77 | 78 | it('should format the env variable keys using dots to represent object properties', async () => { 79 | const sut = new EnvConfigLoader({ 80 | env: { OPENAI_KEY: 'openai-key-value' }, 81 | }); 82 | 83 | const result = (await sut.load({ name: 'env-source', type: 'env' })) as { 84 | openai: { key: string }; 85 | }; 86 | 87 | expect(result.openai.key).toBe('openai-key-value'); 88 | }); 89 | 90 | it('should skip empty env variables with prefix', async () => { 91 | const mockEnv = { 92 | commit_gen_config_emptyKey: '', 93 | }; 94 | 95 | const sut = new EnvConfigLoader({ 96 | env: mockEnv, 97 | }); 98 | const result = (await sut.load({ 99 | name: 'env-source', 100 | type: 'env', 101 | prefix: 'commit_gen_config_', 102 | })) as { 103 | [key: string]: unknown; 104 | }; 105 | 106 | expect(result.emptyKey).toBeUndefined(); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /packages/config/tests/unit/loaders/FileConfigLoader.spec.ts: -------------------------------------------------------------------------------- 1 | import FileConfigLoader from '@/loaders/FileConfigLoader'; 2 | import { ISource } from '@/types/ISource'; 3 | import fs from 'node:fs/promises'; 4 | import { describe, expect, it, vi } from 'vitest'; 5 | 6 | const sut = new FileConfigLoader(); 7 | 8 | const mockFilePath = 'path/to/file'; 9 | const mockFileContent = JSON.stringify({ 10 | openaiKey: 'file_key', 11 | }); 12 | 13 | vi.mock('node:fs/promises'); 14 | 15 | describe('FileConfigLoader', () => { 16 | describe('validate', () => { 17 | it('should throw an error if source of type "file" has no path', () => { 18 | const invalidSource: ISource = { name: 'invalidSource', type: 'file' }; 19 | const sut = new FileConfigLoader(); 20 | 21 | expect(() => sut.validate(invalidSource)).toThrowError( 22 | 'Config Error: Source of type "file" must have a "path": invalidSource', 23 | ); 24 | }); 25 | 26 | it('should not throw an error if source of type "file" has a path', () => { 27 | const source: ISource = { 28 | name: 'testSource', 29 | type: 'file', 30 | path: '/config.json', 31 | }; 32 | 33 | const sut = new FileConfigLoader(); 34 | 35 | expect(() => sut.validate(source)).not.toThrow(); 36 | }); 37 | }); 38 | 39 | describe('load', () => { 40 | it('should load and parse the config file correctly', async () => { 41 | vi.mocked(fs.readFile).mockResolvedValue(mockFileContent); 42 | const config = await sut.load({ path: mockFilePath } as ISource); 43 | 44 | expect(config).toEqual(JSON.parse(mockFileContent)); 45 | expect(fs.readFile).toHaveBeenCalledWith(mockFilePath, { 46 | encoding: 'utf8', 47 | }); 48 | }); 49 | 50 | it('should return an empty object on error', async () => { 51 | vi.mocked(fs.readFile).mockRejectedValue(new Error('Permission denied')); 52 | const config = await sut.load({ path: mockFilePath } as ISource); 53 | expect(config).toEqual({}); 54 | }); 55 | }); 56 | 57 | describe('write', () => { 58 | it('should write the config to the file correctly', async () => { 59 | vi.mocked(fs.writeFile).mockResolvedValue(); 60 | await sut.write({ path: mockFilePath } as ISource, { openaiKey: 'key' }); 61 | expect(fs.writeFile).toHaveBeenCalledWith( 62 | mockFilePath, 63 | JSON.stringify({ openaiKey: 'key' }, null, 2), 64 | { 65 | encoding: 'utf8', 66 | }, 67 | ); 68 | }); 69 | 70 | it('should throw an error if the file cannot be written', async () => { 71 | vi.mocked(fs.writeFile).mockRejectedValue(new Error('Permission denied')); 72 | expect( 73 | async () => 74 | await sut.write({ path: mockFilePath } as ISource, { 75 | openaiKey: 'key', 76 | }), 77 | ).rejects.toThrow('Permission denied'); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /packages/config/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["tests"] 4 | } -------------------------------------------------------------------------------- /packages/config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@commit-generator/typescript-config/base.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "rootDirs": ["src", "tests"], 6 | "outDir": "dist", 7 | "paths": { 8 | "@/tests/*": ["../tests/*"], 9 | "@/*": ["*"] 10 | }, 11 | }, 12 | "include": ["src", "tests"], 13 | "exclude": [] 14 | } -------------------------------------------------------------------------------- /packages/config/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | clean: true, 6 | tsconfig: 'tsconfig.build.json', 7 | treeshake: true, 8 | minify: true, 9 | shims: true, 10 | dts: true, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/eslint-config/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/eslint-config 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - Fixed export issue where certain modules were not being exported correctly. 8 | -------------------------------------------------------------------------------- /packages/eslint-config/README.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/eslint-config 2 | 3 | This package contains a shared **ESLint** configuration for the **Commit Generator** project. 4 | 5 | ## Installation 6 | To use the ESLint configuration in your project, install the package as a development dependency: 7 | 8 | ```bash 9 | pnpm install --save-dev @commit-generator/eslint-config 10 | ``` 11 | 12 | ## Usage 13 | After installation, extend the ESLint configuration in your project. 14 | 15 | 1. Create or update your `eslint.config.mjs` file with the following configuration: 16 | 17 | ```javascript 18 | import eslintNode from '@commit-generator/eslint-config/node.js'; 19 | 20 | export default [...eslintNode]; 21 | ``` 22 | 23 | 2. Customizing the Configuration 24 | If you'd like to customize the configuration, create your own `eslint.config.mjs` and extend the default settings: 25 | 26 | ```javascript 27 | import eslintNode from '@commit-generator/eslint-config/node.js'; 28 | 29 | export default [ 30 | ...eslintNode, 31 | myConfigs 32 | ]; 33 | ``` 34 | 35 | ## License 36 | This package is licensed under the MIT License. -------------------------------------------------------------------------------- /packages/eslint-config/node.js: -------------------------------------------------------------------------------- 1 | import eslintJs from "@eslint/js"; 2 | import prettierConfig from "eslint-config-prettier"; 3 | import prettierPlugin from "eslint-plugin-prettier/recommended"; 4 | import turbo from 'eslint-plugin-turbo'; 5 | import globals from "globals"; 6 | import tsEslint from "typescript-eslint"; 7 | 8 | export default [ 9 | // Recommended JavaScript configuration 10 | eslintJs.configs.recommended, 11 | 12 | // Recommended TypeScript configuration 13 | ...tsEslint.configs.recommended, 14 | 15 | // Turbo-specific rules for monorepos 16 | turbo.configs['flat/recommended'], 17 | 18 | // Disables rules that conflict with Prettier 19 | prettierConfig, 20 | 21 | // Recommended rules from the Prettier plugin 22 | prettierPlugin, 23 | 24 | // Language and global environment settings 25 | { 26 | languageOptions: { 27 | ecmaVersion: 2022, 28 | sourceType: "module", 29 | globals: { 30 | ...globals.node, 31 | ...globals.es2022, 32 | }, 33 | }, 34 | }, 35 | 36 | // Directories ignored by ESLint 37 | { 38 | ignores: ["node_modules", "dist"], 39 | }, 40 | 41 | // Prettier custom rules 42 | { 43 | rules: { 44 | "prettier/prettier": [ 45 | "error", 46 | { 47 | singleQuote: true, 48 | trailingComma: "all", 49 | bracketSpacing: true, 50 | bracketSameLine: false, 51 | tabWidth: 2, 52 | endOfLine: "auto", 53 | }, 54 | ], 55 | }, 56 | }, 57 | ]; 58 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@commit-generator/eslint-config", 3 | "version": "0.1.0", 4 | "description": "ESLint configuration package for the Commit Generator project", 5 | "author": "JulioC090", 6 | "type": "module", 7 | "files": [ 8 | "node.js" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/JulioC090/commit-generator.git", 13 | "directory": "packages/eslint-config" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/JulioC090/commit-generator/issues" 17 | }, 18 | "license": "MIT", 19 | "devDependencies": { 20 | "@eslint/js": "^9.19.0", 21 | "eslint-config-prettier": "^10.0.1", 22 | "eslint-plugin-prettier": "^5.2.3", 23 | "eslint-plugin-turbo": "^2.4.0", 24 | "globals": "^15.14.0", 25 | "prettier": "^3.4.2", 26 | "typescript-eslint": "^8.22.0" 27 | } 28 | } -------------------------------------------------------------------------------- /packages/git/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/git 2 | 3 | ## 1.0.1 4 | 5 | ### Patch Changes 6 | 7 | - update local package to use workspace 8 | 9 | ## 1.0.0 10 | 11 | ### Major Changes 12 | 13 | - - Initial release of `@commit-generator/git` package. 14 | - Added essential Git utilities for managing repositories and commits. 15 | -------------------------------------------------------------------------------- /packages/git/README.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/git 2 | 3 | This package provides Git integration utilities for the **Commit Generator project**. It enables checking repository status, managing commits, viewing logs, and handling diffs. 4 | 5 | ## Installation 6 | 7 | To use this package in your project, install it as a dependency: 8 | 9 | ```bash 10 | pnpm install @commit-generator/git 11 | ``` 12 | 13 | ## Usage 14 | 15 | 1. Checking if the current directory is a Git repository 16 | 17 | ```javascript 18 | import { git } from '@commit-generator/git'; 19 | 20 | if (git.isRepository()) { 21 | console.log('This is a Git repository'); 22 | } else { 23 | console.log('Not a Git repository'); 24 | } 25 | ``` 26 | 27 | 2. Viewing unstaged/staged changes with `diff` 28 | 29 | ```javascript 30 | const diffOutput = git.diff({ staged: false }); 31 | console.log(diffOutput); 32 | ``` 33 | 34 | 3. Committing changes 35 | 36 | ```javascript 37 | git.commit('Initial commit'); 38 | ``` 39 | 40 | 4. Amending the last commit 41 | 42 | ```javascript 43 | git.amend('Updated commit message'); 44 | ``` 45 | 46 | 5. Viewing commit logs 47 | 48 | ```javascript 49 | const logs = git.log(5); 50 | console.log(logs); 51 | ``` 52 | 53 | 6. Adding all changes to staging 54 | 55 | ```javascript 56 | git.add(); 57 | ``` 58 | 59 | ## License 60 | 61 | This package is licensed under the MIT License. -------------------------------------------------------------------------------- /packages/git/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslintNode from '@commit-generator/eslint-config/node.js'; 2 | 3 | export default [...eslintNode]; 4 | -------------------------------------------------------------------------------- /packages/git/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@commit-generator/git", 3 | "version": "1.0.1", 4 | "description": "Git integration utilities", 5 | "author": "JulioC090", 6 | "files": [ 7 | "dist" 8 | ], 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "import": "./dist/index.mjs", 13 | "require": "./dist/index.js", 14 | "default": "./dist/index.js" 15 | } 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/JulioC090/commit-generator.git", 20 | "directory": "packages/git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/JulioC090/commit-generator/issues" 24 | }, 25 | "license": "MIT", 26 | "scripts": { 27 | "build": "tsup --format cjs,esm", 28 | "lint": "eslint . --report-unused-disable-directives --max-warnings 0", 29 | "lint:fix": "eslint . --fix", 30 | "test": "vitest run", 31 | "test:watch": "vitest watch", 32 | "test:coverage": "vitest run --coverage", 33 | "typecheck": "tsc --noEmit -p ." 34 | }, 35 | "devDependencies": { 36 | "@commit-generator/eslint-config": "workspace:^", 37 | "@commit-generator/typescript-config": "workspace:^", 38 | "@types/node": "^22.10.2", 39 | "eslint": "9.19.0" 40 | } 41 | } -------------------------------------------------------------------------------- /packages/git/src/Git.ts: -------------------------------------------------------------------------------- 1 | import buildDiffArgs from '@/buildDiffArgs'; 2 | import IDiffOptions from '@/types/IDiffOptions'; 3 | import IGit from '@/types/IGit'; 4 | import { execSync } from 'node:child_process'; 5 | 6 | export default class Git implements IGit { 7 | private _isRepository?: boolean; 8 | 9 | public isRepository(): boolean { 10 | if (this._isRepository) return this._isRepository; 11 | 12 | try { 13 | const output = execSync('git rev-parse --is-inside-work-tree --quiet', { 14 | encoding: 'utf-8', 15 | }); 16 | return output.trim() === 'true'; 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | } catch (e) { 19 | return false; 20 | } 21 | } 22 | 23 | public diff(options: IDiffOptions = { staged: false }): string { 24 | if (!this.isRepository()) { 25 | throw new Error( 26 | 'Error: The current directory is not a valid Git repository.', 27 | ); 28 | } 29 | 30 | try { 31 | return execSync(`git diff ${buildDiffArgs(options)}`, { 32 | encoding: 'utf-8', 33 | }).trim(); 34 | } catch (e) { 35 | console.log(e); 36 | return ''; 37 | } 38 | } 39 | 40 | public commit(commitMessage: string): void { 41 | if (!this.isRepository()) { 42 | throw new Error( 43 | 'Error: The current directory is not a valid Git repository.', 44 | ); 45 | } 46 | 47 | execSync('git commit -F -', { input: commitMessage }); 48 | } 49 | 50 | public amend(commitMessage: string): void { 51 | if (!this.isRepository()) { 52 | throw new Error( 53 | 'Error: The current directory is not a valid Git repository.', 54 | ); 55 | } 56 | 57 | execSync('git commit --amend -F -', { input: commitMessage }); 58 | } 59 | 60 | public log(amount: number): string { 61 | try { 62 | return execSync(`git log -${amount} --pretty=format:%s`, { 63 | encoding: 'utf-8', 64 | }).trim(); 65 | } catch { 66 | return ''; 67 | } 68 | } 69 | 70 | public add(): void { 71 | if (!this.isRepository()) { 72 | throw new Error( 73 | 'Error: The current directory is not a valid Git repository.', 74 | ); 75 | } 76 | 77 | execSync('git add .'); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/git/src/buildDiffArgs.ts: -------------------------------------------------------------------------------- 1 | import { emptyTree } from '@/constants'; 2 | import IDiffOptions from '@/types/IDiffOptions'; 3 | import { execSync } from 'node:child_process'; 4 | 5 | export default function buildDiffArgs(options: IDiffOptions) { 6 | const diffArgs: Array = []; 7 | 8 | if (options.staged) { 9 | diffArgs.push('--cached --staged'); 10 | } 11 | 12 | if (options.lastCommit) { 13 | if (options.staged) { 14 | throw new Error('Cannot use both lastCommit and staged together'); 15 | } 16 | 17 | const commitList = execSync('git rev-list --max-count=2 HEAD', { 18 | encoding: 'utf8', 19 | }) 20 | .split('\n') 21 | .filter(Boolean); 22 | 23 | if (commitList.length === 0) { 24 | throw new Error('No commits were found'); 25 | } else if (commitList.length === 1) { 26 | diffArgs.push(`${emptyTree} ${commitList[0]}`); 27 | } else { 28 | diffArgs.push(`${commitList[1]} ${commitList[0]}`); 29 | } 30 | } 31 | 32 | if (options.excludeFiles && options.excludeFiles.length > 0) { 33 | diffArgs.push('-- .'); 34 | diffArgs.push(...options.excludeFiles.map((file) => `:(exclude)${file}`)); 35 | } 36 | 37 | return diffArgs.join(' ').trim(); 38 | } 39 | -------------------------------------------------------------------------------- /packages/git/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const emptyTree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; 2 | -------------------------------------------------------------------------------- /packages/git/src/index.ts: -------------------------------------------------------------------------------- 1 | import Git from '@/Git'; 2 | 3 | export const git = new Git(); 4 | 5 | export { default as IDiffOptions } from '@/types/IDiffOptions'; 6 | export { default as IGit } from '@/types/IGit'; 7 | -------------------------------------------------------------------------------- /packages/git/src/types/IDiffOptions.ts: -------------------------------------------------------------------------------- 1 | export default interface IDiffOptions { 2 | staged?: boolean; 3 | excludeFiles?: Array; 4 | lastCommit?: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /packages/git/src/types/IGit.ts: -------------------------------------------------------------------------------- 1 | import IDiffOptions from '@/types/IDiffOptions'; 2 | 3 | export default interface IGit { 4 | isRepository(): boolean; 5 | diff(options: IDiffOptions): string; 6 | commit(message: string): void; 7 | amend(message: string): void; 8 | log(amount: number): string; 9 | } 10 | -------------------------------------------------------------------------------- /packages/git/tests/unit/buildDiffArgs.spec.ts: -------------------------------------------------------------------------------- 1 | import buildDiffArgs from '@/buildDiffArgs'; 2 | import { emptyTree } from '@/constants'; 3 | import { execSync } from 'node:child_process'; 4 | import { beforeEach, describe, expect, it, vi } from 'vitest'; 5 | 6 | vi.mock('node:child_process', () => ({ 7 | execSync: vi.fn(), 8 | })); 9 | 10 | describe('buildDiffArgs', () => { 11 | beforeEach(() => { 12 | vi.resetAllMocks(); 13 | }); 14 | 15 | it('should return empty string when no options are provided', () => { 16 | expect(buildDiffArgs({})).toBe(''); 17 | }); 18 | 19 | it('should include --cached when staged is true', () => { 20 | expect(buildDiffArgs({ staged: true })).toBe('--cached --staged'); 21 | }); 22 | 23 | it('should throw an error when lastCommit and staged are both true', () => { 24 | expect(() => buildDiffArgs({ lastCommit: true, staged: true })).toThrow( 25 | 'Cannot use both lastCommit and staged together', 26 | ); 27 | }); 28 | 29 | it('should throw an error when lastCommit is true and no commits exist', () => { 30 | vi.mocked(execSync).mockReturnValueOnce(''); 31 | 32 | expect(() => buildDiffArgs({ lastCommit: true })).toThrow( 33 | 'No commits were found', 34 | ); 35 | }); 36 | 37 | it('should compare the only commit with the empty tree when only one commit exists', () => { 38 | vi.mocked(execSync).mockReturnValueOnce('abcd1234\n'); 39 | 40 | expect(buildDiffArgs({ lastCommit: true })).toBe(`${emptyTree} abcd1234`); 41 | }); 42 | 43 | it('should compare last two commits when there are two or more commits', () => { 44 | vi.mocked(execSync).mockReturnValueOnce('abcd1234\nefgh5678\n'); 45 | 46 | expect(buildDiffArgs({ lastCommit: true })).toBe('efgh5678 abcd1234'); 47 | }); 48 | 49 | it('should exclude files when excludeFiles is provided', () => { 50 | expect(buildDiffArgs({ excludeFiles: ['file1.txt', 'file2.js'] })).toBe( 51 | '-- . :(exclude)file1.txt :(exclude)file2.js', 52 | ); 53 | }); 54 | 55 | it('should combine all options correctly', () => { 56 | expect( 57 | buildDiffArgs({ 58 | staged: true, 59 | excludeFiles: ['file1.txt', 'file2.js'], 60 | }), 61 | ).toBe('--cached --staged -- . :(exclude)file1.txt :(exclude)file2.js'); 62 | 63 | vi.mocked(execSync).mockReturnValueOnce('abcd1234\nefgh5678\n'); 64 | 65 | expect( 66 | buildDiffArgs({ 67 | lastCommit: true, 68 | excludeFiles: ['file1.txt', 'file2.js'], 69 | }), 70 | ).toBe('efgh5678 abcd1234 -- . :(exclude)file1.txt :(exclude)file2.js'); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /packages/git/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["tests"] 4 | } -------------------------------------------------------------------------------- /packages/git/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@commit-generator/typescript-config/base.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "rootDirs": ["src", "tests"], 6 | "outDir": "dist", 7 | "paths": { 8 | "@/tests/*": ["../tests/*"], 9 | "@/*": ["*"] 10 | }, 11 | }, 12 | "include": ["src", "tests"], 13 | "exclude": [] 14 | } -------------------------------------------------------------------------------- /packages/git/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | clean: true, 6 | tsconfig: 'tsconfig.build.json', 7 | treeshake: true, 8 | minify: true, 9 | shims: true, 10 | dts: true, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/prompt-parser/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/prompt-parser 2 | 3 | ## 1.0.1 4 | 5 | ### Patch Changes 6 | 7 | - update local package to use workspace 8 | 9 | ## 1.0.0 10 | 11 | ### Major Changes 12 | 13 | - - Initial release of `@commit-generator/prompt-parser` package. 14 | - Added methods for converting structured templates into AI-friendly prompts for commit message generation. 15 | -------------------------------------------------------------------------------- /packages/prompt-parser/README.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/prompt-parser 2 | 3 | This package provides utilities for converting structured templates into AI-friendly prompts for **Commit Generator**. 4 | 5 | ## Installation 6 | 7 | To use this package in your project, install it as a dependency: 8 | 9 | ```bash 10 | pnpm install @commit-generator/prompt-parser 11 | ``` 12 | 13 | ## Usage 14 | 15 | Below is an example of how to use the `PromptParser` to generate a commit message prompt: 16 | 17 | ```javascript 18 | import { promptParser } from '@commit-generator/prompt-parser'; 19 | 20 | interface ICommitInfo { 21 | diff: string; 22 | type?: string; 23 | context?: string; 24 | previousLogs?: string; 25 | } 26 | 27 | const template = ` 28 | [Intro] 29 | You are an AI specialized in generating commit messages following good practices, 30 | such as the Conventional Commits format (type(scope): description). 31 | 32 | [Rules] 33 | Rules: 34 | 1. Identify the type of change (feat, fix, chore, refactor, docs, test) based on the diff. 35 | 2. Generate one commit message only. 36 | 3. Don't generate something more than one commit message 37 | 38 | [Output] 39 | Expected output: 40 | (scope): 41 | 42 | [Examples][Optional] 43 | {previousLogs} 44 | 45 | [Input][Optional] 46 | Type: {type} 47 | Context: {context} 48 | 49 | [Input] 50 | Diff: {diff} 51 | `; 52 | 53 | export function generatePrompt(commitInfo: ICommitInfo): string { 54 | const prompt = promptParser.parse(template, { ...commitInfo }); 55 | return prompt.toString(); 56 | } 57 | ``` 58 | 59 | ## License 60 | 61 | This package is licensed under the MIT License. -------------------------------------------------------------------------------- /packages/prompt-parser/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslintNode from '@commit-generator/eslint-config/node.js'; 2 | 3 | export default [...eslintNode]; 4 | -------------------------------------------------------------------------------- /packages/prompt-parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@commit-generator/prompt-parser", 3 | "version": "1.0.1", 4 | "description": "A parser that converts structured templates into AI-friendly prompts.", 5 | "author": "JulioC090", 6 | "files": [ 7 | "dist" 8 | ], 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "import": "./dist/index.mjs", 13 | "require": "./dist/index.js", 14 | "default": "./dist/index.js" 15 | } 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/JulioC090/commit-generator.git", 20 | "directory": "packages/prompt-parser" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/JulioC090/commit-generator/issues" 24 | }, 25 | "license": "MIT", 26 | "scripts": { 27 | "build": "tsup --format cjs,esm", 28 | "clean": "rimraf dist", 29 | "lint": "eslint . --report-unused-disable-directives --max-warnings 0", 30 | "lint:fix": "eslint . --fix", 31 | "test": "vitest run", 32 | "test:watch": "vitest watch", 33 | "test:coverage": "vitest run --coverage", 34 | "typecheck": "tsc --noEmit -p ." 35 | }, 36 | "devDependencies": { 37 | "@commit-generator/eslint-config": "workspace:^", 38 | "@commit-generator/typescript-config": "workspace:^", 39 | "eslint": "9.19.0" 40 | } 41 | } -------------------------------------------------------------------------------- /packages/prompt-parser/src/Prompt.ts: -------------------------------------------------------------------------------- 1 | interface PromptProps { 2 | intro: string; 3 | rules: string; 4 | output: string; 5 | examples: string; 6 | inputs: string[]; 7 | } 8 | 9 | export default class Prompt { 10 | private intro: string; 11 | private rules: string; 12 | private output: string; 13 | private examples: string; 14 | private inputs: string[] = []; 15 | 16 | constructor(props: PromptProps) { 17 | this.intro = props.intro; 18 | this.rules = props.rules; 19 | this.output = props.output; 20 | this.examples = props.examples; 21 | this.inputs = props.inputs; 22 | } 23 | 24 | public toString(): string { 25 | let output: string = ''; 26 | 27 | if (this.intro) { 28 | output += this.intro + '\n\n'; 29 | } 30 | 31 | if (this.rules) { 32 | output += this.rules + '\n\n'; 33 | } 34 | 35 | if (this.output) { 36 | output += this.output + '\n\n'; 37 | } 38 | 39 | if (this.examples) { 40 | output += 'Examples:\n'; 41 | output += this.examples + '\n\n'; 42 | } 43 | 44 | if (this.inputs.length > 0) { 45 | output += 'Inputs:\n'; 46 | for (const input of this.inputs) { 47 | output += ` - ${input}\n`; 48 | } 49 | } 50 | 51 | return output.trim(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/prompt-parser/src/PromptBuilder.ts: -------------------------------------------------------------------------------- 1 | import Prompt from '@/Prompt'; 2 | 3 | export default class PromptBuilder { 4 | private intro: string = ''; 5 | private rules: string = ''; 6 | private output: string = ''; 7 | private examples: string = ''; 8 | private inputs: string[] = []; 9 | 10 | public addIntro(intro: string): PromptBuilder { 11 | this.intro = intro.trim(); 12 | return this; 13 | } 14 | 15 | public addRules(rules: string): PromptBuilder { 16 | this.rules = rules.trim(); 17 | return this; 18 | } 19 | 20 | public addOutput(output: string): PromptBuilder { 21 | this.output = output.trim(); 22 | return this; 23 | } 24 | 25 | public addExamples(examples: string): PromptBuilder { 26 | this.examples = examples.trim(); 27 | return this; 28 | } 29 | 30 | public addInput(input: string): PromptBuilder { 31 | this.inputs.push(input.trim()); 32 | return this; 33 | } 34 | 35 | public addOptionalInput(input: string, value: unknown): PromptBuilder { 36 | if (value) { 37 | this.inputs.push(input.trim()); 38 | } 39 | 40 | return this; 41 | } 42 | 43 | public build(): Prompt { 44 | const prompt = new Prompt({ 45 | intro: this.intro, 46 | rules: this.rules, 47 | output: this.output, 48 | examples: this.examples, 49 | inputs: this.inputs, 50 | }); 51 | 52 | this.intro = ''; 53 | this.rules = ''; 54 | this.output = ''; 55 | this.examples = ''; 56 | this.inputs = []; 57 | 58 | return prompt; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/prompt-parser/src/PromptTemplateParser.ts: -------------------------------------------------------------------------------- 1 | import Prompt from '@/Prompt'; 2 | import PromptBuilder from '@/PromptBuilder'; 3 | 4 | const sections = ['Intro', 'Rules', 'Output', 'Examples', 'Input']; 5 | 6 | export default class PromptTemplateParser { 7 | constructor(private promptBuilder: PromptBuilder = new PromptBuilder()) {} 8 | 9 | parse( 10 | template: string, 11 | variables: { [key: string]: string | undefined }, 12 | ): Prompt { 13 | let previousSection: string; 14 | let currentSection: string = 'Intro'; 15 | let currentSectionContent: string = ''; 16 | let isOptional: boolean; 17 | 18 | const templateContent = template.trim(); 19 | 20 | for (const line of templateContent.split('\n')) { 21 | const sectionMatches = [...line.matchAll(/\[([^\]]+)\]/g)].map( 22 | (match) => match[1], 23 | ); 24 | 25 | if (sectionMatches.length > 0) { 26 | isOptional = false; 27 | previousSection = currentSection; 28 | currentSection = ''; 29 | 30 | sectionMatches.forEach((value) => { 31 | if (value === 'Optional') { 32 | isOptional = true; 33 | return; 34 | } 35 | 36 | if (sections.includes(value)) { 37 | currentSection = value; 38 | return; 39 | } 40 | }); 41 | 42 | if (!currentSection) throw new Error('Invalid Section'); 43 | 44 | if (previousSection === currentSection) continue; 45 | 46 | switch (previousSection) { 47 | case 'Intro': 48 | this.promptBuilder.addIntro(currentSectionContent); 49 | break; 50 | case 'Rules': 51 | this.promptBuilder.addRules(currentSectionContent); 52 | break; 53 | case 'Output': 54 | this.promptBuilder.addOutput(currentSectionContent); 55 | break; 56 | case 'Examples': 57 | this.promptBuilder.addExamples(currentSectionContent); 58 | break; 59 | } 60 | 61 | currentSectionContent = ''; 62 | continue; 63 | } 64 | 65 | const variablesMatch = [...line.matchAll(/\{([^}]+)\}/g)].map( 66 | (match) => match[1], 67 | ); 68 | 69 | if (variablesMatch.length > 0) { 70 | const parsedVariables: { [key: string]: string } = {}; 71 | 72 | variablesMatch.forEach((variableName) => { 73 | const variableValue = variables[variableName] || ''; 74 | 75 | if (!variableValue && !isOptional) 76 | throw new Error(`${variableName} is not defined`); 77 | 78 | parsedVariables[variableName] = variableValue; 79 | }); 80 | 81 | const result = Object.entries(parsedVariables).reduce( 82 | (result, [key, value]) => 83 | result.replace(new RegExp(`\\{${key}\\}`, 'g'), value), 84 | line, 85 | ); 86 | 87 | currentSectionContent += result; 88 | } else { 89 | currentSectionContent += line; 90 | } 91 | 92 | if (currentSection === 'Input' && currentSectionContent.trim()) { 93 | this.promptBuilder.addInput(currentSectionContent); 94 | currentSectionContent = ''; 95 | continue; 96 | } 97 | 98 | if (line.trim()) { 99 | currentSectionContent += '\n'; 100 | } 101 | } 102 | 103 | return this.promptBuilder.build(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /packages/prompt-parser/src/index.ts: -------------------------------------------------------------------------------- 1 | import PromptTemplateParser from '@/PromptTemplateParser'; 2 | 3 | export const promptParser = new PromptTemplateParser(); 4 | -------------------------------------------------------------------------------- /packages/prompt-parser/tests/unit/Prompt.spec.ts: -------------------------------------------------------------------------------- 1 | import Prompt from '@/Prompt'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('Prompt', () => { 5 | it('should correctly format the output of toString()', () => { 6 | const props = { 7 | intro: 'This is an introduction.', 8 | rules: '1. Follow the rules.\n2. Be concise.', 9 | output: 'Expected output format:', 10 | examples: 'Example: Input -> Output', 11 | inputs: ['Input 1', 'Input 2', 'Input 3'], 12 | }; 13 | 14 | const prompt = new Prompt(props); 15 | 16 | const expectedOutput = `This is an introduction.\n\n1. Follow the rules.\n2. Be concise.\n\nExpected output format:\n\nExamples:\nExample: Input -> Output\n\nInputs:\n - Input 1\n - Input 2\n - Input 3`; 17 | 18 | expect(prompt.toString()).toBe(expectedOutput); 19 | }); 20 | 21 | it('should handle empty inputs array gracefully', () => { 22 | const props = { 23 | intro: 'Intro text.', 24 | rules: 'Rules here.', 25 | output: 'Output info.', 26 | examples: 'Examples here.', 27 | inputs: [], 28 | }; 29 | 30 | const prompt = new Prompt(props); 31 | 32 | const expectedOutput = `Intro text.\n\nRules here.\n\nOutput info.\n\nExamples:\nExamples here.`; 33 | 34 | expect(prompt.toString()).toBe(expectedOutput); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/prompt-parser/tests/unit/PromptBuilder.spec.ts: -------------------------------------------------------------------------------- 1 | import Prompt from '@/Prompt'; 2 | import PromptBuilder from '@/PromptBuilder'; 3 | import { describe, expect, it } from 'vitest'; 4 | 5 | describe('PromptBuilder', () => { 6 | it('should build a Prompt object with the correct values', () => { 7 | const builder = new PromptBuilder(); 8 | 9 | const prompt = builder 10 | .addIntro('This is an introduction.') 11 | .addRules('1. Follow the rules.\n2. Be concise.') 12 | .addOutput('Expected output format:') 13 | .addExamples('Example: Input -> Output') 14 | .addInput('Input 1') 15 | .addInput('Input 2') 16 | .addOptionalInput('Optional Input 3', true) 17 | .build(); 18 | 19 | expect(prompt).toBeInstanceOf(Prompt); 20 | expect(prompt.toString()).toBe( 21 | `This is an introduction.\n\n1. Follow the rules.\n2. Be concise.\n\nExpected output format:\n\nExamples:\nExample: Input -> Output\n\nInputs:\n - Input 1\n - Input 2\n - Optional Input 3`, 22 | ); 23 | }); 24 | 25 | it('should skip optional input if value is falsy', () => { 26 | const builder = new PromptBuilder(); 27 | 28 | const prompt = builder 29 | .addIntro('Intro text.') 30 | .addRules('Rules here.') 31 | .addOutput('Output info.') 32 | .addExamples('Examples here.') 33 | .addInput('Input 1') 34 | .addOptionalInput('Optional Input 2', false) 35 | .build(); 36 | 37 | expect(prompt).toBeInstanceOf(Prompt); 38 | expect(prompt.toString()).toBe( 39 | `Intro text.\n\nRules here.\n\nOutput info.\n\nExamples:\nExamples here.\n\nInputs:\n - Input 1`, 40 | ); 41 | }); 42 | 43 | it('should handle empty inputs gracefully', () => { 44 | const builder = new PromptBuilder(); 45 | 46 | const prompt = builder 47 | .addIntro('Intro only.') 48 | .addRules('') 49 | .addOutput('') 50 | .addExamples('') 51 | .build(); 52 | 53 | expect(prompt).toBeInstanceOf(Prompt); 54 | expect(prompt.toString()).toBe(`Intro only.`); 55 | }); 56 | 57 | it('should reset instance values after building', () => { 58 | const builder = new PromptBuilder(); 59 | 60 | const prompt = builder 61 | .addIntro('Intro only.') 62 | .addRules('') 63 | .addOutput('') 64 | .addExamples('') 65 | .build(); 66 | 67 | expect(prompt).toBeInstanceOf(Prompt); 68 | expect(prompt.toString()).toBe(`Intro only.`); 69 | 70 | const newPrompt = builder.build(); 71 | 72 | expect(prompt).toBeInstanceOf(Prompt); 73 | expect(newPrompt.toString()).toBe(''); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /packages/prompt-parser/tests/unit/PromptTemplateParser.spec.ts: -------------------------------------------------------------------------------- 1 | import PromptBuilder from '@/PromptBuilder'; 2 | import PromptTemplateParser from '@/PromptTemplateParser'; 3 | import { describe, expect, it } from 'vitest'; 4 | 5 | describe('PromptTemplateParser', () => { 6 | it('should parse a template and extract sections correctly', () => { 7 | const parser = new PromptTemplateParser(new PromptBuilder()); 8 | 9 | const template = ` 10 | [Intro] 11 | This is a test intro. 12 | And this is other line. 13 | 14 | [Rules] 15 | Follow the rules strictly. 16 | 17 | [Output] 18 | Expected result format. 19 | 20 | [Examples] 21 | Example 1: Input -> Output 22 | 23 | [Input] 24 | User input: {inputValue} 25 | `; 26 | 27 | const expected = ` 28 | This is a test intro. 29 | And this is other line. 30 | 31 | Follow the rules strictly. 32 | 33 | Expected result format. 34 | 35 | Examples: 36 | Example 1: Input -> Output 37 | 38 | Inputs: 39 | - User input: Test Input 40 | `.trim(); 41 | 42 | const variables = { inputValue: 'Test Input' }; 43 | const prompt = parser.parse(template, variables); 44 | 45 | expect(prompt.toString()).toBe(expected); 46 | }); 47 | 48 | it('should replace variables in the template', () => { 49 | const parser = new PromptTemplateParser(new PromptBuilder()); 50 | 51 | const template = ` 52 | [Input] 53 | Variable test: {testVar} 54 | `; 55 | 56 | const variables = { testVar: 'Success' }; 57 | const prompt = parser.parse(template, variables); 58 | 59 | expect(prompt.toString()).toContain('Variable test: Success'); 60 | }); 61 | 62 | it('should throw an error if a required variable is missing', () => { 63 | const parser = new PromptTemplateParser(new PromptBuilder()); 64 | 65 | const template = ` 66 | [Input] 67 | Required variable: {missingVar} 68 | `; 69 | 70 | expect(() => parser.parse(template, {})).toThrow( 71 | 'missingVar is not defined', 72 | ); 73 | }); 74 | 75 | it('should handle optional sections correctly', () => { 76 | const parser = new PromptTemplateParser(new PromptBuilder()); 77 | 78 | const template = ` 79 | [Intro][Optional] 80 | Optional value: {optionalVar} 81 | `; 82 | 83 | const variables = {}; 84 | const prompt = parser.parse(template, variables); 85 | 86 | expect(prompt.toString()).not.toContain('Optional value:'); 87 | }); 88 | 89 | it('should throw an error for invalid sections', () => { 90 | const parser = new PromptTemplateParser(new PromptBuilder()); 91 | 92 | const template = ` 93 | [InvalidSection] 94 | This section does not exist. 95 | `; 96 | 97 | expect(() => parser.parse(template, {})).toThrow('Invalid Section'); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /packages/prompt-parser/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["tests"] 4 | } -------------------------------------------------------------------------------- /packages/prompt-parser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@commit-generator/typescript-config/base.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "rootDirs": ["src", "tests"], 6 | "outDir": "dist", 7 | "paths": { 8 | "@/tests/*": ["../tests/*"], 9 | "@/*": ["*"] 10 | }, 11 | }, 12 | "include": ["src", "tests"], 13 | "exclude": [] 14 | } -------------------------------------------------------------------------------- /packages/prompt-parser/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | clean: true, 6 | tsconfig: 'tsconfig.build.json', 7 | treeshake: true, 8 | minify: true, 9 | shims: true, 10 | dts: true, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/typescript-config/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/typescript-config 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - - _Strict TypeScript checks_. 8 | - _ES module support_ with NodeNext module resolutio. 9 | - _Targeting ES2022_. 10 | -------------------------------------------------------------------------------- /packages/typescript-config/README.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/typescript-config 2 | 3 | This package contains the shared TypeScript configuration for the **Commit Generator** project. 4 | 5 | ## Installation 6 | To use the TypeScript configuration in your project, install the package as a development dependency: 7 | 8 | ```bash 9 | pnpm install --save-dev @commit-generator/typescript-config 10 | ``` 11 | 12 | ## Usage 13 | After installation, add the `tsconfig.json` configuration file from the package to your project. 14 | 15 | 1. Extend the TypeScript configuration 16 | 17 | ```json 18 | { 19 | "extends": "@commit-generator/typescript-config" 20 | } 21 | ``` 22 | 23 | 2. Customizing the Configuration 24 | 25 | To customize the configuration, create your own tsconfig.json and extend the default settings: 26 | 27 | ```json 28 | { 29 | "extends": "@commit-generator/typescript-config", 30 | "compilerOptions": { 31 | "strict": true, // Enables strict TypeScript checks 32 | "noUnusedLocals": true // Excludes unused variables 33 | } 34 | } 35 | ``` 36 | 37 | ## License 38 | This package is licensed under the MIT License. -------------------------------------------------------------------------------- /packages/typescript-config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "es2022", 5 | "module": "NodeNext", 6 | "moduleDetection": "force", 7 | "moduleResolution": "nodenext", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "removeComments": true 13 | } 14 | } -------------------------------------------------------------------------------- /packages/typescript-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@commit-generator/typescript-config", 3 | "version": "0.1.0", 4 | "description": "TypeScript Shared Configuration for Commit Generator", 5 | "author": "JulioC090", 6 | "files": [ 7 | "base.json" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/JulioC090/commit-generator.git", 12 | "directory": "packages/typescript-config" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/JulioC090/commit-generator/issues" 16 | }, 17 | "license": "MIT" 18 | } -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "projects/*" -------------------------------------------------------------------------------- /projects/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/cli 2 | 3 | ## 1.0.2 4 | 5 | ### Patch Changes 6 | 7 | - update local package to use workspace 8 | - Updated dependencies 9 | - @commit-generator/commit-history@1.0.1 10 | - @commit-generator/config@1.0.1 11 | - @commit-generator/core@1.0.2 12 | - @commit-generator/git@1.0.1 13 | 14 | ## 1.0.1 15 | 16 | ### Patch Changes 17 | 18 | - @commit-generator/core@1.0.1 19 | 20 | ## 1.0.0 21 | 22 | ### Major Changes 23 | 24 | - - Initial release of `@commit-generator/cli` package. 25 | - Integrated an interactive CLI workflow for generating commit messages based on Git diffs. 26 | - Support for manual commit message generation and editing. 27 | - Ability to commit, regenerate, or amend messages. 28 | - Added functionality for validating commit messages based on best practices. 29 | - Includes the ability to validate both custom and the last commit message. 30 | - Introduced a configuration setup with persistent options such as AI provider and excluded files. 31 | - Configuration can be dynamically changed through environment variables and CLI flags. 32 | 33 | ## 0.0.7 34 | 35 | ### Patch Changes 36 | 37 | - Updated dependencies 38 | - @commit-generator/core@1.0.0 39 | 40 | ## 0.0.6 41 | 42 | ### Patch Changes 43 | 44 | - @commit-generator/core@0.0.5 45 | 46 | ## 0.0.5 47 | 48 | ### Patch Changes 49 | 50 | - Updated dependencies 51 | - @commit-generator/git@1.0.0 52 | - @commit-generator/core@0.0.4 53 | 54 | ## 0.0.4 55 | 56 | ### Patch Changes 57 | 58 | - Updated dependencies 59 | - @commit-generator/config@1.0.0 60 | 61 | ## 0.0.3 62 | 63 | ### Patch Changes 64 | 65 | - @commit-generator/core@0.0.3 66 | 67 | ## 0.0.2 68 | 69 | ### Patch Changes 70 | 71 | - Updated dependencies 72 | - @commit-generator/commit-history@1.0.0 73 | - @commit-generator/core@0.0.2 74 | 75 | ## 0.0.1 76 | 77 | ### Patch Changes 78 | 79 | - @commit-generator/core@0.0.1 80 | -------------------------------------------------------------------------------- /projects/cli/README.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/cli 2 | 3 |

4 | 5 |

6 | 7 | This package provides a CLI tool for generating commit messages based on Git diffs and AI models. It offers configurable commit message generation, editing, and validation. 8 | 9 | ## 📌 Table of Contents 10 | - [Installation](#installation) 11 | - [Usage](#usage) 12 | - [Default Workflow](#default-workflow) 13 | - [Manual Generation](#manual-generation) 14 | - [Validate a Commit Message](#validate-a-commit-message) 15 | - [Validate the Last Commit Message](#validate-the-last-commit-message) 16 | - [Persistent Configuration](#persistent-configuration) 17 | - [Dynamic Configuration](#dynamic-configuration) 18 | - [Configuration Options](#configuration-options) 19 | - [License](#-license) 20 | 21 | ## Installation 22 | 23 | To use this package in your project, install it globally: 24 | 25 | ```bash 26 | pnpm install -g @commit-generator/cli 27 | ``` 28 | 29 | To set up the configuration, run: 30 | 31 | ```bash 32 | commitgen config init 33 | ``` 34 | 35 | This will launch an interactive prompt, allowing you to configure your AI model for commit message generation. 36 | 37 | ## Usage 38 | 39 | Once installed, you can use the CLI with `commitgen `. 40 | 41 | ### Default Workflow 42 | 43 | To begin, stage your files: 44 | 45 | ```bash 46 | git add . 47 | ``` 48 | 49 | Then run: 50 | 51 | ```bash 52 | commitgen 53 | ``` 54 | 55 | This will open an interactive prompt, where you can provide additional context to improve message precision. After completion, the following options are available: 56 | - Regenerate the commit message 57 | - Edit the message (opens the default text editor) 58 | - Commit the message 59 | - Exit the prompt 60 | 61 | ### Manual generation 62 | 63 | If you prefer to bypass the interactive prompt, use the following command to generate a commit message: 64 | 65 | ```bash 66 | commitgen generate 67 | ``` 68 | 69 | This command will only generate the commit message without committing. You can then edit it with: 70 | 71 | ```bash 72 | commitgen edit 73 | ``` 74 | 75 | Finally, to commit: 76 | 77 | ```bash 78 | commitgen commit 79 | ``` 80 | 81 | ### Validate a Commit Message 82 | 83 | To validate a commit message, use the following command: 84 | 85 | ```bash 86 | commitgen validate 87 | ``` 88 | 89 | This compares the commit message with the staged diff and checks whether it adheres to best practices. If necessary, it will generate an improved commit message. 90 | 91 | To commit the generated message, run: 92 | 93 | ```bash 94 | commitgen commit 95 | ``` 96 | 97 | ### Validate the Last Commit Message 98 | 99 | To validate the most recent commit message, run: 100 | 101 | ```bash 102 | commitgen validate 103 | ``` 104 | 105 | If needed, amend the commit message with: 106 | 107 | ```bash 108 | commitgen amend 109 | ``` 110 | 111 | ### Persistent Configuration 112 | 113 | You can set the CLI configuration with: 114 | 115 | ```bash 116 | commit config init 117 | ``` 118 | 119 | To set a configuration value: 120 | 121 | ```bash 122 | commitgen config set =value 123 | # Example: commitgen config set provider=openai openai.key=some_key 124 | # Example: commitgen config set exclude.files="pnpm-lock.yaml,package-lock.json" 125 | ``` 126 | 127 | To unset a configuration value: 128 | 129 | ```bash 130 | commitgen config unset 131 | # Example: commitgen config unset exclude.files 132 | ``` 133 | 134 | To list the active configuration: 135 | 136 | ```bash 137 | commitgen config list 138 | ``` 139 | 140 | ### Dynamic Configuration 141 | 142 | You can dynamically change the configuration using environment variables with the `commit_gen_config_` prefix. Replace `.` with `_`. 143 | 144 | For example: 145 | 146 | ```bash 147 | $env:commit_gen_config_openai_key = "some_key" # PowerShell 148 | set commit_gen_config_openai_key=some_key # CMD 149 | export commit_gen_config_openai_key="some_key" # Linux 150 | ``` 151 | 152 | You can also use `--=` for CLI options: 153 | 154 | ```bash 155 | commitgen config list --provider=ollama 156 | ``` 157 | 158 | ## Configuration options 159 | 160 | | Key | Type | Description | 161 | |-----------------|-----------------|-----------------------------------------| 162 | | `provider` | `string` | The AI provider to use | 163 | | `openai.key` | `string` | The OpenAi authentication key | 164 | | `ollama.model` | `string` | The model to use in Ollama | 165 | | `exclude.files` | `Array` | Files to exclude from the diff analysis | 166 | 167 | ## License 168 | 169 | This package is licensed under the MIT License. -------------------------------------------------------------------------------- /projects/cli/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslintNode from '@commit-generator/eslint-config/node.js'; 2 | 3 | export default [...eslintNode]; 4 | -------------------------------------------------------------------------------- /projects/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@commit-generator/cli", 3 | "version": "1.0.2", 4 | "description": "CLI for generating commit messages using AI.", 5 | "author": "JulioC090", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/JulioC090/commit-generator.git", 9 | "directory": "projects/cli" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/JulioC090/commit-generator/issues" 13 | }, 14 | "license": "MIT", 15 | "main": "dist/index.js", 16 | "types": "dist/index.d.ts", 17 | "files": [ 18 | "dist" 19 | ], 20 | "bin": { 21 | "commitgen": "./dist/index.js" 22 | }, 23 | "scripts": { 24 | "start": "node .", 25 | "dev": "tsx watch --env-file=.env ./src/index.ts", 26 | "build": "tsup", 27 | "clean": "rimraf dist", 28 | "lint": "eslint . --report-unused-disable-directives --max-warnings 0", 29 | "lint:fix": "eslint . --fix", 30 | "test": "vitest run", 31 | "test:watch": "vitest watch", 32 | "test:coverage": "vitest run --coverage", 33 | "typecheck": "tsc --noEmit -p ." 34 | }, 35 | "devDependencies": { 36 | "@commit-generator/eslint-config": "workspace:^", 37 | "@commit-generator/typescript-config": "workspace:^", 38 | "@types/node": "^22.10.2", 39 | "eslint": "9.19.0" 40 | }, 41 | "dependencies": { 42 | "@commander-js/extra-typings": "^13.1.0", 43 | "@commit-generator/commit-history": "workspace:^", 44 | "@commit-generator/config": "workspace:^", 45 | "@commit-generator/core": "workspace:^", 46 | "@commit-generator/git": "workspace:^", 47 | "chalk": "^4", 48 | "commander": "^13.0.0", 49 | "inquirer": "^12.4.1", 50 | "ora-classic": "^5.4.2" 51 | } 52 | } -------------------------------------------------------------------------------- /projects/cli/src/commands/amend.ts: -------------------------------------------------------------------------------- 1 | import { historyPath } from '@/constants'; 2 | import { createAmendGenerated } from '@commit-generator/core'; 3 | 4 | export default async function amend() { 5 | await createAmendGenerated(historyPath).execute(); 6 | } 7 | -------------------------------------------------------------------------------- /projects/cli/src/commands/commit.ts: -------------------------------------------------------------------------------- 1 | import { historyPath } from '@/constants'; 2 | import { createCommitGenerated } from '@commit-generator/core'; 3 | 4 | export default async function commit() { 5 | await createCommitGenerated(historyPath).execute(); 6 | } 7 | -------------------------------------------------------------------------------- /projects/cli/src/commands/config/init.ts: -------------------------------------------------------------------------------- 1 | import configManager from '@/config'; 2 | import promptConfirmConfigOverwrite from '@/prompts/promptConfirmConfigOverwrite'; 3 | import promptProvider from '@/prompts/promptProvider'; 4 | import promptProviderParams from '@/prompts/promptProviderParams'; 5 | import { 6 | aiModelSchemes, 7 | IAIModelSchemes, 8 | } from '@commit-generator/core/schemes'; 9 | import chalk from 'chalk'; 10 | 11 | export default async function init(provider?: string) { 12 | console.clear(); 13 | 14 | const providers = Object.keys(aiModelSchemes); 15 | 16 | if (provider && !providers.includes(provider)) { 17 | console.error(chalk.red.bold(`❌ Provider '${provider}' not found.`)); 18 | process.exit(1); 19 | } 20 | 21 | const selectedProvider: keyof IAIModelSchemes = 22 | provider || (await promptProvider(providers)); 23 | 24 | const { properties, required } = aiModelSchemes[selectedProvider]; 25 | 26 | const providerParams = await promptProviderParams( 27 | selectedProvider, 28 | Object.keys(properties), 29 | required, 30 | ); 31 | 32 | if (!(await promptConfirmConfigOverwrite())) { 33 | console.clear(); 34 | console.log(chalk.yellow('⚠️ Operation canceled.')); 35 | process.exit(0); 36 | } 37 | 38 | await configManager.set('provider', selectedProvider, 'local'); 39 | await configManager.set(selectedProvider, providerParams, 'local'); 40 | 41 | console.clear(); 42 | 43 | console.log( 44 | chalk.blue( 45 | `✅ Configuration for '${selectedProvider}' saved successfully!`, 46 | ), 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /projects/cli/src/commands/config/list.ts: -------------------------------------------------------------------------------- 1 | import configManager from '@/config'; 2 | import maskSecret from '@/utils/maskSecret'; 3 | import chalk from 'chalk'; 4 | 5 | export default async function list(): Promise { 6 | const config = await configManager.loadConfig(); 7 | 8 | if (Object.keys(config).length === 0) { 9 | console.log('No configuration set.'); 10 | return; 11 | } 12 | 13 | console.log(chalk.bold('\n🛠️ Current Configuration:\n')); 14 | 15 | if (config.provider) { 16 | console.log( 17 | `${chalk.white('🚀 Active Provider: ')} ${chalk.blue(config.provider)}`, 18 | ); 19 | const providerConfig = config[config.provider]; 20 | 21 | if (providerConfig && typeof providerConfig === 'object') { 22 | Object.entries(providerConfig).forEach(([key, value]) => { 23 | if (key === 'key') { 24 | console.log( 25 | ` 🔹 ${chalk.white(key)}: ${chalk.green(maskSecret(value))}`, 26 | ); 27 | return; 28 | } 29 | console.log(` 🔹 ${chalk.white(key)}: ${chalk.green(value)}`); 30 | }); 31 | } 32 | console.log(''); 33 | } 34 | 35 | if (config.exclude && config.exclude.files.length > 0) { 36 | console.log(chalk.white('📂 Excluded Files:')); 37 | config.exclude.files.forEach((file) => 38 | console.log(` - ${chalk.green(file)}`), 39 | ); 40 | console.log(''); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /projects/cli/src/commands/config/set.ts: -------------------------------------------------------------------------------- 1 | import configManager, { IConfigType } from '@/config'; 2 | import { formatConfigValue } from '@commit-generator/config'; 3 | 4 | export default async function set( 5 | pairs: Array<{ key: string; value: string }>, 6 | ) { 7 | for (const { key, value } of pairs) { 8 | const formattedValue = formatConfigValue(value); 9 | await configManager.set(key as keyof IConfigType, formattedValue, 'local'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /projects/cli/src/commands/config/unset.ts: -------------------------------------------------------------------------------- 1 | import configManager from '@/config'; 2 | 3 | export default async function unset(keys: Array) { 4 | for (const key of keys) { 5 | await configManager.unset(key, 'local'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/cli/src/commands/edit.ts: -------------------------------------------------------------------------------- 1 | import { historyPath } from '@/constants'; 2 | import editLine from '@/utils/editLine'; 3 | import { createEditLastGenerated } from '@commit-generator/core'; 4 | 5 | export default async function edit() { 6 | await createEditLastGenerated(editLine, historyPath).execute(); 7 | } 8 | -------------------------------------------------------------------------------- /projects/cli/src/commands/generate.ts: -------------------------------------------------------------------------------- 1 | import configManager from '@/config'; 2 | import { historyPath } from '@/constants'; 3 | import checkStagedFiles from '@/utils/checkStagedFiles'; 4 | import { createGenerateCommit } from '@commit-generator/core'; 5 | 6 | export default async function generate(options: { 7 | type?: string; 8 | context?: string; 9 | }) { 10 | const config = await configManager.loadConfig(); 11 | 12 | if (!config.provider || !config[config.provider]) { 13 | throw new Error(`Invalid provider: ${config.provider ?? 'unknown'}`); 14 | } 15 | 16 | await checkStagedFiles(config.exclude?.files ?? [], true); 17 | 18 | const generateCommitConfig = { 19 | provider: config.provider, 20 | params: config[config.provider]!, 21 | }; 22 | 23 | const generateCommit = createGenerateCommit( 24 | generateCommitConfig, 25 | historyPath, 26 | config.exclude?.files ?? [], 27 | ); 28 | 29 | console.log(await generateCommit.execute(options)); 30 | } 31 | -------------------------------------------------------------------------------- /projects/cli/src/commands/generateAndCommit.ts: -------------------------------------------------------------------------------- 1 | import configManager from '@/config'; 2 | import { historyPath } from '@/constants'; 3 | import promptCommitContext from '@/prompts/promptCommitContext'; 4 | import promptCommitType from '@/prompts/promptCommitType'; 5 | import promptInteractiveGeneration from '@/prompts/promptInterativeGeneration'; 6 | import checkStagedFiles from '@/utils/checkStagedFiles'; 7 | import { createGenerateCommit } from '@commit-generator/core'; 8 | import { git } from '@commit-generator/git'; 9 | import chalk from 'chalk'; 10 | import ora from 'ora-classic'; 11 | 12 | export default async function generateAndCommit(options: { 13 | type?: string; 14 | context?: string; 15 | force?: boolean; 16 | }) { 17 | const spinner = ora('Generating commit message, please wait...'); 18 | 19 | try { 20 | const config = await configManager.loadConfig(); 21 | 22 | if (!config.provider || !config[config.provider]) { 23 | throw new Error(`Invalid provider: ${config.provider ?? 'unknown'}`); 24 | } 25 | 26 | await checkStagedFiles(config.exclude?.files ?? [], options.force); 27 | 28 | if (!options.type && !options.force) { 29 | options.type = await promptCommitType(); 30 | } 31 | 32 | if (!options.context && !options.force) { 33 | options.context = await promptCommitContext(); 34 | } 35 | 36 | const generateCommitConfig = { 37 | provider: config.provider, 38 | params: config[config.provider]!, 39 | }; 40 | 41 | const generateCommit = createGenerateCommit( 42 | generateCommitConfig, 43 | historyPath, 44 | config.exclude?.files ?? [], 45 | ); 46 | 47 | console.clear(); 48 | spinner.start(); 49 | let commitMessage = await generateCommit.execute(options); 50 | spinner.stop(); 51 | 52 | if (!options.force) { 53 | commitMessage = await promptInteractiveGeneration( 54 | async () => await generateCommit.execute(options), 55 | commitMessage, 56 | ); 57 | } 58 | 59 | git.commit(commitMessage); 60 | console.clear(); 61 | console.log(chalk.blue('Commit successful!')); 62 | } catch (error) { 63 | spinner.stop(); 64 | console.clear(); 65 | console.error(chalk.red('An error occurred:'), (error as Error).message); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /projects/cli/src/commands/validate.ts: -------------------------------------------------------------------------------- 1 | import configManager from '@/config'; 2 | import { historyPath } from '@/constants'; 3 | import wrapText from '@/utils/wrapText'; 4 | import { createValidateCommit } from '@commit-generator/core'; 5 | import chalk from 'chalk'; 6 | 7 | export default async function validate(commitMessage?: string) { 8 | const config = await configManager.loadConfig(); 9 | 10 | if (!config.provider || !config[config.provider]) { 11 | throw new Error(`Invalid provider: ${config.provider ?? 'unknown'}`); 12 | } 13 | 14 | const validateCommitConfig = { 15 | provider: config.provider, 16 | params: config[config.provider]!, 17 | }; 18 | 19 | const validateCommit = createValidateCommit( 20 | validateCommitConfig, 21 | historyPath, 22 | config.exclude?.files ?? [], 23 | ); 24 | 25 | const result = await validateCommit.execute({ commitMessage }); 26 | 27 | console.log( 28 | `${chalk.yellow('🔍 Analysis:\n')} ${wrapText(result.analysis, 120, 3)}`, 29 | ); 30 | 31 | console.log( 32 | `\n${chalk.blue('💡 Recommended commit message:\n')} ${chalk.magenta(result.recommendedMessage)}\n`, 33 | ); 34 | 35 | if (result.isValid) { 36 | console.log(chalk.green.bold('✅ Commit message is valid.\n')); 37 | } else { 38 | console.log(chalk.red('❌ Commit message is not valid')); 39 | process.exit(1); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /projects/cli/src/config.ts: -------------------------------------------------------------------------------- 1 | import createConfigManager, { 2 | IConfigDefinitions, 3 | IConfigSource, 4 | } from '@commit-generator/config'; 5 | import { 6 | aiModelSchemes, 7 | IAIModelSchemes, 8 | } from '@commit-generator/core/schemes'; 9 | import path from 'node:path'; 10 | 11 | const configFileName = '.commitgen.json'; 12 | const configFilePath = path.join(__dirname, '..', configFileName); 13 | 14 | const sources: Array = [ 15 | { 16 | name: 'local', 17 | type: 'file', 18 | path: configFilePath, 19 | }, 20 | { 21 | name: 'env', 22 | type: 'env', 23 | prefix: 'commit_gen_config_', 24 | }, 25 | { 26 | name: 'arg', 27 | type: 'arg', 28 | }, 29 | ]; 30 | 31 | export type IConfigType = { 32 | provider: keyof IAIModelSchemes; 33 | exclude?: { files: Array }; 34 | } & Partial; 35 | 36 | const providers = Object.entries(aiModelSchemes).map(([provider, schema]) => ({ 37 | properties: { 38 | provider: { const: provider }, 39 | [provider]: schema, 40 | }, 41 | required: [provider], 42 | })); 43 | 44 | const configDefinitions = { 45 | type: 'object', 46 | properties: { 47 | provider: { type: 'string' }, 48 | ...aiModelSchemes, 49 | exclude: { 50 | type: 'object', 51 | properties: { 52 | files: { type: 'array', items: { type: 'string' } }, 53 | }, 54 | required: ['files'], 55 | additionalProperties: false, 56 | }, 57 | }, 58 | required: ['provider'], 59 | anyOf: providers, 60 | additionalProperties: false, 61 | }; 62 | 63 | const configManager = createConfigManager({ 64 | sources, 65 | definitions: configDefinitions as unknown as IConfigDefinitions, 66 | }); 67 | 68 | export default configManager; 69 | -------------------------------------------------------------------------------- /projects/cli/src/constants.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | export const historyPath = path.join(__dirname, '..', 'history'); 4 | -------------------------------------------------------------------------------- /projects/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --no-deprecation 2 | 3 | import amend from '@/commands/amend'; 4 | import commit from '@/commands/commit'; 5 | import init from '@/commands/config/init'; 6 | import list from '@/commands/config/list'; 7 | import set from '@/commands/config/set'; 8 | import unset from '@/commands/config/unset'; 9 | import edit from '@/commands/edit'; 10 | import generate from '@/commands/generate'; 11 | import generateAndCommit from '@/commands/generateAndCommit'; 12 | import validate from '@/commands/validate'; 13 | import keyValueParser from '@/parsers/keyValueParser'; 14 | import { program } from '@commander-js/extra-typings'; 15 | 16 | // tsc-alias don't support json files 17 | // eslint-disable-next-line @typescript-eslint/no-require-imports 18 | const packageJSON = require('../package.json'); 19 | 20 | program.version(packageJSON.version); 21 | 22 | program 23 | .passThroughOptions() 24 | .description('Generate a commit message based on Git diffs and Commit') 25 | .option( 26 | '-t, --type ', 27 | 'Specify the type of commit (e.g., feat, fix, chore, docs, refactor, test, style, build, ci, perf, revert)', 28 | ) 29 | .option( 30 | '-c, --context ', 31 | 'Provide additional context for the commit message (e.g., related issue, scope, or extra details)', 32 | ) 33 | .option('-f, --force', 'Make commit automatically') 34 | .action(generateAndCommit); 35 | 36 | program 37 | .command('generate') 38 | .description('Generate a commit message based on Git diffs') 39 | .option( 40 | '-t, --type ', 41 | 'Specify the type of commit (e.g., feat, fix, chore, docs, refactor, test, style, build, ci, perf, revert)', 42 | ) 43 | .option( 44 | '-c, --context ', 45 | 'Provide additional context for the commit message (e.g., related issue, scope, or extra details)', 46 | ) 47 | .action(generate); 48 | 49 | program 50 | .command('edit') 51 | .description('Edit the last generated commit message') 52 | .action(edit); 53 | 54 | program 55 | .command('commit') 56 | .description('Commit the last generated message') 57 | .action(commit); 58 | 59 | program 60 | .command('amend') 61 | .description( 62 | 'Amend the last commit by replacing its message with the latest generated one, without modifying the staged files.', 63 | ) 64 | .action(amend); 65 | 66 | program 67 | .command('validate') 68 | .argument( 69 | '[message]', 70 | 'Optional commit message. If not provided, the latest commit will be used.', 71 | ) 72 | .description( 73 | 'Validate the commit message. If no message is provided, the latest commit will be validated.\n' + 74 | 'This command ensures the message follows best practices and can provide recommendations.', 75 | ) 76 | .action(validate); 77 | 78 | const configCommand = program 79 | .command('config') 80 | .description('Manage configuration'); 81 | 82 | configCommand 83 | .command('init') 84 | .argument( 85 | '[provider]', 86 | 'Specify an AI provider to configure. If omitted, you will be prompted to choose one.', 87 | ) 88 | .description( 89 | 'Initialize and configure an AI provider. This will overwrite existing configurations.', 90 | ) 91 | .action(init); 92 | 93 | configCommand 94 | .command('set') 95 | .argument('', 'Key-value pairs (key=value)', keyValueParser) 96 | .description('Set configuration keys with values') 97 | .action(set); 98 | 99 | configCommand 100 | .command('unset ') 101 | .description('Remove configuration keys') 102 | .action(unset); 103 | 104 | configCommand 105 | .command('list') 106 | .description( 107 | 'Display the active provider configuration with its parameters and additional settings', 108 | ) 109 | .action(list); 110 | 111 | const commanderArgs = process.argv.filter( 112 | (arg) => !/--[\w.]+=[\w.]+/g.test(arg), 113 | ); 114 | 115 | program.parse(commanderArgs); 116 | -------------------------------------------------------------------------------- /projects/cli/src/parsers/keyValueParser.ts: -------------------------------------------------------------------------------- 1 | import { InvalidArgumentError } from '@commander-js/extra-typings'; 2 | 3 | export default function keyValueParser( 4 | pair: string, 5 | pairs?: Array<{ key: string; value: string }>, 6 | ) { 7 | const [key, value] = pair.split('='); 8 | 9 | if (!key || !value) 10 | throw new InvalidArgumentError( 11 | 'Invalid key-value pair format. Expected "key=value".', 12 | ); 13 | 14 | if (!pairs) { 15 | pairs = []; 16 | } 17 | 18 | pairs.push({ key, value }); 19 | return pairs; 20 | } 21 | -------------------------------------------------------------------------------- /projects/cli/src/prompts/promptCommitContext.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | 3 | export default async function promptCommitContext() { 4 | const { context } = await inquirer.prompt([ 5 | { 6 | type: 'input', 7 | name: 'context', 8 | message: 'Provide additional context for the commit (optional):', 9 | }, 10 | ]); 11 | 12 | return context.trim(); 13 | } 14 | -------------------------------------------------------------------------------- /projects/cli/src/prompts/promptCommitType.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | 3 | export default async function promptCommitType(): Promise { 4 | const commitTypeChoices = [ 5 | { name: '✨ feat: Introduce a new feature', value: 'feat' }, 6 | { name: '🐛 fix: Fix a bug', value: 'fix' }, 7 | { name: '📝 docs: Update or add documentation', value: 'docs' }, 8 | { name: '💄 style: Code formatting and style changes', value: 'style' }, 9 | { 10 | name: '♻️ refactor: Code refactoring without changing functionality', 11 | value: 'refactor', 12 | }, 13 | { name: '✅ test: Add or update tests', value: 'test' }, 14 | { name: '🔨 chore: Maintenance and chores', value: 'chore' }, 15 | { name: '🚫 None: Do not specify a commit type', value: '' }, 16 | ]; 17 | 18 | const { commitType } = await inquirer.prompt([ 19 | { 20 | type: 'list', 21 | name: 'commitType', 22 | message: 'Select the commit type:', 23 | choices: commitTypeChoices, 24 | }, 25 | ]); 26 | 27 | return commitType; 28 | } 29 | -------------------------------------------------------------------------------- /projects/cli/src/prompts/promptConfirmConfigOverwrite.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | 3 | export default async function promptConfirmConfigOverwrite() { 4 | const { confirm } = await inquirer.prompt([ 5 | { 6 | type: 'confirm', 7 | name: 'confirm', 8 | message: 9 | 'This will overwrite existing configurations. Do you want to continue?', 10 | default: true, 11 | }, 12 | ]); 13 | 14 | return confirm; 15 | } 16 | -------------------------------------------------------------------------------- /projects/cli/src/prompts/promptConfirmStage.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | 3 | export default async function promptConfirmStage() { 4 | const { confirm } = await inquirer.prompt([ 5 | { 6 | type: 'confirm', 7 | name: 'confirm', 8 | message: 'You have no files staged. Would you like to add all files?', 9 | default: true, 10 | }, 11 | ]); 12 | 13 | return confirm; 14 | } 15 | -------------------------------------------------------------------------------- /projects/cli/src/prompts/promptInterativeGeneration.ts: -------------------------------------------------------------------------------- 1 | import { historyPath } from '@/constants'; 2 | import { CommitHistory } from '@commit-generator/commit-history'; 3 | import chalk from 'chalk'; 4 | import inquirer from 'inquirer'; 5 | import ora from 'ora-classic'; 6 | 7 | export default async function promptInteractiveGeneration( 8 | generateCommit: () => Promise, 9 | initialMessage: string, 10 | ) { 11 | const commitHistory = new CommitHistory(historyPath); 12 | 13 | let commitMessage = initialMessage; 14 | let confirmed = false; 15 | const spinner = ora('Generating commit message, please wait...'); 16 | 17 | while (!confirmed) { 18 | console.clear(); 19 | console.log('\nGenerated commit message:\n'); 20 | console.log(chalk.green.bold(commitMessage)); 21 | console.log('\n'); 22 | 23 | const { action } = await inquirer.prompt([ 24 | { 25 | type: 'list', 26 | name: 'action', 27 | message: 'What do you want to do?', 28 | choices: [ 29 | { 30 | name: '🔄 Re-generate: Generate a new commit message', 31 | value: 'regenerate', 32 | }, 33 | { 34 | name: '✏️ Edit: Modify the current commit message', 35 | value: 'edit', 36 | }, 37 | { 38 | name: '✅ Commit: Use the current commit message', 39 | value: 'commit', 40 | }, 41 | { name: '🚪 Exit: Quit without committing', value: 'exit' }, 42 | ], 43 | }, 44 | ]); 45 | 46 | switch (action) { 47 | case 'regenerate': 48 | try { 49 | console.clear(); 50 | spinner.start(); 51 | commitMessage = await generateCommit(); 52 | } catch (error) { 53 | console.clear(); 54 | console.error( 55 | chalk.red('An error occurred:'), 56 | (error as Error).message, 57 | ); 58 | } finally { 59 | spinner.stop(); 60 | } 61 | break; 62 | case 'edit': { 63 | const { newMessage } = await inquirer.prompt([ 64 | { 65 | type: 'editor', 66 | name: 'newMessage', 67 | message: 'Enter the new commit message:', 68 | default: commitMessage, 69 | }, 70 | ]); 71 | commitMessage = newMessage; 72 | commitHistory.add(commitMessage); 73 | break; 74 | } 75 | case 'commit': 76 | confirmed = true; 77 | break; 78 | case 'exit': 79 | console.clear(); 80 | console.log(chalk.blue('Exiting without committing.')); 81 | process.exit(0); 82 | } 83 | } 84 | return commitMessage; 85 | } 86 | -------------------------------------------------------------------------------- /projects/cli/src/prompts/promptProvider.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | 3 | export default async function promptProvider(providers: Array) { 4 | const { provider } = await inquirer.prompt([ 5 | { 6 | type: 'list', 7 | name: 'provider', 8 | message: 'Escolha o provedor de IA:', 9 | choices: providers, 10 | }, 11 | ]); 12 | 13 | return provider; 14 | } 15 | -------------------------------------------------------------------------------- /projects/cli/src/prompts/promptProviderParams.ts: -------------------------------------------------------------------------------- 1 | import inquirer, { Answers } from 'inquirer'; 2 | 3 | export default async function promptProviderParams( 4 | provider: string, 5 | properties: Array, 6 | required: Array, 7 | ) { 8 | const providerPrompt = properties.map((key) => ({ 9 | type: key === 'key' ? 'password' : 'input', 10 | name: key, 11 | mask: true, 12 | message: `Enter ${provider} ${key}:`, 13 | validate: (input: string) => { 14 | if (required.includes(key) && !input.trim()) { 15 | return 'Required Field'; 16 | } 17 | return true; 18 | }, 19 | })); 20 | 21 | return await inquirer.prompt(providerPrompt as Answers); 22 | } 23 | -------------------------------------------------------------------------------- /projects/cli/src/utils/checkStagedFiles.ts: -------------------------------------------------------------------------------- 1 | import promptConfirmStage from '@/prompts/promptConfirmStage'; 2 | import { git } from '@commit-generator/git'; 3 | import chalk from 'chalk'; 4 | 5 | export default async function checkStagedFiles( 6 | excludeFiles: Array, 7 | force?: boolean, 8 | ) { 9 | const diff = git.diff({ 10 | staged: true, 11 | excludeFiles, 12 | }); 13 | 14 | if (diff) { 15 | return; 16 | } 17 | 18 | if (force) { 19 | console.log(chalk.red('No staged files found.')); 20 | process.exit(1); 21 | } 22 | 23 | const confirm = await promptConfirmStage(); 24 | 25 | if (!confirm) { 26 | console.log(chalk.red('No staged files found.')); 27 | process.exit(1); 28 | } 29 | 30 | git.add(); 31 | 32 | const newDiff = git.diff({ 33 | staged: true, 34 | excludeFiles, 35 | }); 36 | 37 | if (!newDiff) { 38 | console.log(chalk.red('No staged files found.')); 39 | process.exit(1); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /projects/cli/src/utils/editLine.ts: -------------------------------------------------------------------------------- 1 | import readline from 'node:readline/promises'; 2 | 3 | export default async function editLine(line: string): Promise { 4 | const rl = readline.createInterface({ 5 | input: process.stdin, 6 | output: process.stdout, 7 | }); 8 | 9 | const rlPromise = rl.question('Edit: \n'); 10 | rl.write(line); 11 | 12 | const result = await rlPromise; 13 | rl.close(); 14 | 15 | return result; 16 | } 17 | -------------------------------------------------------------------------------- /projects/cli/src/utils/errorHandler.ts: -------------------------------------------------------------------------------- 1 | export function exitWithError(message: string) { 2 | console.error(`Error: ${message}`); 3 | process.exit(1); 4 | } 5 | -------------------------------------------------------------------------------- /projects/cli/src/utils/maskSecret.ts: -------------------------------------------------------------------------------- 1 | export default function maskSecret( 2 | secret: string, 3 | visibleStart = 4, 4 | visibleEnd = 4, 5 | ): string { 6 | if (secret.length <= visibleStart + visibleEnd) 7 | return '*'.repeat(secret.length); 8 | return `${secret.slice(0, visibleStart)}...${secret.slice(-visibleEnd)}`; 9 | } 10 | -------------------------------------------------------------------------------- /projects/cli/src/utils/wrapText.ts: -------------------------------------------------------------------------------- 1 | export default function wrapText( 2 | text: string, 3 | width: number, 4 | spaces: number = 0, 5 | ) { 6 | const regex = new RegExp(`(.{1,${width}})(\\s|$)`, 'g'); 7 | const indent = '\u0020'.repeat(spaces); 8 | return text.match(regex)?.join(`\n${indent}`); 9 | } 10 | -------------------------------------------------------------------------------- /projects/cli/tests/unit/parsers/keyValueParser.spec.ts: -------------------------------------------------------------------------------- 1 | import keyValueParser from '@/parsers/keyValueParser'; 2 | import { InvalidArgumentError } from '@commander-js/extra-typings'; 3 | import { describe, expect, it } from 'vitest'; 4 | 5 | describe('keyValueParser', () => { 6 | it('should parse a valid key-value pair', () => { 7 | const result = keyValueParser('foo=bar', []); 8 | expect(result).toEqual([{ key: 'foo', value: 'bar' }]); 9 | }); 10 | 11 | it('should parse multiple key-value pairs', () => { 12 | const result = keyValueParser('key1=value1', [ 13 | { key: 'key2', value: 'value2' }, 14 | ]); 15 | expect(result).toEqual([ 16 | { key: 'key2', value: 'value2' }, 17 | { key: 'key1', value: 'value1' }, 18 | ]); 19 | }); 20 | 21 | it('should throw an error for invalid key-value pairs', () => { 22 | expect(() => keyValueParser('invalidPair', [])).toThrowError( 23 | 'Invalid key-value pair format. Expected "key=value".', 24 | ); 25 | }); 26 | 27 | it('should throw an error when key or value is missing', () => { 28 | expect(() => keyValueParser('keyOnly=', [])).toThrowError( 29 | new InvalidArgumentError( 30 | 'Invalid key-value pair format. Expected "key=value".', 31 | ), 32 | ); 33 | expect(() => keyValueParser('=valueOnly', [])).toThrowError( 34 | new InvalidArgumentError( 35 | 'Invalid key-value pair format. Expected "key=value".', 36 | ), 37 | ); 38 | }); 39 | 40 | it('should initialize an empty array if pairs is undefined', () => { 41 | const result = keyValueParser('test=value', undefined); 42 | expect(result).toEqual([{ key: 'test', value: 'value' }]); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /projects/cli/tests/unit/utils/editLine.spec.ts: -------------------------------------------------------------------------------- 1 | import editLine from '@/utils/editLine'; 2 | import readline from 'node:readline/promises'; 3 | import { describe, expect, it, vi } from 'vitest'; 4 | 5 | vi.mock('node:readline/promises', () => { 6 | const readline = { 7 | createInterface: vi.fn().mockReturnValue({ 8 | question: vi.fn().mockImplementation(async () => { 9 | return 'user confirmed message'; 10 | }), 11 | write: vi.fn(), 12 | close: vi.fn(), 13 | }), 14 | }; 15 | return { default: readline }; 16 | }); 17 | 18 | describe('editLine', () => { 19 | it('should confirm the commit message from user input', async () => { 20 | const commitMessage = 'Initial commit'; 21 | 22 | const finalCommit = await editLine(commitMessage); 23 | 24 | expect(finalCommit).toBe('user confirmed message'); 25 | 26 | const rl = readline.createInterface({ input: process.stdin }); 27 | expect(rl.write).toHaveBeenCalledWith(commitMessage); 28 | expect(rl.close).toHaveBeenCalled(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /projects/cli/tests/unit/utils/maskSecret.spec.ts: -------------------------------------------------------------------------------- 1 | import maskSecret from '@/utils/maskSecret'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('maskSecret', () => { 5 | it('should mask a long secret while keeping the start and end visible', () => { 6 | expect(maskSecret('sk-12345678-ABCD')).toBe('sk-1...ABCD'); 7 | }); 8 | 9 | it('should fully mask secrets shorter than the visible range', () => { 10 | expect(maskSecret('abcdef')).toBe('******'); 11 | expect(maskSecret('xyz')).toBe('***'); 12 | }); 13 | 14 | it('should work with custom visibleStart and visibleEnd values', () => { 15 | expect(maskSecret('sk-12345678-ABCD', 2, 2)).toBe('sk...CD'); 16 | expect(maskSecret('sk-12345678-ABCD', 6, 6)).toBe('sk-123...8-ABCD'); 17 | }); 18 | 19 | it('should handle empty strings correctly', () => { 20 | expect(maskSecret('')).toBe(''); 21 | }); 22 | 23 | it('should handle secrets with exactly visibleStart + visibleEnd length', () => { 24 | expect(maskSecret('12345678', 4, 4)).toBe('********'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /projects/cli/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["tests"] 4 | } -------------------------------------------------------------------------------- /projects/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@commit-generator/typescript-config/base.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "rootDirs": ["src", "tests"], 6 | "outDir": "dist", 7 | "paths": { 8 | "@/tests/*": ["../tests/*"], 9 | "@/*": ["*"] 10 | }, 11 | }, 12 | "include": ["src", "tests"], 13 | "exclude": [] 14 | } -------------------------------------------------------------------------------- /projects/cli/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | target: 'node18', 6 | clean: true, 7 | tsconfig: 'tsconfig.build.json', 8 | treeshake: true, 9 | minify: true, 10 | shims: true, 11 | }); 12 | -------------------------------------------------------------------------------- /projects/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/core 2 | 3 | ## 1.0.2 4 | 5 | ### Patch Changes 6 | 7 | - update local package to use workspace 8 | - Updated dependencies 9 | - @commit-generator/commit-history@1.0.1 10 | - @commit-generator/prompt-parser@1.0.1 11 | - @commit-generator/ai-models@1.1.1 12 | - @commit-generator/git@1.0.1 13 | 14 | ## 1.0.1 15 | 16 | ### Patch Changes 17 | 18 | - Updated dependencies 19 | - @commit-generator/ai-models@1.1.0 20 | 21 | ## 1.0.0 22 | 23 | ### Major Changes 24 | 25 | - - Initial release of `@commit-generator/core` package. 26 | 27 | ## 0.0.5 28 | 29 | ### Patch Changes 30 | 31 | - Updated dependencies 32 | - @commit-generator/prompt-parser@1.0.0 33 | 34 | ## 0.0.4 35 | 36 | ### Patch Changes 37 | 38 | - Updated dependencies 39 | - @commit-generator/git@1.0.0 40 | 41 | ## 0.0.3 42 | 43 | ### Patch Changes 44 | 45 | - Updated dependencies 46 | - @commit-generator/ai-models@1.0.1 47 | 48 | ## 0.0.2 49 | 50 | ### Patch Changes 51 | 52 | - Updated dependencies 53 | - @commit-generator/commit-history@1.0.0 54 | 55 | ## 0.0.1 56 | 57 | ### Patch Changes 58 | 59 | - Updated dependencies 60 | - @commit-generator/ai-models@1.0.0 61 | -------------------------------------------------------------------------------- /projects/core/README.md: -------------------------------------------------------------------------------- 1 | # @commit-generator/core 2 | 3 | This package provides the core functionalities for the Commit Generator project. It includes essential commit operations and AI model schemes. 4 | 5 | ## Installation 6 | 7 | To use this package in your project, install it as a dependency: 8 | 9 | ```bash 10 | pnpm install @commit-generator/core 11 | ``` 12 | 13 | ## Usage 14 | 15 | 1. Using Factories 16 | Actions are generated using factories. Here’s an example: 17 | 18 | ```javascript 19 | import { createGenerateCommit } from '@commit-generator/core'; 20 | 21 | async function generate() { 22 | const generateCommitConfig = { 23 | provider: 'openai', 24 | params: { key: 'some_key' }, 25 | }; 26 | 27 | const generateCommit = createGenerateCommit( 28 | generateCommitConfig, 29 | 'path/to/history', 30 | ['pnpm-lock.yaml'] 31 | ); 32 | 33 | console.log( 34 | await generateCommit.execute({ 35 | type: 'feat', 36 | context: 'This is a test commit', 37 | }) 38 | ); 39 | } 40 | 41 | generate(); 42 | ``` 43 | 44 | 2. AI Model Schemes 45 | For convenience, this module exports AI schemes from [AI Models](../../packages/ai-models/). 46 | 47 | ```javascript 48 | import { aiModelSchemes } from '@commit-generator/core/schemes'; 49 | 50 | console.log(aiModelSchemes); 51 | ``` 52 | 53 | ## License 54 | This package is licensed under the MIT License. -------------------------------------------------------------------------------- /projects/core/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslintNode from '@commit-generator/eslint-config/node.js'; 2 | 3 | export default [...eslintNode]; 4 | -------------------------------------------------------------------------------- /projects/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@commit-generator/core", 3 | "version": "1.0.2", 4 | "description": "Core module for the Commit Generator, providing essential operations and AI model schemes.", 5 | "author": "JulioC090", 6 | "files": [ 7 | "dist" 8 | ], 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "import": "./dist/index.mjs", 13 | "require": "./dist/index.js", 14 | "default": "./dist/index.js" 15 | }, 16 | "./schemes": { 17 | "types": "./dist/schemes.d.ts", 18 | "import": "./dist/schemes.mjs", 19 | "require": "./dist/schemes.js", 20 | "default": "./dist/schemes.js" 21 | } 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/JulioC090/commit-generator.git", 26 | "directory": "projects/core" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/JulioC090/commit-generator/issues" 30 | }, 31 | "license": "MIT", 32 | "scripts": { 33 | "build": "tsup --format cjs,esm", 34 | "clean": "rimraf dist", 35 | "lint": "eslint . --report-unused-disable-directives --max-warnings 0", 36 | "lint:fix": "eslint . --fix", 37 | "test": "vitest run", 38 | "test:watch": "vitest watch", 39 | "test:coverage": "vitest run --coverage", 40 | "typecheck": "tsc --noEmit -p ." 41 | }, 42 | "dependencies": { 43 | "@commit-generator/ai-models": "workspace:^", 44 | "@commit-generator/commit-history": "workspace:^", 45 | "@commit-generator/git": "workspace:^", 46 | "@commit-generator/prompt-parser": "workspace:^" 47 | }, 48 | "devDependencies": { 49 | "@commit-generator/eslint-config": "workspace:^", 50 | "@commit-generator/typescript-config": "workspace:^", 51 | "@types/node": "^22.10.2", 52 | "eslint": "9.19.0" 53 | } 54 | } -------------------------------------------------------------------------------- /projects/core/src/application/AmendGenerated.ts: -------------------------------------------------------------------------------- 1 | import ICommitHistory from '@/types/ICommitHistory'; 2 | import { IGit } from '@commit-generator/git'; 3 | 4 | interface AmendGeneratedProps { 5 | commitHistory: ICommitHistory; 6 | git: IGit; 7 | } 8 | 9 | export default class AmendGenerated { 10 | private commitHistory: ICommitHistory; 11 | private git: IGit; 12 | 13 | constructor({ commitHistory, git }: AmendGeneratedProps) { 14 | this.commitHistory = commitHistory; 15 | this.git = git; 16 | } 17 | 18 | async execute(): Promise { 19 | const history = await this.commitHistory.get(1); 20 | 21 | if (history.length === 0) { 22 | throw new Error('No commits found in history'); 23 | } 24 | 25 | const lastCommitMessage = history[0]; 26 | 27 | this.git.amend(lastCommitMessage); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /projects/core/src/application/CommitGenerated.ts: -------------------------------------------------------------------------------- 1 | import ICommitHistory from '@/types/ICommitHistory'; 2 | import { IGit } from '@commit-generator/git'; 3 | 4 | interface CommitGeneratedProps { 5 | commitHistory: ICommitHistory; 6 | git: IGit; 7 | } 8 | 9 | export default class CommitGenerated { 10 | private commitHistory: ICommitHistory; 11 | private git: IGit; 12 | 13 | constructor({ commitHistory, git }: CommitGeneratedProps) { 14 | this.commitHistory = commitHistory; 15 | this.git = git; 16 | } 17 | 18 | async execute(): Promise { 19 | const history = await this.commitHistory.get(1); 20 | 21 | if (history.length === 0) { 22 | throw new Error('No commits found in history'); 23 | } 24 | 25 | const lastCommitMessage = history[0]; 26 | 27 | this.git.commit(lastCommitMessage); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /projects/core/src/application/EditLastGenerated.ts: -------------------------------------------------------------------------------- 1 | import ICommitHistory from '@/types/ICommitHistory'; 2 | 3 | interface EditLastGeneratedProps { 4 | commitHistory: ICommitHistory; 5 | editMessage: (message: string) => Promise; 6 | } 7 | 8 | export default class EditLastGenerated { 9 | private commitHistory: ICommitHistory; 10 | private editMessage: (message: string) => Promise; 11 | 12 | constructor({ commitHistory, editMessage }: EditLastGeneratedProps) { 13 | this.commitHistory = commitHistory; 14 | this.editMessage = editMessage; 15 | } 16 | 17 | async execute(): Promise { 18 | const history = await this.commitHistory.get(1); 19 | 20 | if (history.length === 0) { 21 | throw new Error('No commits found in history'); 22 | } 23 | 24 | const lastCommitMessage = history[0]; 25 | const newMessage = await this.editMessage(lastCommitMessage); 26 | 27 | if (!newMessage || newMessage.trim() === '') { 28 | throw new Error('Commit message cannot be empty'); 29 | } 30 | 31 | await this.commitHistory.add(newMessage); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /projects/core/src/application/GenerateCommit.ts: -------------------------------------------------------------------------------- 1 | import ICommitGenerator from '@/types/ICommitGenerator'; 2 | import ICommitHistory from '@/types/ICommitHistory'; 3 | import ICommitInfo from '@/types/ICommitInfo'; 4 | import { IGit } from '@commit-generator/git'; 5 | 6 | interface GenerateCommitProps { 7 | commitGenerator: ICommitGenerator; 8 | commitHistory: ICommitHistory; 9 | git: IGit; 10 | excludeFiles?: string[]; 11 | } 12 | 13 | type ExecuteOptions = Omit; 14 | 15 | export default class GenerateCommit { 16 | private commitGenerator: ICommitGenerator; 17 | private commitHistory: ICommitHistory; 18 | private git: IGit; 19 | private excludeFiles?: string[]; 20 | 21 | constructor({ 22 | commitGenerator, 23 | commitHistory, 24 | git, 25 | excludeFiles, 26 | }: GenerateCommitProps) { 27 | this.commitGenerator = commitGenerator; 28 | this.git = git; 29 | this.excludeFiles = excludeFiles; 30 | this.commitHistory = commitHistory; 31 | } 32 | 33 | public async execute(options: ExecuteOptions): Promise { 34 | const diff = this.git.diff({ 35 | staged: true, 36 | excludeFiles: this.excludeFiles, 37 | }); 38 | 39 | if (!diff) { 40 | throw new Error('Error: No staged files found.'); 41 | } 42 | 43 | const previousLogs = this.git.log(5); 44 | 45 | const commitMessage = await this.commitGenerator.generate({ 46 | diff, 47 | previousLogs, 48 | ...options, 49 | }); 50 | 51 | this.commitHistory.add(commitMessage); 52 | 53 | return commitMessage; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /projects/core/src/application/ValidateCommit.ts: -------------------------------------------------------------------------------- 1 | import ICommitHistory from '@/types/ICommitHistory'; 2 | import ICommitValidator, { IValidateResult } from '@/types/ICommitValidator'; 3 | import { IGit } from '@commit-generator/git'; 4 | 5 | interface ValidateCommitProps { 6 | commitValidator: ICommitValidator; 7 | commitHistory: ICommitHistory; 8 | git: IGit; 9 | excludeFiles?: string[]; 10 | } 11 | 12 | interface ExecuteOptions { 13 | commitMessage?: string; 14 | } 15 | 16 | export default class ValidateCommit { 17 | private commitValidator: ICommitValidator; 18 | private git: IGit; 19 | private excludeFiles?: string[]; 20 | private commitHistory: ICommitHistory; 21 | constructor({ 22 | commitValidator, 23 | commitHistory, 24 | git, 25 | excludeFiles, 26 | }: ValidateCommitProps) { 27 | this.commitValidator = commitValidator; 28 | this.git = git; 29 | this.excludeFiles = excludeFiles; 30 | this.commitHistory = commitHistory; 31 | } 32 | 33 | public async execute(options: ExecuteOptions): Promise { 34 | let diff; 35 | let commitMessage; 36 | 37 | if (options.commitMessage) { 38 | diff = this.git.diff({ staged: true, excludeFiles: this.excludeFiles }); 39 | commitMessage = options.commitMessage; 40 | } else { 41 | diff = this.git.diff({ 42 | excludeFiles: this.excludeFiles, 43 | lastCommit: true, 44 | }); 45 | commitMessage = this.git.log(1); 46 | } 47 | 48 | if (!diff) { 49 | throw new Error('No staged files found.'); 50 | } 51 | 52 | const validateResult = await this.commitValidator.validate(commitMessage, { 53 | diff, 54 | }); 55 | 56 | this.commitHistory.add(validateResult.recommendedMessage); 57 | 58 | return validateResult; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /projects/core/src/factories/createAmendGenerated.ts: -------------------------------------------------------------------------------- 1 | import AmendGenerated from '@/application/AmendGenerated'; 2 | import { CommitHistory } from '@commit-generator/commit-history'; 3 | import { git } from '@commit-generator/git'; 4 | 5 | export default function createAmendGenerated(historyPath: string) { 6 | return new AmendGenerated({ 7 | commitHistory: new CommitHistory(historyPath), 8 | git, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /projects/core/src/factories/createCommitGenerated.ts: -------------------------------------------------------------------------------- 1 | import CommitGenerated from '@/application/CommitGenerated'; 2 | import { CommitHistory } from '@commit-generator/commit-history'; 3 | import { git } from '@commit-generator/git'; 4 | 5 | export default function createCommitGenerated(historyPath: string) { 6 | return new CommitGenerated({ 7 | commitHistory: new CommitHistory(historyPath), 8 | git, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /projects/core/src/factories/createEditLastGenerated.ts: -------------------------------------------------------------------------------- 1 | import EditLastGenerated from '@/application/EditLastGenerated'; 2 | import { CommitHistory } from '@commit-generator/commit-history'; 3 | 4 | export default function createEditLastGenerated( 5 | editMessage: (message: string) => Promise, 6 | historyPath: string, 7 | ) { 8 | return new EditLastGenerated({ 9 | commitHistory: new CommitHistory(historyPath), 10 | editMessage, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /projects/core/src/factories/createGenerateCommit.ts: -------------------------------------------------------------------------------- 1 | import GenerateCommit from '@/application/GenerateCommit'; 2 | import CommitGenerator from '@/services/CommitGenerator'; 3 | import { IAIModelParams } from '@/types/IAIModel'; 4 | import { createAIModel } from '@commit-generator/ai-models'; 5 | import { CommitHistory } from '@commit-generator/commit-history'; 6 | import { git } from '@commit-generator/git'; 7 | 8 | export default function createGenerateCommit( 9 | config: { 10 | provider: string; 11 | params: IAIModelParams; 12 | }, 13 | historyPath: string, 14 | excludeFiles: string[], 15 | ) { 16 | const aiModel = createAIModel(config.provider, config.params); 17 | const commitGenerator = new CommitGenerator(aiModel); 18 | 19 | return new GenerateCommit({ 20 | commitGenerator, 21 | commitHistory: new CommitHistory(historyPath), 22 | excludeFiles, 23 | git, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /projects/core/src/factories/createValidateCommit.ts: -------------------------------------------------------------------------------- 1 | import ValidateCommit from '@/application/ValidateCommit'; 2 | import CommitValidator from '@/services/CommitValidator'; 3 | import { IAIModelParams } from '@/types/IAIModel'; 4 | import { createAIModel } from '@commit-generator/ai-models'; 5 | import { CommitHistory } from '@commit-generator/commit-history'; 6 | import { git } from '@commit-generator/git'; 7 | 8 | export default function createValidateCommit( 9 | config: { 10 | provider: string; 11 | params: IAIModelParams; 12 | }, 13 | historyPath: string, 14 | excludeFiles: string[], 15 | ) { 16 | const aiModel = createAIModel(config.provider, config.params); 17 | const commitValidator = new CommitValidator(aiModel); 18 | 19 | return new ValidateCommit({ 20 | commitValidator, 21 | commitHistory: new CommitHistory(historyPath), 22 | excludeFiles, 23 | git, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /projects/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as createAmendGenerated } from '@/factories/createAmendGenerated'; 2 | export { default as createCommitGenerated } from '@/factories/createCommitGenerated'; 3 | export { default as createEditLastGenerated } from '@/factories/createEditLastGenerated'; 4 | export { default as createGenerateCommit } from '@/factories/createGenerateCommit'; 5 | export { default as createValidateCommit } from '@/factories/createValidateCommit'; 6 | -------------------------------------------------------------------------------- /projects/core/src/prompts/generatePrompt.ts: -------------------------------------------------------------------------------- 1 | import ICommitInfo from '@/types/ICommitInfo'; 2 | import { promptParser } from '@commit-generator/prompt-parser'; 3 | 4 | const template = ` 5 | [Intro] 6 | You are an AI specialized in generating commit messages following good practices, 7 | such as the Conventional Commits format (type(scope): description). 8 | 9 | [Rules] 10 | Rules: 11 | 1. Identify the type of change (feat, fix, chore, refactor, docs, test) based on the diff. 12 | 2. Generate one commit message only. 13 | 3. Don't generate something more than one commit message 14 | 15 | [Output] 16 | Expected output: 17 | (scope): 18 | 19 | [Examples][Optional] 20 | {previousLogs} 21 | 22 | [Input][Optional] 23 | Type: {type} 24 | Context: {context} 25 | 26 | [Input] 27 | Diff: {diff} 28 | `; 29 | 30 | export function generatePrompt(commitInfo: ICommitInfo): string { 31 | const prompt = promptParser.parse(template, { ...commitInfo }); 32 | return prompt.toString(); 33 | } 34 | -------------------------------------------------------------------------------- /projects/core/src/prompts/validatePrompt.ts: -------------------------------------------------------------------------------- 1 | import ICommitInfo from '@/types/ICommitInfo'; 2 | import { promptParser } from '@commit-generator/prompt-parser'; 3 | 4 | const template = ` 5 | [Intro] 6 | You are an AI specialized in generating commit messages following good practices, 7 | such as the Conventional Commits format (type(scope): description). 8 | 9 | [Rules] 10 | Rules: 11 | 1. I will send you a commit message and additional information 12 | 2. Your duty is to analyze this information 13 | 3. Respond if this commit is valid, that is, it follows all the desired rules 14 | 4. Generate the commit that would be ideal 15 | 16 | [Output] 17 | Expected output: 18 | { 19 | isValid: boolean, 20 | recommendedMessage: "(scope): ", 21 | analysis: string 22 | } 23 | 24 | [Input] 25 | Commit: "{commitMessage}" 26 | Diff: {diff} 27 | `; 28 | 29 | export default function validatePrompt( 30 | commitMessage: string, 31 | { diff }: ICommitInfo, 32 | ) { 33 | const prompt = promptParser.parse(template, { 34 | commitMessage, 35 | diff, 36 | }); 37 | 38 | return prompt.toString(); 39 | } 40 | -------------------------------------------------------------------------------- /projects/core/src/sanitizers/normalizeJson.ts: -------------------------------------------------------------------------------- 1 | export default function normalizeJson(json: string) { 2 | // Add double quotes around object keys 3 | const normalized = json.replace(/(\w+):/g, '"$1":'); 4 | return normalized; 5 | } 6 | -------------------------------------------------------------------------------- /projects/core/src/sanitizers/sanitize.ts: -------------------------------------------------------------------------------- 1 | export default function sanitize(response: string) { 2 | if (!response || typeof response !== 'string') { 3 | throw new Error('Invalid AI response: Response is empty or not a string'); 4 | } 5 | 6 | return response 7 | .replace(/^```json/, '') 8 | .replace(/^```/, '') 9 | .replace(/```$/, '') 10 | .trim(); 11 | } 12 | -------------------------------------------------------------------------------- /projects/core/src/schemes.ts: -------------------------------------------------------------------------------- 1 | export { 2 | aiModelSchemes, 3 | IAIModelSchemes, 4 | } from '@commit-generator/ai-models/schemes'; 5 | -------------------------------------------------------------------------------- /projects/core/src/services/CommitGenerator.ts: -------------------------------------------------------------------------------- 1 | import { generatePrompt } from '@/prompts/generatePrompt'; 2 | import sanitize from '@/sanitizers/sanitize'; 3 | import IAIModel from '@/types/IAIModel'; 4 | import ICommitGenerator from '@/types/ICommitGenerator'; 5 | import ICommitInfo from '@/types/ICommitInfo'; 6 | 7 | export default class CommitGenerator implements ICommitGenerator { 8 | constructor(private aiModel: IAIModel) {} 9 | 10 | async generate(commitInfo: ICommitInfo): Promise { 11 | const complete = await this.aiModel.complete(generatePrompt(commitInfo)); 12 | return sanitize(complete); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /projects/core/src/services/CommitValidator.ts: -------------------------------------------------------------------------------- 1 | import validatePrompt from '@/prompts/validatePrompt'; 2 | import normalizeJson from '@/sanitizers/normalizeJson'; 3 | import sanitize from '@/sanitizers/sanitize'; 4 | import IAIModel from '@/types/IAIModel'; 5 | import ICommitInfo from '@/types/ICommitInfo'; 6 | import ICommitValidator, { IValidateResult } from '@/types/ICommitValidator'; 7 | 8 | export default class CommitValidator implements ICommitValidator { 9 | constructor(private aiModel: IAIModel) {} 10 | 11 | async validate( 12 | commitMessage: string, 13 | commitInfo: ICommitInfo, 14 | ): Promise { 15 | const complete = await this.aiModel.complete( 16 | validatePrompt(commitMessage, commitInfo), 17 | ); 18 | 19 | const sanitizedResponse = sanitize(complete); 20 | const normalizedResponse = normalizeJson(sanitizedResponse); 21 | 22 | try { 23 | const result = JSON.parse(normalizedResponse) as IValidateResult; 24 | return result; 25 | } catch { 26 | throw new Error(`Invalid AI response: ${normalizedResponse}`); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /projects/core/src/types/IAIModel.ts: -------------------------------------------------------------------------------- 1 | export type IAIModelParams = { [key: string]: unknown }; 2 | 3 | export default interface IAIModel { 4 | complete(prompt: string): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /projects/core/src/types/ICommitGenerator.ts: -------------------------------------------------------------------------------- 1 | import ICommitInfo from '@/types/ICommitInfo'; 2 | 3 | export default interface ICommitGenerator { 4 | generate(commitInfo: ICommitInfo): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /projects/core/src/types/ICommitHistory.ts: -------------------------------------------------------------------------------- 1 | export default interface ICommitHistory { 2 | add(commitMessage: string): Promise; 3 | get(numberOfLines: number): Promise>; 4 | } 5 | -------------------------------------------------------------------------------- /projects/core/src/types/ICommitInfo.ts: -------------------------------------------------------------------------------- 1 | export default interface ICommitInfo { 2 | diff: string; 3 | type?: string; 4 | context?: string; 5 | previousLogs?: string; 6 | } 7 | -------------------------------------------------------------------------------- /projects/core/src/types/ICommitValidator.ts: -------------------------------------------------------------------------------- 1 | import ICommitInfo from '@/types/ICommitInfo'; 2 | 3 | export interface IValidateResult { 4 | isValid: boolean; 5 | recommendedMessage: string; 6 | analysis: string; 7 | } 8 | 9 | export default interface ICommitValidator { 10 | validate( 11 | commitMessage: string, 12 | commitInfo: ICommitInfo, 13 | ): Promise; 14 | } 15 | -------------------------------------------------------------------------------- /projects/core/tests/unit/application/AmendGenerated.spec.ts: -------------------------------------------------------------------------------- 1 | import AmendGenerated from '@/application/AmendGenerated'; 2 | import ICommitHistory from '@/types/ICommitHistory'; 3 | import { IGit } from '@commit-generator/git'; 4 | import { beforeEach, describe, expect, it, vi } from 'vitest'; 5 | 6 | const mockCommitHistory = { 7 | add: vi.fn(), 8 | get: vi.fn(), 9 | } as unknown as ICommitHistory; 10 | 11 | const mockGit = { 12 | amend: vi.fn(), 13 | } as unknown as IGit; 14 | 15 | describe('AmendGenerated', () => { 16 | let sut: AmendGenerated; 17 | 18 | beforeEach(() => { 19 | sut = new AmendGenerated({ 20 | commitHistory: mockCommitHistory, 21 | git: mockGit, 22 | }); 23 | 24 | vi.resetAllMocks(); 25 | }); 26 | 27 | it('should amend the last commit with the latest generated message', async () => { 28 | vi.mocked(mockCommitHistory.get).mockResolvedValueOnce([ 29 | 'Fix bug in authentication', 30 | ]); 31 | 32 | await sut.execute(); 33 | 34 | expect(mockCommitHistory.get).toHaveBeenCalledWith(1); 35 | expect(mockGit.amend).toHaveBeenCalledWith('Fix bug in authentication'); 36 | }); 37 | 38 | it('should throw an error if no commits are found', async () => { 39 | vi.mocked(mockCommitHistory.get).mockResolvedValueOnce([]); 40 | 41 | await expect(sut.execute()).rejects.toThrow('No commits found in history'); 42 | 43 | expect(mockCommitHistory.get).toHaveBeenCalledWith(1); 44 | expect(mockGit.amend).not.toHaveBeenCalled(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /projects/core/tests/unit/application/CommitGenerated.spec.ts: -------------------------------------------------------------------------------- 1 | import CommitGenerated from '@/application/CommitGenerated'; 2 | import ICommitHistory from '@/types/ICommitHistory'; 3 | import { IGit } from '@commit-generator/git'; 4 | import { beforeEach, describe, expect, it, vi } from 'vitest'; 5 | 6 | const mockCommitHistory = { 7 | add: vi.fn(), 8 | get: vi.fn(), 9 | } as unknown as ICommitHistory; 10 | 11 | const mockGitMock = { 12 | commit: vi.fn(), 13 | } as unknown as IGit; 14 | 15 | describe('CommitGenerated', () => { 16 | let sut: CommitGenerated; 17 | 18 | beforeEach(() => { 19 | sut = new CommitGenerated({ 20 | commitHistory: mockCommitHistory, 21 | git: mockGitMock, 22 | }); 23 | 24 | vi.resetAllMocks(); 25 | }); 26 | 27 | it('should commit the last commit message if history exists', async () => { 28 | vi.mocked(mockCommitHistory.get).mockResolvedValueOnce(['Initial commit']); 29 | 30 | await sut.execute(); 31 | 32 | expect(mockCommitHistory.get).toHaveBeenCalledWith(1); 33 | expect(mockGitMock.commit).toHaveBeenCalledWith('Initial commit'); 34 | }); 35 | 36 | it('should throw an error if no commits are found in history', async () => { 37 | vi.mocked(mockCommitHistory.get).mockResolvedValueOnce([]); 38 | 39 | await expect(sut.execute()).rejects.toThrow('No commits found in history'); 40 | 41 | expect(mockCommitHistory.get).toHaveBeenCalledWith(1); 42 | expect(mockGitMock.commit).not.toHaveBeenCalled(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /projects/core/tests/unit/application/EditLastGenerated.spec.ts: -------------------------------------------------------------------------------- 1 | import EditLastGenerated from '@/application/EditLastGenerated'; 2 | import ICommitHistory from '@/types/ICommitHistory'; 3 | import { beforeEach, describe, expect, it, vi } from 'vitest'; 4 | 5 | const mockCommitHistory = { 6 | add: vi.fn(), 7 | get: vi.fn().mockResolvedValue(['Initial commit']), 8 | } as unknown as ICommitHistory; 9 | 10 | const mockEditMessage = vi.fn(); 11 | 12 | describe('EditLastGenerated', () => { 13 | let sut: EditLastGenerated; 14 | 15 | beforeEach(() => { 16 | vi.resetAllMocks(); 17 | 18 | sut = new EditLastGenerated({ 19 | commitHistory: mockCommitHistory, 20 | editMessage: mockEditMessage, 21 | }); 22 | }); 23 | 24 | it('should edit the last commit message and save it to history', async () => { 25 | vi.mocked(mockCommitHistory.get).mockResolvedValueOnce(['Initial commit']); 26 | vi.mocked(mockEditMessage).mockReturnValueOnce('Updated commit message'); 27 | 28 | await sut.execute(); 29 | 30 | expect(mockCommitHistory.get).toHaveBeenCalledWith(1); 31 | expect(mockEditMessage).toHaveBeenCalledWith('Initial commit'); 32 | expect(mockCommitHistory.add).toHaveBeenCalledWith( 33 | 'Updated commit message', 34 | ); 35 | }); 36 | 37 | it('should throw an error if no commits are found', async () => { 38 | vi.mocked(mockCommitHistory.get).mockResolvedValueOnce([]); 39 | 40 | await expect(sut.execute()).rejects.toThrow('No commits found in history'); 41 | 42 | expect(mockCommitHistory.get).toHaveBeenCalledWith(1); 43 | expect(mockCommitHistory.add).not.toHaveBeenCalled(); 44 | }); 45 | 46 | it('should throw an error if the new commit message is empty', async () => { 47 | vi.mocked(mockCommitHistory.get).mockResolvedValueOnce(['Initial commit']); 48 | vi.mocked(mockEditMessage).mockReturnValueOnce(''); 49 | 50 | await expect(sut.execute()).rejects.toThrow( 51 | 'Commit message cannot be empty', 52 | ); 53 | 54 | expect(mockCommitHistory.get).toHaveBeenCalledWith(1); 55 | expect(mockEditMessage).toHaveBeenCalledWith('Initial commit'); 56 | expect(mockCommitHistory.add).not.toHaveBeenCalled(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /projects/core/tests/unit/application/GenerateCommit.spec.ts: -------------------------------------------------------------------------------- 1 | import GenerateCommit from '@/application/GenerateCommit'; 2 | import ICommitHistory from '@/types/ICommitHistory'; 3 | import { IGit } from '@commit-generator/git'; 4 | import { describe, expect, it, vi } from 'vitest'; 5 | 6 | const mockGit = { 7 | isRepository: vi.fn(), 8 | diff: vi.fn(), 9 | commit: vi.fn(), 10 | log: vi.fn(), 11 | } as unknown as IGit; 12 | 13 | const mockCommitGenerator = { 14 | generate: vi.fn(), 15 | }; 16 | 17 | const mockCommitHistory = { 18 | add: vi.fn(), 19 | get: vi.fn(), 20 | } as unknown as ICommitHistory; 21 | 22 | describe('GenerateCommit', () => { 23 | it('should generate a commit', async () => { 24 | vi.mocked(mockGit.isRepository).mockReturnValue(true); 25 | vi.mocked(mockGit.diff).mockReturnValue('some diff'); 26 | mockCommitGenerator.generate.mockResolvedValue('commit message'); 27 | 28 | const sut = new GenerateCommit({ 29 | commitGenerator: mockCommitGenerator, 30 | git: mockGit, 31 | commitHistory: mockCommitHistory, 32 | }); 33 | 34 | const result = await sut.execute({ type: 'feat', context: 'some context' }); 35 | 36 | expect(mockCommitHistory.add).toHaveBeenCalledWith('commit message'); 37 | expect(mockCommitGenerator.generate).toHaveBeenCalledWith({ 38 | diff: 'some diff', 39 | type: 'feat', 40 | context: 'some context', 41 | }); 42 | expect(result).toBe('commit message'); 43 | }); 44 | 45 | it('should throw if there are no staged files', async () => { 46 | vi.mocked(mockGit.isRepository).mockReturnValue(true); 47 | vi.mocked(mockGit.diff).mockReturnValue(''); 48 | 49 | const sut = new GenerateCommit({ 50 | commitGenerator: mockCommitGenerator, 51 | commitHistory: mockCommitHistory, 52 | git: mockGit, 53 | }); 54 | 55 | await expect(sut.execute({})).rejects.toThrow( 56 | 'Error: No staged files found.', 57 | ); 58 | }); 59 | 60 | it('should save commit in history', async () => { 61 | vi.mocked(mockGit.isRepository).mockReturnValue(true); 62 | vi.mocked(mockGit.diff).mockReturnValue('some diff'); 63 | mockCommitGenerator.generate.mockResolvedValue('commit message'); 64 | 65 | const sut = new GenerateCommit({ 66 | commitGenerator: mockCommitGenerator, 67 | git: mockGit, 68 | commitHistory: mockCommitHistory, 69 | }); 70 | 71 | await sut.execute({}); 72 | 73 | expect(mockCommitHistory.add).toHaveBeenCalledWith('commit message'); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /projects/core/tests/unit/application/ValidateCommit.spec.ts: -------------------------------------------------------------------------------- 1 | import ValidateCommit from '@/application/ValidateCommit'; 2 | import ICommitHistory from '@/types/ICommitHistory'; 3 | import ICommitValidator from '@/types/ICommitValidator'; 4 | import { IGit } from '@commit-generator/git'; 5 | import { beforeEach, describe, expect, it, vi } from 'vitest'; 6 | 7 | describe('ValidateCommit', () => { 8 | let sut: ValidateCommit; 9 | let mockCommitValidator: ICommitValidator; 10 | let mockCommitHistory: ICommitHistory; 11 | let mockGit: IGit; 12 | 13 | beforeEach(() => { 14 | vi.resetAllMocks(); 15 | 16 | mockCommitValidator = { 17 | validate: vi.fn(), 18 | }; 19 | 20 | mockGit = { 21 | diff: vi.fn(), 22 | log: vi.fn(), 23 | } as unknown as IGit; 24 | 25 | mockCommitHistory = { 26 | add: vi.fn(), 27 | get: vi.fn(), 28 | } as unknown as ICommitHistory; 29 | 30 | sut = new ValidateCommit({ 31 | commitValidator: mockCommitValidator, 32 | commitHistory: mockCommitHistory, 33 | git: mockGit, 34 | }); 35 | }); 36 | 37 | it('should throw if there are no staged files', async () => { 38 | vi.mocked(mockGit.diff).mockReturnValueOnce(''); 39 | 40 | await expect( 41 | sut.execute({ 42 | commitMessage: 'fix: adjust login flow', 43 | }), 44 | ).rejects.toThrow('No staged files found.'); 45 | }); 46 | 47 | it('should validate a staged commit message', async () => { 48 | vi.mocked(mockGit.diff).mockReturnValueOnce('mocked diff'); 49 | vi.mocked(mockCommitValidator.validate).mockResolvedValueOnce({ 50 | isValid: true, 51 | recommendedMessage: 'feat(auth): improve login security', 52 | analysis: 'Ok', 53 | }); 54 | 55 | const result = await sut.execute({ 56 | commitMessage: 'fix: adjust login flow', 57 | }); 58 | 59 | expect(mockGit.diff).toHaveBeenCalledWith({ 60 | staged: true, 61 | excludeFiles: undefined, 62 | }); 63 | expect(mockCommitValidator.validate).toHaveBeenCalledWith( 64 | 'fix: adjust login flow', 65 | { diff: 'mocked diff' }, 66 | ); 67 | expect(mockCommitHistory.add).toHaveBeenCalledWith( 68 | 'feat(auth): improve login security', 69 | ); 70 | expect(result).toEqual({ 71 | isValid: true, 72 | recommendedMessage: 'feat(auth): improve login security', 73 | analysis: 'Ok', 74 | }); 75 | }); 76 | 77 | it('should validate the last commit if no commit message is provided', async () => { 78 | vi.mocked(mockGit.diff).mockReturnValueOnce('mocked diff'); 79 | vi.mocked(mockGit.log).mockReturnValueOnce('Initial commit message'); 80 | vi.mocked(mockCommitValidator.validate).mockResolvedValueOnce({ 81 | isValid: true, 82 | recommendedMessage: 'feat(auth): improve login security', 83 | analysis: 'Ok', 84 | }); 85 | 86 | const validateCommit = new ValidateCommit({ 87 | commitValidator: mockCommitValidator, 88 | git: mockGit, 89 | commitHistory: mockCommitHistory, 90 | }); 91 | 92 | const result = await validateCommit.execute({}); 93 | 94 | expect(mockGit.diff).toHaveBeenCalledWith({ 95 | excludeFiles: undefined, 96 | lastCommit: true, 97 | }); 98 | expect(mockGit.log).toHaveBeenCalledWith(1); 99 | expect(mockCommitValidator.validate).toHaveBeenCalledWith( 100 | 'Initial commit message', 101 | { diff: 'mocked diff' }, 102 | ); 103 | expect(mockCommitHistory.add).toHaveBeenCalledWith( 104 | 'feat(auth): improve login security', 105 | ); 106 | expect(result).toEqual({ 107 | isValid: true, 108 | recommendedMessage: 'feat(auth): improve login security', 109 | analysis: 'Ok', 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /projects/core/tests/unit/prompts/generatePrompt.spec.ts: -------------------------------------------------------------------------------- 1 | import { generatePrompt } from '@/prompts/generatePrompt'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('generatePrompt', () => { 5 | it('should generate a prompt with all provided inputs', () => { 6 | const diff = 'Added new functionality to the login module'; 7 | const type = 'feat'; 8 | const context = 'Improve security'; 9 | const previousLogs = 10 | 'feat(auth): added login endpoint\nfix(auth): resolved login issue'; 11 | 12 | const result = generatePrompt({ diff, type, context, previousLogs }); 13 | 14 | expect(result.includes(diff)).toBeTruthy(); 15 | expect(result.includes('Type: ' + type)).toBeTruthy(); 16 | expect(result.includes('Context: ' + context)).toBeTruthy(); 17 | expect(result.includes(previousLogs)).toBeTruthy(); 18 | }); 19 | 20 | it('should generate a prompt without previous logs when not provided', () => { 21 | const diff = 'Fixed a bug in the payment module'; 22 | const type = 'fix'; 23 | const previousLogs = undefined; 24 | 25 | const result = generatePrompt({ diff, type, previousLogs }); 26 | 27 | expect(result.includes(diff)).toBeTruthy(); 28 | expect(result.includes('Type: ' + type)).toBeTruthy(); 29 | expect(result.includes('Examples:')).toBeFalsy(); 30 | }); 31 | 32 | it('should generate a prompt without type when not provided', () => { 33 | const diff = 'Refactored the user profile module'; 34 | const type = undefined; 35 | const previousLogs = 36 | 'refactor(user): cleaned up user module\nfeat(profile): added profile picture upload'; 37 | 38 | const result = generatePrompt({ diff, type, previousLogs }); 39 | 40 | expect(result.includes(diff)).toBeTruthy(); 41 | expect(result.includes('Type: ')).toBeFalsy(); 42 | expect(result.includes(previousLogs)).toBeTruthy(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /projects/core/tests/unit/prompts/validatePrompt.spec.ts: -------------------------------------------------------------------------------- 1 | import validatePrompt from '@/prompts/validatePrompt'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('validatePrompt', () => { 5 | it('should generate a structured prompt', () => { 6 | const commitMessage = 'feat(auth): add login validation'; 7 | const commitInfo = { diff: 'add login diff' }; 8 | 9 | const prompt = validatePrompt(commitMessage, commitInfo); 10 | 11 | expect(prompt).toContain( 12 | 'You are an AI specialized in generating commit messages', 13 | ); 14 | expect(prompt).toContain('Rules:'); 15 | expect(prompt).toContain('Expected output:'); 16 | expect(prompt).toContain(`Commit: "${commitMessage}"`); 17 | expect(prompt).toContain(`Diff: ${commitInfo.diff}`); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /projects/core/tests/unit/sanitizers/normalizeJson.spec.ts: -------------------------------------------------------------------------------- 1 | import normalizeJson from '@/sanitizers/normalizeJson'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('normalizeJson', () => { 5 | it('should normalize a JSON string with missing quotes around keys', () => { 6 | const input = `{ 7 | isValid: true, 8 | recommendedMessage: "feat(cli): add validate command for commit message validation", 9 | analysis: "The commit message follows the Conventional Commits format. It identifies the type as 'feat', specifies the scope as 'cli', and provides a concise description of the new feature being added, which is appropriate given the context of the diff where a new command for validation is added to the CLI." 10 | }`; 11 | 12 | const expectedOutput = `{ 13 | "isValid": true, 14 | "recommendedMessage": "feat(cli): add validate command for commit message validation", 15 | "analysis": "The commit message follows the Conventional Commits format. It identifies the type as 'feat', specifies the scope as 'cli', and provides a concise description of the new feature being added, which is appropriate given the context of the diff where a new command for validation is added to the CLI." 16 | }`; 17 | 18 | const result = normalizeJson(input); 19 | 20 | expect(result).toBe(expectedOutput); 21 | expect(JSON.parse(result)).toEqual({ 22 | isValid: true, 23 | recommendedMessage: 24 | 'feat(cli): add validate command for commit message validation', 25 | analysis: 26 | "The commit message follows the Conventional Commits format. It identifies the type as 'feat', specifies the scope as 'cli', and provides a concise description of the new feature being added, which is appropriate given the context of the diff where a new command for validation is added to the CLI.", 27 | }); 28 | }); 29 | 30 | it('should handle an already correctly formatted JSON string', () => { 31 | const input = `{ 32 | "isValid": true, 33 | "recommendedMessage": "feat(cli): add validate command for commit message validation", 34 | "analysis": "The commit message follows the Conventional Commits format." 35 | }`; 36 | 37 | const result = normalizeJson(input); 38 | 39 | expect(result).toBe(input); 40 | expect(JSON.parse(result)).toEqual({ 41 | isValid: true, 42 | recommendedMessage: 43 | 'feat(cli): add validate command for commit message validation', 44 | analysis: 'The commit message follows the Conventional Commits format.', 45 | }); 46 | }); 47 | 48 | it('should handle empty strings gracefully', () => { 49 | const input = ''; 50 | const result = normalizeJson(input); 51 | expect(result).toBe(''); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /projects/core/tests/unit/sanitizers/sanitize.spec.ts: -------------------------------------------------------------------------------- 1 | import sanitize from '@/sanitizers/sanitize'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | describe('sanitize', () => { 5 | it('should remove ```json and ``` from a formatted response', () => { 6 | const response = `\`\`\`json 7 | { 8 | "isValid": true, 9 | "recommendedMessage": "feat(auth): add login validation", 10 | "analysis": "The commit follows the best practices" 11 | } 12 | \`\`\``; 13 | 14 | expect(sanitize(response)).toBe( 15 | `{ 16 | "isValid": true, 17 | "recommendedMessage": "feat(auth): add login validation", 18 | "analysis": "The commit follows the best practices" 19 | }`.trim(), 20 | ); 21 | }); 22 | 23 | it('should remove ``` from a formatted response', () => { 24 | const response = `\`\`\` 25 | { 26 | "isValid": false, 27 | "recommendedMessage": "fix: correct login bug", 28 | "analysis": "Message does not follow the proper format" 29 | } 30 | \`\`\``; 31 | 32 | expect(sanitize(response)).toBe( 33 | `{ 34 | "isValid": false, 35 | "recommendedMessage": "fix: correct login bug", 36 | "analysis": "Message does not follow the proper format" 37 | }`.trim(), 38 | ); 39 | }); 40 | 41 | it('should return the response unchanged if no formatting is present', () => { 42 | const response = `{ 43 | "isValid": true, 44 | "recommendedMessage": "feat: improve performance", 45 | "analysis": "Good commit message" 46 | }`; 47 | 48 | expect(sanitize(response)).toBe(response.trim()); 49 | }); 50 | 51 | it('should throw an error if response is empty', () => { 52 | expect(() => sanitize('')).toThrow('Invalid AI response'); 53 | }); 54 | 55 | it('should throw an error if response is not a string', () => { 56 | expect(() => sanitize(null as unknown as string)).toThrow( 57 | 'Invalid AI response', 58 | ); 59 | expect(() => sanitize(undefined as unknown as string)).toThrow( 60 | 'Invalid AI response', 61 | ); 62 | expect(() => sanitize(42 as unknown as string)).toThrow( 63 | 'Invalid AI response', 64 | ); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /projects/core/tests/unit/services/CommitGenerator.spec.ts: -------------------------------------------------------------------------------- 1 | import sanitize from '@/sanitizers/sanitize'; 2 | import CommitGenerator from '@/services/CommitGenerator'; 3 | import IAIModel from '@/types/IAIModel'; 4 | import ICommitInfo from '@/types/ICommitInfo'; 5 | import { describe, expect, it, vi } from 'vitest'; 6 | 7 | vi.mock('@/prompts/generatePrompt', () => ({ 8 | generatePrompt: vi.fn(), 9 | })); 10 | 11 | vi.mock('@/sanitizers/sanitize', () => ({ 12 | default: vi.fn(), 13 | })); 14 | 15 | describe('CommitGenerator', () => { 16 | it('should generate a commit using the AI model', async () => { 17 | const fakeCommitInfo: ICommitInfo = { 18 | diff: 'some diff', 19 | type: 'type', 20 | previousLogs: 'logs', 21 | }; 22 | const fakeCompletion = 'Fake commit message from AI model'; 23 | const aiModelMock = { 24 | complete: vi.fn().mockResolvedValue(fakeCompletion), 25 | } as unknown as IAIModel; 26 | 27 | const sut = new CommitGenerator(aiModelMock); 28 | 29 | vi.mocked(sanitize).mockReturnValue(fakeCompletion); 30 | 31 | const commitMessage = await sut.generate(fakeCommitInfo); 32 | 33 | expect(aiModelMock.complete).toHaveBeenCalledTimes(1); 34 | expect(sanitize).toHaveBeenCalledWith(fakeCompletion); 35 | expect(commitMessage).toBe(fakeCompletion); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /projects/core/tests/unit/services/CommitValidator.spec.ts: -------------------------------------------------------------------------------- 1 | import normalizeJson from '@/sanitizers/normalizeJson'; 2 | import sanitize from '@/sanitizers/sanitize'; 3 | import CommitValidator from '@/services/CommitValidator'; 4 | import IAIModel from '@/types/IAIModel'; 5 | import ICommitInfo from '@/types/ICommitInfo'; 6 | import { describe, expect, it, vi } from 'vitest'; 7 | 8 | vi.mock('@/prompts/validatePrompt', () => ({ 9 | default: vi.fn(), 10 | })); 11 | 12 | vi.mock('@/sanitizers/normalizeJson', () => ({ 13 | default: vi.fn(), 14 | })); 15 | 16 | vi.mock('@/sanitizers/sanitize', () => ({ 17 | default: vi.fn(), 18 | })); 19 | 20 | describe('CommitValidator', () => { 21 | it('should validate a commit message using the AI model', async () => { 22 | const fakeCommitMessage = 'Fake commit message'; 23 | const fakeCommitInfo: ICommitInfo = { 24 | diff: 'some diff', 25 | type: 'type', 26 | previousLogs: 'logs', 27 | }; 28 | const fakeAIResponse = '{"isValid": true}'; 29 | const mockAIModel = { 30 | complete: vi.fn().mockResolvedValue(fakeAIResponse), 31 | } as unknown as IAIModel; 32 | 33 | vi.mocked(sanitize).mockReturnValue(fakeAIResponse); 34 | vi.mocked(normalizeJson).mockReturnValue(fakeAIResponse); 35 | 36 | const commitValidator = new CommitValidator(mockAIModel); 37 | 38 | const validationResult = await commitValidator.validate( 39 | fakeCommitMessage, 40 | fakeCommitInfo, 41 | ); 42 | 43 | expect(mockAIModel.complete).toHaveBeenCalledTimes(1); 44 | expect(sanitize).toHaveBeenCalledWith(fakeAIResponse); 45 | expect(normalizeJson).toHaveBeenCalledWith(fakeAIResponse); 46 | expect(validationResult).toEqual({ isValid: true }); 47 | }); 48 | 49 | it('should throw an error if AI response is invalid', async () => { 50 | const fakeCommitMessage = 'Fake commit message'; 51 | const fakeCommitInfo: ICommitInfo = { 52 | diff: 'some diff', 53 | type: 'type', 54 | previousLogs: 'logs', 55 | }; 56 | const fakeAIResponse = 'invalid json'; 57 | const mockAIModel = { 58 | complete: vi.fn().mockResolvedValue(fakeAIResponse), 59 | } as unknown as IAIModel; 60 | 61 | vi.mocked(sanitize).mockReturnValue(fakeAIResponse); 62 | vi.mocked(normalizeJson).mockReturnValue(fakeAIResponse); 63 | 64 | const commitValidator = new CommitValidator(mockAIModel); 65 | 66 | await expect( 67 | commitValidator.validate(fakeCommitMessage, fakeCommitInfo), 68 | ).rejects.toThrow('Invalid AI response: invalid json'); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /projects/core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["tests"] 4 | } -------------------------------------------------------------------------------- /projects/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@commit-generator/typescript-config/base.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "rootDirs": ["src", "tests"], 6 | "outDir": "dist", 7 | "paths": { 8 | "@/tests/*": ["../tests/*"], 9 | "@/*": ["*"] 10 | }, 11 | }, 12 | "include": ["src", "tests"], 13 | "exclude": [] 14 | } -------------------------------------------------------------------------------- /projects/core/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/schemes.ts'], 5 | clean: true, 6 | tsconfig: 'tsconfig.build.json', 7 | treeshake: true, 8 | minify: true, 9 | shims: true, 10 | dts: true, 11 | }); 12 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "transit": { 5 | "dependsOn": ["^transit"] 6 | }, 7 | "precommit": { 8 | "dependsOn": ["^transit"], 9 | "cache": true 10 | }, 11 | "clean": { 12 | "cache": false 13 | }, 14 | "build": { 15 | "dependsOn": ["^build"], 16 | "outputs": ["dist/**"], 17 | "cache": true 18 | }, 19 | "lint": { 20 | "cache": true 21 | }, 22 | "lint:fix": { 23 | "cache": false 24 | }, 25 | "typecheck": { 26 | "dependsOn": ["^transit"], 27 | "cache": true 28 | }, 29 | "test": { 30 | "cache": true 31 | }, 32 | "test:coverage": { 33 | "cache": false 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | }); 7 | --------------------------------------------------------------------------------