Compare commits

...

154 Commits

Author SHA1 Message Date
di-sukharev
69b3c00a52 docs(README): update OCO_AI_PROVIDER and OCO_MODEL instructions for clarity and add valid model name options to OCO_MODEL description 2024-09-02 11:13:02 +03:00
di-sukharev
6f4afbfb52 build 2024-09-02 10:19:33 +03:00
di-sukharev
796de7b07e 3.1.2 2024-09-02 10:19:31 +03:00
unconstructive
9ad281a4ee 🔧 (ollama.ts): update client post request to use getUri method for endpoint URL construction (#404) 2024-09-02 10:18:01 +03:00
di-sukharev
1ce357b023 Merge branch 'dev' of github.com:di-sukharev/opencommit into dev 2024-09-01 18:26:33 +03:00
di-sukharev
45dd07d229 Merge branch 'master' into dev 2024-09-01 18:26:20 +03:00
di-sukharev
fa164377e4 build 2024-09-01 18:23:16 +03:00
di-sukharev
0b89767de0 3.1.1 2024-09-01 18:23:14 +03:00
GPT10
2dded4caa4 v 3.1.0 (#397) 2024-09-01 18:21:56 +03:00
GPT10
670f74ebc7 398: make why configurable (#403)
* feat(config): add OCO_WHY configuration option to enable output of change explanations in commit messages
docs(README): document the new OCO_WHY config option and its usage for outputting reasons for changes
2024-09-01 18:17:25 +03:00
Aloha
89d2aa603b feat: support pnpm (#394)
* feat: support pnpm

* fix(commitlint.ts): format commitlint config output as JSON for better readability

* test(commitlint.test.ts): update expected console output for commit message consistency
2024-09-01 16:11:26 +03:00
GPT10
8702c17758 390 add config set tests (#399)
* fix(commit.ts): improve user confirmation handling by exiting on cancel actions to prevent unintended behavior
refactor(commit.ts): streamline conditional checks for user confirmations to enhance code readability and maintainability

* refactor(commit.ts): rename spinner variables for clarity and consistency in commit message generation process
fix(commit.ts): ensure proper stopping of spinners in case of errors during commit message generation and committing process

* refactor(config.ts): extract default configuration to a constant for better maintainability and readability
refactor(config.ts): improve initGlobalConfig function to accept a configPath parameter for flexibility
feat(config.ts): enhance getConfig function to support separate paths for global and environment configurations
test(config.test.ts): update tests to reflect changes in config handling and ensure proper functionality
style(utils.ts): clean up code formatting for consistency and readability
style(tsconfig.json): adjust formatting in tsconfig.json for better clarity and maintainability

* fix(utils.ts): add existsSync check before removing temp directory to prevent errors if directory does not exist (#401)

---------

Co-authored-by: Takanori Matsumoto <matscube@gmail.com>
2024-09-01 13:28:06 +03:00
di-sukharev
60597d23eb pump v to 3.1.0 2024-08-27 17:31:40 +03:00
di-sukharev
6f04927369 build 2024-08-27 17:31:23 +03:00
di-sukharev
0c0cf9c627 Merge remote-tracking branch 'origin/master' into dev 2024-08-27 17:31:13 +03:00
GPT10
8fe8e614ac refactoring_v1 (#391) 2024-08-27 17:04:36 +03:00
Takanori Matsumoto
68c9ed359c fix(dependencies): update tr46, webidl-conversions, and whatwg-url to latest versions for compatibility and security improvements (#395) 2024-08-26 10:28:34 +03:00
di-sukharev
1b29f3a9fd build 2024-08-19 12:37:38 +03:00
GPT10
596dcd7cea 3.0.20 (#389) 2024-08-19 12:37:17 +03:00
di-sukharev
eb3be62a4f 3.0.20 2024-08-19 12:34:35 +03:00
di-sukharev
58ad1880ef Merge remote-tracking branch 'origin/dev' into marcelwiegand-master 2024-08-19 12:29:55 +03:00
marcelwiegand
5ee17235cb 🐛(anthropic.ts): fix incorrect usage of 'typeof' operator for MODEL variable 2024-08-19 11:13:37 +02:00
marcelwiegand
f9c7316eb3 🐛(openAi.ts): fix incorrect usage of 'typeof' operator for MODEL variable 2024-08-19 11:13:09 +02:00
di-sukharev
dfe2730a45 build 2024-08-19 10:42:36 +03:00
GPT10
a979ba091a 3.0.19 (#384)
* fix(config.ts): move 'gpt-4o-mini' in MODEL_LIST
* docs(config.ts): update API key validation message with detailed instructions
* refactor(config.ts): simplify model validation logic to check for string type instead of MODEL_LIST

* 3.0.19
2024-08-19 10:41:26 +03:00
di-sukharev
6bbe07a9a1 build 2024-08-19 10:28:45 +03:00
di-sukharev
0156bb9dc9 3.0.19 2024-08-19 10:26:46 +03:00
di-sukharev
e27bbd0ac1 fix(config.ts): move 'gpt-4o-mini' in MODEL_LIST
docs(config.ts): update API key validation message with detailed instructions
refactor(config.ts): simplify model validation logic to check for string type instead of MODEL_LIST
2024-08-19 10:25:24 +03:00
GPT10
f3371ac1e3 3.0.18 (#382)
* 📝 (README.md): add support for custom AI models and update documentation to reflect new environment variable OCO_ AI_PROVIDER (#351)

* feat/add gemini (#349)

* fix: prompt-module/@commitlint (#336)

* docs: spelling fix (#325)

---------

Co-authored-by: tumf <y.takahara@gmail.com>
Co-authored-by: Drew Payment <drew.payment@gmail.com>
Co-authored-by: Takanori Matsumoto <matscube@gmail.com>
Co-authored-by: Kellan Stevens <kellan@kellanstevens.com>
Co-authored-by: JMN09 <jmn09@mail.aub.edu>
Co-authored-by: JMN09 <157629053+JMN09@users.noreply.github.com>
2024-08-18 14:55:33 +03:00
di-sukharev
9cc9f9d757 build 2024-08-18 14:47:25 +03:00
di-sukharev
c8d5c53db1 3.0.18 2024-08-18 14:47:23 +03:00
di-sukharev
fc4326233d Merge remote-tracking branch 'origin/master' into dev 2024-08-18 14:23:26 +03:00
XIAOTIAN LIU
07f7a05c4b support read OCO_OLLAMA_API_URL from env to OllamaAiEngine and fix bug in getting ollma model name when slash exists (#375) 2024-08-18 14:22:30 +03:00
di-sukharev
9b2e41d255 fix(package.json): add override for whatwg-url to version 13.0.0 to fix punnycode warning 2024-08-18 14:21:27 +03:00
di-sukharev
6fad862aa5 Merge remote-tracking branch 'origin/master' into dev 2024-08-18 14:04:27 +03:00
XiaomaiTX
c425878f21 feat: allow any string as model (#367)
* refactor(anthropic.ts, openAi.ts): remove model validation against predefined list to allow any string as model, simplifying configuration and improving flexibility
2024-08-18 13:47:18 +03:00
di-sukharev
1fb592afb2 Merge branch 'murex-feature/Flowise' into dev 2024-08-18 13:42:19 +03:00
di-sukharev
684f3dadfc build 2024-08-18 13:41:58 +03:00
di-sukharev
76e6070012 Merge branch 'feature/Flowise' of github.com:murex/opencommit into murex-feature/Flowise 2024-08-18 13:41:38 +03:00
di-sukharev
cb72837866 build 2024-08-18 13:36:02 +03:00
di-sukharev
2432ef9de3 Merge remote-tracking branch 'origin/master' into dev 2024-08-18 13:35:38 +03:00
JMN09
026fd1822a Merge branch 'dev' into feature/Flowise
# Conflicts:
#	out/cli.cjs
#	out/github-action.cjs
#	src/commands/config.ts
#	src/utils/engine.ts
2024-08-14 15:30:12 +03:00
JMN09
b42a52ef24 Merge branch 'master' of https://github.com/murex/opencommit into feature/Flowise 2024-08-13 16:26:25 +03:00
XYCode Kerman
10b031ab36 fix(i18n): zh_CN is wrong (#379) 2024-08-13 10:59:48 +03:00
Isaque Borges
d63b825ae5 fix(config.ts): invalid default config (#371)
* 📝 (README.md): add support for custom AI models and update documentation to reflect new environment variable OCO_ AI_PROVIDER (#351)

* Feat/add gemini (#349)

* 🐛Fix: prompt-module/@commitlint (#336)

* fix(commitlint/utils.ts): correct variable used in search for JSON block end tag

* ♻️ (commitlint/config.ts & pwd-commitlint.ts): Refactor commitlint config loading to support both CJS and ESM modules

💡 (pwd-commitlint.ts): Add detailed comments and error handling for better clarity and robustness in commitlint module loading process

*  (package.json): Add setup script for e2e tests to install dependencies for commitlint configurations
🔧 (setup.sh): Add shell script to set up commitlint configurations for e2e tests

*  (config.ts): Add support for OCO_TEST_MOCK_TYPE configuration key to define test mock type for testing purposes
📝 (config.ts): Update documentation for OCO_TEST_MOCK_TYPE configuration key in configValidators and getConfig functions
📝 (testAi.ts): Add TEST_MOCK_TYPES constant array to define supported test mock types
📝 (testAi.ts): Update generateCommitMessage function to use OCO_TEST_MOCK_TYPE from config for different test mock types
📝 (commitlint.test.ts): Add e2e test for running "oco commitlint force" with different @commitlint versions using CJS and ESM
📝 (utils.ts): Add wait function to introduce delay in milliseconds for testing purposes

*  (commitlint.test.ts): refactor setupCommitlint function to accept a version parameter for better code organization and readability
📝 (commitlint.test.ts): add test case for commitlint@9 using CJS to ensure proper functionality and compatibility
📝 (commitlint.test.ts): add test case for commitlint@18 using CJS to ensure proper functionality and compatibility
📝 (commitlint.test.ts): add test case for commitlint@19 using ESM to ensure proper functionality and compatibility

* 🔧 (commitlint.test.ts): remove unnecessary commands to create and add index.ts file before running tests

* refactor(test/e2e/prompt-module/commitlint.test.ts): remove unused import configure
style(test/e2e/prompt-module/commitlint.test.ts): add missing semicolon for consistency
test(test/e2e/prompt-module/commitlint.test.ts): add e2e tests for @commitlint prompt-module integration

*  (e2e tests): add package.json copying to setupCommitlint for version accuracy
♻️ (commitlint config): refactor commitlint.config.js to use ES module syntax
 (package.json): specify "type": "module" to support ES module syntax

* docs: spelling fix (#325)

* 3.0.17

* ♻️ (config.ts): refactor OCO_AZURE_ENDPOINT default value from empty string to undefined resolving #352 - creates invalid config file

---------

Co-authored-by: tumf <y.takahara@gmail.com>
Co-authored-by: Drew Payment <drew.payment@gmail.com>
Co-authored-by: Takanori Matsumoto <matscube@gmail.com>
Co-authored-by: Kellan Stevens <kellan@kellanstevens.com>
Co-authored-by: di-sukharev <dim.sukharev@gmail.com>
2024-08-13 10:25:26 +03:00
JMN09
b66da48106 build 2024-08-02 17:52:16 +03:00
JMN09
6f24afc600 feature(commands/configs.ts, engine/flowise.ts, utils/engine.ts): Incorporated flowise in configuration variables, added it as an AiEngine, and added it as a return value for getEngine() 2024-08-02 17:50:34 +03:00
di-sukharev
a80dcb03c4 build 2024-07-20 11:05:26 +03:00
GPT10
bebbed856f v 3.0.17 (#366) 2024-07-20 11:03:05 +03:00
di-sukharev
2d5882c257 Merge remote-tracking branch 'origin/master' into dev 2024-07-20 11:00:56 +03:00
Ying-Shan Lin
37fb140563 Update Claude Models (#362) 2024-07-20 11:00:10 +03:00
di-sukharev
aebe7e200f 3.0.17 2024-07-20 10:59:52 +03:00
di-sukharev
6f1e4bcec6 Merge remote-tracking branch 'origin/master' into dev 2024-07-20 10:58:16 +03:00
XiaomaiTX
2059549dce feat: Add all of OpenAI GPT models (#365) 2024-07-20 10:48:00 +03:00
Kellan Stevens
8361dc6838 docs: spelling fix (#325) 2024-07-04 11:08:44 +03:00
Takanori Matsumoto
73ccae9de3 🐛Fix: prompt-module/@commitlint (#336)
* fix(commitlint/utils.ts): correct variable used in search for JSON block end tag

* ♻️ (commitlint/config.ts & pwd-commitlint.ts): Refactor commitlint config loading to support both CJS and ESM modules

💡 (pwd-commitlint.ts): Add detailed comments and error handling for better clarity and robustness in commitlint module loading process

*  (package.json): Add setup script for e2e tests to install dependencies for commitlint configurations
🔧 (setup.sh): Add shell script to set up commitlint configurations for e2e tests

*  (config.ts): Add support for OCO_TEST_MOCK_TYPE configuration key to define test mock type for testing purposes
📝 (config.ts): Update documentation for OCO_TEST_MOCK_TYPE configuration key in configValidators and getConfig functions
📝 (testAi.ts): Add TEST_MOCK_TYPES constant array to define supported test mock types
📝 (testAi.ts): Update generateCommitMessage function to use OCO_TEST_MOCK_TYPE from config for different test mock types
📝 (commitlint.test.ts): Add e2e test for running "oco commitlint force" with different @commitlint versions using CJS and ESM
📝 (utils.ts): Add wait function to introduce delay in milliseconds for testing purposes

*  (commitlint.test.ts): refactor setupCommitlint function to accept a version parameter for better code organization and readability
📝 (commitlint.test.ts): add test case for commitlint@9 using CJS to ensure proper functionality and compatibility
📝 (commitlint.test.ts): add test case for commitlint@18 using CJS to ensure proper functionality and compatibility
📝 (commitlint.test.ts): add test case for commitlint@19 using ESM to ensure proper functionality and compatibility

* 🔧 (commitlint.test.ts): remove unnecessary commands to create and add index.ts file before running tests

* refactor(test/e2e/prompt-module/commitlint.test.ts): remove unused import configure
style(test/e2e/prompt-module/commitlint.test.ts): add missing semicolon for consistency
test(test/e2e/prompt-module/commitlint.test.ts): add e2e tests for @commitlint prompt-module integration

*  (e2e tests): add package.json copying to setupCommitlint for version accuracy
♻️ (commitlint config): refactor commitlint.config.js to use ES module syntax
 (package.json): specify "type": "module" to support ES module syntax
2024-07-04 11:08:08 +03:00
Drew Payment
c58e0c62a4 Feat/add gemini (#349) 2024-07-04 11:03:54 +03:00
tumf
a4b4e65011 📝 (README.md): add support for custom AI models and update documentation to reflect new environment variable OCO_ AI_PROVIDER (#351) 2024-07-04 11:03:17 +03:00
senovr
18f52772b3 Make endpoint url for Ollama configurable (#355) 2024-07-02 20:59:55 +03:00
di-sukharev
fef25a2d06 build 2024-05-25 19:23:06 +03:00
di-sukharev
bf578da16a 3.0.16 2024-05-25 19:23:04 +03:00
di-sukharev
41330d5517 Merge branch 'master' of github.com:di-sukharev/opencommit 2024-05-25 19:22:06 +03:00
GPT10
a703fde7b2 release candidate (#348)
* test:  add the first E2E test and configuration to CI (#316)

* add tests

* Add push config (#220)

* feat: add instructions and support for configuring gpt-4-turbo (#320)

* 3.0.12

* build

* feat: add 'gpt-4-turbo' to supported models in README and config validation

---------

Co-authored-by: di-sukharev <dim.sukharev@gmail.com>

*  fix the broken E2E tests due to the addition of OCO_GITPUSH (#321)

* test(oneFile.test.ts): update test expectations to match new push prompt text

* build

* Feat: Add Claude 3 support (#318)

* 3.0.12

* build

* feat: anthropic claude 3 support

* fix: add system prompt

* fix: type check

* fix: package version

* fix: update anthropic for dependency bug fix

* feat: update build files

* feat: update version number

---------

Co-authored-by: di-sukharev <dim.sukharev@gmail.com>

* 🐛bug fix: enable to use the new format of OpenAI's project API Key (#328)

* fix(config.ts): remove validation for OCO_OPENAI_API_KEY length to accommodate variable key lengths

* build

* ♻️ refactor(config.ts): Addition of UnitTest environment and unittest for commands/config.ts#getConfig (#330)

* feat(jest.config.ts): update jest preset for TS ESM support and ignore patterns
feat(package.json): add test:unit script with NODE_OPTIONS for ESM
refactor(src/commands/config.ts): improve dotenv usage with dynamic paths
feat(src/commands/config.ts): allow custom config and env paths in getConfig
refactor(src/commands/config.ts): streamline environment variable access

feat(test/unit): add unit tests for config handling and utility functions

- Implement unit tests for `getConfig` function to ensure correct behavior
  in various scenarios including default values, global config, and local
  env file precedence.
- Add utility function `prepareFile` for creating temporary files during
  tests, facilitating testing of file-based configurations.

* feat(e2e.yml): add unit-test job to GitHub Actions for running unit tests on pull requests

* ci(test.yml): add GitHub Actions workflow for unit and e2e tests on pull requests

* refactor(config.ts): streamline environment variable access using process.env directly
test(config.test.ts): add setup and teardown for environment variables in tests to ensure test isolation

* feat(package.json): add `test:all` script to run all tests in Docker
refactor(package.json): consolidate Docker build steps into `test:docker-build` script for DRY principle
fix(package.json): ensure `test:unit:docker` and `test:e2e:docker` scripts use the same Docker image and remove container after run
chore(test/Dockerfile): remove default CMD to allow dynamic test script execution in Docker

* refactor(config.test.ts): anonymize API keys in tests for better security practices

* feat(config.test.ts): add tests for OCO_ANTHROPIC_API_KEY configuration

* refactor(config.ts): streamline path imports and remove unused DotenvParseOutput

- Simplify path module imports by removing default import and using named imports for `pathJoin` and `pathResolve`.
- Remove unused `DotenvParseOutput` import to clean up the code.

* refactor(config.test.ts): simplify API key mock values for clarity in tests

* test(config.test.ts): remove tests for default config values and redundant cases

- Removed tests that checked for default config values when no config or env files are present, as these scenarios are now handled differently.
- Eliminated tests for empty global config and local env files to streamline testing focus on actual config loading logic.
- Removed test for prioritizing local env over global config due to changes in config loading strategy, simplifying the configuration management.

* new version

* improve OCO_AI_PROVIDER=ollama[/model name] (#327)

* 🐛 (config.ts, engine/ollama.ts, utils/engine.ts): improve Ollama AI configuration and usage
 (config.ts): add support for multiple Ollama models and allow users to specify the model in their config
 (engine/ollama.ts, utils/engine.ts): refactor code to use the specified Ollama model instead of hardcoding it

* add build results

* add support for Azure OpenAI API - continue MR 167 (#324)

*  feat(api.ts): add support for Azure OpenAI API

The Azure OpenAI API is now supported in addition to the OpenAI API. The API type can be specified in the configuration file using the OPENAI_API_TYPE key. If the key is not specified, the default value is 'openai'. The AzureOpenAIApi class is added to the utils folder to handle the Azure OpenAI API calls. The createChatCompletion method is implemented in the AzureOpenAIApi class to handle the chat completion requests. The method is called in the generateCommitMessage method in the OpenAi class if the apiType is set to 'azure'.

* 🐛 fix(AzureOpenAI.ts): fix import path for AxiosRequestConfig to avoid conflicts with openai's axios dependency

In AzureOpenAI.ts, the import path for AxiosRequestConfig was changed to avoid conflicts with openai's axios dependency, which was causing lint errors.

* 🔧 fix(AzureOpenAI.ts): import RequiredError to fix error handling and remove commented out debug code

The RequiredError class was not being imported from the openai/dist/base module, causing errors to be thrown incorrectly. This has been fixed by importing the RequiredError class. Debug code has been removed and comments have been updated to reflect the changes made.

* 🔇 chore(AzureOpenAI.ts): remove console.log statement and translate Japanese comment

The commented console.log statement was removed to improve code cleanliness.

* 🔥 refactoring(api.ts, AzureOpenAI.ts): Leverage openai npm package
🐛 fix(config.ts): API Key string validation

*  (README.md): update opencommit command to set OCO_AI_PROVIDER instead of OPENAI_API_TYPE to improve consistency and clarity in configuration
♻️ (config.ts): update OCO_AI_PROVIDER enum in configValidators to include 'azure' and remove unnecessary conditionals to improve maintainability and extensibility
⬆️ (config.ts): add OCO_AZURE_API_VERSION to ConfigType and getConfig() to support new azure api version configuration
♻️ (engine/ollama.ts): add space between temperature and top_p properties to improve readability
♻️ (engine/openAi.ts): refactor OpenAi class to improve readability and maintainability by extracting configuration logic into separate switch statement
🔧 (generateCommitMessageFromGitDiff.ts): refactor MAX_TOKENS_INPUT and MAX_TOKENS_OUTPUT lines to improve readability
🔧 (generateCommitMessageFromGitDiff.ts): refactor generateCommitMessageByDiff and getMessagesPromisesByChangesInFile functions to use destructuring and improve readability
♻️ (generateCommitMessageFromGitDiff.ts): refactor getCommitMsgsPromisesFromFileDiffs function to use destructuring and improve readability
📝 (modules/commitlint/config.ts): add missing types to function parameters and improve readability by removing unnecessary comments and whitespace
📝 (modules/commitlint/utils.ts): fix indentation and add missing types to function parameters
📝 (prompts.ts): update INIT_MAIN_PROMPT description to include clarification on the use of present tense and line length
📝 (version.ts): fix import statements and add missing types to function parameters

*  (package.json): add @azure/openai dependency to support integration with Azure AI services
🔧 (config.ts): change CONFIG_KEYS.OCO_AZURE_API_VERSION to CONFIG_KEYS.OCO_AZURE_ENDPOINT to improve semantics and allow configuration of Azure endpoint URL
♻️ (config.ts): refactor configValidators to use OCO_AZURE_ENDPOINT instead of OCO_AZURE_API_VERSION and update validation message for OCO_AZURE_ENDPOINT
 (config.ts): add OCO_AZURE_ENDPOINT to getConfig function to retrieve Azure endpoint configuration from environment variables
 (azure.ts): introduce a new file azure.ts to implement Azure AI engine
 (azure.ts): implement generateCommitMessage function in Azure AI engine
 (prompts.ts): add a new line to INIT_MAIN_PROMPT to mention that changes within a single file should be described with a single commit message
♻️ (engine.ts): refactor getEngine function to add support for 'azure' as the AI provider and return the azure engine

* 📝 (prompts.ts): remove unnecessary information about crafting a concise commit message with a one single message for OCO_ONE_LINE_COMMIT configuration

* 3.0.14 (#333)

* test:  add the first E2E test and configuration to CI (#316)

* add tests

* Add push config (#220)

* feat: add instructions and support for configuring gpt-4-turbo (#320)

* 3.0.12

* build

* feat: add 'gpt-4-turbo' to supported models in README and config validation

---------

Co-authored-by: di-sukharev <dim.sukharev@gmail.com>

*  fix the broken E2E tests due to the addition of OCO_GITPUSH (#321)

* test(oneFile.test.ts): update test expectations to match new push prompt text

* build

* Feat: Add Claude 3 support (#318)

* 3.0.12

* build

* feat: anthropic claude 3 support

* fix: add system prompt

* fix: type check

* fix: package version

* fix: update anthropic for dependency bug fix

* feat: update build files

* feat: update version number

---------

Co-authored-by: di-sukharev <dim.sukharev@gmail.com>

* 🐛bug fix: enable to use the new format of OpenAI's project API Key (#328)

* fix(config.ts): remove validation for OCO_OPENAI_API_KEY length to accommodate variable key lengths

* build

* ♻️ refactor(config.ts): Addition of UnitTest environment and unittest for commands/config.ts#getConfig (#330)

* feat(jest.config.ts): update jest preset for TS ESM support and ignore patterns
feat(package.json): add test:unit script with NODE_OPTIONS for ESM
refactor(src/commands/config.ts): improve dotenv usage with dynamic paths
feat(src/commands/config.ts): allow custom config and env paths in getConfig
refactor(src/commands/config.ts): streamline environment variable access

feat(test/unit): add unit tests for config handling and utility functions

- Implement unit tests for `getConfig` function to ensure correct behavior
  in various scenarios including default values, global config, and local
  env file precedence.
- Add utility function `prepareFile` for creating temporary files during
  tests, facilitating testing of file-based configurations.

* feat(e2e.yml): add unit-test job to GitHub Actions for running unit tests on pull requests

* ci(test.yml): add GitHub Actions workflow for unit and e2e tests on pull requests

* refactor(config.ts): streamline environment variable access using process.env directly
test(config.test.ts): add setup and teardown for environment variables in tests to ensure test isolation

* feat(package.json): add `test:all` script to run all tests in Docker
refactor(package.json): consolidate Docker build steps into `test:docker-build` script for DRY principle
fix(package.json): ensure `test:unit:docker` and `test:e2e:docker` scripts use the same Docker image and remove container after run
chore(test/Dockerfile): remove default CMD to allow dynamic test script execution in Docker

* refactor(config.test.ts): anonymize API keys in tests for better security practices

* feat(config.test.ts): add tests for OCO_ANTHROPIC_API_KEY configuration

* refactor(config.ts): streamline path imports and remove unused DotenvParseOutput

- Simplify path module imports by removing default import and using named imports for `pathJoin` and `pathResolve`.
- Remove unused `DotenvParseOutput` import to clean up the code.

* refactor(config.test.ts): simplify API key mock values for clarity in tests

* test(config.test.ts): remove tests for default config values and redundant cases

- Removed tests that checked for default config values when no config or env files are present, as these scenarios are now handled differently.
- Eliminated tests for empty global config and local env files to streamline testing focus on actual config loading logic.
- Removed test for prioritizing local env over global config due to changes in config loading strategy, simplifying the configuration management.

* new version

---------

Co-authored-by: Takanori Matsumoto <matscube@gmail.com>
Co-authored-by: Moret84 <aurelienrivet@hotmail.fr>
Co-authored-by: yowatari <4982161+YOwatari@users.noreply.github.com>
Co-authored-by: metavind <94786679+metavind@users.noreply.github.com>

* 3.0.15

* build

* 🐛 (prepare-commit-msg-hook.ts): improve error message to cover missing OCO_ANTHROPIC_API_KEY and OCO_AZURE_API_KEY in addition to OCO_OPENAI_API_KEY

* 🐛 (azure.ts): fix check for OCO_AI_PROVIDER to properly assign provider variable
🐛 (azure.ts): initialize OpenAIClient only if provider is 'azure'
🔧 (Dockerfile): rearrange instructions to optimize caching by copying package.json and package-lock.json first before running npm ci and copying the rest of the files
🔧 (e2e/noChanges.test.ts): remove unnecessary line break
🔧 (e2e/oneFile.test.ts): remove unnecessary line break

---------

Co-authored-by: Takuya Ono <takuya-o@users.osdn.me>
Co-authored-by: GPT10 <57486732+di-sukharev@users.noreply.github.com>
Co-authored-by: Takanori Matsumoto <matscube@gmail.com>
Co-authored-by: Moret84 <aurelienrivet@hotmail.fr>
Co-authored-by: yowatari <4982161+YOwatari@users.noreply.github.com>
Co-authored-by: metavind <94786679+metavind@users.noreply.github.com>
Co-authored-by: di-sukharev <dim.sukharev@gmail.com>

* feat(cli.ts, commit.ts): add `--yes` flag to skip commit confirmation prompt (#341)

docs(README.md): document the `--yes` flag usage in README for user guidance

---------

Co-authored-by: Takanori Matsumoto <matscube@gmail.com>
Co-authored-by: Moret84 <aurelienrivet@hotmail.fr>
Co-authored-by: yowatari <4982161+YOwatari@users.noreply.github.com>
Co-authored-by: metavind <94786679+metavind@users.noreply.github.com>
Co-authored-by: tumf <y.takahara@gmail.com>
Co-authored-by: Jakub Rosa <jakub.rosa.95@gmail.com>
Co-authored-by: Takuya Ono <takuya-o@users.osdn.me>
Co-authored-by: Ignacio Lago <ignacio@ignaciolago.com>
2024-05-25 19:21:50 +03:00
di-sukharev
c5ee5cd8df Merge remote-tracking branch 'origin/dev' 2024-05-25 19:14:41 +03:00
Ignacio Lago
312540456a feat(cli.ts, commit.ts): add --yes flag to skip commit confirmation prompt (#341)
docs(README.md): document the `--yes` flag usage in README for user guidance
2024-05-23 10:56:35 +03:00
yowatari
7ddbaf477a feat: add instructions and support for configuring gpt-4o (#340)
* feat(config.ts): add 'gpt-4o' to supported model list for enhanced model options
docs(config.ts): update error message to include 'gpt-4o' in the list of supported models

* docs(README.md): update OCO_MODEL options to include 'gpt-4o' model
2024-05-14 14:16:21 +03:00
Kai Xu
9a0f412fff fix(config.ts): add optional config parameter to [CONFIG_KEYS.OCO_ MODEL] validator to allow for dynamic model selection based on configuration (#337)
feat(engine/ollama.ts): integrate with config command to load OCO_MODEL from configuration and use it as the default AI engine model
2024-05-12 15:56:04 +03:00
Kotaro Nishigori
7cd3ef09cb Update README.md (#335) 2024-05-09 11:23:54 +03:00
Jakub Rosa
f814c6b89d add support for Azure OpenAI API - continue MR 167 (#324)
*  feat(api.ts): add support for Azure OpenAI API

The Azure OpenAI API is now supported in addition to the OpenAI API. The API type can be specified in the configuration file using the OPENAI_API_TYPE key. If the key is not specified, the default value is 'openai'. The AzureOpenAIApi class is added to the utils folder to handle the Azure OpenAI API calls. The createChatCompletion method is implemented in the AzureOpenAIApi class to handle the chat completion requests. The method is called in the generateCommitMessage method in the OpenAi class if the apiType is set to 'azure'.

* 🐛 fix(AzureOpenAI.ts): fix import path for AxiosRequestConfig to avoid conflicts with openai's axios dependency

In AzureOpenAI.ts, the import path for AxiosRequestConfig was changed to avoid conflicts with openai's axios dependency, which was causing lint errors.

* 🔧 fix(AzureOpenAI.ts): import RequiredError to fix error handling and remove commented out debug code

The RequiredError class was not being imported from the openai/dist/base module, causing errors to be thrown incorrectly. This has been fixed by importing the RequiredError class. Debug code has been removed and comments have been updated to reflect the changes made.

* 🔇 chore(AzureOpenAI.ts): remove console.log statement and translate Japanese comment

The commented console.log statement was removed to improve code cleanliness.

* 🔥 refactoring(api.ts, AzureOpenAI.ts): Leverage openai npm package
🐛 fix(config.ts): API Key string validation

*  (README.md): update opencommit command to set OCO_AI_PROVIDER instead of OPENAI_API_TYPE to improve consistency and clarity in configuration
♻️ (config.ts): update OCO_AI_PROVIDER enum in configValidators to include 'azure' and remove unnecessary conditionals to improve maintainability and extensibility
⬆️ (config.ts): add OCO_AZURE_API_VERSION to ConfigType and getConfig() to support new azure api version configuration
♻️ (engine/ollama.ts): add space between temperature and top_p properties to improve readability
♻️ (engine/openAi.ts): refactor OpenAi class to improve readability and maintainability by extracting configuration logic into separate switch statement
🔧 (generateCommitMessageFromGitDiff.ts): refactor MAX_TOKENS_INPUT and MAX_TOKENS_OUTPUT lines to improve readability
🔧 (generateCommitMessageFromGitDiff.ts): refactor generateCommitMessageByDiff and getMessagesPromisesByChangesInFile functions to use destructuring and improve readability
♻️ (generateCommitMessageFromGitDiff.ts): refactor getCommitMsgsPromisesFromFileDiffs function to use destructuring and improve readability
📝 (modules/commitlint/config.ts): add missing types to function parameters and improve readability by removing unnecessary comments and whitespace
📝 (modules/commitlint/utils.ts): fix indentation and add missing types to function parameters
📝 (prompts.ts): update INIT_MAIN_PROMPT description to include clarification on the use of present tense and line length
📝 (version.ts): fix import statements and add missing types to function parameters

*  (package.json): add @azure/openai dependency to support integration with Azure AI services
🔧 (config.ts): change CONFIG_KEYS.OCO_AZURE_API_VERSION to CONFIG_KEYS.OCO_AZURE_ENDPOINT to improve semantics and allow configuration of Azure endpoint URL
♻️ (config.ts): refactor configValidators to use OCO_AZURE_ENDPOINT instead of OCO_AZURE_API_VERSION and update validation message for OCO_AZURE_ENDPOINT
 (config.ts): add OCO_AZURE_ENDPOINT to getConfig function to retrieve Azure endpoint configuration from environment variables
 (azure.ts): introduce a new file azure.ts to implement Azure AI engine
 (azure.ts): implement generateCommitMessage function in Azure AI engine
 (prompts.ts): add a new line to INIT_MAIN_PROMPT to mention that changes within a single file should be described with a single commit message
♻️ (engine.ts): refactor getEngine function to add support for 'azure' as the AI provider and return the azure engine

* 📝 (prompts.ts): remove unnecessary information about crafting a concise commit message with a one single message for OCO_ONE_LINE_COMMIT configuration

* 3.0.14 (#333)

* test:  add the first E2E test and configuration to CI (#316)

* add tests

* Add push config (#220)

* feat: add instructions and support for configuring gpt-4-turbo (#320)

* 3.0.12

* build

* feat: add 'gpt-4-turbo' to supported models in README and config validation

---------

Co-authored-by: di-sukharev <dim.sukharev@gmail.com>

*  fix the broken E2E tests due to the addition of OCO_GITPUSH (#321)

* test(oneFile.test.ts): update test expectations to match new push prompt text

* build

* Feat: Add Claude 3 support (#318)

* 3.0.12

* build

* feat: anthropic claude 3 support

* fix: add system prompt

* fix: type check

* fix: package version

* fix: update anthropic for dependency bug fix

* feat: update build files

* feat: update version number

---------

Co-authored-by: di-sukharev <dim.sukharev@gmail.com>

* 🐛bug fix: enable to use the new format of OpenAI's project API Key (#328)

* fix(config.ts): remove validation for OCO_OPENAI_API_KEY length to accommodate variable key lengths

* build

* ♻️ refactor(config.ts): Addition of UnitTest environment and unittest for commands/config.ts#getConfig (#330)

* feat(jest.config.ts): update jest preset for TS ESM support and ignore patterns
feat(package.json): add test:unit script with NODE_OPTIONS for ESM
refactor(src/commands/config.ts): improve dotenv usage with dynamic paths
feat(src/commands/config.ts): allow custom config and env paths in getConfig
refactor(src/commands/config.ts): streamline environment variable access

feat(test/unit): add unit tests for config handling and utility functions

- Implement unit tests for `getConfig` function to ensure correct behavior
  in various scenarios including default values, global config, and local
  env file precedence.
- Add utility function `prepareFile` for creating temporary files during
  tests, facilitating testing of file-based configurations.

* feat(e2e.yml): add unit-test job to GitHub Actions for running unit tests on pull requests

* ci(test.yml): add GitHub Actions workflow for unit and e2e tests on pull requests

* refactor(config.ts): streamline environment variable access using process.env directly
test(config.test.ts): add setup and teardown for environment variables in tests to ensure test isolation

* feat(package.json): add `test:all` script to run all tests in Docker
refactor(package.json): consolidate Docker build steps into `test:docker-build` script for DRY principle
fix(package.json): ensure `test:unit:docker` and `test:e2e:docker` scripts use the same Docker image and remove container after run
chore(test/Dockerfile): remove default CMD to allow dynamic test script execution in Docker

* refactor(config.test.ts): anonymize API keys in tests for better security practices

* feat(config.test.ts): add tests for OCO_ANTHROPIC_API_KEY configuration

* refactor(config.ts): streamline path imports and remove unused DotenvParseOutput

- Simplify path module imports by removing default import and using named imports for `pathJoin` and `pathResolve`.
- Remove unused `DotenvParseOutput` import to clean up the code.

* refactor(config.test.ts): simplify API key mock values for clarity in tests

* test(config.test.ts): remove tests for default config values and redundant cases

- Removed tests that checked for default config values when no config or env files are present, as these scenarios are now handled differently.
- Eliminated tests for empty global config and local env files to streamline testing focus on actual config loading logic.
- Removed test for prioritizing local env over global config due to changes in config loading strategy, simplifying the configuration management.

* new version

---------

Co-authored-by: Takanori Matsumoto <matscube@gmail.com>
Co-authored-by: Moret84 <aurelienrivet@hotmail.fr>
Co-authored-by: yowatari <4982161+YOwatari@users.noreply.github.com>
Co-authored-by: metavind <94786679+metavind@users.noreply.github.com>

* 3.0.15

* build

* 🐛 (prepare-commit-msg-hook.ts): improve error message to cover missing OCO_ANTHROPIC_API_KEY and OCO_AZURE_API_KEY in addition to OCO_OPENAI_API_KEY

* 🐛 (azure.ts): fix check for OCO_AI_PROVIDER to properly assign provider variable
🐛 (azure.ts): initialize OpenAIClient only if provider is 'azure'
🔧 (Dockerfile): rearrange instructions to optimize caching by copying package.json and package-lock.json first before running npm ci and copying the rest of the files
🔧 (e2e/noChanges.test.ts): remove unnecessary line break
🔧 (e2e/oneFile.test.ts): remove unnecessary line break

---------

Co-authored-by: Takuya Ono <takuya-o@users.osdn.me>
Co-authored-by: GPT10 <57486732+di-sukharev@users.noreply.github.com>
Co-authored-by: Takanori Matsumoto <matscube@gmail.com>
Co-authored-by: Moret84 <aurelienrivet@hotmail.fr>
Co-authored-by: yowatari <4982161+YOwatari@users.noreply.github.com>
Co-authored-by: metavind <94786679+metavind@users.noreply.github.com>
Co-authored-by: di-sukharev <dim.sukharev@gmail.com>
2024-05-09 11:23:00 +03:00
tumf
74024a4997 improve OCO_AI_PROVIDER=ollama[/model name] (#327)
* 🐛 (config.ts, engine/ollama.ts, utils/engine.ts): improve Ollama AI configuration and usage
 (config.ts): add support for multiple Ollama models and allow users to specify the model in their config
 (engine/ollama.ts, utils/engine.ts): refactor code to use the specified Ollama model instead of hardcoding it

* add build results
2024-05-07 10:51:24 +03:00
di-sukharev
cb7f5dd44d build 2024-05-05 19:12:25 +03:00
di-sukharev
9cf2db84a9 3.0.15 2024-05-05 19:12:23 +03:00
GPT10
ec307d561f 3.0.14 (#333)
* test:  add the first E2E test and configuration to CI (#316)

* add tests

* Add push config (#220)

* feat: add instructions and support for configuring gpt-4-turbo (#320)

* 3.0.12

* build

* feat: add 'gpt-4-turbo' to supported models in README and config validation

---------

Co-authored-by: di-sukharev <dim.sukharev@gmail.com>

*  fix the broken E2E tests due to the addition of OCO_GITPUSH (#321)

* test(oneFile.test.ts): update test expectations to match new push prompt text

* build

* Feat: Add Claude 3 support (#318)

* 3.0.12

* build

* feat: anthropic claude 3 support

* fix: add system prompt

* fix: type check

* fix: package version

* fix: update anthropic for dependency bug fix

* feat: update build files

* feat: update version number

---------

Co-authored-by: di-sukharev <dim.sukharev@gmail.com>

* 🐛bug fix: enable to use the new format of OpenAI's project API Key (#328)

* fix(config.ts): remove validation for OCO_OPENAI_API_KEY length to accommodate variable key lengths

* build

* ♻️ refactor(config.ts): Addition of UnitTest environment and unittest for commands/config.ts#getConfig (#330)

* feat(jest.config.ts): update jest preset for TS ESM support and ignore patterns
feat(package.json): add test:unit script with NODE_OPTIONS for ESM
refactor(src/commands/config.ts): improve dotenv usage with dynamic paths
feat(src/commands/config.ts): allow custom config and env paths in getConfig
refactor(src/commands/config.ts): streamline environment variable access

feat(test/unit): add unit tests for config handling and utility functions

- Implement unit tests for `getConfig` function to ensure correct behavior
  in various scenarios including default values, global config, and local
  env file precedence.
- Add utility function `prepareFile` for creating temporary files during
  tests, facilitating testing of file-based configurations.

* feat(e2e.yml): add unit-test job to GitHub Actions for running unit tests on pull requests

* ci(test.yml): add GitHub Actions workflow for unit and e2e tests on pull requests

* refactor(config.ts): streamline environment variable access using process.env directly
test(config.test.ts): add setup and teardown for environment variables in tests to ensure test isolation

* feat(package.json): add `test:all` script to run all tests in Docker
refactor(package.json): consolidate Docker build steps into `test:docker-build` script for DRY principle
fix(package.json): ensure `test:unit:docker` and `test:e2e:docker` scripts use the same Docker image and remove container after run
chore(test/Dockerfile): remove default CMD to allow dynamic test script execution in Docker

* refactor(config.test.ts): anonymize API keys in tests for better security practices

* feat(config.test.ts): add tests for OCO_ANTHROPIC_API_KEY configuration

* refactor(config.ts): streamline path imports and remove unused DotenvParseOutput

- Simplify path module imports by removing default import and using named imports for `pathJoin` and `pathResolve`.
- Remove unused `DotenvParseOutput` import to clean up the code.

* refactor(config.test.ts): simplify API key mock values for clarity in tests

* test(config.test.ts): remove tests for default config values and redundant cases

- Removed tests that checked for default config values when no config or env files are present, as these scenarios are now handled differently.
- Eliminated tests for empty global config and local env files to streamline testing focus on actual config loading logic.
- Removed test for prioritizing local env over global config due to changes in config loading strategy, simplifying the configuration management.

* new version

---------

Co-authored-by: Takanori Matsumoto <matscube@gmail.com>
Co-authored-by: Moret84 <aurelienrivet@hotmail.fr>
Co-authored-by: yowatari <4982161+YOwatari@users.noreply.github.com>
Co-authored-by: metavind <94786679+metavind@users.noreply.github.com>
2024-05-05 19:07:55 +03:00
di-sukharev
058bad95cd new version 2024-05-05 19:04:19 +03:00
di-sukharev
7469633e3d Merge remote-tracking branch 'origin/master' into dev 2024-05-05 19:03:02 +03:00
Takanori Matsumoto
278e4cb4c2 ♻️ refactor(config.ts): Addition of UnitTest environment and unittest for commands/config.ts#getConfig (#330)
* feat(jest.config.ts): update jest preset for TS ESM support and ignore patterns
feat(package.json): add test:unit script with NODE_OPTIONS for ESM
refactor(src/commands/config.ts): improve dotenv usage with dynamic paths
feat(src/commands/config.ts): allow custom config and env paths in getConfig
refactor(src/commands/config.ts): streamline environment variable access

feat(test/unit): add unit tests for config handling and utility functions

- Implement unit tests for `getConfig` function to ensure correct behavior
  in various scenarios including default values, global config, and local
  env file precedence.
- Add utility function `prepareFile` for creating temporary files during
  tests, facilitating testing of file-based configurations.

* feat(e2e.yml): add unit-test job to GitHub Actions for running unit tests on pull requests

* ci(test.yml): add GitHub Actions workflow for unit and e2e tests on pull requests

* refactor(config.ts): streamline environment variable access using process.env directly
test(config.test.ts): add setup and teardown for environment variables in tests to ensure test isolation

* feat(package.json): add `test:all` script to run all tests in Docker
refactor(package.json): consolidate Docker build steps into `test:docker-build` script for DRY principle
fix(package.json): ensure `test:unit:docker` and `test:e2e:docker` scripts use the same Docker image and remove container after run
chore(test/Dockerfile): remove default CMD to allow dynamic test script execution in Docker

* refactor(config.test.ts): anonymize API keys in tests for better security practices

* feat(config.test.ts): add tests for OCO_ANTHROPIC_API_KEY configuration

* refactor(config.ts): streamline path imports and remove unused DotenvParseOutput

- Simplify path module imports by removing default import and using named imports for `pathJoin` and `pathResolve`.
- Remove unused `DotenvParseOutput` import to clean up the code.

* refactor(config.test.ts): simplify API key mock values for clarity in tests

* test(config.test.ts): remove tests for default config values and redundant cases

- Removed tests that checked for default config values when no config or env files are present, as these scenarios are now handled differently.
- Eliminated tests for empty global config and local env files to streamline testing focus on actual config loading logic.
- Removed test for prioritizing local env over global config due to changes in config loading strategy, simplifying the configuration management.
2024-05-05 18:46:15 +03:00
Takanori Matsumoto
e19305dee2 🐛bug fix: enable to use the new format of OpenAI's project API Key (#328)
* fix(config.ts): remove validation for OCO_OPENAI_API_KEY length to accommodate variable key lengths

* build
2024-05-02 12:07:21 +03:00
metavind
673eee209d Feat: Add Claude 3 support (#318)
* 3.0.12

* build

* feat: anthropic claude 3 support

* fix: add system prompt

* fix: type check

* fix: package version

* fix: update anthropic for dependency bug fix

* feat: update build files

* feat: update version number

---------

Co-authored-by: di-sukharev <dim.sukharev@gmail.com>
2024-04-14 13:20:12 +08:00
Takanori Matsumoto
91399a0c68 fix the broken E2E tests due to the addition of OCO_GITPUSH (#321)
* test(oneFile.test.ts): update test expectations to match new push prompt text

* build
2024-04-13 20:42:32 +08:00
yowatari
a4480893cb feat: add instructions and support for configuring gpt-4-turbo (#320)
* 3.0.12

* build

* feat: add 'gpt-4-turbo' to supported models in README and config validation

---------

Co-authored-by: di-sukharev <dim.sukharev@gmail.com>
2024-04-12 11:55:41 +08:00
Moret84
c410486e30 Add push config (#220) 2024-03-29 10:57:20 +08:00
Takanori Matsumoto
5cda8b1b03 test: add the first E2E test and configuration to CI (#316)
* add tests
2024-03-25 12:01:05 +08:00
di-sukharev
0ac7211ff7 build 2024-03-18 15:22:14 +08:00
di-sukharev
670a758bee 3.0.12 2024-03-18 15:22:13 +08:00
di-sukharev
bdc98c6fa8 docs(README.md): update description text for better readability
style(git.ts): add line breaks for better code readability
2024-03-18 15:21:38 +08:00
di-sukharev
f0251d14bb build 2024-03-18 15:19:59 +08:00
di-sukharev
61f1a27377 Merge remote-tracking branch 'origin/master' into dev 2024-03-18 15:19:45 +08:00
Takanori Matsumoto
c39181e5bd fix(cli.ts): fix the commit function call by passing the correct arguments to the commit function (#313) 2024-03-18 12:29:35 +08:00
Mouadh HSOUMI
45dc2c4535 feat: add OCO_ONE_LINE_COMMIT config for enabling one line commit message (#307) 2024-03-09 12:44:12 +08:00
digitalstudium
a192441f68 Switch ollama api endpoint from /api/generate to /api/chat (#304)
* 3.0.11

* build

* docs: update ollama usage readme (#301)

Signed-off-by: Albert Simon <albert.simon.sge@mango.com>
Co-authored-by: Albert Simon <albert.simon.sge@mango.com>

* 🚨 BREAKING CHANGES 🚨

- feat(engine/ollama): add support for local models and change prompt format to improve AI performance
+ fix(engine/ollama): fix issue with local model not responding correctly to requests

The commit message is now more concise, clear, and informative. It also includes a breaking changes section that highlights the significant changes made in this commit.

---------

Signed-off-by: Albert Simon <albert.simon.sge@mango.com>
Co-authored-by: di-sukharev <dim.sukharev@gmail.com>
Co-authored-by: Albert Simon <47634918+willyw0nka@users.noreply.github.com>
Co-authored-by: Albert Simon <albert.simon.sge@mango.com>
Co-authored-by: Константин Шуткин <shutkin-kn@mosmetro.ru>
2024-03-08 10:08:52 +08:00
Kuma Taro
744bb9b11d Feat: Update OCO_MODEL to Latest Version and Remove Legacy GPT-3.5 Turbo Models (#299)
* fix(config.ts): improve code formatting for better readability
feat(config.ts): add additional supported models for OCO_MODEL configuration
feat(config.ts): update default OCO_MODEL value to 'gpt-3.5-turbo' for better compatibility

* docs(README.md): update OCO_MODEL options to include new GPT-4 models and remove
mention of 'gpt-3.5-turbo-16k' model
docs(README.md): update default OCO_MODEL to 'gpt-3.5-turbo' and provide
instructions for switching to GPT-4 models, specifically 'gpt-4-0125-preview'
and 'gpt-4-turbo-preview'
docs(README.md): update OCO_MODEL configuration in CI/CD pipeline to 'gpt-3.5-turbo'
docs(README.md): update cost information related to default and GPT-4 models in
OpenCommit
2024-03-06 22:00:35 +08:00
Albert Simon
f3adc86693 docs: update ollama usage readme (#301)
Signed-off-by: Albert Simon <albert.simon.sge@mango.com>
Co-authored-by: Albert Simon <albert.simon.sge@mango.com>
2024-03-06 21:56:53 +08:00
di-sukharev
714fac0637 build 2024-03-03 20:02:23 +08:00
di-sukharev
eaf6600299 3.0.11 2024-03-03 20:02:21 +08:00
di-sukharev
401be04b4d fix(githook.ts): update error message to include command example for better user guidance 2024-03-03 19:58:23 +08:00
di-sukharev
a9a2131ebf build 2024-03-03 19:57:37 +08:00
Maxence d'Espeuilles
7dd8094760 🔧 refactor(commit.ts): remove messageTemplate from extraArgs to avoid duplication in commit message (#279) 2024-03-03 19:56:53 +08:00
di-sukharev
a3d3363a01 build 2024-03-03 19:44:31 +08:00
di-sukharev
75d0f57f09 3.0.10 2024-03-03 19:44:30 +08:00
di-sukharev
8c92b92868 Merge branch 'dev' 2024-03-03 19:44:07 +08:00
di-sukharev
a33027b4db 🔀 refactor(commit.ts): rearrange function parameters for better readability
🎨 style(commit.ts): improve code formatting for generateCommitMessageByDiff function call
2024-03-03 19:43:40 +08:00
di-sukharev
c1797de3da build 2024-03-03 19:43:15 +08:00
di-sukharev
3d49081f6d build 2024-03-03 19:39:00 +08:00
di-sukharev
8c318d96f4 Merge branch 'dev' of github.com:di-sukharev/opencommit into dev 2024-03-03 19:37:21 +08:00
Malthe Poulsen
9b7337f67f Support Gitmoji Format in Commit Messages (#249)
* 📝 docs(prompts.ts): update prompt message to include information about GitMoji convention and descriptions of changes

📝 docs(prompts.ts): update prompt message to include information about GitMoji convention and descriptions of changes

* 🎨 (prompts.ts): import `removeConventionalCommitWord` function to remove conventional commit word from commit prompts
🐛 (prompts.ts): remove conventional commit word from `commitFix` and `commitFeat` prompts to improve clarity
📝 (removeConventionalCommitWord.ts): add `removeConventionalCommitWord` function to remove conventional commit word from commit message

* 📝 (package.json): update version from 3.0.3 to 3.0.0 to align with the latest release

* 🔧 (cli.ts): add a new flag 'fgm' to the 'flags' object to support the '--fgm' flag in the CLI command
🔧 (commit.ts): pass the value of the 'fgm' flag to the 'commit' function to enable or disable full GitMoji specification
♻️ (commit.ts): refactor the 'commit' function to accept the 'fullGitMojiSpec' parameter and pass it to the 'commit' function recursively
♻️ (generateCommitMessageFromGitDiff.ts): refactor the 'generateCommitMessageByDiff' function to accept the 'fullGitMojiSpec' parameter and pass it to the 'generateCommitMessageChatCompletionPrompt' function
♻️ (generateCommitMessageFromGitDiff.ts): refactor the 'generateCommitMessageChatCompletionPrompt' function to accept the 'fullGitMojiSpec' parameter and pass it to the 'getMainCommitPrompt' function
♻️ (generateCommitMessageFromGitDiff.ts): refactor the 'getCommitMsgsPromisesFromFileDiffs' function to accept the 'fullGitMojiSpec' parameter and pass it to the 'getMessagesPromisesByChangesInFile' function
♻️ (generateCommitMessageFromGitDiff.ts): refactor the 'getMessagesPromisesByChangesInFile' function to accept the 'fullGitMojiSpec' parameter and pass it to the 'generateCommitMessageChatCompletionPrompt' function
♻️ (prompts.ts): refactor the 'getMainCommitPrompt' function to accept the 'fullGitMojiSpec' parameter and pass it to the 'INIT_MAIN_PROMPT' function

* 📝 (README.md): add documentation for the `--fgm` flag in the `oco` command to enable the use of the full GitMoji specification

* 📝 (README.md): update flag description for using full GitMoji specification
📝 (README.md): add link to the GitMoji specification for reference

* 🔧 (README.md): fix a typo in the description of the `Use Full GitMoji Specification` flag
🔧 (api.ts): update the default value of the `apiKey` variable to a placeholder value for testing purposes

* Revert "🔧 (README.md): fix a typo in the description of the `Use Full GitMoji Specification` flag"

This reverts commit 230a4aa449.

* 🔧 (README.md): fix a typo in the description of the `Use Full GitMoji Specification` flag

* 📝 (prompts.ts): update INIT_MAIN_PROMPT content to include information about the fullGitMojiSpec flag and provide instructions on how to choose the right emoji for the commit message

---------

Co-authored-by: GPT10 <57486732+di-sukharev@users.noreply.github.com>
2024-03-03 19:36:55 +08:00
di-sukharev
0b5adf104a build 2024-03-03 19:36:23 +08:00
Jack Lukic
ec699c48bf Bug (commitling) Parse JSON blocks from return response (#297) 2024-03-03 19:23:44 +08:00
mattsalt123
c9b45492a5 feat: Add Input Token Limit Config Option (#281) 2024-02-29 23:06:23 +08:00
di-sukharev
b0b90679a4 build 2024-02-28 19:29:20 +08:00
di-sukharev
02cef105a6 3.0.9 2024-02-28 19:29:17 +08:00
di-sukharev
407ca4b244 build 2024-02-28 19:19:42 +08:00
di-sukharev
62e44e5e35 3.0.8 2024-02-28 19:19:41 +08:00
di-sukharev
03fce6f5cf build 2024-02-28 19:19:11 +08:00
di-sukharev
6155fca4b1 3.0.7 2024-02-28 19:19:09 +08:00
di-sukharev
22d4af48c7 build 2024-02-28 19:19:04 +08:00
di-sukharev
17d5a7143f Merge branch 'dev' 2024-02-28 19:18:11 +08:00
Isaac Johnston
011db5ad5e feat: add instructions and support for configuring GPT-4 Turbo (Preview) (#274)
* docs(README.md): add instructions for configuring GPT-4 Turbo (Preview)
feat(config.ts): include 'gpt-4-1106-preview' in the list of supported models

* fix(config.ts): update error message to include 'gpt-4-1106-preview' as a supported model
2024-02-28 19:17:10 +08:00
di-sukharev
89682f0397 remove stale issue action 2024-02-28 14:01:07 +08:00
di-sukharev
9ed9174b6f build 2024-02-27 22:17:08 +08:00
di-sukharev
53ae8926fa 3.0.6 2024-02-27 22:17:06 +08:00
di-sukharev
6c743ba230 chore(README.md): update link to GitHub 2023 hackathon in README
The link to the GitHub 2023 hackathon in the README has been updated to point to the correct URL. This ensures that users can easily access the relevant information about the hackathon.
2024-02-27 22:16:23 +08:00
di-sukharev
9852c36a98 build 2024-02-27 22:07:18 +08:00
di-sukharev
5f85cafc7e 3.0.5 2024-02-27 22:07:16 +08:00
di-sukharev
0591e6e81e docs(README.md): add alternative ways to run OpenCommit via npx and bunx
The README.md file has been updated to include alternative ways to run OpenCommit via the `npx opencommit` and `bunx opencommit` commands. This provides users with more options to execute the tool without the need for a global installation.
2024-02-27 22:06:08 +08:00
di-sukharev
a296892aaf build 2024-02-27 22:04:39 +08:00
di-sukharev
45958284c9 Merge branch 'master' into dev 2024-02-27 22:04:13 +08:00
Jaroslaw Weber
1d6980faf3 add ollama support (#269)
* add ollama support

---------

Co-authored-by: di-sukharev <dim.sukharev@gmail.com>
Co-authored-by: GPT10 <57486732+di-sukharev@users.noreply.github.com>
Co-authored-by: Jaroslaw Weber <jaroslaw.weber@adriel.com>
2024-02-27 22:01:13 +08:00
di-sukharev
f793f01059 Merge branch 'master' of github.com:di-sukharev/opencommit 2023-11-27 12:14:13 +08:00
di-sukharev
7deffa8ee2 update twitter 2023-11-27 12:00:28 +08:00
GPT10
84dfc85328 Update README.md
fix(README.md): GitHub hackathon link
2023-10-05 22:05:45 +08:00
di-sukharev
b79aef5fad build 2023-09-18 14:23:43 +08:00
di-sukharev
57d9cc59b5 fix(config.ts): add default value for config parameter in OCO_OPENAI_API_KEY validator function
The OCO_OPENAI_API_KEY validator function now accepts an optional config parameter with a default value of an empty object {}. This change ensures that the function can be called without providing the config parameter, improving flexibility and avoiding potential errors when the parameter is not provided.
2023-09-18 14:21:55 +08:00
di-sukharev
e599700d72 build 2023-09-09 15:52:29 +08:00
di-sukharev
2761403735 Merge remote-tracking branch 'origin/master' into dev 2023-09-09 15:52:13 +08:00
di-sukharev
e57033c4a1 chore(README.md): update information about default model and cost
The default model used by OpenCommit is ChatGPT (3.5-turbo-16k) official model. It is mentioned that the cost should not exceed $0.10 per casual working day. Additionally, it is now mentioned that users have the option to switch to gpt-4, which is better but more expensive. This update provides users with more information about the available models and their associated costs.
2023-09-05 20:30:39 +08:00
di-sukharev
ca049e4b5d chore(package.json): update deploy script to push tags before publishing to npm
The deploy script in package.json has been updated to push tags before publishing to npm. This ensures that the latest version tag is pushed to the remote repository before publishing the package.
2023-09-05 13:57:20 +08:00
di-sukharev
2d48648f52 chore(package.json): update version to 3.0.3
The version in package.json has been updated from 3.0.2 to 3.0.3. This change is made to reflect the latest version of the project.
2023-09-05 13:51:21 +08:00
di-sukharev
40297e0c6a chore(package.json): update version to 3.0.2
The version in package.json has been updated from 3.0.1 to 3.0.2. This change is made to reflect the latest version of the project.
2023-09-05 13:49:17 +08:00
di-sukharev
75f0cd47b8 chore(package.json): update version to 3.0.1
The version in package.json has been updated from 3.0.0 to 3.0.1. This change is made to reflect the new version of the project.
2023-09-05 13:48:09 +08:00
di-sukharev
a2b1890e7e build 2023-09-05 13:42:30 +08:00
di-sukharev
df705b97b7 Merge remote-tracking branch 'origin/master' into dev 2023-09-05 13:42:22 +08:00
di-sukharev
df280b7db7 fix(commitlint): use process.cwd() as fallback for process.env.PWD to ensure compatibility across platforms
The commitlint module now uses process.cwd() as a fallback when process.env.PWD is not available. This ensures compatibility across different platforms and environments when determining the path to the node_modules directory.
2023-09-05 13:39:49 +08:00
Sukharev
67dff60a7d chore(prompts.ts): update INIT_MAIN_PROMPT content to provide clearer instructions and improve readability (#245) 2023-09-03 15:39:52 +08:00
di-sukharev
08fb4d801f chore: add .npmignore file and update package.json to include specific files in the "files" field
The .npmignore file is added to the project. It currently includes the file "out/github-action.cjs" to be ignored when publishing the package to npm.

In the package.json file, the "files" field is updated to include the files "out/cli.cjs" and "out/tiktoken_bg.wasm". This ensures that these files are included when publishing the package to npm.

The "deploy" script in the package.json file is also updated to include pushing tags to the remote repository using "git push --tags". This ensures that the newly created version tags are pushed along with the code changes.
2023-09-03 15:29:29 +08:00
di-sukharev
0460a252e2 update major version 2023-09-03 15:11:14 +08:00
di-sukharev
ebeb68fd9b chore(package.json): update deploy script to use 'git push --follow-tags' instead of 'git push --tags'
The deploy script in package.json has been updated to use 'git push --follow-tags' instead of 'git push --tags'. This change ensures that tags are pushed along with the commit when deploying the application.
2023-09-03 15:07:43 +08:00
di-sukharev
a9f550fb79 Merge remote-tracking branch 'origin/master' into dev 2023-09-03 15:04:09 +08:00
Sukharev
4181c0b20d Refactoring and cleaning for v3 (#244)
* chore(README.md): update setup instructions for OpenCommit as a GitHub Action
fix(commit.ts): check if OCO_MESSAGE_TEMPLATE_PLACEHOLDER exists before replacing it in the commit message template
The README.md file has been updated to reflect the changes in the setup instructions for using OpenCommit as a GitHub Action. The new instructions provide a file template for the GitHub Action workflow and include the latest version of the OpenCommit GitHub Action.

In the commit.ts file, a fix has been made to check if the OCO_MESSAGE_TEMPLATE_PLACEHOLDER exists before replacing it in the commit message template. This ensures that the replacement is only performed when the placeholder is present, preventing any potential errors.
2023-09-03 14:59:37 +08:00
di-sukharev
765e9884dd chore(package.json): reorder scripts to improve readability and maintainability 2023-09-03 14:10:56 +08:00
di-sukharev
ea9411fa69 build 2023-09-03 14:09:45 +08:00
Sébastien Fichot
12956d7633 feat: Integrate @commitlint for Enhanced Commit Message Generation and Configuration Support (#209)
* add commitlint support
* refactor code
* improve readme text
2023-09-03 14:00:18 +08:00
Malthe Poulsen
c1627bb98c 🔧 chore(github-action.ts): add check for changes in commit messages before rebasing to improve efficiency (#222)
🐛 fix(github-action.ts): fix logic to check if there are any changes in commit messages before rebasing to ensure correct behavior
2023-09-03 12:22:05 +08:00
Max Zavodniuk
4e374aa9db fix(config.ts): pass config object to configValidators[CONFIG_KEYS.OCO_OPENAI_API_KEY] to allow access to other config values (#225)
* any length `OCO_OPENAI_API_KEY` if `OCO_OPENAI_BASE_PATH` is set
2023-09-03 12:21:13 +08:00
E Einowski
ef003bdad6 Add Detailed Explanation of OCO_MESSAGE_TEMPLATE_PLACEHOLDER Feature to README.md (#242)
* 📝 docs(README.md): add detailed explanation of OCO_MESSAGE_TEMPLATE_PLACEHOLDER feature
2023-09-03 12:18:10 +08:00
60 changed files with 92389 additions and 20428 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
.env

View File

@@ -1,28 +0,0 @@
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
#
# You can adjust the behavior by modifying this file.
# For more information, see:
# https://github.com/actions/stale
name: Mark stale issues and pull requests
on:
schedule:
- cron: '27 21 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 40
stale-issue-message: 'Stale issue message'
stale-pr-message: 'Stale pull request message'
stale-issue-label: 'no-issue-activity'
stale-pr-label: 'no-pr-activity'

46
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Testing
on: [pull_request]
jobs:
unit-test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm install
- name: Run Unit Tests
run: npm run test:unit
e2e-test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install git
run: |
sudo apt-get update
sudo apt-get install -y git
git --version
- name: Setup git
run: |
git config --global user.email "test@example.com"
git config --global user.name "Test User"
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Run E2E Tests
run: npm run test:e2e

3
.gitignore vendored
View File

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

137
README.md
View File

@@ -2,13 +2,12 @@
<div>
<img src=".github/logo-grad.svg" alt="OpenCommit logo"/>
<h1 align="center">OpenCommit</h1>
<h4 align="center">Follow the bird <a href="https://twitter.com/io_Y_oi"><img src="https://img.shields.io/twitter/follow/io_Y_oi?style=flat&label=io_Y_oi&logo=twitter&color=0bf&logoColor=fff" align="center"></a>
<h4 align="center">Author <a href="https://twitter.com/_sukharev_"><img src="https://img.shields.io/twitter/follow/_sukharev_?style=flat&label=_sukharev_&logo=twitter&color=0bf&logoColor=fff" align="center"></a>
</div>
<h2>Auto-generate meaningful commits in 1 second</h2>
<h2>Auto-generate meaningful commits in a second</h2>
<p>Killing lame commits with AI 🤯🔫</p>
<a href="https://www.npmjs.com/package/opencommit"><img src="https://img.shields.io/npm/v/opencommit" alt="Current version"></a>
<h4 align="center">🪩 Winner of GitHub 2023 HACKATHON <a href="https://twitter.com/io_Y_oi/status/1683448136973582336"><img style="width:14px; height:14px; margin-top: -4px" src=".github/github-mark-white.png" align="center"></a>
</h4>
<h4 align="center">🪩 Winner of <a href="https://twitter.com/_sukharev_/status/1683448136973582336">GitHub 2023 hackathon</a> 🪩</h4>
</div>
---
@@ -17,7 +16,7 @@
<img src=".github/opencommit-example.png" alt="OpenCommit example"/>
</div>
All the commits in this repo are authored by OpenCommit — look at [the commits](https://github.com/di-sukharev/opencommit/commit/eae7618d575ee8d2e9fff5de56da79d40c4bc5fc) to see how OpenCommit works. Emojis and long commit descriptions are configurable.
All the commits in this repo are authored by OpenCommit — look at [the commits](https://github.com/di-sukharev/opencommit/commit/eae7618d575ee8d2e9fff5de56da79d40c4bc5fc) to see how OpenCommit works. Emojis and long commit descriptions are configurable, basically everything is.
## Setup OpenCommit as a CLI tool
@@ -29,6 +28,8 @@ You can use OpenCommit by simply running it via the CLI like this `oco`. 2 secon
npm install -g opencommit
```
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.
@@ -57,6 +58,62 @@ git add <files...>
oco
```
Running `git add` is optional, `oco` will do it for you.
### Running locally with Ollama
You can also run it with local model through ollama:
- install and start ollama
- run `ollama run mistral` (do this only once, to pull model)
- run (in your project directory):
```sh
git add <files...>
oco config set OCO_AI_PROVIDER='ollama'
```
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'
oco config set OCO_MODEL='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.
You can do so by setting the `OCO_OLLAMA_API_URL` environment variable as follows:
```sh
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.
### Flags
There are multiple optional flags that can be used with the `oco` command:
#### Use Full GitMoji Specification
Link to the GitMoji specification: https://gitmoji.dev/
This flag can only be used if the `OCO_EMOJI` configuration item is set to `true`. This flag allows users to use all emojis in the GitMoji specification, By default, the GitMoji full specification is set to `false`, which only includes 10 emojis (🐛✨📝🚀✅♻️⬆️🔧🌐💡).
This is due to limit the number of tokens sent in each request. However, if you would like to use the full GitMoji specification, you can use the `--fgm` flag.
```
oco --fgm
```
#### Skip Commit Confirmation
This flag allows users to automatically commit the changes without having to manually confirm the commit message. This is useful for users who want to streamline the commit process and avoid additional steps. To use this flag, you can run the following command:
```
oco --yes
```
## Configuration
### Local per repo configuration
@@ -64,17 +121,24 @@ oco
Create a `.env` file and add OpenCommit config variables there like this:
```env
...
OCO_OPENAI_API_KEY=<your OpenAI API token>
OCO_OPENAI_MAX_TOKENS=<max response tokens from OpenAI API>
OCO_TOKENS_MAX_INPUT=<max model token limit (default: 4096)>
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_EMOJI=<boolean, add GitMoji>
OCO_MODEL=<either 'gpt-4', 'gpt-3.5-turbo-16k' (default), 'gpt-3.5-turbo-0613' or 'gpt-3.5-turbo'>
OCO_MODEL=<either 'gpt-4o', 'gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo' (default), 'gpt-3.5-turbo-0125', 'gpt-4-1106-preview', 'gpt-4-turbo-preview' or 'gpt-4-0125-preview' or any Anthropic or Ollama model or any string basically, but it should be a valid model name>
OCO_LANGUAGE=<locale, scroll to the bottom to see options>
OCO_MESSAGE_TEMPLATE_PLACEHOLDER=<message template placeholder, default: '$msg'>
OCO_PROMPT_MODULE=<either conventional-commit or @commitlint, default: conventional-commit>
OCO_ONE_LINE_COMMIT=<one line commit message, default: false>
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama>
...
```
This are not all the config options, but you get the point.
### Global config for all repos
Local config still has more priority than Global config, but you may set `OCO_MODEL` and `OCO_LOCALE` globally and set local configs for `OCO_EMOJI` and `OCO_DESCRIPTION` per repo which is more convenient.
@@ -82,7 +146,7 @@ Local config still has more priority than Global config, but you may set `OCO_MO
Simply set any of the variables above like this:
```sh
oco config set OCO_MODEL=gpt-4
oco config set OCO_MODEL=gpt-4o-mini
```
Configure [GitMoji](https://gitmoji.dev/) to preface a message.
@@ -97,14 +161,26 @@ To remove preface emojis:
oco config set OCO_EMOJI=false
```
### Switch to GPT-4 or other models
Other config options are behaving the same.
By default, OpenCommit uses `gpt-3.5-turbo-16k` model.
### Output WHY the changes were done (WIP)
You may switch to GPT-4 which performs better, but costs ~x15 times more 🤠
You can set the `OCO_WHY` config to `true` to have OpenCommit output a short description of WHY the changes were done after the commit message. Default is `false`.
To make this perform accurate we must store 'what files do' in some kind of an index or embedding and perform a lookup (kinda RAG) for the accurate git commit message. If you feel like building this comment on this ticket https://github.com/di-sukharev/opencommit/issues/398 and let's go from there together.
```sh
oco config set OCO_MODEL=gpt-4
oco config set OCO_WHY=true
```
### Switch to GPT-4 or other models
By default, OpenCommit uses `gpt-4o-mini` model.
You may switch to gpt-4o which performs better, but costs more 🤠
```sh
oco config set OCO_MODEL=gpt-4o
```
or for as a cheaper option:
@@ -113,7 +189,19 @@ or for as a cheaper option:
oco config set OCO_MODEL=gpt-3.5-turbo
```
Make sure that you spell it `gpt-4` (lowercase) and that you have API access to the 4th model. Even if you have ChatGPT+, that doesn't necessarily mean that you have API access to GPT-4.
### Switch to Azure OpenAI
By default OpenCommit uses [OpenAI](https://openai.com).
You could switch to [Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/)🚀
```sh
opencommit config set OCO_AI_PROVIDER=azure
```
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
@@ -134,6 +222,14 @@ oco config set OCO_LANGUAGE=française
The default language setting is **English**
All available languages are currently listed in the [i18n](https://github.com/di-sukharev/opencommit/tree/master/src/i18n) folder
### Push to git (gonna be deprecated)
A prompt to ushing to git is on by default but if you would like to turn it off just use:
```sh
oco config set OCO_GITPUSH=false
```
### 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.
@@ -148,7 +244,7 @@ Replace `<module>` with either `conventional-commit` or `@commitlint`.
#### Example:
To switch to using th` '@commitlint` prompt module, run:
To switch to using the `'@commitlint` prompt module, run:
```sh
oco config set OCO_PROMPT_MODULE=@commitlint
@@ -216,7 +312,7 @@ In our codebase, the implementation of this feature can be found in the followin
```javascript
commitMessage = messageTemplate.replace(
config?.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
config.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
commitMessage
);
```
@@ -273,7 +369,7 @@ Or follow the process of your IDE Source Control feature, when it calls `git com
OpenCommit is now available as a GitHub Action which automatically improves all new commits messages when you push to remote!
This is great if you want to make sure all of the commits in all of your repository branches are meaningful and not lame like `fix1` or `done2`.
This is great if you want to make sure all commits in all of your repository branches are meaningful and not lame like `fix1` or `done2`.
Create a file `.github/workflows/opencommit.yml` with the contents below:
@@ -311,11 +407,12 @@ jobs:
OCO_OPENAI_API_KEY: ${{ secrets.OCO_OPENAI_API_KEY }}
# customization
OCO_OPENAI_MAX_TOKENS: 500
OCO_TOKENS_MAX_INPUT: 4096
OCO_TOKENS_MAX_OUTPUT: 500
OCO_OPENAI_BASE_PATH: ''
OCO_DESCRIPTION: false
OCO_EMOJI: false
OCO_MODEL: gpt-3.5-turbo-16k
OCO_MODEL: gpt-4o
OCO_LANGUAGE: en
OCO_PROMPT_MODULE: conventional-commit
```
@@ -332,4 +429,6 @@ You pay for your requests to OpenAI API on your own.
OpenCommit stores your key locally.
OpenCommit by default uses ChatGPT (3.5-turbo-16k) official model, which is a lot cheaper than gpt-4.
OpenCommit by default uses 3.5-turbo model, it should not exceed $0.10 per casual working day.
You may switch to gpt-4, it's better, but more expensive.

28
jest.config.ts Normal file
View File

@@ -0,0 +1,28 @@
/**
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
import type { Config } from 'jest';
const config: Config = {
testTimeout: 100_000,
coverageProvider: 'v8',
moduleDirectories: ['node_modules', 'src'],
preset: 'ts-jest/presets/js-with-ts-esm',
setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'],
testEnvironment: 'node',
testRegex: ['.*\\.test\\.ts$'],
transformIgnorePatterns: ['node_modules/(?!cli-testing-library)'],
transform: {
'^.+\\.(ts|tsx)$': [
'ts-jest',
{
diagnostics: false,
useESM: true
}
]
}
};
export default config;

45318
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

Binary file not shown.

6834
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "opencommit",
"version": "3.0.0",
"version": "3.1.2",
"description": "Auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
"keywords": [
"git",
@@ -12,7 +12,8 @@
"aicommit",
"aicommits",
"gptcommit",
"commit"
"commit",
"ollama"
],
"main": "cli.js",
"bin": {
@@ -40,24 +41,39 @@
"scripts": {
"watch": "npm run -S build -- --sourcemap --watch",
"start": "node ./out/cli.cjs",
"ollama:start": "OCO_AI_PROVIDER='ollama' node ./out/cli.cjs",
"dev": "ts-node ./src/cli.ts",
"dev:gemini": "OCO_AI_PROVIDER='gemini' ts-node ./src/cli.ts",
"build": "rimraf out && node esbuild.config.js",
"build:push": "npm run build && git add . && git commit -m 'build' && git push",
"deploy": "npm run build:push && npm version patch && git push --follow-tags && npm publish --tag latest",
"deploy": "npm run build:push && git push --tags && npm publish --tag latest",
"deploy:patch": "npm version patch && npm run deploy",
"lint": "eslint src --ext ts && tsc --noEmit",
"format": "prettier --write src"
"format": "prettier --write src",
"test": "node --no-warnings --experimental-vm-modules $( [ -f ./node_modules/.bin/jest ] && echo ./node_modules/.bin/jest || which jest ) test/unit",
"test:all": "npm run test:unit:docker && npm run test:e2e:docker",
"test:docker-build": "docker build -t oco-test -f test/Dockerfile .",
"test:unit": "NODE_OPTIONS=--experimental-vm-modules jest test/unit",
"test:unit:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:unit",
"test:e2e": "npm run test:e2e:setup && jest test/e2e",
"test:e2e:setup": "sh test/e2e/setup.sh",
"test:e2e:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:e2e"
},
"devDependencies": {
"@commitlint/types": "^17.4.4",
"@types/ini": "^1.3.31",
"@types/inquirer": "^9.0.3",
"@types/jest": "^29.5.12",
"@types/node": "^16.18.14",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"cli-testing-library": "^2.0.2",
"dotenv": "^16.0.3",
"esbuild": "^0.15.18",
"eslint": "^8.28.0",
"jest": "^29.7.0",
"prettier": "^2.8.4",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.1",
"typescript": "^4.9.3"
},
@@ -65,10 +81,14 @@
"@actions/core": "^1.10.0",
"@actions/exec": "^1.1.1",
"@actions/github": "^5.1.1",
"@anthropic-ai/sdk": "^0.19.2",
"@azure/openai": "^1.0.0-beta.12",
"@clack/prompts": "^0.6.1",
"@dqbd/tiktoken": "^1.0.2",
"@google/generative-ai": "^0.11.4",
"@octokit/webhooks-schemas": "^6.11.0",
"@octokit/webhooks-types": "^6.11.0",
"ai": "^2.2.14",
"axios": "^1.3.4",
"chalk": "^5.2.0",
"cleye": "^1.3.2",
@@ -77,6 +97,6 @@
"ignore": "^5.2.4",
"ini": "^3.0.1",
"inquirer": "^9.1.4",
"openai": "^3.2.1"
"openai": "^4.56.0"
}
}

View File

@@ -1,115 +0,0 @@
import axios from 'axios';
import chalk from 'chalk';
import { execa } from 'execa';
import {
ChatCompletionRequestMessage,
Configuration as OpenAiApiConfiguration,
OpenAIApi
} from 'openai';
import { intro, outro } from '@clack/prompts';
import {
CONFIG_MODES,
DEFAULT_MODEL_TOKEN_LIMIT,
getConfig
} from './commands/config';
import { GenerateCommitMessageErrorEnum } from './generateCommitMessageFromGitDiff';
import { tokenCount } from './utils/tokenCount';
const config = getConfig();
let maxTokens = config?.OCO_OPENAI_MAX_TOKENS;
let basePath = config?.OCO_OPENAI_BASE_PATH;
let apiKey = config?.OCO_OPENAI_API_KEY;
const [command, mode] = process.argv.slice(2);
if (!apiKey && command !== 'config' && mode !== CONFIG_MODES.set) {
intro('opencommit');
outro(
'OCO_OPENAI_API_KEY is not set, please run `oco config set OCO_OPENAI_API_KEY=<your token>. Make sure you add payment details, so API works.`'
);
outro(
'For help look into README https://github.com/di-sukharev/opencommit#setup'
);
process.exit(1);
}
const MODEL = config?.OCO_MODEL || 'gpt-3.5-turbo';
class OpenAi {
private openAiApiConfiguration = new OpenAiApiConfiguration({
apiKey: apiKey
});
private openAI!: OpenAIApi;
constructor() {
if (basePath) {
this.openAiApiConfiguration.basePath = basePath;
}
this.openAI = new OpenAIApi(this.openAiApiConfiguration);
}
public generateCommitMessage = async (
messages: Array<ChatCompletionRequestMessage>
): Promise<string | undefined> => {
const params = {
model: MODEL,
messages,
temperature: 0,
top_p: 0.1,
max_tokens: maxTokens || 500
};
try {
const REQUEST_TOKENS = messages
.map((msg) => tokenCount(msg.content) + 4)
.reduce((a, b) => a + b, 0);
if (REQUEST_TOKENS > DEFAULT_MODEL_TOKEN_LIMIT - maxTokens) {
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
}
const { data } = await this.openAI.createChatCompletion(params);
const message = data.choices[0].message;
return message?.content;
} catch (error) {
outro(`${chalk.red('✖')} ${JSON.stringify(params)}`);
const err = error as Error;
outro(`${chalk.red('✖')} ${err?.message || err}`);
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const openAiError = error.response.data.error;
if (openAiError?.message) outro(openAiError.message);
outro(
'For help look into README https://github.com/di-sukharev/opencommit#setup'
);
}
throw err;
}
};
}
export const getOpenCommitLatestVersion = async (): Promise<
string | undefined
> => {
try {
const { stdout } = await execa('npm', ['view', 'opencommit', 'version']);
return stdout;
} catch (_) {
outro('Error while getting the latest version of opencommit');
return undefined;
}
};
export const api = new OpenAi();

View File

@@ -17,17 +17,25 @@ cli(
version: packageJSON.version,
name: 'opencommit',
commands: [configCommand, hookCommand, commitlintConfigCommand],
flags: {},
flags: {
fgm: Boolean,
yes: {
type: Boolean,
alias: 'y',
description: 'Skip commit confirmation prompt',
default: false
}
},
ignoreArgv: (type) => type === 'unknown-flag' || type === 'argument',
help: { description: packageJSON.description }
},
async () => {
async ({ flags }) => {
await checkIsLatestVersion();
if (await isHookCalled()) {
prepareCommitMessageHook();
} else {
commit(extraArgs);
commit(extraArgs, false, flags.fgm, flags.yes);
}
},
extraArgs

5
src/commands/ENUMS.ts Normal file
View File

@@ -0,0 +1,5 @@
export enum COMMANDS {
config = 'config',
hook = 'hook',
commitlint = 'commitlint'
}

View File

@@ -1,6 +1,3 @@
import chalk from 'chalk';
import { execa } from 'execa';
import {
confirm,
intro,
@@ -10,7 +7,8 @@ import {
select,
spinner
} from '@clack/prompts';
import chalk from 'chalk';
import { execa } from 'execa';
import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
import {
assertGitRepo,
@@ -32,35 +30,50 @@ const getGitRemotes = async () => {
// Check for the presence of message templates
const checkMessageTemplate = (extraArgs: string[]): string | false => {
for (const key in extraArgs) {
if (extraArgs[key].includes(config?.OCO_MESSAGE_TEMPLATE_PLACEHOLDER))
if (extraArgs[key].includes(config.OCO_MESSAGE_TEMPLATE_PLACEHOLDER))
return extraArgs[key];
}
return false;
};
const generateCommitMessageFromGitDiff = async (
diff: string,
extraArgs: string[]
): Promise<void> => {
interface GenerateCommitMessageFromGitDiffParams {
diff: string;
extraArgs: string[];
fullGitMojiSpec?: boolean;
skipCommitConfirmation?: boolean;
}
const generateCommitMessageFromGitDiff = async ({
diff,
extraArgs,
fullGitMojiSpec = false,
skipCommitConfirmation = false
}: GenerateCommitMessageFromGitDiffParams): Promise<void> => {
await assertGitRepo();
const commitSpinner = spinner();
commitSpinner.start('Generating the commit message');
const commitGenerationSpinner = spinner();
commitGenerationSpinner.start('Generating the commit message');
try {
let commitMessage = await generateCommitMessageByDiff(diff);
let commitMessage = await generateCommitMessageByDiff(
diff,
fullGitMojiSpec
);
const messageTemplate = checkMessageTemplate(extraArgs);
if (
config?.OCO_MESSAGE_TEMPLATE_PLACEHOLDER &&
config.OCO_MESSAGE_TEMPLATE_PLACEHOLDER &&
typeof messageTemplate === 'string'
) {
const messageTemplateIndex = extraArgs.indexOf(messageTemplate);
extraArgs.splice(messageTemplateIndex, 1);
commitMessage = messageTemplate.replace(
config?.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
config.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
commitMessage
);
}
commitSpinner.stop('📝 Commit message generated');
commitGenerationSpinner.stop('📝 Commit message generated');
outro(
`Generated commit message:
@@ -69,19 +82,26 @@ ${commitMessage}
${chalk.grey('——————————————————')}`
);
const isCommitConfirmedByUser = await confirm({
message: 'Confirm the commit message?'
});
const isCommitConfirmedByUser =
skipCommitConfirmation ||
(await confirm({
message: 'Confirm the commit message?'
}));
if (isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
if (isCancel(isCommitConfirmedByUser)) process.exit(1);
if (isCommitConfirmedByUser) {
const committingChangesSpinner = spinner();
committingChangesSpinner.start('Committing the changes');
const { stdout } = await execa('git', [
'commit',
'-m',
commitMessage,
...extraArgs
]);
outro(`${chalk.green('✔')} Successfully committed`);
committingChangesSpinner.stop(
`${chalk.green('✔')} Successfully committed`
);
outro(stdout);
@@ -93,12 +113,14 @@ ${chalk.grey('——————————————————')}`
process.exit(0);
}
if (remotes.length === 1) {
if (remotes.length === 1 && config.OCO_GITPUSH !== true) {
const isPushConfirmedByUser = await confirm({
message: 'Do you want to run `git push`?'
});
if (isPushConfirmedByUser && !isCancel(isPushConfirmedByUser)) {
if (isCancel(isPushConfirmedByUser)) process.exit(1);
if (isPushConfirmedByUser) {
const pushSpinner = spinner();
pushSpinner.start(`Running 'git push ${remotes[0]}'`);
@@ -126,25 +148,39 @@ ${chalk.grey('——————————————————')}`
options: remotes.map((remote) => ({ value: remote, label: remote }))
})) as string;
if (!isCancel(selectedRemote)) {
const pushSpinner = spinner();
if (isCancel(selectedRemote)) process.exit(1);
pushSpinner.start(`Running 'git push ${selectedRemote}'`);
const pushSpinner = spinner();
const { stdout } = await execa('git', ['push', selectedRemote]);
pushSpinner.start(`Running 'git push ${selectedRemote}'`);
pushSpinner.stop(
`${chalk.green(
'✔'
)} Successfully pushed all commits to ${selectedRemote}`
);
const { stdout } = await execa('git', ['push', selectedRemote]);
if (stdout) outro(stdout);
} else outro(`${chalk.gray('✖')} process cancelled`);
pushSpinner.stop(
`${chalk.green(
'✔'
)} Successfully pushed all commits to ${selectedRemote}`
);
if (stdout) outro(stdout);
}
} else {
const regenerateMessage = await confirm({
message: 'Do you want to regenerate the message?'
});
if (isCancel(regenerateMessage)) process.exit(1);
if (regenerateMessage) {
await generateCommitMessageFromGitDiff({
diff,
extraArgs,
fullGitMojiSpec
});
}
}
} catch (error) {
commitSpinner.stop('📝 Commit message generated');
commitGenerationSpinner.stop('📝 Commit message generated');
const err = error as Error;
outro(`${chalk.red('✖')} ${err?.message || err}`);
@@ -154,7 +190,9 @@ ${chalk.grey('——————————————————')}`
export async function commit(
extraArgs: string[] = [],
isStageAllFlag: Boolean = false
isStageAllFlag: Boolean = false,
fullGitMojiSpec: boolean = false,
skipCommitConfirmation: boolean = false
) {
if (isStageAllFlag) {
const changedFiles = await getChangedFiles();
@@ -190,11 +228,10 @@ export async function commit(
message: 'Do you want to stage all files and generate commit message?'
});
if (
isStageAllAndCommitConfirmedByUser &&
!isCancel(isStageAllAndCommitConfirmedByUser)
) {
await commit(extraArgs, true);
if (isCancel(isStageAllAndCommitConfirmedByUser)) process.exit(1);
if (isStageAllAndCommitConfirmedByUser) {
await commit(extraArgs, true, fullGitMojiSpec);
process.exit(1);
}
@@ -212,7 +249,7 @@ export async function commit(
await gitAdd({ files });
}
await commit(extraArgs, false);
await commit(extraArgs, false, fullGitMojiSpec);
process.exit(1);
}
@@ -223,10 +260,12 @@ export async function commit(
);
const [, generateCommitError] = await trytm(
generateCommitMessageFromGitDiff(
await getDiff({ files: stagedFiles }),
extraArgs
)
generateCommitMessageFromGitDiff({
diff: await getDiff({ files: stagedFiles }),
extraArgs,
fullGitMojiSpec,
skipCommitConfirmation
})
);
if (generateCommitError) {

View File

@@ -1,11 +1,9 @@
import { intro, outro } from '@clack/prompts';
import chalk from 'chalk';
import { command } from 'cleye';
import { intro, outro } from '@clack/prompts';
import { COMMANDS } from '../CommandsEnum';
import { configureCommitlintIntegration } from '../modules/commitlint/config';
import { getCommitlintLLMConfig } from '../modules/commitlint/utils';
import { COMMANDS } from './ENUMS';
export enum CONFIG_MODES {
get = 'get',
@@ -25,7 +23,7 @@ export const commitlintConfigCommand = command(
if (mode === CONFIG_MODES.get) {
const commitLintConfig = await getCommitlintLLMConfig();
outro(commitLintConfig.toString());
outro(JSON.stringify(commitLintConfig, null, 2));
return;
}

View File

@@ -1,45 +1,121 @@
import { intro, outro } from '@clack/prompts';
import chalk from 'chalk';
import { command } from 'cleye';
import * as dotenv from 'dotenv';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { parse as iniParse, stringify as iniStringify } from 'ini';
import { homedir } from 'os';
import { join as pathJoin } from 'path';
import { intro, outro } from '@clack/prompts';
import { COMMANDS } from '../CommandsEnum';
import { getI18nLocal } from '../i18n';
dotenv.config();
import { join as pathJoin, resolve as pathResolve } from 'path';
import { COMMANDS } from './ENUMS';
import { TEST_MOCK_TYPES } from '../engine/testAi';
import { getI18nLocal, i18n } from '../i18n';
export enum CONFIG_KEYS {
OCO_OPENAI_API_KEY = 'OCO_OPENAI_API_KEY',
OCO_OPENAI_MAX_TOKENS = 'OCO_OPENAI_MAX_TOKENS',
OCO_ANTHROPIC_API_KEY = 'OCO_ANTHROPIC_API_KEY',
OCO_AZURE_API_KEY = 'OCO_AZURE_API_KEY',
OCO_GEMINI_API_KEY = 'OCO_GEMINI_API_KEY',
OCO_GEMINI_BASE_PATH = 'OCO_GEMINI_BASE_PATH',
OCO_TOKENS_MAX_INPUT = 'OCO_TOKENS_MAX_INPUT',
OCO_TOKENS_MAX_OUTPUT = 'OCO_TOKENS_MAX_OUTPUT',
OCO_OPENAI_BASE_PATH = 'OCO_OPENAI_BASE_PATH',
OCO_DESCRIPTION = 'OCO_DESCRIPTION',
OCO_EMOJI = 'OCO_EMOJI',
OCO_MODEL = 'OCO_MODEL',
OCO_LANGUAGE = 'OCO_LANGUAGE',
OCO_WHY = 'OCO_WHY',
OCO_MESSAGE_TEMPLATE_PLACEHOLDER = 'OCO_MESSAGE_TEMPLATE_PLACEHOLDER',
OCO_PROMPT_MODULE = 'OCO_PROMPT_MODULE'
OCO_PROMPT_MODULE = 'OCO_PROMPT_MODULE',
OCO_AI_PROVIDER = 'OCO_AI_PROVIDER',
OCO_GITPUSH = 'OCO_GITPUSH', // todo: deprecate
OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT',
OCO_AZURE_ENDPOINT = 'OCO_AZURE_ENDPOINT',
OCO_TEST_MOCK_TYPE = 'OCO_TEST_MOCK_TYPE',
OCO_API_URL = 'OCO_API_URL',
OCO_OLLAMA_API_URL = 'OCO_OLLAMA_API_URL',
OCO_FLOWISE_ENDPOINT = 'OCO_FLOWISE_ENDPOINT',
OCO_FLOWISE_API_KEY = 'OCO_FLOWISE_API_KEY'
}
export const DEFAULT_MODEL_TOKEN_LIMIT = 4096;
export enum CONFIG_MODES {
get = 'get',
set = 'set'
}
export const MODEL_LIST = {
openai: [
'gpt-4o-mini',
'gpt-3.5-turbo',
'gpt-3.5-turbo-instruct',
'gpt-3.5-turbo-0613',
'gpt-3.5-turbo-0301',
'gpt-3.5-turbo-1106',
'gpt-3.5-turbo-0125',
'gpt-3.5-turbo-16k',
'gpt-3.5-turbo-16k-0613',
'gpt-3.5-turbo-16k-0301',
'gpt-4',
'gpt-4-0314',
'gpt-4-0613',
'gpt-4-1106-preview',
'gpt-4-0125-preview',
'gpt-4-turbo-preview',
'gpt-4-vision-preview',
'gpt-4-1106-vision-preview',
'gpt-4-turbo',
'gpt-4-turbo-2024-04-09',
'gpt-4-32k',
'gpt-4-32k-0314',
'gpt-4-32k-0613',
'gpt-4o',
'gpt-4o-2024-05-13',
'gpt-4o-mini-2024-07-18'
],
anthropic: [
'claude-3-5-sonnet-20240620',
'claude-3-opus-20240229',
'claude-3-sonnet-20240229',
'claude-3-haiku-20240307'
],
gemini: [
'gemini-1.5-flash',
'gemini-1.5-pro',
'gemini-1.0-pro',
'gemini-pro-vision',
'text-embedding-004'
]
};
const getDefaultModel = (provider: string | undefined): string => {
switch (provider) {
case 'ollama':
return '';
case 'anthropic':
return MODEL_LIST.anthropic[0];
case 'gemini':
return MODEL_LIST.gemini[0];
default:
return MODEL_LIST.openai[0];
}
};
export enum DEFAULT_TOKEN_LIMITS {
DEFAULT_MAX_TOKENS_INPUT = 40960,
DEFAULT_MAX_TOKENS_OUTPUT = 4096
}
const validateConfig = (
key: string,
condition: any,
validationMessage: string
) => {
if (!condition) {
outro(`${chalk.red('✖')} wrong value for ${key}: ${validationMessage}.`);
outro(
`${chalk.red('✖')} Unsupported config key ${key}: ${validationMessage}`
'For more help refer to docs https://github.com/di-sukharev/opencommit'
);
process.exit(1);
@@ -47,17 +123,65 @@ const validateConfig = (
};
export const configValidators = {
[CONFIG_KEYS.OCO_OPENAI_API_KEY](value: any, config?: any) {
validateConfig(CONFIG_KEYS.OCO_OPENAI_API_KEY, value, 'Cannot be empty');
[CONFIG_KEYS.OCO_OPENAI_API_KEY](value: any, config: any = {}) {
if (config.OCO_AI_PROVIDER !== 'openai') return value;
validateConfig(
CONFIG_KEYS.OCO_OPENAI_API_KEY,
value.startsWith('sk-'),
'Must start with "sk-"'
'OCO_OPENAI_API_KEY',
typeof value === 'string' && value.length > 0,
'Empty value is not allowed'
);
validateConfig(
CONFIG_KEYS.OCO_OPENAI_API_KEY,
config[CONFIG_KEYS.OCO_OPENAI_BASE_PATH] || value.length === 51,
'Must be 51 characters long'
'OCO_OPENAI_API_KEY',
value,
'You need to provide the OCO_OPENAI_API_KEY when OCO_AI_PROVIDER is set to "openai" (default). Run `oco config set OCO_OPENAI_API_KEY=your_key`'
);
return value;
},
[CONFIG_KEYS.OCO_AZURE_API_KEY](value: any, config: any = {}) {
if (config.OCO_AI_PROVIDER !== 'azure') return value;
validateConfig(
'OCO_AZURE_API_KEY',
!!value,
'You need to provide the OCO_AZURE_API_KEY when OCO_AI_PROVIDER is set to "azure". Run: `oco config set OCO_AZURE_API_KEY=your_key`'
);
return value;
},
[CONFIG_KEYS.OCO_GEMINI_API_KEY](value: any, config: any = {}) {
if (config.OCO_AI_PROVIDER !== 'gemini') return value;
validateConfig(
'OCO_GEMINI_API_KEY',
value || config.OCO_GEMINI_API_KEY || config.OCO_AI_PROVIDER === 'test',
'You need to provide the OCO_GEMINI_API_KEY when OCO_AI_PROVIDER is set to "gemini". Run: `oco config set OCO_GEMINI_API_KEY=your_key`'
);
return value;
},
[CONFIG_KEYS.OCO_ANTHROPIC_API_KEY](value: any, config: any = {}) {
if (config.OCO_AI_PROVIDER !== 'anthropic') return value;
validateConfig(
'ANTHROPIC_API_KEY',
!!value,
'You need to provide the OCO_ANTHROPIC_API_KEY key when OCO_AI_PROVIDER is set to "anthropic". Run: `oco config set OCO_ANTHROPIC_API_KEY=your_key`'
);
return value;
},
[CONFIG_KEYS.OCO_FLOWISE_API_KEY](value: any, config: any = {}) {
validateConfig(
CONFIG_KEYS.OCO_FLOWISE_API_KEY,
value || config.OCO_AI_PROVIDER !== 'flowise',
'You need to provide the OCO_FLOWISE_API_KEY when OCO_AI_PROVIDER is set to "flowise". Run: `oco config set OCO_FLOWISE_API_KEY=your_key`'
);
return value;
@@ -67,25 +191,28 @@ export const configValidators = {
validateConfig(
CONFIG_KEYS.OCO_DESCRIPTION,
typeof value === 'boolean',
'Must be true or false'
'Must be boolean: true or false'
);
return value;
},
[CONFIG_KEYS.OCO_OPENAI_MAX_TOKENS](value: any) {
// If the value is a string, convert it to a number.
if (typeof value === 'string') {
value = parseInt(value);
validateConfig(
CONFIG_KEYS.OCO_OPENAI_MAX_TOKENS,
!isNaN(value),
'Must be a number'
);
}
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT](value: any) {
value = parseInt(value);
validateConfig(
CONFIG_KEYS.OCO_OPENAI_MAX_TOKENS,
value ? typeof value === 'number' : undefined,
CONFIG_KEYS.OCO_TOKENS_MAX_INPUT,
!isNaN(value),
'Must be a number'
);
return value;
},
[CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT](value: any) {
value = parseInt(value);
validateConfig(
CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT,
!isNaN(value),
'Must be a number'
);
@@ -96,18 +223,21 @@ export const configValidators = {
validateConfig(
CONFIG_KEYS.OCO_EMOJI,
typeof value === 'boolean',
'Must be true or false'
'Must be boolean: true or false'
);
return value;
},
[CONFIG_KEYS.OCO_LANGUAGE](value: any) {
const supportedLanguages = Object.keys(i18n);
validateConfig(
CONFIG_KEYS.OCO_LANGUAGE,
getI18nLocal(value),
`${value} is not supported yet`
`${value} is not supported yet. Supported languages: ${supportedLanguages}`
);
return getI18nLocal(value);
},
@@ -120,19 +250,19 @@ export const configValidators = {
return value;
},
[CONFIG_KEYS.OCO_MODEL](value: any) {
[CONFIG_KEYS.OCO_MODEL](value: any, config: any = {}) {
validateConfig(
CONFIG_KEYS.OCO_MODEL,
[
'gpt-3.5-turbo',
'gpt-4',
'gpt-3.5-turbo-16k',
'gpt-3.5-turbo-0613'
].includes(value),
`${value} is not supported yet, use 'gpt-4', 'gpt-3.5-turbo-16k' (default), 'gpt-3.5-turbo-0613' or 'gpt-3.5-turbo'`
typeof value === 'string',
`${value} is not supported yet, use:\n\n ${[
...MODEL_LIST.openai,
...MODEL_LIST.anthropic,
...MODEL_LIST.gemini
].join('\n')}`
);
return value;
},
[CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER](value: any) {
validateConfig(
CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
@@ -148,93 +278,297 @@ export const configValidators = {
['conventional-commit', '@commitlint'].includes(value),
`${value} is not supported yet, use '@commitlint' or 'conventional-commit' (default)`
);
return value;
},
// todo: deprecate
[CONFIG_KEYS.OCO_GITPUSH](value: any) {
validateConfig(
CONFIG_KEYS.OCO_GITPUSH,
typeof value === 'boolean',
'Must be true or false'
);
return value;
},
[CONFIG_KEYS.OCO_AI_PROVIDER](value: any) {
if (!value) value = 'openai';
validateConfig(
CONFIG_KEYS.OCO_AI_PROVIDER,
['openai', 'anthropic', 'gemini', 'azure', 'test', 'flowise'].includes(
value
) || value.startsWith('ollama'),
`${value} is not supported yet, use 'ollama', 'anthropic', 'azure', 'gemini', 'flowise' or 'openai' (default)`
);
return value;
},
[CONFIG_KEYS.OCO_ONE_LINE_COMMIT](value: any) {
validateConfig(
CONFIG_KEYS.OCO_ONE_LINE_COMMIT,
typeof value === 'boolean',
'Must be true or false'
);
return value;
},
[CONFIG_KEYS.OCO_AZURE_ENDPOINT](value: any) {
validateConfig(
CONFIG_KEYS.OCO_AZURE_ENDPOINT,
value.includes('openai.azure.com'),
'Must be in format "https://<resource name>.openai.azure.com/"'
);
return value;
},
[CONFIG_KEYS.OCO_FLOWISE_ENDPOINT](value: any) {
validateConfig(
CONFIG_KEYS.OCO_FLOWISE_ENDPOINT,
typeof value === 'string' && value.includes(':'),
'Value must be string and should include both I.P. and port number' // Considering the possibility of DNS lookup or feeding the I.P. explicitly, there is no pattern to verify, except a column for the port number
);
return value;
},
[CONFIG_KEYS.OCO_TEST_MOCK_TYPE](value: any) {
validateConfig(
CONFIG_KEYS.OCO_TEST_MOCK_TYPE,
TEST_MOCK_TYPES.includes(value),
`${value} is not supported yet, use ${TEST_MOCK_TYPES.map(
(t) => `'${t}'`
).join(', ')}`
);
return value;
},
[CONFIG_KEYS.OCO_OLLAMA_API_URL](value: any) {
validateConfig(
CONFIG_KEYS.OCO_OLLAMA_API_URL,
typeof value === 'string' && value.startsWith('http'),
`${value} is not a valid URL. It should start with 'http://' or 'https://'.`
);
return value;
}
};
export enum OCO_AI_PROVIDER_ENUM {
OLLAMA = 'ollama',
OPENAI = 'openai',
ANTHROPIC = 'anthropic',
GEMINI = 'gemini',
AZURE = 'azure',
TEST = 'test',
FLOWISE = 'flowise'
}
export type ConfigType = {
[key in CONFIG_KEYS]?: any;
[CONFIG_KEYS.OCO_OPENAI_API_KEY]?: string;
[CONFIG_KEYS.OCO_ANTHROPIC_API_KEY]?: string;
[CONFIG_KEYS.OCO_AZURE_API_KEY]?: string;
[CONFIG_KEYS.OCO_GEMINI_API_KEY]?: string;
[CONFIG_KEYS.OCO_GEMINI_BASE_PATH]?: string;
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT]: number;
[CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT]: number;
[CONFIG_KEYS.OCO_OPENAI_BASE_PATH]?: string;
[CONFIG_KEYS.OCO_DESCRIPTION]: boolean;
[CONFIG_KEYS.OCO_EMOJI]: boolean;
[CONFIG_KEYS.OCO_WHY]: boolean;
[CONFIG_KEYS.OCO_MODEL]: string;
[CONFIG_KEYS.OCO_LANGUAGE]: string;
[CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER]: string;
[CONFIG_KEYS.OCO_PROMPT_MODULE]: OCO_PROMPT_MODULE_ENUM;
[CONFIG_KEYS.OCO_AI_PROVIDER]: OCO_AI_PROVIDER_ENUM;
[CONFIG_KEYS.OCO_GITPUSH]: boolean;
[CONFIG_KEYS.OCO_ONE_LINE_COMMIT]: boolean;
[CONFIG_KEYS.OCO_AZURE_ENDPOINT]?: string;
[CONFIG_KEYS.OCO_TEST_MOCK_TYPE]: string;
[CONFIG_KEYS.OCO_API_URL]?: string;
[CONFIG_KEYS.OCO_OLLAMA_API_URL]?: string;
[CONFIG_KEYS.OCO_FLOWISE_ENDPOINT]: string;
[CONFIG_KEYS.OCO_FLOWISE_API_KEY]?: string;
};
const configPath = pathJoin(homedir(), '.opencommit');
const defaultConfigPath = pathJoin(homedir(), '.opencommit');
const defaultEnvPath = pathResolve(process.cwd(), '.env');
export const getConfig = (): ConfigType | null => {
const configFromEnv = {
OCO_OPENAI_API_KEY: process.env.OCO_OPENAI_API_KEY,
OCO_OPENAI_MAX_TOKENS: process.env.OCO_OPENAI_MAX_TOKENS
? Number(process.env.OCO_OPENAI_MAX_TOKENS)
: undefined,
OCO_OPENAI_BASE_PATH: process.env.OCO_OPENAI_BASE_PATH,
OCO_DESCRIPTION: process.env.OCO_DESCRIPTION === 'true' ? true : false,
OCO_EMOJI: process.env.OCO_EMOJI === 'true' ? true : false,
OCO_MODEL: process.env.OCO_MODEL || 'gpt-3.5-turbo-16k',
OCO_LANGUAGE: process.env.OCO_LANGUAGE || 'en',
OCO_MESSAGE_TEMPLATE_PLACEHOLDER:
process.env.OCO_MESSAGE_TEMPLATE_PLACEHOLDER || '$msg',
OCO_PROMPT_MODULE: process.env.OCO_PROMPT_MODULE || 'conventional-commit'
};
const assertConfigsAreValid = (config: Record<string, any>) => {
for (const [key, value] of Object.entries(config)) {
if (!value) continue;
const configExists = existsSync(configPath);
if (!configExists) return configFromEnv;
const configFile = readFileSync(configPath, 'utf8');
const config = iniParse(configFile);
for (const configKey of Object.keys(config)) {
if (
!config[configKey] ||
['null', 'undefined'].includes(config[configKey])
) {
config[configKey] = undefined;
if (typeof value === 'string' && ['null', 'undefined'].includes(value)) {
config[key] = undefined;
continue;
}
try {
const validator = configValidators[configKey as CONFIG_KEYS];
const validValue = validator(
config[configKey] ?? configFromEnv[configKey as CONFIG_KEYS],
config
);
config[configKey] = validValue;
try {
const validate = configValidators[key as CONFIG_KEYS];
validate(value, config);
} catch (error) {
outro(
`'${configKey}' name is invalid, it should be either 'OCO_${configKey.toUpperCase()}' or it doesn't exist.`
);
outro(`Unknown '${key}' config option or missing validator.`);
outro(
`Manually fix the '.env' file or global '~/.opencommit' config file.`
);
process.exit(1);
}
}
};
enum OCO_PROMPT_MODULE_ENUM {
CONVENTIONAL_COMMIT = 'conventional-commit',
COMMITLINT = '@commitlint'
}
export const DEFAULT_CONFIG = {
OCO_TOKENS_MAX_INPUT: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT,
OCO_TOKENS_MAX_OUTPUT: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT,
OCO_DESCRIPTION: false,
OCO_EMOJI: false,
OCO_MODEL: getDefaultModel('openai'),
OCO_LANGUAGE: 'en',
OCO_MESSAGE_TEMPLATE_PLACEHOLDER: '$msg',
OCO_PROMPT_MODULE: OCO_PROMPT_MODULE_ENUM.CONVENTIONAL_COMMIT,
OCO_AI_PROVIDER: OCO_AI_PROVIDER_ENUM.OPENAI,
OCO_ONE_LINE_COMMIT: false,
OCO_TEST_MOCK_TYPE: 'commit-message',
OCO_FLOWISE_ENDPOINT: ':',
OCO_WHY: false,
OCO_GITPUSH: true // todo: deprecate
};
const initGlobalConfig = (configPath: string = defaultConfigPath) => {
writeFileSync(configPath, iniStringify(DEFAULT_CONFIG), 'utf8');
return DEFAULT_CONFIG;
};
const parseEnvVarValue = (value?: any) => {
try {
return JSON.parse(value);
} catch (error) {
return value;
}
};
const getEnvConfig = (envPath: string) => {
dotenv.config({ path: envPath });
return {
OCO_MODEL: process.env.OCO_MODEL,
OCO_OPENAI_API_KEY: process.env.OCO_OPENAI_API_KEY,
OCO_ANTHROPIC_API_KEY: process.env.OCO_ANTHROPIC_API_KEY,
OCO_AZURE_API_KEY: process.env.OCO_AZURE_API_KEY,
OCO_GEMINI_API_KEY: process.env.OCO_GEMINI_API_KEY,
OCO_FLOWISE_API_KEY: process.env.OCO_FLOWISE_API_KEY,
OCO_TOKENS_MAX_INPUT: parseEnvVarValue(process.env.OCO_TOKENS_MAX_INPUT),
OCO_TOKENS_MAX_OUTPUT: parseEnvVarValue(process.env.OCO_TOKENS_MAX_OUTPUT),
OCO_OPENAI_BASE_PATH: process.env.OCO_OPENAI_BASE_PATH,
OCO_GEMINI_BASE_PATH: process.env.OCO_GEMINI_BASE_PATH,
OCO_AZURE_ENDPOINT: process.env.OCO_AZURE_ENDPOINT,
OCO_FLOWISE_ENDPOINT: process.env.OCO_FLOWISE_ENDPOINT,
OCO_OLLAMA_API_URL: process.env.OCO_OLLAMA_API_URL,
OCO_DESCRIPTION: parseEnvVarValue(process.env.OCO_DESCRIPTION),
OCO_EMOJI: parseEnvVarValue(process.env.OCO_EMOJI),
OCO_LANGUAGE: process.env.OCO_LANGUAGE,
OCO_MESSAGE_TEMPLATE_PLACEHOLDER:
process.env.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
OCO_PROMPT_MODULE: process.env.OCO_PROMPT_MODULE as OCO_PROMPT_MODULE_ENUM,
OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER as OCO_AI_PROVIDER_ENUM,
OCO_ONE_LINE_COMMIT: parseEnvVarValue(process.env.OCO_ONE_LINE_COMMIT),
OCO_TEST_MOCK_TYPE: process.env.OCO_TEST_MOCK_TYPE,
OCO_GITPUSH: parseEnvVarValue(process.env.OCO_GITPUSH) // todo: deprecate
};
};
const getGlobalConfig = (configPath: string) => {
let globalConfig: ConfigType;
const isGlobalConfigFileExist = existsSync(configPath);
if (!isGlobalConfigFileExist) globalConfig = initGlobalConfig(configPath);
else {
const configFile = readFileSync(configPath, 'utf8');
globalConfig = iniParse(configFile) as ConfigType;
}
return globalConfig;
};
/**
* Merges two configs.
* Env config takes precedence over global ~/.opencommit config file
* @param main - env config
* @param fallback - global ~/.opencommit config file
* @returns merged config
*/
const mergeConfigs = (main: Partial<ConfigType>, fallback: ConfigType) =>
Object.keys(CONFIG_KEYS).reduce((acc, key) => {
acc[key] = parseEnvVarValue(main[key] ?? fallback[key]);
return acc;
}, {} as ConfigType);
interface GetConfigOptions {
globalPath?: string;
envPath?: string;
}
export const getConfig = ({
envPath = defaultEnvPath,
globalPath = defaultConfigPath
}: GetConfigOptions = {}): ConfigType => {
const envConfig = getEnvConfig(envPath);
const globalConfig = getGlobalConfig(globalPath);
const config = mergeConfigs(envConfig, globalConfig);
return config;
};
export const setConfig = (keyValues: [key: string, value: string][]) => {
const config = getConfig() || {};
export const setConfig = (
keyValues: [key: string, value: string][],
globalConfigPath: string = defaultConfigPath
) => {
const config = getConfig({
globalPath: globalConfigPath
});
for (const [configKey, configValue] of keyValues) {
if (!configValidators.hasOwnProperty(configKey)) {
throw new Error(`Unsupported config key: ${configKey}`);
for (let [key, value] of keyValues) {
if (!configValidators.hasOwnProperty(key)) {
const supportedKeys = Object.keys(configValidators).join('\n');
throw new Error(
`Unsupported config key: ${key}. Expected keys are:\n\n${supportedKeys}.\n\nFor more help refer to our docs: https://github.com/di-sukharev/opencommit`
);
}
let parsedConfigValue;
try {
parsedConfigValue = JSON.parse(configValue);
parsedConfigValue = JSON.parse(value);
} catch (error) {
parsedConfigValue = configValue;
parsedConfigValue = value;
}
const validValue =
configValidators[configKey as CONFIG_KEYS](parsedConfigValue);
config[configKey as CONFIG_KEYS] = validValue;
const validValue = configValidators[key as CONFIG_KEYS](
parsedConfigValue,
config
);
config[key] = validValue;
}
writeFileSync(configPath, iniStringify(config), 'utf8');
writeFileSync(globalConfigPath, iniStringify(config), 'utf8');
outro(`${chalk.green('✔')} Config successfully set`);
outro(`${chalk.green('✔')} config successfully set`);
};
export const configCommand = command(
@@ -243,9 +577,9 @@ export const configCommand = command(
parameters: ['<mode>', '<key=values...>']
},
async (argv) => {
intro('opencommit — config');
try {
const { mode, keyValues } = argv._;
intro(`COMMAND: config ${mode} ${keyValues}`);
if (mode === CONFIG_MODES.get) {
const config = getConfig() || {};

View File

@@ -1,13 +1,11 @@
import { intro, outro } from '@clack/prompts';
import chalk from 'chalk';
import { command } from 'cleye';
import { existsSync } from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { intro, outro } from '@clack/prompts';
import { COMMANDS } from '../CommandsEnum.js';
import { assertGitRepo, getCoreHooksPath } from '../utils/git.js';
import { COMMANDS } from './ENUMS';
const HOOK_NAME = 'prepare-commit-msg';
const DEFAULT_SYMLINK_URL = path.join('.git', 'hooks', HOOK_NAME);
@@ -94,7 +92,7 @@ export const hookCommand = command(
}
throw new Error(
`Unsupported mode: ${mode}. Supported modes are: 'set' or 'unset'`
`Unsupported mode: ${mode}. Supported modes are: 'set' or 'unset'. Run: \`oco hook set\``
);
} catch (error) {
outro(`${chalk.red('✖')} ${error}`);

View File

@@ -39,9 +39,13 @@ export const prepareCommitMessageHook = async (
const config = getConfig();
if (!config?.OCO_OPENAI_API_KEY) {
if (
!config.OCO_OPENAI_API_KEY &&
!config.OCO_ANTHROPIC_API_KEY &&
!config.OCO_AZURE_API_KEY
) {
throw new Error(
'No OPEN_AI_API exists. Set your OPEN_AI_API=<key> in ~/.opencommit'
'No OPEN_AI_API or OCO_ANTHROPIC_API_KEY or OCO_AZURE_API_KEY exists. Set your key in ~/.opencommit'
);
}

28
src/engine/Engine.ts Normal file
View File

@@ -0,0 +1,28 @@
import AnthropicClient from '@anthropic-ai/sdk';
import { OpenAIClient as AzureOpenAIClient } from '@azure/openai';
import { GoogleGenerativeAI as GeminiClient } from '@google/generative-ai';
import { AxiosInstance as RawAxiosClient } from 'axios';
import { OpenAI as OpenAIClient } from 'openai';
export interface AiEngineConfig {
apiKey: string;
model: string;
maxTokensOutput: number;
maxTokensInput: number;
baseURL?: string;
}
type Client =
| OpenAIClient
| AzureOpenAIClient
| AnthropicClient
| RawAxiosClient
| GeminiClient;
export interface AiEngine {
config: AiEngineConfig;
client: Client;
generateCommitMessage(
messages: Array<OpenAIClient.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | null | undefined>;
}

78
src/engine/anthropic.ts Normal file
View File

@@ -0,0 +1,78 @@
import AnthropicClient from '@anthropic-ai/sdk';
import {
MessageCreateParamsNonStreaming,
MessageParam
} from '@anthropic-ai/sdk/resources/messages.mjs';
import { outro } from '@clack/prompts';
import axios from 'axios';
import chalk from 'chalk';
import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine';
interface AnthropicConfig extends AiEngineConfig {}
export class AnthropicEngine implements AiEngine {
config: AnthropicConfig;
client: AnthropicClient;
constructor(config) {
this.config = config;
this.client = new AnthropicClient({ apiKey: this.config.apiKey });
}
public generateCommitMessage = async (
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | undefined> => {
const systemMessage = messages.find((msg) => msg.role === 'system')
?.content as string;
const restMessages = messages.filter(
(msg) => msg.role !== 'system'
) as MessageParam[];
const params: MessageCreateParamsNonStreaming = {
model: this.config.model,
system: systemMessage,
messages: restMessages,
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 data = await this.client.messages.create(params);
const message = data?.content[0].text;
return message;
} catch (error) {
const err = error as Error;
outro(`${chalk.red('✖')} ${err?.message || err}`);
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const anthropicAiError = error.response.data.error;
if (anthropicAiError?.message) outro(anthropicAiError.message);
outro(
'For help look into README https://github.com/di-sukharev/opencommit#setup'
);
}
throw err;
}
};
}

77
src/engine/azure.ts Normal file
View File

@@ -0,0 +1,77 @@
import {
AzureKeyCredential,
OpenAIClient as AzureOpenAIClient
} from '@azure/openai';
import { outro } from '@clack/prompts';
import axios from 'axios';
import chalk from 'chalk';
import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine';
interface AzureAiEngineConfig extends AiEngineConfig {
baseURL: string;
apiKey: string;
}
export class AzureEngine implements AiEngine {
config: AzureAiEngineConfig;
client: AzureOpenAIClient;
constructor(config: AzureAiEngineConfig) {
this.config = config;
this.client = new AzureOpenAIClient(
this.config.baseURL,
new AzureKeyCredential(this.config.apiKey)
);
}
generateCommitMessage = async (
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | undefined> => {
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 data = await this.client.getChatCompletions(
this.config.model,
messages
);
const message = data.choices[0].message;
if (message?.content === null) {
return undefined;
}
return message?.content;
} catch (error) {
outro(`${chalk.red('✖')} ${this.config.model}`);
const err = error as Error;
outro(`${chalk.red('✖')} ${JSON.stringify(error)}`);
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const openAiError = error.response.data.error;
if (openAiError?.message) outro(openAiError.message);
outro(
'For help look into README https://github.com/di-sukharev/opencommit#setup'
);
}
throw err;
}
};
}

45
src/engine/flowise.ts Normal file
View File

@@ -0,0 +1,45 @@
import axios, { AxiosInstance } from 'axios';
import { OpenAI } from 'openai';
import { AiEngine, AiEngineConfig } from './Engine';
interface FlowiseAiConfig extends AiEngineConfig {}
export class FlowiseAi implements AiEngine {
config: FlowiseAiConfig;
client: AxiosInstance;
constructor(config) {
this.config = config;
this.client = axios.create({
url: `${config.baseURL}/${config.apiKey}`,
headers: { 'Content-Type': 'application/json' }
});
}
async generateCommitMessage(
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | undefined> {
const gitDiff = (messages[messages.length - 1]?.content as string)
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
const payload = {
question: gitDiff,
overrideConfig: {
systemMessagePrompt: messages[0]?.content
},
history: messages.slice(1, -1)
};
try {
const response = await this.client.post('', payload);
const message = response.data;
return message?.text;
} catch (err: any) {
const message = err.response?.data?.error ?? err.message;
throw new Error('local model issues. details: ' + message);
}
}
}

88
src/engine/gemini.ts Normal file
View File

@@ -0,0 +1,88 @@
import {
Content,
GoogleGenerativeAI,
HarmBlockThreshold,
HarmCategory,
Part
} from '@google/generative-ai';
import axios from 'axios';
import { OpenAI } from 'openai';
import { AiEngine, AiEngineConfig } from './Engine';
interface GeminiConfig extends AiEngineConfig {}
export class Gemini implements AiEngine {
config: GeminiConfig;
client: GoogleGenerativeAI;
constructor(config) {
this.client = new GoogleGenerativeAI(config.apiKey);
this.config = config;
}
async generateCommitMessage(
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | undefined> {
const systemInstruction = messages
.filter((m) => m.role === 'system')
.map((m) => m.content)
.join('\n');
const gemini = this.client.getGenerativeModel({
model: this.config.model,
systemInstruction
});
const contents = messages
.filter((m) => m.role !== 'system')
.map(
(m) =>
({
parts: [{ text: m.content } as Part],
role: m.role === 'user' ? m.role : 'model'
} as Content)
);
try {
const result = await gemini.generateContent({
contents,
safetySettings: [
{
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
},
{
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
},
{
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
},
{
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
}
],
generationConfig: {
maxOutputTokens: this.config.maxTokensOutput,
temperature: 0,
topP: 0.1
}
});
return result.response.text();
} catch (error) {
const err = error as Error;
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const geminiError = error.response.data.error;
if (geminiError) throw new Error(geminiError?.message);
}
throw err;
}
}
}

44
src/engine/ollama.ts Normal file
View File

@@ -0,0 +1,44 @@
import axios, { AxiosInstance } from 'axios';
import { OpenAI } from 'openai';
import { AiEngine, AiEngineConfig } from './Engine';
interface OllamaConfig extends AiEngineConfig {}
export class OllamaAi implements AiEngine {
config: OllamaConfig;
client: AxiosInstance;
constructor(config) {
this.config = config;
this.client = axios.create({
url: config.baseURL
? `${config.baseURL}/${config.apiKey}`
: 'http://localhost:11434/api/chat',
headers: { 'Content-Type': 'application/json' }
});
}
async generateCommitMessage(
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | undefined> {
const params = {
model: this.config.model ?? 'mistral',
messages,
options: { temperature: 0, top_p: 0.1 },
stream: false
};
try {
const response = await this.client.post(
this.client.getUri(this.config),
params
);
const message = response.data.message;
return message?.content;
} catch (err: any) {
const message = err.response?.data?.error ?? err.message;
throw new Error(`Ollama provider error: ${message}`);
}
}
}

59
src/engine/openAi.ts Normal file
View File

@@ -0,0 +1,59 @@
import axios from 'axios';
import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine';
interface OpenAiConfig extends AiEngineConfig {}
export class OpenAiEngine implements AiEngine {
config: OpenAiConfig;
client: OpenAI;
constructor(config: OpenAiConfig) {
this.config = config;
this.client = new OpenAI({ apiKey: config.apiKey });
}
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;
return message?.content;
} 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;
}
};
}

47
src/engine/testAi.ts Normal file
View File

@@ -0,0 +1,47 @@
import { OpenAI } from 'openai';
import { AiEngine } from './Engine';
export const TEST_MOCK_TYPES = [
'commit-message',
'prompt-module-commitlint-config'
] as const;
export type TestMockType = (typeof TEST_MOCK_TYPES)[number];
type TestAiEngine = Partial<AiEngine> & {
mockType: TestMockType;
};
export class TestAi implements TestAiEngine {
mockType: TestMockType;
// those are not used in the test engine
config: any;
client: any;
// ---
constructor(mockType: TestMockType) {
this.mockType = mockType;
}
async generateCommitMessage(
_messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | undefined> {
switch (this.mockType) {
case 'commit-message':
return 'fix(testAi.ts): test commit message';
case 'prompt-module-commitlint-config':
return (
`{\n` +
` "localLanguage": "english",\n` +
` "commitFix": "fix(server): Change 'port' variable to uppercase 'PORT'",\n` +
` "commitFeat": "feat(server): Allow server to listen on a port specified through environment variable",\n` +
` "commitDescription": "Change 'port' variable to uppercase 'PORT'. Allow server to listen on a port specified through environment variable."\n` +
`}`
);
default:
throw Error('unsupported test mock type');
}
}
}

View File

@@ -1,25 +1,27 @@
import {
ChatCompletionRequestMessage,
ChatCompletionRequestMessageRoleEnum
} from 'openai';
import { api } from './api';
import { DEFAULT_MODEL_TOKEN_LIMIT, getConfig } from './commands/config';
import { OpenAI } from 'openai';
import { DEFAULT_TOKEN_LIMITS, getConfig } from './commands/config';
import { getMainCommitPrompt } from './prompts';
import { getEngine } from './utils/engine';
import { mergeDiffs } from './utils/mergeDiffs';
import { tokenCount } from './utils/tokenCount';
const config = getConfig();
const MAX_TOKENS_INPUT =
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 (
diff: string
): Promise<Array<ChatCompletionRequestMessage>> => {
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt();
diff: string,
fullGitMojiSpec: boolean
): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => {
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec);
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
chatContextAsCompletionRequest.push({
role: ChatCompletionRequestMessageRoleEnum.User,
role: 'user',
content: diff
});
@@ -29,45 +31,52 @@ const generateCommitMessageChatCompletionPrompt = async (
export enum GenerateCommitMessageErrorEnum {
tooMuchTokens = 'TOO_MUCH_TOKENS',
internalError = 'INTERNAL_ERROR',
emptyMessage = 'EMPTY_MESSAGE'
emptyMessage = 'EMPTY_MESSAGE',
outputTokensTooHigh = `Token limit exceeded, OCO_TOKENS_MAX_OUTPUT must not be much higher than the default ${DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT} tokens.`
}
const ADJUSTMENT_FACTOR = 20;
export const generateCommitMessageByDiff = async (
diff: string
diff: string,
fullGitMojiSpec: boolean = false
): Promise<string> => {
try {
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt();
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec);
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
(msg) => tokenCount(msg.content) + 4
(msg) => tokenCount(msg.content as string) + 4
).reduce((a, b) => a + b, 0);
const MAX_REQUEST_TOKENS =
DEFAULT_MODEL_TOKEN_LIMIT -
MAX_TOKENS_INPUT -
ADJUSTMENT_FACTOR -
INIT_MESSAGES_PROMPT_LENGTH -
config?.OCO_OPENAI_MAX_TOKENS;
MAX_TOKENS_OUTPUT;
if (tokenCount(diff) >= MAX_REQUEST_TOKENS) {
const commitMessagePromises = await getCommitMsgsPromisesFromFileDiffs(
diff,
MAX_REQUEST_TOKENS
MAX_REQUEST_TOKENS,
fullGitMojiSpec
);
const commitMessages = [];
const commitMessages = [] as string[];
for (const promise of commitMessagePromises) {
commitMessages.push(await promise);
commitMessages.push((await promise) as string);
await delay(2000);
}
return commitMessages.join('\n\n');
}
const messages = await generateCommitMessageChatCompletionPrompt(diff);
const messages = await generateCommitMessageChatCompletionPrompt(
diff,
fullGitMojiSpec
);
const commitMessage = await api.generateCommitMessage(messages);
const engine = getEngine();
const commitMessage = await engine.generateCommitMessage(messages);
if (!commitMessage)
throw new Error(GenerateCommitMessageErrorEnum.emptyMessage);
@@ -81,7 +90,8 @@ export const generateCommitMessageByDiff = async (
function getMessagesPromisesByChangesInFile(
fileDiff: string,
separator: string,
maxChangeLength: number
maxChangeLength: number,
fullGitMojiSpec: boolean
) {
const hunkHeaderSeparator = '@@ ';
const [fileHeader, ...fileDiffByLines] = fileDiff.split(hunkHeaderSeparator);
@@ -92,7 +102,7 @@ function getMessagesPromisesByChangesInFile(
maxChangeLength
);
const lineDiffsWithHeader = [];
const lineDiffsWithHeader = [] as string[];
for (const change of mergedChanges) {
const totalChange = fileHeader + change;
if (tokenCount(totalChange) > maxChangeLength) {
@@ -104,13 +114,15 @@ function getMessagesPromisesByChangesInFile(
}
}
const engine = getEngine();
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map(
async (lineDiff) => {
const messages = await generateCommitMessageChatCompletionPrompt(
separator + lineDiff
separator + lineDiff,
fullGitMojiSpec
);
return api.generateCommitMessage(messages);
return engine.generateCommitMessage(messages);
}
);
@@ -119,9 +131,13 @@ function getMessagesPromisesByChangesInFile(
function splitDiff(diff: string, maxChangeLength: number) {
const lines = diff.split('\n');
const splitDiffs = [];
const splitDiffs = [] as string[];
let currentDiff = '';
if (maxChangeLength <= 0) {
throw new Error(GenerateCommitMessageErrorEnum.outputTokensTooHigh);
}
for (let line of lines) {
// If a single line exceeds maxChangeLength, split it into multiple lines
while (tokenCount(line) > maxChangeLength) {
@@ -151,7 +167,8 @@ function splitDiff(diff: string, maxChangeLength: number) {
export const getCommitMsgsPromisesFromFileDiffs = async (
diff: string,
maxDiffLength: number
maxDiffLength: number,
fullGitMojiSpec: boolean
) => {
const separator = 'diff --git ';
@@ -160,7 +177,7 @@ export const getCommitMsgsPromisesFromFileDiffs = async (
// merge multiple files-diffs into 1 prompt to save tokens
const mergedFilesDiffs = mergeDiffs(diffByFiles, maxDiffLength);
const commitMessagePromises = [];
const commitMessagePromises = [] as Promise<string | null | undefined>[];
for (const fileDiff of mergedFilesDiffs) {
if (tokenCount(fileDiff) >= maxDiffLength) {
@@ -168,16 +185,19 @@ export const getCommitMsgsPromisesFromFileDiffs = async (
const messagesPromises = getMessagesPromisesByChangesInFile(
fileDiff,
separator,
maxDiffLength
maxDiffLength,
fullGitMojiSpec
);
commitMessagePromises.push(...messagesPromises);
} else {
const messages = await generateCommitMessageChatCompletionPrompt(
separator + fileDiff
separator + fileDiff,
fullGitMojiSpec
);
commitMessagePromises.push(api.generateCommitMessage(messages));
const engine = getEngine();
commitMessagePromises.push(engine.generateCommitMessage(messages));
}
}

View File

@@ -1,11 +1,9 @@
import { unlinkSync, writeFileSync } from 'fs';
import core from '@actions/core';
import exec from '@actions/exec';
import github from '@actions/github';
import { intro, outro } from '@clack/prompts';
import { PushEvent } from '@octokit/webhooks-types';
import { unlinkSync, writeFileSync } from 'fs';
import { generateCommitMessageByDiff } from './generateCommitMessageFromGitDiff';
import { randomIntFromInterval } from './utils/randomIntFromInterval';
import { sleep } from './utils/sleep';
@@ -54,7 +52,7 @@ async function improveMessagesInChunks(diffsAndSHAs: DiffAndSHA[]) {
const chunkSize = diffsAndSHAs!.length % 2 === 0 ? 4 : 3;
outro(`Improving commit messages in chunks of ${chunkSize}.`);
const improvePromises = diffsAndSHAs!.map((commit) =>
generateCommitMessageByDiff(commit.diff)
generateCommitMessageByDiff(commit.diff, false)
);
let improvedMessagesAndSHAs: MsgAndSHA[] = [];

View File

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

View File

@@ -1,8 +1,8 @@
import { spinner } from '@clack/prompts';
import { api } from '../../api';
import { getConfig } from '../../commands/config';
import { i18n, I18nLocals } from '../../i18n';
import { getEngine } from '../../utils/engine';
import { COMMITLINT_LLM_CONFIG_PATH } from './constants';
import { computeHash } from './crypto';
import { commitlintPrompts, inferPromptsFromCommitlintConfig } from './prompts';
@@ -11,7 +11,7 @@ import { CommitlintLLMConfig } from './types';
import * as utils from './utils';
const config = getConfig();
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
const translation = i18n[(config.OCO_LANGUAGE as I18nLocals) || 'en'];
export const configureCommitlintIntegration = async (force = false) => {
const spin = spinner();
@@ -19,7 +19,16 @@ export const configureCommitlintIntegration = async (force = false) => {
const fileExists = await utils.commitlintLLMConfigExists();
let commitLintConfig = await getCommitLintPWDConfig();
const commitLintConfig = await getCommitLintPWDConfig();
if (commitLintConfig === null) {
throw new Error(
`Failed to load @commitlint config. Please check the following:
* @commitlint >= 9.0.0 is installed in the local directory.
* 'node_modules/@commitlint/load' package exists.
* A valid @commitlint configuration exists.
`
);
}
// debug complete @commitlint configuration
// await fs.writeFile(
@@ -55,11 +64,16 @@ export const configureCommitlintIntegration = async (force = false) => {
// consistencyPrompts.map((p) => p.content)
// );
const engine = getEngine();
let consistency =
(await api.generateCommitMessage(consistencyPrompts)) || '{}';
(await engine.generateCommitMessage(consistencyPrompts)) || '{}';
// Cleanup the consistency answer. Sometimes 'gpt-3.5-turbo' sends rule's back.
prompts.forEach((prompt) => (consistency = consistency.replace(prompt, '')));
// sometimes consistency is preceded by explanatory text like "Here is your JSON:"
consistency = utils.getJSONBlock(consistency);
// ... remaining might be extra set of "\n"
consistency = utils.removeDoubleNewlines(consistency);

View File

@@ -1,8 +1,5 @@
import chalk from 'chalk';
import {
ChatCompletionRequestMessage,
ChatCompletionRequestMessageRoleEnum
} from 'openai';
import { OpenAI } from 'openai';
import { outro } from '@clack/prompts';
import {
@@ -17,7 +14,7 @@ import { i18n, I18nLocals } from '../../i18n';
import { IDENTITY, INIT_DIFF_PROMPT } from '../../prompts';
const config = getConfig();
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
const translation = i18n[(config.OCO_LANGUAGE as I18nLocals) || 'en'];
type DeepPartial<T> = {
[P in keyof T]?: {
@@ -214,11 +211,10 @@ const STRUCTURE_OF_COMMIT = `
// Prompt to generate LLM-readable rules based on @commitlint rules.
const GEN_COMMITLINT_CONSISTENCY_PROMPT = (
prompts: string[]
): ChatCompletionRequestMessage[] => [
): OpenAI.Chat.Completions.ChatCompletionMessageParam[] => [
{
role: ChatCompletionRequestMessageRoleEnum.Assistant,
// prettier-ignore
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages for two different changes in a single codebase and output them in the provided JSON format: one for a bug fix and another for a new feature.
role: 'system',
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages for two different changes in a single codebase and output them in the provided JSON format: one for a bug fix and another for a new feature.
Here are the specific requirements and conventions that should be strictly followed:
@@ -260,21 +256,33 @@ Example Git Diff is to follow:`
const INIT_MAIN_PROMPT = (
language: string,
prompts: string[]
): ChatCompletionRequestMessage => ({
role: ChatCompletionRequestMessageRoleEnum.System,
// prettier-ignore
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes 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 ? 'Use GitMoji convention to preface the commit.' : 'Do not preface the commit with anything.'}
${config?.OCO_DESCRIPTION ? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.' : "Don't add any descriptions to the commit, only commit message."}
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
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 ${
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
? 'Use GitMoji convention to preface the commit.'
: 'Do not preface the commit with anything.'
}
${
config.OCO_DESCRIPTION
? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.'
: "Don't add any descriptions to the commit, only commit message."
}
Use the present tense. Use ${language} to answer.
${
config.OCO_ONE_LINE_COMMIT
? 'Craft a concise commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in a one single message, without diverging into a list of commit per file change.'
: ''
}
You will strictly follow the following conventions to generate the content of the commit message:
- ${prompts.join('\n- ')}
The conventions refers to the following structure of commit message:
${STRUCTURE_OF_COMMIT}
`
${STRUCTURE_OF_COMMIT}`
});
export const commitlintPrompts = {

View File

@@ -1,11 +1,41 @@
import fs from 'fs/promises';
import path from 'path';
const nodeModulesPath = path.join(
process.env.PWD || process.cwd(),
'node_modules',
'@commitlint',
'load'
);
const findModulePath = (moduleName: string) => {
const searchPaths = [
path.join('node_modules', moduleName),
path.join('node_modules', '.pnpm')
];
for (const basePath of searchPaths) {
try {
const resolvedPath = require.resolve(moduleName, { paths: [basePath] });
return resolvedPath;
} catch {
// Continue to the next search path if the module is not found
}
}
throw new Error(`Cannot find module ${moduleName}`);
};
const getCommitLintModuleType = async (): Promise<'cjs' | 'esm'> => {
const packageFile = '@commitlint/load/package.json';
const packageJsonPath = findModulePath(packageFile);
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
if (!packageJson) {
throw new Error(`Failed to parse ${packageFile}`);
}
return packageJson.type === 'module' ? 'esm' : 'cjs';
};
/**
* QualifiedConfig from any version of @commitlint/types
* @see https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/types/src/load.ts
*/
type QualifiedConfigOnAnyVersion = { [key: string]: unknown };
/**
* This code is loading the configuration for the `@commitlint` package from the current working
@@ -13,13 +43,31 @@ const nodeModulesPath = path.join(
*
* @returns
*/
export const getCommitLintPWDConfig = async () => {
const load = require(nodeModulesPath).default;
export const getCommitLintPWDConfig =
async (): Promise<QualifiedConfigOnAnyVersion | null> => {
let load: Function, modulePath: string;
switch (await getCommitLintModuleType()) {
case 'cjs':
/**
* CommonJS (<= commitlint@v18.x.x.)
*/
modulePath = findModulePath('@commitlint/load');
load = require(modulePath).default;
break;
case 'esm':
/**
* ES Module (commitlint@v19.x.x. <= )
* Directory import is not supported in ES Module resolution, so import the file directly
*/
modulePath = await findModulePath('@commitlint/load/lib/load.js');
load = (await import(modulePath)).default;
break;
}
if (load && typeof load === 'function') {
return await load();
}
if (load && typeof load === 'function') {
return await load();
}
// @commitlint/load is not a function
return null;
};
// @commitlint/load is not a function
return null;
};

View File

@@ -16,6 +16,16 @@ export const removeDoubleNewlines = (input: string): string => {
return input;
};
export const getJSONBlock = (input: string): string => {
const jsonIndex = input.search('```json');
if (jsonIndex > -1) {
input = input.slice(jsonIndex + 8);
const endJsonIndex = input.search('```');
input = input.slice(0, endJsonIndex);
}
return input;
};
export const commitlintLLMConfigExists = async (): Promise<boolean> => {
let exists;
try {

View File

@@ -1,42 +1,141 @@
import {
ChatCompletionRequestMessage,
ChatCompletionRequestMessageRoleEnum
} from 'openai';
import { note } from '@clack/prompts';
import { OpenAI } from 'openai';
import { getConfig } from './commands/config';
import { i18n, I18nLocals } from './i18n';
import { configureCommitlintIntegration } from './modules/commitlint/config';
import { commitlintPrompts } from './modules/commitlint/prompts';
import { ConsistencyPrompt } from './modules/commitlint/types';
import * as utils from './modules/commitlint/utils';
import { removeConventionalCommitWord } from './utils/removeConventionalCommitWord';
const config = getConfig();
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
const translation = i18n[(config.OCO_LANGUAGE as I18nLocals) || 'en'];
export const IDENTITY =
'You are to act as the author of a commit message in git.';
'You are to act as an author of a commit message in git.';
const INIT_MAIN_PROMPT = (language: string): ChatCompletionRequestMessage => ({
role: ChatCompletionRequestMessageRoleEnum.System,
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages as per the conventional commit convention and explain WHAT were the changes and mainly WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you are to convert it into a commit message.
${
config?.OCO_EMOJI
? 'Use GitMoji convention to preface the commit.'
: 'Do not preface the commit with anything.'
}
${
config?.OCO_DESCRIPTION
? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.'
: "Don't add any descriptions to the commit, only commit message."
}
Use the present tense. Lines must not be longer than 74 characters. Use ${language} for the commit message.`
const GITMOJI_HELP = `Use GitMoji convention to preface the commit. Here are some help to choose the right emoji (emoji, description):
🐛, Fix a bug;
✨, Introduce new features;
📝, Add or update documentation;
🚀, Deploy stuff;
✅, Add, update, or pass tests;
♻️, Refactor code;
⬆️, Upgrade dependencies;
🔧, Add or update configuration files;
🌐, Internationalization and localization;
💡, Add or update comments in source code;`;
const FULL_GITMOJI_SPEC = `${GITMOJI_HELP}
🎨, Improve structure / format of the code;
⚡️, Improve performance;
🔥, Remove code or files;
🚑️, Critical hotfix;
💄, Add or update the UI and style files;
🎉, Begin a project;
🔒️, Fix security issues;
🔐, Add or update secrets;
🔖, Release / Version tags;
🚨, Fix compiler / linter warnings;
🚧, Work in progress;
💚, Fix CI Build;
⬇️, Downgrade dependencies;
📌, Pin dependencies to specific versions;
👷, Add or update CI build system;
📈, Add or update analytics or track code;
, Add a dependency;
, Remove a dependency;
🔨, Add or update development scripts;
✏️, Fix typos;
💩, Write bad code that needs to be improved;
⏪️, Revert changes;
🔀, Merge branches;
📦️, Add or update compiled files or packages;
👽️, Update code due to external API changes;
🚚, Move or rename resources (e.g.: files, paths, routes);
📄, Add or update license;
💥, Introduce breaking changes;
🍱, Add or update assets;
♿️, Improve accessibility;
🍻, Write code drunkenly;
💬, Add or update text and literals;
🗃️, Perform database related changes;
🔊, Add or update logs;
🔇, Remove logs;
👥, Add or update contributor(s);
🚸, Improve user experience / usability;
🏗️, Make architectural changes;
📱, Work on responsive design;
🤡, Mock things;
🥚, Add or update an easter egg;
🙈, Add or update a .gitignore file;
📸, Add or update snapshots;
⚗️, Perform experiments;
🔍️, Improve SEO;
🏷️, Add or update types;
🌱, Add or update seed files;
🚩, Add, update, or remove feature flags;
🥅, Catch errors;
💫, Add or update animations and transitions;
🗑️, Deprecate code that needs to be cleaned up;
🛂, Work on code related to authorization, roles and permissions;
🩹, Simple fix for a non-critical issue;
🧐, Data exploration/inspection;
⚰️, Remove dead code;
🧪, Add a failing test;
👔, Add or update business logic;
🩺, Add or update healthcheck;
🧱, Infrastructure related changes;
🧑‍💻, Improve developer experience;
💸, Add sponsorships or money related infrastructure;
🧵, Add or update code related to multithreading or concurrency;
🦺, Add or update code related to validation.`;
const CONVENTIONAL_COMMIT_KEYWORDS =
'Do not preface the commit with anything, except for the conventional commit keywords: fix, feat, build, chore, ci, docs, style, refactor, perf, test.';
const getCommitConvention = (fullGitMojiSpec: boolean) =>
config.OCO_EMOJI
? fullGitMojiSpec
? FULL_GITMOJI_SPEC
: GITMOJI_HELP
: CONVENTIONAL_COMMIT_KEYWORDS;
const getDescriptionInstruction = () =>
config.OCO_DESCRIPTION
? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.'
: "Don't add any descriptions to the commit, only commit message.";
const getOneLineCommitInstruction = () =>
config.OCO_ONE_LINE_COMMIT
? 'Craft a concise commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in a one single message, without diverging into a list of commit per file change.'
: '';
const INIT_MAIN_PROMPT = (
language: string,
fullGitMojiSpec: boolean
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
role: 'system',
content: (() => {
const commitConvention = fullGitMojiSpec
? 'GitMoji specification'
: 'Conventional Commit Convention';
const missionStatement = `${IDENTITY} Your mission is to create clean and comprehensive commit messages as per the ${commitConvention} and explain WHAT were the changes and mainly WHY the changes were done.`;
const diffInstruction =
"I'll send you an output of 'git diff --staged' command, and you are to convert it into a commit message.";
const conventionGuidelines = getCommitConvention(fullGitMojiSpec);
const descriptionGuideline = getDescriptionInstruction();
const oneLineCommitGuideline = getOneLineCommitInstruction();
const generalGuidelines = `Use the present tense. Lines must not be longer than 74 characters. Use ${language} for the commit message.`;
return `${missionStatement}\n${diffInstruction}\n${conventionGuidelines}\n${descriptionGuideline}\n${oneLineCommitGuideline}\n${generalGuidelines}`;
})()
});
export const INIT_DIFF_PROMPT: ChatCompletionRequestMessage = {
role: ChatCompletionRequestMessageRoleEnum.User,
content: `diff --git a/src/server.ts b/src/server.ts
export const INIT_DIFF_PROMPT: OpenAI.Chat.Completions.ChatCompletionMessageParam =
{
role: 'user',
content: `diff --git a/src/server.ts b/src/server.ts
index ad4db42..f3b18a9 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -60,21 +159,35 @@ export const INIT_DIFF_PROMPT: ChatCompletionRequestMessage = {
+app.listen(process.env.PORT || PORT, () => {
+ console.log(\`Server listening on port \${PORT}\`);
});`
};
const getContent = (translation: ConsistencyPrompt) => {
const fix = config.OCO_EMOJI
? `🐛 ${removeConventionalCommitWord(translation.commitFix)}`
: translation.commitFix;
const feat = config.OCO_EMOJI
? `${removeConventionalCommitWord(translation.commitFeat)}`
: translation.commitFeat;
const description = config.OCO_DESCRIPTION
? translation.commitDescription
: '';
return `${fix}\n${feat}\n${description}`;
};
const INIT_CONSISTENCY_PROMPT = (
translation: ConsistencyPrompt
): ChatCompletionRequestMessage => ({
role: ChatCompletionRequestMessageRoleEnum.Assistant,
content: `${config?.OCO_EMOJI ? '🐛 ' : ''}${translation.commitFix}
${config?.OCO_EMOJI ? '✨ ' : ''}${translation.commitFeat}
${config?.OCO_DESCRIPTION ? translation.commitDescription : ''}`
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
role: 'assistant',
content: getContent(translation)
});
export const getMainCommitPrompt = async (): Promise<
ChatCompletionRequestMessage[]
> => {
switch (config?.OCO_PROMPT_MODULE) {
export const getMainCommitPrompt = async (
fullGitMojiSpec: boolean
): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => {
switch (config.OCO_PROMPT_MODULE) {
case '@commitlint':
if (!(await utils.commitlintLLMConfigExists())) {
note(
@@ -100,9 +213,8 @@ export const getMainCommitPrompt = async (): Promise<
];
default:
// conventional-commit
return [
INIT_MAIN_PROMPT(translation.localLanguage),
INIT_MAIN_PROMPT(translation.localLanguage, fullGitMojiSpec),
INIT_DIFF_PROMPT,
INIT_CONSISTENCY_PROMPT(translation)
];

View File

@@ -3,7 +3,7 @@ import chalk from 'chalk';
import { outro } from '@clack/prompts';
import currentPackage from '../../package.json';
import { getOpenCommitLatestVersion } from '../api';
import { getOpenCommitLatestVersion } from '../version';
export const checkIsLatestVersion = async () => {
const latestVersion = await getOpenCommitLatestVersion();

65
src/utils/engine.ts Normal file
View File

@@ -0,0 +1,65 @@
import { getConfig, OCO_AI_PROVIDER_ENUM } from '../commands/config';
import { AnthropicEngine } from '../engine/anthropic';
import { AzureEngine } from '../engine/azure';
import { AiEngine } from '../engine/Engine';
import { FlowiseAi } from '../engine/flowise';
import { Gemini } from '../engine/gemini';
import { OllamaAi } from '../engine/ollama';
import { OpenAiEngine } from '../engine/openAi';
import { TestAi, TestMockType } from '../engine/testAi';
export function getEngine(): AiEngine {
const config = getConfig();
const provider = config.OCO_AI_PROVIDER;
const DEFAULT_CONFIG = {
model: config.OCO_MODEL!,
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
baseURL: config.OCO_OPENAI_BASE_PATH!
};
switch (provider) {
case OCO_AI_PROVIDER_ENUM.OLLAMA:
return new OllamaAi({
...DEFAULT_CONFIG,
apiKey: '',
baseURL: config.OCO_OLLAMA_API_URL!
});
case OCO_AI_PROVIDER_ENUM.ANTHROPIC:
return new AnthropicEngine({
...DEFAULT_CONFIG,
apiKey: config.OCO_ANTHROPIC_API_KEY!
});
case OCO_AI_PROVIDER_ENUM.TEST:
return new TestAi(config.OCO_TEST_MOCK_TYPE as TestMockType);
case OCO_AI_PROVIDER_ENUM.GEMINI:
return new Gemini({
...DEFAULT_CONFIG,
apiKey: config.OCO_GEMINI_API_KEY!,
baseURL: config.OCO_GEMINI_BASE_PATH!
});
case OCO_AI_PROVIDER_ENUM.AZURE:
return new AzureEngine({
...DEFAULT_CONFIG,
apiKey: config.OCO_AZURE_API_KEY!
});
case OCO_AI_PROVIDER_ENUM.FLOWISE:
return new FlowiseAi({
...DEFAULT_CONFIG,
baseURL: config.OCO_FLOWISE_ENDPOINT || DEFAULT_CONFIG.baseURL,
apiKey: config.OCO_FLOWISE_API_KEY!
});
default:
return new OpenAiEngine({
...DEFAULT_CONFIG,
apiKey: config.OCO_OPENAI_API_KEY!
});
}
}

View File

@@ -75,8 +75,11 @@ export const getChangedFiles = async (): Promise<string[]> => {
export const gitAdd = async ({ files }: { files: string[] }) => {
const gitAddSpinner = spinner();
gitAddSpinner.start('Adding files to commit');
await execa('git', ['add', ...files]);
gitAddSpinner.stop('Done');
};

View File

@@ -0,0 +1,3 @@
export function removeConventionalCommitWord(message: string): string {
return message.replace(/^(fix|feat)\((.+?)\):/, '($2):');
}

14
src/version.ts Normal file
View File

@@ -0,0 +1,14 @@
import { outro } from '@clack/prompts';
import { execa } from 'execa';
export const getOpenCommitLatestVersion = async (): Promise<
string | undefined
> => {
try {
const { stdout } = await execa('npm', ['view', 'opencommit', 'version']);
return stdout;
} catch (_) {
outro('Error while getting the latest version of opencommit');
return undefined;
}
};

22
test/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM ubuntu:latest
RUN apt-get update && apt-get install -y curl git
# Install Node.js v20
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
RUN apt-get install -y nodejs
# Setup git
RUN git config --global user.email "test@example.com"
RUN git config --global user.name "Test User"
WORKDIR /app
COPY package.json /app/
COPY package-lock.json /app/
RUN ls -la
RUN npm ci
COPY . /app
RUN ls -la
RUN npm run build

View File

@@ -0,0 +1,12 @@
import { resolve } from 'path'
import { render } from 'cli-testing-library'
import 'cli-testing-library/extend-expect';
import { prepareEnvironment } from './utils';
it('cli flow when there are no changes', async () => {
const { gitDir, cleanup } = await prepareEnvironment();
const { findByText } = await render(`OCO_AI_PROVIDER='test' node`, [resolve('./out/cli.cjs')], { cwd: gitDir });
expect(await findByText('No changes detected')).toBeInTheConsole();
await cleanup();
});

55
test/e2e/oneFile.test.ts Normal file
View File

@@ -0,0 +1,55 @@
import { resolve } from 'path'
import { render } from 'cli-testing-library'
import 'cli-testing-library/extend-expect';
import { prepareEnvironment } from './utils';
it('cli flow to generate commit message for 1 new file (staged)', async () => {
const { gitDir, cleanup } = await prepareEnvironment();
await render('echo' ,[`'console.log("Hello World");' > index.ts`], { cwd: gitDir });
await render('git' ,['add index.ts'], { cwd: gitDir });
const { queryByText, findByText, userEvent } = await render(`OCO_AI_PROVIDER='test' node`, [resolve('./out/cli.cjs')], { cwd: gitDir });
expect(await 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 findByText('Generating the commit message')).toBeInTheConsole();
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();
});
it('cli flow to generate commit message for 1 changed file (not staged)', async () => {
const { gitDir, cleanup } = await prepareEnvironment();
await render('echo' ,[`'console.log("Hello World");' > index.ts`], { cwd: gitDir });
await render('git' ,['add index.ts'], { cwd: gitDir });
await render('git' ,[`commit -m 'add new file'`], { cwd: gitDir });
await render('echo' ,[`'console.log("Good night World");' >> index.ts`], { 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('Do you want to stage all files and generate commit message?')).toBeInTheConsole();
userEvent.keyboard('[Enter]');
expect(await findByText('Generating the commit message')).toBeInTheConsole();
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
userEvent.keyboard('[Enter]');
expect(await findByText('Successfully committed')).toBeInTheConsole();
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

@@ -0,0 +1,222 @@
import { resolve } from 'path';
import { render } from 'cli-testing-library';
import 'cli-testing-library/extend-expect';
import { prepareEnvironment, wait } from '../utils';
import path from 'path';
function getAbsolutePath(relativePath: string) {
const scriptDir = path.dirname(__filename);
return path.resolve(scriptDir, relativePath);
}
async function setupCommitlint(dir: string, ver: 9 | 18 | 19) {
let packagePath, packageJsonPath, configPath;
switch (ver) {
case 9:
packagePath = getAbsolutePath('./data/commitlint_9/node_modules');
packageJsonPath = getAbsolutePath('./data/commitlint_9/package.json');
configPath = getAbsolutePath('./data/commitlint_9/commitlint.config.js');
break;
case 18:
packagePath = getAbsolutePath('./data/commitlint_18/node_modules');
packageJsonPath = getAbsolutePath('./data/commitlint_18/package.json');
configPath = getAbsolutePath('./data/commitlint_18/commitlint.config.js');
break;
case 19:
packagePath = getAbsolutePath('./data/commitlint_19/node_modules');
packageJsonPath = getAbsolutePath('./data/commitlint_19/package.json');
configPath = getAbsolutePath('./data/commitlint_19/commitlint.config.js');
break;
}
await render('cp', ['-r', packagePath, '.'], { cwd: dir });
await render('cp', [packageJsonPath, '.'], { cwd: dir });
await render('cp', [configPath, '.'], { cwd: dir });
await wait(3000); // Avoid flakiness by waiting
}
describe('cli flow to run "oco commitlint force"', () => {
it('on commitlint@9 using CJS', async () => {
const { gitDir, cleanup } = await prepareEnvironment();
await setupCommitlint(gitDir, 9);
const npmList = await render('npm', ['list', '@commitlint/load'], {
cwd: gitDir
});
expect(await npmList.findByText('@commitlint/load@9')).toBeInTheConsole();
const { findByText } = await render(
`
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' \
node ${resolve('./out/cli.cjs')} commitlint force \
`,
[],
{ cwd: gitDir }
);
expect(
await findByText('opencommit — configure @commitlint')
).toBeInTheConsole();
expect(
await findByText('Read @commitlint configuration')
).toBeInTheConsole();
expect(
await findByText('Generating consistency with given @commitlint rules')
).toBeInTheConsole();
expect(
await findByText('Done - please review contents of')
).toBeInTheConsole();
await cleanup();
});
it('on commitlint@18 using CJS', async () => {
const { gitDir, cleanup } = await prepareEnvironment();
await setupCommitlint(gitDir, 18);
const npmList = await render('npm', ['list', '@commitlint/load'], {
cwd: gitDir
});
expect(await npmList.findByText('@commitlint/load@18')).toBeInTheConsole();
const { findByText } = await render(
`
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' \
node ${resolve('./out/cli.cjs')} commitlint force \
`,
[],
{ cwd: gitDir }
);
expect(
await findByText('opencommit — configure @commitlint')
).toBeInTheConsole();
expect(
await findByText('Read @commitlint configuration')
).toBeInTheConsole();
expect(
await findByText('Generating consistency with given @commitlint rules')
).toBeInTheConsole();
expect(
await findByText('Done - please review contents of')
).toBeInTheConsole();
await cleanup();
});
it('on commitlint@19 using ESM', async () => {
const { gitDir, cleanup } = await prepareEnvironment();
await setupCommitlint(gitDir, 19);
const npmList = await render('npm', ['list', '@commitlint/load'], {
cwd: gitDir
});
expect(await npmList.findByText('@commitlint/load@19')).toBeInTheConsole();
const { findByText } = await render(
`
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' \
node ${resolve('./out/cli.cjs')} commitlint force \
`,
[],
{ cwd: gitDir }
);
expect(
await findByText('opencommit — configure @commitlint')
).toBeInTheConsole();
expect(
await findByText('Read @commitlint configuration')
).toBeInTheConsole();
expect(
await findByText('Generating consistency with given @commitlint rules')
).toBeInTheConsole();
expect(
await findByText('Done - please review contents of')
).toBeInTheConsole();
await cleanup();
});
});
describe('cli flow to generate commit message using @commitlint prompt-module', () => {
it('on commitlint@19 using ESM', async () => {
const { gitDir, cleanup } = await prepareEnvironment();
// Setup commitlint@19
await setupCommitlint(gitDir, 19);
const npmList = await render('npm', ['list', '@commitlint/load'], {
cwd: gitDir
});
expect(await npmList.findByText('@commitlint/load@19')).toBeInTheConsole();
// Run `oco commitlint force`
const commitlintForce = await render(
`
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' \
node ${resolve('./out/cli.cjs')} commitlint force \
`,
[],
{ cwd: gitDir }
);
expect(
await commitlintForce.findByText('Done - please review contents of')
).toBeInTheConsole();
// Run `oco commitlint get`
const commitlintGet = await render(
`
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' \
node ${resolve('./out/cli.cjs')} commitlint get \
`,
[],
{ cwd: gitDir }
);
expect(await commitlintGet.findByText('consistency')).toBeInTheConsole();
// Run 'oco' using .opencommit-commitlint
await render('echo', [`'console.log("Hello World");' > index.ts`], {
cwd: gitDir
});
await render('git', ['add index.ts'], { cwd: gitDir });
const oco = await render(
`
OCO_TEST_MOCK_TYPE='commit-message' \
OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' \
node ${resolve('./out/cli.cjs')} \
`,
[],
{ cwd: gitDir }
);
expect(
await oco.findByText('Generating the commit message')
).toBeInTheConsole();
expect(
await oco.findByText('Confirm the commit message?')
).toBeInTheConsole();
oco.userEvent.keyboard('[Enter]');
expect(
await oco.findByText('Choose a remote to push to')
).toBeInTheConsole();
oco.userEvent.keyboard('[Enter]');
expect(
await oco.findByText('Successfully pushed all commits to origin')
).toBeInTheConsole();
await cleanup();
});
});

View File

@@ -0,0 +1,3 @@
module.exports = {
extends: ['@commitlint/config-conventional']
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
{
"name": "commitlint-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@commitlint/cli": "^18.0.0",
"@commitlint/config-conventional": "^18.0.0"
}
}

View File

@@ -0,0 +1,3 @@
export default {
extends: ['@commitlint/config-conventional']
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
{
"name": "commitlint-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@commitlint/cli": "^19.0.0",
"@commitlint/config-conventional": "^19.0.0"
}
}

View File

@@ -0,0 +1,3 @@
module.exports = {
extends: ['@commitlint/config-conventional']
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
{
"name": "commitlint-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@commitlint/cli": "^9.0.0",
"@commitlint/config-conventional": "^9.0.0"
}
}

11
test/e2e/setup.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/sh
current_dir=$(pwd)
setup_dir="$(cd "$(dirname "$0")" && pwd)"
# Set up for prompt-module/commitlint
cd $setup_dir && cd prompt-module/data/commitlint_9 && npm ci
cd $setup_dir && cd prompt-module/data/commitlint_18 && npm ci
cd $setup_dir && cd prompt-module/data/commitlint_19 && npm ci
cd $current_dir

33
test/e2e/utils.ts Normal file
View File

@@ -0,0 +1,33 @@
import path from 'path'
import { mkdtemp, rm } from 'fs'
import { promisify } from 'util';
import { tmpdir } from 'os';
import { exec } from 'child_process';
const fsMakeTempDir = promisify(mkdtemp);
const fsExec = promisify(exec);
const fsRemove = promisify(rm);
/**
* Prepare the environment for the test
* Create a temporary git repository in the temp directory
*/
export const prepareEnvironment = async (): Promise<{
gitDir: string;
cleanup: () => Promise<void>;
}> => {
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
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,
}
}
export const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

10
test/jest-setup.ts Normal file
View File

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

295
test/unit/config.test.ts Normal file
View File

@@ -0,0 +1,295 @@
import { existsSync, readFileSync, rmSync } from 'fs';
import {
DEFAULT_CONFIG,
getConfig,
setConfig
} from '../../src/commands/config';
import { prepareFile } from './utils';
import { dirname } from 'path';
describe('config', () => {
const originalEnv = { ...process.env };
let globalConfigFile: { filePath: string; cleanup: () => Promise<void> };
let envConfigFile: { filePath: string; cleanup: () => Promise<void> };
function resetEnv(env: NodeJS.ProcessEnv) {
Object.keys(process.env).forEach((key) => {
if (!(key in env)) {
delete process.env[key];
} else {
process.env[key] = env[key];
}
});
}
beforeEach(async () => {
resetEnv(originalEnv);
if (globalConfigFile) await globalConfigFile.cleanup();
if (envConfigFile) await envConfigFile.cleanup();
});
afterEach(async () => {
if (globalConfigFile) await globalConfigFile.cleanup();
if (envConfigFile) await envConfigFile.cleanup();
});
afterAll(() => {
resetEnv(originalEnv);
});
const generateConfig = async (
fileName: string,
content: Record<string, string>
) => {
const fileContent = Object.entries(content)
.map(([key, value]) => `${key}="${value}"`)
.join('\n');
return await prepareFile(fileName, fileContent);
};
describe('getConfig', () => {
it('should prioritize local .env over global .opencommit config', async () => {
globalConfigFile = await generateConfig('.opencommit', {
OCO_OPENAI_API_KEY: 'global-key',
OCO_MODEL: 'gpt-3.5-turbo',
OCO_LANGUAGE: 'en'
});
envConfigFile = await generateConfig('.env', {
OCO_OPENAI_API_KEY: 'local-key',
OCO_ANTHROPIC_API_KEY: 'local-anthropic-key',
OCO_LANGUAGE: 'fr'
});
const config = getConfig({
globalPath: globalConfigFile.filePath,
envPath: envConfigFile.filePath
});
expect(config).not.toEqual(null);
expect(config.OCO_OPENAI_API_KEY).toEqual('local-key');
expect(config.OCO_MODEL).toEqual('gpt-3.5-turbo');
expect(config.OCO_LANGUAGE).toEqual('fr');
expect(config.OCO_ANTHROPIC_API_KEY).toEqual('local-anthropic-key');
});
it('should fallback to global config when local config is not set', async () => {
globalConfigFile = await generateConfig('.opencommit', {
OCO_OPENAI_API_KEY: 'global-key',
OCO_MODEL: 'gpt-4',
OCO_LANGUAGE: 'de',
OCO_DESCRIPTION: 'true'
});
envConfigFile = await generateConfig('.env', {
OCO_ANTHROPIC_API_KEY: 'local-anthropic-key'
});
const config = getConfig({
globalPath: globalConfigFile.filePath,
envPath: envConfigFile.filePath
});
expect(config).not.toEqual(null);
expect(config.OCO_OPENAI_API_KEY).toEqual('global-key');
expect(config.OCO_ANTHROPIC_API_KEY).toEqual('local-anthropic-key');
expect(config.OCO_MODEL).toEqual('gpt-4');
expect(config.OCO_LANGUAGE).toEqual('de');
expect(config.OCO_DESCRIPTION).toEqual(true);
});
it('should handle boolean and numeric values correctly', async () => {
globalConfigFile = await generateConfig('.opencommit', {
OCO_TOKENS_MAX_INPUT: '4096',
OCO_TOKENS_MAX_OUTPUT: '500',
OCO_GITPUSH: 'true'
});
envConfigFile = await generateConfig('.env', {
OCO_TOKENS_MAX_INPUT: '8192',
OCO_ONE_LINE_COMMIT: 'false'
});
const config = getConfig({
globalPath: globalConfigFile.filePath,
envPath: envConfigFile.filePath
});
expect(config).not.toEqual(null);
expect(config.OCO_TOKENS_MAX_INPUT).toEqual(8192);
expect(config.OCO_TOKENS_MAX_OUTPUT).toEqual(500);
expect(config.OCO_GITPUSH).toEqual(true);
expect(config.OCO_ONE_LINE_COMMIT).toEqual(false);
});
it('should handle empty local config correctly', async () => {
globalConfigFile = await generateConfig('.opencommit', {
OCO_OPENAI_API_KEY: 'global-key',
OCO_MODEL: 'gpt-4',
OCO_LANGUAGE: 'es'
});
envConfigFile = await generateConfig('.env', {});
const config = getConfig({
globalPath: globalConfigFile.filePath,
envPath: envConfigFile.filePath
});
expect(config).not.toEqual(null);
expect(config.OCO_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'
});
envConfigFile = await generateConfig('.env', {
OCO_OPENAI_API_KEY: 'null'
});
const config = getConfig({
globalPath: globalConfigFile.filePath,
envPath: envConfigFile.filePath
});
expect(config).not.toEqual(null);
expect(config.OCO_OPENAI_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_OPENAI_API_KEY).toEqual(undefined);
});
});
describe('setConfig', () => {
beforeEach(async () => {
// we create and delete the file to have the parent directory, but not the file, to test the creation of the file
globalConfigFile = await generateConfig('.opencommit', {});
rmSync(globalConfigFile.filePath);
});
it('should create .opencommit file with DEFAULT CONFIG if it does not exist on first setConfig run', async () => {
const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath);
expect(isGlobalConfigFileExist).toBe(false);
await setConfig(
[['OCO_OPENAI_API_KEY', 'persisted-key_1']],
globalConfigFile.filePath
);
const fileContent = readFileSync(globalConfigFile.filePath, 'utf8');
expect(fileContent).toContain('OCO_OPENAI_API_KEY=persisted-key_1');
Object.entries(DEFAULT_CONFIG).forEach(([key, value]) => {
expect(fileContent).toContain(`${key}=${value}`);
});
});
it('should set new config values', async () => {
globalConfigFile = await generateConfig('.opencommit', {});
await setConfig(
[
['OCO_OPENAI_API_KEY', 'new-key'],
['OCO_MODEL', 'gpt-4']
],
globalConfigFile.filePath
);
const config = getConfig({ globalPath: globalConfigFile.filePath });
expect(config.OCO_OPENAI_API_KEY).toEqual('new-key');
expect(config.OCO_MODEL).toEqual('gpt-4');
});
it('should update existing config values', async () => {
globalConfigFile = await generateConfig('.opencommit', {
OCO_OPENAI_API_KEY: 'initial-key'
});
await setConfig(
[['OCO_OPENAI_API_KEY', 'updated-key']],
globalConfigFile.filePath
);
const config = getConfig({ globalPath: globalConfigFile.filePath });
expect(config.OCO_OPENAI_API_KEY).toEqual('updated-key');
});
it('should handle boolean and numeric values correctly', async () => {
globalConfigFile = await generateConfig('.opencommit', {});
await setConfig(
[
['OCO_TOKENS_MAX_INPUT', '8192'],
['OCO_DESCRIPTION', 'true'],
['OCO_ONE_LINE_COMMIT', 'false']
],
globalConfigFile.filePath
);
const config = getConfig({ globalPath: globalConfigFile.filePath });
expect(config.OCO_TOKENS_MAX_INPUT).toEqual(8192);
expect(config.OCO_DESCRIPTION).toEqual(true);
expect(config.OCO_ONE_LINE_COMMIT).toEqual(false);
});
it('should throw an error for unsupported config keys', async () => {
globalConfigFile = await generateConfig('.opencommit', {});
try {
await setConfig(
[['UNSUPPORTED_KEY', 'value']],
globalConfigFile.filePath
);
throw new Error('NEVER_REACHED');
} catch (error) {
expect(error.message).toContain(
'Unsupported config key: UNSUPPORTED_KEY'
);
expect(error.message).not.toContain('NEVER_REACHED');
}
});
it('should persist changes to the config file', async () => {
const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath);
expect(isGlobalConfigFileExist).toBe(false);
await setConfig(
[['OCO_OPENAI_API_KEY', 'persisted-key']],
globalConfigFile.filePath
);
const fileContent = readFileSync(globalConfigFile.filePath, 'utf8');
expect(fileContent).toContain('OCO_OPENAI_API_KEY=persisted-key');
});
it('should set multiple configs in a row and keep the changes', async () => {
const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath);
expect(isGlobalConfigFileExist).toBe(false);
await setConfig(
[['OCO_OPENAI_API_KEY', 'persisted-key']],
globalConfigFile.filePath
);
const fileContent1 = readFileSync(globalConfigFile.filePath, 'utf8');
expect(fileContent1).toContain('OCO_OPENAI_API_KEY=persisted-key');
await setConfig([['OCO_MODEL', 'gpt-4']], globalConfigFile.filePath);
const fileContent2 = readFileSync(globalConfigFile.filePath, 'utf8');
expect(fileContent2).toContain('OCO_MODEL=gpt-4');
});
});
});

98
test/unit/gemini.test.ts Normal file
View File

@@ -0,0 +1,98 @@
import { Gemini } from '../../src/engine/gemini';
import { GenerativeModel, GoogleGenerativeAI } from '@google/generative-ai';
import {
ConfigType,
getConfig,
OCO_AI_PROVIDER_ENUM
} from '../../src/commands/config';
import { OpenAI } from 'openai';
describe('Gemini', () => {
let gemini: Gemini;
let mockConfig: ConfigType;
let mockGoogleGenerativeAi: GoogleGenerativeAI;
let mockGenerativeModel: GenerativeModel;
let mockExit: jest.SpyInstance<never, [code?: number | undefined], any>;
const noop: (...args: any[]) => any = (...args: any[]) => {};
const mockGemini = () => {
mockConfig = getConfig() as ConfigType;
gemini = new Gemini({
apiKey: mockConfig.OCO_GEMINI_API_KEY,
model: mockConfig.OCO_MODEL
});
};
const oldEnv = process.env;
beforeEach(() => {
jest.resetModules();
process.env = { ...oldEnv };
jest.mock('@google/generative-ai');
jest.mock('../src/commands/config');
jest.mock('@clack/prompts', () => ({
intro: jest.fn(),
outro: jest.fn()
}));
mockExit = jest.spyOn(process, 'exit').mockImplementation();
mockConfig = getConfig() as ConfigType;
mockConfig.OCO_AI_PROVIDER = OCO_AI_PROVIDER_ENUM.GEMINI;
mockConfig.OCO_GEMINI_API_KEY = 'mock-api-key';
mockConfig.OCO_MODEL = 'gemini-1.5-flash';
mockGoogleGenerativeAi = new GoogleGenerativeAI(
mockConfig.OCO_GEMINI_API_KEY
);
mockGenerativeModel = mockGoogleGenerativeAi.getGenerativeModel({
model: mockConfig.OCO_MODEL
});
});
afterEach(() => {
gemini = undefined as any;
});
afterAll(() => {
mockExit.mockRestore();
process.env = oldEnv;
});
it.skip('should exit process if OCO_GEMINI_API_KEY is not set and command is not config', () => {
process.env.OCO_GEMINI_API_KEY = undefined;
process.env.OCO_AI_PROVIDER = 'gemini';
mockGemini();
expect(mockExit).toHaveBeenCalledWith(1);
});
it('should generate commit message', async () => {
const mockGenerateContent = jest
.fn()
.mockResolvedValue({ response: { text: () => 'generated content' } });
mockGenerativeModel.generateContent = mockGenerateContent;
mockGemini();
const messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam> =
[
{ role: 'system', content: 'system message' },
{ role: 'assistant', content: 'assistant message' }
];
jest
.spyOn(gemini, 'generateCommitMessage')
.mockImplementation(async () => 'generated content');
const result = await gemini.generateCommitMessage(messages);
expect(result).toEqual('generated content');
});
});

32
test/unit/utils.ts Normal file
View File

@@ -0,0 +1,32 @@
import { existsSync, mkdtemp, rm, writeFile } from 'fs';
import { tmpdir } from 'os';
import path from 'path';
import { promisify } from 'util';
const fsMakeTempDir = promisify(mkdtemp);
const fsRemove = promisify(rm);
const fsWriteFile = promisify(writeFile);
/**
* Prepare tmp file for the test
*/
export async function prepareFile(
fileName: string,
content: string
): Promise<{
filePath: string;
cleanup: () => Promise<void>;
}> {
const tempDir = await fsMakeTempDir(path.join(tmpdir(), 'opencommit-test-'));
const filePath = path.resolve(tempDir, fileName);
await fsWriteFile(filePath, content);
const cleanup = async () => {
if (existsSync(tempDir)) {
await fsRemove(tempDir, { recursive: true });
}
};
return {
filePath,
cleanup
};
}

View File

@@ -1,12 +1,12 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["ES5", "ES6"],
"target": "ES2020",
"lib": ["ES6", "ES2020"],
"module": "CommonJS",
"module": "ESNext",
// "rootDir": "./src",
"resolveJsonModule": true,
"moduleResolution": "node",
"moduleResolution": "Node",
"allowJs": true,
@@ -21,6 +21,7 @@
"skipLibCheck": true
},
"include": ["test/jest-setup.ts"],
"exclude": ["node_modules"],
"ts-node": {
"esm": true,