mirror of
https://github.com/di-sukharev/opencommit.git
synced 2026-01-14 16:18:02 -05:00
Compare commits
74 Commits
v3.0.16
...
refactorin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88d6a92567 | ||
|
|
36aab60393 | ||
|
|
ba3c0b7e0b | ||
|
|
fa1cf46050 | ||
|
|
5209610236 | ||
|
|
d1f03f1105 | ||
|
|
3df5b241dc | ||
|
|
5ddf2cb21a | ||
|
|
1d19ddd9e2 | ||
|
|
c6b6b2f3fd | ||
|
|
e1ce774538 | ||
|
|
f1ea54cf1c | ||
|
|
ccb7cd99e5 | ||
|
|
ce6ae0b514 | ||
|
|
69b3c00a52 | ||
|
|
6f4afbfb52 | ||
|
|
796de7b07e | ||
|
|
9ad281a4ee | ||
|
|
1ce357b023 | ||
|
|
45dd07d229 | ||
|
|
fa164377e4 | ||
|
|
0b89767de0 | ||
|
|
2dded4caa4 | ||
|
|
670f74ebc7 | ||
|
|
89d2aa603b | ||
|
|
8702c17758 | ||
|
|
60597d23eb | ||
|
|
6f04927369 | ||
|
|
0c0cf9c627 | ||
|
|
8fe8e614ac | ||
|
|
68c9ed359c | ||
|
|
1b29f3a9fd | ||
|
|
596dcd7cea | ||
|
|
eb3be62a4f | ||
|
|
58ad1880ef | ||
|
|
5ee17235cb | ||
|
|
f9c7316eb3 | ||
|
|
dfe2730a45 | ||
|
|
a979ba091a | ||
|
|
6bbe07a9a1 | ||
|
|
0156bb9dc9 | ||
|
|
e27bbd0ac1 | ||
|
|
f3371ac1e3 | ||
|
|
9cc9f9d757 | ||
|
|
c8d5c53db1 | ||
|
|
fc4326233d | ||
|
|
07f7a05c4b | ||
|
|
9b2e41d255 | ||
|
|
6fad862aa5 | ||
|
|
c425878f21 | ||
|
|
1fb592afb2 | ||
|
|
684f3dadfc | ||
|
|
76e6070012 | ||
|
|
cb72837866 | ||
|
|
2432ef9de3 | ||
|
|
026fd1822a | ||
|
|
b42a52ef24 | ||
|
|
10b031ab36 | ||
|
|
d63b825ae5 | ||
|
|
b66da48106 | ||
|
|
6f24afc600 | ||
|
|
a80dcb03c4 | ||
|
|
bebbed856f | ||
|
|
2d5882c257 | ||
|
|
37fb140563 | ||
|
|
aebe7e200f | ||
|
|
6f1e4bcec6 | ||
|
|
2059549dce | ||
|
|
8361dc6838 | ||
|
|
73ccae9de3 | ||
|
|
c58e0c62a4 | ||
|
|
a4b4e65011 | ||
|
|
18f52772b3 | ||
|
|
fef25a2d06 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,4 +10,5 @@ uncaughtExceptions.log
|
|||||||
.vscode
|
.vscode
|
||||||
src/*.json
|
src/*.json
|
||||||
.idea
|
.idea
|
||||||
test.ts
|
test.ts
|
||||||
|
notes.md
|
||||||
114
README.md
114
README.md
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<img src=".github/logo-grad.svg" alt="OpenCommit logo"/>
|
<img src=".github/logo-grad.svg" alt="OpenCommit logo"/>
|
||||||
<h1 align="center">OpenCommit</h1>
|
<h1 align="center">OpenCommit</h1>
|
||||||
<h4 align="center">Follow the bird <a href="https://twitter.com/_sukharev_"><img src="https://img.shields.io/twitter/follow/_sukharev_?style=flat&label=_sukharev_&logo=twitter&color=0bf&logoColor=fff" align="center"></a>
|
<h4 align="center">Author <a href="https://twitter.com/_sukharev_"><img src="https://img.shields.io/twitter/follow/_sukharev_?style=flat&label=_sukharev_&logo=twitter&color=0bf&logoColor=fff" align="center"></a>
|
||||||
</div>
|
</div>
|
||||||
<h2>Auto-generate meaningful commits in a second</h2>
|
<h2>Auto-generate meaningful commits in a second</h2>
|
||||||
<p>Killing lame commits with AI 🤯🔫</p>
|
<p>Killing lame commits with AI 🤯🔫</p>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<img src=".github/opencommit-example.png" alt="OpenCommit example"/>
|
<img src=".github/opencommit-example.png" alt="OpenCommit example"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
All the commits in this repo are authored by OpenCommit — look at [the commits](https://github.com/di-sukharev/opencommit/commit/eae7618d575ee8d2e9fff5de56da79d40c4bc5fc) to see how OpenCommit works. Emojis and long commit descriptions are configurable.
|
All the commits in this repo are authored by OpenCommit — look at [the commits](https://github.com/di-sukharev/opencommit/commit/eae7618d575ee8d2e9fff5de56da79d40c4bc5fc) to see how OpenCommit works. Emojis and long commit descriptions are configurable, basically everything is.
|
||||||
|
|
||||||
## Setup OpenCommit as a CLI tool
|
## Setup OpenCommit as a CLI tool
|
||||||
|
|
||||||
@@ -28,37 +28,28 @@ You can use OpenCommit by simply running it via the CLI like this `oco`. 2 secon
|
|||||||
npm install -g opencommit
|
npm install -g opencommit
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively run it via `npx opencommit` or `bunx opencommit`
|
2. Get your API key from [OpenAI](https://platform.openai.com/account/api-keys) or other supported LLM providers (we support them all). Make sure that you add your OpenAI payment details to your account, so the API works.
|
||||||
|
|
||||||
MacOS may ask to run the command with `sudo` when installing a package globally.
|
|
||||||
|
|
||||||
2. Get your API key from [OpenAI](https://platform.openai.com/account/api-keys). Make sure that you add your payment details, so the API works.
|
|
||||||
|
|
||||||
3. Set the key to OpenCommit config:
|
3. Set the key to OpenCommit config:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
oco config set OCO_OPENAI_API_KEY=<your_api_key>
|
oco config set OCO_API_KEY=<your_api_key>
|
||||||
```
|
```
|
||||||
|
|
||||||
Your API key is stored locally in the `~/.opencommit` config file.
|
Your API key is stored locally in the `~/.opencommit` config file.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
You can call OpenCommit directly to generate a commit message for your staged changes:
|
You can call OpenCommit with `oco` command to generate a commit message for your staged changes:
|
||||||
|
|
||||||
```sh
|
|
||||||
git add <files...>
|
|
||||||
opencommit
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also use the `oco` shortcut:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git add <files...>
|
git add <files...>
|
||||||
oco
|
oco
|
||||||
```
|
```
|
||||||
|
|
||||||
Link to the GitMoji specification: https://gitmoji.dev/
|
Running `git add` is optional, `oco` will do it for you.
|
||||||
|
|
||||||
|
### Running locally with Ollama
|
||||||
|
|
||||||
You can also run it with local model through ollama:
|
You can also run it with local model through ollama:
|
||||||
|
|
||||||
@@ -68,16 +59,31 @@ You can also run it with local model through ollama:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
git add <files...>
|
git add <files...>
|
||||||
OCO_AI_PROVIDER='ollama' opencommit
|
oco config set OCO_AI_PROVIDER='ollama' OCO_MODEL='llama3:8b'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Default model is `mistral`.
|
||||||
|
|
||||||
|
If you have ollama that is set up in docker/ on another machine with GPUs (not locally), you can change the default endpoint url.
|
||||||
|
|
||||||
|
You can do so by setting the `OCO_API_URL` environment variable as follows:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco config set OCO_API_URL='http://192.168.1.10:11434/api/chat'
|
||||||
|
```
|
||||||
|
|
||||||
|
where 192.168.1.10 is example of endpoint URL, where you have ollama set up.
|
||||||
|
|
||||||
### Flags
|
### Flags
|
||||||
|
|
||||||
There are multiple optional flags that can be used with the `oco` command:
|
There are multiple optional flags that can be used with the `oco` command:
|
||||||
|
|
||||||
#### Use Full GitMoji Specification
|
#### Use Full GitMoji Specification
|
||||||
|
|
||||||
|
Link to the GitMoji specification: https://gitmoji.dev/
|
||||||
|
|
||||||
This flag can only be used if the `OCO_EMOJI` configuration item is set to `true`. This flag allows users to use all emojis in the GitMoji specification, By default, the GitMoji full specification is set to `false`, which only includes 10 emojis (🐛✨📝🚀✅♻️⬆️🔧🌐💡).
|
This flag can only be used if the `OCO_EMOJI` configuration item is set to `true`. This flag allows users to use all emojis in the GitMoji specification, By default, the GitMoji full specification is set to `false`, which only includes 10 emojis (🐛✨📝🚀✅♻️⬆️🔧🌐💡).
|
||||||
|
|
||||||
This is due to limit the number of tokens sent in each request. However, if you would like to use the full GitMoji specification, you can use the `--fgm` flag.
|
This is due to limit the number of tokens sent in each request. However, if you would like to use the full GitMoji specification, you can use the `--fgm` flag.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -99,19 +105,23 @@ oco --yes
|
|||||||
Create a `.env` file and add OpenCommit config variables there like this:
|
Create a `.env` file and add OpenCommit config variables there like this:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
OCO_OPENAI_API_KEY=<your OpenAI API token>
|
...
|
||||||
|
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama, gemini, flowise>
|
||||||
|
OCO_API_KEY=<your OpenAI API token> // or other LLM provider API token
|
||||||
|
OCO_API_URL=<may be used to set proxy path to OpenAI api>
|
||||||
OCO_TOKENS_MAX_INPUT=<max model token limit (default: 4096)>
|
OCO_TOKENS_MAX_INPUT=<max model token limit (default: 4096)>
|
||||||
OCO_TOKENS_MAX_OUTPUT=<max response tokens (default: 500)>
|
OCO_TOKENS_MAX_OUTPUT=<max response tokens (default: 500)>
|
||||||
OCO_OPENAI_BASE_PATH=<may be used to set proxy path to OpenAI api>
|
|
||||||
OCO_DESCRIPTION=<postface a message with ~3 sentences description of the changes>
|
OCO_DESCRIPTION=<postface a message with ~3 sentences description of the changes>
|
||||||
OCO_EMOJI=<boolean, add GitMoji>
|
OCO_EMOJI=<boolean, add GitMoji>
|
||||||
OCO_MODEL=<either 'gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo' (default), 'gpt-3.5-turbo-0125', 'gpt-4-1106-preview', 'gpt-4-turbo-preview' or 'gpt-4-0125-preview'>
|
OCO_MODEL=<either 'gpt-4o', 'gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo' (default), 'gpt-3.5-turbo-0125', 'gpt-4-1106-preview', 'gpt-4-turbo-preview' or 'gpt-4-0125-preview' or any Anthropic or Ollama model or any string basically, but it should be a valid model name>
|
||||||
OCO_LANGUAGE=<locale, scroll to the bottom to see options>
|
OCO_LANGUAGE=<locale, scroll to the bottom to see options>
|
||||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER=<message template placeholder, default: '$msg'>
|
OCO_MESSAGE_TEMPLATE_PLACEHOLDER=<message template placeholder, default: '$msg'>
|
||||||
OCO_PROMPT_MODULE=<either conventional-commit or @commitlint, default: conventional-commit>
|
OCO_PROMPT_MODULE=<either conventional-commit or @commitlint, default: conventional-commit>
|
||||||
OCO_ONE_LINE_COMMIT=<one line commit message, default: false>
|
OCO_ONE_LINE_COMMIT=<one line commit message, default: false>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Global configs are same as local configs, but they are stored in the global `~/.opencommit` config file and set with `oco config set` command, e.g. `oco config set OCO_MODEL=gpt-4o`.
|
||||||
|
|
||||||
### Global config for all repos
|
### Global config for all repos
|
||||||
|
|
||||||
Local config still has more priority than Global config, but you may set `OCO_MODEL` and `OCO_LOCALE` globally and set local configs for `OCO_EMOJI` and `OCO_DESCRIPTION` per repo which is more convenient.
|
Local config still has more priority than Global config, but you may set `OCO_MODEL` and `OCO_LOCALE` globally and set local configs for `OCO_EMOJI` and `OCO_DESCRIPTION` per repo which is more convenient.
|
||||||
@@ -119,7 +129,7 @@ Local config still has more priority than Global config, but you may set `OCO_MO
|
|||||||
Simply set any of the variables above like this:
|
Simply set any of the variables above like this:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
oco config set OCO_MODEL=gpt-4
|
oco config set OCO_MODEL=gpt-4o-mini
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure [GitMoji](https://gitmoji.dev/) to preface a message.
|
Configure [GitMoji](https://gitmoji.dev/) to preface a message.
|
||||||
@@ -134,14 +144,26 @@ To remove preface emojis:
|
|||||||
oco config set OCO_EMOJI=false
|
oco config set OCO_EMOJI=false
|
||||||
```
|
```
|
||||||
|
|
||||||
### Switch to GPT-4 or other models
|
Other config options are behaving the same.
|
||||||
|
|
||||||
By default, OpenCommit uses `gpt-3.5-turbo` model.
|
### Output WHY the changes were done (WIP)
|
||||||
|
|
||||||
You may switch to GPT-4 which performs better, but costs ~x15 times more 🤠
|
You can set the `OCO_WHY` config to `true` to have OpenCommit output a short description of WHY the changes were done after the commit message. Default is `false`.
|
||||||
|
|
||||||
|
To make this perform accurate we must store 'what files do' in some kind of an index or embedding and perform a lookup (kinda RAG) for the accurate git commit message. If you feel like building this comment on this ticket https://github.com/di-sukharev/opencommit/issues/398 and let's go from there together.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
oco config set OCO_MODEL=gpt-4
|
oco config set OCO_WHY=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Switch to GPT-4 or other models
|
||||||
|
|
||||||
|
By default, OpenCommit uses `gpt-4o-mini` model.
|
||||||
|
|
||||||
|
You may switch to gpt-4o which performs better, but costs more 🤠
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco config set OCO_MODEL=gpt-4o
|
||||||
```
|
```
|
||||||
|
|
||||||
or for as a cheaper option:
|
or for as a cheaper option:
|
||||||
@@ -150,34 +172,26 @@ or for as a cheaper option:
|
|||||||
oco config set OCO_MODEL=gpt-3.5-turbo
|
oco config set OCO_MODEL=gpt-3.5-turbo
|
||||||
```
|
```
|
||||||
|
|
||||||
or for GPT-4 Turbo (Preview) which is more capable, has knowledge of world events up to April 2023, a 128k context window and 2-3x cheaper vs GPT-4:
|
### Switch to other LLM providers with a custom URL
|
||||||
|
|
||||||
```sh
|
|
||||||
oco config set OCO_MODEL=gpt-4-0125-preview
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure that you spell it `gpt-4` (lowercase) and that you have API access to the 4th model. Even if you have ChatGPT+, that doesn't necessarily mean that you have API access to GPT-4.
|
|
||||||
|
|
||||||
### Switch to Azure OpenAI
|
|
||||||
|
|
||||||
By default OpenCommit uses [OpenAI](https://openai.com).
|
By default OpenCommit uses [OpenAI](https://openai.com).
|
||||||
|
|
||||||
You could switch to [Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/)🚀
|
You could switch to [Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/) or Flowise or Ollama.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
opencommit config set OCO_AI_PROVIDER=azure
|
oco config set OCO_AI_PROVIDER=azure OCO_API_KEY=<your_azure_api_key> OCO_API_URL=<your_azure_endpoint>
|
||||||
```
|
|
||||||
|
|
||||||
Of course need to set 'OPENAI_API_KEY'. And also need to set the
|
oco config set OCO_AI_PROVIDER=flowise OCO_API_KEY=<your_flowise_api_key> OCO_API_URL=<your_flowise_endpoint>
|
||||||
'OPENAI_BASE_PATH' for the endpoint and set the deployment name to
|
|
||||||
'model'.
|
oco config set OCO_AI_PROVIDER=ollama OCO_API_KEY=<your_ollama_api_key> OCO_API_URL=<your_ollama_endpoint>
|
||||||
|
```
|
||||||
|
|
||||||
### Locale configuration
|
### Locale configuration
|
||||||
|
|
||||||
To globally specify the language used to generate commit messages:
|
To globally specify the language used to generate commit messages:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# de, German ,Deutsch
|
# de, German, Deutsch
|
||||||
oco config set OCO_LANGUAGE=de
|
oco config set OCO_LANGUAGE=de
|
||||||
oco config set OCO_LANGUAGE=German
|
oco config set OCO_LANGUAGE=German
|
||||||
oco config set OCO_LANGUAGE=Deutsch
|
oco config set OCO_LANGUAGE=Deutsch
|
||||||
@@ -191,14 +205,16 @@ oco config set OCO_LANGUAGE=française
|
|||||||
The default language setting is **English**
|
The default language setting is **English**
|
||||||
All available languages are currently listed in the [i18n](https://github.com/di-sukharev/opencommit/tree/master/src/i18n) folder
|
All available languages are currently listed in the [i18n](https://github.com/di-sukharev/opencommit/tree/master/src/i18n) folder
|
||||||
|
|
||||||
### Push to git
|
### Push to git (gonna be deprecated)
|
||||||
|
|
||||||
Pushing to git is on by default but if you would like to turn it off just use:
|
A prompt for pushing to git is on by default but if you would like to turn it off just use:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
oco config set OCO_GITPUSH=false
|
oco config set OCO_GITPUSH=false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
and it will exit right after commit is confirmed without asking if you would like to push to remote.
|
||||||
|
|
||||||
### Switch to `@commitlint`
|
### Switch to `@commitlint`
|
||||||
|
|
||||||
OpenCommit allows you to choose the prompt module used to generate commit messages. By default, OpenCommit uses its conventional-commit message generator. However, you can switch to using the `@commitlint` prompt module if you prefer. This option lets you generate commit messages in respect with the local config.
|
OpenCommit allows you to choose the prompt module used to generate commit messages. By default, OpenCommit uses its conventional-commit message generator. However, you can switch to using the `@commitlint` prompt module if you prefer. This option lets you generate commit messages in respect with the local config.
|
||||||
@@ -213,7 +229,7 @@ Replace `<module>` with either `conventional-commit` or `@commitlint`.
|
|||||||
|
|
||||||
#### Example:
|
#### Example:
|
||||||
|
|
||||||
To switch to using th` '@commitlint` prompt module, run:
|
To switch to using the `'@commitlint` prompt module, run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
oco config set OCO_PROMPT_MODULE=@commitlint
|
oco config set OCO_PROMPT_MODULE=@commitlint
|
||||||
@@ -281,7 +297,7 @@ In our codebase, the implementation of this feature can be found in the followin
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
commitMessage = messageTemplate.replace(
|
commitMessage = messageTemplate.replace(
|
||||||
config?.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
|
config.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
|
||||||
commitMessage
|
commitMessage
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
@@ -338,7 +354,7 @@ Or follow the process of your IDE Source Control feature, when it calls `git com
|
|||||||
|
|
||||||
OpenCommit is now available as a GitHub Action which automatically improves all new commits messages when you push to remote!
|
OpenCommit is now available as a GitHub Action which automatically improves all new commits messages when you push to remote!
|
||||||
|
|
||||||
This is great if you want to make sure all of the commits in all of your repository branches are meaningful and not lame like `fix1` or `done2`.
|
This is great if you want to make sure all commits in all of your repository branches are meaningful and not lame like `fix1` or `done2`.
|
||||||
|
|
||||||
Create a file `.github/workflows/opencommit.yml` with the contents below:
|
Create a file `.github/workflows/opencommit.yml` with the contents below:
|
||||||
|
|
||||||
@@ -373,7 +389,7 @@ jobs:
|
|||||||
# set openAI api key in repo actions secrets,
|
# set openAI api key in repo actions secrets,
|
||||||
# for openAI keys go to: https://platform.openai.com/account/api-keys
|
# for openAI keys go to: https://platform.openai.com/account/api-keys
|
||||||
# for repo secret go to: <your_repo_url>/settings/secrets/actions
|
# for repo secret go to: <your_repo_url>/settings/secrets/actions
|
||||||
OCO_OPENAI_API_KEY: ${{ secrets.OCO_OPENAI_API_KEY }}
|
OCO_API_KEY: ${{ secrets.OCO_API_KEY }}
|
||||||
|
|
||||||
# customization
|
# customization
|
||||||
OCO_TOKENS_MAX_INPUT: 4096
|
OCO_TOKENS_MAX_INPUT: 4096
|
||||||
@@ -381,7 +397,7 @@ jobs:
|
|||||||
OCO_OPENAI_BASE_PATH: ''
|
OCO_OPENAI_BASE_PATH: ''
|
||||||
OCO_DESCRIPTION: false
|
OCO_DESCRIPTION: false
|
||||||
OCO_EMOJI: false
|
OCO_EMOJI: false
|
||||||
OCO_MODEL: gpt-3.5-turbo
|
OCO_MODEL: gpt-4o
|
||||||
OCO_LANGUAGE: en
|
OCO_LANGUAGE: en
|
||||||
OCO_PROMPT_MODULE: conventional-commit
|
OCO_PROMPT_MODULE: conventional-commit
|
||||||
```
|
```
|
||||||
|
|||||||
50950
out/cli.cjs
50950
out/cli.cjs
File diff suppressed because one or more lines are too long
44599
out/github-action.cjs
44599
out/github-action.cjs
File diff suppressed because one or more lines are too long
158
package-lock.json
generated
158
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "opencommit",
|
"name": "opencommit",
|
||||||
"version": "3.0.16",
|
"version": "3.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "opencommit",
|
"name": "opencommit",
|
||||||
"version": "3.0.16",
|
"version": "3.1.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"@azure/openai": "^1.0.0-beta.12",
|
"@azure/openai": "^1.0.0-beta.12",
|
||||||
"@clack/prompts": "^0.6.1",
|
"@clack/prompts": "^0.6.1",
|
||||||
"@dqbd/tiktoken": "^1.0.2",
|
"@dqbd/tiktoken": "^1.0.2",
|
||||||
|
"@google/generative-ai": "^0.11.4",
|
||||||
"@octokit/webhooks-schemas": "^6.11.0",
|
"@octokit/webhooks-schemas": "^6.11.0",
|
||||||
"@octokit/webhooks-types": "^6.11.0",
|
"@octokit/webhooks-types": "^6.11.0",
|
||||||
"ai": "^2.2.14",
|
"ai": "^2.2.14",
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
"ignore": "^5.2.4",
|
"ignore": "^5.2.4",
|
||||||
"ini": "^3.0.1",
|
"ini": "^3.0.1",
|
||||||
"inquirer": "^9.1.4",
|
"inquirer": "^9.1.4",
|
||||||
"openai": "^3.2.1"
|
"openai": "^4.57.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"oco": "out/cli.cjs",
|
"oco": "out/cli.cjs",
|
||||||
@@ -1051,6 +1052,14 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@google/generative-ai": {
|
||||||
|
"version": "0.11.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.11.4.tgz",
|
||||||
|
"integrity": "sha512-hlw+E9Prv9aUIQISRnLSXi4rukFqKe5WhxPvzBccTvIvXjw2BHMFOJWSC/Gq7WE0W+L/qRHGmYxopmx9qjrB9w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.14",
|
"version": "0.11.14",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
||||||
@@ -2089,6 +2098,11 @@
|
|||||||
"form-data": "^4.0.0"
|
"form-data": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/qs": {
|
||||||
|
"version": "6.9.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
|
||||||
|
"integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg=="
|
||||||
|
},
|
||||||
"node_modules/@types/semver": {
|
"node_modules/@types/semver": {
|
||||||
"version": "7.5.8",
|
"version": "7.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
|
||||||
@@ -2648,9 +2662,9 @@
|
|||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.6.8",
|
"version": "1.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
|
||||||
"integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
|
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
@@ -2882,12 +2896,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/braces": {
|
"node_modules/braces": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fill-range": "^7.0.1"
|
"fill-range": "^7.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -4537,9 +4551,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
@@ -7145,6 +7159,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-fetch/node_modules/tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||||
|
},
|
||||||
|
"node_modules/node-fetch/node_modules/webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||||
|
},
|
||||||
|
"node_modules/node-fetch/node_modules/whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-int64": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
@@ -7191,6 +7224,17 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.13.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
||||||
|
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/once": {
|
"node_modules/once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
@@ -7214,20 +7258,38 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/openai": {
|
"node_modules/openai": {
|
||||||
"version": "3.3.0",
|
"version": "4.57.0",
|
||||||
"resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/openai/-/openai-4.57.0.tgz",
|
||||||
"integrity": "sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==",
|
"integrity": "sha512-JnwBSIYqiZ3jYjB5f2in8hQ0PRA092c6m+/6dYB0MzK0BEbn+0dioxZsPLBm5idJbg9xzLNOiGVm2OSuhZ+BdQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.26.0",
|
"@types/node": "^18.11.18",
|
||||||
"form-data": "^4.0.0"
|
"@types/node-fetch": "^2.6.4",
|
||||||
|
"@types/qs": "^6.9.7",
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"agentkeepalive": "^4.2.1",
|
||||||
|
"form-data-encoder": "1.7.2",
|
||||||
|
"formdata-node": "^4.3.2",
|
||||||
|
"node-fetch": "^2.6.7",
|
||||||
|
"qs": "^6.10.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"openai": "bin/cli"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"zod": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/openai/node_modules/axios": {
|
"node_modules/openai/node_modules/@types/node": {
|
||||||
"version": "0.26.1",
|
"version": "18.19.45",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.45.tgz",
|
||||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
"integrity": "sha512-VZxPKNNhjKmaC1SUYowuXSRSMGyQGmQjvvA1xE4QZ0xce2kLtEhPDS+kqpCPBZYgqblCLQ2DAjSzmgCM5auvhA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.14.8"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
@@ -7660,6 +7722,20 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
|
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.0.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@@ -8006,6 +8082,23 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.7",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.4",
|
||||||
|
"object-inspect": "^1.13.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||||
@@ -8400,11 +8493,6 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tr46": {
|
|
||||||
"version": "0.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
|
||||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
|
||||||
},
|
|
||||||
"node_modules/tree-kill": {
|
"node_modules/tree-kill": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||||
@@ -8735,20 +8823,6 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webidl-conversions": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
|
||||||
},
|
|
||||||
"node_modules/whatwg-url": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
|
||||||
"dependencies": {
|
|
||||||
"tr46": "~0.0.3",
|
|
||||||
"webidl-conversions": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "opencommit",
|
"name": "opencommit",
|
||||||
"version": "3.0.16",
|
"version": "3.1.2",
|
||||||
"description": "Auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
|
"description": "Auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"git",
|
"git",
|
||||||
@@ -43,16 +43,20 @@
|
|||||||
"start": "node ./out/cli.cjs",
|
"start": "node ./out/cli.cjs",
|
||||||
"ollama:start": "OCO_AI_PROVIDER='ollama' node ./out/cli.cjs",
|
"ollama:start": "OCO_AI_PROVIDER='ollama' node ./out/cli.cjs",
|
||||||
"dev": "ts-node ./src/cli.ts",
|
"dev": "ts-node ./src/cli.ts",
|
||||||
|
"dev:gemini": "OCO_AI_PROVIDER='gemini' ts-node ./src/cli.ts",
|
||||||
"build": "rimraf out && node esbuild.config.js",
|
"build": "rimraf out && node esbuild.config.js",
|
||||||
"build:push": "npm run build && git add . && git commit -m 'build' && git push",
|
"build:push": "npm run build && git add . && git commit -m 'build' && git push",
|
||||||
"deploy": "npm version patch && npm run build:push && git push --tags && npm publish --tag latest",
|
"deploy": "npm run build:push && git push --tags && npm publish --tag latest",
|
||||||
|
"deploy:patch": "npm version patch && npm run deploy",
|
||||||
"lint": "eslint src --ext ts && tsc --noEmit",
|
"lint": "eslint src --ext ts && tsc --noEmit",
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
|
"test": "node --no-warnings --experimental-vm-modules $( [ -f ./node_modules/.bin/jest ] && echo ./node_modules/.bin/jest || which jest ) test/unit",
|
||||||
"test:all": "npm run test:unit:docker && npm run test:e2e:docker",
|
"test:all": "npm run test:unit:docker && npm run test:e2e:docker",
|
||||||
"test:docker-build": "docker build -t oco-test -f test/Dockerfile .",
|
"test:docker-build": "docker build -t oco-test -f test/Dockerfile .",
|
||||||
"test:unit": "NODE_OPTIONS=--experimental-vm-modules jest test/unit",
|
"test:unit": "NODE_OPTIONS=--experimental-vm-modules jest test/unit",
|
||||||
"test:unit:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:unit",
|
"test:unit:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:unit",
|
||||||
"test:e2e": "jest test/e2e",
|
"test:e2e": "npm run test:e2e:setup && jest test/e2e",
|
||||||
|
"test:e2e:setup": "sh test/e2e/setup.sh",
|
||||||
"test:e2e:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:e2e"
|
"test:e2e:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:e2e"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -77,13 +81,13 @@
|
|||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"@actions/github": "^5.1.1",
|
"@actions/github": "^5.1.1",
|
||||||
"@azure/openai": "^1.0.0-beta.12",
|
|
||||||
"@anthropic-ai/sdk": "^0.19.2",
|
"@anthropic-ai/sdk": "^0.19.2",
|
||||||
|
"@azure/openai": "^1.0.0-beta.12",
|
||||||
"@clack/prompts": "^0.6.1",
|
"@clack/prompts": "^0.6.1",
|
||||||
"@dqbd/tiktoken": "^1.0.2",
|
"@dqbd/tiktoken": "^1.0.2",
|
||||||
|
"@google/generative-ai": "^0.11.4",
|
||||||
"@octokit/webhooks-schemas": "^6.11.0",
|
"@octokit/webhooks-schemas": "^6.11.0",
|
||||||
"@octokit/webhooks-types": "^6.11.0",
|
"@octokit/webhooks-types": "^6.11.0",
|
||||||
"ai": "^2.2.14",
|
|
||||||
"axios": "^1.3.4",
|
"axios": "^1.3.4",
|
||||||
"chalk": "^5.2.0",
|
"chalk": "^5.2.0",
|
||||||
"cleye": "^1.3.2",
|
"cleye": "^1.3.2",
|
||||||
@@ -92,6 +96,6 @@
|
|||||||
"ignore": "^5.2.4",
|
"ignore": "^5.2.4",
|
||||||
"ini": "^3.0.1",
|
"ini": "^3.0.1",
|
||||||
"inquirer": "^9.1.4",
|
"inquirer": "^9.1.4",
|
||||||
"openai": "^3.2.1"
|
"openai": "^4.57.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { configCommand } from './commands/config';
|
|||||||
import { hookCommand, isHookCalled } from './commands/githook.js';
|
import { hookCommand, isHookCalled } from './commands/githook.js';
|
||||||
import { prepareCommitMessageHook } from './commands/prepare-commit-msg-hook';
|
import { prepareCommitMessageHook } from './commands/prepare-commit-msg-hook';
|
||||||
import { checkIsLatestVersion } from './utils/checkIsLatestVersion';
|
import { checkIsLatestVersion } from './utils/checkIsLatestVersion';
|
||||||
|
import { runMigrations } from './migrations/_run.js';
|
||||||
|
|
||||||
const extraArgs = process.argv.slice(2);
|
const extraArgs = process.argv.slice(2);
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ cli(
|
|||||||
help: { description: packageJSON.description }
|
help: { description: packageJSON.description }
|
||||||
},
|
},
|
||||||
async ({ flags }) => {
|
async ({ flags }) => {
|
||||||
|
await runMigrations();
|
||||||
await checkIsLatestVersion();
|
await checkIsLatestVersion();
|
||||||
|
|
||||||
if (await isHookCalled()) {
|
if (await isHookCalled()) {
|
||||||
|
|||||||
5
src/commands/ENUMS.ts
Normal file
5
src/commands/ENUMS.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export enum COMMANDS {
|
||||||
|
config = 'config',
|
||||||
|
hook = 'hook',
|
||||||
|
commitlint = 'commitlint'
|
||||||
|
}
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
import chalk from 'chalk';
|
|
||||||
import { execa } from 'execa';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
confirm,
|
confirm,
|
||||||
intro,
|
intro,
|
||||||
@@ -10,7 +7,8 @@ import {
|
|||||||
select,
|
select,
|
||||||
spinner
|
spinner
|
||||||
} from '@clack/prompts';
|
} from '@clack/prompts';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { execa } from 'execa';
|
||||||
import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
|
import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
|
||||||
import {
|
import {
|
||||||
assertGitRepo,
|
assertGitRepo,
|
||||||
@@ -32,21 +30,28 @@ const getGitRemotes = async () => {
|
|||||||
// Check for the presence of message templates
|
// Check for the presence of message templates
|
||||||
const checkMessageTemplate = (extraArgs: string[]): string | false => {
|
const checkMessageTemplate = (extraArgs: string[]): string | false => {
|
||||||
for (const key in extraArgs) {
|
for (const key in extraArgs) {
|
||||||
if (extraArgs[key].includes(config?.OCO_MESSAGE_TEMPLATE_PLACEHOLDER))
|
if (extraArgs[key].includes(config.OCO_MESSAGE_TEMPLATE_PLACEHOLDER))
|
||||||
return extraArgs[key];
|
return extraArgs[key];
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateCommitMessageFromGitDiff = async (
|
interface GenerateCommitMessageFromGitDiffParams {
|
||||||
diff: string,
|
diff: string;
|
||||||
extraArgs: string[],
|
extraArgs: string[];
|
||||||
fullGitMojiSpec: boolean,
|
fullGitMojiSpec?: boolean;
|
||||||
skipCommitConfirmation: boolean
|
skipCommitConfirmation?: boolean;
|
||||||
): Promise<void> => {
|
}
|
||||||
|
|
||||||
|
const generateCommitMessageFromGitDiff = async ({
|
||||||
|
diff,
|
||||||
|
extraArgs,
|
||||||
|
fullGitMojiSpec = false,
|
||||||
|
skipCommitConfirmation = false
|
||||||
|
}: GenerateCommitMessageFromGitDiffParams): Promise<void> => {
|
||||||
await assertGitRepo();
|
await assertGitRepo();
|
||||||
const commitSpinner = spinner();
|
const commitGenerationSpinner = spinner();
|
||||||
commitSpinner.start('Generating the commit message');
|
commitGenerationSpinner.start('Generating the commit message');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let commitMessage = await generateCommitMessageByDiff(
|
let commitMessage = await generateCommitMessageByDiff(
|
||||||
@@ -56,19 +61,19 @@ const generateCommitMessageFromGitDiff = async (
|
|||||||
|
|
||||||
const messageTemplate = checkMessageTemplate(extraArgs);
|
const messageTemplate = checkMessageTemplate(extraArgs);
|
||||||
if (
|
if (
|
||||||
config?.OCO_MESSAGE_TEMPLATE_PLACEHOLDER &&
|
config.OCO_MESSAGE_TEMPLATE_PLACEHOLDER &&
|
||||||
typeof messageTemplate === 'string'
|
typeof messageTemplate === 'string'
|
||||||
) {
|
) {
|
||||||
const messageTemplateIndex = extraArgs.indexOf(messageTemplate);
|
const messageTemplateIndex = extraArgs.indexOf(messageTemplate);
|
||||||
extraArgs.splice(messageTemplateIndex, 1);
|
extraArgs.splice(messageTemplateIndex, 1);
|
||||||
|
|
||||||
commitMessage = messageTemplate.replace(
|
commitMessage = messageTemplate.replace(
|
||||||
config?.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
|
config.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
|
||||||
commitMessage
|
commitMessage
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
commitSpinner.stop('📝 Commit message generated');
|
commitGenerationSpinner.stop('📝 Commit message generated');
|
||||||
|
|
||||||
outro(
|
outro(
|
||||||
`Generated commit message:
|
`Generated commit message:
|
||||||
@@ -77,27 +82,33 @@ ${commitMessage}
|
|||||||
${chalk.grey('——————————————————')}`
|
${chalk.grey('——————————————————')}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const isCommitConfirmedByUser = skipCommitConfirmation || await confirm({
|
const isCommitConfirmedByUser =
|
||||||
message: 'Confirm the commit message?'
|
skipCommitConfirmation ||
|
||||||
});
|
(await confirm({
|
||||||
|
message: 'Confirm the commit message?'
|
||||||
|
}));
|
||||||
|
|
||||||
if (isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
|
if (isCancel(isCommitConfirmedByUser)) process.exit(1);
|
||||||
|
|
||||||
|
if (isCommitConfirmedByUser) {
|
||||||
|
const committingChangesSpinner = spinner();
|
||||||
|
committingChangesSpinner.start('Committing the changes');
|
||||||
const { stdout } = await execa('git', [
|
const { stdout } = await execa('git', [
|
||||||
'commit',
|
'commit',
|
||||||
'-m',
|
'-m',
|
||||||
commitMessage,
|
commitMessage,
|
||||||
...extraArgs
|
...extraArgs
|
||||||
]);
|
]);
|
||||||
|
committingChangesSpinner.stop(
|
||||||
outro(`${chalk.green('✔')} Successfully committed`);
|
`${chalk.green('✔')} Successfully committed`
|
||||||
|
);
|
||||||
|
|
||||||
outro(stdout);
|
outro(stdout);
|
||||||
|
|
||||||
const remotes = await getGitRemotes();
|
const remotes = await getGitRemotes();
|
||||||
|
|
||||||
// user isn't pushing, return early
|
// user isn't pushing, return early
|
||||||
if (config?.OCO_GITPUSH === false)
|
if (config.OCO_GITPUSH === false) return;
|
||||||
return
|
|
||||||
|
|
||||||
if (!remotes.length) {
|
if (!remotes.length) {
|
||||||
const { stdout } = await execa('git', ['push']);
|
const { stdout } = await execa('git', ['push']);
|
||||||
@@ -105,12 +116,14 @@ ${chalk.grey('——————————————————')}`
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remotes.length === 1 && config?.OCO_GITPUSH !== true) {
|
if (remotes.length === 1) {
|
||||||
const isPushConfirmedByUser = await confirm({
|
const isPushConfirmedByUser = await confirm({
|
||||||
message: 'Do you want to run `git push`?'
|
message: 'Do you want to run `git push`?'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPushConfirmedByUser && !isCancel(isPushConfirmedByUser)) {
|
if (isCancel(isPushConfirmedByUser)) process.exit(1);
|
||||||
|
|
||||||
|
if (isPushConfirmedByUser) {
|
||||||
const pushSpinner = spinner();
|
const pushSpinner = spinner();
|
||||||
|
|
||||||
pushSpinner.start(`Running 'git push ${remotes[0]}'`);
|
pushSpinner.start(`Running 'git push ${remotes[0]}'`);
|
||||||
@@ -138,37 +151,39 @@ ${chalk.grey('——————————————————')}`
|
|||||||
options: remotes.map((remote) => ({ value: remote, label: remote }))
|
options: remotes.map((remote) => ({ value: remote, label: remote }))
|
||||||
})) as string;
|
})) as string;
|
||||||
|
|
||||||
if (!isCancel(selectedRemote)) {
|
if (isCancel(selectedRemote)) process.exit(1);
|
||||||
const pushSpinner = spinner();
|
|
||||||
|
|
||||||
pushSpinner.start(`Running 'git push ${selectedRemote}'`);
|
const pushSpinner = spinner();
|
||||||
|
|
||||||
const { stdout } = await execa('git', ['push', selectedRemote]);
|
pushSpinner.start(`Running 'git push ${selectedRemote}'`);
|
||||||
|
|
||||||
pushSpinner.stop(
|
const { stdout } = await execa('git', ['push', selectedRemote]);
|
||||||
`${chalk.green(
|
|
||||||
'✔'
|
|
||||||
)} Successfully pushed all commits to ${selectedRemote}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (stdout) outro(stdout);
|
if (stdout) outro(stdout);
|
||||||
} else outro(`${chalk.gray('✖')} process cancelled`);
|
|
||||||
|
pushSpinner.stop(
|
||||||
|
`${chalk.green(
|
||||||
|
'✔'
|
||||||
|
)} successfully pushed all commits to ${selectedRemote}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
if (!isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
|
|
||||||
const regenerateMessage = await confirm({
|
const regenerateMessage = await confirm({
|
||||||
message: 'Do you want to regenerate the message ?'
|
message: 'Do you want to regenerate the message?'
|
||||||
});
|
});
|
||||||
if (regenerateMessage && !isCancel(isCommitConfirmedByUser)) {
|
|
||||||
await generateCommitMessageFromGitDiff(
|
if (isCancel(regenerateMessage)) process.exit(1);
|
||||||
|
|
||||||
|
if (regenerateMessage) {
|
||||||
|
await generateCommitMessageFromGitDiff({
|
||||||
diff,
|
diff,
|
||||||
extraArgs,
|
extraArgs,
|
||||||
fullGitMojiSpec
|
fullGitMojiSpec
|
||||||
)
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
commitSpinner.stop('📝 Commit message generated');
|
commitGenerationSpinner.stop('📝 Commit message generated');
|
||||||
|
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
||||||
@@ -216,10 +231,9 @@ export async function commit(
|
|||||||
message: 'Do you want to stage all files and generate commit message?'
|
message: 'Do you want to stage all files and generate commit message?'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (isCancel(isStageAllAndCommitConfirmedByUser)) process.exit(1);
|
||||||
isStageAllAndCommitConfirmedByUser &&
|
|
||||||
!isCancel(isStageAllAndCommitConfirmedByUser)
|
if (isStageAllAndCommitConfirmedByUser) {
|
||||||
) {
|
|
||||||
await commit(extraArgs, true, fullGitMojiSpec);
|
await commit(extraArgs, true, fullGitMojiSpec);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -249,12 +263,12 @@ export async function commit(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [, generateCommitError] = await trytm(
|
const [, generateCommitError] = await trytm(
|
||||||
generateCommitMessageFromGitDiff(
|
generateCommitMessageFromGitDiff({
|
||||||
await getDiff({ files: stagedFiles }),
|
diff: await getDiff({ files: stagedFiles }),
|
||||||
extraArgs,
|
extraArgs,
|
||||||
fullGitMojiSpec,
|
fullGitMojiSpec,
|
||||||
skipCommitConfirmation
|
skipCommitConfirmation
|
||||||
)
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (generateCommitError) {
|
if (generateCommitError) {
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
|
import { intro, outro } from '@clack/prompts';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { command } from 'cleye';
|
import { command } from 'cleye';
|
||||||
|
|
||||||
import { intro, outro } from '@clack/prompts';
|
|
||||||
|
|
||||||
import { COMMANDS } from '../CommandsEnum';
|
|
||||||
import { configureCommitlintIntegration } from '../modules/commitlint/config';
|
import { configureCommitlintIntegration } from '../modules/commitlint/config';
|
||||||
import { getCommitlintLLMConfig } from '../modules/commitlint/utils';
|
import { getCommitlintLLMConfig } from '../modules/commitlint/utils';
|
||||||
|
import { COMMANDS } from './ENUMS';
|
||||||
|
|
||||||
export enum CONFIG_MODES {
|
export enum CONFIG_MODES {
|
||||||
get = 'get',
|
get = 'get',
|
||||||
@@ -25,7 +23,7 @@ export const commitlintConfigCommand = command(
|
|||||||
if (mode === CONFIG_MODES.get) {
|
if (mode === CONFIG_MODES.get) {
|
||||||
const commitLintConfig = await getCommitlintLLMConfig();
|
const commitLintConfig = await getCommitlintLLMConfig();
|
||||||
|
|
||||||
outro(commitLintConfig.toString());
|
outro(JSON.stringify(commitLintConfig, null, 2));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { intro, outro } from '@clack/prompts';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { command } from 'cleye';
|
import { command } from 'cleye';
|
||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
@@ -5,29 +6,26 @@ import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|||||||
import { parse as iniParse, stringify as iniStringify } from 'ini';
|
import { parse as iniParse, stringify as iniStringify } from 'ini';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import { join as pathJoin, resolve as pathResolve } from 'path';
|
import { join as pathJoin, resolve as pathResolve } from 'path';
|
||||||
|
import { COMMANDS } from './ENUMS';
|
||||||
import { intro, outro } from '@clack/prompts';
|
import { TEST_MOCK_TYPES } from '../engine/testAi';
|
||||||
|
import { getI18nLocal, i18n } from '../i18n';
|
||||||
import { COMMANDS } from '../CommandsEnum';
|
|
||||||
import { getI18nLocal } from '../i18n';
|
|
||||||
|
|
||||||
export enum CONFIG_KEYS {
|
export enum CONFIG_KEYS {
|
||||||
OCO_OPENAI_API_KEY = 'OCO_OPENAI_API_KEY',
|
OCO_API_KEY = 'OCO_API_KEY',
|
||||||
OCO_ANTHROPIC_API_KEY = 'OCO_ANTHROPIC_API_KEY',
|
|
||||||
OCO_AZURE_API_KEY = 'OCO_AZURE_API_KEY',
|
|
||||||
OCO_TOKENS_MAX_INPUT = 'OCO_TOKENS_MAX_INPUT',
|
OCO_TOKENS_MAX_INPUT = 'OCO_TOKENS_MAX_INPUT',
|
||||||
OCO_TOKENS_MAX_OUTPUT = 'OCO_TOKENS_MAX_OUTPUT',
|
OCO_TOKENS_MAX_OUTPUT = 'OCO_TOKENS_MAX_OUTPUT',
|
||||||
OCO_OPENAI_BASE_PATH = 'OCO_OPENAI_BASE_PATH',
|
|
||||||
OCO_DESCRIPTION = 'OCO_DESCRIPTION',
|
OCO_DESCRIPTION = 'OCO_DESCRIPTION',
|
||||||
OCO_EMOJI = 'OCO_EMOJI',
|
OCO_EMOJI = 'OCO_EMOJI',
|
||||||
OCO_MODEL = 'OCO_MODEL',
|
OCO_MODEL = 'OCO_MODEL',
|
||||||
OCO_LANGUAGE = 'OCO_LANGUAGE',
|
OCO_LANGUAGE = 'OCO_LANGUAGE',
|
||||||
|
OCO_WHY = 'OCO_WHY',
|
||||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER = 'OCO_MESSAGE_TEMPLATE_PLACEHOLDER',
|
OCO_MESSAGE_TEMPLATE_PLACEHOLDER = 'OCO_MESSAGE_TEMPLATE_PLACEHOLDER',
|
||||||
OCO_PROMPT_MODULE = 'OCO_PROMPT_MODULE',
|
OCO_PROMPT_MODULE = 'OCO_PROMPT_MODULE',
|
||||||
OCO_AI_PROVIDER = 'OCO_AI_PROVIDER',
|
OCO_AI_PROVIDER = 'OCO_AI_PROVIDER',
|
||||||
OCO_GITPUSH = 'OCO_GITPUSH',
|
|
||||||
OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT',
|
OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT',
|
||||||
OCO_AZURE_ENDPOINT = 'OCO_AZURE_ENDPOINT'
|
OCO_TEST_MOCK_TYPE = 'OCO_TEST_MOCK_TYPE',
|
||||||
|
OCO_API_URL = 'OCO_API_URL',
|
||||||
|
OCO_GITPUSH = 'OCO_GITPUSH' // todo: deprecate
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CONFIG_MODES {
|
export enum CONFIG_MODES {
|
||||||
@@ -36,19 +34,50 @@ export enum CONFIG_MODES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const MODEL_LIST = {
|
export const MODEL_LIST = {
|
||||||
openai: ['gpt-3.5-turbo',
|
openai: [
|
||||||
'gpt-3.5-turbo-0125',
|
'gpt-4o-mini',
|
||||||
'gpt-4',
|
'gpt-3.5-turbo',
|
||||||
'gpt-4-turbo',
|
'gpt-3.5-turbo-instruct',
|
||||||
'gpt-4-1106-preview',
|
'gpt-3.5-turbo-0613',
|
||||||
'gpt-4-turbo-preview',
|
'gpt-3.5-turbo-0301',
|
||||||
'gpt-4-0125-preview',
|
'gpt-3.5-turbo-1106',
|
||||||
'gpt-4o'],
|
'gpt-3.5-turbo-0125',
|
||||||
|
'gpt-3.5-turbo-16k',
|
||||||
|
'gpt-3.5-turbo-16k-0613',
|
||||||
|
'gpt-3.5-turbo-16k-0301',
|
||||||
|
'gpt-4',
|
||||||
|
'gpt-4-0314',
|
||||||
|
'gpt-4-0613',
|
||||||
|
'gpt-4-1106-preview',
|
||||||
|
'gpt-4-0125-preview',
|
||||||
|
'gpt-4-turbo-preview',
|
||||||
|
'gpt-4-vision-preview',
|
||||||
|
'gpt-4-1106-vision-preview',
|
||||||
|
'gpt-4-turbo',
|
||||||
|
'gpt-4-turbo-2024-04-09',
|
||||||
|
'gpt-4-32k',
|
||||||
|
'gpt-4-32k-0314',
|
||||||
|
'gpt-4-32k-0613',
|
||||||
|
'gpt-4o',
|
||||||
|
'gpt-4o-2024-05-13',
|
||||||
|
'gpt-4o-mini-2024-07-18'
|
||||||
|
],
|
||||||
|
|
||||||
anthropic: ['claude-3-haiku-20240307',
|
anthropic: [
|
||||||
'claude-3-sonnet-20240229',
|
'claude-3-5-sonnet-20240620',
|
||||||
'claude-3-opus-20240229']
|
'claude-3-opus-20240229',
|
||||||
}
|
'claude-3-sonnet-20240229',
|
||||||
|
'claude-3-haiku-20240307'
|
||||||
|
],
|
||||||
|
|
||||||
|
gemini: [
|
||||||
|
'gemini-1.5-flash',
|
||||||
|
'gemini-1.5-pro',
|
||||||
|
'gemini-1.0-pro',
|
||||||
|
'gemini-pro-vision',
|
||||||
|
'text-embedding-004'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
const getDefaultModel = (provider: string | undefined): string => {
|
const getDefaultModel = (provider: string | undefined): string => {
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
@@ -56,14 +85,16 @@ const getDefaultModel = (provider: string | undefined): string => {
|
|||||||
return '';
|
return '';
|
||||||
case 'anthropic':
|
case 'anthropic':
|
||||||
return MODEL_LIST.anthropic[0];
|
return MODEL_LIST.anthropic[0];
|
||||||
|
case 'gemini':
|
||||||
|
return MODEL_LIST.gemini[0];
|
||||||
default:
|
default:
|
||||||
return MODEL_LIST.openai[0];
|
return MODEL_LIST.openai[0];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum DEFAULT_TOKEN_LIMITS {
|
export enum DEFAULT_TOKEN_LIMITS {
|
||||||
DEFAULT_MAX_TOKENS_INPUT = 4096,
|
DEFAULT_MAX_TOKENS_INPUT = 40960,
|
||||||
DEFAULT_MAX_TOKENS_OUTPUT = 500
|
DEFAULT_MAX_TOKENS_OUTPUT = 4096
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateConfig = (
|
const validateConfig = (
|
||||||
@@ -72,8 +103,10 @@ const validateConfig = (
|
|||||||
validationMessage: string
|
validationMessage: string
|
||||||
) => {
|
) => {
|
||||||
if (!condition) {
|
if (!condition) {
|
||||||
|
outro(`${chalk.red('✖')} wrong value for ${key}: ${validationMessage}.`);
|
||||||
|
|
||||||
outro(
|
outro(
|
||||||
`${chalk.red('✖')} Unsupported config key ${key}: ${validationMessage}`
|
'For more help refer to docs https://github.com/di-sukharev/opencommit'
|
||||||
);
|
);
|
||||||
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -81,37 +114,19 @@ const validateConfig = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const configValidators = {
|
export const configValidators = {
|
||||||
[CONFIG_KEYS.OCO_OPENAI_API_KEY](value: any, config: any = {}) {
|
[CONFIG_KEYS.OCO_API_KEY](value: any, config: any = {}) {
|
||||||
//need api key unless running locally with ollama
|
if (config.OCO_AI_PROVIDER !== 'openai') return value;
|
||||||
|
|
||||||
validateConfig(
|
validateConfig(
|
||||||
'OpenAI API_KEY',
|
'OCO_API_KEY',
|
||||||
value || config.OCO_ANTHROPIC_API_KEY || config.OCO_AI_PROVIDER.startsWith('ollama') || config.OCO_AZURE_API_KEY || config.OCO_AI_PROVIDER == 'test' ,
|
typeof value === 'string' && value.length > 0,
|
||||||
'You need to provide an OpenAI/Anthropic/Azure API key'
|
'Empty value is not allowed'
|
||||||
);
|
|
||||||
validateConfig(
|
|
||||||
CONFIG_KEYS.OCO_OPENAI_API_KEY,
|
|
||||||
value.startsWith('sk-') || config.OCO_AI_PROVIDER != 'openai',
|
|
||||||
'Must start with "sk-" for openai provider'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
[CONFIG_KEYS.OCO_AZURE_API_KEY](value: any, config: any = {}) {
|
|
||||||
validateConfig(
|
validateConfig(
|
||||||
'ANTHROPIC_API_KEY',
|
'OCO_API_KEY',
|
||||||
value || config.OCO_OPENAI_API_KEY || config.OCO_AZURE_API_KEY || config.OCO_AI_PROVIDER == 'ollama' || config.OCO_AI_PROVIDER == 'test',
|
value,
|
||||||
'You need to provide an OpenAI/Anthropic/Azure API key'
|
'You need to provide the OCO_API_KEY when OCO_AI_PROVIDER set to "openai" (default) or "ollama" or "azure" or "gemini" or "flowise" or "anthropic". Run `oco config set OCO_API_KEY=your_key OCO_AI_PROVIDER=openai`'
|
||||||
);
|
|
||||||
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
[CONFIG_KEYS.OCO_ANTHROPIC_API_KEY](value: any, config: any = {}) {
|
|
||||||
validateConfig(
|
|
||||||
'ANTHROPIC_API_KEY',
|
|
||||||
value || config.OCO_OPENAI_API_KEY || config.OCO_AI_PROVIDER == 'ollama' || config.OCO_AI_PROVIDER == 'test',
|
|
||||||
'You need to provide an OpenAI/Anthropic/Azure API key'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
@@ -121,25 +136,17 @@ export const configValidators = {
|
|||||||
validateConfig(
|
validateConfig(
|
||||||
CONFIG_KEYS.OCO_DESCRIPTION,
|
CONFIG_KEYS.OCO_DESCRIPTION,
|
||||||
typeof value === 'boolean',
|
typeof value === 'boolean',
|
||||||
'Must be true or false'
|
'Must be boolean: true or false'
|
||||||
);
|
);
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
|
|
||||||
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT](value: any) {
|
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT](value: any) {
|
||||||
// If the value is a string, convert it to a number.
|
value = parseInt(value);
|
||||||
if (typeof value === 'string') {
|
|
||||||
value = parseInt(value);
|
|
||||||
validateConfig(
|
|
||||||
CONFIG_KEYS.OCO_TOKENS_MAX_INPUT,
|
|
||||||
!isNaN(value),
|
|
||||||
'Must be a number'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
validateConfig(
|
validateConfig(
|
||||||
CONFIG_KEYS.OCO_TOKENS_MAX_INPUT,
|
CONFIG_KEYS.OCO_TOKENS_MAX_INPUT,
|
||||||
value ? typeof value === 'number' : undefined,
|
!isNaN(value),
|
||||||
'Must be a number'
|
'Must be a number'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -147,18 +154,10 @@ export const configValidators = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
[CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT](value: any) {
|
[CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT](value: any) {
|
||||||
// If the value is a string, convert it to a number.
|
value = parseInt(value);
|
||||||
if (typeof value === 'string') {
|
|
||||||
value = parseInt(value);
|
|
||||||
validateConfig(
|
|
||||||
CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT,
|
|
||||||
!isNaN(value),
|
|
||||||
'Must be a number'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
validateConfig(
|
validateConfig(
|
||||||
CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT,
|
CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT,
|
||||||
value ? typeof value === 'number' : undefined,
|
!isNaN(value),
|
||||||
'Must be a number'
|
'Must be a number'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -169,26 +168,29 @@ export const configValidators = {
|
|||||||
validateConfig(
|
validateConfig(
|
||||||
CONFIG_KEYS.OCO_EMOJI,
|
CONFIG_KEYS.OCO_EMOJI,
|
||||||
typeof value === 'boolean',
|
typeof value === 'boolean',
|
||||||
'Must be true or false'
|
'Must be boolean: true or false'
|
||||||
);
|
);
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
|
|
||||||
[CONFIG_KEYS.OCO_LANGUAGE](value: any) {
|
[CONFIG_KEYS.OCO_LANGUAGE](value: any) {
|
||||||
|
const supportedLanguages = Object.keys(i18n);
|
||||||
|
|
||||||
validateConfig(
|
validateConfig(
|
||||||
CONFIG_KEYS.OCO_LANGUAGE,
|
CONFIG_KEYS.OCO_LANGUAGE,
|
||||||
getI18nLocal(value),
|
getI18nLocal(value),
|
||||||
`${value} is not supported yet`
|
`${value} is not supported yet. Supported languages: ${supportedLanguages}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return getI18nLocal(value);
|
return getI18nLocal(value);
|
||||||
},
|
},
|
||||||
|
|
||||||
[CONFIG_KEYS.OCO_OPENAI_BASE_PATH](value: any) {
|
[CONFIG_KEYS.OCO_API_URL](value: any) {
|
||||||
validateConfig(
|
validateConfig(
|
||||||
CONFIG_KEYS.OCO_OPENAI_BASE_PATH,
|
CONFIG_KEYS.OCO_API_URL,
|
||||||
typeof value === 'string',
|
typeof value === 'string',
|
||||||
'Must be string'
|
`${value} is not a valid URL. It should start with 'http://' or 'https://'.`
|
||||||
);
|
);
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
@@ -196,15 +198,12 @@ export const configValidators = {
|
|||||||
[CONFIG_KEYS.OCO_MODEL](value: any, config: any = {}) {
|
[CONFIG_KEYS.OCO_MODEL](value: any, config: any = {}) {
|
||||||
validateConfig(
|
validateConfig(
|
||||||
CONFIG_KEYS.OCO_MODEL,
|
CONFIG_KEYS.OCO_MODEL,
|
||||||
[...MODEL_LIST.openai, ...MODEL_LIST.anthropic].includes(value) || config.OCO_AI_PROVIDER == 'ollama' || config.OCO_AI_PROVIDER == 'test'|| config.OCO_AI_PROVIDER == 'azure',
|
typeof value === 'string',
|
||||||
`${value} is not supported yet, use 'gpt-4o', 'gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo' (default), 'gpt-3.5-turbo-0125', 'gpt-4-1106-preview', 'gpt-4-turbo-preview', 'gpt-4-0125-preview', 'claude-3-opus-20240229', 'claude-3-sonnet-20240229' or 'claude-3-haiku-20240307'`
|
`${value} is not supported yet, use:\n\n ${[
|
||||||
);
|
...MODEL_LIST.openai,
|
||||||
validateConfig(
|
...MODEL_LIST.anthropic,
|
||||||
CONFIG_KEYS.OCO_MODEL,
|
...MODEL_LIST.gemini
|
||||||
typeof value === 'string' &&
|
].join('\n')}`
|
||||||
value.match(/^[a-zA-Z0-9~\-]{1,63}[a-zA-Z0-9]$/) ||
|
|
||||||
config.OCO_AI_PROVIDER != 'azure',
|
|
||||||
`${value} is not model deployed name.`
|
|
||||||
);
|
);
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
@@ -227,6 +226,7 @@ export const configValidators = {
|
|||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// todo: deprecate
|
||||||
[CONFIG_KEYS.OCO_GITPUSH](value: any) {
|
[CONFIG_KEYS.OCO_GITPUSH](value: any) {
|
||||||
validateConfig(
|
validateConfig(
|
||||||
CONFIG_KEYS.OCO_GITPUSH,
|
CONFIG_KEYS.OCO_GITPUSH,
|
||||||
@@ -237,18 +237,16 @@ export const configValidators = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
[CONFIG_KEYS.OCO_AI_PROVIDER](value: any) {
|
[CONFIG_KEYS.OCO_AI_PROVIDER](value: any) {
|
||||||
|
if (!value) value = 'openai';
|
||||||
|
|
||||||
validateConfig(
|
validateConfig(
|
||||||
CONFIG_KEYS.OCO_AI_PROVIDER,
|
CONFIG_KEYS.OCO_AI_PROVIDER,
|
||||||
[
|
['openai', 'anthropic', 'gemini', 'azure', 'test', 'flowise'].includes(
|
||||||
'',
|
value
|
||||||
'openai',
|
) || value.startsWith('ollama'),
|
||||||
'anthropic',
|
`${value} is not supported yet, use 'ollama', 'anthropic', 'azure', 'gemini', 'flowise' or 'openai' (default)`
|
||||||
'azure',
|
|
||||||
'ollama',
|
|
||||||
'test'
|
|
||||||
].includes(value) || value.startsWith('ollama'),
|
|
||||||
`${value} is not supported yet, use 'ollama/{model}', 'azure', 'anthropic' or 'openai' (default)`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -261,114 +259,241 @@ export const configValidators = {
|
|||||||
|
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
[CONFIG_KEYS.OCO_AZURE_ENDPOINT](value: any) {
|
|
||||||
validateConfig(
|
|
||||||
CONFIG_KEYS.OCO_AZURE_ENDPOINT,
|
|
||||||
value.includes('openai.azure.com'),
|
|
||||||
'Must be in format "https://<resource name>.openai.azure.com/"'
|
|
||||||
);
|
|
||||||
|
|
||||||
|
[CONFIG_KEYS.OCO_TEST_MOCK_TYPE](value: any) {
|
||||||
|
validateConfig(
|
||||||
|
CONFIG_KEYS.OCO_TEST_MOCK_TYPE,
|
||||||
|
TEST_MOCK_TYPES.includes(value),
|
||||||
|
`${value} is not supported yet, use ${TEST_MOCK_TYPES.map(
|
||||||
|
(t) => `'${t}'`
|
||||||
|
).join(', ')}`
|
||||||
|
);
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[CONFIG_KEYS.OCO_WHY](value: any) {
|
||||||
|
validateConfig(
|
||||||
|
CONFIG_KEYS.OCO_WHY,
|
||||||
|
typeof value === 'boolean',
|
||||||
|
'Must be true or false'
|
||||||
|
);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum OCO_AI_PROVIDER_ENUM {
|
||||||
|
OLLAMA = 'ollama',
|
||||||
|
OPENAI = 'openai',
|
||||||
|
ANTHROPIC = 'anthropic',
|
||||||
|
GEMINI = 'gemini',
|
||||||
|
AZURE = 'azure',
|
||||||
|
TEST = 'test',
|
||||||
|
FLOWISE = 'flowise'
|
||||||
|
}
|
||||||
|
|
||||||
export type ConfigType = {
|
export type ConfigType = {
|
||||||
[key in CONFIG_KEYS]?: any;
|
[CONFIG_KEYS.OCO_API_KEY]?: string;
|
||||||
|
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT]: number;
|
||||||
|
[CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT]: number;
|
||||||
|
[CONFIG_KEYS.OCO_API_URL]?: string;
|
||||||
|
[CONFIG_KEYS.OCO_DESCRIPTION]: boolean;
|
||||||
|
[CONFIG_KEYS.OCO_EMOJI]: boolean;
|
||||||
|
[CONFIG_KEYS.OCO_WHY]: boolean;
|
||||||
|
[CONFIG_KEYS.OCO_MODEL]: string;
|
||||||
|
[CONFIG_KEYS.OCO_LANGUAGE]: string;
|
||||||
|
[CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER]: string;
|
||||||
|
[CONFIG_KEYS.OCO_PROMPT_MODULE]: OCO_PROMPT_MODULE_ENUM;
|
||||||
|
[CONFIG_KEYS.OCO_AI_PROVIDER]: OCO_AI_PROVIDER_ENUM;
|
||||||
|
[CONFIG_KEYS.OCO_GITPUSH]: boolean;
|
||||||
|
[CONFIG_KEYS.OCO_ONE_LINE_COMMIT]: boolean;
|
||||||
|
[CONFIG_KEYS.OCO_TEST_MOCK_TYPE]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultConfigPath = pathJoin(homedir(), '.opencommit');
|
export const defaultConfigPath = pathJoin(homedir(), '.opencommit');
|
||||||
const defaultEnvPath = pathResolve(process.cwd(), '.env');
|
export const defaultEnvPath = pathResolve(process.cwd(), '.env');
|
||||||
|
|
||||||
export const getConfig = ({
|
const assertConfigsAreValid = (config: Record<string, any>) => {
|
||||||
configPath = defaultConfigPath,
|
for (const [key, value] of Object.entries(config)) {
|
||||||
envPath = defaultEnvPath
|
if (!value) continue;
|
||||||
}: {
|
|
||||||
configPath?: string
|
|
||||||
envPath?: string
|
|
||||||
} = {}): ConfigType | null => {
|
|
||||||
dotenv.config({ path: envPath });
|
|
||||||
const configFromEnv = {
|
|
||||||
OCO_OPENAI_API_KEY: process.env.OCO_OPENAI_API_KEY,
|
|
||||||
OCO_ANTHROPIC_API_KEY: process.env.OCO_ANTHROPIC_API_KEY,
|
|
||||||
OCO_AZURE_API_KEY: process.env.OCO_AZURE_API_KEY,
|
|
||||||
OCO_TOKENS_MAX_INPUT: process.env.OCO_TOKENS_MAX_INPUT
|
|
||||||
? Number(process.env.OCO_TOKENS_MAX_INPUT)
|
|
||||||
: undefined,
|
|
||||||
OCO_TOKENS_MAX_OUTPUT: process.env.OCO_TOKENS_MAX_OUTPUT
|
|
||||||
? Number(process.env.OCO_TOKENS_MAX_OUTPUT)
|
|
||||||
: undefined,
|
|
||||||
OCO_OPENAI_BASE_PATH: process.env.OCO_OPENAI_BASE_PATH,
|
|
||||||
OCO_DESCRIPTION: process.env.OCO_DESCRIPTION === 'true' ? true : false,
|
|
||||||
OCO_EMOJI: process.env.OCO_EMOJI === 'true' ? true : false,
|
|
||||||
OCO_MODEL: process.env.OCO_MODEL || getDefaultModel(process.env.OCO_AI_PROVIDER),
|
|
||||||
OCO_LANGUAGE: process.env.OCO_LANGUAGE || 'en',
|
|
||||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER:
|
|
||||||
process.env.OCO_MESSAGE_TEMPLATE_PLACEHOLDER || '$msg',
|
|
||||||
OCO_PROMPT_MODULE: process.env.OCO_PROMPT_MODULE || 'conventional-commit',
|
|
||||||
OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER || 'openai',
|
|
||||||
OCO_GITPUSH: process.env.OCO_GITPUSH === 'false' ? false : true,
|
|
||||||
OCO_ONE_LINE_COMMIT:
|
|
||||||
process.env.OCO_ONE_LINE_COMMIT === 'true' ? true : false,
|
|
||||||
OCO_AZURE_ENDPOINT: process.env.OCO_AZURE_ENDPOINT || '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const configExists = existsSync(configPath);
|
if (typeof value === 'string' && ['null', 'undefined'].includes(value)) {
|
||||||
if (!configExists) return configFromEnv;
|
config[key] = undefined;
|
||||||
|
|
||||||
const configFile = readFileSync(configPath, 'utf8');
|
|
||||||
const config = iniParse(configFile);
|
|
||||||
|
|
||||||
for (const configKey of Object.keys(config)) {
|
|
||||||
if (
|
|
||||||
['null', 'undefined'].includes(config[configKey])
|
|
||||||
) {
|
|
||||||
config[configKey] = undefined;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const validator = configValidators[configKey as CONFIG_KEYS];
|
|
||||||
const validValue = validator(
|
|
||||||
config[configKey] ?? configFromEnv[configKey as CONFIG_KEYS],
|
|
||||||
config
|
|
||||||
);
|
|
||||||
|
|
||||||
config[configKey] = validValue;
|
try {
|
||||||
|
const validate = configValidators[key as CONFIG_KEYS];
|
||||||
|
validate(value, config);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
outro(`Unknown '${configKey}' config option or missing validator.`);
|
outro(`Unknown '${key}' config option or missing validator.`);
|
||||||
outro(
|
outro(
|
||||||
`Manually fix the '.env' file or global '~/.opencommit' config file.`
|
`Manually fix the '.env' file or global '~/.opencommit' config file.`
|
||||||
);
|
);
|
||||||
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
enum OCO_PROMPT_MODULE_ENUM {
|
||||||
|
CONVENTIONAL_COMMIT = 'conventional-commit',
|
||||||
|
COMMITLINT = '@commitlint'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_CONFIG = {
|
||||||
|
OCO_TOKENS_MAX_INPUT: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT,
|
||||||
|
OCO_TOKENS_MAX_OUTPUT: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT,
|
||||||
|
OCO_DESCRIPTION: false,
|
||||||
|
OCO_EMOJI: false,
|
||||||
|
OCO_MODEL: getDefaultModel('openai'),
|
||||||
|
OCO_LANGUAGE: 'en',
|
||||||
|
OCO_MESSAGE_TEMPLATE_PLACEHOLDER: '$msg',
|
||||||
|
OCO_PROMPT_MODULE: OCO_PROMPT_MODULE_ENUM.CONVENTIONAL_COMMIT,
|
||||||
|
OCO_AI_PROVIDER: OCO_AI_PROVIDER_ENUM.OPENAI,
|
||||||
|
OCO_ONE_LINE_COMMIT: false,
|
||||||
|
OCO_TEST_MOCK_TYPE: 'commit-message',
|
||||||
|
OCO_FLOWISE_ENDPOINT: ':',
|
||||||
|
OCO_WHY: false,
|
||||||
|
OCO_GITPUSH: true // todo: deprecate
|
||||||
|
};
|
||||||
|
|
||||||
|
const initGlobalConfig = (configPath: string = defaultConfigPath) => {
|
||||||
|
writeFileSync(configPath, iniStringify(DEFAULT_CONFIG), 'utf8');
|
||||||
|
return DEFAULT_CONFIG;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseConfigVarValue = (value?: any) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch (error) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEnvConfig = (envPath: string) => {
|
||||||
|
dotenv.config({ path: envPath });
|
||||||
|
|
||||||
|
return {
|
||||||
|
OCO_MODEL: process.env.OCO_MODEL,
|
||||||
|
OCO_API_URL: process.env.OCO_API_URL,
|
||||||
|
OCO_API_KEY: process.env.OCO_API_KEY,
|
||||||
|
OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER as OCO_AI_PROVIDER_ENUM,
|
||||||
|
|
||||||
|
OCO_TOKENS_MAX_INPUT: parseConfigVarValue(process.env.OCO_TOKENS_MAX_INPUT),
|
||||||
|
OCO_TOKENS_MAX_OUTPUT: parseConfigVarValue(
|
||||||
|
process.env.OCO_TOKENS_MAX_OUTPUT
|
||||||
|
),
|
||||||
|
|
||||||
|
OCO_DESCRIPTION: parseConfigVarValue(process.env.OCO_DESCRIPTION),
|
||||||
|
OCO_EMOJI: parseConfigVarValue(process.env.OCO_EMOJI),
|
||||||
|
OCO_LANGUAGE: process.env.OCO_LANGUAGE,
|
||||||
|
OCO_MESSAGE_TEMPLATE_PLACEHOLDER:
|
||||||
|
process.env.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
|
||||||
|
OCO_PROMPT_MODULE: process.env.OCO_PROMPT_MODULE as OCO_PROMPT_MODULE_ENUM,
|
||||||
|
OCO_ONE_LINE_COMMIT: parseConfigVarValue(process.env.OCO_ONE_LINE_COMMIT),
|
||||||
|
OCO_TEST_MOCK_TYPE: process.env.OCO_TEST_MOCK_TYPE,
|
||||||
|
|
||||||
|
OCO_GITPUSH: parseConfigVarValue(process.env.OCO_GITPUSH) // todo: deprecate
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setGlobalConfig = (
|
||||||
|
config: ConfigType,
|
||||||
|
configPath: string = defaultConfigPath
|
||||||
|
) => {
|
||||||
|
writeFileSync(configPath, iniStringify(config), 'utf8');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIsGlobalConfigFileExist = (
|
||||||
|
configPath: string = defaultConfigPath
|
||||||
|
) => {
|
||||||
|
return existsSync(configPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGlobalConfig = (configPath: string = defaultConfigPath) => {
|
||||||
|
let globalConfig: ConfigType;
|
||||||
|
|
||||||
|
const isGlobalConfigFileExist = getIsGlobalConfigFileExist(configPath);
|
||||||
|
if (!isGlobalConfigFileExist) globalConfig = initGlobalConfig(configPath);
|
||||||
|
else {
|
||||||
|
const configFile = readFileSync(configPath, 'utf8');
|
||||||
|
globalConfig = iniParse(configFile) as ConfigType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges two configs.
|
||||||
|
* Env config takes precedence over global ~/.opencommit config file
|
||||||
|
* @param main - env config
|
||||||
|
* @param fallback - global ~/.opencommit config file
|
||||||
|
* @returns merged config
|
||||||
|
*/
|
||||||
|
const mergeConfigs = (main: Partial<ConfigType>, fallback: ConfigType) => {
|
||||||
|
const allKeys = new Set([...Object.keys(main), ...Object.keys(fallback)]);
|
||||||
|
return Array.from(allKeys).reduce((acc, key) => {
|
||||||
|
acc[key] = parseConfigVarValue(main[key] ?? fallback[key]);
|
||||||
|
return acc;
|
||||||
|
}, {} as ConfigType);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GetConfigOptions {
|
||||||
|
globalPath?: string;
|
||||||
|
envPath?: string;
|
||||||
|
setDefaultValues?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getConfig = ({
|
||||||
|
envPath = defaultEnvPath,
|
||||||
|
globalPath = defaultConfigPath
|
||||||
|
}: GetConfigOptions = {}): ConfigType => {
|
||||||
|
const envConfig = getEnvConfig(envPath);
|
||||||
|
const globalConfig = getGlobalConfig(globalPath);
|
||||||
|
|
||||||
|
const config = mergeConfigs(envConfig, globalConfig);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setConfig = (keyValues: [key: string, value: string][], configPath: string = defaultConfigPath) => {
|
export const setConfig = (
|
||||||
const config = getConfig() || {};
|
keyValues: [key: string, value: string | boolean | number | null][],
|
||||||
|
globalConfigPath: string = defaultConfigPath
|
||||||
|
) => {
|
||||||
|
const config = getConfig({
|
||||||
|
globalPath: globalConfigPath
|
||||||
|
});
|
||||||
|
|
||||||
for (const [configKey, configValue] of keyValues) {
|
const configToSet = {};
|
||||||
if (!configValidators.hasOwnProperty(configKey)) {
|
|
||||||
throw new Error(`Unsupported config key: ${configKey}`);
|
for (let [key, value] of keyValues) {
|
||||||
|
if (!configValidators.hasOwnProperty(key)) {
|
||||||
|
const supportedKeys = Object.keys(configValidators).join('\n');
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported config key: ${key}. Expected keys are:\n\n${supportedKeys}.\n\nFor more help refer to our docs: https://github.com/di-sukharev/opencommit`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsedConfigValue;
|
let parsedConfigValue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parsedConfigValue = JSON.parse(configValue);
|
if (typeof value === 'string') parsedConfigValue = JSON.parse(value);
|
||||||
|
else parsedConfigValue = value;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
parsedConfigValue = configValue;
|
parsedConfigValue = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validValue =
|
const validValue = configValidators[key as CONFIG_KEYS](
|
||||||
configValidators[configKey as CONFIG_KEYS](parsedConfigValue);
|
parsedConfigValue,
|
||||||
config[configKey as CONFIG_KEYS] = validValue;
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
configToSet[key] = validValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFileSync(configPath, iniStringify(config), 'utf8');
|
setGlobalConfig(mergeConfigs(configToSet, config), globalConfigPath);
|
||||||
|
|
||||||
outro(`${chalk.green('✔')} Config successfully set`);
|
outro(`${chalk.green('✔')} config successfully set`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const configCommand = command(
|
export const configCommand = command(
|
||||||
@@ -377,9 +502,9 @@ export const configCommand = command(
|
|||||||
parameters: ['<mode>', '<key=values...>']
|
parameters: ['<mode>', '<key=values...>']
|
||||||
},
|
},
|
||||||
async (argv) => {
|
async (argv) => {
|
||||||
intro('opencommit — config');
|
|
||||||
try {
|
try {
|
||||||
const { mode, keyValues } = argv._;
|
const { mode, keyValues } = argv._;
|
||||||
|
intro(`COMMAND: config ${mode} ${keyValues}`);
|
||||||
|
|
||||||
if (mode === CONFIG_MODES.get) {
|
if (mode === CONFIG_MODES.get) {
|
||||||
const config = getConfig() || {};
|
const config = getConfig() || {};
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
|
import { intro, outro } from '@clack/prompts';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { command } from 'cleye';
|
import { command } from 'cleye';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { intro, outro } from '@clack/prompts';
|
|
||||||
|
|
||||||
import { COMMANDS } from '../CommandsEnum.js';
|
|
||||||
import { assertGitRepo, getCoreHooksPath } from '../utils/git.js';
|
import { assertGitRepo, getCoreHooksPath } from '../utils/git.js';
|
||||||
|
import { COMMANDS } from './ENUMS';
|
||||||
|
|
||||||
const HOOK_NAME = 'prepare-commit-msg';
|
const HOOK_NAME = 'prepare-commit-msg';
|
||||||
const DEFAULT_SYMLINK_URL = path.join('.git', 'hooks', HOOK_NAME);
|
const DEFAULT_SYMLINK_URL = path.join('.git', 'hooks', HOOK_NAME);
|
||||||
@@ -94,7 +92,7 @@ export const hookCommand = command(
|
|||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unsupported mode: ${mode}. Supported modes are: 'set' or 'unset', do: \`oco hook set\``
|
`Unsupported mode: ${mode}. Supported modes are: 'set' or 'unset'. Run: \`oco hook set\``
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
outro(`${chalk.red('✖')} ${error}`);
|
outro(`${chalk.red('✖')} ${error}`);
|
||||||
|
|||||||
@@ -39,10 +39,11 @@ export const prepareCommitMessageHook = async (
|
|||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
if (!config?.OCO_OPENAI_API_KEY && !config?.OCO_ANTHROPIC_API_KEY && !config?.OCO_AZURE_API_KEY) {
|
if (!config.OCO_API_KEY) {
|
||||||
throw new Error(
|
outro(
|
||||||
'No OPEN_AI_API or OCO_ANTHROPIC_API_KEY or OCO_AZURE_API_KEY exists. Set your key in ~/.opencommit'
|
'No OCO_API_KEY is set. Set your key via `oco config set OCO_API_KEY=<value>. For more info see https://github.com/di-sukharev/opencommit'
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const spin = spinner();
|
const spin = spinner();
|
||||||
|
|||||||
@@ -1,7 +1,28 @@
|
|||||||
import { ChatCompletionRequestMessage } from 'openai';
|
import AnthropicClient from '@anthropic-ai/sdk';
|
||||||
|
import { OpenAIClient as AzureOpenAIClient } from '@azure/openai';
|
||||||
|
import { GoogleGenerativeAI as GeminiClient } from '@google/generative-ai';
|
||||||
|
import { AxiosInstance as RawAxiosClient } from 'axios';
|
||||||
|
import { OpenAI as OpenAIClient } from 'openai';
|
||||||
|
|
||||||
|
export interface AiEngineConfig {
|
||||||
|
apiKey: string;
|
||||||
|
model: string;
|
||||||
|
maxTokensOutput: number;
|
||||||
|
maxTokensInput: number;
|
||||||
|
baseURL?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client =
|
||||||
|
| OpenAIClient
|
||||||
|
| AzureOpenAIClient
|
||||||
|
| AnthropicClient
|
||||||
|
| RawAxiosClient
|
||||||
|
| GeminiClient;
|
||||||
|
|
||||||
export interface AiEngine {
|
export interface AiEngine {
|
||||||
|
config: AiEngineConfig;
|
||||||
|
client: Client;
|
||||||
generateCommitMessage(
|
generateCommitMessage(
|
||||||
messages: Array<ChatCompletionRequestMessage>
|
messages: Array<OpenAIClient.Chat.Completions.ChatCompletionMessageParam>
|
||||||
): Promise<string | undefined>;
|
): Promise<string | null | undefined>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,106 +1,62 @@
|
|||||||
|
import AnthropicClient from '@anthropic-ai/sdk';
|
||||||
|
import {
|
||||||
|
MessageCreateParamsNonStreaming,
|
||||||
|
MessageParam
|
||||||
|
} from '@anthropic-ai/sdk/resources/messages.mjs';
|
||||||
|
import { outro } from '@clack/prompts';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import { OpenAI } from 'openai';
|
||||||
import Anthropic from '@anthropic-ai/sdk';
|
|
||||||
import {ChatCompletionRequestMessage} from 'openai'
|
|
||||||
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources';
|
|
||||||
|
|
||||||
import { intro, outro } from '@clack/prompts';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CONFIG_MODES,
|
|
||||||
DEFAULT_TOKEN_LIMITS,
|
|
||||||
getConfig
|
|
||||||
} from '../commands/config';
|
|
||||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||||
import { tokenCount } from '../utils/tokenCount';
|
import { tokenCount } from '../utils/tokenCount';
|
||||||
import { AiEngine } from './Engine';
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
import { MODEL_LIST } from '../commands/config';
|
|
||||||
|
|
||||||
const config = getConfig();
|
interface AnthropicConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
const MAX_TOKENS_OUTPUT =
|
export class AnthropicEngine implements AiEngine {
|
||||||
config?.OCO_TOKENS_MAX_OUTPUT ||
|
config: AnthropicConfig;
|
||||||
DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
client: AnthropicClient;
|
||||||
const MAX_TOKENS_INPUT =
|
|
||||||
config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
|
||||||
|
|
||||||
let provider = config?.OCO_AI_PROVIDER;
|
constructor(config) {
|
||||||
let apiKey = config?.OCO_ANTHROPIC_API_KEY;
|
this.config = config;
|
||||||
const [command, mode] = process.argv.slice(2);
|
this.client = new AnthropicClient({ apiKey: this.config.apiKey });
|
||||||
if (
|
|
||||||
provider === 'anthropic' &&
|
|
||||||
!apiKey &&
|
|
||||||
command !== 'config' &&
|
|
||||||
mode !== CONFIG_MODES.set
|
|
||||||
) {
|
|
||||||
intro('opencommit');
|
|
||||||
|
|
||||||
outro(
|
|
||||||
'OCO_ANTHROPIC_API_KEY is not set, please run `oco config set OCO_ANTHROPIC_API_KEY=<your token> . If you are using Claude, make sure you add payment details, so API works.`'
|
|
||||||
);
|
|
||||||
outro(
|
|
||||||
'For help look into README https://github.com/di-sukharev/opencommit#setup'
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const MODEL = config?.OCO_MODEL;
|
|
||||||
if (provider === 'anthropic' &&
|
|
||||||
!MODEL_LIST.anthropic.includes(MODEL) &&
|
|
||||||
command !== 'config' &&
|
|
||||||
mode !== CONFIG_MODES.set) {
|
|
||||||
outro(
|
|
||||||
`${chalk.red('✖')} Unsupported model ${MODEL} for Anthropic. Supported models are: ${MODEL_LIST.anthropic.join(
|
|
||||||
', '
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
class AnthropicAi implements AiEngine {
|
|
||||||
private anthropicAiApiConfiguration = {
|
|
||||||
apiKey: apiKey
|
|
||||||
};
|
|
||||||
private anthropicAI!: Anthropic;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.anthropicAI = new Anthropic(this.anthropicAiApiConfiguration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateCommitMessage = async (
|
public generateCommitMessage = async (
|
||||||
messages: Array<ChatCompletionRequestMessage>
|
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||||
): Promise<string | undefined> => {
|
): Promise<string | undefined> => {
|
||||||
|
const systemMessage = messages.find((msg) => msg.role === 'system')
|
||||||
const systemMessage = messages.find(msg => msg.role === 'system')?.content as string;
|
?.content as string;
|
||||||
const restMessages = messages.filter((msg) => msg.role !== 'system') as MessageParam[];
|
const restMessages = messages.filter(
|
||||||
|
(msg) => msg.role !== 'system'
|
||||||
|
) as MessageParam[];
|
||||||
|
|
||||||
const params: MessageCreateParamsNonStreaming = {
|
const params: MessageCreateParamsNonStreaming = {
|
||||||
model: MODEL,
|
model: this.config.model,
|
||||||
system: systemMessage,
|
system: systemMessage,
|
||||||
messages: restMessages,
|
messages: restMessages,
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
top_p: 0.1,
|
top_p: 0.1,
|
||||||
max_tokens: MAX_TOKENS_OUTPUT
|
max_tokens: this.config.maxTokensOutput
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const REQUEST_TOKENS = messages
|
const REQUEST_TOKENS = messages
|
||||||
.map((msg) => tokenCount(msg.content as string) + 4)
|
.map((msg) => tokenCount(msg.content as string) + 4)
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
if (REQUEST_TOKENS > MAX_TOKENS_INPUT - MAX_TOKENS_OUTPUT) {
|
if (
|
||||||
|
REQUEST_TOKENS >
|
||||||
|
this.config.maxTokensInput - this.config.maxTokensOutput
|
||||||
|
) {
|
||||||
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await this.anthropicAI.messages.create(params);
|
const data = await this.client.messages.create(params);
|
||||||
|
|
||||||
const message = data?.content[0].text;
|
const message = data?.content[0].text;
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
outro(`${chalk.red('✖')} ${JSON.stringify(params)}`);
|
|
||||||
|
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
||||||
|
|
||||||
@@ -120,5 +76,3 @@ class AnthropicAi implements AiEngine {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const anthropicAi = new AnthropicAi();
|
|
||||||
@@ -1,81 +1,51 @@
|
|||||||
|
import {
|
||||||
|
AzureKeyCredential,
|
||||||
|
OpenAIClient as AzureOpenAIClient
|
||||||
|
} from '@azure/openai';
|
||||||
|
import { outro } from '@clack/prompts';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { execa } from 'execa';
|
import { OpenAI } from 'openai';
|
||||||
import {
|
|
||||||
ChatCompletionRequestMessage,
|
|
||||||
} from 'openai';
|
|
||||||
|
|
||||||
import { OpenAIClient, AzureKeyCredential } from '@azure/openai';
|
|
||||||
|
|
||||||
import { intro, outro } from '@clack/prompts';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CONFIG_MODES,
|
|
||||||
DEFAULT_TOKEN_LIMITS,
|
|
||||||
getConfig
|
|
||||||
} from '../commands/config';
|
|
||||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||||
import { tokenCount } from '../utils/tokenCount';
|
import { tokenCount } from '../utils/tokenCount';
|
||||||
import { AiEngine } from './Engine';
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
const config = getConfig();
|
interface AzureAiEngineConfig extends AiEngineConfig {
|
||||||
|
baseURL: string;
|
||||||
const MAX_TOKENS_OUTPUT =
|
apiKey: string;
|
||||||
config?.OCO_TOKENS_MAX_OUTPUT ||
|
|
||||||
DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
|
||||||
const MAX_TOKENS_INPUT =
|
|
||||||
config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
|
||||||
let basePath = config?.OCO_OPENAI_BASE_PATH;
|
|
||||||
let apiKey = config?.OCO_AZURE_API_KEY;
|
|
||||||
let apiEndpoint = config?.OCO_AZURE_ENDPOINT;
|
|
||||||
|
|
||||||
const [command, mode] = process.argv.slice(2);
|
|
||||||
|
|
||||||
const provider = config?.OCO_AI_PROVIDER;
|
|
||||||
|
|
||||||
if (
|
|
||||||
provider === 'azure' &&
|
|
||||||
!apiKey &&
|
|
||||||
!apiEndpoint &&
|
|
||||||
command !== 'config' &&
|
|
||||||
mode !== CONFIG_MODES.set
|
|
||||||
) {
|
|
||||||
intro('opencommit');
|
|
||||||
|
|
||||||
outro(
|
|
||||||
'OCO_AZURE_API_KEY or OCO_AZURE_ENDPOINT are not set, please run `oco config set OCO_AZURE_API_KEY=<your token> . If you are using GPT, make sure you add payment details, so API works.`'
|
|
||||||
);
|
|
||||||
outro(
|
|
||||||
'For help look into README https://github.com/di-sukharev/opencommit#setup'
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODEL = config?.OCO_MODEL || 'gpt-3.5-turbo';
|
export class AzureEngine implements AiEngine {
|
||||||
|
config: AzureAiEngineConfig;
|
||||||
|
client: AzureOpenAIClient;
|
||||||
|
|
||||||
class Azure implements AiEngine {
|
constructor(config: AzureAiEngineConfig) {
|
||||||
private openAI!: OpenAIClient;
|
this.config = config;
|
||||||
|
this.client = new AzureOpenAIClient(
|
||||||
constructor() {
|
this.config.baseURL,
|
||||||
if (provider === 'azure') {
|
new AzureKeyCredential(this.config.apiKey)
|
||||||
this.openAI = new OpenAIClient(apiEndpoint, new AzureKeyCredential(apiKey));
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateCommitMessage = async (
|
generateCommitMessage = async (
|
||||||
messages: Array<ChatCompletionRequestMessage>
|
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||||
): Promise<string | undefined> => {
|
): Promise<string | undefined> => {
|
||||||
try {
|
try {
|
||||||
const REQUEST_TOKENS = messages
|
const REQUEST_TOKENS = messages
|
||||||
.map((msg) => tokenCount(msg.content) + 4)
|
.map((msg) => tokenCount(msg.content as string) + 4)
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
if (REQUEST_TOKENS > MAX_TOKENS_INPUT - MAX_TOKENS_OUTPUT) {
|
if (
|
||||||
|
REQUEST_TOKENS >
|
||||||
|
this.config.maxTokensInput - this.config.maxTokensOutput
|
||||||
|
) {
|
||||||
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await this.openAI.getChatCompletions(MODEL, messages);
|
const data = await this.client.getChatCompletions(
|
||||||
|
this.config.model,
|
||||||
|
messages
|
||||||
|
);
|
||||||
|
|
||||||
const message = data.choices[0].message;
|
const message = data.choices[0].message;
|
||||||
|
|
||||||
@@ -84,10 +54,10 @@ class Azure implements AiEngine {
|
|||||||
}
|
}
|
||||||
return message?.content;
|
return message?.content;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
outro(`${chalk.red('✖')} ${MODEL}`);
|
outro(`${chalk.red('✖')} ${this.config.model}`);
|
||||||
|
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
outro(`${chalk.red('✖')} ${JSON.stringify(error)}`);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
axios.isAxiosError<{ error?: { message: string } }>(error) &&
|
axios.isAxiosError<{ error?: { message: string } }>(error) &&
|
||||||
@@ -105,5 +75,3 @@ class Azure implements AiEngine {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const azure = new Azure();
|
|
||||||
|
|||||||
45
src/engine/flowise.ts
Normal file
45
src/engine/flowise.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import { OpenAI } from 'openai';
|
||||||
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
|
interface FlowiseAiConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
|
export class FlowiseEngine implements AiEngine {
|
||||||
|
config: FlowiseAiConfig;
|
||||||
|
client: AxiosInstance;
|
||||||
|
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.client = axios.create({
|
||||||
|
url: `${config.baseURL}/${config.apiKey}`,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateCommitMessage(
|
||||||
|
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const gitDiff = (messages[messages.length - 1]?.content as string)
|
||||||
|
.replace(/\\/g, '\\\\')
|
||||||
|
.replace(/"/g, '\\"')
|
||||||
|
.replace(/\n/g, '\\n')
|
||||||
|
.replace(/\r/g, '\\r')
|
||||||
|
.replace(/\t/g, '\\t');
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
question: gitDiff,
|
||||||
|
overrideConfig: {
|
||||||
|
systemMessagePrompt: messages[0]?.content
|
||||||
|
},
|
||||||
|
history: messages.slice(1, -1)
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const response = await this.client.post('', payload);
|
||||||
|
const message = response.data;
|
||||||
|
return message?.text;
|
||||||
|
} catch (err: any) {
|
||||||
|
const message = err.response?.data?.error ?? err.message;
|
||||||
|
throw new Error('local model issues. details: ' + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/engine/gemini.ts
Normal file
88
src/engine/gemini.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import {
|
||||||
|
Content,
|
||||||
|
GoogleGenerativeAI,
|
||||||
|
HarmBlockThreshold,
|
||||||
|
HarmCategory,
|
||||||
|
Part
|
||||||
|
} from '@google/generative-ai';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { OpenAI } from 'openai';
|
||||||
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
|
interface GeminiConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
|
export class GeminiEngine implements AiEngine {
|
||||||
|
config: GeminiConfig;
|
||||||
|
client: GoogleGenerativeAI;
|
||||||
|
|
||||||
|
constructor(config) {
|
||||||
|
this.client = new GoogleGenerativeAI(config.apiKey);
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateCommitMessage(
|
||||||
|
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const systemInstruction = messages
|
||||||
|
.filter((m) => m.role === 'system')
|
||||||
|
.map((m) => m.content)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const gemini = this.client.getGenerativeModel({
|
||||||
|
model: this.config.model,
|
||||||
|
systemInstruction
|
||||||
|
});
|
||||||
|
|
||||||
|
const contents = messages
|
||||||
|
.filter((m) => m.role !== 'system')
|
||||||
|
.map(
|
||||||
|
(m) =>
|
||||||
|
({
|
||||||
|
parts: [{ text: m.content } as Part],
|
||||||
|
role: m.role === 'user' ? m.role : 'model'
|
||||||
|
} as Content)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await gemini.generateContent({
|
||||||
|
contents,
|
||||||
|
safetySettings: [
|
||||||
|
{
|
||||||
|
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||||
|
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
|
||||||
|
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
||||||
|
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
||||||
|
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||||
|
}
|
||||||
|
],
|
||||||
|
generationConfig: {
|
||||||
|
maxOutputTokens: this.config.maxTokensOutput,
|
||||||
|
temperature: 0,
|
||||||
|
topP: 0.1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.response.text();
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
if (
|
||||||
|
axios.isAxiosError<{ error?: { message: string } }>(error) &&
|
||||||
|
error.response?.status === 401
|
||||||
|
) {
|
||||||
|
const geminiError = error.response.data.error;
|
||||||
|
if (geminiError) throw new Error(geminiError?.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +1,44 @@
|
|||||||
import axios, { AxiosError } from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
import { ChatCompletionRequestMessage } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
import { AiEngine } from './Engine';
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
import {
|
interface OllamaConfig extends AiEngineConfig {}
|
||||||
getConfig
|
|
||||||
} from '../commands/config';
|
|
||||||
|
|
||||||
const config = getConfig();
|
export class OllamaEngine implements AiEngine {
|
||||||
|
config: OllamaConfig;
|
||||||
|
client: AxiosInstance;
|
||||||
|
|
||||||
export class OllamaAi implements AiEngine {
|
constructor(config) {
|
||||||
private model = "mistral"; // as default model of Ollama
|
this.config = config;
|
||||||
|
this.client = axios.create({
|
||||||
setModel(model: string) {
|
url: config.baseURL
|
||||||
this.model = model ?? config?.OCO_MODEL ?? 'mistral';
|
? `${config.baseURL}/${config.apiKey}`
|
||||||
|
: 'http://localhost:11434/api/chat',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateCommitMessage(
|
async generateCommitMessage(
|
||||||
messages: Array<ChatCompletionRequestMessage>
|
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
const model = this.model;
|
const params = {
|
||||||
|
model: this.config.model ?? 'mistral',
|
||||||
//console.log(messages);
|
|
||||||
//process.exit()
|
|
||||||
|
|
||||||
const url = 'http://localhost:11434/api/chat';
|
|
||||||
const p = {
|
|
||||||
model,
|
|
||||||
messages,
|
messages,
|
||||||
options: { temperature: 0, top_p: 0.1 },
|
options: { temperature: 0, top_p: 0.1 },
|
||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(url, p, {
|
const response = await this.client.post(
|
||||||
headers: {
|
this.client.getUri(this.config),
|
||||||
'Content-Type': 'application/json'
|
params
|
||||||
}
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const message = response.data.message;
|
const message = response.data.message;
|
||||||
|
|
||||||
return message?.content;
|
return message?.content;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const message = err.response?.data?.error ?? err.message;
|
const message = err.response?.data?.error ?? err.message;
|
||||||
throw new Error('local model issues. details: ' + message);
|
throw new Error(`Ollama provider error: ${message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ollamaAi = new OllamaAi();
|
|
||||||
|
|||||||
@@ -1,129 +1,59 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import chalk from 'chalk';
|
import { OpenAI } from 'openai';
|
||||||
import { execa } from 'execa';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ChatCompletionRequestMessage,
|
|
||||||
Configuration as OpenAiApiConfiguration,
|
|
||||||
OpenAIApi
|
|
||||||
} from 'openai';
|
|
||||||
|
|
||||||
import { intro, outro } from '@clack/prompts';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CONFIG_MODES,
|
|
||||||
DEFAULT_TOKEN_LIMITS,
|
|
||||||
getConfig
|
|
||||||
} from '../commands/config';
|
|
||||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||||
import { tokenCount } from '../utils/tokenCount';
|
import { tokenCount } from '../utils/tokenCount';
|
||||||
import { AiEngine } from './Engine';
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
import { MODEL_LIST } from '../commands/config';
|
|
||||||
|
|
||||||
const config = getConfig();
|
interface OpenAiConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
const MAX_TOKENS_OUTPUT =
|
export class OpenAiEngine implements AiEngine {
|
||||||
config?.OCO_TOKENS_MAX_OUTPUT ||
|
config: OpenAiConfig;
|
||||||
DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
client: OpenAI;
|
||||||
const MAX_TOKENS_INPUT =
|
|
||||||
config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
|
||||||
let basePath = config?.OCO_OPENAI_BASE_PATH;
|
|
||||||
let apiKey = config?.OCO_OPENAI_API_KEY;
|
|
||||||
|
|
||||||
const [command, mode] = process.argv.slice(2);
|
constructor(config: OpenAiConfig) {
|
||||||
|
this.config = config;
|
||||||
const provider = config?.OCO_AI_PROVIDER;
|
this.client = new OpenAI({ apiKey: config.apiKey });
|
||||||
|
|
||||||
if (
|
|
||||||
provider === 'openai' &&
|
|
||||||
!apiKey &&
|
|
||||||
command !== 'config' &&
|
|
||||||
mode !== CONFIG_MODES.set
|
|
||||||
) {
|
|
||||||
intro('opencommit');
|
|
||||||
|
|
||||||
outro(
|
|
||||||
'OCO_OPENAI_API_KEY is not set, please run `oco config set OCO_OPENAI_API_KEY=<your token> . If you are using GPT, make sure you add payment details, so API works.`'
|
|
||||||
);
|
|
||||||
outro(
|
|
||||||
'For help look into README https://github.com/di-sukharev/opencommit#setup'
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const MODEL = config?.OCO_MODEL || 'gpt-3.5-turbo';
|
|
||||||
if (provider === 'openai' &&
|
|
||||||
!MODEL_LIST.openai.includes(MODEL) &&
|
|
||||||
command !== 'config' &&
|
|
||||||
mode !== CONFIG_MODES.set) {
|
|
||||||
outro(
|
|
||||||
`${chalk.red('✖')} Unsupported model ${MODEL} for OpenAI. Supported models are: ${MODEL_LIST.openai.join(
|
|
||||||
', '
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
class OpenAi implements AiEngine {
|
|
||||||
private openAiApiConfiguration = new OpenAiApiConfiguration({
|
|
||||||
apiKey: apiKey
|
|
||||||
});
|
|
||||||
private openAI!: OpenAIApi;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
if (basePath) {
|
|
||||||
this.openAiApiConfiguration.basePath = basePath;
|
|
||||||
}
|
|
||||||
this.openAI = new OpenAIApi(this.openAiApiConfiguration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateCommitMessage = async (
|
public generateCommitMessage = async (
|
||||||
messages: Array<ChatCompletionRequestMessage>
|
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||||
): Promise<string | undefined> => {
|
): Promise<string | null> => {
|
||||||
const params = {
|
const params = {
|
||||||
model: MODEL,
|
model: this.config.model,
|
||||||
messages,
|
messages,
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
top_p: 0.1,
|
top_p: 0.1,
|
||||||
max_tokens: MAX_TOKENS_OUTPUT
|
max_tokens: this.config.maxTokensOutput
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const REQUEST_TOKENS = messages
|
const REQUEST_TOKENS = messages
|
||||||
.map((msg) => tokenCount(msg.content) + 4)
|
.map((msg) => tokenCount(msg.content as string) + 4)
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
if (REQUEST_TOKENS > MAX_TOKENS_INPUT - MAX_TOKENS_OUTPUT) {
|
if (
|
||||||
|
REQUEST_TOKENS >
|
||||||
|
this.config.maxTokensInput - this.config.maxTokensOutput
|
||||||
|
)
|
||||||
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await this.openAI.createChatCompletion(params);
|
const completion = await this.client.chat.completions.create(params);
|
||||||
|
|
||||||
const message = data.choices[0].message;
|
const message = completion.choices[0].message;
|
||||||
|
|
||||||
return message?.content;
|
return message?.content;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
outro(`${chalk.red('✖')} ${JSON.stringify(params)}`);
|
|
||||||
|
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
axios.isAxiosError<{ error?: { message: string } }>(error) &&
|
axios.isAxiosError<{ error?: { message: string } }>(error) &&
|
||||||
error.response?.status === 401
|
error.response?.status === 401
|
||||||
) {
|
) {
|
||||||
const openAiError = error.response.data.error;
|
const openAiError = error.response.data.error;
|
||||||
|
|
||||||
if (openAiError?.message) outro(openAiError.message);
|
if (openAiError) throw new Error(openAiError.message);
|
||||||
outro(
|
|
||||||
'For help look into README https://github.com/di-sukharev/opencommit#setup'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const api = new OpenAi();
|
|
||||||
|
|||||||
@@ -1,12 +1,47 @@
|
|||||||
import { ChatCompletionRequestMessage } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
|
|
||||||
import { AiEngine } from './Engine';
|
import { AiEngine } from './Engine';
|
||||||
|
|
||||||
export class TestAi implements AiEngine {
|
export const TEST_MOCK_TYPES = [
|
||||||
|
'commit-message',
|
||||||
|
'prompt-module-commitlint-config'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type TestMockType = (typeof TEST_MOCK_TYPES)[number];
|
||||||
|
|
||||||
|
type TestAiEngine = Partial<AiEngine> & {
|
||||||
|
mockType: TestMockType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class TestAi implements TestAiEngine {
|
||||||
|
mockType: TestMockType;
|
||||||
|
|
||||||
|
// those are not used in the test engine
|
||||||
|
config: any;
|
||||||
|
client: any;
|
||||||
|
// ---
|
||||||
|
|
||||||
|
constructor(mockType: TestMockType) {
|
||||||
|
this.mockType = mockType;
|
||||||
|
}
|
||||||
|
|
||||||
async generateCommitMessage(
|
async generateCommitMessage(
|
||||||
messages: Array<ChatCompletionRequestMessage>
|
_messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
return 'test commit message';
|
switch (this.mockType) {
|
||||||
|
case 'commit-message':
|
||||||
|
return 'fix(testAi.ts): test commit message';
|
||||||
|
case 'prompt-module-commitlint-config':
|
||||||
|
return (
|
||||||
|
`{\n` +
|
||||||
|
` "localLanguage": "english",\n` +
|
||||||
|
` "commitFix": "fix(server): Change 'port' variable to uppercase 'PORT'",\n` +
|
||||||
|
` "commitFeat": "feat(server): Allow server to listen on a port specified through environment variable",\n` +
|
||||||
|
` "commitDescription": "Change 'port' variable to uppercase 'PORT'. Allow server to listen on a port specified through environment variable."\n` +
|
||||||
|
`}`
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw Error('unsupported test mock type');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const testAi = new TestAi();
|
|
||||||
|
|||||||
@@ -1,31 +1,24 @@
|
|||||||
import {
|
import { OpenAI } from 'openai';
|
||||||
ChatCompletionRequestMessage,
|
|
||||||
ChatCompletionRequestMessageRoleEnum
|
|
||||||
} from 'openai';
|
|
||||||
|
|
||||||
import { DEFAULT_TOKEN_LIMITS, getConfig } from './commands/config';
|
import { DEFAULT_TOKEN_LIMITS, getConfig } from './commands/config';
|
||||||
import { getMainCommitPrompt } from './prompts';
|
import { getMainCommitPrompt } from './prompts';
|
||||||
|
import { getEngine } from './utils/engine';
|
||||||
import { mergeDiffs } from './utils/mergeDiffs';
|
import { mergeDiffs } from './utils/mergeDiffs';
|
||||||
import { tokenCount } from './utils/tokenCount';
|
import { tokenCount } from './utils/tokenCount';
|
||||||
import { getEngine } from './utils/engine';
|
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
const MAX_TOKENS_INPUT =
|
const MAX_TOKENS_INPUT = config.OCO_TOKENS_MAX_INPUT;
|
||||||
config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
const MAX_TOKENS_OUTPUT = config.OCO_TOKENS_MAX_OUTPUT;
|
||||||
const MAX_TOKENS_OUTPUT =
|
|
||||||
config?.OCO_TOKENS_MAX_OUTPUT ||
|
|
||||||
DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
|
||||||
|
|
||||||
const generateCommitMessageChatCompletionPrompt = async (
|
const generateCommitMessageChatCompletionPrompt = async (
|
||||||
diff: string,
|
diff: string,
|
||||||
fullGitMojiSpec: boolean
|
fullGitMojiSpec: boolean
|
||||||
): Promise<Array<ChatCompletionRequestMessage>> => {
|
): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => {
|
||||||
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec);
|
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec);
|
||||||
|
|
||||||
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
|
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
|
||||||
|
|
||||||
chatContextAsCompletionRequest.push({
|
chatContextAsCompletionRequest.push({
|
||||||
role: ChatCompletionRequestMessageRoleEnum.User,
|
role: 'user',
|
||||||
content: diff
|
content: diff
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,13 +36,13 @@ const ADJUSTMENT_FACTOR = 20;
|
|||||||
|
|
||||||
export const generateCommitMessageByDiff = async (
|
export const generateCommitMessageByDiff = async (
|
||||||
diff: string,
|
diff: string,
|
||||||
fullGitMojiSpec: boolean
|
fullGitMojiSpec: boolean = false
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec);
|
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec);
|
||||||
|
|
||||||
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
|
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
|
||||||
(msg) => tokenCount(msg.content) + 4
|
(msg) => tokenCount(msg.content as string) + 4
|
||||||
).reduce((a, b) => a + b, 0);
|
).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
const MAX_REQUEST_TOKENS =
|
const MAX_REQUEST_TOKENS =
|
||||||
@@ -65,9 +58,9 @@ export const generateCommitMessageByDiff = async (
|
|||||||
fullGitMojiSpec
|
fullGitMojiSpec
|
||||||
);
|
);
|
||||||
|
|
||||||
const commitMessages = [];
|
const commitMessages = [] as string[];
|
||||||
for (const promise of commitMessagePromises) {
|
for (const promise of commitMessagePromises) {
|
||||||
commitMessages.push(await promise);
|
commitMessages.push((await promise) as string);
|
||||||
await delay(2000);
|
await delay(2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +99,7 @@ function getMessagesPromisesByChangesInFile(
|
|||||||
maxChangeLength
|
maxChangeLength
|
||||||
);
|
);
|
||||||
|
|
||||||
const lineDiffsWithHeader = [];
|
const lineDiffsWithHeader = [] as string[];
|
||||||
for (const change of mergedChanges) {
|
for (const change of mergedChanges) {
|
||||||
const totalChange = fileHeader + change;
|
const totalChange = fileHeader + change;
|
||||||
if (tokenCount(totalChange) > maxChangeLength) {
|
if (tokenCount(totalChange) > maxChangeLength) {
|
||||||
@@ -135,7 +128,7 @@ function getMessagesPromisesByChangesInFile(
|
|||||||
|
|
||||||
function splitDiff(diff: string, maxChangeLength: number) {
|
function splitDiff(diff: string, maxChangeLength: number) {
|
||||||
const lines = diff.split('\n');
|
const lines = diff.split('\n');
|
||||||
const splitDiffs = [];
|
const splitDiffs = [] as string[];
|
||||||
let currentDiff = '';
|
let currentDiff = '';
|
||||||
|
|
||||||
if (maxChangeLength <= 0) {
|
if (maxChangeLength <= 0) {
|
||||||
@@ -181,7 +174,7 @@ export const getCommitMsgsPromisesFromFileDiffs = async (
|
|||||||
// merge multiple files-diffs into 1 prompt to save tokens
|
// merge multiple files-diffs into 1 prompt to save tokens
|
||||||
const mergedFilesDiffs = mergeDiffs(diffByFiles, maxDiffLength);
|
const mergedFilesDiffs = mergeDiffs(diffByFiles, maxDiffLength);
|
||||||
|
|
||||||
const commitMessagePromises = [];
|
const commitMessagePromises = [] as Promise<string | null | undefined>[];
|
||||||
|
|
||||||
for (const fileDiff of mergedFilesDiffs) {
|
for (const fileDiff of mergedFilesDiffs) {
|
||||||
if (tokenCount(fileDiff) >= maxDiffLength) {
|
if (tokenCount(fileDiff) >= maxDiffLength) {
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { unlinkSync, writeFileSync } from 'fs';
|
|
||||||
|
|
||||||
import core from '@actions/core';
|
import core from '@actions/core';
|
||||||
import exec from '@actions/exec';
|
import exec from '@actions/exec';
|
||||||
import github from '@actions/github';
|
import github from '@actions/github';
|
||||||
import { intro, outro } from '@clack/prompts';
|
import { intro, outro } from '@clack/prompts';
|
||||||
import { PushEvent } from '@octokit/webhooks-types';
|
import { PushEvent } from '@octokit/webhooks-types';
|
||||||
|
import { unlinkSync, writeFileSync } from 'fs';
|
||||||
import { generateCommitMessageByDiff } from './generateCommitMessageFromGitDiff';
|
import { generateCommitMessageByDiff } from './generateCommitMessageFromGitDiff';
|
||||||
import { randomIntFromInterval } from './utils/randomIntFromInterval';
|
import { randomIntFromInterval } from './utils/randomIntFromInterval';
|
||||||
import { sleep } from './utils/sleep';
|
import { sleep } from './utils/sleep';
|
||||||
@@ -54,7 +52,7 @@ async function improveMessagesInChunks(diffsAndSHAs: DiffAndSHA[]) {
|
|||||||
const chunkSize = diffsAndSHAs!.length % 2 === 0 ? 4 : 3;
|
const chunkSize = diffsAndSHAs!.length % 2 === 0 ? 4 : 3;
|
||||||
outro(`Improving commit messages in chunks of ${chunkSize}.`);
|
outro(`Improving commit messages in chunks of ${chunkSize}.`);
|
||||||
const improvePromises = diffsAndSHAs!.map((commit) =>
|
const improvePromises = diffsAndSHAs!.map((commit) =>
|
||||||
generateCommitMessageByDiff(commit.diff)
|
generateCommitMessageByDiff(commit.diff, false)
|
||||||
);
|
);
|
||||||
|
|
||||||
let improvedMessagesAndSHAs: MsgAndSHA[] = [];
|
let improvedMessagesAndSHAs: MsgAndSHA[] = [];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"localLanguage": "简体中文",
|
"localLanguage": "简体中文",
|
||||||
"commitFix": "修复(server.ts):将端口变量从小写port改为大写PORT",
|
"commitFix": "fix(server.ts):将端口变量从小写port改为大写PORT",
|
||||||
"commitFeat": "功能(server.ts):添加对process.env.PORT环境变量的支持",
|
"commitFeat": "feat(server.ts):添加对process.env.PORT环境变量的支持",
|
||||||
"commitDescription": "现在端口变量被命名为PORT,这提高了命名约定的一致性,因为PORT是一个常量。环境变量的支持使应用程序更加灵活,因为它现在可以通过process.env.PORT环境变量在任何可用端口上运行。"
|
"commitDescription": "现在端口变量被命名为PORT,这提高了命名约定的一致性,因为PORT是一个常量。环境变量的支持使应用程序更加灵活,因为它现在可以通过process.env.PORT环境变量在任何可用端口上运行。"
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/migrations/00_use_single_api_key_and_url.ts
Normal file
45
src/migrations/00_use_single_api_key_and_url.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
CONFIG_KEYS,
|
||||||
|
getConfig,
|
||||||
|
OCO_AI_PROVIDER_ENUM,
|
||||||
|
setConfig
|
||||||
|
} from '../commands/config';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const config = getConfig({ setDefaultValues: false });
|
||||||
|
|
||||||
|
const aiProvider = config.OCO_AI_PROVIDER;
|
||||||
|
|
||||||
|
let apiKey: string | undefined;
|
||||||
|
let apiUrl: string | undefined;
|
||||||
|
|
||||||
|
if (aiProvider === OCO_AI_PROVIDER_ENUM.OLLAMA) {
|
||||||
|
apiKey = config['OCO_OLLAMA_API_KEY'];
|
||||||
|
apiUrl = config['OCO_OLLAMA_API_URL'];
|
||||||
|
} else if (aiProvider === OCO_AI_PROVIDER_ENUM.ANTHROPIC) {
|
||||||
|
apiKey = config['OCO_ANTHROPIC_API_KEY'];
|
||||||
|
apiUrl = config['OCO_ANTHROPIC_BASE_PATH'];
|
||||||
|
} else if (aiProvider === OCO_AI_PROVIDER_ENUM.OPENAI) {
|
||||||
|
apiKey = config['OCO_OPENAI_API_KEY'];
|
||||||
|
apiUrl = config['OCO_OPENAI_BASE_PATH'];
|
||||||
|
} else if (aiProvider === OCO_AI_PROVIDER_ENUM.AZURE) {
|
||||||
|
apiKey = config['OCO_AZURE_API_KEY'];
|
||||||
|
apiUrl = config['OCO_AZURE_ENDPOINT'];
|
||||||
|
} else if (aiProvider === OCO_AI_PROVIDER_ENUM.GEMINI) {
|
||||||
|
apiKey = config['OCO_GEMINI_API_KEY'];
|
||||||
|
apiUrl = config['OCO_GEMINI_BASE_PATH'];
|
||||||
|
} else if (aiProvider === OCO_AI_PROVIDER_ENUM.FLOWISE) {
|
||||||
|
apiKey = config['OCO_FLOWISE_API_KEY'];
|
||||||
|
apiUrl = config['OCO_FLOWISE_ENDPOINT'];
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Migration failed, set AI provider first. Run "oco config set OCO_AI_PROVIDER=<provider>", where <provider> is one of: ${Object.values(
|
||||||
|
OCO_AI_PROVIDER_ENUM
|
||||||
|
).join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey) setConfig([[CONFIG_KEYS.OCO_API_KEY, apiKey]]);
|
||||||
|
|
||||||
|
if (apiUrl) setConfig([[CONFIG_KEYS.OCO_API_URL, apiUrl]]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { getGlobalConfig, setGlobalConfig } from '../commands/config';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const obsoleteKeys = [
|
||||||
|
'OCO_OLLAMA_API_KEY',
|
||||||
|
'OCO_OLLAMA_API_URL',
|
||||||
|
'OCO_ANTHROPIC_API_KEY',
|
||||||
|
'OCO_ANTHROPIC_BASE_PATH',
|
||||||
|
'OCO_OPENAI_API_KEY',
|
||||||
|
'OCO_OPENAI_BASE_PATH',
|
||||||
|
'OCO_AZURE_API_KEY',
|
||||||
|
'OCO_AZURE_ENDPOINT',
|
||||||
|
'OCO_GEMINI_API_KEY',
|
||||||
|
'OCO_GEMINI_BASE_PATH',
|
||||||
|
'OCO_FLOWISE_API_KEY',
|
||||||
|
'OCO_FLOWISE_ENDPOINT'
|
||||||
|
];
|
||||||
|
|
||||||
|
const globalConfig = getGlobalConfig();
|
||||||
|
|
||||||
|
const configToOverride = { ...globalConfig };
|
||||||
|
|
||||||
|
for (const key of obsoleteKeys) delete configToOverride[key];
|
||||||
|
|
||||||
|
setGlobalConfig(configToOverride);
|
||||||
|
}
|
||||||
20
src/migrations/02_set_missing_default_values.ts
Normal file
20
src/migrations/02_set_missing_default_values.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import {
|
||||||
|
ConfigType,
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
getGlobalConfig,
|
||||||
|
setConfig
|
||||||
|
} from '../commands/config';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const setDefaultConfigValues = (config: ConfigType) => {
|
||||||
|
const entriesToSet: [key: string, value: string | boolean | number][] = [];
|
||||||
|
for (const entry of Object.entries(DEFAULT_CONFIG)) {
|
||||||
|
const [key, _value] = entry;
|
||||||
|
if (config[key] === 'undefined') entriesToSet.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entriesToSet.length > 0) setConfig(entriesToSet);
|
||||||
|
};
|
||||||
|
|
||||||
|
setDefaultConfigValues(getGlobalConfig());
|
||||||
|
}
|
||||||
18
src/migrations/_migrations.ts
Normal file
18
src/migrations/_migrations.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import migration00 from './00_use_single_api_key_and_url';
|
||||||
|
import migration01 from './01_remove_obsolete_config_keys_from_global_file';
|
||||||
|
import migration02 from './02_set_missing_default_values';
|
||||||
|
|
||||||
|
export const migrations = [
|
||||||
|
{
|
||||||
|
name: '00_use_single_api_key_and_url',
|
||||||
|
run: migration00
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '01_remove_obsolete_config_keys_from_global_file',
|
||||||
|
run: migration01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '02_set_missing_default_values',
|
||||||
|
run: migration02
|
||||||
|
}
|
||||||
|
];
|
||||||
70
src/migrations/_run.ts
Normal file
70
src/migrations/_run.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import { homedir } from 'os';
|
||||||
|
import { join as pathJoin } from 'path';
|
||||||
|
import { migrations } from './_migrations';
|
||||||
|
import { outro } from '@clack/prompts';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import {
|
||||||
|
getConfig,
|
||||||
|
getIsGlobalConfigFileExist,
|
||||||
|
OCO_AI_PROVIDER_ENUM
|
||||||
|
} from '../commands/config';
|
||||||
|
|
||||||
|
const migrationsFile = pathJoin(homedir(), '.opencommit_migrations');
|
||||||
|
|
||||||
|
const getCompletedMigrations = (): string[] => {
|
||||||
|
if (!fs.existsSync(migrationsFile)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const data = fs.readFileSync(migrationsFile, 'utf-8');
|
||||||
|
return data ? JSON.parse(data) : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveCompletedMigration = (migrationName: string) => {
|
||||||
|
const completedMigrations = getCompletedMigrations();
|
||||||
|
completedMigrations.push(migrationName);
|
||||||
|
fs.writeFileSync(
|
||||||
|
migrationsFile,
|
||||||
|
JSON.stringify(completedMigrations, null, 2)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runMigrations = async () => {
|
||||||
|
// if no config file, we assume it's a new installation and no migrations are needed
|
||||||
|
if (!getIsGlobalConfigFileExist()) return;
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
if (config.OCO_AI_PROVIDER === OCO_AI_PROVIDER_ENUM.TEST) return;
|
||||||
|
|
||||||
|
const completedMigrations = getCompletedMigrations();
|
||||||
|
|
||||||
|
let isMigrated = false;
|
||||||
|
|
||||||
|
for (const migration of migrations) {
|
||||||
|
if (!completedMigrations.includes(migration.name)) {
|
||||||
|
try {
|
||||||
|
console.log('Applying migration', migration.name);
|
||||||
|
migration.run();
|
||||||
|
console.log('Migration applied successfully', migration.name);
|
||||||
|
saveCompletedMigration(migration.name);
|
||||||
|
} catch (error) {
|
||||||
|
outro(
|
||||||
|
`${chalk.red('Failed to apply migration')} ${
|
||||||
|
migration.name
|
||||||
|
}: ${error}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isMigrated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMigrated) {
|
||||||
|
outro(
|
||||||
|
`${chalk.green(
|
||||||
|
'✔'
|
||||||
|
)} Migrations to your config were applied successfully. Please rerun.`
|
||||||
|
);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,16 +2,16 @@ import { spinner } from '@clack/prompts';
|
|||||||
|
|
||||||
import { getConfig } from '../../commands/config';
|
import { getConfig } from '../../commands/config';
|
||||||
import { i18n, I18nLocals } from '../../i18n';
|
import { i18n, I18nLocals } from '../../i18n';
|
||||||
|
import { getEngine } from '../../utils/engine';
|
||||||
import { COMMITLINT_LLM_CONFIG_PATH } from './constants';
|
import { COMMITLINT_LLM_CONFIG_PATH } from './constants';
|
||||||
import { computeHash } from './crypto';
|
import { computeHash } from './crypto';
|
||||||
import { commitlintPrompts, inferPromptsFromCommitlintConfig } from './prompts';
|
import { commitlintPrompts, inferPromptsFromCommitlintConfig } from './prompts';
|
||||||
import { getCommitLintPWDConfig } from './pwd-commitlint';
|
import { getCommitLintPWDConfig } from './pwd-commitlint';
|
||||||
import { CommitlintLLMConfig } from './types';
|
import { CommitlintLLMConfig } from './types';
|
||||||
import * as utils from './utils';
|
import * as utils from './utils';
|
||||||
import { getEngine } from '../../utils/engine';
|
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
|
const translation = i18n[(config.OCO_LANGUAGE as I18nLocals) || 'en'];
|
||||||
|
|
||||||
export const configureCommitlintIntegration = async (force = false) => {
|
export const configureCommitlintIntegration = async (force = false) => {
|
||||||
const spin = spinner();
|
const spin = spinner();
|
||||||
@@ -19,7 +19,16 @@ export const configureCommitlintIntegration = async (force = false) => {
|
|||||||
|
|
||||||
const fileExists = await utils.commitlintLLMConfigExists();
|
const fileExists = await utils.commitlintLLMConfigExists();
|
||||||
|
|
||||||
let commitLintConfig = await getCommitLintPWDConfig();
|
const commitLintConfig = await getCommitLintPWDConfig();
|
||||||
|
if (commitLintConfig === null) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to load @commitlint config. Please check the following:
|
||||||
|
* @commitlint >= 9.0.0 is installed in the local directory.
|
||||||
|
* 'node_modules/@commitlint/load' package exists.
|
||||||
|
* A valid @commitlint configuration exists.
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// debug complete @commitlint configuration
|
// debug complete @commitlint configuration
|
||||||
// await fs.writeFile(
|
// await fs.writeFile(
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import {
|
import { OpenAI } from 'openai';
|
||||||
ChatCompletionRequestMessage,
|
|
||||||
ChatCompletionRequestMessageRoleEnum
|
|
||||||
} from 'openai';
|
|
||||||
|
|
||||||
import { outro } from '@clack/prompts';
|
import { outro } from '@clack/prompts';
|
||||||
import {
|
import {
|
||||||
@@ -17,7 +14,7 @@ import { i18n, I18nLocals } from '../../i18n';
|
|||||||
import { IDENTITY, INIT_DIFF_PROMPT } from '../../prompts';
|
import { IDENTITY, INIT_DIFF_PROMPT } from '../../prompts';
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
|
const translation = i18n[(config.OCO_LANGUAGE as I18nLocals) || 'en'];
|
||||||
|
|
||||||
type DeepPartial<T> = {
|
type DeepPartial<T> = {
|
||||||
[P in keyof T]?: {
|
[P in keyof T]?: {
|
||||||
@@ -214,11 +211,10 @@ const STRUCTURE_OF_COMMIT = `
|
|||||||
// Prompt to generate LLM-readable rules based on @commitlint rules.
|
// Prompt to generate LLM-readable rules based on @commitlint rules.
|
||||||
const GEN_COMMITLINT_CONSISTENCY_PROMPT = (
|
const GEN_COMMITLINT_CONSISTENCY_PROMPT = (
|
||||||
prompts: string[]
|
prompts: string[]
|
||||||
): ChatCompletionRequestMessage[] => [
|
): OpenAI.Chat.Completions.ChatCompletionMessageParam[] => [
|
||||||
{
|
{
|
||||||
role: ChatCompletionRequestMessageRoleEnum.Assistant,
|
role: 'system',
|
||||||
// prettier-ignore
|
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages for two different changes in a single codebase and output them in the provided JSON format: one for a bug fix and another for a new feature.
|
||||||
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages for two different changes in a single codebase and output them in the provided JSON format: one for a bug fix and another for a new feature.
|
|
||||||
|
|
||||||
Here are the specific requirements and conventions that should be strictly followed:
|
Here are the specific requirements and conventions that should be strictly followed:
|
||||||
|
|
||||||
@@ -260,22 +256,33 @@ Example Git Diff is to follow:`
|
|||||||
const INIT_MAIN_PROMPT = (
|
const INIT_MAIN_PROMPT = (
|
||||||
language: string,
|
language: string,
|
||||||
prompts: string[]
|
prompts: string[]
|
||||||
): ChatCompletionRequestMessage => ({
|
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
|
||||||
role: ChatCompletionRequestMessageRoleEnum.System,
|
role: 'system',
|
||||||
// prettier-ignore
|
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes ${
|
||||||
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes and WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
|
config.OCO_WHY ? 'and WHY the changes were done' : ''
|
||||||
${config?.OCO_EMOJI ? 'Use GitMoji convention to preface the commit.' : 'Do not preface the commit with anything.'}
|
}. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
|
||||||
${config?.OCO_DESCRIPTION ? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.' : "Don't add any descriptions to the commit, only commit message."}
|
${
|
||||||
|
config.OCO_EMOJI
|
||||||
|
? 'Use GitMoji convention to preface the commit.'
|
||||||
|
: 'Do not preface the commit with anything.'
|
||||||
|
}
|
||||||
|
${
|
||||||
|
config.OCO_DESCRIPTION
|
||||||
|
? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.'
|
||||||
|
: "Don't add any descriptions to the commit, only commit message."
|
||||||
|
}
|
||||||
Use the present tense. Use ${language} to answer.
|
Use the present tense. Use ${language} to answer.
|
||||||
${ config?.OCO_ONE_LINE_COMMIT ? 'Craft a concise commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in a one single message, without diverging into a list of commit per file change.' : ""}
|
${
|
||||||
|
config.OCO_ONE_LINE_COMMIT
|
||||||
|
? 'Craft a concise commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in a one single message, without diverging into a list of commit per file change.'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
|
||||||
You will strictly follow the following conventions to generate the content of the commit message:
|
You will strictly follow the following conventions to generate the content of the commit message:
|
||||||
- ${prompts.join('\n- ')}
|
- ${prompts.join('\n- ')}
|
||||||
|
|
||||||
The conventions refers to the following structure of commit message:
|
The conventions refers to the following structure of commit message:
|
||||||
${STRUCTURE_OF_COMMIT}
|
${STRUCTURE_OF_COMMIT}`
|
||||||
|
|
||||||
`
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const commitlintPrompts = {
|
export const commitlintPrompts = {
|
||||||
|
|||||||
@@ -1,11 +1,41 @@
|
|||||||
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const nodeModulesPath = path.join(
|
const findModulePath = (moduleName: string) => {
|
||||||
process.env.PWD || process.cwd(),
|
const searchPaths = [
|
||||||
'node_modules',
|
path.join('node_modules', moduleName),
|
||||||
'@commitlint',
|
path.join('node_modules', '.pnpm')
|
||||||
'load'
|
];
|
||||||
);
|
|
||||||
|
for (const basePath of searchPaths) {
|
||||||
|
try {
|
||||||
|
const resolvedPath = require.resolve(moduleName, { paths: [basePath] });
|
||||||
|
return resolvedPath;
|
||||||
|
} catch {
|
||||||
|
// Continue to the next search path if the module is not found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Cannot find module ${moduleName}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCommitLintModuleType = async (): Promise<'cjs' | 'esm'> => {
|
||||||
|
const packageFile = '@commitlint/load/package.json';
|
||||||
|
const packageJsonPath = findModulePath(packageFile);
|
||||||
|
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
||||||
|
|
||||||
|
if (!packageJson) {
|
||||||
|
throw new Error(`Failed to parse ${packageFile}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return packageJson.type === 'module' ? 'esm' : 'cjs';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* QualifiedConfig from any version of @commitlint/types
|
||||||
|
* @see https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/types/src/load.ts
|
||||||
|
*/
|
||||||
|
type QualifiedConfigOnAnyVersion = { [key: string]: unknown };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This code is loading the configuration for the `@commitlint` package from the current working
|
* This code is loading the configuration for the `@commitlint` package from the current working
|
||||||
@@ -13,13 +43,31 @@ const nodeModulesPath = path.join(
|
|||||||
*
|
*
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getCommitLintPWDConfig = async () => {
|
export const getCommitLintPWDConfig =
|
||||||
const load = require(nodeModulesPath).default;
|
async (): Promise<QualifiedConfigOnAnyVersion | null> => {
|
||||||
|
let load: Function, modulePath: string;
|
||||||
|
switch (await getCommitLintModuleType()) {
|
||||||
|
case 'cjs':
|
||||||
|
/**
|
||||||
|
* CommonJS (<= commitlint@v18.x.x.)
|
||||||
|
*/
|
||||||
|
modulePath = findModulePath('@commitlint/load');
|
||||||
|
load = require(modulePath).default;
|
||||||
|
break;
|
||||||
|
case 'esm':
|
||||||
|
/**
|
||||||
|
* ES Module (commitlint@v19.x.x. <= )
|
||||||
|
* Directory import is not supported in ES Module resolution, so import the file directly
|
||||||
|
*/
|
||||||
|
modulePath = await findModulePath('@commitlint/load/lib/load.js');
|
||||||
|
load = (await import(modulePath)).default;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (load && typeof load === 'function') {
|
if (load && typeof load === 'function') {
|
||||||
return await load();
|
return await load();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @commitlint/load is not a function
|
// @commitlint/load is not a function
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export const getJSONBlock = (input: string): string => {
|
|||||||
const jsonIndex = input.search('```json');
|
const jsonIndex = input.search('```json');
|
||||||
if (jsonIndex > -1) {
|
if (jsonIndex > -1) {
|
||||||
input = input.slice(jsonIndex + 8);
|
input = input.slice(jsonIndex + 8);
|
||||||
const endJsonIndex = consistency.search('```');
|
const endJsonIndex = input.search('```');
|
||||||
input = input.slice(0, endJsonIndex);
|
input = input.slice(0, endJsonIndex);
|
||||||
}
|
}
|
||||||
return input;
|
return input;
|
||||||
};
|
};
|
||||||
|
|||||||
267
src/prompts.ts
267
src/prompts.ts
@@ -1,10 +1,5 @@
|
|||||||
import {
|
|
||||||
ChatCompletionRequestMessage,
|
|
||||||
ChatCompletionRequestMessageRoleEnum
|
|
||||||
} from 'openai';
|
|
||||||
|
|
||||||
import { note } from '@clack/prompts';
|
import { note } from '@clack/prompts';
|
||||||
|
import { OpenAI } from 'openai';
|
||||||
import { getConfig } from './commands/config';
|
import { getConfig } from './commands/config';
|
||||||
import { i18n, I18nLocals } from './i18n';
|
import { i18n, I18nLocals } from './i18n';
|
||||||
import { configureCommitlintIntegration } from './modules/commitlint/config';
|
import { configureCommitlintIntegration } from './modules/commitlint/config';
|
||||||
@@ -14,118 +9,133 @@ import * as utils from './modules/commitlint/utils';
|
|||||||
import { removeConventionalCommitWord } from './utils/removeConventionalCommitWord';
|
import { removeConventionalCommitWord } from './utils/removeConventionalCommitWord';
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
|
const translation = i18n[(config.OCO_LANGUAGE as I18nLocals) || 'en'];
|
||||||
|
|
||||||
export const IDENTITY =
|
export const IDENTITY =
|
||||||
'You are to act as the author of a commit message in git.';
|
'You are to act as an author of a commit message in git.';
|
||||||
|
|
||||||
|
const GITMOJI_HELP = `Use GitMoji convention to preface the commit. Here are some help to choose the right emoji (emoji, description):
|
||||||
|
🐛, Fix a bug;
|
||||||
|
✨, Introduce new features;
|
||||||
|
📝, Add or update documentation;
|
||||||
|
🚀, Deploy stuff;
|
||||||
|
✅, Add, update, or pass tests;
|
||||||
|
♻️, Refactor code;
|
||||||
|
⬆️, Upgrade dependencies;
|
||||||
|
🔧, Add or update configuration files;
|
||||||
|
🌐, Internationalization and localization;
|
||||||
|
💡, Add or update comments in source code;`;
|
||||||
|
|
||||||
|
const FULL_GITMOJI_SPEC = `${GITMOJI_HELP}
|
||||||
|
🎨, Improve structure / format of the code;
|
||||||
|
⚡️, Improve performance;
|
||||||
|
🔥, Remove code or files;
|
||||||
|
🚑️, Critical hotfix;
|
||||||
|
💄, Add or update the UI and style files;
|
||||||
|
🎉, Begin a project;
|
||||||
|
🔒️, Fix security issues;
|
||||||
|
🔐, Add or update secrets;
|
||||||
|
🔖, Release / Version tags;
|
||||||
|
🚨, Fix compiler / linter warnings;
|
||||||
|
🚧, Work in progress;
|
||||||
|
💚, Fix CI Build;
|
||||||
|
⬇️, Downgrade dependencies;
|
||||||
|
📌, Pin dependencies to specific versions;
|
||||||
|
👷, Add or update CI build system;
|
||||||
|
📈, Add or update analytics or track code;
|
||||||
|
➕, Add a dependency;
|
||||||
|
➖, Remove a dependency;
|
||||||
|
🔨, Add or update development scripts;
|
||||||
|
✏️, Fix typos;
|
||||||
|
💩, Write bad code that needs to be improved;
|
||||||
|
⏪️, Revert changes;
|
||||||
|
🔀, Merge branches;
|
||||||
|
📦️, Add or update compiled files or packages;
|
||||||
|
👽️, Update code due to external API changes;
|
||||||
|
🚚, Move or rename resources (e.g.: files, paths, routes);
|
||||||
|
📄, Add or update license;
|
||||||
|
💥, Introduce breaking changes;
|
||||||
|
🍱, Add or update assets;
|
||||||
|
♿️, Improve accessibility;
|
||||||
|
🍻, Write code drunkenly;
|
||||||
|
💬, Add or update text and literals;
|
||||||
|
🗃️, Perform database related changes;
|
||||||
|
🔊, Add or update logs;
|
||||||
|
🔇, Remove logs;
|
||||||
|
👥, Add or update contributor(s);
|
||||||
|
🚸, Improve user experience / usability;
|
||||||
|
🏗️, Make architectural changes;
|
||||||
|
📱, Work on responsive design;
|
||||||
|
🤡, Mock things;
|
||||||
|
🥚, Add or update an easter egg;
|
||||||
|
🙈, Add or update a .gitignore file;
|
||||||
|
📸, Add or update snapshots;
|
||||||
|
⚗️, Perform experiments;
|
||||||
|
🔍️, Improve SEO;
|
||||||
|
🏷️, Add or update types;
|
||||||
|
🌱, Add or update seed files;
|
||||||
|
🚩, Add, update, or remove feature flags;
|
||||||
|
🥅, Catch errors;
|
||||||
|
💫, Add or update animations and transitions;
|
||||||
|
🗑️, Deprecate code that needs to be cleaned up;
|
||||||
|
🛂, Work on code related to authorization, roles and permissions;
|
||||||
|
🩹, Simple fix for a non-critical issue;
|
||||||
|
🧐, Data exploration/inspection;
|
||||||
|
⚰️, Remove dead code;
|
||||||
|
🧪, Add a failing test;
|
||||||
|
👔, Add or update business logic;
|
||||||
|
🩺, Add or update healthcheck;
|
||||||
|
🧱, Infrastructure related changes;
|
||||||
|
🧑💻, Improve developer experience;
|
||||||
|
💸, Add sponsorships or money related infrastructure;
|
||||||
|
🧵, Add or update code related to multithreading or concurrency;
|
||||||
|
🦺, Add or update code related to validation.`;
|
||||||
|
|
||||||
|
const CONVENTIONAL_COMMIT_KEYWORDS =
|
||||||
|
'Do not preface the commit with anything, except for the conventional commit keywords: fix, feat, build, chore, ci, docs, style, refactor, perf, test.';
|
||||||
|
|
||||||
|
const getCommitConvention = (fullGitMojiSpec: boolean) =>
|
||||||
|
config.OCO_EMOJI
|
||||||
|
? fullGitMojiSpec
|
||||||
|
? FULL_GITMOJI_SPEC
|
||||||
|
: GITMOJI_HELP
|
||||||
|
: CONVENTIONAL_COMMIT_KEYWORDS;
|
||||||
|
|
||||||
|
const getDescriptionInstruction = () =>
|
||||||
|
config.OCO_DESCRIPTION
|
||||||
|
? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.'
|
||||||
|
: "Don't add any descriptions to the commit, only commit message.";
|
||||||
|
|
||||||
|
const getOneLineCommitInstruction = () =>
|
||||||
|
config.OCO_ONE_LINE_COMMIT
|
||||||
|
? 'Craft a concise commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in a one single message, without diverging into a list of commit per file change.'
|
||||||
|
: '';
|
||||||
|
|
||||||
const INIT_MAIN_PROMPT = (
|
const INIT_MAIN_PROMPT = (
|
||||||
language: string,
|
language: string,
|
||||||
fullGitMojiSpec: boolean
|
fullGitMojiSpec: boolean
|
||||||
): ChatCompletionRequestMessage => ({
|
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
|
||||||
role: ChatCompletionRequestMessageRoleEnum.System,
|
role: 'system',
|
||||||
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages as per the ${
|
content: (() => {
|
||||||
fullGitMojiSpec ? 'GitMoji specification' : 'conventional commit convention'
|
const commitConvention = fullGitMojiSpec
|
||||||
} and explain WHAT were the changes and mainly WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you are to convert it into a commit message.
|
? 'GitMoji specification'
|
||||||
${
|
: 'Conventional Commit Convention';
|
||||||
config?.OCO_EMOJI
|
const missionStatement = `${IDENTITY} Your mission is to create clean and comprehensive commit messages as per the ${commitConvention} and explain WHAT were the changes and mainly WHY the changes were done.`;
|
||||||
? 'Use GitMoji convention to preface the commit. Here are some help to choose the right emoji (emoji, description): ' +
|
const diffInstruction =
|
||||||
'🐛, Fix a bug; ' +
|
"I'll send you an output of 'git diff --staged' command, and you are to convert it into a commit message.";
|
||||||
'✨, Introduce new features; ' +
|
const conventionGuidelines = getCommitConvention(fullGitMojiSpec);
|
||||||
'📝, Add or update documentation; ' +
|
const descriptionGuideline = getDescriptionInstruction();
|
||||||
'🚀, Deploy stuff; ' +
|
const oneLineCommitGuideline = getOneLineCommitInstruction();
|
||||||
'✅, Add, update, or pass tests; ' +
|
const generalGuidelines = `Use the present tense. Lines must not be longer than 74 characters. Use ${language} for the commit message.`;
|
||||||
'♻️, Refactor code; ' +
|
|
||||||
'⬆️, Upgrade dependencies; ' +
|
return `${missionStatement}\n${diffInstruction}\n${conventionGuidelines}\n${descriptionGuideline}\n${oneLineCommitGuideline}\n${generalGuidelines}`;
|
||||||
'🔧, Add or update configuration files; ' +
|
})()
|
||||||
'🌐, Internationalization and localization; ' +
|
|
||||||
'💡, Add or update comments in source code; ' +
|
|
||||||
`${
|
|
||||||
fullGitMojiSpec
|
|
||||||
? '🎨, Improve structure / format of the code; ' +
|
|
||||||
'⚡️, Improve performance; ' +
|
|
||||||
'🔥, Remove code or files; ' +
|
|
||||||
'🚑️, Critical hotfix; ' +
|
|
||||||
'💄, Add or update the UI and style files; ' +
|
|
||||||
'🎉, Begin a project; ' +
|
|
||||||
'🔒️, Fix security issues; ' +
|
|
||||||
'🔐, Add or update secrets; ' +
|
|
||||||
'🔖, Release / Version tags; ' +
|
|
||||||
'🚨, Fix compiler / linter warnings; ' +
|
|
||||||
'🚧, Work in progress; ' +
|
|
||||||
'💚, Fix CI Build; ' +
|
|
||||||
'⬇️, Downgrade dependencies; ' +
|
|
||||||
'📌, Pin dependencies to specific versions; ' +
|
|
||||||
'👷, Add or update CI build system; ' +
|
|
||||||
'📈, Add or update analytics or track code; ' +
|
|
||||||
'➕, Add a dependency; ' +
|
|
||||||
'➖, Remove a dependency; ' +
|
|
||||||
'🔨, Add or update development scripts; ' +
|
|
||||||
'✏️, Fix typos; ' +
|
|
||||||
'💩, Write bad code that needs to be improved; ' +
|
|
||||||
'⏪️, Revert changes; ' +
|
|
||||||
'🔀, Merge branches; ' +
|
|
||||||
'📦️, Add or update compiled files or packages; ' +
|
|
||||||
'👽️, Update code due to external API changes; ' +
|
|
||||||
'🚚, Move or rename resources (e.g.: files, paths, routes); ' +
|
|
||||||
'📄, Add or update license; ' +
|
|
||||||
'💥, Introduce breaking changes; ' +
|
|
||||||
'🍱, Add or update assets; ' +
|
|
||||||
'♿️, Improve accessibility; ' +
|
|
||||||
'🍻, Write code drunkenly; ' +
|
|
||||||
'💬, Add or update text and literals; ' +
|
|
||||||
'🗃️, Perform database related changes; ' +
|
|
||||||
'🔊, Add or update logs; ' +
|
|
||||||
'🔇, Remove logs; ' +
|
|
||||||
'👥, Add or update contributor(s); ' +
|
|
||||||
'🚸, Improve user experience / usability; ' +
|
|
||||||
'🏗️, Make architectural changes; ' +
|
|
||||||
'📱, Work on responsive design; ' +
|
|
||||||
'🤡, Mock things; ' +
|
|
||||||
'🥚, Add or update an easter egg; ' +
|
|
||||||
'🙈, Add or update a .gitignore file; ' +
|
|
||||||
'📸, Add or update snapshots; ' +
|
|
||||||
'⚗️, Perform experiments; ' +
|
|
||||||
'🔍️, Improve SEO; ' +
|
|
||||||
'🏷️, Add or update types; ' +
|
|
||||||
'🌱, Add or update seed files; ' +
|
|
||||||
'🚩, Add, update, or remove feature flags; ' +
|
|
||||||
'🥅, Catch errors; ' +
|
|
||||||
'💫, Add or update animations and transitions; ' +
|
|
||||||
'🗑️, Deprecate code that needs to be cleaned up; ' +
|
|
||||||
'🛂, Work on code related to authorization, roles and permissions; ' +
|
|
||||||
'🩹, Simple fix for a non-critical issue; ' +
|
|
||||||
'🧐, Data exploration/inspection; ' +
|
|
||||||
'⚰️, Remove dead code; ' +
|
|
||||||
'🧪, Add a failing test; ' +
|
|
||||||
'👔, Add or update business logic; ' +
|
|
||||||
'🩺, Add or update healthcheck; ' +
|
|
||||||
'🧱, Infrastructure related changes; ' +
|
|
||||||
'🧑💻, Improve developer experience; ' +
|
|
||||||
'💸, Add sponsorships or money related infrastructure; ' +
|
|
||||||
'🧵, Add or update code related to multithreading or concurrency; ' +
|
|
||||||
'🦺, Add or update code related to validation.'
|
|
||||||
: ''
|
|
||||||
}`
|
|
||||||
: 'Do not preface the commit with anything. Conventional commit keywords:' +
|
|
||||||
'fix, feat, build, chore, ci, docs, style, refactor, perf, test.'
|
|
||||||
}
|
|
||||||
${
|
|
||||||
config?.OCO_DESCRIPTION
|
|
||||||
? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.'
|
|
||||||
: "Don't add any descriptions to the commit, only commit message."
|
|
||||||
}
|
|
||||||
${
|
|
||||||
config?.OCO_ONE_LINE_COMMIT
|
|
||||||
? 'Craft a concise commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in a one single message, without diverging into a list of commit per file change.'
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
Use the present tense. Lines must not be longer than 74 characters. Use ${language} for the commit message.`
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const INIT_DIFF_PROMPT: ChatCompletionRequestMessage = {
|
export const INIT_DIFF_PROMPT: OpenAI.Chat.Completions.ChatCompletionMessageParam =
|
||||||
role: ChatCompletionRequestMessageRoleEnum.User,
|
{
|
||||||
content: `diff --git a/src/server.ts b/src/server.ts
|
role: 'user',
|
||||||
|
content: `diff --git a/src/server.ts b/src/server.ts
|
||||||
index ad4db42..f3b18a9 100644
|
index ad4db42..f3b18a9 100644
|
||||||
--- a/src/server.ts
|
--- a/src/server.ts
|
||||||
+++ b/src/server.ts
|
+++ b/src/server.ts
|
||||||
@@ -149,29 +159,35 @@ export const INIT_DIFF_PROMPT: ChatCompletionRequestMessage = {
|
|||||||
+app.listen(process.env.PORT || PORT, () => {
|
+app.listen(process.env.PORT || PORT, () => {
|
||||||
+ console.log(\`Server listening on port \${PORT}\`);
|
+ console.log(\`Server listening on port \${PORT}\`);
|
||||||
});`
|
});`
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContent = (translation: ConsistencyPrompt) => {
|
||||||
|
const fix = config.OCO_EMOJI
|
||||||
|
? `🐛 ${removeConventionalCommitWord(translation.commitFix)}`
|
||||||
|
: translation.commitFix;
|
||||||
|
|
||||||
|
const feat = config.OCO_EMOJI
|
||||||
|
? `✨ ${removeConventionalCommitWord(translation.commitFeat)}`
|
||||||
|
: translation.commitFeat;
|
||||||
|
|
||||||
|
const description = config.OCO_DESCRIPTION
|
||||||
|
? translation.commitDescription
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return `${fix}\n${feat}\n${description}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const INIT_CONSISTENCY_PROMPT = (
|
const INIT_CONSISTENCY_PROMPT = (
|
||||||
translation: ConsistencyPrompt
|
translation: ConsistencyPrompt
|
||||||
): ChatCompletionRequestMessage => ({
|
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
|
||||||
role: ChatCompletionRequestMessageRoleEnum.Assistant,
|
role: 'assistant',
|
||||||
content: `${
|
content: getContent(translation)
|
||||||
config?.OCO_EMOJI
|
|
||||||
? `🐛 ${removeConventionalCommitWord(translation.commitFix)}`
|
|
||||||
: translation.commitFix
|
|
||||||
}
|
|
||||||
${
|
|
||||||
config?.OCO_EMOJI
|
|
||||||
? `✨ ${removeConventionalCommitWord(translation.commitFeat)}`
|
|
||||||
: translation.commitFeat
|
|
||||||
}
|
|
||||||
${config?.OCO_DESCRIPTION ? translation.commitDescription : ''}`
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getMainCommitPrompt = async (
|
export const getMainCommitPrompt = async (
|
||||||
fullGitMojiSpec: boolean
|
fullGitMojiSpec: boolean
|
||||||
): Promise<ChatCompletionRequestMessage[]> => {
|
): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => {
|
||||||
switch (config?.OCO_PROMPT_MODULE) {
|
switch (config.OCO_PROMPT_MODULE) {
|
||||||
case '@commitlint':
|
case '@commitlint':
|
||||||
if (!(await utils.commitlintLLMConfigExists())) {
|
if (!(await utils.commitlintLLMConfigExists())) {
|
||||||
note(
|
note(
|
||||||
@@ -197,7 +213,6 @@ export const getMainCommitPrompt = async (
|
|||||||
];
|
];
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// conventional-commit
|
|
||||||
return [
|
return [
|
||||||
INIT_MAIN_PROMPT(translation.localLanguage, fullGitMojiSpec),
|
INIT_MAIN_PROMPT(translation.localLanguage, fullGitMojiSpec),
|
||||||
INIT_DIFF_PROMPT,
|
INIT_DIFF_PROMPT,
|
||||||
|
|||||||
@@ -1,26 +1,45 @@
|
|||||||
|
import { getConfig, OCO_AI_PROVIDER_ENUM } from '../commands/config';
|
||||||
|
import { AnthropicEngine } from '../engine/anthropic';
|
||||||
|
import { AzureEngine } from '../engine/azure';
|
||||||
import { AiEngine } from '../engine/Engine';
|
import { AiEngine } from '../engine/Engine';
|
||||||
import { api } from '../engine/openAi';
|
import { FlowiseEngine } from '../engine/flowise';
|
||||||
import { getConfig } from '../commands/config';
|
import { GeminiEngine } from '../engine/gemini';
|
||||||
import { ollamaAi } from '../engine/ollama';
|
import { OllamaEngine } from '../engine/ollama';
|
||||||
import { azure } from '../engine/azure';
|
import { OpenAiEngine } from '../engine/openAi';
|
||||||
import { anthropicAi } from '../engine/anthropic'
|
import { TestAi, TestMockType } from '../engine/testAi';
|
||||||
import { testAi } from '../engine/testAi';
|
|
||||||
|
|
||||||
export function getEngine(): AiEngine {
|
export function getEngine(): AiEngine {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
const provider = config?.OCO_AI_PROVIDER;
|
const provider = config.OCO_AI_PROVIDER;
|
||||||
if (provider?.startsWith('ollama')) {
|
|
||||||
const model = provider.split('/')[1];
|
const DEFAULT_CONFIG = {
|
||||||
if (model) ollamaAi.setModel(model);
|
model: config.OCO_MODEL!,
|
||||||
|
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
|
||||||
return ollamaAi;
|
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
|
||||||
} else if (config?.OCO_AI_PROVIDER == 'anthropic') {
|
baseURL: config.OCO_API_URL!,
|
||||||
return anthropicAi;
|
apiKey: config.OCO_API_KEY!
|
||||||
} else if (config?.OCO_AI_PROVIDER == 'test') {
|
};
|
||||||
return testAi;
|
|
||||||
} else if (config?.OCO_AI_PROVIDER == 'azure') {
|
switch (provider) {
|
||||||
return azure;
|
case OCO_AI_PROVIDER_ENUM.OLLAMA:
|
||||||
|
return new OllamaEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.ANTHROPIC:
|
||||||
|
return new AnthropicEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.TEST:
|
||||||
|
return new TestAi(config.OCO_TEST_MOCK_TYPE as TestMockType);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.GEMINI:
|
||||||
|
return new GeminiEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.AZURE:
|
||||||
|
return new AzureEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.FLOWISE:
|
||||||
|
return new FlowiseEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return new OpenAiEngine(DEFAULT_CONFIG);
|
||||||
}
|
}
|
||||||
// open ai gpt by default
|
|
||||||
return api;
|
|
||||||
}
|
}
|
||||||
|
|||||||
205
test/e2e/gitPush.test.ts
Normal file
205
test/e2e/gitPush.test.ts
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import 'cli-testing-library/extend-expect';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { prepareTempDir } from './utils';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { render } from 'cli-testing-library';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { rm } from 'fs';
|
||||||
|
const fsExec = promisify(exec);
|
||||||
|
const fsRemove = promisify(rm);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* git remote -v
|
||||||
|
*
|
||||||
|
* [no remotes]
|
||||||
|
*/
|
||||||
|
const prepareNoRemoteGitRepository = async (): Promise<{
|
||||||
|
gitDir: string;
|
||||||
|
cleanup: () => Promise<void>;
|
||||||
|
}> => {
|
||||||
|
const tempDir = await prepareTempDir();
|
||||||
|
await fsExec('git init test', { cwd: tempDir });
|
||||||
|
const gitDir = path.resolve(tempDir, 'test');
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
return fsRemove(tempDir, { recursive: true });
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
gitDir,
|
||||||
|
cleanup
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* git remote -v
|
||||||
|
*
|
||||||
|
* origin /tmp/remote.git (fetch)
|
||||||
|
* origin /tmp/remote.git (push)
|
||||||
|
*/
|
||||||
|
const prepareOneRemoteGitRepository = async (): Promise<{
|
||||||
|
gitDir: string;
|
||||||
|
cleanup: () => Promise<void>;
|
||||||
|
}> => {
|
||||||
|
const tempDir = await prepareTempDir();
|
||||||
|
await fsExec('git init --bare remote.git', { cwd: tempDir });
|
||||||
|
await fsExec('git clone remote.git test', { cwd: tempDir });
|
||||||
|
const gitDir = path.resolve(tempDir, 'test');
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
return fsRemove(tempDir, { recursive: true });
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
gitDir,
|
||||||
|
cleanup
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* git remote -v
|
||||||
|
*
|
||||||
|
* origin /tmp/remote.git (fetch)
|
||||||
|
* origin /tmp/remote.git (push)
|
||||||
|
* other ../remote2.git (fetch)
|
||||||
|
* other ../remote2.git (push)
|
||||||
|
*/
|
||||||
|
const prepareTwoRemotesGitRepository = async (): Promise<{
|
||||||
|
gitDir: string;
|
||||||
|
cleanup: () => Promise<void>;
|
||||||
|
}> => {
|
||||||
|
const tempDir = await prepareTempDir();
|
||||||
|
await fsExec('git init --bare remote.git', { cwd: tempDir });
|
||||||
|
await fsExec('git init --bare other.git', { cwd: tempDir });
|
||||||
|
await fsExec('git clone remote.git test', { cwd: tempDir });
|
||||||
|
const gitDir = path.resolve(tempDir, 'test');
|
||||||
|
await fsExec('git remote add other ../other.git', { cwd: gitDir });
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
return fsRemove(tempDir, { recursive: true });
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
gitDir,
|
||||||
|
cleanup
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('cli flow to push git branch', () => {
|
||||||
|
it('do nothing when OCO_GITPUSH is set to false', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareNoRemoteGitRepository();
|
||||||
|
|
||||||
|
await render('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
|
||||||
|
const { queryByText, findByText, userEvent } = await render(
|
||||||
|
`OCO_AI_PROVIDER='test' OCO_GITPUSH='false' node`,
|
||||||
|
[resolve('./out/cli.cjs')],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await queryByText('Choose a remote to push to')
|
||||||
|
).not.toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await queryByText('Do you want to run `git push`?')
|
||||||
|
).not.toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await queryByText('Successfully pushed all commits to origin')
|
||||||
|
).not.toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await queryByText('Command failed with exit code 1')
|
||||||
|
).not.toBeInTheConsole();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('push and cause error when there is no remote', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareNoRemoteGitRepository();
|
||||||
|
|
||||||
|
await render('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
|
||||||
|
const { queryByText, findByText, userEvent } = await render(
|
||||||
|
`OCO_AI_PROVIDER='test' node`,
|
||||||
|
[resolve('./out/cli.cjs')],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await queryByText('Choose a remote to push to')
|
||||||
|
).not.toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await queryByText('Do you want to run `git push`?')
|
||||||
|
).not.toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await queryByText('Successfully pushed all commits to origin')
|
||||||
|
).not.toBeInTheConsole();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await findByText('Command failed with exit code 1')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('push when one remote is set', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareOneRemoteGitRepository();
|
||||||
|
|
||||||
|
await render('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
|
||||||
|
const { findByText, userEvent } = await render(
|
||||||
|
`OCO_AI_PROVIDER='test' node`,
|
||||||
|
[resolve('./out/cli.cjs')],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await findByText('Do you want to run `git push`?')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await findByText('Successfully pushed all commits to origin')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('push when two remotes are set', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareTwoRemotesGitRepository();
|
||||||
|
|
||||||
|
await render('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
|
||||||
|
const { findByText, userEvent } = await render(
|
||||||
|
`OCO_AI_PROVIDER='test' node`,
|
||||||
|
[resolve('./out/cli.cjs')],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(await findByText('Choose a remote to push to')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await findByText('Successfully pushed all commits to origin')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,7 +5,6 @@ import { prepareEnvironment } from './utils';
|
|||||||
|
|
||||||
it('cli flow when there are no changes', async () => {
|
it('cli flow when there are no changes', async () => {
|
||||||
const { gitDir, cleanup } = await prepareEnvironment();
|
const { gitDir, cleanup } = await prepareEnvironment();
|
||||||
|
|
||||||
const { findByText } = await render(`OCO_AI_PROVIDER='test' node`, [resolve('./out/cli.cjs')], { cwd: gitDir });
|
const { findByText } = await render(`OCO_AI_PROVIDER='test' node`, [resolve('./out/cli.cjs')], { cwd: gitDir });
|
||||||
expect(await findByText('No changes detected')).toBeInTheConsole();
|
expect(await findByText('No changes detected')).toBeInTheConsole();
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ it('cli flow to generate commit message for 1 new file (staged)', async () => {
|
|||||||
await render('git' ,['add index.ts'], { cwd: gitDir });
|
await render('git' ,['add index.ts'], { cwd: gitDir });
|
||||||
|
|
||||||
const { queryByText, findByText, userEvent } = await render(`OCO_AI_PROVIDER='test' node`, [resolve('./out/cli.cjs')], { cwd: gitDir });
|
const { queryByText, findByText, userEvent } = await render(`OCO_AI_PROVIDER='test' node`, [resolve('./out/cli.cjs')], { cwd: gitDir });
|
||||||
|
|
||||||
expect(await queryByText('No files are staged')).not.toBeInTheConsole();
|
expect(await queryByText('No files are staged')).not.toBeInTheConsole();
|
||||||
expect(await queryByText('Do you want to stage all files and generate commit message?')).not.toBeInTheConsole();
|
expect(await queryByText('Do you want to stage all files and generate commit message?')).not.toBeInTheConsole();
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ it('cli flow to generate commit message for 1 new file (staged)', async () => {
|
|||||||
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
userEvent.keyboard('[Enter]');
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
expect(await findByText('Choose a remote to push to')).toBeInTheConsole();
|
expect(await findByText('Do you want to run `git push`?')).toBeInTheConsole();
|
||||||
userEvent.keyboard('[Enter]');
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
expect(await findByText('Successfully pushed all commits to origin')).toBeInTheConsole();
|
expect(await findByText('Successfully pushed all commits to origin')).toBeInTheConsole();
|
||||||
@@ -47,7 +46,7 @@ it('cli flow to generate commit message for 1 changed file (not staged)', async
|
|||||||
|
|
||||||
expect(await findByText('Successfully committed')).toBeInTheConsole();
|
expect(await findByText('Successfully committed')).toBeInTheConsole();
|
||||||
|
|
||||||
expect(await findByText('Choose a remote to push to')).toBeInTheConsole();
|
expect(await findByText('Do you want to run `git push`?')).toBeInTheConsole();
|
||||||
userEvent.keyboard('[Enter]');
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
expect(await findByText('Successfully pushed all commits to origin')).toBeInTheConsole();
|
expect(await findByText('Successfully pushed all commits to origin')).toBeInTheConsole();
|
||||||
|
|||||||
222
test/e2e/prompt-module/commitlint.test.ts
Normal file
222
test/e2e/prompt-module/commitlint.test.ts
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import { resolve } from 'path';
|
||||||
|
import { render } from 'cli-testing-library';
|
||||||
|
import 'cli-testing-library/extend-expect';
|
||||||
|
import { prepareEnvironment, wait } from '../utils';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
function getAbsolutePath(relativePath: string) {
|
||||||
|
const scriptDir = path.dirname(__filename);
|
||||||
|
return path.resolve(scriptDir, relativePath);
|
||||||
|
}
|
||||||
|
async function setupCommitlint(dir: string, ver: 9 | 18 | 19) {
|
||||||
|
let packagePath, packageJsonPath, configPath;
|
||||||
|
switch (ver) {
|
||||||
|
case 9:
|
||||||
|
packagePath = getAbsolutePath('./data/commitlint_9/node_modules');
|
||||||
|
packageJsonPath = getAbsolutePath('./data/commitlint_9/package.json');
|
||||||
|
configPath = getAbsolutePath('./data/commitlint_9/commitlint.config.js');
|
||||||
|
break;
|
||||||
|
case 18:
|
||||||
|
packagePath = getAbsolutePath('./data/commitlint_18/node_modules');
|
||||||
|
packageJsonPath = getAbsolutePath('./data/commitlint_18/package.json');
|
||||||
|
configPath = getAbsolutePath('./data/commitlint_18/commitlint.config.js');
|
||||||
|
break;
|
||||||
|
case 19:
|
||||||
|
packagePath = getAbsolutePath('./data/commitlint_19/node_modules');
|
||||||
|
packageJsonPath = getAbsolutePath('./data/commitlint_19/package.json');
|
||||||
|
configPath = getAbsolutePath('./data/commitlint_19/commitlint.config.js');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await render('cp', ['-r', packagePath, '.'], { cwd: dir });
|
||||||
|
await render('cp', [packageJsonPath, '.'], { cwd: dir });
|
||||||
|
await render('cp', [configPath, '.'], { cwd: dir });
|
||||||
|
await wait(3000); // Avoid flakiness by waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('cli flow to run "oco commitlint force"', () => {
|
||||||
|
it('on commitlint@9 using CJS', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareEnvironment();
|
||||||
|
|
||||||
|
await setupCommitlint(gitDir, 9);
|
||||||
|
const npmList = await render('npm', ['list', '@commitlint/load'], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
expect(await npmList.findByText('@commitlint/load@9')).toBeInTheConsole();
|
||||||
|
|
||||||
|
const { findByText } = await render(
|
||||||
|
`
|
||||||
|
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
||||||
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
|
OCO_AI_PROVIDER='test' \
|
||||||
|
node ${resolve('./out/cli.cjs')} commitlint force \
|
||||||
|
`,
|
||||||
|
[],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await findByText('opencommit — configure @commitlint')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await findByText('Read @commitlint configuration')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await findByText('Generating consistency with given @commitlint rules')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await findByText('Done - please review contents of')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
it('on commitlint@18 using CJS', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareEnvironment();
|
||||||
|
|
||||||
|
await setupCommitlint(gitDir, 18);
|
||||||
|
const npmList = await render('npm', ['list', '@commitlint/load'], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
expect(await npmList.findByText('@commitlint/load@18')).toBeInTheConsole();
|
||||||
|
|
||||||
|
const { findByText } = await render(
|
||||||
|
`
|
||||||
|
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
||||||
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
|
OCO_AI_PROVIDER='test' \
|
||||||
|
node ${resolve('./out/cli.cjs')} commitlint force \
|
||||||
|
`,
|
||||||
|
[],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await findByText('opencommit — configure @commitlint')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await findByText('Read @commitlint configuration')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await findByText('Generating consistency with given @commitlint rules')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await findByText('Done - please review contents of')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
it('on commitlint@19 using ESM', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareEnvironment();
|
||||||
|
|
||||||
|
await setupCommitlint(gitDir, 19);
|
||||||
|
const npmList = await render('npm', ['list', '@commitlint/load'], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
expect(await npmList.findByText('@commitlint/load@19')).toBeInTheConsole();
|
||||||
|
|
||||||
|
const { findByText } = await render(
|
||||||
|
`
|
||||||
|
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
||||||
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
|
OCO_AI_PROVIDER='test' \
|
||||||
|
node ${resolve('./out/cli.cjs')} commitlint force \
|
||||||
|
`,
|
||||||
|
[],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await findByText('opencommit — configure @commitlint')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await findByText('Read @commitlint configuration')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await findByText('Generating consistency with given @commitlint rules')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await findByText('Done - please review contents of')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cli flow to generate commit message using @commitlint prompt-module', () => {
|
||||||
|
it('on commitlint@19 using ESM', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareEnvironment();
|
||||||
|
|
||||||
|
// Setup commitlint@19
|
||||||
|
await setupCommitlint(gitDir, 19);
|
||||||
|
const npmList = await render('npm', ['list', '@commitlint/load'], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
expect(await npmList.findByText('@commitlint/load@19')).toBeInTheConsole();
|
||||||
|
|
||||||
|
// Run `oco commitlint force`
|
||||||
|
const commitlintForce = await render(
|
||||||
|
`
|
||||||
|
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
||||||
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
|
OCO_AI_PROVIDER='test' \
|
||||||
|
node ${resolve('./out/cli.cjs')} commitlint force \
|
||||||
|
`,
|
||||||
|
[],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
await commitlintForce.findByText('Done - please review contents of')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
|
||||||
|
// Run `oco commitlint get`
|
||||||
|
const commitlintGet = await render(
|
||||||
|
`
|
||||||
|
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
||||||
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
|
OCO_AI_PROVIDER='test' \
|
||||||
|
node ${resolve('./out/cli.cjs')} commitlint get \
|
||||||
|
`,
|
||||||
|
[],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
expect(await commitlintGet.findByText('consistency')).toBeInTheConsole();
|
||||||
|
|
||||||
|
// Run 'oco' using .opencommit-commitlint
|
||||||
|
await render('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
|
||||||
|
const oco = await render(
|
||||||
|
`
|
||||||
|
OCO_TEST_MOCK_TYPE='commit-message' \
|
||||||
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
|
OCO_AI_PROVIDER='test' \
|
||||||
|
node ${resolve('./out/cli.cjs')} \
|
||||||
|
`,
|
||||||
|
[],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await oco.findByText('Generating the commit message')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await oco.findByText('Confirm the commit message?')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
oco.userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await oco.findByText('Do you want to run `git push`?')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
oco.userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await oco.findByText('Successfully pushed all commits to origin')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['@commitlint/config-conventional']
|
||||||
|
};
|
||||||
2029
test/e2e/prompt-module/data/commitlint_18/package-lock.json
generated
Normal file
2029
test/e2e/prompt-module/data/commitlint_18/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
test/e2e/prompt-module/data/commitlint_18/package.json
Normal file
15
test/e2e/prompt-module/data/commitlint_18/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "commitlint-test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^18.0.0",
|
||||||
|
"@commitlint/config-conventional": "^18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
extends: ['@commitlint/config-conventional']
|
||||||
|
};
|
||||||
1453
test/e2e/prompt-module/data/commitlint_19/package-lock.json
generated
Normal file
1453
test/e2e/prompt-module/data/commitlint_19/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
test/e2e/prompt-module/data/commitlint_19/package.json
Normal file
16
test/e2e/prompt-module/data/commitlint_19/package.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "commitlint-test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^19.0.0",
|
||||||
|
"@commitlint/config-conventional": "^19.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['@commitlint/config-conventional']
|
||||||
|
};
|
||||||
1671
test/e2e/prompt-module/data/commitlint_9/package-lock.json
generated
Normal file
1671
test/e2e/prompt-module/data/commitlint_9/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
test/e2e/prompt-module/data/commitlint_9/package.json
Normal file
15
test/e2e/prompt-module/data/commitlint_9/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "commitlint-test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^9.0.0",
|
||||||
|
"@commitlint/config-conventional": "^9.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
test/e2e/setup.sh
Executable file
11
test/e2e/setup.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
current_dir=$(pwd)
|
||||||
|
setup_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
# Set up for prompt-module/commitlint
|
||||||
|
cd $setup_dir && cd prompt-module/data/commitlint_9 && npm ci
|
||||||
|
cd $setup_dir && cd prompt-module/data/commitlint_18 && npm ci
|
||||||
|
cd $setup_dir && cd prompt-module/data/commitlint_19 && npm ci
|
||||||
|
|
||||||
|
cd $current_dir
|
||||||
@@ -15,7 +15,7 @@ export const prepareEnvironment = async (): Promise<{
|
|||||||
gitDir: string;
|
gitDir: string;
|
||||||
cleanup: () => Promise<void>;
|
cleanup: () => Promise<void>;
|
||||||
}> => {
|
}> => {
|
||||||
const tempDir = await fsMakeTempDir(path.join(tmpdir(), 'opencommit-test-'));
|
const tempDir = await prepareTempDir();
|
||||||
// Create a remote git repository int the temp directory. This is necessary to execute the `git push` command
|
// Create a remote git repository int the temp directory. This is necessary to execute the `git push` command
|
||||||
await fsExec('git init --bare remote.git', { cwd: tempDir });
|
await fsExec('git init --bare remote.git', { cwd: tempDir });
|
||||||
await fsExec('git clone remote.git test', { cwd: tempDir });
|
await fsExec('git clone remote.git test', { cwd: tempDir });
|
||||||
@@ -29,3 +29,9 @@ export const prepareEnvironment = async (): Promise<{
|
|||||||
cleanup,
|
cleanup,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const prepareTempDir = async(): Promise<string> => {
|
||||||
|
return await fsMakeTempDir(path.join(tmpdir(), 'opencommit-test-'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import 'cli-testing-library/extend-expect'
|
import 'cli-testing-library/extend-expect'
|
||||||
import { configure } from 'cli-testing-library'
|
import { configure } from 'cli-testing-library'
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
global.jest = jest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adjusted the wait time for waitFor/findByText to 2000ms, because the default 1000ms makes the test results flaky
|
* Adjusted the wait time for waitFor/findByText to 2000ms, because the default 1000ms makes the test results flaky
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
import { getConfig } from '../../src/commands/config';
|
import { existsSync, readFileSync, rmSync } from 'fs';
|
||||||
|
import {
|
||||||
|
CONFIG_KEYS,
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
getConfig,
|
||||||
|
setConfig
|
||||||
|
} from '../../src/commands/config';
|
||||||
import { prepareFile } from './utils';
|
import { prepareFile } from './utils';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
describe('getConfig', () => {
|
describe('config', () => {
|
||||||
const originalEnv = { ...process.env };
|
const originalEnv = { ...process.env };
|
||||||
|
let globalConfigFile: { filePath: string; cleanup: () => Promise<void> };
|
||||||
|
let envConfigFile: { filePath: string; cleanup: () => Promise<void> };
|
||||||
|
|
||||||
function resetEnv(env: NodeJS.ProcessEnv) {
|
function resetEnv(env: NodeJS.ProcessEnv) {
|
||||||
Object.keys(process.env).forEach((key) => {
|
Object.keys(process.env).forEach((key) => {
|
||||||
if (!(key in env)) {
|
if (!(key in env)) {
|
||||||
@@ -13,93 +23,281 @@ describe('getConfig', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
resetEnv(originalEnv);
|
resetEnv(originalEnv);
|
||||||
|
if (globalConfigFile) await globalConfigFile.cleanup();
|
||||||
|
if (envConfigFile) await envConfigFile.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (globalConfigFile) await globalConfigFile.cleanup();
|
||||||
|
if (envConfigFile) await envConfigFile.cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
resetEnv(originalEnv);
|
resetEnv(originalEnv);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('return config values from the global config file', async () => {
|
const generateConfig = async (
|
||||||
const configFile = await prepareFile(
|
fileName: string,
|
||||||
'.opencommit',
|
content: Record<string, string>
|
||||||
`
|
) => {
|
||||||
OCO_OPENAI_API_KEY="sk-key"
|
const fileContent = Object.entries(content)
|
||||||
OCO_ANTHROPIC_API_KEY="secret-key"
|
.map(([key, value]) => `${key}="${value}"`)
|
||||||
OCO_TOKENS_MAX_INPUT="8192"
|
.join('\n');
|
||||||
OCO_TOKENS_MAX_OUTPUT="1000"
|
return await prepareFile(fileName, fileContent);
|
||||||
OCO_OPENAI_BASE_PATH="/openai/api"
|
};
|
||||||
OCO_DESCRIPTION="true"
|
|
||||||
OCO_EMOJI="true"
|
|
||||||
OCO_MODEL="gpt-4"
|
|
||||||
OCO_LANGUAGE="de"
|
|
||||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER="$m"
|
|
||||||
OCO_PROMPT_MODULE="@commitlint"
|
|
||||||
OCO_AI_PROVIDER="ollama"
|
|
||||||
OCO_GITPUSH="false"
|
|
||||||
OCO_ONE_LINE_COMMIT="true"
|
|
||||||
`
|
|
||||||
);
|
|
||||||
const config = getConfig({ configPath: configFile.filePath, envPath: '' });
|
|
||||||
|
|
||||||
expect(config).not.toEqual(null);
|
describe('getConfig', () => {
|
||||||
expect(config!['OCO_OPENAI_API_KEY']).toEqual('sk-key');
|
it('should prioritize local .env over global .opencommit config', async () => {
|
||||||
expect(config!['OCO_ANTHROPIC_API_KEY']).toEqual('secret-key');
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
expect(config!['OCO_TOKENS_MAX_INPUT']).toEqual(8192);
|
OCO_API_KEY: 'global-key',
|
||||||
expect(config!['OCO_TOKENS_MAX_OUTPUT']).toEqual(1000);
|
OCO_MODEL: 'gpt-3.5-turbo',
|
||||||
expect(config!['OCO_OPENAI_BASE_PATH']).toEqual('/openai/api');
|
OCO_LANGUAGE: 'en'
|
||||||
expect(config!['OCO_DESCRIPTION']).toEqual(true);
|
});
|
||||||
expect(config!['OCO_EMOJI']).toEqual(true);
|
|
||||||
expect(config!['OCO_MODEL']).toEqual('gpt-4');
|
|
||||||
expect(config!['OCO_LANGUAGE']).toEqual('de');
|
|
||||||
expect(config!['OCO_MESSAGE_TEMPLATE_PLACEHOLDER']).toEqual('$m');
|
|
||||||
expect(config!['OCO_PROMPT_MODULE']).toEqual('@commitlint');
|
|
||||||
expect(config!['OCO_AI_PROVIDER']).toEqual('ollama');
|
|
||||||
expect(config!['OCO_GITPUSH']).toEqual(false);
|
|
||||||
expect(config!['OCO_ONE_LINE_COMMIT']).toEqual(true);
|
|
||||||
|
|
||||||
await configFile.cleanup();
|
envConfigFile = await generateConfig('.env', {
|
||||||
|
OCO_API_KEY: 'local-key',
|
||||||
|
OCO_LANGUAGE: 'fr'
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = getConfig({
|
||||||
|
globalPath: globalConfigFile.filePath,
|
||||||
|
envPath: envConfigFile.filePath
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config).not.toEqual(null);
|
||||||
|
expect(config.OCO_API_KEY).toEqual('local-key');
|
||||||
|
expect(config.OCO_MODEL).toEqual('gpt-3.5-turbo');
|
||||||
|
expect(config.OCO_LANGUAGE).toEqual('fr');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fallback to global config when local config is not set', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
|
OCO_API_KEY: 'global-key',
|
||||||
|
OCO_MODEL: 'gpt-4',
|
||||||
|
OCO_LANGUAGE: 'de',
|
||||||
|
OCO_DESCRIPTION: 'true'
|
||||||
|
});
|
||||||
|
|
||||||
|
envConfigFile = await generateConfig('.env', {
|
||||||
|
OCO_API_URL: 'local-api-url'
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = getConfig({
|
||||||
|
globalPath: globalConfigFile.filePath,
|
||||||
|
envPath: envConfigFile.filePath
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config).not.toEqual(null);
|
||||||
|
expect(config.OCO_API_KEY).toEqual('global-key');
|
||||||
|
expect(config.OCO_API_URL).toEqual('local-api-url');
|
||||||
|
expect(config.OCO_MODEL).toEqual('gpt-4');
|
||||||
|
expect(config.OCO_LANGUAGE).toEqual('de');
|
||||||
|
expect(config.OCO_DESCRIPTION).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle boolean and numeric values correctly', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
|
OCO_TOKENS_MAX_INPUT: '4096',
|
||||||
|
OCO_TOKENS_MAX_OUTPUT: '500',
|
||||||
|
OCO_GITPUSH: 'true'
|
||||||
|
});
|
||||||
|
|
||||||
|
envConfigFile = await generateConfig('.env', {
|
||||||
|
OCO_TOKENS_MAX_INPUT: '8192',
|
||||||
|
OCO_ONE_LINE_COMMIT: 'false'
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = getConfig({
|
||||||
|
globalPath: globalConfigFile.filePath,
|
||||||
|
envPath: envConfigFile.filePath
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config).not.toEqual(null);
|
||||||
|
expect(config.OCO_TOKENS_MAX_INPUT).toEqual(8192);
|
||||||
|
expect(config.OCO_TOKENS_MAX_OUTPUT).toEqual(500);
|
||||||
|
expect(config.OCO_GITPUSH).toEqual(true);
|
||||||
|
expect(config.OCO_ONE_LINE_COMMIT).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty local config correctly', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
|
OCO_API_KEY: 'global-key',
|
||||||
|
OCO_MODEL: 'gpt-4',
|
||||||
|
OCO_LANGUAGE: 'es'
|
||||||
|
});
|
||||||
|
|
||||||
|
envConfigFile = await generateConfig('.env', {});
|
||||||
|
|
||||||
|
const config = getConfig({
|
||||||
|
globalPath: globalConfigFile.filePath,
|
||||||
|
envPath: envConfigFile.filePath
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config).not.toEqual(null);
|
||||||
|
expect(config.OCO_API_KEY).toEqual('global-key');
|
||||||
|
expect(config.OCO_MODEL).toEqual('gpt-4');
|
||||||
|
expect(config.OCO_LANGUAGE).toEqual('es');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should override global config with null values in local .env', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
|
OCO_API_KEY: 'global-key',
|
||||||
|
OCO_MODEL: 'gpt-4',
|
||||||
|
OCO_LANGUAGE: 'es'
|
||||||
|
});
|
||||||
|
|
||||||
|
envConfigFile = await generateConfig('.env', {
|
||||||
|
OCO_API_KEY: 'null'
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = getConfig({
|
||||||
|
globalPath: globalConfigFile.filePath,
|
||||||
|
envPath: envConfigFile.filePath
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config).not.toEqual(null);
|
||||||
|
expect(config.OCO_API_KEY).toEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty global config', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {});
|
||||||
|
envConfigFile = await generateConfig('.env', {});
|
||||||
|
|
||||||
|
const config = getConfig({
|
||||||
|
globalPath: globalConfigFile.filePath,
|
||||||
|
envPath: envConfigFile.filePath
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config).not.toEqual(null);
|
||||||
|
expect(config.OCO_API_KEY).toEqual(undefined);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('return config values from the local env file', async () => {
|
describe('setConfig', () => {
|
||||||
const envFile = await prepareFile(
|
beforeEach(async () => {
|
||||||
'.env',
|
// we create and delete the file to have the parent directory, but not the file, to test the creation of the file
|
||||||
`
|
globalConfigFile = await generateConfig('.opencommit', {});
|
||||||
OCO_OPENAI_API_KEY="sk-key"
|
rmSync(globalConfigFile.filePath);
|
||||||
OCO_ANTHROPIC_API_KEY="secret-key"
|
});
|
||||||
OCO_TOKENS_MAX_INPUT="8192"
|
|
||||||
OCO_TOKENS_MAX_OUTPUT="1000"
|
|
||||||
OCO_OPENAI_BASE_PATH="/openai/api"
|
|
||||||
OCO_DESCRIPTION="true"
|
|
||||||
OCO_EMOJI="true"
|
|
||||||
OCO_MODEL="gpt-4"
|
|
||||||
OCO_LANGUAGE="de"
|
|
||||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER="$m"
|
|
||||||
OCO_PROMPT_MODULE="@commitlint"
|
|
||||||
OCO_AI_PROVIDER="ollama"
|
|
||||||
OCO_GITPUSH="false"
|
|
||||||
OCO_ONE_LINE_COMMIT="true"
|
|
||||||
`
|
|
||||||
);
|
|
||||||
const config = getConfig({ configPath: '', envPath: envFile.filePath });
|
|
||||||
|
|
||||||
expect(config).not.toEqual(null);
|
it('should create .opencommit file with DEFAULT CONFIG if it does not exist on first setConfig run', async () => {
|
||||||
expect(config!['OCO_OPENAI_API_KEY']).toEqual('sk-key');
|
const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath);
|
||||||
expect(config!['OCO_ANTHROPIC_API_KEY']).toEqual('secret-key');
|
expect(isGlobalConfigFileExist).toBe(false);
|
||||||
expect(config!['OCO_TOKENS_MAX_INPUT']).toEqual(8192);
|
|
||||||
expect(config!['OCO_TOKENS_MAX_OUTPUT']).toEqual(1000);
|
|
||||||
expect(config!['OCO_OPENAI_BASE_PATH']).toEqual('/openai/api');
|
|
||||||
expect(config!['OCO_DESCRIPTION']).toEqual(true);
|
|
||||||
expect(config!['OCO_EMOJI']).toEqual(true);
|
|
||||||
expect(config!['OCO_MODEL']).toEqual('gpt-4');
|
|
||||||
expect(config!['OCO_LANGUAGE']).toEqual('de');
|
|
||||||
expect(config!['OCO_MESSAGE_TEMPLATE_PLACEHOLDER']).toEqual('$m');
|
|
||||||
expect(config!['OCO_PROMPT_MODULE']).toEqual('@commitlint');
|
|
||||||
expect(config!['OCO_AI_PROVIDER']).toEqual('ollama');
|
|
||||||
expect(config!['OCO_GITPUSH']).toEqual(false);
|
|
||||||
expect(config!['OCO_ONE_LINE_COMMIT']).toEqual(true);
|
|
||||||
|
|
||||||
await envFile.cleanup();
|
await setConfig(
|
||||||
|
[[CONFIG_KEYS.OCO_API_KEY, 'persisted-key_1']],
|
||||||
|
globalConfigFile.filePath
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileContent = readFileSync(globalConfigFile.filePath, 'utf8');
|
||||||
|
expect(fileContent).toContain('OCO_API_KEY=persisted-key_1');
|
||||||
|
Object.entries(DEFAULT_CONFIG).forEach(([key, value]) => {
|
||||||
|
expect(fileContent).toContain(`${key}=${value}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set new config values', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {});
|
||||||
|
await setConfig(
|
||||||
|
[
|
||||||
|
[CONFIG_KEYS.OCO_API_KEY, 'new-key'],
|
||||||
|
[CONFIG_KEYS.OCO_MODEL, 'gpt-4']
|
||||||
|
],
|
||||||
|
globalConfigFile.filePath
|
||||||
|
);
|
||||||
|
|
||||||
|
const config = getConfig({
|
||||||
|
globalPath: globalConfigFile.filePath
|
||||||
|
});
|
||||||
|
expect(config.OCO_API_KEY).toEqual('new-key');
|
||||||
|
expect(config.OCO_MODEL).toEqual('gpt-4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update existing config values', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
|
OCO_API_KEY: 'initial-key'
|
||||||
|
});
|
||||||
|
await setConfig(
|
||||||
|
[[CONFIG_KEYS.OCO_API_KEY, 'updated-key']],
|
||||||
|
globalConfigFile.filePath
|
||||||
|
);
|
||||||
|
|
||||||
|
const config = getConfig({
|
||||||
|
globalPath: globalConfigFile.filePath
|
||||||
|
});
|
||||||
|
expect(config.OCO_API_KEY).toEqual('updated-key');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle boolean and numeric values correctly', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {});
|
||||||
|
await setConfig(
|
||||||
|
[
|
||||||
|
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT, '8192'],
|
||||||
|
[CONFIG_KEYS.OCO_DESCRIPTION, 'true'],
|
||||||
|
[CONFIG_KEYS.OCO_ONE_LINE_COMMIT, 'false']
|
||||||
|
],
|
||||||
|
globalConfigFile.filePath
|
||||||
|
);
|
||||||
|
|
||||||
|
const config = getConfig({
|
||||||
|
globalPath: globalConfigFile.filePath
|
||||||
|
});
|
||||||
|
expect(config.OCO_TOKENS_MAX_INPUT).toEqual(8192);
|
||||||
|
expect(config.OCO_DESCRIPTION).toEqual(true);
|
||||||
|
expect(config.OCO_ONE_LINE_COMMIT).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error for unsupported config keys', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await setConfig(
|
||||||
|
[['UNSUPPORTED_KEY', 'value']],
|
||||||
|
globalConfigFile.filePath
|
||||||
|
);
|
||||||
|
throw new Error('NEVER_REACHED');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.message).toContain(
|
||||||
|
'Unsupported config key: UNSUPPORTED_KEY'
|
||||||
|
);
|
||||||
|
expect(error.message).not.toContain('NEVER_REACHED');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should persist changes to the config file', async () => {
|
||||||
|
const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath);
|
||||||
|
expect(isGlobalConfigFileExist).toBe(false);
|
||||||
|
|
||||||
|
await setConfig(
|
||||||
|
[[CONFIG_KEYS.OCO_API_KEY, 'persisted-key']],
|
||||||
|
globalConfigFile.filePath
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileContent = readFileSync(globalConfigFile.filePath, 'utf8');
|
||||||
|
expect(fileContent).toContain('OCO_API_KEY=persisted-key');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set multiple configs in a row and keep the changes', async () => {
|
||||||
|
const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath);
|
||||||
|
expect(isGlobalConfigFileExist).toBe(false);
|
||||||
|
|
||||||
|
await setConfig(
|
||||||
|
[[CONFIG_KEYS.OCO_API_KEY, 'persisted-key']],
|
||||||
|
globalConfigFile.filePath
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileContent1 = readFileSync(globalConfigFile.filePath, 'utf8');
|
||||||
|
expect(fileContent1).toContain('OCO_API_KEY=persisted-key');
|
||||||
|
|
||||||
|
await setConfig(
|
||||||
|
[[CONFIG_KEYS.OCO_MODEL, 'gpt-4']],
|
||||||
|
globalConfigFile.filePath
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileContent2 = readFileSync(globalConfigFile.filePath, 'utf8');
|
||||||
|
expect(fileContent2).toContain('OCO_MODEL=gpt-4');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
96
test/unit/gemini.test.ts
Normal file
96
test/unit/gemini.test.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { GeminiEngine } from '../../src/engine/gemini';
|
||||||
|
|
||||||
|
import { GenerativeModel, GoogleGenerativeAI } from '@google/generative-ai';
|
||||||
|
import {
|
||||||
|
ConfigType,
|
||||||
|
getConfig,
|
||||||
|
OCO_AI_PROVIDER_ENUM
|
||||||
|
} from '../../src/commands/config';
|
||||||
|
import { OpenAI } from 'openai';
|
||||||
|
|
||||||
|
describe('Gemini', () => {
|
||||||
|
let gemini: GeminiEngine;
|
||||||
|
let mockConfig: ConfigType;
|
||||||
|
let mockGoogleGenerativeAi: GoogleGenerativeAI;
|
||||||
|
let mockGenerativeModel: GenerativeModel;
|
||||||
|
let mockExit: jest.SpyInstance<never, [code?: number | undefined], any>;
|
||||||
|
|
||||||
|
const noop: (...args: any[]) => any = (...args: any[]) => {};
|
||||||
|
|
||||||
|
const mockGemini = () => {
|
||||||
|
mockConfig = getConfig() as ConfigType;
|
||||||
|
|
||||||
|
gemini = new GeminiEngine({
|
||||||
|
apiKey: mockConfig.OCO_API_KEY,
|
||||||
|
model: mockConfig.OCO_MODEL
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const oldEnv = process.env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
process.env = { ...oldEnv };
|
||||||
|
|
||||||
|
jest.mock('@google/generative-ai');
|
||||||
|
jest.mock('../src/commands/config');
|
||||||
|
|
||||||
|
jest.mock('@clack/prompts', () => ({
|
||||||
|
intro: jest.fn(),
|
||||||
|
outro: jest.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
mockExit = jest.spyOn(process, 'exit').mockImplementation();
|
||||||
|
|
||||||
|
mockConfig = getConfig() as ConfigType;
|
||||||
|
|
||||||
|
mockConfig.OCO_AI_PROVIDER = OCO_AI_PROVIDER_ENUM.GEMINI;
|
||||||
|
mockConfig.OCO_API_KEY = 'mock-api-key';
|
||||||
|
mockConfig.OCO_MODEL = 'gemini-1.5-flash';
|
||||||
|
|
||||||
|
mockGoogleGenerativeAi = new GoogleGenerativeAI(mockConfig.OCO_API_KEY);
|
||||||
|
mockGenerativeModel = mockGoogleGenerativeAi.getGenerativeModel({
|
||||||
|
model: mockConfig.OCO_MODEL
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
gemini = undefined as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
mockExit.mockRestore();
|
||||||
|
process.env = oldEnv;
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip('should exit process if OCO_GEMINI_API_KEY is not set and command is not config', () => {
|
||||||
|
process.env.OCO_GEMINI_API_KEY = undefined;
|
||||||
|
process.env.OCO_AI_PROVIDER = 'gemini';
|
||||||
|
|
||||||
|
mockGemini();
|
||||||
|
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate commit message', async () => {
|
||||||
|
const mockGenerateContent = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ response: { text: () => 'generated content' } });
|
||||||
|
mockGenerativeModel.generateContent = mockGenerateContent;
|
||||||
|
|
||||||
|
mockGemini();
|
||||||
|
|
||||||
|
const messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam> =
|
||||||
|
[
|
||||||
|
{ role: 'system', content: 'system message' },
|
||||||
|
{ role: 'assistant', content: 'assistant message' }
|
||||||
|
];
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(gemini, 'generateCommitMessage')
|
||||||
|
.mockImplementation(async () => 'generated content');
|
||||||
|
const result = await gemini.generateCommitMessage(messages);
|
||||||
|
|
||||||
|
expect(result).toEqual('generated content');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import path from 'path';
|
import { existsSync, mkdtemp, rm, writeFile } from 'fs';
|
||||||
import { mkdtemp, rm, writeFile } from 'fs';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import { promisify } from 'util';
|
||||||
const fsMakeTempDir = promisify(mkdtemp);
|
const fsMakeTempDir = promisify(mkdtemp);
|
||||||
const fsRemove = promisify(rm);
|
const fsRemove = promisify(rm);
|
||||||
const fsWriteFile = promisify(writeFile);
|
const fsWriteFile = promisify(writeFile);
|
||||||
@@ -20,8 +20,11 @@ export async function prepareFile(
|
|||||||
const filePath = path.resolve(tempDir, fileName);
|
const filePath = path.resolve(tempDir, fileName);
|
||||||
await fsWriteFile(filePath, content);
|
await fsWriteFile(filePath, content);
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
return fsRemove(tempDir, { recursive: true });
|
if (existsSync(tempDir)) {
|
||||||
|
await fsRemove(tempDir, { recursive: true });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filePath,
|
filePath,
|
||||||
cleanup
|
cleanup
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ES2020",
|
||||||
"lib": ["ES5", "ES6"],
|
"lib": ["ES6", "ES2020"],
|
||||||
|
|
||||||
|
"module": "CommonJS",
|
||||||
|
|
||||||
"module": "ESNext",
|
|
||||||
// "rootDir": "./src",
|
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "Node",
|
||||||
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
|
||||||
@@ -21,9 +21,7 @@
|
|||||||
|
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["test/jest-setup.ts"],
|
||||||
"test/jest-setup.ts"
|
|
||||||
],
|
|
||||||
"exclude": ["node_modules"],
|
"exclude": ["node_modules"],
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"esm": true,
|
"esm": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user