Compare commits

..

24 Commits

Author SHA1 Message Date
di-sukharev
676c2b20c1 update package-lock.json 2024-08-27 16:58:22 +03:00
di-sukharev
21da102f6d Merge remote-tracking branch 'origin/dev' into refactoring_v1 2024-08-27 16:57:54 +03:00
di-sukharev
6ae7ce2720 chore(config.ts): remove debug console log to clean up the code and improve readability 2024-08-27 16:49:17 +03:00
di-sukharev
e44610fea3 chore(.gitignore): add notes.md to .gitignore to prevent tracking of notes files 2024-08-27 16:49:03 +03:00
di-sukharev
9dcb264420 test(config.test.ts): refactor generateConfig function to accept an object for content to improve readability and maintainability 2024-08-20 21:36:00 +03:00
di-sukharev
dd7fdba94e fix(config.ts): revert OCO_GITPUSH to its original position in the config object for clarity
refactor(config.ts): rename configFromEnv to envConfig for better readability
refactor(gemini.ts): simplify client initialization in the Gemini constructor
test(config.test.ts): add test case to check overriding global config with null values in local .env
test(gemini.test.ts): update AI provider assignment to use OCO_AI_PROVIDER_ENUM for consistency
2024-08-20 21:32:16 +03:00
di-sukharev
5fa12e2d4a feat(config): export OCO_AI_PROVIDER_ENUM to allow external access to AI provider constants
refactor(config): simplify mergeObjects function to improve readability and maintainability
refactor(setConfig): remove unnecessary keysToSet variable to streamline logging
refactor(engine): update switch cases to use OCO_AI_PROVIDER_ENUM for better consistency and clarity
2024-08-20 15:37:41 +03:00
di-sukharev
42a36492ad build 2024-08-20 15:37:33 +03:00
di-sukharev
443d27fc8d chore(docs): mark "Push to git" section in README as deprecated to inform users of upcoming changes
refactor(commit.ts): remove early return for non-pushing users to streamline commit process
refactor(config.ts): add deprecation comments for OCO_GITPUSH to indicate future removal
test(config.test.ts): enhance tests to ensure correct handling of local and global config priorities
test(gemini.test.ts): improve tests for Gemini class to ensure proper functionality and error handling
2024-08-20 15:34:09 +03:00
di-sukharev
04991dd00f fix(engine.ts): include DEFAULT_CONFIG in Gemini and Azure engine instantiation to ensure consistent configuration across engines 2024-08-20 12:58:00 +03:00
di-sukharev
3ded6062c1 fix: remove optional chaining from config access to ensure compatibility and prevent potential runtime errors
refactor(flowise.ts, ollama.ts): update axios client configuration to use a consistent URL format for API requests
fix: update README example to reflect the removal of optional chaining in config access
2024-08-20 12:32:40 +03:00
di-sukharev
f8584e7b78 refactor(engine): rename basePath to baseURL for consistency across interfaces and implementations
fix(engine): update Azure and Flowise engines to use baseURL instead of basePath for API configuration
fix(engine): adjust Ollama engine to handle baseURL and fallback to default URL
style(engine): clean up constructor formatting in OpenAiEngine for better readability
chore(engine): update getEngine function to use baseURL in configuration for all engines
2024-08-20 12:21:13 +03:00
di-sukharev
94faceefd3 remove mb confusing line 2024-08-20 12:06:01 +03:00
di-sukharev
720cd6f9c1 clear readme 2024-08-20 12:05:15 +03:00
di-sukharev
b6a92d557f docs(README.md): update author section and clarify API key storage details
docs(README.md): improve instructions for using OpenCommit CLI and configuration
fix(README.md): correct default model name to gpt-4o-mini in usage examples
fix(package.json): update openai package version to 4.56.0 for compatibility
2024-08-20 12:04:07 +03:00
di-sukharev
71354e4687 feat: add CommandsEnum to define command constants for better maintainability
refactor(generateCommitMessageFromGitDiff): update types for OpenAI messages to improve type safety
fix(commitlint/config): remove optional chaining for OCO_LANGUAGE to ensure proper access
refactor(commitlint/prompts): update types for OpenAI messages to improve type safety
refactor(prompts): update types for OpenAI messages to improve type safety
2024-08-20 12:03:40 +03:00
di-sukharev
8f85ee8f8e refactor(testAi.ts): update import statements to use OpenAI type for better clarity and maintainability
fix(testAi.ts): change parameter type in generateCommitMessage method to align with OpenAI's updated type definitions
2024-08-20 12:01:51 +03:00
di-sukharev
f9103a3c6a build 2024-08-20 12:01:38 +03:00
di-sukharev
4afd7de7a8 feat(commands): add COMMANDS enum to standardize command names across the application
refactor(commit.ts): restructure generateCommitMessageFromGitDiff function to use an interface for parameters and improve readability
fix(config.ts): update DEFAULT_TOKEN_LIMITS to correct values for max tokens input and output
chore(config.ts): enhance config validation to handle undefined and null values more effectively
style(commit.ts): improve formatting and consistency in the commit confirmation logic
style(config.ts): clean up error messages and improve clarity in config setting process
2024-08-20 12:01:14 +03:00
di-sukharev
5cfa3cded2 feat(engine): refactor AI engine interfaces and implementations to support multiple AI providers and improve configurability
- Introduce `AiEngineConfig` interface for consistent configuration across AI engines.
- Update `generateCommitMessage` method signatures to use `OpenAIClient.Chat.Completions.ChatCompletionMessageParam`.
- Implement specific configurations for each AI provider (Anthropic, Azure, Gemini, Ollama, OpenAI) to enhance flexibility.
- Replace hardcoded values with configurable parameters for model, API key, and token limits.
- Refactor client initialization to use Axios instances for better HTTP request handling.
- Remove deprecated code and improve error handling for better user feedback.
2024-08-20 11:58:19 +03:00
di-sukharev
bb0b0e804e build 2024-08-20 11:56:44 +03:00
di-sukharev
5d87cc514b feat(ENUMS.ts): add ENUMS file to centralize command constants
refactor(commitlint.ts): update import path to use ENUMS for command constants
refactor(config.ts): update import path to use ENUMS for command constants
refactor(githook.ts): update import path to use ENUMS for command constants
fix(prompts.ts): correct conventional commit keywords instruction text
2024-08-19 14:09:27 +03:00
di-sukharev
6f4e8fde93 docs(README.md): update usage examples to remove redundant 'opencommit' command
chore(example.txt): remove unused example.txt file
fix(config.ts): correct import order and improve validation messages
fix(githook.ts): improve error message for unsupported mode
fix(azure.ts): add non-null assertion for message content
fix(gemini.ts): use strict equality for role comparison
refactor(generateCommitMessageFromGitDiff.ts): reorder imports for consistency
refactor(github-action.ts): reorder imports for consistency
refactor(prompts.ts): simplify prompt content generation and improve readability
style(engine.ts): fix inconsistent spacing and import order
2024-08-19 14:00:08 +03:00
di-sukharev
745bb5218f update imports 2024-08-19 13:09:46 +03:00
72 changed files with 14003 additions and 66014 deletions

View File

