├── .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 |
--------------------------------------------------------------------------------