mirror of
https://github.com/di-sukharev/opencommit.git
synced 2026-01-15 00:28:30 -05:00
Compare commits
33 Commits
refactorin
...
fix_migrat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d990bf0bf5 | ||
|
|
119fedad53 | ||
|
|
3493cbb42c | ||
|
|
2c07d5be44 | ||
|
|
03b570c85c | ||
|
|
e3529e9ca7 | ||
|
|
2d7e3842d6 | ||
|
|
8ae927e2dc | ||
|
|
2859d4ebe3 | ||
|
|
306522e796 | ||
|
|
a91aa3b4de | ||
|
|
f46336b86a | ||
|
|
f975e49760 | ||
|
|
fa1cf46050 | ||
|
|
1d19ddd9e2 | ||
|
|
69b3c00a52 | ||
|
|
6f4afbfb52 | ||
|
|
796de7b07e | ||
|
|
9ad281a4ee | ||
|
|
1ce357b023 | ||
|
|
45dd07d229 | ||
|
|
fa164377e4 | ||
|
|
0b89767de0 | ||
|
|
2dded4caa4 | ||
|
|
670f74ebc7 | ||
|
|
89d2aa603b | ||
|
|
8702c17758 | ||
|
|
60597d23eb | ||
|
|
6f04927369 | ||
|
|
0c0cf9c627 | ||
|
|
8fe8e614ac | ||
|
|
1b29f3a9fd | ||
|
|
596dcd7cea |
72
README.md
72
README.md
@@ -28,30 +28,19 @@ 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...>
|
||||||
@@ -70,21 +59,17 @@ You can also run it with local model through ollama:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
git add <files...>
|
git add <files...>
|
||||||
oco config set OCO_AI_PROVIDER='ollama'
|
oco config set OCO_AI_PROVIDER='ollama' OCO_MODEL='llama3:8b'
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to use a model other than mistral (default), you can do so by setting the `OCO_AI_PROVIDER` environment variable as follows:
|
Default model is `mistral`.
|
||||||
|
|
||||||
```sh
|
|
||||||
oco config set OCO_AI_PROVIDER='ollama/llama3:8b'
|
|
||||||
```
|
|
||||||
|
|
||||||
If you have ollama that is set up in docker/ on another machine with GPUs (not locally), you can change the default endpoint url.
|
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_OLLAMA_API_URL` environment variable as follows:
|
You can do so by setting the `OCO_API_URL` environment variable as follows:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
oco config set OCO_OLLAMA_API_URL='http://192.168.1.10:11434/api/chat'
|
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.
|
where 192.168.1.10 is example of endpoint URL, where you have ollama set up.
|
||||||
@@ -121,22 +106,21 @@ 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-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 string basically, but it should be a valid model name>
|
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>
|
||||||
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama or ollama/model>
|
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This are not all the config options, but you get the point.
|
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
|
||||||
|
|
||||||
@@ -162,6 +146,16 @@ oco config set OCO_EMOJI=false
|
|||||||
|
|
||||||
Other config options are behaving the same.
|
Other config options are behaving the same.
|
||||||
|
|
||||||
|
### Output WHY the changes were done (WIP)
|
||||||
|
|
||||||
|
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
|
||||||
|
oco config set OCO_WHY=true
|
||||||
|
```
|
||||||
|
|
||||||
### Switch to GPT-4 or other models
|
### Switch to GPT-4 or other models
|
||||||
|
|
||||||
By default, OpenCommit uses `gpt-4o-mini` model.
|
By default, OpenCommit uses `gpt-4o-mini` model.
|
||||||
@@ -178,26 +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
|
||||||
```
|
```
|
||||||
|
|
||||||
### Switch to Azure OpenAI
|
### Switch to other LLM providers with a custom URL
|
||||||
|
|
||||||
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 'OCO_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
|
||||||
@@ -213,12 +207,14 @@ All available languages are currently listed in the [i18n](https://github.com/di
|
|||||||
|
|
||||||
### Push to git (gonna be deprecated)
|
### Push to git (gonna be deprecated)
|
||||||
|
|
||||||
A prompt to ushing 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.
|
||||||
@@ -393,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
|
||||||
|
|||||||
2846
out/cli.cjs
2846
out/cli.cjs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
63
package-lock.json
generated
63
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "opencommit",
|
"name": "opencommit",
|
||||||
"version": "3.0.20",
|
"version": "3.2.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "opencommit",
|
"name": "opencommit",
|
||||||
"version": "3.0.20",
|
"version": "3.2.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
@@ -28,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": "^4.56.0"
|
"openai": "^4.57.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"oco": "out/cli.cjs",
|
"oco": "out/cli.cjs",
|
||||||
@@ -2098,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",
|
||||||
@@ -7219,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",
|
||||||
@@ -7242,17 +7258,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/openai": {
|
"node_modules/openai": {
|
||||||
"version": "4.56.0",
|
"version": "4.57.0",
|
||||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.56.0.tgz",
|
"resolved": "https://registry.npmjs.org/openai/-/openai-4.57.0.tgz",
|
||||||
"integrity": "sha512-zcag97+3bG890MNNa0DQD9dGmmTWL8unJdNkulZzWRXrl+QeD+YkBI4H58rJcwErxqGK6a0jVPZ4ReJjhDGcmw==",
|
"integrity": "sha512-JnwBSIYqiZ3jYjB5f2in8hQ0PRA092c6m+/6dYB0MzK0BEbn+0dioxZsPLBm5idJbg9xzLNOiGVm2OSuhZ+BdQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
"@types/node-fetch": "^2.6.4",
|
"@types/node-fetch": "^2.6.4",
|
||||||
|
"@types/qs": "^6.9.7",
|
||||||
"abort-controller": "^3.0.0",
|
"abort-controller": "^3.0.0",
|
||||||
"agentkeepalive": "^4.2.1",
|
"agentkeepalive": "^4.2.1",
|
||||||
"form-data-encoder": "1.7.2",
|
"form-data-encoder": "1.7.2",
|
||||||
"formdata-node": "^4.3.2",
|
"formdata-node": "^4.3.2",
|
||||||
"node-fetch": "^2.6.7"
|
"node-fetch": "^2.6.7",
|
||||||
|
"qs": "^6.10.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"openai": "bin/cli"
|
"openai": "bin/cli"
|
||||||
@@ -7704,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",
|
||||||
@@ -8050,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",
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "opencommit",
|
"name": "opencommit",
|
||||||
"version": "3.0.20",
|
"version": "3.2.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",
|
||||||
@@ -46,8 +46,9 @@
|
|||||||
"dev:gemini": "OCO_AI_PROVIDER='gemini' 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 run build:push && git push --tags && npm publish --tag latest",
|
"deploy": "npm publish --tag latest",
|
||||||
"deploy:patch": "npm version patch && npm run deploy",
|
"deploy:build": "npm run build:push && git push --tags && npm run deploy",
|
||||||
|
"deploy:patch": "npm version patch && npm run deploy:build",
|
||||||
"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": "node --no-warnings --experimental-vm-modules $( [ -f ./node_modules/.bin/jest ] && echo ./node_modules/.bin/jest || which jest ) test/unit",
|
||||||
@@ -88,7 +89,6 @@
|
|||||||
"@google/generative-ai": "^0.11.4",
|
"@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",
|
||||||
@@ -97,6 +97,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": "^4.56.0"
|
"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()) {
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ const generateCommitMessageFromGitDiff = async ({
|
|||||||
skipCommitConfirmation = false
|
skipCommitConfirmation = false
|
||||||
}: GenerateCommitMessageFromGitDiffParams): Promise<void> => {
|
}: 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(
|
||||||
@@ -73,7 +73,7 @@ const generateCommitMessageFromGitDiff = async ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
commitSpinner.stop('📝 Commit message generated');
|
commitGenerationSpinner.stop('📝 Commit message generated');
|
||||||
|
|
||||||
outro(
|
outro(
|
||||||
`Generated commit message:
|
`Generated commit message:
|
||||||
@@ -88,32 +88,42 @@ ${chalk.grey('——————————————————')}`
|
|||||||
message: 'Confirm the commit message?'
|
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
|
||||||
|
if (config.OCO_GITPUSH === false) return;
|
||||||
|
|
||||||
if (!remotes.length) {
|
if (!remotes.length) {
|
||||||
const { stdout } = await execa('git', ['push']);
|
const { stdout } = await execa('git', ['push']);
|
||||||
if (stdout) outro(stdout);
|
if (stdout) outro(stdout);
|
||||||
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]}'`);
|
||||||
@@ -141,28 +151,30 @@ ${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)) {
|
|
||||||
|
if (isCancel(regenerateMessage)) process.exit(1);
|
||||||
|
|
||||||
|
if (regenerateMessage) {
|
||||||
await generateCommitMessageFromGitDiff({
|
await generateCommitMessageFromGitDiff({
|
||||||
diff,
|
diff,
|
||||||
extraArgs,
|
extraArgs,
|
||||||
@@ -171,7 +183,11 @@ ${chalk.grey('——————————————————')}`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
commitSpinner.stop('📝 Commit message generated');
|
commitGenerationSpinner.stop(
|
||||||
|
`${chalk.red('✖')} Failed to generate the commit message`
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
||||||
@@ -219,10 +235,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,29 +11,21 @@ import { TEST_MOCK_TYPES } from '../engine/testAi';
|
|||||||
import { getI18nLocal, i18n } from '../i18n';
|
import { getI18nLocal, i18n } 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_GEMINI_API_KEY = 'OCO_GEMINI_API_KEY',
|
|
||||||
OCO_GEMINI_BASE_PATH = 'OCO_GEMINI_BASE_PATH',
|
|
||||||
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', // todo: deprecate
|
|
||||||
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_TEST_MOCK_TYPE = 'OCO_TEST_MOCK_TYPE',
|
||||||
OCO_API_URL = 'OCO_API_URL',
|
OCO_API_URL = 'OCO_API_URL',
|
||||||
OCO_OLLAMA_API_URL = 'OCO_OLLAMA_API_URL',
|
OCO_GITPUSH = 'OCO_GITPUSH' // todo: deprecate
|
||||||
OCO_FLOWISE_ENDPOINT = 'OCO_FLOWISE_ENDPOINT',
|
|
||||||
OCO_FLOWISE_API_KEY = 'OCO_FLOWISE_API_KEY'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CONFIG_MODES {
|
export enum CONFIG_MODES {
|
||||||
@@ -84,6 +76,16 @@ export const MODEL_LIST = {
|
|||||||
'gemini-1.0-pro',
|
'gemini-1.0-pro',
|
||||||
'gemini-pro-vision',
|
'gemini-pro-vision',
|
||||||
'text-embedding-004'
|
'text-embedding-004'
|
||||||
|
],
|
||||||
|
|
||||||
|
groq: [
|
||||||
|
'llama3-70b-8192', // Meta Llama 3 70B (default one, no daily token limit and 14 400 reqs/day)
|
||||||
|
'llama3-8b-8192', // Meta Llama 3 8B
|
||||||
|
'llama-guard-3-8b', // Llama Guard 3 8B
|
||||||
|
'llama-3.1-8b-instant', // Llama 3.1 8B (Preview)
|
||||||
|
'llama-3.1-70b-versatile', // Llama 3.1 70B (Preview)
|
||||||
|
'gemma-7b-it', // Gemma 7B
|
||||||
|
'gemma2-9b-it' // Gemma 2 9B
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,6 +97,8 @@ const getDefaultModel = (provider: string | undefined): string => {
|
|||||||
return MODEL_LIST.anthropic[0];
|
return MODEL_LIST.anthropic[0];
|
||||||
case 'gemini':
|
case 'gemini':
|
||||||
return MODEL_LIST.gemini[0];
|
return MODEL_LIST.gemini[0];
|
||||||
|
case 'groq':
|
||||||
|
return MODEL_LIST.groq[0];
|
||||||
default:
|
default:
|
||||||
return MODEL_LIST.openai[0];
|
return MODEL_LIST.openai[0];
|
||||||
}
|
}
|
||||||
@@ -122,65 +126,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 = {}) {
|
||||||
if (config.OCO_AI_PROVIDER !== 'openai') return value;
|
if (config.OCO_AI_PROVIDER !== 'openai') return value;
|
||||||
|
|
||||||
validateConfig(
|
validateConfig(
|
||||||
'OCO_OPENAI_API_KEY',
|
'OCO_API_KEY',
|
||||||
typeof value === 'string' && value.length > 0,
|
typeof value === 'string' && value.length > 0,
|
||||||
'Empty value is not allowed'
|
'Empty value is not allowed'
|
||||||
);
|
);
|
||||||
|
|
||||||
validateConfig(
|
validateConfig(
|
||||||
'OCO_OPENAI_API_KEY',
|
'OCO_API_KEY',
|
||||||
value,
|
value,
|
||||||
'You need to provide the OCO_OPENAI_API_KEY when OCO_AI_PROVIDER is set to "openai" (default). Run `oco config set OCO_OPENAI_API_KEY=your_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_AZURE_API_KEY](value: any, config: any = {}) {
|
|
||||||
if (config.OCO_AI_PROVIDER !== 'azure') return value;
|
|
||||||
|
|
||||||
validateConfig(
|
|
||||||
'OCO_AZURE_API_KEY',
|
|
||||||
!!value,
|
|
||||||
'You need to provide the OCO_AZURE_API_KEY when OCO_AI_PROVIDER is set to "azure". Run: `oco config set OCO_AZURE_API_KEY=your_key`'
|
|
||||||
);
|
|
||||||
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
[CONFIG_KEYS.OCO_GEMINI_API_KEY](value: any, config: any = {}) {
|
|
||||||
if (config.OCO_AI_PROVIDER !== 'gemini') return value;
|
|
||||||
|
|
||||||
validateConfig(
|
|
||||||
'OCO_GEMINI_API_KEY',
|
|
||||||
value || config.OCO_GEMINI_API_KEY || config.OCO_AI_PROVIDER === 'test',
|
|
||||||
'You need to provide the OCO_GEMINI_API_KEY when OCO_AI_PROVIDER is set to "gemini". Run: `oco config set OCO_GEMINI_API_KEY=your_key`'
|
|
||||||
);
|
|
||||||
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
[CONFIG_KEYS.OCO_ANTHROPIC_API_KEY](value: any, config: any = {}) {
|
|
||||||
if (config.OCO_AI_PROVIDER !== 'anthropic') return value;
|
|
||||||
|
|
||||||
validateConfig(
|
|
||||||
'ANTHROPIC_API_KEY',
|
|
||||||
!!value,
|
|
||||||
'You need to provide the OCO_ANTHROPIC_API_KEY key when OCO_AI_PROVIDER is set to "anthropic". Run: `oco config set OCO_ANTHROPIC_API_KEY=your_key`'
|
|
||||||
);
|
|
||||||
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
[CONFIG_KEYS.OCO_FLOWISE_API_KEY](value: any, config: any = {}) {
|
|
||||||
validateConfig(
|
|
||||||
CONFIG_KEYS.OCO_FLOWISE_API_KEY,
|
|
||||||
value || config.OCO_AI_PROVIDER !== 'flowise',
|
|
||||||
'You need to provide the OCO_FLOWISE_API_KEY when OCO_AI_PROVIDER is set to "flowise". Run: `oco config set OCO_FLOWISE_API_KEY=your_key`'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
@@ -240,11 +198,11 @@ export const configValidators = {
|
|||||||
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;
|
||||||
},
|
},
|
||||||
@@ -295,9 +253,15 @@ export const configValidators = {
|
|||||||
|
|
||||||
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',
|
||||||
|
'gemini',
|
||||||
|
'azure',
|
||||||
|
'test',
|
||||||
|
'flowise',
|
||||||
|
'groq'
|
||||||
|
].includes(value) || value.startsWith('ollama'),
|
||||||
`${value} is not supported yet, use 'ollama', 'anthropic', 'azure', 'gemini', 'flowise' or 'openai' (default)`
|
`${value} is not supported yet, use 'ollama', 'anthropic', 'azure', 'gemini', 'flowise' or 'openai' (default)`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -314,26 +278,6 @@ 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/"'
|
|
||||||
);
|
|
||||||
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
[CONFIG_KEYS.OCO_FLOWISE_ENDPOINT](value: any) {
|
|
||||||
validateConfig(
|
|
||||||
CONFIG_KEYS.OCO_FLOWISE_ENDPOINT,
|
|
||||||
typeof value === 'string' && value.includes(':'),
|
|
||||||
'Value must be string and should include both I.P. and port number' // Considering the possibility of DNS lookup or feeding the I.P. explicitly, there is no pattern to verify, except a column for the port number
|
|
||||||
);
|
|
||||||
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
[CONFIG_KEYS.OCO_TEST_MOCK_TYPE](value: any) {
|
[CONFIG_KEYS.OCO_TEST_MOCK_TYPE](value: any) {
|
||||||
validateConfig(
|
validateConfig(
|
||||||
CONFIG_KEYS.OCO_TEST_MOCK_TYPE,
|
CONFIG_KEYS.OCO_TEST_MOCK_TYPE,
|
||||||
@@ -345,11 +289,11 @@ export const configValidators = {
|
|||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
|
|
||||||
[CONFIG_KEYS.OCO_OLLAMA_API_URL](value: any) {
|
[CONFIG_KEYS.OCO_WHY](value: any) {
|
||||||
validateConfig(
|
validateConfig(
|
||||||
CONFIG_KEYS.OCO_OLLAMA_API_URL,
|
CONFIG_KEYS.OCO_WHY,
|
||||||
typeof value === 'string' && value.startsWith('http'),
|
typeof value === 'boolean',
|
||||||
`${value} is not a valid URL. It should start with 'http://' or 'https://'.`
|
'Must be true or false'
|
||||||
);
|
);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -362,20 +306,18 @@ export enum OCO_AI_PROVIDER_ENUM {
|
|||||||
GEMINI = 'gemini',
|
GEMINI = 'gemini',
|
||||||
AZURE = 'azure',
|
AZURE = 'azure',
|
||||||
TEST = 'test',
|
TEST = 'test',
|
||||||
FLOWISE = 'flowise'
|
FLOWISE = 'flowise',
|
||||||
|
GROQ = 'groq'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConfigType = {
|
export type ConfigType = {
|
||||||
[CONFIG_KEYS.OCO_OPENAI_API_KEY]?: string;
|
[CONFIG_KEYS.OCO_API_KEY]?: string;
|
||||||
[CONFIG_KEYS.OCO_ANTHROPIC_API_KEY]?: string;
|
|
||||||
[CONFIG_KEYS.OCO_AZURE_API_KEY]?: string;
|
|
||||||
[CONFIG_KEYS.OCO_GEMINI_API_KEY]?: string;
|
|
||||||
[CONFIG_KEYS.OCO_GEMINI_BASE_PATH]?: string;
|
|
||||||
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT]: number;
|
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT]: number;
|
||||||
[CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT]: number;
|
[CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT]: number;
|
||||||
[CONFIG_KEYS.OCO_OPENAI_BASE_PATH]?: string;
|
[CONFIG_KEYS.OCO_API_URL]?: string;
|
||||||
[CONFIG_KEYS.OCO_DESCRIPTION]: boolean;
|
[CONFIG_KEYS.OCO_DESCRIPTION]: boolean;
|
||||||
[CONFIG_KEYS.OCO_EMOJI]: boolean;
|
[CONFIG_KEYS.OCO_EMOJI]: boolean;
|
||||||
|
[CONFIG_KEYS.OCO_WHY]: boolean;
|
||||||
[CONFIG_KEYS.OCO_MODEL]: string;
|
[CONFIG_KEYS.OCO_MODEL]: string;
|
||||||
[CONFIG_KEYS.OCO_LANGUAGE]: string;
|
[CONFIG_KEYS.OCO_LANGUAGE]: string;
|
||||||
[CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER]: string;
|
[CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER]: string;
|
||||||
@@ -383,16 +325,11 @@ export type ConfigType = {
|
|||||||
[CONFIG_KEYS.OCO_AI_PROVIDER]: OCO_AI_PROVIDER_ENUM;
|
[CONFIG_KEYS.OCO_AI_PROVIDER]: OCO_AI_PROVIDER_ENUM;
|
||||||
[CONFIG_KEYS.OCO_GITPUSH]: boolean;
|
[CONFIG_KEYS.OCO_GITPUSH]: boolean;
|
||||||
[CONFIG_KEYS.OCO_ONE_LINE_COMMIT]: boolean;
|
[CONFIG_KEYS.OCO_ONE_LINE_COMMIT]: boolean;
|
||||||
[CONFIG_KEYS.OCO_AZURE_ENDPOINT]?: string;
|
|
||||||
[CONFIG_KEYS.OCO_TEST_MOCK_TYPE]: string;
|
[CONFIG_KEYS.OCO_TEST_MOCK_TYPE]: string;
|
||||||
[CONFIG_KEYS.OCO_API_URL]?: string;
|
|
||||||
[CONFIG_KEYS.OCO_OLLAMA_API_URL]?: string;
|
|
||||||
[CONFIG_KEYS.OCO_FLOWISE_ENDPOINT]: string;
|
|
||||||
[CONFIG_KEYS.OCO_FLOWISE_API_KEY]?: 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');
|
||||||
|
|
||||||
const assertConfigsAreValid = (config: Record<string, any>) => {
|
const assertConfigsAreValid = (config: Record<string, any>) => {
|
||||||
for (const [key, value] of Object.entries(config)) {
|
for (const [key, value] of Object.entries(config)) {
|
||||||
@@ -422,28 +359,28 @@ enum OCO_PROMPT_MODULE_ENUM {
|
|||||||
COMMITLINT = '@commitlint'
|
COMMITLINT = '@commitlint'
|
||||||
}
|
}
|
||||||
|
|
||||||
const initGlobalConfig = () => {
|
export const DEFAULT_CONFIG = {
|
||||||
const defaultConfig = {
|
OCO_TOKENS_MAX_INPUT: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT,
|
||||||
OCO_TOKENS_MAX_INPUT: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT,
|
OCO_TOKENS_MAX_OUTPUT: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT,
|
||||||
OCO_TOKENS_MAX_OUTPUT: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT,
|
OCO_DESCRIPTION: false,
|
||||||
OCO_DESCRIPTION: false,
|
OCO_EMOJI: false,
|
||||||
OCO_EMOJI: false,
|
OCO_MODEL: getDefaultModel('openai'),
|
||||||
OCO_MODEL: getDefaultModel('openai'),
|
OCO_LANGUAGE: 'en',
|
||||||
OCO_LANGUAGE: 'en',
|
OCO_MESSAGE_TEMPLATE_PLACEHOLDER: '$msg',
|
||||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER: '$msg',
|
OCO_PROMPT_MODULE: OCO_PROMPT_MODULE_ENUM.CONVENTIONAL_COMMIT,
|
||||||
OCO_PROMPT_MODULE: OCO_PROMPT_MODULE_ENUM.CONVENTIONAL_COMMIT,
|
OCO_AI_PROVIDER: OCO_AI_PROVIDER_ENUM.OPENAI,
|
||||||
OCO_AI_PROVIDER: OCO_AI_PROVIDER_ENUM.OPENAI,
|
OCO_ONE_LINE_COMMIT: false,
|
||||||
OCO_ONE_LINE_COMMIT: false,
|
OCO_TEST_MOCK_TYPE: 'commit-message',
|
||||||
OCO_TEST_MOCK_TYPE: 'commit-message',
|
OCO_WHY: false,
|
||||||
OCO_FLOWISE_ENDPOINT: ':',
|
OCO_GITPUSH: true // todo: deprecate
|
||||||
OCO_GITPUSH: true // todo: deprecate
|
|
||||||
};
|
|
||||||
|
|
||||||
writeFileSync(defaultConfigPath, iniStringify(defaultConfig), 'utf8');
|
|
||||||
return defaultConfig;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseEnvVarValue = (value?: any) => {
|
const initGlobalConfig = (configPath: string = defaultConfigPath) => {
|
||||||
|
writeFileSync(configPath, iniStringify(DEFAULT_CONFIG), 'utf8');
|
||||||
|
return DEFAULT_CONFIG;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseConfigVarValue = (value?: any) => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(value);
|
return JSON.parse(value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -451,73 +388,122 @@ const parseEnvVarValue = (value?: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getConfig = ({
|
const getEnvConfig = (envPath: string) => {
|
||||||
configPath = defaultConfigPath,
|
|
||||||
envPath = defaultEnvPath
|
|
||||||
}: {
|
|
||||||
configPath?: string;
|
|
||||||
envPath?: string;
|
|
||||||
} = {}): ConfigType => {
|
|
||||||
dotenv.config({ path: envPath });
|
dotenv.config({ path: envPath });
|
||||||
|
|
||||||
const envConfig = {
|
return {
|
||||||
OCO_MODEL: process.env.OCO_MODEL,
|
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_OPENAI_API_KEY: process.env.OCO_OPENAI_API_KEY,
|
OCO_TOKENS_MAX_INPUT: parseConfigVarValue(process.env.OCO_TOKENS_MAX_INPUT),
|
||||||
OCO_ANTHROPIC_API_KEY: process.env.OCO_ANTHROPIC_API_KEY,
|
OCO_TOKENS_MAX_OUTPUT: parseConfigVarValue(
|
||||||
OCO_AZURE_API_KEY: process.env.OCO_AZURE_API_KEY,
|
process.env.OCO_TOKENS_MAX_OUTPUT
|
||||||
OCO_GEMINI_API_KEY: process.env.OCO_GEMINI_API_KEY,
|
),
|
||||||
OCO_FLOWISE_API_KEY: process.env.OCO_FLOWISE_API_KEY,
|
|
||||||
|
|
||||||
OCO_TOKENS_MAX_INPUT: parseEnvVarValue(process.env.OCO_TOKENS_MAX_INPUT),
|
OCO_DESCRIPTION: parseConfigVarValue(process.env.OCO_DESCRIPTION),
|
||||||
OCO_TOKENS_MAX_OUTPUT: parseEnvVarValue(process.env.OCO_TOKENS_MAX_OUTPUT),
|
OCO_EMOJI: parseConfigVarValue(process.env.OCO_EMOJI),
|
||||||
|
|
||||||
OCO_OPENAI_BASE_PATH: process.env.OCO_OPENAI_BASE_PATH,
|
|
||||||
OCO_GEMINI_BASE_PATH: process.env.OCO_GEMINI_BASE_PATH,
|
|
||||||
|
|
||||||
OCO_AZURE_ENDPOINT: process.env.OCO_AZURE_ENDPOINT,
|
|
||||||
OCO_FLOWISE_ENDPOINT: process.env.OCO_FLOWISE_ENDPOINT,
|
|
||||||
OCO_OLLAMA_API_URL: process.env.OCO_OLLAMA_API_URL,
|
|
||||||
|
|
||||||
OCO_DESCRIPTION: parseEnvVarValue(process.env.OCO_DESCRIPTION),
|
|
||||||
OCO_EMOJI: parseEnvVarValue(process.env.OCO_EMOJI),
|
|
||||||
OCO_LANGUAGE: process.env.OCO_LANGUAGE,
|
OCO_LANGUAGE: process.env.OCO_LANGUAGE,
|
||||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER:
|
OCO_MESSAGE_TEMPLATE_PLACEHOLDER:
|
||||||
process.env.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
|
process.env.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
|
||||||
OCO_PROMPT_MODULE: process.env.OCO_PROMPT_MODULE as OCO_PROMPT_MODULE_ENUM,
|
OCO_PROMPT_MODULE: process.env.OCO_PROMPT_MODULE as OCO_PROMPT_MODULE_ENUM,
|
||||||
OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER as OCO_AI_PROVIDER_ENUM,
|
OCO_ONE_LINE_COMMIT: parseConfigVarValue(process.env.OCO_ONE_LINE_COMMIT),
|
||||||
OCO_ONE_LINE_COMMIT: parseEnvVarValue(process.env.OCO_ONE_LINE_COMMIT),
|
|
||||||
OCO_TEST_MOCK_TYPE: process.env.OCO_TEST_MOCK_TYPE,
|
OCO_TEST_MOCK_TYPE: process.env.OCO_TEST_MOCK_TYPE,
|
||||||
|
|
||||||
OCO_GITPUSH: parseEnvVarValue(process.env.OCO_GITPUSH) // todo: deprecate
|
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;
|
let globalConfig: ConfigType;
|
||||||
const isGlobalConfigFileExist = existsSync(configPath);
|
|
||||||
if (!isGlobalConfigFileExist) globalConfig = initGlobalConfig();
|
const isGlobalConfigFileExist = getIsGlobalConfigFileExist(configPath);
|
||||||
|
if (!isGlobalConfigFileExist) globalConfig = initGlobalConfig(configPath);
|
||||||
else {
|
else {
|
||||||
const configFile = readFileSync(configPath, 'utf8');
|
const configFile = readFileSync(configPath, 'utf8');
|
||||||
globalConfig = iniParse(configFile) as ConfigType;
|
globalConfig = iniParse(configFile) as ConfigType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergeObjects = (main: Partial<ConfigType>, fallback: ConfigType) =>
|
return globalConfig;
|
||||||
Object.keys(CONFIG_KEYS).reduce((acc, key) => {
|
};
|
||||||
acc[key] = parseEnvVarValue(main[key] ?? fallback[key]);
|
|
||||||
|
|
||||||
return acc;
|
/**
|
||||||
}, {} as ConfigType);
|
* 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);
|
||||||
|
};
|
||||||
|
|
||||||
// env config takes precedence over global ~/.opencommit config file
|
interface GetConfigOptions {
|
||||||
const config = mergeObjects(envConfig, globalConfig);
|
globalPath?: string;
|
||||||
|
envPath?: string;
|
||||||
|
setDefaultValues?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
const cleanUndefinedValues = (config: ConfigType) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(config).map(([_, v]) => {
|
||||||
|
try {
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
if (v === 'undefined') return [_, undefined];
|
||||||
|
if (v === 'null') return [_, null];
|
||||||
|
|
||||||
|
const parsedValue = JSON.parse(v);
|
||||||
|
return [_, parsedValue];
|
||||||
|
}
|
||||||
|
return [_, v];
|
||||||
|
} catch (error) {
|
||||||
|
return [_, v];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getConfig = ({
|
||||||
|
envPath = defaultEnvPath,
|
||||||
|
globalPath = defaultConfigPath
|
||||||
|
}: GetConfigOptions = {}): ConfigType => {
|
||||||
|
const envConfig = getEnvConfig(envPath);
|
||||||
|
const globalConfig = getGlobalConfig(globalPath);
|
||||||
|
|
||||||
|
const config = mergeConfigs(envConfig, globalConfig);
|
||||||
|
|
||||||
|
const cleanConfig = cleanUndefinedValues(config);
|
||||||
|
|
||||||
|
return cleanConfig as ConfigType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setConfig = (
|
export const setConfig = (
|
||||||
keyValues: [key: string, value: string][],
|
keyValues: [key: string, value: string | boolean | number | null][],
|
||||||
configPath: string = defaultConfigPath
|
globalConfigPath: string = defaultConfigPath
|
||||||
) => {
|
) => {
|
||||||
const config = getConfig();
|
const config = getConfig({
|
||||||
|
globalPath: globalConfigPath
|
||||||
|
});
|
||||||
|
|
||||||
|
const configToSet = {};
|
||||||
|
|
||||||
for (let [key, value] of keyValues) {
|
for (let [key, value] of keyValues) {
|
||||||
if (!configValidators.hasOwnProperty(key)) {
|
if (!configValidators.hasOwnProperty(key)) {
|
||||||
@@ -530,7 +516,8 @@ export const setConfig = (
|
|||||||
let parsedConfigValue;
|
let parsedConfigValue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parsedConfigValue = JSON.parse(value);
|
if (typeof value === 'string') parsedConfigValue = JSON.parse(value);
|
||||||
|
else parsedConfigValue = value;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
parsedConfigValue = value;
|
parsedConfigValue = value;
|
||||||
}
|
}
|
||||||
@@ -540,12 +527,10 @@ export const setConfig = (
|
|||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
|
||||||
config[key] = validValue;
|
configToSet[key] = validValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFileSync(configPath, iniStringify(config), 'utf8');
|
setGlobalConfig(mergeConfigs(configToSet, config), globalConfigPath);
|
||||||
|
|
||||||
assertConfigsAreValid(config);
|
|
||||||
|
|
||||||
outro(`${chalk.green('✔')} config successfully set`);
|
outro(`${chalk.green('✔')} config successfully set`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,14 +39,11 @@ export const prepareCommitMessageHook = async (
|
|||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
if (
|
if (!config.OCO_API_KEY) {
|
||||||
!config.OCO_OPENAI_API_KEY &&
|
outro(
|
||||||
!config.OCO_ANTHROPIC_API_KEY &&
|
'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'
|
||||||
!config.OCO_AZURE_API_KEY
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
'No OPEN_AI_API or OCO_ANTHROPIC_API_KEY or OCO_AZURE_API_KEY exists. Set your key in ~/.opencommit'
|
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const spin = spinner();
|
const spin = spinner();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AiEngine, AiEngineConfig } from './Engine';
|
|||||||
|
|
||||||
interface FlowiseAiConfig extends AiEngineConfig {}
|
interface FlowiseAiConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
export class FlowiseAi implements AiEngine {
|
export class FlowiseEngine implements AiEngine {
|
||||||
config: FlowiseAiConfig;
|
config: FlowiseAiConfig;
|
||||||
client: AxiosInstance;
|
client: AxiosInstance;
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { AiEngine, AiEngineConfig } from './Engine';
|
|||||||
|
|
||||||
interface GeminiConfig extends AiEngineConfig {}
|
interface GeminiConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
export class Gemini implements AiEngine {
|
export class GeminiEngine implements AiEngine {
|
||||||
config: GeminiConfig;
|
config: GeminiConfig;
|
||||||
client: GoogleGenerativeAI;
|
client: GoogleGenerativeAI;
|
||||||
|
|
||||||
|
|||||||
10
src/engine/groq.ts
Normal file
10
src/engine/groq.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { OpenAiConfig, OpenAiEngine } from './openAi';
|
||||||
|
|
||||||
|
interface GroqConfig extends OpenAiConfig {}
|
||||||
|
|
||||||
|
export class GroqEngine extends OpenAiEngine {
|
||||||
|
constructor(config: GroqConfig) {
|
||||||
|
config.baseURL = 'https://api.groq.com/openai/v1';
|
||||||
|
super(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { AiEngine, AiEngineConfig } from './Engine';
|
|||||||
|
|
||||||
interface OllamaConfig extends AiEngineConfig {}
|
interface OllamaConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
export class OllamaAi implements AiEngine {
|
export class OllamaEngine implements AiEngine {
|
||||||
config: OllamaConfig;
|
config: OllamaConfig;
|
||||||
client: AxiosInstance;
|
client: AxiosInstance;
|
||||||
|
|
||||||
@@ -28,7 +28,10 @@ export class OllamaAi implements AiEngine {
|
|||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const response = await this.client.post('', params);
|
const response = await this.client.post(
|
||||||
|
this.client.getUri(this.config),
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
const message = response.data.message;
|
const message = response.data.message;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitD
|
|||||||
import { tokenCount } from '../utils/tokenCount';
|
import { tokenCount } from '../utils/tokenCount';
|
||||||
import { AiEngine, AiEngineConfig } from './Engine';
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
interface OpenAiConfig extends AiEngineConfig {}
|
export interface OpenAiConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
export class OpenAiEngine implements AiEngine {
|
export class OpenAiEngine implements AiEngine {
|
||||||
config: OpenAiConfig;
|
config: OpenAiConfig;
|
||||||
@@ -12,7 +12,12 @@ export class OpenAiEngine implements AiEngine {
|
|||||||
|
|
||||||
constructor(config: OpenAiConfig) {
|
constructor(config: OpenAiConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.client = new OpenAI({ apiKey: config.apiKey });
|
|
||||||
|
if (!config.baseURL) {
|
||||||
|
this.client = new OpenAI({ apiKey: config.apiKey });
|
||||||
|
} else {
|
||||||
|
this.client = new OpenAI({ apiKey: config.apiKey, baseURL: config.baseURL });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateCommitMessage = async (
|
public generateCommitMessage = async (
|
||||||
|
|||||||
@@ -6,11 +6,8 @@ import { mergeDiffs } from './utils/mergeDiffs';
|
|||||||
import { tokenCount } from './utils/tokenCount';
|
import { tokenCount } from './utils/tokenCount';
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
22
src/migrations/02_set_missing_default_values.ts
Normal file
22
src/migrations/02_set_missing_default_values.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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' || config[key] === undefined)
|
||||||
|
entriesToSet.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entriesToSet.length > 0) setConfig(entriesToSet);
|
||||||
|
console.log(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
|
||||||
|
}
|
||||||
|
];
|
||||||
71
src/migrations/_run.ts
Normal file
71
src/migrations/_run.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
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}`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
isMigrated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMigrated) {
|
||||||
|
outro(
|
||||||
|
`${chalk.green(
|
||||||
|
'✔'
|
||||||
|
)} Migrations to your config were applied successfully. Please rerun.`
|
||||||
|
);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -258,7 +258,9 @@ const INIT_MAIN_PROMPT = (
|
|||||||
prompts: string[]
|
prompts: string[]
|
||||||
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
|
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
|
||||||
role: 'system',
|
role: 'system',
|
||||||
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.
|
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes ${
|
||||||
|
config.OCO_WHY ? '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_EMOJI
|
config.OCO_EMOJI
|
||||||
? 'Use GitMoji convention to preface the commit.'
|
? 'Use GitMoji convention to preface the commit.'
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
const findModulePath = (moduleName: string) => {
|
||||||
|
const searchPaths = [
|
||||||
|
path.join('node_modules', moduleName),
|
||||||
|
path.join('node_modules', '.pnpm')
|
||||||
|
];
|
||||||
|
|
||||||
|
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 getCommitLintModuleType = async (): Promise<'cjs' | 'esm'> => {
|
||||||
const packageFile = 'node_modules/@commitlint/load/package.json';
|
const packageFile = '@commitlint/load/package.json';
|
||||||
const packageJsonPath = path.join(
|
const packageJsonPath = findModulePath(packageFile);
|
||||||
process.env.PWD || process.cwd(),
|
|
||||||
packageFile,
|
|
||||||
);
|
|
||||||
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
||||||
|
|
||||||
if (!packageJson) {
|
if (!packageJson) {
|
||||||
throw new Error(`Failed to parse ${packageFile}`);
|
throw new Error(`Failed to parse ${packageFile}`);
|
||||||
}
|
}
|
||||||
@@ -19,7 +35,7 @@ const getCommitLintModuleType = async (): Promise<'cjs' | 'esm'> => {
|
|||||||
* QualifiedConfig from any version of @commitlint/types
|
* QualifiedConfig from any version of @commitlint/types
|
||||||
* @see https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/types/src/load.ts
|
* @see https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/types/src/load.ts
|
||||||
*/
|
*/
|
||||||
type QualifiedConfigOnAnyVersion = { [key:string]: unknown };
|
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
|
||||||
@@ -27,36 +43,31 @@ type QualifiedConfigOnAnyVersion = { [key:string]: unknown };
|
|||||||
*
|
*
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getCommitLintPWDConfig = async (): Promise<QualifiedConfigOnAnyVersion | null> => {
|
export const getCommitLintPWDConfig =
|
||||||
let load, nodeModulesPath;
|
async (): Promise<QualifiedConfigOnAnyVersion | null> => {
|
||||||
switch (await getCommitLintModuleType()) {
|
let load: Function, modulePath: string;
|
||||||
case 'cjs':
|
switch (await getCommitLintModuleType()) {
|
||||||
/**
|
case 'cjs':
|
||||||
* CommonJS (<= commitlint@v18.x.x.)
|
/**
|
||||||
*/
|
* CommonJS (<= commitlint@v18.x.x.)
|
||||||
nodeModulesPath = path.join(
|
*/
|
||||||
process.env.PWD || process.cwd(),
|
modulePath = findModulePath('@commitlint/load');
|
||||||
'node_modules/@commitlint/load',
|
load = require(modulePath).default;
|
||||||
);
|
break;
|
||||||
load = require(nodeModulesPath).default;
|
case 'esm':
|
||||||
break;
|
/**
|
||||||
case 'esm':
|
* ES Module (commitlint@v19.x.x. <= )
|
||||||
/**
|
* Directory import is not supported in ES Module resolution, so import the file directly
|
||||||
* 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;
|
||||||
nodeModulesPath = path.join(
|
break;
|
||||||
process.env.PWD || process.cwd(),
|
}
|
||||||
'node_modules/@commitlint/load/lib/load.js',
|
|
||||||
);
|
|
||||||
load = (await import(nodeModulesPath)).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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { getConfig, OCO_AI_PROVIDER_ENUM } from '../commands/config';
|
|||||||
import { AnthropicEngine } from '../engine/anthropic';
|
import { AnthropicEngine } from '../engine/anthropic';
|
||||||
import { AzureEngine } from '../engine/azure';
|
import { AzureEngine } from '../engine/azure';
|
||||||
import { AiEngine } from '../engine/Engine';
|
import { AiEngine } from '../engine/Engine';
|
||||||
import { FlowiseAi } from '../engine/flowise';
|
import { FlowiseEngine } from '../engine/flowise';
|
||||||
import { Gemini } from '../engine/gemini';
|
import { GeminiEngine } from '../engine/gemini';
|
||||||
import { OllamaAi } from '../engine/ollama';
|
import { OllamaEngine } from '../engine/ollama';
|
||||||
import { OpenAiEngine } from '../engine/openAi';
|
import { OpenAiEngine } from '../engine/openAi';
|
||||||
import { TestAi, TestMockType } from '../engine/testAi';
|
import { TestAi, TestMockType } from '../engine/testAi';
|
||||||
|
import { GroqEngine } from '../engine/groq';
|
||||||
|
|
||||||
export function getEngine(): AiEngine {
|
export function getEngine(): AiEngine {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
@@ -16,50 +17,33 @@ export function getEngine(): AiEngine {
|
|||||||
model: config.OCO_MODEL!,
|
model: config.OCO_MODEL!,
|
||||||
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
|
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
|
||||||
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
|
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
|
||||||
baseURL: config.OCO_OPENAI_BASE_PATH!
|
baseURL: config.OCO_API_URL!,
|
||||||
|
apiKey: config.OCO_API_KEY!
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
case OCO_AI_PROVIDER_ENUM.OLLAMA:
|
case OCO_AI_PROVIDER_ENUM.OLLAMA:
|
||||||
return new OllamaAi({
|
return new OllamaEngine(DEFAULT_CONFIG);
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
apiKey: '',
|
|
||||||
baseURL: config.OCO_OLLAMA_API_URL!
|
|
||||||
});
|
|
||||||
|
|
||||||
case OCO_AI_PROVIDER_ENUM.ANTHROPIC:
|
case OCO_AI_PROVIDER_ENUM.ANTHROPIC:
|
||||||
return new AnthropicEngine({
|
return new AnthropicEngine(DEFAULT_CONFIG);
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
apiKey: config.OCO_ANTHROPIC_API_KEY!
|
|
||||||
});
|
|
||||||
|
|
||||||
case OCO_AI_PROVIDER_ENUM.TEST:
|
case OCO_AI_PROVIDER_ENUM.TEST:
|
||||||
return new TestAi(config.OCO_TEST_MOCK_TYPE as TestMockType);
|
return new TestAi(config.OCO_TEST_MOCK_TYPE as TestMockType);
|
||||||
|
|
||||||
case OCO_AI_PROVIDER_ENUM.GEMINI:
|
case OCO_AI_PROVIDER_ENUM.GEMINI:
|
||||||
return new Gemini({
|
return new GeminiEngine(DEFAULT_CONFIG);
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
apiKey: config.OCO_GEMINI_API_KEY!,
|
|
||||||
baseURL: config.OCO_GEMINI_BASE_PATH!
|
|
||||||
});
|
|
||||||
|
|
||||||
case OCO_AI_PROVIDER_ENUM.AZURE:
|
case OCO_AI_PROVIDER_ENUM.AZURE:
|
||||||
return new AzureEngine({
|
return new AzureEngine(DEFAULT_CONFIG);
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
apiKey: config.OCO_AZURE_API_KEY!
|
|
||||||
});
|
|
||||||
|
|
||||||
case OCO_AI_PROVIDER_ENUM.FLOWISE:
|
case OCO_AI_PROVIDER_ENUM.FLOWISE:
|
||||||
return new FlowiseAi({
|
return new FlowiseEngine(DEFAULT_CONFIG);
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
baseURL: config.OCO_FLOWISE_ENDPOINT || DEFAULT_CONFIG.baseURL,
|
case OCO_AI_PROVIDER_ENUM.GROQ:
|
||||||
apiKey: config.OCO_FLOWISE_API_KEY!
|
return new GroqEngine(DEFAULT_CONFIG);
|
||||||
});
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return new OpenAiEngine({
|
return new OpenAiEngine(DEFAULT_CONFIG);
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
apiKey: config.OCO_OPENAI_API_KEY!
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,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();
|
||||||
@@ -46,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();
|
||||||
|
|||||||
@@ -181,9 +181,7 @@ describe('cli flow to generate commit message using @commitlint prompt-module',
|
|||||||
[],
|
[],
|
||||||
{ cwd: gitDir }
|
{ cwd: gitDir }
|
||||||
);
|
);
|
||||||
expect(
|
expect(await commitlintGet.findByText('consistency')).toBeInTheConsole();
|
||||||
await commitlintGet.findByText('[object Object]')
|
|
||||||
).toBeInTheConsole();
|
|
||||||
|
|
||||||
// Run 'oco' using .opencommit-commitlint
|
// Run 'oco' using .opencommit-commitlint
|
||||||
await render('echo', [`'console.log("Hello World");' > index.ts`], {
|
await render('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||||
@@ -211,7 +209,7 @@ describe('cli flow to generate commit message using @commitlint prompt-module',
|
|||||||
oco.userEvent.keyboard('[Enter]');
|
oco.userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await oco.findByText('Choose a remote to push to')
|
await oco.findByText('Do you want to run `git push`?')
|
||||||
).toBeInTheConsole();
|
).toBeInTheConsole();
|
||||||
oco.userEvent.keyboard('[Enter]');
|
oco.userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
@@ -30,4 +30,8 @@ export const prepareEnvironment = async (): Promise<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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));
|
export const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
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 globalConfigFile: { filePath: string; cleanup: () => Promise<void> };
|
||||||
let localEnvFile: { 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) => {
|
||||||
@@ -19,7 +26,12 @@ describe('getConfig', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
resetEnv(originalEnv);
|
resetEnv(originalEnv);
|
||||||
if (globalConfigFile) await globalConfigFile.cleanup();
|
if (globalConfigFile) await globalConfigFile.cleanup();
|
||||||
if (localEnvFile) await localEnvFile.cleanup();
|
if (envConfigFile) await envConfigFile.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (globalConfigFile) await globalConfigFile.cleanup();
|
||||||
|
if (envConfigFile) await envConfigFile.cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -36,115 +48,256 @@ describe('getConfig', () => {
|
|||||||
return await prepareFile(fileName, fileContent);
|
return await prepareFile(fileName, fileContent);
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should prioritize local .env over global .opencommit config', async () => {
|
describe('getConfig', () => {
|
||||||
globalConfigFile = await generateConfig('.opencommit', {
|
it('should prioritize local .env over global .opencommit config', async () => {
|
||||||
OCO_OPENAI_API_KEY: 'global-key',
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
OCO_MODEL: 'gpt-3.5-turbo',
|
OCO_API_KEY: 'global-key',
|
||||||
OCO_LANGUAGE: 'en'
|
OCO_MODEL: 'gpt-3.5-turbo',
|
||||||
|
OCO_LANGUAGE: 'en'
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
});
|
});
|
||||||
|
|
||||||
localEnvFile = await generateConfig('.env', {
|
it('should fallback to global config when local config is not set', async () => {
|
||||||
OCO_OPENAI_API_KEY: 'local-key',
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
OCO_ANTHROPIC_API_KEY: 'local-anthropic-key',
|
OCO_API_KEY: 'global-key',
|
||||||
OCO_LANGUAGE: 'fr'
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
const config = getConfig({
|
it('should handle boolean and numeric values correctly', async () => {
|
||||||
configPath: globalConfigFile.filePath,
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
envPath: localEnvFile.filePath
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(config).not.toEqual(null);
|
it('should handle empty local config correctly', async () => {
|
||||||
expect(config.OCO_OPENAI_API_KEY).toEqual('local-key');
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
expect(config.OCO_MODEL).toEqual('gpt-3.5-turbo');
|
OCO_API_KEY: 'global-key',
|
||||||
expect(config.OCO_LANGUAGE).toEqual('fr');
|
OCO_MODEL: 'gpt-4',
|
||||||
expect(config.OCO_ANTHROPIC_API_KEY).toEqual('local-anthropic-key');
|
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('should fallback to global config when local config is not set', async () => {
|
describe('setConfig', () => {
|
||||||
globalConfigFile = await generateConfig('.opencommit', {
|
beforeEach(async () => {
|
||||||
OCO_OPENAI_API_KEY: 'global-key',
|
// we create and delete the file to have the parent directory, but not the file, to test the creation of the file
|
||||||
OCO_MODEL: 'gpt-4',
|
globalConfigFile = await generateConfig('.opencommit', {});
|
||||||
OCO_LANGUAGE: 'de',
|
rmSync(globalConfigFile.filePath);
|
||||||
OCO_DESCRIPTION: 'true'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
localEnvFile = await generateConfig('.env', {
|
it('should create .opencommit file with DEFAULT CONFIG if it does not exist on first setConfig run', async () => {
|
||||||
OCO_ANTHROPIC_API_KEY: 'local-anthropic-key'
|
const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath);
|
||||||
|
expect(isGlobalConfigFileExist).toBe(false);
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const config = getConfig({
|
it('should set new config values', async () => {
|
||||||
configPath: globalConfigFile.filePath,
|
globalConfigFile = await generateConfig('.opencommit', {});
|
||||||
envPath: localEnvFile.filePath
|
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');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(config).not.toEqual(null);
|
it('should update existing config values', async () => {
|
||||||
expect(config.OCO_OPENAI_API_KEY).toEqual('global-key');
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
expect(config.OCO_ANTHROPIC_API_KEY).toEqual('local-anthropic-key');
|
OCO_API_KEY: 'initial-key'
|
||||||
expect(config.OCO_MODEL).toEqual('gpt-4');
|
});
|
||||||
expect(config.OCO_LANGUAGE).toEqual('de');
|
await setConfig(
|
||||||
expect(config.OCO_DESCRIPTION).toEqual(true);
|
[[CONFIG_KEYS.OCO_API_KEY, 'updated-key']],
|
||||||
});
|
globalConfigFile.filePath
|
||||||
|
);
|
||||||
|
|
||||||
it('should handle boolean and numeric values correctly', async () => {
|
const config = getConfig({
|
||||||
globalConfigFile = await generateConfig('.opencommit', {
|
globalPath: globalConfigFile.filePath
|
||||||
OCO_TOKENS_MAX_INPUT: '4096',
|
});
|
||||||
OCO_TOKENS_MAX_OUTPUT: '500',
|
expect(config.OCO_API_KEY).toEqual('updated-key');
|
||||||
OCO_GITPUSH: 'true'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
localEnvFile = await generateConfig('.env', {
|
it('should handle boolean and numeric values correctly', async () => {
|
||||||
OCO_TOKENS_MAX_INPUT: '8192',
|
globalConfigFile = await generateConfig('.opencommit', {});
|
||||||
OCO_ONE_LINE_COMMIT: 'false'
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
const config = getConfig({
|
it('should throw an error for unsupported config keys', async () => {
|
||||||
configPath: globalConfigFile.filePath,
|
globalConfigFile = await generateConfig('.opencommit', {});
|
||||||
envPath: localEnvFile.filePath
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(config).not.toEqual(null);
|
it('should persist changes to the config file', async () => {
|
||||||
expect(config.OCO_TOKENS_MAX_INPUT).toEqual(8192);
|
const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath);
|
||||||
expect(config.OCO_TOKENS_MAX_OUTPUT).toEqual(500);
|
expect(isGlobalConfigFileExist).toBe(false);
|
||||||
expect(config.OCO_GITPUSH).toEqual(true);
|
|
||||||
expect(config.OCO_ONE_LINE_COMMIT).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle empty local config correctly', async () => {
|
await setConfig(
|
||||||
globalConfigFile = await generateConfig('.opencommit', {
|
[[CONFIG_KEYS.OCO_API_KEY, 'persisted-key']],
|
||||||
OCO_OPENAI_API_KEY: 'global-key',
|
globalConfigFile.filePath
|
||||||
OCO_MODEL: 'gpt-4',
|
);
|
||||||
OCO_LANGUAGE: 'es'
|
|
||||||
|
const fileContent = readFileSync(globalConfigFile.filePath, 'utf8');
|
||||||
|
expect(fileContent).toContain('OCO_API_KEY=persisted-key');
|
||||||
});
|
});
|
||||||
|
|
||||||
localEnvFile = await generateConfig('.env', {});
|
it('should set multiple configs in a row and keep the changes', async () => {
|
||||||
|
const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath);
|
||||||
|
expect(isGlobalConfigFileExist).toBe(false);
|
||||||
|
|
||||||
const config = getConfig({
|
await setConfig(
|
||||||
configPath: globalConfigFile.filePath,
|
[[CONFIG_KEYS.OCO_API_KEY, 'persisted-key']],
|
||||||
envPath: localEnvFile.filePath
|
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');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(config).not.toEqual(null);
|
|
||||||
expect(config.OCO_OPENAI_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_OPENAI_API_KEY: 'global-key',
|
|
||||||
OCO_MODEL: 'gpt-4',
|
|
||||||
OCO_LANGUAGE: 'es'
|
|
||||||
});
|
|
||||||
|
|
||||||
localEnvFile = await generateConfig('.env', { OCO_OPENAI_API_KEY: 'null' });
|
|
||||||
|
|
||||||
const config = getConfig({
|
|
||||||
configPath: globalConfigFile.filePath,
|
|
||||||
envPath: localEnvFile.filePath
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(config).not.toEqual(null);
|
|
||||||
expect(config.OCO_OPENAI_API_KEY).toEqual(null);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Gemini } from '../../src/engine/gemini';
|
import { GeminiEngine } from '../../src/engine/gemini';
|
||||||
|
|
||||||
import { GenerativeModel, GoogleGenerativeAI } from '@google/generative-ai';
|
import { GenerativeModel, GoogleGenerativeAI } from '@google/generative-ai';
|
||||||
import {
|
import {
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { OpenAI } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
|
|
||||||
describe('Gemini', () => {
|
describe('Gemini', () => {
|
||||||
let gemini: Gemini;
|
let gemini: GeminiEngine;
|
||||||
let mockConfig: ConfigType;
|
let mockConfig: ConfigType;
|
||||||
let mockGoogleGenerativeAi: GoogleGenerativeAI;
|
let mockGoogleGenerativeAi: GoogleGenerativeAI;
|
||||||
let mockGenerativeModel: GenerativeModel;
|
let mockGenerativeModel: GenerativeModel;
|
||||||
@@ -20,8 +20,8 @@ describe('Gemini', () => {
|
|||||||
const mockGemini = () => {
|
const mockGemini = () => {
|
||||||
mockConfig = getConfig() as ConfigType;
|
mockConfig = getConfig() as ConfigType;
|
||||||
|
|
||||||
gemini = new Gemini({
|
gemini = new GeminiEngine({
|
||||||
apiKey: mockConfig.OCO_GEMINI_API_KEY,
|
apiKey: mockConfig.OCO_API_KEY,
|
||||||
model: mockConfig.OCO_MODEL
|
model: mockConfig.OCO_MODEL
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -45,12 +45,10 @@ describe('Gemini', () => {
|
|||||||
mockConfig = getConfig() as ConfigType;
|
mockConfig = getConfig() as ConfigType;
|
||||||
|
|
||||||
mockConfig.OCO_AI_PROVIDER = OCO_AI_PROVIDER_ENUM.GEMINI;
|
mockConfig.OCO_AI_PROVIDER = OCO_AI_PROVIDER_ENUM.GEMINI;
|
||||||
mockConfig.OCO_GEMINI_API_KEY = 'mock-api-key';
|
mockConfig.OCO_API_KEY = 'mock-api-key';
|
||||||
mockConfig.OCO_MODEL = 'gemini-1.5-flash';
|
mockConfig.OCO_MODEL = 'gemini-1.5-flash';
|
||||||
|
|
||||||
mockGoogleGenerativeAi = new GoogleGenerativeAI(
|
mockGoogleGenerativeAi = new GoogleGenerativeAI(mockConfig.OCO_API_KEY);
|
||||||
mockConfig.OCO_GEMINI_API_KEY
|
|
||||||
);
|
|
||||||
mockGenerativeModel = mockGoogleGenerativeAi.getGenerativeModel({
|
mockGenerativeModel = mockGoogleGenerativeAi.getGenerativeModel({
|
||||||
model: mockConfig.OCO_MODEL
|
model: mockConfig.OCO_MODEL
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,7 +20,9 @@ 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 {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"lib": ["ES6", "ES2020"],
|
"lib": ["ES6", "ES2020"],
|
||||||
|
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
|
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
|
|
||||||
@@ -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