@@ -18,7 +18,7 @@ To get started, follow these steps:
1. Clone the project repository locally. 1. Clone the project repository locally.
2. Install dependencies with `npm install`. 2. Install dependencies with `npm install`.
3. Run the project with `npm run dev`. 3. Run the project with `npm run dev`.
4. See [issues](https://github.com/di-sukharev/opencommit/issues) or [TODO.md](TODO.md) to help the project. 4. See [issues](https://github.com/di-sukharev/open-commit/issues) or [TODO.md](../TODO.md) to help the project.
## Commit message guidelines ## Commit message guidelines
@@ -30,7 +30,7 @@ If you encounter any issues while using the project, please report them on the G
## Contacts ## Contacts
If you have any questions about contributing to the project, please contact by [creating an issue](https://github.com/di-sukharev/opencommit/issues) on the GitHub issue tracker. If you have any questions about contributing to the project, please contact by [creating an issue](https://github.com/di-sukharev/open-commit/issues) on the GitHub issue tracker.
## License ## License

View File

@@ -40,11 +40,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -58,7 +58,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v3 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -71,6 +71,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v2
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -15,6 +15,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v3 uses: actions/dependency-review-action@v2

View File

@@ -1,11 +1,6 @@
name: Testing name: Testing
on: on: [pull_request]
pull_request:
push:
branches:
- master
- main
jobs: jobs:
unit-test: unit-test:
@@ -14,12 +9,11 @@ jobs:
matrix: matrix:
node-version: [20.x] node-version: [20.x]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3 uses: actions/setup-node@v2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Run Unit Tests - name: Run Unit Tests
@@ -30,12 +24,11 @@ jobs:
matrix: matrix:
node-version: [20.x] node-version: [20.x]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3 uses: actions/setup-node@v2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install git - name: Install git
run: | run: |
sudo apt-get update sudo apt-get update
@@ -51,21 +44,3 @@ jobs:
run: npm run build run: npm run build
- name: Run E2E Tests - name: Run E2E Tests
run: npm run test:e2e run: npm run test:e2e
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run Prettier
run: npm run format:check
- name: Prettier Output
if: failure()
run: |
echo "Prettier check failed. Please run 'npm run format' to fix formatting issues."
exit 1

3
.gitignore vendored
View File

@@ -11,5 +11,4 @@ uncaughtExceptions.log
src/*.json src/*.json
.idea .idea
test.ts test.ts
notes.md notes.md
.nvmrc

View File

@@ -28,19 +28,30 @@ You can use OpenCommit by simply running it via the CLI like this `oco`. 2 secon
npm install -g opencommit npm install -g 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. Alternatively run it via `npx opencommit` or `bunx opencommit`
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_API_KEY=<your_api_key> oco config set OCO_OPENAI_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 with `oco` command to generate a commit message for your staged changes: You can call OpenCommit directly 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...>
@@ -59,17 +70,21 @@ 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_MODEL='llama3:8b' oco config set OCO_AI_PROVIDER='ollama'
``` ```
Default model is `mistral`. 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:
```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_API_URL` environment variable as follows: You can do so by setting the `OCO_OLLAMA_API_URL` environment variable as follows:
```sh ```sh
oco config set OCO_API_URL='http://192.168.1.10:11434/api/chat' oco config set OCO_OLLAMA_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.
@@ -106,22 +121,22 @@ Create a `.env` file and add OpenCommit config variables there like this:
```env ```env
... ...
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama, gemini, flowise, deepseek, aimlapi> OCO_OPENAI_API_KEY=<your OpenAI API token>
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_API_CUSTOM_HEADERS=<JSON string of custom HTTP headers to include in API requests>
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-mini' (default), 'gpt-4o', 'gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo', '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_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_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>
...
``` ```
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`. This are not all the config options, but you get the point.
### Global config for all repos ### Global config for all repos
@@ -133,18 +148,6 @@ Simply set any of the variables above like this:
oco config set OCO_MODEL=gpt-4o-mini oco config set OCO_MODEL=gpt-4o-mini
``` ```
To see all available configuration parameters and their accepted values:
```sh
oco config describe
```
To see details for a specific parameter:
```sh
oco config describe OCO_MODEL
```
Configure [GitMoji](https://gitmoji.dev/) to preface a message. Configure [GitMoji](https://gitmoji.dev/) to preface a message.
```sh ```sh
@@ -159,16 +162,6 @@ 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.
@@ -185,26 +178,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 other LLM providers with a custom URL ### Switch to Azure OpenAI
By default OpenCommit uses [OpenAI](https://openai.com). By default OpenCommit uses [OpenAI](https://openai.com).
You could switch to [Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/) or Flowise or Ollama. You could switch to [Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/)🚀
```sh ```sh
oco config set OCO_AI_PROVIDER=azure OCO_API_KEY=<your_azure_api_key> OCO_API_URL=<your_azure_endpoint> opencommit config set OCO_AI_PROVIDER=azure
oco config set OCO_AI_PROVIDER=flowise OCO_API_KEY=<your_flowise_api_key> OCO_API_URL=<your_flowise_endpoint>
oco config set OCO_AI_PROVIDER=ollama OCO_API_KEY=<your_ollama_api_key> OCO_API_URL=<your_ollama_endpoint>
``` ```
Of course need to set 'OCO_OPENAI_API_KEY'. And also need to set the
'OPENAI_BASE_PATH' for the endpoint and set the deployment name to
'model'.
### 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
@@ -220,14 +213,12 @@ 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 for pushing to git is on by default but if you would like to turn it off just use: A prompt to ushing 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.
@@ -402,7 +393,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_API_KEY: ${{ secrets.OCO_API_KEY }} OCO_OPENAI_API_KEY: ${{ secrets.OCO_OPENAI_API_KEY }}
# customization # customization
OCO_TOKENS_MAX_INPUT: 4096 OCO_TOKENS_MAX_INPUT: 4096

View File

@@ -9,33 +9,19 @@ const config: Config = {
testTimeout: 100_000, testTimeout: 100_000,
coverageProvider: 'v8', coverageProvider: 'v8',
moduleDirectories: ['node_modules', 'src'], moduleDirectories: ['node_modules', 'src'],
preset: 'ts-jest/presets/default-esm', preset: 'ts-jest/presets/js-with-ts-esm',
setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'], setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'],
testEnvironment: 'node', testEnvironment: 'node',
testRegex: ['.*\\.test\\.ts$'], testRegex: ['.*\\.test\\.ts$'],
// Tell Jest to ignore the specific duplicate package.json files transformIgnorePatterns: ['node_modules/(?!cli-testing-library)'],
// that are causing Haste module naming collisions
modulePathIgnorePatterns: [
'<rootDir>/test/e2e/prompt-module/data/'
],
transformIgnorePatterns: [
'node_modules/(?!(cli-testing-library|@clack|cleye)/.*)'
],
transform: { transform: {
'^.+\\.(ts|tsx|js|jsx|mjs)$': [ '^.+\\.(ts|tsx)$': [
'ts-jest', 'ts-jest',
{ {
diagnostics: false, diagnostics: false,
useESM: true, useESM: true
tsconfig: {
module: 'ESNext',
target: 'ES2022'
}
} }
] ]
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1'
} }
}; };

31310
out/cli.cjs

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4902
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "opencommit", "name": "opencommit",
"version": "3.2.10", "version": "3.0.20",
"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",
@@ -17,11 +17,11 @@
], ],
"main": "cli.js", "main": "cli.js",
"bin": { "bin": {
"opencommit": "out/cli.cjs", "opencommit": "./out/cli.cjs",
"oco": "out/cli.cjs" "oco": "./out/cli.cjs"
}, },
"repository": { "repository": {
"url": "git+https://github.com/di-sukharev/opencommit.git" "url": "https://github.com/di-sukharev/opencommit"
}, },
"type": "module", "type": "module",
"author": "https://github.com/di-sukharev", "author": "https://github.com/di-sukharev",
@@ -44,14 +44,12 @@
"ollama:start": "OCO_AI_PROVIDER='ollama' node ./out/cli.cjs", "ollama:start": "OCO_AI_PROVIDER='ollama' node ./out/cli.cjs",
"dev": "ts-node ./src/cli.ts", "dev": "ts-node ./src/cli.ts",
"dev:gemini": "OCO_AI_PROVIDER='gemini' ts-node ./src/cli.ts", "dev:gemini": "OCO_AI_PROVIDER='gemini' ts-node ./src/cli.ts",
"build": "npx 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 publish --tag latest", "deploy": "npm run build:push && git push --tags && npm publish --tag latest",
"deploy:build": "npm run build:push && git push --tags && npm run deploy", "deploy:patch": "npm version patch && 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",
"format:check": "prettier --check 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",
"test:all": "npm run test:unit:docker && npm run test:e2e:docker", "test:all": "npm run test:unit:docker && npm run test:e2e:docker",
"test:docker-build": "docker build -t oco-test -f test/Dockerfile .", "test:docker-build": "docker build -t oco-test -f test/Dockerfile .",
@@ -59,8 +57,7 @@
"test:unit:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:unit", "test:unit:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:unit",
"test:e2e": "npm run test:e2e:setup && jest test/e2e", "test:e2e": "npm run test:e2e:setup && jest test/e2e",
"test:e2e:setup": "sh test/e2e/setup.sh", "test:e2e:setup": "sh test/e2e/setup.sh",
"test:e2e:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:e2e", "test:e2e:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:e2e"
"mlx:start": "OCO_AI_PROVIDER='mlx' node ./out/cli.cjs"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/types": "^17.4.4", "@commitlint/types": "^17.4.4",
@@ -68,15 +65,14 @@
"@types/inquirer": "^9.0.3", "@types/inquirer": "^9.0.3",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/node": "^16.18.14", "@types/node": "^16.18.14",
"@typescript-eslint/eslint-plugin": "^8.29.0", "@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^8.29.0", "@typescript-eslint/parser": "^5.45.0",
"cli-testing-library": "^2.0.2", "cli-testing-library": "^2.0.2",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"esbuild": "^0.25.5", "esbuild": "^0.15.18",
"eslint": "^9.24.0", "eslint": "^8.28.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"prettier": "^2.8.4", "prettier": "^2.8.4",
"rimraf": "^6.0.1",
"ts-jest": "^29.1.2", "ts-jest": "^29.1.2",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^4.9.3" "typescript": "^4.9.3"
@@ -84,15 +80,15 @@
"dependencies": { "dependencies": {
"@actions/core": "^1.10.0", "@actions/core": "^1.10.0",
"@actions/exec": "^1.1.1", "@actions/exec": "^1.1.1",
"@actions/github": "^6.0.1", "@actions/github": "^5.1.1",
"@anthropic-ai/sdk": "^0.19.2", "@anthropic-ai/sdk": "^0.19.2",
"@azure/openai": "^1.0.0-beta.12", "@azure/openai": "^1.0.0-beta.12",
"@clack/prompts": "^0.6.1", "@clack/prompts": "^0.6.1",
"@dqbd/tiktoken": "^1.0.2", "@dqbd/tiktoken": "^1.0.2",
"@google/generative-ai": "^0.11.4", "@google/generative-ai": "^0.11.4",
"@mistralai/mistralai": "^1.3.5",
"@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",
@@ -101,12 +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.57.0", "openai": "^4.56.0"
"punycode": "^2.3.1",
"zod": "^3.23.8"
},
"overrides": {
"ajv": "^8.17.1",
"whatwg-url": "^14.0.0"
} }
} }

View File

@@ -9,7 +9,6 @@ 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);
@@ -19,17 +18,7 @@ cli(
name: 'opencommit', name: 'opencommit',
commands: [configCommand, hookCommand, commitlintConfigCommand], commands: [configCommand, hookCommand, commitlintConfigCommand],
flags: { flags: {
fgm: { fgm: Boolean,
type: Boolean,
description: 'Use full GitMoji specification',
default: false
},
context: {
type: String,
alias: 'c',
description: 'Additional user input context for the commit message',
default: ''
},
yes: { yes: {
type: Boolean, type: Boolean,
alias: 'y', alias: 'y',
@@ -41,13 +30,12 @@ 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()) {
prepareCommitMessageHook(); prepareCommitMessageHook();
} else { } else {
commit(extraArgs, flags.context, false, flags.fgm, flags.yes); commit(extraArgs, false, flags.fgm, flags.yes);
} }
}, },
extraArgs extraArgs

View File

@@ -1,5 +1,4 @@
import { import {
text,
confirm, confirm,
intro, intro,
isCancel, isCancel,
@@ -40,7 +39,6 @@ const checkMessageTemplate = (extraArgs: string[]): string | false => {
interface GenerateCommitMessageFromGitDiffParams { interface GenerateCommitMessageFromGitDiffParams {
diff: string; diff: string;
extraArgs: string[]; extraArgs: string[];
context?: string;
fullGitMojiSpec?: boolean; fullGitMojiSpec?: boolean;
skipCommitConfirmation?: boolean; skipCommitConfirmation?: boolean;
} }
@@ -48,19 +46,17 @@ interface GenerateCommitMessageFromGitDiffParams {
const generateCommitMessageFromGitDiff = async ({ const generateCommitMessageFromGitDiff = async ({
diff, diff,
extraArgs, extraArgs,
context = '',
fullGitMojiSpec = false, fullGitMojiSpec = false,
skipCommitConfirmation = false skipCommitConfirmation = false
}: GenerateCommitMessageFromGitDiffParams): Promise<void> => { }: GenerateCommitMessageFromGitDiffParams): Promise<void> => {
await assertGitRepo(); await assertGitRepo();
const commitGenerationSpinner = spinner(); const commitSpinner = spinner();
commitGenerationSpinner.start('Generating the commit message'); commitSpinner.start('Generating the commit message');
try { try {
let commitMessage = await generateCommitMessageByDiff( let commitMessage = await generateCommitMessageByDiff(
diff, diff,
fullGitMojiSpec, fullGitMojiSpec
context
); );
const messageTemplate = checkMessageTemplate(extraArgs); const messageTemplate = checkMessageTemplate(extraArgs);
@@ -77,7 +73,7 @@ const generateCommitMessageFromGitDiff = async ({
); );
} }
commitGenerationSpinner.stop('📝 Commit message generated'); commitSpinner.stop('📝 Commit message generated');
outro( outro(
`Generated commit message: `Generated commit message:
@@ -86,62 +82,38 @@ ${commitMessage}
${chalk.grey('——————————————————')}` ${chalk.grey('——————————————————')}`
); );
const userAction = skipCommitConfirmation const isCommitConfirmedByUser =
? 'Yes' skipCommitConfirmation ||
: await select({ (await confirm({
message: 'Confirm the commit message?', message: 'Confirm the commit message?'
options: [ }));
{ value: 'Yes', label: 'Yes' },
{ value: 'No', label: 'No' },
{ value: 'Edit', label: 'Edit' }
]
});
if (isCancel(userAction)) process.exit(1); if (isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
if (userAction === 'Edit') {
const textResponse = await text({
message: 'Please edit the commit message: (press Enter to continue)',
initialValue: commitMessage
});
commitMessage = textResponse.toString();
}
if (userAction === 'Yes' || userAction === 'Edit') {
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(
`${chalk.green('✔')} Successfully committed` outro(`${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) { if (remotes.length === 1 && config.OCO_GITPUSH !== true) {
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 (isCancel(isPushConfirmedByUser)) process.exit(1); if (isPushConfirmedByUser && !isCancel(isPushConfirmedByUser)) {
if (isPushConfirmedByUser) {
const pushSpinner = spinner(); const pushSpinner = spinner();
pushSpinner.start(`Running 'git push ${remotes[0]}'`); pushSpinner.start(`Running 'git push ${remotes[0]}'`);
@@ -164,41 +136,33 @@ ${chalk.grey('——————————————————')}`
process.exit(0); process.exit(0);
} }
} else { } else {
const skipOption = `don't push`;
const selectedRemote = (await select({ const selectedRemote = (await select({
message: 'Choose a remote to push to', message: 'Choose a remote to push to',
options: [...remotes, skipOption].map((remote) => ({ options: remotes.map((remote) => ({ value: remote, label: remote }))
value: remote,
label: remote
}))
})) as string; })) as string;
if (isCancel(selectedRemote)) process.exit(1); if (!isCancel(selectedRemote)) {
if (selectedRemote !== skipOption) {
const pushSpinner = spinner(); const pushSpinner = spinner();
pushSpinner.start(`Running 'git push ${selectedRemote}'`); pushSpinner.start(`Running 'git push ${selectedRemote}'`);
const { stdout } = await execa('git', ['push', selectedRemote]); const { stdout } = await execa('git', ['push', selectedRemote]);
if (stdout) outro(stdout);
pushSpinner.stop( pushSpinner.stop(
`${chalk.green( `${chalk.green(
'✔' '✔'
)} successfully pushed all commits to ${selectedRemote}` )} Successfully pushed all commits to ${selectedRemote}`
); );
}
if (stdout) outro(stdout);
} else outro(`${chalk.gray('✖')} process cancelled`);
} }
} 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,
@@ -207,11 +171,7 @@ ${chalk.grey('——————————————————')}`
} }
} }
} catch (error) { } catch (error) {
commitGenerationSpinner.stop( commitSpinner.stop('📝 Commit message generated');
`${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}`);
@@ -221,7 +181,6 @@ ${chalk.grey('——————————————————')}`
export async function commit( export async function commit(
extraArgs: string[] = [], extraArgs: string[] = [],
context: string = '',
isStageAllFlag: Boolean = false, isStageAllFlag: Boolean = false,
fullGitMojiSpec: boolean = false, fullGitMojiSpec: boolean = false,
skipCommitConfirmation: boolean = false skipCommitConfirmation: boolean = false
@@ -254,18 +213,18 @@ export async function commit(
stagedFilesSpinner.start('Counting staged files'); stagedFilesSpinner.start('Counting staged files');
if (stagedFiles.length === 0) { if (!stagedFiles.length) {
stagedFilesSpinner.stop('No files are staged'); stagedFilesSpinner.stop('No files are staged');
const isStageAllAndCommitConfirmedByUser = await confirm({ const isStageAllAndCommitConfirmedByUser = await confirm({
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 (isCancel(isStageAllAndCommitConfirmedByUser)) process.exit(1); if (
isStageAllAndCommitConfirmedByUser &&
if (isStageAllAndCommitConfirmedByUser) { !isCancel(isStageAllAndCommitConfirmedByUser)
await commit(extraArgs, context, true, fullGitMojiSpec); ) {
process.exit(0); await commit(extraArgs, true, fullGitMojiSpec);
process.exit(1);
} }
if (stagedFiles.length === 0 && changedFiles.length > 0) { if (stagedFiles.length === 0 && changedFiles.length > 0) {
@@ -277,13 +236,13 @@ export async function commit(
})) }))
})) as string[]; })) as string[];
if (isCancel(files)) process.exit(0); if (isCancel(files)) process.exit(1);
await gitAdd({ files }); await gitAdd({ files });
} }
await commit(extraArgs, context, false, fullGitMojiSpec); await commit(extraArgs, false, fullGitMojiSpec);
process.exit(0); process.exit(1);
} }
stagedFilesSpinner.stop( stagedFilesSpinner.stop(
@@ -296,7 +255,6 @@ export async function commit(
generateCommitMessageFromGitDiff({ generateCommitMessageFromGitDiff({
diff: await getDiff({ files: stagedFiles }), diff: await getDiff({ files: stagedFiles }),
extraArgs, extraArgs,
context,
fullGitMojiSpec, fullGitMojiSpec,
skipCommitConfirmation skipCommitConfirmation
}) })

View File

@@ -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(JSON.stringify(commitLintConfig, null, 2)); outro(commitLintConfig.toString());
return; return;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -39,11 +39,14 @@ export const prepareCommitMessageHook = async (
const config = getConfig(); const config = getConfig();
if (!config.OCO_API_KEY) { if (
outro( !config.OCO_OPENAI_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_ANTHROPIC_API_KEY &&
!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();
@@ -56,14 +59,10 @@ export const prepareCommitMessageHook = async (
const fileContent = await fs.readFile(messageFilePath); const fileContent = await fs.readFile(messageFilePath);
const messageWithComment = `# ${commitMessage}\n\n# ---------- [OpenCommit] ---------- #\n# Remove the # above to use this generated commit message.\n# To cancel the commit, just close this window without making any changes.\n\n${fileContent.toString()}`; await fs.writeFile(
const messageWithoutComment = `${commitMessage}\n\n${fileContent.toString()}`; messageFilePath,
commitMessage + '\n' + fileContent.toString()
const message = config.OCO_HOOK_AUTO_UNCOMMENT );
? messageWithoutComment
: messageWithComment;
await fs.writeFile(messageFilePath, message);
} catch (error) { } catch (error) {
outro(`${chalk.red('✖')} ${error}`); outro(`${chalk.red('✖')} ${error}`);
process.exit(1); process.exit(1);

View File

@@ -3,7 +3,6 @@ import { OpenAIClient as AzureOpenAIClient } from '@azure/openai';
import { GoogleGenerativeAI as GeminiClient } from '@google/generative-ai'; import { GoogleGenerativeAI as GeminiClient } from '@google/generative-ai';
import { AxiosInstance as RawAxiosClient } from 'axios'; import { AxiosInstance as RawAxiosClient } from 'axios';
import { OpenAI as OpenAIClient } from 'openai'; import { OpenAI as OpenAIClient } from 'openai';
import { Mistral as MistralClient } from '@mistralai/mistralai';
export interface AiEngineConfig { export interface AiEngineConfig {
apiKey: string; apiKey: string;
@@ -11,7 +10,6 @@ export interface AiEngineConfig {
maxTokensOutput: number; maxTokensOutput: number;
maxTokensInput: number; maxTokensInput: number;
baseURL?: string; baseURL?: string;
customHeaders?: Record<string, string>;
} }
type Client = type Client =
@@ -19,8 +17,7 @@ type Client =
| AzureOpenAIClient | AzureOpenAIClient
| AnthropicClient | AnthropicClient
| RawAxiosClient | RawAxiosClient
| GeminiClient | GeminiClient;
| MistralClient;
export interface AiEngine { export interface AiEngine {
config: AiEngineConfig; config: AiEngineConfig;

View File

@@ -1,47 +0,0 @@
import OpenAI from 'openai';
import axios, { AxiosInstance } from 'axios';
import { AiEngine, AiEngineConfig } from './Engine';
interface AimlApiConfig extends AiEngineConfig {}
export class AimlApiEngine implements AiEngine {
client: AxiosInstance;
constructor(public config: AimlApiConfig) {
this.client = axios.create({
baseURL: config.baseURL || 'https://api.aimlapi.com/v1/chat/completions',
headers: {
Authorization: `Bearer ${config.apiKey}`,
'HTTP-Referer': 'https://github.com/di-sukharev/opencommit',
'X-Title': 'opencommit',
'Content-Type': 'application/json',
...config.customHeaders
}
});
}
public generateCommitMessage = async (
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | null> => {
try {
const response = await this.client.post('', {
model: this.config.model,
messages
});
const message = response.data.choices?.[0]?.message;
return message?.content ?? null;
} catch (error) {
const err = error as Error;
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const apiError = error.response.data.error;
if (apiError) throw new Error(apiError.message);
}
throw err;
}
};
}

View File

@@ -8,7 +8,6 @@ import axios from 'axios';
import chalk from 'chalk'; import chalk from 'chalk';
import { OpenAI } from 'openai'; import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff'; import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount'; import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine'; import { AiEngine, AiEngineConfig } from './Engine';
@@ -55,8 +54,8 @@ export class AnthropicEngine implements AiEngine {
const data = await this.client.messages.create(params); const data = await this.client.messages.create(params);
const message = data?.content[0].text; const message = data?.content[0].text;
let content = message;
return removeContentTags(content, 'think'); return message;
} catch (error) { } catch (error) {
const err = error as Error; const err = error as Error;
outro(`${chalk.red('✖')} ${err?.message || err}`); outro(`${chalk.red('✖')} ${err?.message || err}`);

View File

@@ -7,7 +7,6 @@ import axios from 'axios';
import chalk from 'chalk'; import chalk from 'chalk';
import { OpenAI } from 'openai'; import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff'; import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount'; import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine'; import { AiEngine, AiEngineConfig } from './Engine';
@@ -53,9 +52,7 @@ export class AzureEngine implements AiEngine {
if (message?.content === null) { if (message?.content === null) {
return undefined; return undefined;
} }
return message?.content;
let content = message?.content;
return removeContentTags(content, 'think');
} catch (error) { } catch (error) {
outro(`${chalk.red('✖')} ${this.config.model}`); outro(`${chalk.red('✖')} ${this.config.model}`);

View File

@@ -1,61 +0,0 @@
import axios from 'axios';
import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount';
import { OpenAiEngine, OpenAiConfig } from './openAi';
export interface DeepseekConfig extends OpenAiConfig {}
export class DeepseekEngine extends OpenAiEngine {
constructor(config: DeepseekConfig) {
// Call OpenAIEngine constructor with forced Deepseek baseURL
super({
...config,
baseURL: 'https://api.deepseek.com/v1'
});
}
// Identical method from OpenAiEngine, re-implemented here
public generateCommitMessage = async (
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | null> => {
const params = {
model: this.config.model,
messages,
temperature: 0,
top_p: 0.1,
max_tokens: this.config.maxTokensOutput
};
try {
const REQUEST_TOKENS = messages
.map((msg) => tokenCount(msg.content as string) + 4)
.reduce((a, b) => a + b, 0);
if (
REQUEST_TOKENS >
this.config.maxTokensInput - this.config.maxTokensOutput
)
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
const completion = await this.client.chat.completions.create(params);
const message = completion.choices[0].message;
let content = message?.content;
return removeContentTags(content, 'think');
} catch (error) {
const err = error as Error;
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const openAiError = error.response.data.error;
if (openAiError) throw new Error(openAiError.message);
}
throw err;
}
};
}

View File

@@ -1,11 +1,10 @@
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
import { OpenAI } from 'openai'; import { OpenAI } from 'openai';
import { removeContentTags } from '../utils/removeContentTags';
import { AiEngine, AiEngineConfig } from './Engine'; import { AiEngine, AiEngineConfig } from './Engine';
interface FlowiseAiConfig extends AiEngineConfig {} interface FlowiseAiConfig extends AiEngineConfig {}
export class FlowiseEngine implements AiEngine { export class FlowiseAi implements AiEngine {
config: FlowiseAiConfig; config: FlowiseAiConfig;
client: AxiosInstance; client: AxiosInstance;
@@ -37,8 +36,7 @@ export class FlowiseEngine implements AiEngine {
try { try {
const response = await this.client.post('', payload); const response = await this.client.post('', payload);
const message = response.data; const message = response.data;
let content = message?.text; return message?.text;
return removeContentTags(content, 'think');
} catch (err: any) { } catch (err: any) {
const message = err.response?.data?.error ?? err.message; const message = err.response?.data?.error ?? err.message;
throw new Error('local model issues. details: ' + message); throw new Error('local model issues. details: ' + message);

View File

@@ -7,12 +7,11 @@ import {
} from '@google/generative-ai'; } from '@google/generative-ai';
import axios from 'axios'; import axios from 'axios';
import { OpenAI } from 'openai'; import { OpenAI } from 'openai';
import { removeContentTags } from '../utils/removeContentTags';
import { AiEngine, AiEngineConfig } from './Engine'; import { AiEngine, AiEngineConfig } from './Engine';
interface GeminiConfig extends AiEngineConfig {} interface GeminiConfig extends AiEngineConfig {}
export class GeminiEngine implements AiEngine { export class Gemini implements AiEngine {
config: GeminiConfig; config: GeminiConfig;
client: GoogleGenerativeAI; client: GoogleGenerativeAI;
@@ -72,8 +71,7 @@ export class GeminiEngine implements AiEngine {
} }
}); });
const content = result.response.text(); return result.response.text();
return removeContentTags(content, 'think');
} catch (error) { } catch (error) {
const err = error as Error; const err = error as Error;
if ( if (

View File

@@ -1,10 +0,0 @@
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);
}
}

View File

@@ -1,79 +0,0 @@
import axios from 'axios';
import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine';
// Using any for Mistral types to avoid type declaration issues
export interface MistralAiConfig extends AiEngineConfig {}
export type MistralCompletionMessageParam = Array<any>;
// Import Mistral dynamically to avoid TS errors
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Mistral = require('@mistralai/mistralai').Mistral;
export class MistralAiEngine implements AiEngine {
config: MistralAiConfig;
client: any; // Using any type for Mistral client to avoid TS errors
constructor(config: MistralAiConfig) {
this.config = config;
if (!config.baseURL) {
this.client = new Mistral({ apiKey: config.apiKey });
} else {
this.client = new Mistral({
apiKey: config.apiKey,
serverURL: config.baseURL
});
}
}
public generateCommitMessage = async (
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | null> => {
const params = {
model: this.config.model,
messages: messages as MistralCompletionMessageParam,
topP: 0.1,
maxTokens: this.config.maxTokensOutput
};
try {
const REQUEST_TOKENS = messages
.map((msg) => tokenCount(msg.content as string) + 4)
.reduce((a, b) => a + b, 0);
if (
REQUEST_TOKENS >
this.config.maxTokensInput - this.config.maxTokensOutput
)
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
const completion = await this.client.chat.complete(params);
if (!completion.choices) throw Error('No completion choice available.');
const message = completion.choices[0].message;
if (!message || !message.content)
throw Error('No completion choice available.');
let content = message.content as string;
return removeContentTags(content, 'think');
} catch (error) {
const err = error as Error;
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const mistralError = error.response.data.error;
if (mistralError) throw new Error(mistralError.message);
}
throw err;
}
};
}

View File

@@ -1,47 +0,0 @@
import axios, { AxiosInstance } from 'axios';
import { OpenAI } from 'openai';
import { removeContentTags } from '../utils/removeContentTags';
import { AiEngine, AiEngineConfig } from './Engine';
interface MLXConfig extends AiEngineConfig {}
export class MLXEngine implements AiEngine {
config: MLXConfig;
client: AxiosInstance;
constructor(config) {
this.config = config;
this.client = axios.create({
url: config.baseURL
? `${config.baseURL}/${config.apiKey}`
: 'http://localhost:8080/v1/chat/completions',
headers: { 'Content-Type': 'application/json' }
});
}
async generateCommitMessage(
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | undefined> {
const params = {
messages,
temperature: 0,
top_p: 0.1,
repetition_penalty: 1.5,
stream: false
};
try {
const response = await this.client.post(
this.client.getUri(this.config),
params
);
const choices = response.data.choices;
const message = choices[0].message;
let content = message?.content;
return removeContentTags(content, 'think');
} catch (err: any) {
const message = err.response?.data?.error ?? err.message;
throw new Error(`MLX provider error: ${message}`);
}
}
}

View File

@@ -1,28 +1,20 @@
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
import { OpenAI } from 'openai'; import { OpenAI } from 'openai';
import { removeContentTags } from '../utils/removeContentTags';
import { AiEngine, AiEngineConfig } from './Engine'; import { AiEngine, AiEngineConfig } from './Engine';
interface OllamaConfig extends AiEngineConfig {} interface OllamaConfig extends AiEngineConfig {}
export class OllamaEngine implements AiEngine { export class OllamaAi implements AiEngine {
config: OllamaConfig; config: OllamaConfig;
client: AxiosInstance; client: AxiosInstance;
constructor(config) { constructor(config) {
this.config = config; this.config = config;
// Combine base headers with custom headers
const headers = {
'Content-Type': 'application/json',
...config.customHeaders
};
this.client = axios.create({ this.client = axios.create({
url: config.baseURL url: config.baseURL
? `${config.baseURL}/${config.apiKey}` ? `${config.baseURL}/${config.apiKey}`
: 'http://localhost:11434/api/chat', : 'http://localhost:11434/api/chat',
headers headers: { 'Content-Type': 'application/json' }
}); });
} }
@@ -36,14 +28,11 @@ export class OllamaEngine implements AiEngine {
stream: false stream: false
}; };
try { try {
const response = await this.client.post( const response = await this.client.post('', params);
this.client.getUri(this.config),
params
);
const { message } = response.data; const message = response.data.message;
let content = message?.content;
return removeContentTags(content, 'think'); return message?.content;
} catch (err: any) { } catch (err: any) {
const message = err.response?.data?.error ?? err.message; const message = err.response?.data?.error ?? err.message;
throw new Error(`Ollama provider error: ${message}`); throw new Error(`Ollama provider error: ${message}`);

View File

@@ -1,12 +1,10 @@
import axios from 'axios'; import axios from 'axios';
import { OpenAI } from 'openai'; import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff'; import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { parseCustomHeaders } from '../utils/engine';
import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount'; import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine'; import { AiEngine, AiEngineConfig } from './Engine';
export interface OpenAiConfig extends AiEngineConfig {} interface OpenAiConfig extends AiEngineConfig {}
export class OpenAiEngine implements AiEngine { export class OpenAiEngine implements AiEngine {
config: OpenAiConfig; config: OpenAiConfig;
@@ -14,23 +12,7 @@ export class OpenAiEngine implements AiEngine {
constructor(config: OpenAiConfig) { constructor(config: OpenAiConfig) {
this.config = config; this.config = config;
this.client = new OpenAI({ apiKey: config.apiKey });
const clientOptions: OpenAI.ClientOptions = {
apiKey: config.apiKey
};
if (config.baseURL) {
clientOptions.baseURL = config.baseURL;
}
if (config.customHeaders) {
const headers = parseCustomHeaders(config.customHeaders);
if (Object.keys(headers).length > 0) {
clientOptions.defaultHeaders = headers;
}
}
this.client = new OpenAI(clientOptions);
} }
public generateCommitMessage = async ( public generateCommitMessage = async (
@@ -58,8 +40,8 @@ export class OpenAiEngine implements AiEngine {
const completion = await this.client.chat.completions.create(params); const completion = await this.client.chat.completions.create(params);
const message = completion.choices[0].message; const message = completion.choices[0].message;
let content = message?.content;
return removeContentTags(content, 'think'); return message?.content;
} catch (error) { } catch (error) {
const err = error as Error; const err = error as Error;
if ( if (

View File

@@ -1,49 +0,0 @@
import OpenAI from 'openai';
import { AiEngine, AiEngineConfig } from './Engine';
import axios, { AxiosInstance } from 'axios';
import { removeContentTags } from '../utils/removeContentTags';
interface OpenRouterConfig extends AiEngineConfig {}
export class OpenRouterEngine implements AiEngine {
client: AxiosInstance;
constructor(public config: OpenRouterConfig) {
this.client = axios.create({
baseURL: 'https://openrouter.ai/api/v1/chat/completions',
headers: {
Authorization: `Bearer ${config.apiKey}`,
'HTTP-Referer': 'https://github.com/di-sukharev/opencommit',
'X-Title': 'OpenCommit',
'Content-Type': 'application/json'
}
});
}
public generateCommitMessage = async (
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | null> => {
try {
const response = await this.client.post('', {
model: this.config.model,
messages
});
const message = response.data.choices[0].message;
let content = message?.content;
return removeContentTags(content, 'think');
} catch (error) {
const err = error as Error;
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const openRouterError = error.response.data.error;
if (openRouterError) throw new Error(openRouterError.message);
}
throw err;
}
};
}

View File

@@ -6,18 +6,17 @@ 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 = config.OCO_TOKENS_MAX_INPUT; const MAX_TOKENS_INPUT =
const MAX_TOKENS_OUTPUT = config.OCO_TOKENS_MAX_OUTPUT; config.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
const MAX_TOKENS_OUTPUT =
config.OCO_TOKENS_MAX_OUTPUT ||
DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
const generateCommitMessageChatCompletionPrompt = async ( const generateCommitMessageChatCompletionPrompt = async (
diff: string, diff: string,
fullGitMojiSpec: boolean, fullGitMojiSpec: boolean
context: string
): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => { ): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => {
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt( const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec);
fullGitMojiSpec,
context
);
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT]; const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
@@ -40,14 +39,10 @@ const ADJUSTMENT_FACTOR = 20;
export const generateCommitMessageByDiff = async ( export const generateCommitMessageByDiff = async (
diff: string, diff: string,
fullGitMojiSpec: boolean = false, fullGitMojiSpec: boolean = false
context: string = ''
): Promise<string> => { ): Promise<string> => {
try { try {
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt( const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec);
fullGitMojiSpec,
context
);
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map( const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
(msg) => tokenCount(msg.content as string) + 4 (msg) => tokenCount(msg.content as string) + 4
@@ -77,8 +72,7 @@ export const generateCommitMessageByDiff = async (
const messages = await generateCommitMessageChatCompletionPrompt( const messages = await generateCommitMessageChatCompletionPrompt(
diff, diff,
fullGitMojiSpec, fullGitMojiSpec
context
); );
const engine = getEngine(); const engine = getEngine();

View File

@@ -2,7 +2,5 @@
"localLanguage": "česky", "localLanguage": "česky",
"commitFix": "fix(server.ts): zlepšení velikosti proměnné port na velká písmena PORT", "commitFix": "fix(server.ts): zlepšení velikosti proměnné port na velká písmena PORT",
"commitFeat": "feat(server.ts): přidání podpory pro proměnnou prostředí process.env.PORT", "commitFeat": "feat(server.ts): přidání podpory pro proměnnou prostředí process.env.PORT",
"commitDescription": "Proměnná port se nyní jmenuje PORT, což odpovídá konvenci pojmenování, protože PORT je konstanta. Podpora proměnné prostředí process.env.PORT umožňuje snadnější správu nastavení při spuštění.", "commitDescription": "Proměnná port se nyní jmenuje PORT, což odpovídá konvenci pojmenování, protože PORT je konstanta. Podpora proměnné prostředí process.env.PORT umožňuje snadnější správu nastavení při spuštění."
"commitFixOmitScope": "fix: zlepšení velikosti proměnné port na velká písmena PORT",
"commitFeatOmitScope": "feat: přidání podpory pro proměnnou prostředí process.env.PORT"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "Deutsch", "localLanguage": "Deutsch",
"commitFix": "fix(server.ts): Ändere die Groß- und Kleinschreibung der Port-Variable von Kleinbuchstaben auf Großbuchstaben PORT.", "commitFix": "fix(server.ts): Ändere die Groß- und Kleinschreibung der Port-Variable von Kleinbuchstaben auf Großbuchstaben PORT.",
"commitFeat": "Funktion(server.ts): Unterstützung für die Umgebungsvariable process.env.PORT hinzufügen", "commitFeat": "Funktion(server.ts): Unterstützung für die Umgebungsvariable process.env.PORT hinzufügen",
"commitDescription": "Die Port-Variable heißt jetzt PORT, was die Konsistenz mit den Namenskonventionen verbessert, da PORT eine Konstante ist. Die Unterstützung für eine Umgebungsvariable ermöglicht es der Anwendung, flexibler zu sein, da sie jetzt auf jedem verfügbaren Port laufen kann, der über die Umgebungsvariable process.env.PORT angegeben wird.", "commitDescription": "Die Port-Variable heißt jetzt PORT, was die Konsistenz mit den Namenskonventionen verbessert, da PORT eine Konstante ist. Die Unterstützung für eine Umgebungsvariable ermöglicht es der Anwendung, flexibler zu sein, da sie jetzt auf jedem verfügbaren Port laufen kann, der über die Umgebungsvariable process.env.PORT angegeben wird."
"commitFixOmitScope": "fix: Ändere die Groß- und Kleinschreibung der Port-Variable von Kleinbuchstaben auf Großbuchstaben PORT.",
"commitFeatOmitScope": "Funktion: Unterstützung für die Umgebungsvariable process.env.PORT hinzufügen"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "english", "localLanguage": "english",
"commitFix": "fix(server.ts): change port variable case from lowercase port to uppercase PORT to improve semantics", "commitFix": "fix(server.ts): change port variable case from lowercase port to uppercase PORT to improve semantics",
"commitFeat": "feat(server.ts): add support for process.env.PORT environment variable to be able to run app on a configurable port", "commitFeat": "feat(server.ts): add support for process.env.PORT environment variable to be able to run app on a configurable port",
"commitDescription": "The port variable is now named PORT, which improves consistency with the naming conventions as PORT is a constant. Support for an environment variable allows the application to be more flexible as it can now run on any available port specified via the process.env.PORT environment variable.", "commitDescription": "The port variable is now named PORT, which improves consistency with the naming conventions as PORT is a constant. Support for an environment variable allows the application to be more flexible as it can now run on any available port specified via the process.env.PORT environment variable."
"commitFixOmitScope": "fix: change port variable case from lowercase port to uppercase PORT to improve semantics",
"commitFeatOmitScope": "feat: add support for process.env.PORT environment variable to be able to run app on a configurable port"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "spanish", "localLanguage": "spanish",
"commitFix": "fix(server.ts): cambiar la variable port de minúsculas a mayúsculas PORT", "commitFix": "fix(server.ts): cambiar la variable port de minúsculas a mayúsculas PORT",
"commitFeat": "feat(server.ts): añadir soporte para la variable de entorno process.env.PORT", "commitFeat": "feat(server.ts): añadir soporte para la variable de entorno process.env.PORT",
"commitDescription": "La variable port ahora se llama PORT, lo que mejora la coherencia con las convenciones de nomenclatura, ya que PORT es una constante. El soporte para una variable de entorno permite que la aplicación sea más flexible, ya que ahora puede ejecutarse en cualquier puerto disponible especificado a través de la variable de entorno process.env.PORT.", "commitDescription": "La variable port ahora se llama PORT, lo que mejora la coherencia con las convenciones de nomenclatura, ya que PORT es una constante. El soporte para una variable de entorno permite que la aplicación sea más flexible, ya que ahora puede ejecutarse en cualquier puerto disponible especificado a través de la variable de entorno process.env.PORT."
"commitFixOmitScope": "fix: cambiar la variable port de minúsculas a mayúsculas PORT",
"commitFeatOmitScope": "feat: añadir soporte para la variable de entorno process.env.PORT"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "française", "localLanguage": "française",
"commitFix": "corriger(server.ts) : changer la casse de la variable de port de minuscules à majuscules (PORT)", "commitFix": "corriger(server.ts) : changer la casse de la variable de port de minuscules à majuscules (PORT)",
"commitFeat": "fonctionnalité(server.ts) : ajouter la prise en charge de la variable d'environnement process.env.PORT", "commitFeat": "fonctionnalité(server.ts) : ajouter la prise en charge de la variable d'environnement process.env.PORT",
"commitDescription": "La variable de port est maintenant nommée PORT, ce qui améliore la cohérence avec les conventions de nommage car PORT est une constante. La prise en charge d'une variable d'environnement permet à l'application d'être plus flexible car elle peut maintenant s'exécuter sur n'importe quel port disponible spécifié via la variable d'environnement process.env.PORT.", "commitDescription": "La variable de port est maintenant nommée PORT, ce qui améliore la cohérence avec les conventions de nommage car PORT est une constante. La prise en charge d'une variable d'environnement permet à l'application d'être plus flexible car elle peut maintenant s'exécuter sur n'importe quel port disponible spécifié via la variable d'environnement process.env.PORT."
"commitFixOmitScope": "corriger : changer la casse de la variable de port de minuscules à majuscules (PORT)",
"commitFeatOmitScope": "fonctionnalité : ajouter la prise en charge de la variable d'environnement process.env.PORT"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "bahasa", "localLanguage": "bahasa",
"commitFix": "fix(server.ts): mengubah huruf port variable dari huruf kecil ke huruf besar PORT", "commitFix": "fix(server.ts): mengubah huruf port variable dari huruf kecil ke huruf besar PORT",
"commitFeat": "feat(server.ts): menambahkan support di process.env.PORT environment variabel", "commitFeat": "feat(server.ts): menambahkan support di process.env.PORT environment variabel",
"commitDescription": "Port variabel bernama PORT, yang membantu konsistensi dengan memberi nama yaitu PORT yang konstan. Bantuan environment variabel membantu aplikasi lebih fleksibel, dan dapat di jalankan di port manapun yang tertulis pada process.env.PORT", "commitDescription": "Port variabel bernama PORT, yang membantu konsistensi dengan memberi nama yaitu PORT yang konstan. Bantuan environment variabel membantu aplikasi lebih fleksibel, dan dapat di jalankan di port manapun yang tertulis pada process.env.PORT"
"commitFixOmitScope": "fix: mengubah huruf port variable dari huruf kecil ke huruf besar PORT",
"commitFeatOmitScope": "feat: menambahkan support di process.env.PORT environment variabel"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "italiano", "localLanguage": "italiano",
"commitFix": "fix(server.ts): cambia la grafia della variabile della porta dal minuscolo port al maiuscolo PORT", "commitFix": "fix(server.ts): cambia la grafia della variabile della porta dal minuscolo port al maiuscolo PORT",
"commitFeat": "feat(server.ts): aggiunge il supporto per la variabile di ambiente process.env.PORT", "commitFeat": "feat(server.ts): aggiunge il supporto per la variabile di ambiente process.env.PORT",
"commitDescription": "La variabile port è ora chiamata PORT, migliorando la coerenza con le convenzioni di denominazione in quanto PORT è una costante. Il supporto per una variabile di ambiente consente all'applicazione di essere più flessibile poiché ora può essere eseguita su qualsiasi porta disponibile specificata tramite la variabile di ambiente process.env.PORT.", "commitDescription": "La variabile port è ora chiamata PORT, migliorando la coerenza con le convenzioni di denominazione in quanto PORT è una costante. Il supporto per una variabile di ambiente consente all'applicazione di essere più flessibile poiché ora può essere eseguita su qualsiasi porta disponibile specificata tramite la variabile di ambiente process.env.PORT."
"commitFixOmitScope": "fix: cambia la grafia della variabile della porta dal minuscolo port al maiuscolo PORT",
"commitFeatOmitScope": "feat: aggiunge il supporto per la variabile di ambiente process.env.PORT"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "日本語", "localLanguage": "日本語",
"commitFix": "修正(server.ts): ポート変数を小文字のportから大文字のPORTに変更", "commitFix": "修正(server.ts): ポート変数を小文字のportから大文字のPORTに変更",
"commitFeat": "新機能(server.ts): 環境変数process.env.PORTのサポートを追加", "commitFeat": "新機能(server.ts): 環境変数process.env.PORTのサポートを追加",
"commitDescription": "ポート変数は現在PORTという名前になり、定数であるPORTを使うことで命名規則に一貫性が生まれました。環境変数をサポートすることで、環境変数process.env.PORTで指定された任意の利用可能なポートで実行できるようになり、アプリケーションはより柔軟になりました。", "commitDescription": "ポート変数は現在PORTという名前になり、定数であるPORTを使うことで命名規則に一貫性が生まれました。環境変数をサポートすることで、環境変数process.env.PORTで指定された任意の利用可能なポートで実行できるようになり、アプリケーションはより柔軟になりました。"
"commitFixOmitScope": "修正: ポート変数を小文字のportから大文字のPORTに変更",
"commitFeatOmitScope": "新機能: 環境変数process.env.PORTのサポートを追加"
} }

View File

@@ -1,8 +1,6 @@
{ {
"localLanguage": "한국어", "localLanguage": "한국어",
"commitFix": "fix(server.ts): 포트 변수를 소문자 port에서 대문자 PORT로 변경", "commitFix": "fix(server.ts): 포트 변수를 소문자 port에서 대문자 PORT로 변경",
"commitFeat": "feat(server.ts): process.env.PORT 환경 변수 지원 추가", "commitFeat": "피트(server.ts): process.env.PORT 환경 변수 지원 추가",
"commitDescription": "포트 변수는 이제 PORT로 이름이 지정되어 상수인 PORT와 일관성 있는 이름 규칙을 따릅니다. 환경 변수 지원을 통해 애플리케이션은 이제 process.env.PORT 환경 변수로 지정된 사용 가능한 모든 포트에서 실행할 수 있으므로 더 유연해졌습니다.", "commitDescription": "포트 변수는 이제 PORT로 이름이 지정되어 상수인 PORT와 일관성 있는 이름 규칙을 따릅니다. 환경 변수 지원을 통해 애플리케이션은 이제 process.env.PORT 환경 변수로 지정된 사용 가능한 모든 포트에서 실행할 수 있으므로 더 유연해졌습니다."
"commitFixOmitScope": "fix: 포트 변수를 소문자 port에서 대문자 PORT로 변경",
"commitFeatOmitScope": "feat: process.env.PORT 환경 변수 지원 추가"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "Nederlands", "localLanguage": "Nederlands",
"commitFix": "fix(server.ts): verander poortvariabele van kleine letters poort naar hoofdletters PORT", "commitFix": "fix(server.ts): verander poortvariabele van kleine letters poort naar hoofdletters PORT",
"commitFeat": "feat(server.ts): voeg ondersteuning toe voor process.env.PORT omgevingsvariabele", "commitFeat": "feat(server.ts): voeg ondersteuning toe voor process.env.PORT omgevingsvariabele",
"commitDescription": "De poortvariabele heet nu PORT, wat de consistentie met de naamgevingsconventies verbetert omdat PORT een constante is. Ondersteuning voor een omgevingsvariabele maakt de applicatie flexibeler, omdat deze nu kan draaien op elke beschikbare poort die is gespecificeerd via de process.env.PORT omgevingsvariabele.", "commitDescription": "De poortvariabele heet nu PORT, wat de consistentie met de naamgevingsconventies verbetert omdat PORT een constante is. Ondersteuning voor een omgevingsvariabele maakt de applicatie flexibeler, omdat deze nu kan draaien op elke beschikbare poort die is gespecificeerd via de process.env.PORT omgevingsvariabele."
"commitFixOmitScope": "fix: verander poortvariabele van kleine letters poort naar hoofdletters PORT",
"commitFeatOmitScope": "feat: voeg ondersteuning toe voor process.env.PORT omgevingsvariabele"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "polski", "localLanguage": "polski",
"commitFix": "fix(server.ts): poprawa wielkości zmiennej port na pisane z dużymi literami PORT", "commitFix": "fix(server.ts): poprawa wielkości zmiennej port na pisane z dużymi literami PORT",
"commitFeat": "feat(server.ts): dodanie obsługi zmiennej środowiskowej process.env.PORT", "commitFeat": "feat(server.ts): dodanie obsługi zmiennej środowiskowej process.env.PORT",
"commitDescription": "Zmienna port jest teraz nazwana PORT, co jest zgodne z konwencją nazewniczą ponieważ PORT jest stałą. Obsługa zmiennej środowiskowej process.env.PORT pozwala łatwiej zarządzać ustawieniami przy starcie.", "commitDescription": "Zmienna port jest teraz nazwana PORT, co jest zgodne z konwencją nazewniczą ponieważ PORT jest stałą. Obsługa zmiennej środowiskowej process.env.PORT pozwala łatwiej zarządzać ustawieniami przy starcie."
"commitFixOmitScope": "fix: poprawa wielkości zmiennej port na pisane z dużymi literami PORT",
"commitFeatOmitScope": "feat: dodanie obsługi zmiennej środowiskowej process.env.PORT"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "português", "localLanguage": "português",
"commitFix": "fix(server.ts): altera o caso da variável de porta de port minúscula para PORT maiúscula", "commitFix": "fix(server.ts): altera o caso da variável de porta de port minúscula para PORT maiúscula",
"commitFeat": "feat(server.ts): adiciona suporte para a variável de ambiente process.env.PORT", "commitFeat": "feat(server.ts): adiciona suporte para a variável de ambiente process.env.PORT",
"commitDescription": "A variável de porta agora é denominada PORT, o que melhora a consistência com as convenções de nomenclatura, pois PORT é uma constante. O suporte para uma variável de ambiente permite que o aplicativo seja mais flexível, pois agora pode ser executado em qualquer porta disponível especificada por meio da variável de ambiente process.env.PORT.", "commitDescription": "A variável de porta agora é denominada PORT, o que melhora a consistência com as convenções de nomenclatura, pois PORT é uma constante. O suporte para uma variável de ambiente permite que o aplicativo seja mais flexível, pois agora pode ser executado em qualquer porta disponível especificada por meio da variável de ambiente process.env.PORT."
"commitFixOmitScope": "fix: altera o caso da variável de porta de port minúscula para PORT maiúscula",
"commitFeatOmitScope": "feat: adiciona suporte para a variável de ambiente process.env.PORT"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "русский", "localLanguage": "русский",
"commitFix": "fix(server.ts): изменение регистра переменной порта с нижнего регистра port на верхний регистр PORT", "commitFix": "fix(server.ts): изменение регистра переменной порта с нижнего регистра port на верхний регистр PORT",
"commitFeat": "feat(server.ts): добавлена поддержка переменной окружения process.env.PORT", "commitFeat": "feat(server.ts): добавлена поддержка переменной окружения process.env.PORT",
"commitDescription": "Переменная port теперь называется PORT, что улучшает согласованность с соглашениями об именовании констант. Поддержка переменной окружения позволяет приложению быть более гибким, запускаясь на любом доступном порту, указанном с помощью переменной окружения process.env.PORT.", "commitDescription": "Переменная port теперь называется PORT, что улучшает согласованность с соглашениями об именовании констант. Поддержка переменной окружения позволяет приложению быть более гибким, запускаясь на любом доступном порту, указанном с помощью переменной окружения process.env.PORT."
"commitFixOmitScope": "fix: изменение регистра переменной порта с нижнего регистра port на верхний регистр PORT",
"commitFeatOmitScope": "feat: добавлена поддержка переменной окружения process.env.PORT"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "svenska", "localLanguage": "svenska",
"commitFix": "fixa(server.ts): ändra variabelnamnet för port från små bokstäver till stora bokstäver PORT", "commitFix": "fixa(server.ts): ändra variabelnamnet för port från små bokstäver till stora bokstäver PORT",
"commitFeat": "nyhet(server.ts): lägg till stöd för process.env.PORT miljövariabel", "commitFeat": "nyhet(server.ts): lägg till stöd för process.env.PORT miljövariabel",
"commitDescription": "Variabeln som innehåller portnumret heter nu PORT vilket förbättrar konsekvensen med namngivningskonventionerna eftersom PORT är en konstant. Stöd för en miljövariabel gör att applikationen kan vara mer flexibel då den nu kan köras på vilken port som helst som specificeras via miljövariabeln process.env.PORT.", "commitDescription": "Variabeln som innehåller portnumret heter nu PORT vilket förbättrar konsekvensen med namngivningskonventionerna eftersom PORT är en konstant. Stöd för en miljövariabel gör att applikationen kan vara mer flexibel då den nu kan köras på vilken port som helst som specificeras via miljövariabeln process.env.PORT."
"commitFixOmitScope": "fixa: ändra variabelnamnet för port från små bokstäver till stora bokstäver PORT",
"commitFeatOmitScope": "nyhet: lägg till stöd för process.env.PORT miljövariabel"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "ไทย", "localLanguage": "ไทย",
"commitFix": "fix(server.ts): เปลี่ยนตัวพิมพ์ของตัวแปร จากตัวพิมพ์เล็ก port เป็นตัวพิมพ์ใหญ่ PORT", "commitFix": "fix(server.ts): เปลี่ยนตัวพิมพ์ของตัวแปร จากตัวพิมพ์เล็ก port เป็นตัวพิมพ์ใหญ่ PORT",
"commitFeat": "feat(server.ts): เพิ่มการรองรับสำหรับตัวแปรสภาพแวดล้อม process.env.PORT", "commitFeat": "feat(server.ts): เพิ่มการรองรับสำหรับตัวแปรสภาพแวดล้อม process.env.PORT",
"commitDescription": "ตอนนี้ตัวแปรพอร์ตมีชื่อว่า PORT, ซึ่งปรับปรุงความสอดคล้องกับหลักการตั้งชื่อเนื่องจาก PORT เป็นค่าคงที่. การสนับสนุนสำหรับตัวแปรสภาพแวดล้อม ช่วยให้แอปพลิเคชันมีความยืดหยุ่นมากขึ้นเนื่องจาก สามารถทำงานบนพอร์ตใด ๆ ตามที่กำหนด ซึ่งระบุผ่านตัวแปรสภาพแวดล้อม process.env.PORT", "commitDescription": "ตอนนี้ตัวแปรพอร์ตมีชื่อว่า PORT, ซึ่งปรับปรุงความสอดคล้องกับหลักการตั้งชื่อเนื่องจาก PORT เป็นค่าคงที่. การสนับสนุนสำหรับตัวแปรสภาพแวดล้อม ช่วยให้แอปพลิเคชันมีความยืดหยุ่นมากขึ้นเนื่องจาก สามารถทำงานบนพอร์ตใด ๆ ตามที่กำหนด ซึ่งระบุผ่านตัวแปรสภาพแวดล้อม process.env.PORT"
"commitFixOmitScope": "fix: เปลี่ยนตัวพิมพ์ของตัวแปร จากตัวพิมพ์เล็ก port เป็นตัวพิมพ์ใหญ่ PORT",
"commitFeatOmitScope": "feat: เพิ่มการรองรับสำหรับตัวแปรสภาพแวดล้อม process.env.PORT"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "Turkish", "localLanguage": "Turkish",
"commitFix": "fix(server.ts): port değişkeni küçük harfli porttan büyük harfli PORT'a değiştirildi", "commitFix": "fix(server.ts): port değişkeni küçük harfli porttan büyük harfli PORT'a değiştirildi",
"commitFeat": "feat(server.ts): process.env.PORT ortam değişkeni için destek eklendi.", "commitFeat": "feat(server.ts): process.env.PORT ortam değişkeni için destek eklendi.",
"commitDescription": "Bağlantı noktası değişkeni artık PORT olarak adlandırıldı ve PORT bir sabit değişken olduğu için bu adlandırma tutarlılığı artırır. Ortam değişkeni desteği, artık process.env.PORT ortam değişkeni aracılığıyla belirtilen herhangi bir kullanılabilir bağlantı noktasında çalışabileceğinden uygulamanın daha esnek olmasını sağlar.", "commitDescription": "Bağlantı noktası değişkeni artık PORT olarak adlandırıldı ve PORT bir sabit değişken olduğu için bu adlandırma tutarlılığı artırır. Ortam değişkeni desteği, artık process.env.PORT ortam değişkeni aracılığıyla belirtilen herhangi bir kullanılabilir bağlantı noktasında çalışabileceğinden uygulamanın daha esnek olmasını sağlar."
"commitFixOmitScope": "fix: port değişkeni küçük harfli porttan büyük harfli PORT'a değiştirildi",
"commitFeatOmitScope": "feat: process.env.PORT ortam değişkeni için destek eklendi."
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "vietnamese", "localLanguage": "vietnamese",
"commitFix": "fix(server.ts): thay đổi chữ viết thường của biến port thành chữ viết hoa PORT", "commitFix": "fix(server.ts): thay đổi chữ viết thường của biến port thành chữ viết hoa PORT",
"commitFeat": "feat(server.ts): thêm hỗ trợ cho biến môi trường process.env.PORT", "commitFeat": "feat(server.ts): thêm hỗ trợ cho biến môi trường process.env.PORT",
"commitDescription": "Biến port đã được đổi tên thành PORT, giúp cải thiện tính nhất quán trong việc đặt tên theo quy ước vì PORT là một hằng số. Hỗ trợ cho biến môi trường cho phép ứng dụng linh hoạt hơn khi có thể chạy trên bất kỳ cổng nào được chỉ định thông qua biến môi trường process.env.PORT.", "commitDescription": "Biến port đã được đổi tên thành PORT, giúp cải thiện tính nhất quán trong việc đặt tên theo quy ước vì PORT là một hằng số. Hỗ trợ cho biến môi trường cho phép ứng dụng linh hoạt hơn khi có thể chạy trên bất kỳ cổng nào được chỉ định thông qua biến môi trường process.env.PORT."
"commitFixOmitScope": "fix: thay đổi chữ viết thường của biến port thành chữ viết hoa PORT",
"commitFeatOmitScope": "feat: thêm hỗ trợ cho biến môi trường process.env.PORT"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "简体中文", "localLanguage": "简体中文",
"commitFix": "fix(server.ts)将端口变量从小写port改为大写PORT", "commitFix": "fix(server.ts)将端口变量从小写port改为大写PORT",
"commitFeat": "feat(server.ts)添加对process.env.PORT环境变量的支持", "commitFeat": "feat(server.ts)添加对process.env.PORT环境变量的支持",
"commitDescription": "现在端口变量被命名为PORT这提高了命名约定的一致性因为PORT是一个常量。环境变量的支持使应用程序更加灵活因为它现在可以通过process.env.PORT环境变量在任何可用端口上运行。", "commitDescription": "现在端口变量被命名为PORT这提高了命名约定的一致性因为PORT是一个常量。环境变量的支持使应用程序更加灵活因为它现在可以通过process.env.PORT环境变量在任何可用端口上运行。"
"commitFixOmitScope": "fix将端口变量从小写port改为大写PORT",
"commitFeatOmitScope": "feat添加对process.env.PORT环境变量的支持"
} }

View File

@@ -2,7 +2,5 @@
"localLanguage": "繁體中文", "localLanguage": "繁體中文",
"commitFix": "修正(server.ts)將端口變數從小寫端口改為大寫PORT", "commitFix": "修正(server.ts)將端口變數從小寫端口改為大寫PORT",
"commitFeat": "功能(server.ts)新增對process.env.PORT環境變數的支援", "commitFeat": "功能(server.ts)新增對process.env.PORT環境變數的支援",
"commitDescription": "現在port變數已更名為PORT以符合命名慣例因為PORT是一個常量。支援環境變數可以使應用程序更靈活因為它現在可以通過process.env.PORT環境變數運行在任何可用端口上。", "commitDescription": "現在port變數已更名為PORT以符合命名慣例因為PORT是一個常量。支援環境變數可以使應用程序更靈活因為它現在可以通過process.env.PORT環境變數運行在任何可用端口上。"
"commitFixOmitScope": "修正將端口變數從小寫端口改為大寫PORT",
"commitFeatOmitScope": "功能新增對process.env.PORT環境變數的支援"
} }

View File

@@ -1,45 +0,0 @@
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]]);
}

View File

@@ -1,26 +0,0 @@
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);
}

View File

@@ -1,22 +0,0 @@
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());
}

View File

@@ -1,18 +0,0 @@
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
}
];

View File

@@ -1,84 +0,0 @@
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;
// skip unhandled providers in migration00
if (
[
OCO_AI_PROVIDER_ENUM.DEEPSEEK,
OCO_AI_PROVIDER_ENUM.GROQ,
OCO_AI_PROVIDER_ENUM.MISTRAL,
OCO_AI_PROVIDER_ENUM.MLX,
OCO_AI_PROVIDER_ENUM.OPENROUTER
].includes(config.OCO_AI_PROVIDER)
) {
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);
}
};

View File

@@ -53,7 +53,7 @@ export const configureCommitlintIntegration = async (force = false) => {
spin.start('Generating consistency with given @commitlint rules'); spin.start('Generating consistency with given @commitlint rules');
const prompts = inferPromptsFromCommitlintConfig(commitLintConfig as any); const prompts = inferPromptsFromCommitlintConfig(commitLintConfig);
const consistencyPrompts = const consistencyPrompts =
commitlintPrompts.GEN_COMMITLINT_CONSISTENCY_PROMPT(prompts); commitlintPrompts.GEN_COMMITLINT_CONSISTENCY_PROMPT(prompts);

View File

@@ -58,16 +58,16 @@ const llmReadableRules: {
caseRule: (key, applicable, value: string | Array<string>) => caseRule: (key, applicable, value: string | Array<string>) =>
`The ${key} should ${applicable} be in ${ `The ${key} should ${applicable} be in ${
Array.isArray(value) Array.isArray(value)
? `one of the following case: ? `one of the following case:
- ${value.join('\n - ')}.` - ${value.join('\n - ')}.`
: `${value} case.` : `${value} case.`
}`, }`,
emptyRule: (key, applicable) => `The ${key} should ${applicable} be empty.`, emptyRule: (key, applicable) => `The ${key} should ${applicable} be empty.`,
enumRule: (key, applicable, value: string | Array<string>) => enumRule: (key, applicable, value: string | Array<string>) =>
`The ${key} should ${applicable} be one of the following values: `The ${key} should ${applicable} be one of the following values:
- ${Array.isArray(value) ? value.join('\n - ') : value}.`, - ${Array.isArray(value) ? value.join('\n - ') : value}.`,
enumTypeRule: (key, applicable, value: string | Array<string>, prompt) => enumTypeRule: (key, applicable, value: string | Array<string>, prompt) =>
`The ${key} should ${applicable} be one of the following values: `The ${key} should ${applicable} be one of the following values:
- ${ - ${
Array.isArray(value) Array.isArray(value)
? value ? value
@@ -204,11 +204,7 @@ export const inferPromptsFromCommitlintConfig = (
* ubiquitous language from @commitlint. * ubiquitous language from @commitlint.
* While gpt-4 does this on it self, gpt-3.5 can't map this on his own atm. * While gpt-4 does this on it self, gpt-3.5 can't map this on his own atm.
*/ */
const STRUCTURE_OF_COMMIT = config.OCO_OMIT_SCOPE const STRUCTURE_OF_COMMIT = `
? `
- Header of commit is composed of type and subject: <type-of-commit>: <subject-of-commit>
- Description of commit is composed of body and footer (optional): <body-of-commit>\n<footer(s)-of-commit>`
: `
- Header of commit is composed of type, scope, subject: <type-of-commit>(<scope-of-commit>): <subject-of-commit> - Header of commit is composed of type, scope, subject: <type-of-commit>(<scope-of-commit>): <subject-of-commit>
- Description of commit is composed of body and footer (optional): <body-of-commit>\n<footer(s)-of-commit>`; - Description of commit is composed of body and footer (optional): <body-of-commit>\n<footer(s)-of-commit>`;
@@ -224,12 +220,8 @@ Here are the specific requirements and conventions that should be strictly follo
Commit Message Conventions: Commit Message Conventions:
- The commit message consists of three parts: Header, Body, and Footer. - The commit message consists of three parts: Header, Body, and Footer.
- Header: - Header:
- Format: ${ - Format: \`<type>(<scope>): <subject>\`
config.OCO_OMIT_SCOPE
? '`<type>: <subject>`'
: '`<type>(<scope>): <subject>`'
}
- ${prompts.join('\n- ')} - ${prompts.join('\n- ')}
JSON Output Format: JSON Output Format:
@@ -237,19 +229,17 @@ JSON Output Format:
\`\`\`json \`\`\`json
{ {
"localLanguage": "${translation.localLanguage}", "localLanguage": "${translation.localLanguage}",
"commitFix": "<Header of commit for bug fix with scope>", "commitFix": "<Header of commit for bug fix>",
"commitFeat": "<Header of commit for feature with scope>", "commitFeat": "<Header of commit for feature>",
"commitFixOmitScope": "<Header of commit for bug fix without scope>",
"commitFeatOmitScope": "<Header of commit for feature without scope>",
"commitDescription": "<Description of commit for both the bug fix and the feature>" "commitDescription": "<Description of commit for both the bug fix and the feature>"
} }
\`\`\` \`\`\`
- The "commitDescription" should not include the commit message's header, only the description. - The "commitDescription" should not include the commit messages header, only the description.
- Description should not be more than 74 characters. - Description should not be more than 74 characters.
Additional Details: Additional Details:
- Changing the variable 'port' to uppercase 'PORT' is considered a bug fix. - Changing the variable 'port' to uppercase 'PORT' is considered a bug fix.
- Allowing the server to listen on a port specified through the environment variable is considered a new feature. - Allowing the server to listen on a port specified through the environment variable is considered a new feature.
Example Git Diff is to follow:` Example Git Diff is to follow:`
}, },
@@ -268,9 +258,7 @@ 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 ${ content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes and WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
config.OCO_WHY ? 'and WHY the changes were done' : ''
}. 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.'
@@ -287,11 +275,7 @@ ${
? 'Craft a concise commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in a one single message, without diverging into a list of commit per file change.' ? 'Craft a concise commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in a one single message, without diverging into a list of commit per file change.'
: '' : ''
} }
${
config.OCO_OMIT_SCOPE
? 'Do not include a scope in the commit message format. Use the format: <type>: <subject>'
: ''
}
You will strictly follow the following conventions to generate the content of the commit message: You will strictly follow the following conventions to generate the content of the commit message:
- ${prompts.join('\n- ')} - ${prompts.join('\n- ')}

View File

@@ -1,30 +1,13 @@
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'),
path.resolve(__dirname, '../..')
];
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 = '@commitlint/load/package.json'; const packageFile = 'node_modules/@commitlint/load/package.json';
const packageJsonPath = findModulePath(packageFile); const packageJsonPath = path.join(
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}`);
} }
@@ -36,7 +19,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
@@ -44,31 +27,36 @@ type QualifiedConfigOnAnyVersion = { [key: string]: unknown };
* *
* @returns * @returns
*/ */
export const getCommitLintPWDConfig = export const getCommitLintPWDConfig = async (): Promise<QualifiedConfigOnAnyVersion | null> => {
async (): Promise<QualifiedConfigOnAnyVersion | null> => { let load, nodeModulesPath;
let load: Function, modulePath: string; switch (await getCommitLintModuleType()) {
switch (await getCommitLintModuleType()) { case 'cjs':
case 'cjs': /**
/** * CommonJS (<= commitlint@v18.x.x.)
* CommonJS (<= commitlint@v18.x.x.) */
*/ nodeModulesPath = path.join(
modulePath = findModulePath('@commitlint/load'); process.env.PWD || process.cwd(),
load = require(modulePath).default; 'node_modules/@commitlint/load',
break; );
case 'esm': load = require(nodeModulesPath).default;
/** break;
* ES Module (commitlint@v19.x.x. <= ) case 'esm':
* Directory import is not supported in ES Module resolution, so import the file directly /**
*/ * ES Module (commitlint@v19.x.x. <= )
modulePath = findModulePath('@commitlint/load/lib/load.js'); * Directory import is not supported in ES Module resolution, so import the file directly
load = (await import(modulePath)).default; */
break; nodeModulesPath = path.join(
} 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;
}; };

View File

@@ -21,7 +21,7 @@ export const getJSONBlock = (input: string): string => {
if (jsonIndex > -1) { if (jsonIndex > -1) {
input = input.slice(jsonIndex + 8); input = input.slice(jsonIndex + 8);
const endJsonIndex = input.search('```'); const endJsonIndex = input.search('```');
input = input.slice(0, endJsonIndex); input = input.slice(0, endJsonIndex);
} }
return input; return input;
}; };

View File

@@ -108,32 +108,12 @@ const getDescriptionInstruction = () =>
const getOneLineCommitInstruction = () => const getOneLineCommitInstruction = () =>
config.OCO_ONE_LINE_COMMIT config.OCO_ONE_LINE_COMMIT
? 'Craft a concise, single sentence, commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in one single message.' ? 'Craft a concise commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in a one single message, without diverging into a list of commit per file change.'
: ''; : '';
const getScopeInstruction = () =>
config.OCO_OMIT_SCOPE
? 'Do not include a scope in the commit message format. Use the format: <type>: <subject>'
: '';
/**
* Get the context of the user input
* @param extraArgs - The arguments passed to the command line
* @example
* $ oco -- This is a context used to generate the commit message
* @returns - The context of the user input
*/
const userInputCodeContext = (context: string) => {
if (context !== '' && context !== ' ') {
return `Additional context provided by the user: <context>${context}</context>\nConsider this context when generating the commit message, incorporating relevant information when appropriate.`;
}
return '';
};
const INIT_MAIN_PROMPT = ( const INIT_MAIN_PROMPT = (
language: string, language: string,
fullGitMojiSpec: boolean, fullGitMojiSpec: boolean
context: string
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({ ): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
role: 'system', role: 'system',
content: (() => { content: (() => {
@@ -146,11 +126,9 @@ const INIT_MAIN_PROMPT = (
const conventionGuidelines = getCommitConvention(fullGitMojiSpec); const conventionGuidelines = getCommitConvention(fullGitMojiSpec);
const descriptionGuideline = getDescriptionInstruction(); const descriptionGuideline = getDescriptionInstruction();
const oneLineCommitGuideline = getOneLineCommitInstruction(); const oneLineCommitGuideline = getOneLineCommitInstruction();
const scopeInstruction = getScopeInstruction();
const generalGuidelines = `Use the present tense. Lines must not be longer than 74 characters. Use ${language} for the commit message.`; const generalGuidelines = `Use the present tense. Lines must not be longer than 74 characters. Use ${language} for the commit message.`;
const userInputContext = userInputCodeContext(context);
return `${missionStatement}\n${diffInstruction}\n${conventionGuidelines}\n${descriptionGuideline}\n${oneLineCommitGuideline}\n${scopeInstruction}\n${generalGuidelines}\n${userInputContext}`; return `${missionStatement}\n${diffInstruction}\n${conventionGuidelines}\n${descriptionGuideline}\n${oneLineCommitGuideline}\n${generalGuidelines}`;
})() })()
}); });
@@ -183,52 +161,31 @@ export const INIT_DIFF_PROMPT: OpenAI.Chat.Completions.ChatCompletionMessagePara
});` });`
}; };
const COMMIT_TYPES = { const getContent = (translation: ConsistencyPrompt) => {
fix: '🐛', const fix = config.OCO_EMOJI
feat: '✨' ? `🐛 ${removeConventionalCommitWord(translation.commitFix)}`
} as const; : translation.commitFix;
const generateCommitString = ( const feat = config.OCO_EMOJI
type: keyof typeof COMMIT_TYPES, ? `${removeConventionalCommitWord(translation.commitFeat)}`
message: string : translation.commitFeat;
): string => {
const cleanMessage = removeConventionalCommitWord(message);
return config.OCO_EMOJI ? `${COMMIT_TYPES[type]} ${cleanMessage}` : message;
};
const getConsistencyContent = (translation: ConsistencyPrompt) => {
const fixMessage =
config.OCO_OMIT_SCOPE && translation.commitFixOmitScope
? translation.commitFixOmitScope
: translation.commitFix;
const featMessage =
config.OCO_OMIT_SCOPE && translation.commitFeatOmitScope
? translation.commitFeatOmitScope
: translation.commitFeat;
const fix = generateCommitString('fix', fixMessage);
const feat = config.OCO_ONE_LINE_COMMIT
? ''
: generateCommitString('feat', featMessage);
const description = config.OCO_DESCRIPTION const description = config.OCO_DESCRIPTION
? translation.commitDescription ? translation.commitDescription
: ''; : '';
return [fix, feat, description].filter(Boolean).join('\n'); return `${fix}\n${feat}\n${description}`;
}; };
const INIT_CONSISTENCY_PROMPT = ( const INIT_CONSISTENCY_PROMPT = (
translation: ConsistencyPrompt translation: ConsistencyPrompt
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({ ): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
role: 'assistant', role: 'assistant',
content: getConsistencyContent(translation) content: getContent(translation)
}); });
export const getMainCommitPrompt = async ( export const getMainCommitPrompt = async (
fullGitMojiSpec: boolean, fullGitMojiSpec: boolean
context: string
): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => { ): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => {
switch (config.OCO_PROMPT_MODULE) { switch (config.OCO_PROMPT_MODULE) {
case '@commitlint': case '@commitlint':
@@ -257,7 +214,7 @@ export const getMainCommitPrompt = async (
default: default:
return [ return [
INIT_MAIN_PROMPT(translation.localLanguage, fullGitMojiSpec, context), INIT_MAIN_PROMPT(translation.localLanguage, fullGitMojiSpec),
INIT_DIFF_PROMPT, INIT_DIFF_PROMPT,
INIT_CONSISTENCY_PROMPT(translation) INIT_CONSISTENCY_PROMPT(translation)
]; ];

View File

@@ -2,93 +2,64 @@ 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 { FlowiseEngine } from '../engine/flowise'; import { FlowiseAi } from '../engine/flowise';
import { GeminiEngine } from '../engine/gemini'; import { Gemini } from '../engine/gemini';
import { OllamaEngine } from '../engine/ollama'; import { OllamaAi } from '../engine/ollama';
import { OpenAiEngine } from '../engine/openAi'; import { OpenAiEngine } from '../engine/openAi';
import { MistralAiEngine } from '../engine/mistral';
import { TestAi, TestMockType } from '../engine/testAi'; import { TestAi, TestMockType } from '../engine/testAi';
import { GroqEngine } from '../engine/groq';
import { MLXEngine } from '../engine/mlx';
import { DeepseekEngine } from '../engine/deepseek';
import { AimlApiEngine } from '../engine/aimlapi';
import { OpenRouterEngine } from '../engine/openrouter';
export function parseCustomHeaders(headers: any): Record<string, string> {
let parsedHeaders = {};
if (!headers) {
return parsedHeaders;
}
try {
if (typeof headers === 'object' && !Array.isArray(headers)) {
parsedHeaders = headers;
} else {
parsedHeaders = JSON.parse(headers);
}
} catch (error) {
console.warn(
'Invalid OCO_API_CUSTOM_HEADERS format, ignoring custom headers'
);
}
return parsedHeaders;
}
export function getEngine(): AiEngine { export function getEngine(): AiEngine {
const config = getConfig(); const config = getConfig();
const provider = config.OCO_AI_PROVIDER; const provider = config.OCO_AI_PROVIDER;
const customHeaders = parseCustomHeaders(config.OCO_API_CUSTOM_HEADERS);
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
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_API_URL!, baseURL: config.OCO_OPENAI_BASE_PATH!
apiKey: config.OCO_API_KEY!,
customHeaders
}; };
switch (provider) { switch (provider) {
case OCO_AI_PROVIDER_ENUM.OLLAMA: case OCO_AI_PROVIDER_ENUM.OLLAMA:
return new OllamaEngine(DEFAULT_CONFIG); return new OllamaAi({
...DEFAULT_CONFIG,
apiKey: '',
baseURL: config.OCO_OLLAMA_API_URL!
});
case OCO_AI_PROVIDER_ENUM.ANTHROPIC: case OCO_AI_PROVIDER_ENUM.ANTHROPIC:
return new AnthropicEngine(DEFAULT_CONFIG); return new AnthropicEngine({
...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 GeminiEngine(DEFAULT_CONFIG); return new Gemini({
...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(DEFAULT_CONFIG); return new AzureEngine({
...DEFAULT_CONFIG,
apiKey: config.OCO_AZURE_API_KEY!
});
case OCO_AI_PROVIDER_ENUM.FLOWISE: case OCO_AI_PROVIDER_ENUM.FLOWISE:
return new FlowiseEngine(DEFAULT_CONFIG); return new FlowiseAi({
...DEFAULT_CONFIG,
case OCO_AI_PROVIDER_ENUM.GROQ: baseURL: config.OCO_FLOWISE_ENDPOINT || DEFAULT_CONFIG.baseURL,
return new GroqEngine(DEFAULT_CONFIG); apiKey: config.OCO_FLOWISE_API_KEY!
});
case OCO_AI_PROVIDER_ENUM.MISTRAL:
return new MistralAiEngine(DEFAULT_CONFIG);
case OCO_AI_PROVIDER_ENUM.MLX:
return new MLXEngine(DEFAULT_CONFIG);
case OCO_AI_PROVIDER_ENUM.DEEPSEEK:
return new DeepseekEngine(DEFAULT_CONFIG);
case OCO_AI_PROVIDER_ENUM.AIMLAPI:
return new AimlApiEngine(DEFAULT_CONFIG);
case OCO_AI_PROVIDER_ENUM.OPENROUTER:
return new OpenRouterEngine(DEFAULT_CONFIG);
default: default:
return new OpenAiEngine(DEFAULT_CONFIG); return new OpenAiEngine({
...DEFAULT_CONFIG,
apiKey: config.OCO_OPENAI_API_KEY!
});
} }
} }

View File

@@ -1,7 +1,7 @@
import { execa } from 'execa'; import { execa } from 'execa';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import ignore, { Ignore } from 'ignore'; import ignore, { Ignore } from 'ignore';
import { join } from 'path';
import { outro, spinner } from '@clack/prompts'; import { outro, spinner } from '@clack/prompts';
export const assertGitRepo = async () => { export const assertGitRepo = async () => {
@@ -16,44 +16,41 @@ export const assertGitRepo = async () => {
// (file) => `:(exclude)${file}` // (file) => `:(exclude)${file}`
// ); // );
export const getOpenCommitIgnore = async (): Promise<Ignore> => { export const getOpenCommitIgnore = (): Ignore => {
const gitDir = await getGitDir();
const ig = ignore(); const ig = ignore();
try { try {
ig.add( ig.add(readFileSync('.opencommitignore').toString().split('\n'));
readFileSync(join(gitDir, '.opencommitignore')).toString().split('\n')
);
} catch (e) {} } catch (e) {}
return ig; return ig;
}; };
export const getCoreHooksPath = async (): Promise<string> => { export const getCoreHooksPath = async (): Promise<string> => {
const gitDir = await getGitDir(); const { stdout } = await execa('git', ['config', 'core.hooksPath']);
const { stdout } = await execa('git', ['config', 'core.hooksPath'], {
cwd: gitDir
});
return stdout; return stdout;
}; };
export const getStagedFiles = async (): Promise<string[]> => { export const getStagedFiles = async (): Promise<string[]> => {
const gitDir = await getGitDir(); const { stdout: gitDir } = await execa('git', [
'rev-parse',
'--show-toplevel'
]);
const { stdout: files } = await execa( const { stdout: files } = await execa('git', [
'git', 'diff',
['diff', '--name-only', '--cached', '--relative'], '--name-only',
{ cwd: gitDir } '--cached',
); '--relative',
gitDir
]);
if (!files) return []; if (!files) return [];
const filesList = files.split('\n'); const filesList = files.split('\n');
const ig = await getOpenCommitIgnore(); const ig = getOpenCommitIgnore();
const allowedFiles = filesList.filter((file) => !ig.ignores(file)); const allowedFiles = filesList.filter((file) => !ig.ignores(file));
if (!allowedFiles) return []; if (!allowedFiles) return [];
@@ -62,17 +59,12 @@ export const getStagedFiles = async (): Promise<string[]> => {
}; };
export const getChangedFiles = async (): Promise<string[]> => { export const getChangedFiles = async (): Promise<string[]> => {
const gitDir = await getGitDir(); const { stdout: modified } = await execa('git', ['ls-files', '--modified']);
const { stdout: others } = await execa('git', [
const { stdout: modified } = await execa('git', ['ls-files', '--modified'], { 'ls-files',
cwd: gitDir '--others',
}); '--exclude-standard'
]);
const { stdout: others } = await execa(
'git',
['ls-files', '--others', '--exclude-standard'],
{ cwd: gitDir }
);
const files = [...modified.split('\n'), ...others.split('\n')].filter( const files = [...modified.split('\n'), ...others.split('\n')].filter(
(file) => !!file (file) => !!file
@@ -82,20 +74,16 @@ export const getChangedFiles = async (): Promise<string[]> => {
}; };
export const gitAdd = async ({ files }: { files: string[] }) => { export const gitAdd = async ({ files }: { files: string[] }) => {
const gitDir = await getGitDir();
const gitAddSpinner = spinner(); const gitAddSpinner = spinner();
gitAddSpinner.start('Adding files to commit'); gitAddSpinner.start('Adding files to commit');
await execa('git', ['add', ...files], { cwd: gitDir }); await execa('git', ['add', ...files]);
gitAddSpinner.stop(`Staged ${files.length} files`); gitAddSpinner.stop('Done');
}; };
export const getDiff = async ({ files }: { files: string[] }) => { export const getDiff = async ({ files }: { files: string[] }) => {
const gitDir = await getGitDir();
const lockFiles = files.filter( const lockFiles = files.filter(
(file) => (file) =>
file.includes('.lock') || file.includes('.lock') ||
@@ -120,20 +108,12 @@ export const getDiff = async ({ files }: { files: string[] }) => {
(file) => !file.includes('.lock') && !file.includes('-lock.') (file) => !file.includes('.lock') && !file.includes('-lock.')
); );
const { stdout: diff } = await execa( const { stdout: diff } = await execa('git', [
'git', 'diff',
['diff', '--staged', '--', ...filesWithoutLocks], '--staged',
{ cwd: gitDir } '--',
); ...filesWithoutLocks
]);
return diff; return diff;
}; };
export const getGitDir = async (): Promise<string> => {
const { stdout: gitDir } = await execa('git', [
'rev-parse',
'--show-toplevel'
]);
return gitDir;
};

View File

@@ -1,57 +0,0 @@
/**
* Removes content wrapped in specified tags from a string
* @param content The content string to process
* @param tag The tag name without angle brackets (e.g., 'think' for '<think></think>')
* @returns The content with the specified tags and their contents removed, and trimmed
*/
export function removeContentTags<T extends string | null | undefined>(
content: T,
tag: string
): T {
if (!content || typeof content !== 'string') {
return content;
}
// Dynamic implementation for other cases
const openTag = `<${tag}>`;
const closeTag = `</${tag}>`;
// Parse the content and remove tags
let result = '';
let skipUntil: number | null = null;
let depth = 0;
for (let i = 0; i < content.length; i++) {
// Check for opening tag
if (content.substring(i, i + openTag.length) === openTag) {
depth++;
if (depth === 1) {
skipUntil = content.indexOf(closeTag, i + openTag.length);
i = i + openTag.length - 1; // Skip the opening tag
continue;
}
}
// Check for closing tag
else if (
content.substring(i, i + closeTag.length) === closeTag &&
depth > 0
) {
depth--;
if (depth === 0) {
i = i + closeTag.length - 1; // Skip the closing tag
skipUntil = null;
continue;
}
}
// Only add character if not inside a tag
if (skipUntil === null) {
result += content[i];
}
}
// Normalize multiple spaces/tabs into a single space (preserves newlines), then trim.
result = result.replace(/[ \t]+/g, ' ').trim();
return result as unknown as T;
}

View File

@@ -1,205 +0,0 @@
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' OCO_GITPUSH='true' 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' OCO_GITPUSH='true' 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' OCO_GITPUSH='true' 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();
});
});

View File

@@ -9,7 +9,7 @@ it('cli flow to generate commit message for 1 new file (staged)', async () => {
await render('echo' ,[`'console.log("Hello World");' > index.ts`], { cwd: gitDir }); await render('echo' ,[`'console.log("Hello World");' > index.ts`], { cwd: gitDir });
await render('git' ,['add index.ts'], { cwd: gitDir }); await render('git' ,['add index.ts'], { cwd: gitDir });
const { queryByText, findByText, userEvent } = await render(`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`, [resolve('./out/cli.cjs')], { cwd: gitDir }); const { queryByText, findByText, userEvent } = await render(`OCO_AI_PROVIDER='test' node`, [resolve('./out/cli.cjs')], { cwd: gitDir });
expect(await queryByText('No files are staged')).not.toBeInTheConsole(); expect(await queryByText('No files are staged')).not.toBeInTheConsole();
expect(await queryByText('Do you want to stage all files and generate commit message?')).not.toBeInTheConsole(); expect(await queryByText('Do you want to stage all files and generate commit message?')).not.toBeInTheConsole();
@@ -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('Do you want to run `git push`?')).toBeInTheConsole(); expect(await findByText('Choose a remote to push to')).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();
@@ -34,7 +34,7 @@ it('cli flow to generate commit message for 1 changed file (not staged)', async
await render('echo' ,[`'console.log("Good night World");' >> index.ts`], { cwd: gitDir }); await render('echo' ,[`'console.log("Good night World");' >> index.ts`], { cwd: gitDir });
const { findByText, userEvent } = await render(`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`, [resolve('./out/cli.cjs')], { cwd: gitDir }); const { findByText, userEvent } = await render(`OCO_AI_PROVIDER='test' node`, [resolve('./out/cli.cjs')], { cwd: gitDir });
expect(await findByText('No files are staged')).toBeInTheConsole(); expect(await findByText('No files are staged')).toBeInTheConsole();
expect(await findByText('Do you want to stage all files and generate commit message?')).toBeInTheConsole(); expect(await findByText('Do you want to stage all files and generate commit message?')).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('Do you want to run `git push`?')).toBeInTheConsole(); expect(await findByText('Choose a remote to push to')).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();

View File

@@ -5,8 +5,8 @@ import { prepareEnvironment, wait } from '../utils';
import path from 'path'; import path from 'path';
function getAbsolutePath(relativePath: string) { function getAbsolutePath(relativePath: string) {
// Use process.cwd() which should be the project root during test execution const scriptDir = path.dirname(__filename);
return path.resolve(process.cwd(), 'test/e2e/prompt-module', relativePath); return path.resolve(scriptDir, relativePath);
} }
async function setupCommitlint(dir: string, ver: 9 | 18 | 19) { async function setupCommitlint(dir: string, ver: 9 | 18 | 19) {
let packagePath, packageJsonPath, configPath; let packagePath, packageJsonPath, configPath;
@@ -47,7 +47,7 @@ describe('cli flow to run "oco commitlint force"', () => {
` `
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \ OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
OCO_PROMPT_MODULE='@commitlint' \ OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \ OCO_AI_PROVIDER='test' \
node ${resolve('./out/cli.cjs')} commitlint force \ node ${resolve('./out/cli.cjs')} commitlint force \
`, `,
[], [],
@@ -83,7 +83,7 @@ describe('cli flow to run "oco commitlint force"', () => {
` `
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \ OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
OCO_PROMPT_MODULE='@commitlint' \ OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \ OCO_AI_PROVIDER='test' \
node ${resolve('./out/cli.cjs')} commitlint force \ node ${resolve('./out/cli.cjs')} commitlint force \
`, `,
[], [],
@@ -119,7 +119,7 @@ describe('cli flow to run "oco commitlint force"', () => {
` `
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \ OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
OCO_PROMPT_MODULE='@commitlint' \ OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \ OCO_AI_PROVIDER='test' \
node ${resolve('./out/cli.cjs')} commitlint force \ node ${resolve('./out/cli.cjs')} commitlint force \
`, `,
[], [],
@@ -160,7 +160,7 @@ describe('cli flow to generate commit message using @commitlint prompt-module',
` `
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \ OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
OCO_PROMPT_MODULE='@commitlint' \ OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \ OCO_AI_PROVIDER='test' \
node ${resolve('./out/cli.cjs')} commitlint force \ node ${resolve('./out/cli.cjs')} commitlint force \
`, `,
[], [],
@@ -175,13 +175,15 @@ describe('cli flow to generate commit message using @commitlint prompt-module',
` `
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \ OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
OCO_PROMPT_MODULE='@commitlint' \ OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \ OCO_AI_PROVIDER='test' \
node ${resolve('./out/cli.cjs')} commitlint get \ node ${resolve('./out/cli.cjs')} commitlint get \
`, `,
[], [],
{ cwd: gitDir } { cwd: gitDir }
); );
expect(await commitlintGet.findByText('consistency')).toBeInTheConsole(); expect(
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`], {
@@ -193,7 +195,7 @@ describe('cli flow to generate commit message using @commitlint prompt-module',
` `
OCO_TEST_MOCK_TYPE='commit-message' \ OCO_TEST_MOCK_TYPE='commit-message' \
OCO_PROMPT_MODULE='@commitlint' \ OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \ OCO_AI_PROVIDER='test' \
node ${resolve('./out/cli.cjs')} \ node ${resolve('./out/cli.cjs')} \
`, `,
[], [],
@@ -209,7 +211,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('Do you want to run `git push`?') await oco.findByText('Choose a remote to push to')
).toBeInTheConsole(); ).toBeInTheConsole();
oco.userEvent.keyboard('[Enter]'); oco.userEvent.keyboard('[Enter]');

View File

@@ -15,7 +15,7 @@ export const prepareEnvironment = async (): Promise<{
gitDir: string; gitDir: string;
cleanup: () => Promise<void>; cleanup: () => Promise<void>;
}> => { }> => {
const tempDir = await prepareTempDir(); const tempDir = await fsMakeTempDir(path.join(tmpdir(), 'opencommit-test-'));
// 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,8 +30,4 @@ 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));

View File

@@ -1,11 +1,10 @@
import 'cli-testing-library/extend-expect'
import { configure } from 'cli-testing-library'
import { jest } from '@jest/globals'; import { jest } from '@jest/globals';
import 'cli-testing-library/extend-expect';
import { configure } from 'cli-testing-library';
// Make Jest available globally
global.jest = jest; global.jest = jest;
/** /**
* Adjusted the wait time for waitFor/findByText to 2000ms, because the default 1000ms makes the test results flaky * Adjusted the wait time for waitFor/findByText to 2000ms, because the default 1000ms makes the test results flaky
*/ */
configure({ asyncUtilTimeout: 2000 }); configure({ asyncUtilTimeout: 2000 })

View File

@@ -1,17 +1,10 @@
import { existsSync, readFileSync, rmSync } from 'fs'; import { getConfig } from '../../src/commands/config';
import {
CONFIG_KEYS,
DEFAULT_CONFIG,
getConfig,
setConfig
} from '../../src/commands/config';
import { prepareFile } from './utils'; import { prepareFile } from './utils';
import { dirname } from 'path';
describe('config', () => { describe('getConfig', () => {
const originalEnv = { ...process.env }; const originalEnv = { ...process.env };
let globalConfigFile: { filePath: string; cleanup: () => Promise<void> }; let globalConfigFile: { filePath: string; cleanup: () => Promise<void> };
let envConfigFile: { filePath: string; cleanup: () => Promise<void> }; let localEnvFile: { 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) => {
@@ -26,12 +19,7 @@ describe('config', () => {
beforeEach(async () => { beforeEach(async () => {
resetEnv(originalEnv); resetEnv(originalEnv);
if (globalConfigFile) await globalConfigFile.cleanup(); if (globalConfigFile) await globalConfigFile.cleanup();
if (envConfigFile) await envConfigFile.cleanup(); if (localEnvFile) await localEnvFile.cleanup();
});
afterEach(async () => {
if (globalConfigFile) await globalConfigFile.cleanup();
if (envConfigFile) await envConfigFile.cleanup();
}); });
afterAll(() => { afterAll(() => {
@@ -48,282 +36,115 @@ describe('config', () => {
return await prepareFile(fileName, fileContent); return await prepareFile(fileName, fileContent);
}; };
describe('getConfig', () => { it('should prioritize local .env over global .opencommit config', async () => {
it('should prioritize local .env over global .opencommit config', async () => { globalConfigFile = await generateConfig('.opencommit', {
globalConfigFile = await generateConfig('.opencommit', { OCO_OPENAI_API_KEY: 'global-key',
OCO_API_KEY: 'global-key', OCO_MODEL: 'gpt-3.5-turbo',
OCO_MODEL: 'gpt-3.5-turbo', OCO_LANGUAGE: 'en'
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');
}); });
it('should fallback to global config when local config is not set', async () => { localEnvFile = await generateConfig('.env', {
globalConfigFile = await generateConfig('.opencommit', { OCO_OPENAI_API_KEY: 'local-key',
OCO_API_KEY: 'global-key', OCO_ANTHROPIC_API_KEY: 'local-anthropic-key',
OCO_MODEL: 'gpt-4', OCO_LANGUAGE: 'fr'
OCO_LANGUAGE: 'de',
OCO_DESCRIPTION: 'true'
});
envConfigFile = await generateConfig('.env', {
OCO_API_URL: 'local-api-url'
});
const config = getConfig({
globalPath: globalConfigFile.filePath,
envPath: envConfigFile.filePath
});
expect(config).not.toEqual(null);
expect(config.OCO_API_KEY).toEqual('global-key');
expect(config.OCO_API_URL).toEqual('local-api-url');
expect(config.OCO_MODEL).toEqual('gpt-4');
expect(config.OCO_LANGUAGE).toEqual('de');
expect(config.OCO_DESCRIPTION).toEqual(true);
}); });
it('should handle boolean and numeric values correctly', async () => { const config = getConfig({
globalConfigFile = await generateConfig('.opencommit', { configPath: globalConfigFile.filePath,
OCO_TOKENS_MAX_INPUT: '4096', envPath: localEnvFile.filePath
OCO_TOKENS_MAX_OUTPUT: '500',
OCO_GITPUSH: 'true'
});
envConfigFile = await generateConfig('.env', {
OCO_TOKENS_MAX_INPUT: '8192',
OCO_ONE_LINE_COMMIT: 'false',
OCO_OMIT_SCOPE: 'true'
});
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.OCO_OMIT_SCOPE).toEqual(true);
});
it('should handle custom HTTP headers correctly', async () => {
globalConfigFile = await generateConfig('.opencommit', {
OCO_API_CUSTOM_HEADERS: '{"X-Global-Header": "global-value"}'
});
envConfigFile = await generateConfig('.env', {
OCO_API_CUSTOM_HEADERS: '{"Authorization": "Bearer token123", "X-Custom-Header": "test-value"}'
});
const config = getConfig({
globalPath: globalConfigFile.filePath,
envPath: envConfigFile.filePath
});
expect(config).not.toEqual(null);
expect(config.OCO_API_CUSTOM_HEADERS).toEqual({"Authorization": "Bearer token123", "X-Custom-Header": "test-value"});
// No need to parse JSON again since it's already an object
const parsedHeaders = config.OCO_API_CUSTOM_HEADERS;
expect(parsedHeaders).toHaveProperty('Authorization', 'Bearer token123');
expect(parsedHeaders).toHaveProperty('X-Custom-Header', 'test-value');
expect(parsedHeaders).not.toHaveProperty('X-Global-Header');
}); });
it('should handle empty local config correctly', async () => { expect(config).not.toEqual(null);
globalConfigFile = await generateConfig('.opencommit', { expect(config.OCO_OPENAI_API_KEY).toEqual('local-key');
OCO_API_KEY: 'global-key', expect(config.OCO_MODEL).toEqual('gpt-3.5-turbo');
OCO_MODEL: 'gpt-4', expect(config.OCO_LANGUAGE).toEqual('fr');
OCO_LANGUAGE: 'es' expect(config.OCO_ANTHROPIC_API_KEY).toEqual('local-anthropic-key');
});
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);
});
}); });
describe('setConfig', () => { it('should fallback to global config when local config is not set', async () => {
beforeEach(async () => { globalConfigFile = await generateConfig('.opencommit', {
// we create and delete the file to have the parent directory, but not the file, to test the creation of the file OCO_OPENAI_API_KEY: 'global-key',
globalConfigFile = await generateConfig('.opencommit', {}); OCO_MODEL: 'gpt-4',
rmSync(globalConfigFile.filePath); OCO_LANGUAGE: 'de',
OCO_DESCRIPTION: 'true'
}); });
it('should create .opencommit file with DEFAULT CONFIG if it does not exist on first setConfig run', async () => { localEnvFile = await generateConfig('.env', {
const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath); OCO_ANTHROPIC_API_KEY: 'local-anthropic-key'
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}`);
});
}); });
it('should set new config values', async () => { const config = getConfig({
globalConfigFile = await generateConfig('.opencommit', {}); configPath: globalConfigFile.filePath,
await setConfig( envPath: localEnvFile.filePath
[
[CONFIG_KEYS.OCO_API_KEY, 'new-key'],
[CONFIG_KEYS.OCO_MODEL, 'gpt-4']
],
globalConfigFile.filePath
);
const config = getConfig({
globalPath: globalConfigFile.filePath
});
expect(config.OCO_API_KEY).toEqual('new-key');
expect(config.OCO_MODEL).toEqual('gpt-4');
}); });
it('should update existing config values', async () => { expect(config).not.toEqual(null);
globalConfigFile = await generateConfig('.opencommit', { expect(config.OCO_OPENAI_API_KEY).toEqual('global-key');
OCO_API_KEY: 'initial-key' expect(config.OCO_ANTHROPIC_API_KEY).toEqual('local-anthropic-key');
}); expect(config.OCO_MODEL).toEqual('gpt-4');
await setConfig( expect(config.OCO_LANGUAGE).toEqual('de');
[[CONFIG_KEYS.OCO_API_KEY, 'updated-key']], expect(config.OCO_DESCRIPTION).toEqual(true);
globalConfigFile.filePath });
);
const config = getConfig({ it('should handle boolean and numeric values correctly', async () => {
globalPath: globalConfigFile.filePath globalConfigFile = await generateConfig('.opencommit', {
}); OCO_TOKENS_MAX_INPUT: '4096',
expect(config.OCO_API_KEY).toEqual('updated-key'); OCO_TOKENS_MAX_OUTPUT: '500',
OCO_GITPUSH: 'true'
}); });
it('should handle boolean and numeric values correctly', async () => { localEnvFile = await generateConfig('.env', {
globalConfigFile = await generateConfig('.opencommit', {}); OCO_TOKENS_MAX_INPUT: '8192',
await setConfig( OCO_ONE_LINE_COMMIT: 'false'
[
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT, '8192'],
[CONFIG_KEYS.OCO_DESCRIPTION, 'true'],
[CONFIG_KEYS.OCO_ONE_LINE_COMMIT, 'false']
],
globalConfigFile.filePath
);
const config = getConfig({
globalPath: globalConfigFile.filePath
});
expect(config.OCO_TOKENS_MAX_INPUT).toEqual(8192);
expect(config.OCO_DESCRIPTION).toEqual(true);
expect(config.OCO_ONE_LINE_COMMIT).toEqual(false);
}); });
it('should throw an error for unsupported config keys', async () => { const config = getConfig({
globalConfigFile = await generateConfig('.opencommit', {}); configPath: globalConfigFile.filePath,
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');
}
}); });
it('should persist changes to the config file', async () => { expect(config).not.toEqual(null);
const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath); expect(config.OCO_TOKENS_MAX_INPUT).toEqual(8192);
expect(isGlobalConfigFileExist).toBe(false); expect(config.OCO_TOKENS_MAX_OUTPUT).toEqual(500);
expect(config.OCO_GITPUSH).toEqual(true);
expect(config.OCO_ONE_LINE_COMMIT).toEqual(false);
});
await setConfig( it('should handle empty local config correctly', async () => {
[[CONFIG_KEYS.OCO_API_KEY, 'persisted-key']], globalConfigFile = await generateConfig('.opencommit', {
globalConfigFile.filePath OCO_OPENAI_API_KEY: 'global-key',
); OCO_MODEL: 'gpt-4',
OCO_LANGUAGE: 'es'
const fileContent = readFileSync(globalConfigFile.filePath, 'utf8');
expect(fileContent).toContain('OCO_API_KEY=persisted-key');
}); });
it('should set multiple configs in a row and keep the changes', async () => { localEnvFile = await generateConfig('.env', {});
const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath);
expect(isGlobalConfigFileExist).toBe(false);
await setConfig( const config = getConfig({
[[CONFIG_KEYS.OCO_API_KEY, 'persisted-key']], configPath: globalConfigFile.filePath,
globalConfigFile.filePath envPath: localEnvFile.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);
}); });
}); });

View File

@@ -1,4 +1,4 @@
import { GeminiEngine } from '../../src/engine/gemini'; import { Gemini } 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: GeminiEngine; let gemini: Gemini;
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 GeminiEngine({ gemini = new Gemini({
apiKey: mockConfig.OCO_API_KEY, apiKey: mockConfig.OCO_GEMINI_API_KEY,
model: mockConfig.OCO_MODEL model: mockConfig.OCO_MODEL
}); });
}; };
@@ -45,10 +45,12 @@ 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_API_KEY = 'mock-api-key'; mockConfig.OCO_GEMINI_API_KEY = 'mock-api-key';
mockConfig.OCO_MODEL = 'gemini-1.5-flash'; mockConfig.OCO_MODEL = 'gemini-1.5-flash';
mockGoogleGenerativeAi = new GoogleGenerativeAI(mockConfig.OCO_API_KEY); mockGoogleGenerativeAi = new GoogleGenerativeAI(
mockConfig.OCO_GEMINI_API_KEY
);
mockGenerativeModel = mockGoogleGenerativeAi.getGenerativeModel({ mockGenerativeModel = mockGoogleGenerativeAi.getGenerativeModel({
model: mockConfig.OCO_MODEL model: mockConfig.OCO_MODEL
}); });

View File

@@ -1,57 +0,0 @@
import { removeContentTags } from '../../src/utils/removeContentTags';
describe('removeContentTags', () => {
it('should remove content wrapped in specified tags', () => {
const content = 'This is <think>something to hide</think> visible content';
const result = removeContentTags(content, 'think');
expect(result).toBe('This is visible content');
});
it('should handle multiple tag occurrences', () => {
const content = '<think>hidden</think> visible <think>also hidden</think> text';
const result = removeContentTags(content, 'think');
expect(result).toBe('visible text');
});
it('should handle multiline content within tags', () => {
const content = 'Start <think>hidden\nover multiple\nlines</think> End';
const result = removeContentTags(content, 'think');
expect(result).toBe('Start End');
});
it('should return content as is when tag is not found', () => {
const content = 'Content without any tags';
const result = removeContentTags(content, 'think');
expect(result).toBe('Content without any tags');
});
it('should work with different tag names', () => {
const content = 'This is <custom>something to hide</custom> visible content';
const result = removeContentTags(content, 'custom');
expect(result).toBe('This is visible content');
});
it('should handle null content', () => {
const content = null;
const result = removeContentTags(content, 'think');
expect(result).toBe(null);
});
it('should handle undefined content', () => {
const content = undefined;
const result = removeContentTags(content, 'think');
expect(result).toBe(undefined);
});
it('should trim the result', () => {
const content = ' <think>hidden</think> visible ';
const result = removeContentTags(content, 'think');
expect(result).toBe('visible');
});
it('should handle nested tags correctly', () => {
const content = 'Outside <think>Inside <think>Nested</think></think> End';
const result = removeContentTags(content, 'think');
expect(result).toBe('Outside End');
});
});

View File

@@ -1,7 +1,7 @@
import { existsSync, mkdtemp, rm, writeFile } from 'fs';
import { tmpdir } from 'os';
import path from 'path'; import path from 'path';
import { mkdtemp, rm, writeFile } from 'fs';
import { promisify } from 'util'; import { promisify } from 'util';
import { tmpdir } from 'os';
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,9 +20,7 @@ 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 () => {
if (existsSync(tempDir)) { return fsRemove(tempDir, { recursive: true });
await fsRemove(tempDir, { recursive: true });
}
}; };
return { return {

View File

@@ -3,10 +3,10 @@
"target": "ES2020", "target": "ES2020",
"lib": ["ES6", "ES2020"], "lib": ["ES6", "ES2020"],
"module": "NodeNext", "module": "CommonJS",
"resolveJsonModule": true, "resolveJsonModule": true,
"moduleResolution": "NodeNext", "moduleResolution": "Node",
"allowJs": true, "allowJs": true,
@@ -21,7 +21,9 @@
"skipLibCheck": true "skipLibCheck": true
}, },
"include": ["test/jest-setup.ts"], "include": [
"test/jest-setup.ts"
],
"exclude": ["node_modules"], "exclude": ["node_modules"],
"ts-node": { "ts-node": {
"esm": true, "esm": true,