mirror of
https://github.com/di-sukharev/opencommit.git
synced 2026-01-13 15:47:58 -05:00
Compare commits
341 Commits
github-act
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ae2f7ddf1 | ||
|
|
b318d1d882 | ||
|
|
af0f2c1df4 | ||
|
|
c5ce50aaa3 | ||
|
|
c1756b85af | ||
|
|
dac1271782 | ||
|
|
1cc7a64f99 | ||
|
|
4deb7bca65 | ||
|
|
1a90485a10 | ||
|
|
48b8d9d7b2 | ||
|
|
7e60c68ba5 | ||
|
|
24adc16adf | ||
|
|
881f07eebe | ||
|
|
3a255a3ad9 | ||
|
|
9971b3c74e | ||
|
|
66a5695d89 | ||
|
|
fd22f713ed | ||
|
|
43dc5e6c2b | ||
|
|
3d42dde48c | ||
|
|
19f32ca57d | ||
|
|
c1070789fd | ||
|
|
1f0f44ede0 | ||
|
|
48cdcbceb2 | ||
|
|
45aed936b1 | ||
|
|
e4f7e8dc80 | ||
|
|
15ac076aed | ||
|
|
5725c776a7 | ||
|
|
6f541d33cc | ||
|
|
2540c169dc | ||
|
|
75147e91e7 | ||
|
|
59b6edb49c | ||
|
|
7683004464 | ||
|
|
e1f657939f | ||
|
|
55904155a8 | ||
|
|
c1be5138b6 | ||
|
|
063aa94576 | ||
|
|
668e149ae3 | ||
|
|
b5fca3155f | ||
|
|
bc514f8f4d | ||
|
|
3b868ce6df | ||
|
|
aad62d4fa1 | ||
|
|
21e92164e7 | ||
|
|
f0381c8b12 | ||
|
|
f6de2dc775 | ||
|
|
6aae1c7bd7 | ||
|
|
71a44fac28 | ||
|
|
6c48c935e2 | ||
|
|
25c6a0d5d4 | ||
|
|
b277bf3d50 | ||
|
|
83b6e0bbaf | ||
|
|
2726e51c2a | ||
|
|
da2742edb1 | ||
|
|
0ebff3b974 | ||
|
|
a52589e9fe | ||
|
|
5381c5e18b | ||
|
|
9ffcdbdb3b | ||
|
|
6bc1d90469 | ||
|
|
5fb3d75412 | ||
|
|
b3700ae685 | ||
|
|
1d81229931 | ||
|
|
22f96b34a5 | ||
|
|
beecedf6f3 | ||
|
|
566a9b1a52 | ||
|
|
aecc832529 | ||
|
|
9418f67636 | ||
|
|
f5c6c313fc | ||
|
|
fb533f838d | ||
|
|
60a7650e1c | ||
|
|
beb623cdcd | ||
|
|
cd5198a96f | ||
|
|
44bd14d2c5 | ||
|
|
7feb3ec00e | ||
|
|
ff896fc225 | ||
|
|
6485b8381b | ||
|
|
7945f44259 | ||
|
|
44a35da245 | ||
|
|
f8ce0d32d5 | ||
|
|
b55bcd5c0b | ||
|
|
6816379119 | ||
|
|
9d80991805 | ||
|
|
2e1a39fd2f | ||
|
|
30ddd05764 | ||
|
|
5fd84937c5 | ||
|
|
dc4fe43642 | ||
|
|
98afbe21ea | ||
|
|
041465a81c | ||
|
|
40fa275b4f | ||
|
|
6c9d89afea | ||
|
|
206887612a | ||
|
|
26ebfb416d | ||
|
|
6f16191af2 | ||
|
|
dd65b9c3e3 | ||
|
|
6b822eb6d1 | ||
|
|
809f5ff5f7 | ||
|
|
eca4083a04 | ||
|
|
0f315ae793 | ||
|
|
25105e4c3a | ||
|
|
2769121842 | ||
|
|
31200609b6 | ||
|
|
03b570c85c | ||
|
|
e3529e9ca7 | ||
|
|
2d7e3842d6 | ||
|
|
8ae927e2dc | ||
|
|
2859d4ebe3 | ||
|
|
306522e796 | ||
|
|
a91aa3b4de | ||
|
|
f46336b86a | ||
|
|
f975e49760 | ||
|
|
fa1cf46050 | ||
|
|
1d19ddd9e2 | ||
|
|
69b3c00a52 | ||
|
|
6f4afbfb52 | ||
|
|
796de7b07e | ||
|
|
9ad281a4ee | ||
|
|
1ce357b023 | ||
|
|
45dd07d229 | ||
|
|
fa164377e4 | ||
|
|
0b89767de0 | ||
|
|
2dded4caa4 | ||
|
|
670f74ebc7 | ||
|
|
89d2aa603b | ||
|
|
8702c17758 | ||
|
|
60597d23eb | ||
|
|
6f04927369 | ||
|
|
0c0cf9c627 | ||
|
|
8fe8e614ac | ||
|
|
68c9ed359c | ||
|
|
1b29f3a9fd | ||
|
|
596dcd7cea | ||
|
|
eb3be62a4f | ||
|
|
58ad1880ef | ||
|
|
5ee17235cb | ||
|
|
f9c7316eb3 | ||
|
|
dfe2730a45 | ||
|
|
a979ba091a | ||
|
|
6bbe07a9a1 | ||
|
|
0156bb9dc9 | ||
|
|
e27bbd0ac1 | ||
|
|
f3371ac1e3 | ||
|
|
9cc9f9d757 | ||
|
|
c8d5c53db1 | ||
|
|
fc4326233d | ||
|
|
07f7a05c4b | ||
|
|
9b2e41d255 | ||
|
|
6fad862aa5 | ||
|
|
c425878f21 | ||
|
|
1fb592afb2 | ||
|
|
684f3dadfc | ||
|
|
76e6070012 | ||
|
|
cb72837866 | ||
|
|
2432ef9de3 | ||
|
|
026fd1822a | ||
|
|
b42a52ef24 | ||
|
|
10b031ab36 | ||
|
|
d63b825ae5 | ||
|
|
b66da48106 | ||
|
|
6f24afc600 | ||
|
|
a80dcb03c4 | ||
|
|
bebbed856f | ||
|
|
2d5882c257 | ||
|
|
37fb140563 | ||
|
|
aebe7e200f | ||
|
|
6f1e4bcec6 | ||
|
|
2059549dce | ||
|
|
8361dc6838 | ||
|
|
73ccae9de3 | ||
|
|
c58e0c62a4 | ||
|
|
a4b4e65011 | ||
|
|
18f52772b3 | ||
|
|
fef25a2d06 | ||
|
|
bf578da16a | ||
|
|
41330d5517 | ||
|
|
a703fde7b2 | ||
|
|
c5ee5cd8df | ||
|
|
312540456a | ||
|
|
7ddbaf477a | ||
|
|
9a0f412fff | ||
|
|
7cd3ef09cb | ||
|
|
f814c6b89d | ||
|
|
74024a4997 | ||
|
|
cb7f5dd44d | ||
|
|
9cf2db84a9 | ||
|
|
ec307d561f | ||
|
|
058bad95cd | ||
|
|
7469633e3d | ||
|
|
278e4cb4c2 | ||
|
|
e19305dee2 | ||
|
|
673eee209d | ||
|
|
91399a0c68 | ||
|
|
a4480893cb | ||
|
|
c410486e30 | ||
|
|
5cda8b1b03 | ||
|
|
0ac7211ff7 | ||
|
|
670a758bee | ||
|
|
bdc98c6fa8 | ||
|
|
f0251d14bb | ||
|
|
61f1a27377 | ||
|
|
c39181e5bd | ||
|
|
45dc2c4535 | ||
|
|
a192441f68 | ||
|
|
744bb9b11d | ||
|
|
f3adc86693 | ||
|
|
714fac0637 | ||
|
|
eaf6600299 | ||
|
|
401be04b4d | ||
|
|
a9a2131ebf | ||
|
|
7dd8094760 | ||
|
|
a3d3363a01 | ||
|
|
75d0f57f09 | ||
|
|
8c92b92868 | ||
|
|
a33027b4db | ||
|
|
c1797de3da | ||
|
|
3d49081f6d | ||
|
|
8c318d96f4 | ||
|
|
9b7337f67f | ||
|
|
0b5adf104a | ||
|
|
ec699c48bf | ||
|
|
c9b45492a5 | ||
|
|
b0b90679a4 | ||
|
|
02cef105a6 | ||
|
|
407ca4b244 | ||
|
|
62e44e5e35 | ||
|
|
03fce6f5cf | ||
|
|
6155fca4b1 | ||
|
|
22d4af48c7 | ||
|
|
17d5a7143f | ||
|
|
011db5ad5e | ||
|
|
89682f0397 | ||
|
|
9ed9174b6f | ||
|
|
53ae8926fa | ||
|
|
6c743ba230 | ||
|
|
9852c36a98 | ||
|
|
5f85cafc7e | ||
|
|
0591e6e81e | ||
|
|
a296892aaf | ||
|
|
45958284c9 | ||
|
|
1d6980faf3 | ||
|
|
f793f01059 | ||
|
|
7deffa8ee2 | ||
|
|
84dfc85328 | ||
|
|
b79aef5fad | ||
|
|
57d9cc59b5 | ||
|
|
e599700d72 | ||
|
|
2761403735 | ||
|
|
e57033c4a1 | ||
|
|
ca049e4b5d | ||
|
|
2d48648f52 | ||
|
|
40297e0c6a | ||
|
|
75f0cd47b8 | ||
|
|
c76313737d | ||
|
|
a2b1890e7e | ||
|
|
df705b97b7 | ||
|
|
df280b7db7 | ||
|
|
67dff60a7d | ||
|
|
08fb4d801f | ||
|
|
ac8c87be9e | ||
|
|
a9050fda39 | ||
|
|
b98b892ba1 | ||
|
|
0460a252e2 | ||
|
|
ebeb68fd9b | ||
|
|
a9f550fb79 | ||
|
|
4181c0b20d | ||
|
|
765e9884dd | ||
|
|
ea9411fa69 | ||
|
|
12956d7633 | ||
|
|
c1627bb98c | ||
|
|
4e374aa9db | ||
|
|
ef003bdad6 | ||
|
|
ba5dd848b8 | ||
|
|
916ddf02d1 | ||
|
|
fa85ec0351 | ||
|
|
3a7f2d69e7 | ||
|
|
f240d35231 | ||
|
|
04ee73ca9d | ||
|
|
91885cdd1c | ||
|
|
b7a2cd46b3 | ||
|
|
028c0bc518 | ||
|
|
897eb73cd7 | ||
|
|
e7ce40a8d1 | ||
|
|
18d25672d7 | ||
|
|
79b1008e02 | ||
|
|
3c0a271bf8 | ||
|
|
ccfd24a9e5 | ||
|
|
0c8bf5562d | ||
|
|
96c7676a13 | ||
|
|
90f64d5475 | ||
|
|
a70b831f9d | ||
|
|
cad179953a | ||
|
|
8979841010 | ||
|
|
1e974086d3 | ||
|
|
792ab67ef1 | ||
|
|
a7af55df37 | ||
|
|
5c540abae9 | ||
|
|
f69e716dcc | ||
|
|
1d8d8e57c2 | ||
|
|
fdc638cd86 | ||
|
|
56e02f2604 | ||
|
|
b926a627a8 | ||
|
|
32f3e176f0 | ||
|
|
cf4212016f | ||
|
|
c491fa4bad | ||
|
|
f10fc37fe7 | ||
|
|
f7b1a6358f | ||
|
|
5f9f29c467 | ||
|
|
0ec5dab80a | ||
|
|
8d47a1bb0f | ||
|
|
fad05e0757 | ||
|
|
12f7e7eaf9 | ||
|
|
6490532818 | ||
|
|
e0e953dab8 | ||
|
|
75fa04efd4 | ||
|
|
93019139fb | ||
|
|
ce47c1fe81 | ||
|
|
a880dd6bd2 | ||
|
|
f193bb1d96 | ||
|
|
4cc34dfa5f | ||
|
|
d034238505 | ||
|
|
25acb1c219 | ||
|
|
44c9d48eae | ||
|
|
702eab9ec0 | ||
|
|
e8be3858ab | ||
|
|
50e7cd3576 | ||
|
|
4f57201e98 | ||
|
|
009462f92c | ||
|
|
34775e9e69 | ||
|
|
3f3043c48e | ||
|
|
820422760a | ||
|
|
b40657c1a7 | ||
|
|
dcd8f52be3 | ||
|
|
0eff198eb9 | ||
|
|
b345eee815 | ||
|
|
cba599337d | ||
|
|
4e25f1460a | ||
|
|
61b145455d | ||
|
|
0b8dc12047 | ||
|
|
1a1134a010 | ||
|
|
d40e2ca4af | ||
|
|
540b4b3bf1 | ||
|
|
0429b92120 | ||
|
|
c6036f0570 | ||
|
|
3eb319a919 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"prettier/prettier": "error",
|
"prettier/prettier": "error",
|
||||||
"no-console": "error",
|
"no-console": "error",
|
||||||
"sort-imports": "off",
|
|
||||||
"import/order": "off",
|
"import/order": "off",
|
||||||
|
"sort-imports": "off",
|
||||||
"simple-import-sort/imports": "error",
|
"simple-import-sort/imports": "error",
|
||||||
"simple-import-sort/exports": "error",
|
"simple-import-sort/exports": "error",
|
||||||
"import/first": "error",
|
"import/first": "error",
|
||||||
|
|||||||
6
.github/CONTRIBUTING.md
vendored
6
.github/CONTRIBUTING.md
vendored
@@ -9,7 +9,7 @@ Thanks for considering contributing to the project.
|
|||||||
3. Create a new branch for your changes.
|
3. Create a new branch for your changes.
|
||||||
4. Make your changes and commit them with descriptive commit messages.
|
4. Make your changes and commit them with descriptive commit messages.
|
||||||
5. Push your changes to your forked repository.
|
5. Push your changes to your forked repository.
|
||||||
6. Create a pull request from your branch to the `dev` branch.
|
6. Create a pull request from your branch to the `dev` branch. Not `master` branch, PR to `dev` branch, please.
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ To get started, follow these steps:
|
|||||||
1. Clone the project repository locally.
|
1. Clone the project repository locally.
|
||||||
2. Install dependencies with `npm install`.
|
2. Install dependencies with `npm install`.
|
||||||
3. Run the project with `npm run dev`.
|
3. Run the project with `npm run dev`.
|
||||||
4. See [issues](https://github.com/di-sukharev/open-commit/issues) or [TODO.md](../TODO.md) to help the project.
|
4. See [issues](https://github.com/di-sukharev/opencommit/issues) or [TODO.md](TODO.md) to help the project.
|
||||||
|
|
||||||
## Commit message guidelines
|
## Commit message guidelines
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ If you encounter any issues while using the project, please report them on the G
|
|||||||
|
|
||||||
## Contacts
|
## Contacts
|
||||||
|
|
||||||
If you have any questions about contributing to the project, please contact by [creating an issue](https://github.com/di-sukharev/open-commit/issues) on the GitHub issue tracker.
|
If you have any questions about contributing to the project, please contact by [creating an issue](https://github.com/di-sukharev/opencommit/issues) on the GitHub issue tracker.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
BIN
.github/github-mark-white.png
vendored
Normal file
BIN
.github/github-mark-white.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
@@ -40,11 +40,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -71,6 +71,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v3
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|||||||
4
.github/workflows/dependency-review.yml
vendored
4
.github/workflows/dependency-review.yml
vendored
@@ -15,6 +15,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: 'Checkout Repository'
|
- name: 'Checkout Repository'
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: 'Dependency Review'
|
- name: 'Dependency Review'
|
||||||
uses: actions/dependency-review-action@v2
|
uses: actions/dependency-review-action@v3
|
||||||
|
|||||||
28
.github/workflows/stale.yml
vendored
28
.github/workflows/stale.yml
vendored
@@ -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'
|
|
||||||
71
.github/workflows/test.yml
vendored
Normal file
71
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
name: Testing
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
unit-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20.x]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
- 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@v4
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
- 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
|
||||||
|
prettier:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
cache: 'npm'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Run Prettier
|
||||||
|
run: npm run format:check
|
||||||
|
- name: Prettier Output
|
||||||
|
if: failure()
|
||||||
|
run: |
|
||||||
|
echo "Prettier check failed. Please run 'npm run format' to fix formatting issues."
|
||||||
|
exit 1
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,3 +11,5 @@ uncaughtExceptions.log
|
|||||||
src/*.json
|
src/*.json
|
||||||
.idea
|
.idea
|
||||||
test.ts
|
test.ts
|
||||||
|
notes.md
|
||||||
|
.nvmrc
|
||||||
1
.npmignore
Normal file
1
.npmignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
out/github-action.cjs
|
||||||
1
.opencommitignore
Normal file
1
.opencommitignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
out
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
/build
|
/build
|
||||||
/dist
|
/dist
|
||||||
|
/out
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) Dima Sukharev
|
Copyright (c) Dima Sukharev, https://github.com/di-sukharev
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|||||||
411
README.md
411
README.md
@@ -2,12 +2,12 @@
|
|||||||
<div>
|
<div>
|
||||||
<img src=".github/logo-grad.svg" alt="OpenCommit logo"/>
|
<img src=".github/logo-grad.svg" alt="OpenCommit logo"/>
|
||||||
<h1 align="center">OpenCommit</h1>
|
<h1 align="center">OpenCommit</h1>
|
||||||
<h4 align="center">Follow the bird <a href="https://twitter.com/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>
|
||||||
</h4>
|
|
||||||
</div>
|
</div>
|
||||||
<h2>GPT CLI to auto-generate impressive commits in 1 second</h2>
|
<h2>Auto-generate meaningful commits in a second</h2>
|
||||||
<p>Killing lame commits with AI 🤯🔫</p>
|
<p>Killing lame commits with AI 🤯🔫</p>
|
||||||
<a href="https://www.npmjs.com/package/opencommit"><img src="https://img.shields.io/npm/v/opencommit" alt="Current version"></a>
|
<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 <a href="https://twitter.com/_sukharev_/status/1683448136973582336">GitHub 2023 hackathon</a> 🪩</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -16,72 +16,11 @@
|
|||||||
<img src=".github/opencommit-example.png" alt="OpenCommit example"/>
|
<img src=".github/opencommit-example.png" alt="OpenCommit example"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
All the commits in this repo are done with OpenCommit — look into [the commits](https://github.com/di-sukharev/opencommit/commit/eae7618d575ee8d2e9fff5de56da79d40c4bc5fc) to see how OpenCommit works. Emoji and long commit description text is 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 Github Action
|
## Setup OpenCommit as a CLI tool
|
||||||
|
|
||||||
OpenCommit is now available as a GitHub Action which automatically improves all new commits messages when you push to remote!
|
You can use OpenCommit by simply running it via the CLI like this `oco`. 2 seconds and your staged changes are committed with a meaningful message.
|
||||||
|
|
||||||
This is great if you want to make sure all of the commits in all of repository branches are meaningful and not lame like `fix1` or `done2`.
|
|
||||||
|
|
||||||
### Automatic 1 click setup
|
|
||||||
|
|
||||||
You can simply [setup the action automatically via the GitHub Marketplace](TODO).
|
|
||||||
|
|
||||||
### Manual 3 clicks setup
|
|
||||||
|
|
||||||
Create a file `.github/workflows/opencommit.yml` with contents below:
|
|
||||||
|
|
||||||
```yml
|
|
||||||
name: 'OpenCommit'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
# this list of branches is often enough,
|
|
||||||
# but you may still ignore other public branches
|
|
||||||
branches-ignore: [main master dev development release]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
opencommit:
|
|
||||||
name: OpenCommit
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions: write-all
|
|
||||||
steps:
|
|
||||||
- name: Setup Node.js Environment
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: '16'
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: di-sukharev/opencommit@github-action
|
|
||||||
with:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
env:
|
|
||||||
# set openAI api key in repo actions secrets,
|
|
||||||
# for openAI keys go to: https://platform.openai.com/account/api-keys
|
|
||||||
# for repo secret go to: <your_repo_url>/settings/secrets/actions
|
|
||||||
OCO_OPENAI_API_KEY: ${{ secrets.OCO_OPENAI_API_KEY }}
|
|
||||||
|
|
||||||
# customization
|
|
||||||
OCO_OPENAI_MAX_TOKENS: 500
|
|
||||||
OCO_OPENAI_BASE_PATH: ''
|
|
||||||
OCO_DESCRIPTION: false
|
|
||||||
OCO_EMOJI: false
|
|
||||||
OCO_MODEL: gpt-3.5-turbo
|
|
||||||
OCO_LANGUAGE: en
|
|
||||||
```
|
|
||||||
|
|
||||||
That is it. Now when you push to any branch in your repo — all NEW commits are being improved by never-tired-AI.
|
|
||||||
|
|
||||||
Make sure you exclude public collaboration branches (`main`, `dev`, `etc`) in `branches-ignore`, so OpenCommit does not rebase commits there when improving the messages.
|
|
||||||
|
|
||||||
Interactive rebase (`rebase -i`) changes commit SHA, so commit history in remote becomes different with your local branch history. It's ok when you work on the branch alone, but may be inconvenient for other collaborators.
|
|
||||||
|
|
||||||
## Setup OpenCommit as a CLI
|
|
||||||
|
|
||||||
You can use OpenCommit by simply running it via CLI like this `oc`. 2 seconds and your staged changes are committed with a meaningful message.
|
|
||||||
|
|
||||||
1. Install OpenCommit globally to use in any repository:
|
1. Install OpenCommit globally to use in any repository:
|
||||||
|
|
||||||
@@ -89,107 +28,260 @@ You can use OpenCommit by simply running it via CLI like this `oc`. 2 seconds an
|
|||||||
npm install -g opencommit
|
npm install -g opencommit
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Get your API key from [OpenAI](https://platform.openai.com/account/api-keys). Make sure you add payment details, so API works.
|
2. Get your API key from [OpenAI](https://platform.openai.com/account/api-keys) or other supported LLM providers (we support them all). Make sure that you add your OpenAI payment details to your account, so the API works.
|
||||||
|
|
||||||
3. Set the key to OpenCommit config:
|
3. Set the key to OpenCommit config:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
opencommit config set OCO_OPENAI_API_KEY=<your_api_key>
|
oco config set OCO_API_KEY=<your_api_key>
|
||||||
```
|
```
|
||||||
|
|
||||||
Your api key is stored locally in `~/.opencommit` config file.
|
Your API key is stored locally in the `~/.opencommit` config file.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
You can call OpenCommit directly to generate a commit message for your staged changes:
|
You can call OpenCommit with `oco` command to generate a commit message for your staged changes:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git add <files...>
|
git add <files...>
|
||||||
opencommit
|
oco
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also use the `oc` shortcut:
|
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
|
```sh
|
||||||
git add <files...>
|
git add <files...>
|
||||||
oc
|
oco config set OCO_AI_PROVIDER='ollama' OCO_MODEL='llama3:8b'
|
||||||
|
```
|
||||||
|
|
||||||
|
Default model is `mistral`.
|
||||||
|
|
||||||
|
If you have ollama that is set up in docker/ on another machine with GPUs (not locally), you can change the default endpoint url.
|
||||||
|
|
||||||
|
You can do so by setting the `OCO_API_URL` environment variable as follows:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco config set OCO_API_URL='http://192.168.1.10:11434/api/chat'
|
||||||
|
```
|
||||||
|
|
||||||
|
where 192.168.1.10 is example of endpoint URL, where you have ollama set up.
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
|
||||||
|
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
|
## Configuration
|
||||||
|
|
||||||
### Local per repo configuration
|
### Local per repo configuration
|
||||||
|
|
||||||
Create an `.env` file and add OpenCommit config variables there like this:
|
Create a `.env` file and add OpenCommit config variables there like this:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
OCO_OPENAI_API_KEY=<your openAI API token>
|
...
|
||||||
OCO_OPENAI_MAX_TOKENS=<max response tokens from openAI API>
|
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama, gemini, flowise, deepseek, aimlapi>
|
||||||
OCO_OPENAI_BASE_PATH=<may be used to set proxy path to openAI api>
|
OCO_API_KEY=<your OpenAI API token> // or other LLM provider API token
|
||||||
OCO_DESCRIPTION=<postface a message with ~3 sentences description>
|
OCO_API_URL=<may be used to set proxy path to OpenAI api>
|
||||||
OCO_EMOJI=<add GitMoji>
|
OCO_API_CUSTOM_HEADERS=<JSON string of custom HTTP headers to include in API requests>
|
||||||
OCO_MODEL=<either gpt-3.5-turbo or gpt-4>
|
OCO_TOKENS_MAX_INPUT=<max model token limit (default: 4096)>
|
||||||
|
OCO_TOKENS_MAX_OUTPUT=<max response tokens (default: 500)>
|
||||||
|
OCO_DESCRIPTION=<postface a message with ~3 sentences description of the changes>
|
||||||
|
OCO_EMOJI=<boolean, add GitMoji>
|
||||||
|
OCO_MODEL=<either 'gpt-4o-mini' (default), 'gpt-4o', 'gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0125', 'gpt-4-1106-preview', 'gpt-4-turbo-preview' or 'gpt-4-0125-preview' or any Anthropic or Ollama model or any string basically, but it should be a valid model name>
|
||||||
OCO_LANGUAGE=<locale, scroll to the bottom to see options>
|
OCO_LANGUAGE=<locale, scroll to the bottom to see options>
|
||||||
|
OCO_MESSAGE_TEMPLATE_PLACEHOLDER=<message template placeholder, default: '$msg'>
|
||||||
|
OCO_PROMPT_MODULE=<either conventional-commit or @commitlint, default: conventional-commit>
|
||||||
|
OCO_ONE_LINE_COMMIT=<one line commit message, default: false>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Global configs are same as local configs, but they are stored in the global `~/.opencommit` config file and set with `oco config set` command, e.g. `oco config set OCO_MODEL=gpt-4o`.
|
||||||
|
|
||||||
### Global config for all repos
|
### Global config for all repos
|
||||||
|
|
||||||
Local config still has more priority as Global config, but you may set `OCO_MODEL` and `OCO_LOCALE` globally and set local configs for `OCO_EMOJI` and `OCO_DESCRIPTION` per repo which is more convenient.
|
Local config still has more priority than Global config, but you may set `OCO_MODEL` and `OCO_LOCALE` globally and set local configs for `OCO_EMOJI` and `OCO_DESCRIPTION` per repo which is more convenient.
|
||||||
|
|
||||||
Simply run any of the variable above like this:
|
Simply set any of the variables above like this:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
oc config set OCO_OPENAI_API_KEY=gpt-4
|
oco config set OCO_MODEL=gpt-4o-mini
|
||||||
|
```
|
||||||
|
|
||||||
|
To see all available configuration parameters and their accepted values:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco config describe
|
||||||
|
```
|
||||||
|
|
||||||
|
To see details for a specific parameter:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco config describe OCO_MODEL
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure [GitMoji](https://gitmoji.dev/) to preface a message.
|
Configure [GitMoji](https://gitmoji.dev/) to preface a message.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
oc config set OCO_EMOJI=true
|
oco config set OCO_EMOJI=true
|
||||||
```
|
```
|
||||||
|
|
||||||
To remove preface emoji:
|
To remove preface emojis:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
oc config set OCO_EMOJI=false
|
oco config set OCO_EMOJI=false
|
||||||
```
|
```
|
||||||
|
|
||||||
### Switch to GPT-4
|
Other config options are behaving the same.
|
||||||
|
|
||||||
By default OpenCommit uses GPT-3.5-turbo (ChatGPT).
|
### Output WHY the changes were done (WIP)
|
||||||
|
|
||||||
You may switch to GPT-4 which performs better, but costs ~x15 times more 🤠
|
You can set the `OCO_WHY` config to `true` to have OpenCommit output a short description of WHY the changes were done after the commit message. Default is `false`.
|
||||||
|
|
||||||
|
To make this perform accurate we must store 'what files do' in some kind of an index or embedding and perform a lookup (kinda RAG) for the accurate git commit message. If you feel like building this comment on this ticket https://github.com/di-sukharev/opencommit/issues/398 and let's go from there together.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
oc config set OCO_MODEL=gpt-4
|
oco config set OCO_WHY=true
|
||||||
```
|
```
|
||||||
|
|
||||||
Make sure you do lowercase `gpt-4` and you have API access to the 4th model. Even if you have ChatGPT+ it doesn't necessarily mean that you have API access to GPT-4.
|
### Switch to GPT-4 or other models
|
||||||
|
|
||||||
## Locale configuration
|
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:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco config set OCO_MODEL=gpt-3.5-turbo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Switch to other LLM providers with a custom URL
|
||||||
|
|
||||||
|
By default OpenCommit uses [OpenAI](https://openai.com).
|
||||||
|
|
||||||
|
You could switch to [Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/) or Flowise or Ollama.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco config set OCO_AI_PROVIDER=azure OCO_API_KEY=<your_azure_api_key> OCO_API_URL=<your_azure_endpoint>
|
||||||
|
|
||||||
|
oco config set OCO_AI_PROVIDER=flowise OCO_API_KEY=<your_flowise_api_key> OCO_API_URL=<your_flowise_endpoint>
|
||||||
|
|
||||||
|
oco config set OCO_AI_PROVIDER=ollama OCO_API_KEY=<your_ollama_api_key> OCO_API_URL=<your_ollama_endpoint>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Locale configuration
|
||||||
|
|
||||||
To globally specify the language used to generate commit messages:
|
To globally specify the language used to generate commit messages:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# de, German ,Deutsch
|
# de, German, Deutsch
|
||||||
oc config set OCO_LANGUAGE=de
|
oco config set OCO_LANGUAGE=de
|
||||||
oc config set OCO_LANGUAGE=German
|
oco config set OCO_LANGUAGE=German
|
||||||
oc config set OCO_LANGUAGE=Deutsch
|
oco config set OCO_LANGUAGE=Deutsch
|
||||||
|
|
||||||
# fr, French, française
|
# fr, French, française
|
||||||
oc config set OCO_LANGUAGE=fr
|
oco config set OCO_LANGUAGE=fr
|
||||||
oc config set OCO_LANGUAGE=French
|
oco config set OCO_LANGUAGE=French
|
||||||
oc config set OCO_LANGUAGE=française
|
oco config set OCO_LANGUAGE=française
|
||||||
```
|
```
|
||||||
|
|
||||||
The default language set is **English**
|
The default language setting is **English**
|
||||||
All available languages are currently listed in the [i18n](https://github.com/di-sukharev/opencommit/tree/master/src/i18n) folder
|
All available languages are currently listed in the [i18n](https://github.com/di-sukharev/opencommit/tree/master/src/i18n) folder
|
||||||
|
|
||||||
### Git flags
|
### Push to git (gonna be deprecated)
|
||||||
|
|
||||||
The `opencommit` or `oc` commands can be used in place of the `git commit -m "${generatedMessage}"` command. This means that any regular flags that are used with the `git commit` command will also be applied when using `opencommit` or `oc`.
|
A prompt for pushing to git is on by default but if you would like to turn it off just use:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
oc --no-verify
|
oco config set OCO_GITPUSH=false
|
||||||
|
```
|
||||||
|
|
||||||
|
and it will exit right after commit is confirmed without asking if you would like to push to remote.
|
||||||
|
|
||||||
|
### Switch to `@commitlint`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
You can set this option by running the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco config set OCO_PROMPT_MODULE=<module>
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `<module>` with either `conventional-commit` or `@commitlint`.
|
||||||
|
|
||||||
|
#### Example:
|
||||||
|
|
||||||
|
To switch to using the `'@commitlint` prompt module, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco config set OCO_PROMPT_MODULE=@commitlint
|
||||||
|
```
|
||||||
|
|
||||||
|
To switch back to the default conventional-commit message generator, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco config set OCO_PROMPT_MODULE=conventional-commit
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Integrating with `@commitlint`
|
||||||
|
|
||||||
|
The integration between `@commitlint` and OpenCommit is done automatically the first time OpenCommit is run with `OCO_PROMPT_MODULE` set to `@commitlint`. However, if you need to force set or reset the configuration for `@commitlint`, you can run the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco commitlint force
|
||||||
|
```
|
||||||
|
|
||||||
|
To view the generated configuration for `@commitlint`, you can use this command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco commitlint get
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows you to ensure that the configuration is set up as desired.
|
||||||
|
|
||||||
|
Additionally, the integration creates a file named `.opencommit-commitlint` which contains the prompts used for the local `@commitlint` configuration. You can modify this file to fine-tune the example commit message generated by OpenAI. This gives you the flexibility to make adjustments based on your preferences or project guidelines.
|
||||||
|
|
||||||
|
OpenCommit generates a file named `.opencommit-commitlint` in your project directory which contains the prompts used for the local `@commitlint` configuration. You can modify this file to fine-tune the example commit message generated by OpenAI. If the local `@commitlint` configuration changes, this file will be updated the next time OpenCommit is run.
|
||||||
|
|
||||||
|
This offers you greater control over the generated commit messages, allowing for customization that aligns with your project's conventions.
|
||||||
|
|
||||||
|
## Git flags
|
||||||
|
|
||||||
|
The `opencommit` or `oco` commands can be used in place of the `git commit -m "${generatedMessage}"` command. This means that any regular flags that are used with the `git commit` command will also be applied when using `opencommit` or `oco`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco --no-verify
|
||||||
```
|
```
|
||||||
|
|
||||||
is translated to :
|
is translated to :
|
||||||
@@ -198,33 +290,68 @@ is translated to :
|
|||||||
git commit -m "${generatedMessage}" --no-verify
|
git commit -m "${generatedMessage}" --no-verify
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To include a message in the generated message, you can utilize the template function, for instance:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco '#205: $msg’
|
||||||
|
```
|
||||||
|
|
||||||
|
> opencommit examines placeholders in the parameters, allowing you to append additional information before and after the placeholders, such as the relevant Issue or Pull Request. Similarly, you have the option to customize the OCO_MESSAGE_TEMPLATE_PLACEHOLDER configuration item, for example, simplifying it to $m!"
|
||||||
|
|
||||||
|
### Message Template Placeholder Config
|
||||||
|
|
||||||
|
#### Overview
|
||||||
|
|
||||||
|
The `OCO_MESSAGE_TEMPLATE_PLACEHOLDER` feature in the `opencommit` tool allows users to embed a custom message within the generated commit message using a template function. This configuration is designed to enhance the flexibility and customizability of commit messages, making it easier for users to include relevant information directly within their commits.
|
||||||
|
|
||||||
|
#### Implementation Details
|
||||||
|
|
||||||
|
In our codebase, the implementation of this feature can be found in the following segment:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
commitMessage = messageTemplate.replace(
|
||||||
|
config.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
|
||||||
|
commitMessage
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
This line is responsible for replacing the placeholder in the `messageTemplate` with the actual `commitMessage`.
|
||||||
|
|
||||||
|
#### Usage
|
||||||
|
|
||||||
|
For instance, using the command `oco '$msg #205’`, users can leverage this feature. The provided code represents the backend mechanics of such commands, ensuring that the placeholder is replaced with the appropriate commit message.
|
||||||
|
|
||||||
|
#### Committing with the Message
|
||||||
|
|
||||||
|
Once users have generated their desired commit message, they can proceed to commit using the generated message. By understanding the feature's full potential and its implementation details, users can confidently use the generated messages for their commits.
|
||||||
|
|
||||||
### Ignore files
|
### Ignore files
|
||||||
|
|
||||||
You can ignore files from submission to OpenAI by creating a `.opencommitignore` file. For example:
|
You can remove files from being sent to OpenAI by creating a `.opencommitignore` file. For example:
|
||||||
|
|
||||||
```ignorelang
|
```ignorelang
|
||||||
path/to/large-asset.zip
|
path/to/large-asset.zip
|
||||||
**/*.jpg
|
**/*.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
This is useful for preventing opencommit from uploading artifacts and large files.
|
This helps prevent opencommit from uploading artifacts and large files.
|
||||||
|
|
||||||
By default, opencommit ignores files matching: `*-lock.*` and `*.lock`
|
By default, opencommit ignores files matching: `*-lock.*` and `*.lock`
|
||||||
|
|
||||||
## Git hook (KILLER FEATURE)
|
## Git hook (KILLER FEATURE)
|
||||||
|
|
||||||
You can set OpenCommit as Git [`prepare-commit-msg`](https://git-scm.com/docs/githooks#_prepare_commit_msg) hook. Hook integrates with you IDE Source Control and allows you edit the message before commit.
|
You can set OpenCommit as Git [`prepare-commit-msg`](https://git-scm.com/docs/githooks#_prepare_commit_msg) hook. Hook integrates with your IDE Source Control and allows you to edit the message before committing.
|
||||||
|
|
||||||
To set the hook:
|
To set the hook:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
oc hook set
|
oco hook set
|
||||||
```
|
```
|
||||||
|
|
||||||
To unset the hook:
|
To unset the hook:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
oc hook unset
|
oco hook unset
|
||||||
```
|
```
|
||||||
|
|
||||||
To use the hook:
|
To use the hook:
|
||||||
@@ -236,6 +363,70 @@ git commit
|
|||||||
|
|
||||||
Or follow the process of your IDE Source Control feature, when it calls `git commit` command — OpenCommit will integrate into the flow.
|
Or follow the process of your IDE Source Control feature, when it calls `git commit` command — OpenCommit will integrate into the flow.
|
||||||
|
|
||||||
|
## Setup OpenCommit as a GitHub Action (BETA) 🔥
|
||||||
|
|
||||||
|
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 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:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
name: 'OpenCommit Action'
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# this list of branches is often enough,
|
||||||
|
# but you may still ignore other public branches
|
||||||
|
branches-ignore: [main master dev development release]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
opencommit:
|
||||||
|
timeout-minutes: 10
|
||||||
|
name: OpenCommit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions: write-all
|
||||||
|
steps:
|
||||||
|
- name: Setup Node.js Environment
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: di-sukharev/opencommit@github-action-v1.0.4
|
||||||
|
with:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
env:
|
||||||
|
# set openAI api key in repo actions secrets,
|
||||||
|
# for openAI keys go to: https://platform.openai.com/account/api-keys
|
||||||
|
# for repo secret go to: <your_repo_url>/settings/secrets/actions
|
||||||
|
OCO_API_KEY: ${{ secrets.OCO_API_KEY }}
|
||||||
|
|
||||||
|
# customization
|
||||||
|
OCO_TOKENS_MAX_INPUT: 4096
|
||||||
|
OCO_TOKENS_MAX_OUTPUT: 500
|
||||||
|
OCO_OPENAI_BASE_PATH: ''
|
||||||
|
OCO_DESCRIPTION: false
|
||||||
|
OCO_EMOJI: false
|
||||||
|
OCO_MODEL: gpt-4o
|
||||||
|
OCO_LANGUAGE: en
|
||||||
|
OCO_PROMPT_MODULE: conventional-commit
|
||||||
|
```
|
||||||
|
|
||||||
|
That is it. Now when you push to any branch in your repo — all NEW commits are being improved by your never-tired AI.
|
||||||
|
|
||||||
|
Make sure you exclude public collaboration branches (`main`, `dev`, `etc`) in `branches-ignore`, so OpenCommit does not rebase commits there while improving the messages.
|
||||||
|
|
||||||
|
Interactive rebase (`rebase -i`) changes commits' SHA, so the commit history in remote becomes different from your local branch history. This is okay if you work on the branch alone, but may be inconvenient for other collaborators.
|
||||||
|
|
||||||
## Payments
|
## Payments
|
||||||
|
|
||||||
You pay for your own requests to OpenAI API. OpenCommit uses ChatGPT (3.5-turbo) official model, that is ~15x times cheaper than GPT-4.
|
You pay for your requests to OpenAI API on your own.
|
||||||
|
|
||||||
|
OpenCommit stores your key locally.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: 'OpenCommit — improve commits with AI 🤯🔫'
|
name: 'OpenCommit — improve commits with AI 🧙'
|
||||||
description: 'Replaces lame commit messages with meaningful AI-generated messages when you push to remote'
|
description: 'Replaces lame commit messages with meaningful AI-generated messages when you push to remote'
|
||||||
author: 'https://github.com/di-sukharev'
|
author: 'https://github.com/di-sukharev'
|
||||||
repo: 'https://github.com/di-sukharev/opencommit/tree/github-action'
|
repo: 'https://github.com/di-sukharev/opencommit/tree/github-action'
|
||||||
|
|||||||
42
jest.config.ts
Normal file
42
jest.config.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 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/default-esm',
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'],
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testRegex: ['.*\\.test\\.ts$'],
|
||||||
|
// Tell Jest to ignore the specific duplicate package.json files
|
||||||
|
// that are causing Haste module naming collisions
|
||||||
|
modulePathIgnorePatterns: [
|
||||||
|
'<rootDir>/test/e2e/prompt-module/data/'
|
||||||
|
],
|
||||||
|
transformIgnorePatterns: [
|
||||||
|
'node_modules/(?!(cli-testing-library|@clack|cleye)/.*)'
|
||||||
|
],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(ts|tsx|js|jsx|mjs)$': [
|
||||||
|
'ts-jest',
|
||||||
|
{
|
||||||
|
diagnostics: false,
|
||||||
|
useESM: true,
|
||||||
|
tsconfig: {
|
||||||
|
module: 'ESNext',
|
||||||
|
target: 'ES2022'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^(\\.{1,2}/.*)\\.js$': '$1'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
60753
out/cli.cjs
60753
out/cli.cjs
File diff suppressed because one or more lines are too long
83382
out/github-action.cjs
83382
out/github-action.cjs
File diff suppressed because one or more lines are too long
Binary file not shown.
8266
package-lock.json
generated
8266
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
65
package.json
65
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "opencommit",
|
"name": "opencommit",
|
||||||
"version": "2.0.17",
|
"version": "3.2.10",
|
||||||
"description": "Auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
|
"description": "Auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"git",
|
"git",
|
||||||
@@ -12,22 +12,23 @@
|
|||||||
"aicommit",
|
"aicommit",
|
||||||
"aicommits",
|
"aicommits",
|
||||||
"gptcommit",
|
"gptcommit",
|
||||||
"commit"
|
"commit",
|
||||||
|
"ollama"
|
||||||
],
|
],
|
||||||
"main": "cli.js",
|
"main": "cli.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencommit": "./out/cli.cjs",
|
"opencommit": "out/cli.cjs",
|
||||||
"oc": "./out/cli.cjs",
|
"oco": "out/cli.cjs"
|
||||||
"oco": "./out/cli.cjs"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"url": "https://github.com/di-sukharev/opencommit"
|
"url": "git+https://github.com/di-sukharev/opencommit.git"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "https://github.com/di-sukharev",
|
"author": "https://github.com/di-sukharev",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"files": [
|
"files": [
|
||||||
"out/**/*"
|
"out/cli.cjs",
|
||||||
|
"out/tiktoken_bg.wasm"
|
||||||
],
|
],
|
||||||
"release": {
|
"release": {
|
||||||
"branches": [
|
"branches": [
|
||||||
@@ -40,40 +41,72 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "npm run -S build -- --sourcemap --watch",
|
"watch": "npm run -S build -- --sourcemap --watch",
|
||||||
"start": "node ./out/cli.cjs",
|
"start": "node ./out/cli.cjs",
|
||||||
|
"ollama:start": "OCO_AI_PROVIDER='ollama' node ./out/cli.cjs",
|
||||||
"dev": "ts-node ./src/cli.ts",
|
"dev": "ts-node ./src/cli.ts",
|
||||||
"build": "rimraf out && node esbuild.config.js",
|
"dev:gemini": "OCO_AI_PROVIDER='gemini' ts-node ./src/cli.ts",
|
||||||
"deploy": "npm run build && npm version patch && npm publish --tag latest",
|
"build": "npx rimraf out && node esbuild.config.js",
|
||||||
|
"build:push": "npm run build && git add . && git commit -m 'build' && git push",
|
||||||
|
"deploy": "npm publish --tag latest",
|
||||||
|
"deploy:build": "npm run build:push && git push --tags && npm run deploy",
|
||||||
|
"deploy:patch": "npm version patch && npm run deploy:build",
|
||||||
"lint": "eslint src --ext ts && tsc --noEmit",
|
"lint": "eslint src --ext ts && tsc --noEmit",
|
||||||
"format": "prettier --write src"
|
"format": "prettier --write src",
|
||||||
|
"format:check": "prettier --check src",
|
||||||
|
"test": "node --no-warnings --experimental-vm-modules $( [ -f ./node_modules/.bin/jest ] && echo ./node_modules/.bin/jest || which jest ) test/unit",
|
||||||
|
"test: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",
|
||||||
|
"mlx:start": "OCO_AI_PROVIDER='mlx' node ./out/cli.cjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@commitlint/types": "^17.4.4",
|
||||||
"@types/ini": "^1.3.31",
|
"@types/ini": "^1.3.31",
|
||||||
"@types/inquirer": "^9.0.3",
|
"@types/inquirer": "^9.0.3",
|
||||||
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^16.18.14",
|
"@types/node": "^16.18.14",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^8.29.0",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^8.29.0",
|
||||||
|
"cli-testing-library": "^2.0.2",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"esbuild": "^0.15.18",
|
"esbuild": "^0.25.5",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^9.24.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.4",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"ts-jest": "^29.1.2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.9.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"@actions/github": "^5.1.1",
|
"@actions/github": "^6.0.1",
|
||||||
|
"@anthropic-ai/sdk": "^0.19.2",
|
||||||
|
"@azure/openai": "^1.0.0-beta.12",
|
||||||
"@clack/prompts": "^0.6.1",
|
"@clack/prompts": "^0.6.1",
|
||||||
"@dqbd/tiktoken": "^1.0.2",
|
"@dqbd/tiktoken": "^1.0.2",
|
||||||
|
"@google/generative-ai": "^0.11.4",
|
||||||
|
"@mistralai/mistralai": "^1.3.5",
|
||||||
"@octokit/webhooks-schemas": "^6.11.0",
|
"@octokit/webhooks-schemas": "^6.11.0",
|
||||||
"@octokit/webhooks-types": "^6.11.0",
|
"@octokit/webhooks-types": "^6.11.0",
|
||||||
"axios": "^1.3.4",
|
"axios": "^1.3.4",
|
||||||
"chalk": "^5.2.0",
|
"chalk": "^5.2.0",
|
||||||
"cleye": "^1.3.2",
|
"cleye": "^1.3.2",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
"execa": "^7.0.0",
|
"execa": "^7.0.0",
|
||||||
"ignore": "^5.2.4",
|
"ignore": "^5.2.4",
|
||||||
"ini": "^3.0.1",
|
"ini": "^3.0.1",
|
||||||
"inquirer": "^9.1.4",
|
"inquirer": "^9.1.4",
|
||||||
"openai": "^3.2.1"
|
"openai": "^4.57.0",
|
||||||
|
"punycode": "^2.3.1",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"ajv": "^8.17.1",
|
||||||
|
"whatwg-url": "^14.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export enum COMMANDS {
|
export enum COMMANDS {
|
||||||
|
config = 'config',
|
||||||
hook = 'hook',
|
hook = 'hook',
|
||||||
config = 'config'
|
commitlint = 'commitlint'
|
||||||
}
|
}
|
||||||
|
|||||||
101
src/api.ts
101
src/api.ts
@@ -1,101 +0,0 @@
|
|||||||
import { intro, outro } from '@clack/prompts';
|
|
||||||
import axios from 'axios';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import {
|
|
||||||
ChatCompletionRequestMessage,
|
|
||||||
Configuration as OpenAiApiConfiguration,
|
|
||||||
OpenAIApi
|
|
||||||
} from 'openai';
|
|
||||||
|
|
||||||
import { CONFIG_MODES, getConfig } from './commands/config';
|
|
||||||
|
|
||||||
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 `oc 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 { 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 { data } = await axios.get(
|
|
||||||
'https://unpkg.com/opencommit/package.json'
|
|
||||||
);
|
|
||||||
return data.version;
|
|
||||||
} catch (_) {
|
|
||||||
outro('Error while getting the latest version of opencommit');
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const api = new OpenAi();
|
|
||||||
37
src/cli.ts
37
src/cli.ts
@@ -1,13 +1,15 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { cli } from 'cleye';
|
import { cli } from 'cleye';
|
||||||
import packageJSON from '../package.json' assert { type: 'json' };
|
|
||||||
|
|
||||||
|
import packageJSON from '../package.json';
|
||||||
|
import { commit } from './commands/commit';
|
||||||
|
import { commitlintConfigCommand } from './commands/commitlint';
|
||||||
import { configCommand } from './commands/config';
|
import { configCommand } from './commands/config';
|
||||||
import { hookCommand, isHookCalled } from './commands/githook.js';
|
import { hookCommand, isHookCalled } from './commands/githook.js';
|
||||||
import { prepareCommitMessageHook } from './commands/prepare-commit-msg-hook';
|
import { prepareCommitMessageHook } from './commands/prepare-commit-msg-hook';
|
||||||
import { commit } from './commands/commit';
|
import { checkIsLatestVersion } from './utils/checkIsLatestVersion';
|
||||||
// import { checkIsLatestVersion } from './utils/checkIsLatestVersion';
|
import { runMigrations } from './migrations/_run.js';
|
||||||
|
|
||||||
const extraArgs = process.argv.slice(2);
|
const extraArgs = process.argv.slice(2);
|
||||||
|
|
||||||
@@ -15,18 +17,37 @@ cli(
|
|||||||
{
|
{
|
||||||
version: packageJSON.version,
|
version: packageJSON.version,
|
||||||
name: 'opencommit',
|
name: 'opencommit',
|
||||||
commands: [configCommand, hookCommand],
|
commands: [configCommand, hookCommand, commitlintConfigCommand],
|
||||||
flags: {},
|
flags: {
|
||||||
|
fgm: {
|
||||||
|
type: Boolean,
|
||||||
|
description: 'Use full GitMoji specification',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
type: String,
|
||||||
|
alias: 'c',
|
||||||
|
description: 'Additional user input context for the commit message',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
yes: {
|
||||||
|
type: Boolean,
|
||||||
|
alias: 'y',
|
||||||
|
description: 'Skip commit confirmation prompt',
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
ignoreArgv: (type) => type === 'unknown-flag' || type === 'argument',
|
ignoreArgv: (type) => type === 'unknown-flag' || type === 'argument',
|
||||||
help: { description: packageJSON.description }
|
help: { description: packageJSON.description }
|
||||||
},
|
},
|
||||||
async () => {
|
async ({ flags }) => {
|
||||||
// await checkIsLatestVersion();
|
await runMigrations();
|
||||||
|
await checkIsLatestVersion();
|
||||||
|
|
||||||
if (await isHookCalled()) {
|
if (await isHookCalled()) {
|
||||||
prepareCommitMessageHook();
|
prepareCommitMessageHook();
|
||||||
} else {
|
} else {
|
||||||
commit(extraArgs);
|
commit(extraArgs, flags.context, false, flags.fgm, flags.yes);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extraArgs
|
extraArgs
|
||||||
|
|||||||
5
src/commands/ENUMS.ts
Normal file
5
src/commands/ENUMS.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export enum COMMANDS {
|
||||||
|
config = 'config',
|
||||||
|
hook = 'hook',
|
||||||
|
commitlint = 'commitlint'
|
||||||
|
}
|
||||||
9
src/commands/README.md
Normal file
9
src/commands/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# @commitlint Module for opencommit
|
||||||
|
|
||||||
|
1. Load commitlint configuration within tree.
|
||||||
|
2. Generate a commit with commitlint prompt:
|
||||||
|
- Will not run if hash is the same.
|
||||||
|
- Infer a prompt for each commitlint rule.
|
||||||
|
- Ask OpenAI to generate consistency with embedded commitlint rules.
|
||||||
|
- Store configuration close to commitlint configuration.
|
||||||
|
3. Replace conventional-commit prompt with commitlint prompt.
|
||||||
@@ -1,8 +1,16 @@
|
|||||||
import { execa } from 'execa';
|
|
||||||
import {
|
import {
|
||||||
GenerateCommitMessageErrorEnum,
|
text,
|
||||||
generateCommitMessageByDiff
|
confirm,
|
||||||
} from '../generateCommitMessageFromGitDiff';
|
intro,
|
||||||
|
isCancel,
|
||||||
|
multiselect,
|
||||||
|
outro,
|
||||||
|
select,
|
||||||
|
spinner
|
||||||
|
} from '@clack/prompts';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { execa } from 'execa';
|
||||||
|
import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
|
||||||
import {
|
import {
|
||||||
assertGitRepo,
|
assertGitRepo,
|
||||||
getChangedFiles,
|
getChangedFiles,
|
||||||
@@ -10,61 +18,116 @@ import {
|
|||||||
getStagedFiles,
|
getStagedFiles,
|
||||||
gitAdd
|
gitAdd
|
||||||
} from '../utils/git';
|
} from '../utils/git';
|
||||||
import {
|
|
||||||
spinner,
|
|
||||||
confirm,
|
|
||||||
outro,
|
|
||||||
isCancel,
|
|
||||||
intro,
|
|
||||||
multiselect,
|
|
||||||
select
|
|
||||||
} from '@clack/prompts';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { trytm } from '../utils/trytm';
|
import { trytm } from '../utils/trytm';
|
||||||
|
import { getConfig } from './config';
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
const getGitRemotes = async () => {
|
const getGitRemotes = async () => {
|
||||||
const { stdout } = await execa('git', ['remote']);
|
const { stdout } = await execa('git', ['remote']);
|
||||||
return stdout.split('\n').filter((remote) => Boolean(remote.trim()));
|
return stdout.split('\n').filter((remote) => Boolean(remote.trim()));
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateCommitMessageFromGitDiff = async (
|
// Check for the presence of message templates
|
||||||
diff: string,
|
const checkMessageTemplate = (extraArgs: string[]): string | false => {
|
||||||
extraArgs: string[]
|
for (const key in extraArgs) {
|
||||||
): Promise<void> => {
|
if (extraArgs[key].includes(config.OCO_MESSAGE_TEMPLATE_PLACEHOLDER))
|
||||||
|
return extraArgs[key];
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GenerateCommitMessageFromGitDiffParams {
|
||||||
|
diff: string;
|
||||||
|
extraArgs: string[];
|
||||||
|
context?: string;
|
||||||
|
fullGitMojiSpec?: boolean;
|
||||||
|
skipCommitConfirmation?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateCommitMessageFromGitDiff = async ({
|
||||||
|
diff,
|
||||||
|
extraArgs,
|
||||||
|
context = '',
|
||||||
|
fullGitMojiSpec = false,
|
||||||
|
skipCommitConfirmation = false
|
||||||
|
}: GenerateCommitMessageFromGitDiffParams): Promise<void> => {
|
||||||
await assertGitRepo();
|
await assertGitRepo();
|
||||||
|
const commitGenerationSpinner = spinner();
|
||||||
|
commitGenerationSpinner.start('Generating the commit message');
|
||||||
|
|
||||||
const commitSpinner = spinner();
|
|
||||||
commitSpinner.start('Generating the commit message');
|
|
||||||
try {
|
try {
|
||||||
const commitMessage = await generateCommitMessageByDiff(diff);
|
let commitMessage = await generateCommitMessageByDiff(
|
||||||
|
diff,
|
||||||
|
fullGitMojiSpec,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
commitSpinner.stop('📝 Commit message generated');
|
const messageTemplate = checkMessageTemplate(extraArgs);
|
||||||
|
if (
|
||||||
|
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,
|
||||||
|
commitMessage
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
commitGenerationSpinner.stop('📝 Commit message generated');
|
||||||
|
|
||||||
outro(
|
outro(
|
||||||
`Commit message:
|
`Generated commit message:
|
||||||
${chalk.grey('——————————————————')}
|
${chalk.grey('——————————————————')}
|
||||||
${commitMessage}
|
${commitMessage}
|
||||||
${chalk.grey('——————————————————')}`
|
${chalk.grey('——————————————————')}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const isCommitConfirmedByUser = await confirm({
|
const userAction = skipCommitConfirmation
|
||||||
message: 'Confirm the commit message?'
|
? 'Yes'
|
||||||
|
: await select({
|
||||||
|
message: 'Confirm the commit message?',
|
||||||
|
options: [
|
||||||
|
{ value: 'Yes', label: 'Yes' },
|
||||||
|
{ value: 'No', label: 'No' },
|
||||||
|
{ value: 'Edit', label: 'Edit' }
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
|
if (isCancel(userAction)) process.exit(1);
|
||||||
|
|
||||||
|
if (userAction === 'Edit') {
|
||||||
|
const textResponse = await text({
|
||||||
|
message: 'Please edit the commit message: (press Enter to continue)',
|
||||||
|
initialValue: commitMessage
|
||||||
|
});
|
||||||
|
|
||||||
|
commitMessage = textResponse.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userAction === 'Yes' || userAction === 'Edit') {
|
||||||
|
const committingChangesSpinner = spinner();
|
||||||
|
committingChangesSpinner.start('Committing the changes');
|
||||||
const { stdout } = await execa('git', [
|
const { stdout } = await execa('git', [
|
||||||
'commit',
|
'commit',
|
||||||
'-m',
|
'-m',
|
||||||
commitMessage,
|
commitMessage,
|
||||||
...extraArgs
|
...extraArgs
|
||||||
]);
|
]);
|
||||||
|
committingChangesSpinner.stop(
|
||||||
outro(`${chalk.green('✔')} Successfully committed`);
|
`${chalk.green('✔')} Successfully committed`
|
||||||
|
);
|
||||||
|
|
||||||
outro(stdout);
|
outro(stdout);
|
||||||
|
|
||||||
const remotes = await getGitRemotes();
|
const remotes = await getGitRemotes();
|
||||||
|
|
||||||
|
// user isn't pushing, return early
|
||||||
|
if (config.OCO_GITPUSH === false) return;
|
||||||
|
|
||||||
if (!remotes.length) {
|
if (!remotes.length) {
|
||||||
const { stdout } = await execa('git', ['push']);
|
const { stdout } = await execa('git', ['push']);
|
||||||
if (stdout) outro(stdout);
|
if (stdout) outro(stdout);
|
||||||
@@ -76,10 +139,12 @@ ${chalk.grey('——————————————————')}`
|
|||||||
message: 'Do you want to run `git push`?'
|
message: 'Do you want to run `git push`?'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPushConfirmedByUser && !isCancel(isPushConfirmedByUser)) {
|
if (isCancel(isPushConfirmedByUser)) process.exit(1);
|
||||||
|
|
||||||
|
if (isPushConfirmedByUser) {
|
||||||
const pushSpinner = spinner();
|
const pushSpinner = spinner();
|
||||||
|
|
||||||
pushSpinner.start(`Running \`git push ${remotes[0]}\``);
|
pushSpinner.start(`Running 'git push ${remotes[0]}'`);
|
||||||
|
|
||||||
const { stdout } = await execa('git', [
|
const { stdout } = await execa('git', [
|
||||||
'push',
|
'push',
|
||||||
@@ -99,30 +164,54 @@ ${chalk.grey('——————————————————')}`
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
const skipOption = `don't push`;
|
||||||
const selectedRemote = (await select({
|
const selectedRemote = (await select({
|
||||||
message: 'Choose a remote to push to',
|
message: 'Choose a remote to push to',
|
||||||
options: remotes.map((remote) => ({ value: remote, label: remote }))
|
options: [...remotes, skipOption].map((remote) => ({
|
||||||
|
value: remote,
|
||||||
|
label: remote
|
||||||
|
}))
|
||||||
})) as string;
|
})) as string;
|
||||||
|
|
||||||
if (!isCancel(selectedRemote)) {
|
if (isCancel(selectedRemote)) process.exit(1);
|
||||||
|
|
||||||
|
if (selectedRemote !== skipOption) {
|
||||||
const pushSpinner = spinner();
|
const pushSpinner = spinner();
|
||||||
|
|
||||||
pushSpinner.start(`Running \`git push ${selectedRemote}\``);
|
pushSpinner.start(`Running 'git push ${selectedRemote}'`);
|
||||||
|
|
||||||
const { stdout } = await execa('git', ['push', selectedRemote]);
|
const { stdout } = await execa('git', ['push', selectedRemote]);
|
||||||
|
|
||||||
|
if (stdout) outro(stdout);
|
||||||
|
|
||||||
pushSpinner.stop(
|
pushSpinner.stop(
|
||||||
`${chalk.green(
|
`${chalk.green(
|
||||||
'✔'
|
'✔'
|
||||||
)} Successfully pushed all commits to ${selectedRemote}`
|
)} successfully pushed all commits to ${selectedRemote}`
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const regenerateMessage = await confirm({
|
||||||
|
message: 'Do you want to regenerate the message?'
|
||||||
|
});
|
||||||
|
|
||||||
if (stdout) outro(stdout);
|
if (isCancel(regenerateMessage)) process.exit(1);
|
||||||
} else outro(`${chalk.gray('✖')} process cancelled`);
|
|
||||||
|
if (regenerateMessage) {
|
||||||
|
await generateCommitMessageFromGitDiff({
|
||||||
|
diff,
|
||||||
|
extraArgs,
|
||||||
|
fullGitMojiSpec
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
commitSpinner.stop('📝 Commit message generated');
|
commitGenerationSpinner.stop(
|
||||||
|
`${chalk.red('✖')} Failed to generate the commit message`
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
||||||
@@ -132,14 +221,17 @@ ${chalk.grey('——————————————————')}`
|
|||||||
|
|
||||||
export async function commit(
|
export async function commit(
|
||||||
extraArgs: string[] = [],
|
extraArgs: string[] = [],
|
||||||
isStageAllFlag: Boolean = false
|
context: string = '',
|
||||||
|
isStageAllFlag: Boolean = false,
|
||||||
|
fullGitMojiSpec: boolean = false,
|
||||||
|
skipCommitConfirmation: boolean = false
|
||||||
) {
|
) {
|
||||||
if (isStageAllFlag) {
|
if (isStageAllFlag) {
|
||||||
const changedFiles = await getChangedFiles();
|
const changedFiles = await getChangedFiles();
|
||||||
|
|
||||||
if (changedFiles) await gitAdd({ files: changedFiles });
|
if (changedFiles) await gitAdd({ files: changedFiles });
|
||||||
else {
|
else {
|
||||||
outro('No changes detected, write some code and run `oc` again');
|
outro('No changes detected, write some code and run `oco` again');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,18 +254,18 @@ export async function commit(
|
|||||||
|
|
||||||
stagedFilesSpinner.start('Counting staged files');
|
stagedFilesSpinner.start('Counting staged files');
|
||||||
|
|
||||||
if (!stagedFiles.length) {
|
if (stagedFiles.length === 0) {
|
||||||
stagedFilesSpinner.stop('No files are staged');
|
stagedFilesSpinner.stop('No files are staged');
|
||||||
|
|
||||||
const isStageAllAndCommitConfirmedByUser = await confirm({
|
const isStageAllAndCommitConfirmedByUser = await confirm({
|
||||||
message: 'Do you want to stage all files and generate commit message?'
|
message: 'Do you want to stage all files and generate commit message?'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (isCancel(isStageAllAndCommitConfirmedByUser)) process.exit(1);
|
||||||
isStageAllAndCommitConfirmedByUser &&
|
|
||||||
!isCancel(isStageAllAndCommitConfirmedByUser)
|
if (isStageAllAndCommitConfirmedByUser) {
|
||||||
) {
|
await commit(extraArgs, context, true, fullGitMojiSpec);
|
||||||
await commit(extraArgs, true);
|
process.exit(0);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stagedFiles.length === 0 && changedFiles.length > 0) {
|
if (stagedFiles.length === 0 && changedFiles.length > 0) {
|
||||||
@@ -185,13 +277,13 @@ export async function commit(
|
|||||||
}))
|
}))
|
||||||
})) as string[];
|
})) as string[];
|
||||||
|
|
||||||
if (isCancel(files)) process.exit(1);
|
if (isCancel(files)) process.exit(0);
|
||||||
|
|
||||||
await gitAdd({ files });
|
await gitAdd({ files });
|
||||||
}
|
}
|
||||||
|
|
||||||
await commit(extraArgs, false);
|
await commit(extraArgs, context, false, fullGitMojiSpec);
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
stagedFilesSpinner.stop(
|
stagedFilesSpinner.stop(
|
||||||
@@ -201,10 +293,13 @@ export async function commit(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [, generateCommitError] = await trytm(
|
const [, generateCommitError] = await trytm(
|
||||||
generateCommitMessageFromGitDiff(
|
generateCommitMessageFromGitDiff({
|
||||||
await getDiff({ files: stagedFiles }),
|
diff: await getDiff({ files: stagedFiles }),
|
||||||
extraArgs
|
extraArgs,
|
||||||
)
|
context,
|
||||||
|
fullGitMojiSpec,
|
||||||
|
skipCommitConfirmation
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (generateCommitError) {
|
if (generateCommitError) {
|
||||||
|
|||||||
44
src/commands/commitlint.ts
Normal file
44
src/commands/commitlint.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { intro, outro } from '@clack/prompts';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { command } from 'cleye';
|
||||||
|
import { configureCommitlintIntegration } from '../modules/commitlint/config';
|
||||||
|
import { getCommitlintLLMConfig } from '../modules/commitlint/utils';
|
||||||
|
import { COMMANDS } from './ENUMS';
|
||||||
|
|
||||||
|
export enum CONFIG_MODES {
|
||||||
|
get = 'get',
|
||||||
|
force = 'force'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const commitlintConfigCommand = command(
|
||||||
|
{
|
||||||
|
name: COMMANDS.commitlint,
|
||||||
|
parameters: ['<mode>']
|
||||||
|
},
|
||||||
|
async (argv) => {
|
||||||
|
intro('opencommit — configure @commitlint');
|
||||||
|
try {
|
||||||
|
const { mode } = argv._;
|
||||||
|
|
||||||
|
if (mode === CONFIG_MODES.get) {
|
||||||
|
const commitLintConfig = await getCommitlintLLMConfig();
|
||||||
|
|
||||||
|
outro(JSON.stringify(commitLintConfig, null, 2));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === CONFIG_MODES.force) {
|
||||||
|
await configureCommitlintIntegration(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported mode: ${mode}. Valid modes are: "force" and "get"`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
outro(`${chalk.red('✖')} ${error}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +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 fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { command } from 'cleye';
|
|
||||||
import { assertGitRepo, getCoreHooksPath } from '../utils/git.js';
|
import { assertGitRepo, getCoreHooksPath } from '../utils/git.js';
|
||||||
import { existsSync } from 'fs';
|
import { COMMANDS } from './ENUMS';
|
||||||
import chalk from 'chalk';
|
|
||||||
import { intro, outro } from '@clack/prompts';
|
|
||||||
import { COMMANDS } from '../CommandsEnum.js';
|
|
||||||
|
|
||||||
const HOOK_NAME = 'prepare-commit-msg';
|
const HOOK_NAME = 'prepare-commit-msg';
|
||||||
const DEFAULT_SYMLINK_URL = path.join('.git', 'hooks', HOOK_NAME);
|
const DEFAULT_SYMLINK_URL = path.join('.git', 'hooks', HOOK_NAME);
|
||||||
@@ -92,7 +92,7 @@ export const hookCommand = command(
|
|||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
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) {
|
} catch (error) {
|
||||||
outro(`${chalk.red('✖')} ${error}`);
|
outro(`${chalk.red('✖')} ${error}`);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import fs from 'fs/promises';
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
|
||||||
import { intro, outro, spinner } from '@clack/prompts';
|
import { intro, outro, spinner } from '@clack/prompts';
|
||||||
|
|
||||||
|
import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
|
||||||
import { getChangedFiles, getDiff, getStagedFiles, gitAdd } from '../utils/git';
|
import { getChangedFiles, getDiff, getStagedFiles, gitAdd } from '../utils/git';
|
||||||
import { getConfig } from './config';
|
import { getConfig } from './config';
|
||||||
import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
|
|
||||||
|
|
||||||
const [messageFilePath, commitSource] = process.argv.slice(2);
|
const [messageFilePath, commitSource] = process.argv.slice(2);
|
||||||
|
|
||||||
@@ -24,7 +26,7 @@ export const prepareCommitMessageHook = async (
|
|||||||
|
|
||||||
if (changedFiles) await gitAdd({ files: changedFiles });
|
if (changedFiles) await gitAdd({ files: changedFiles });
|
||||||
else {
|
else {
|
||||||
outro('No changes detected, write some code and run `oc` again');
|
outro('No changes detected, write some code and run `oco` again');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,10 +39,11 @@ export const prepareCommitMessageHook = async (
|
|||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
if (!config?.OCO_OPENAI_API_KEY) {
|
if (!config.OCO_API_KEY) {
|
||||||
throw new Error(
|
outro(
|
||||||
'No OPEN_AI_API exists. Set your OPEN_AI_API=<key> in ~/.opencommit'
|
'No OCO_API_KEY is set. Set your key via `oco config set OCO_API_KEY=<value>. For more info see https://github.com/di-sukharev/opencommit'
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const spin = spinner();
|
const spin = spinner();
|
||||||
@@ -53,10 +56,14 @@ export const prepareCommitMessageHook = async (
|
|||||||
|
|
||||||
const fileContent = await fs.readFile(messageFilePath);
|
const fileContent = await fs.readFile(messageFilePath);
|
||||||
|
|
||||||
await fs.writeFile(
|
const messageWithComment = `# ${commitMessage}\n\n# ---------- [OpenCommit] ---------- #\n# Remove the # above to use this generated commit message.\n# To cancel the commit, just close this window without making any changes.\n\n${fileContent.toString()}`;
|
||||||
messageFilePath,
|
const messageWithoutComment = `${commitMessage}\n\n${fileContent.toString()}`;
|
||||||
commitMessage + '\n' + fileContent.toString()
|
|
||||||
);
|
const message = config.OCO_HOOK_AUTO_UNCOMMENT
|
||||||
|
? messageWithoutComment
|
||||||
|
: messageWithComment;
|
||||||
|
|
||||||
|
await fs.writeFile(messageFilePath, message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
outro(`${chalk.red('✖')} ${error}`);
|
outro(`${chalk.red('✖')} ${error}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
31
src/engine/Engine.ts
Normal file
31
src/engine/Engine.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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';
|
||||||
|
import { Mistral as MistralClient } from '@mistralai/mistralai';
|
||||||
|
|
||||||
|
export interface AiEngineConfig {
|
||||||
|
apiKey: string;
|
||||||
|
model: string;
|
||||||
|
maxTokensOutput: number;
|
||||||
|
maxTokensInput: number;
|
||||||
|
baseURL?: string;
|
||||||
|
customHeaders?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client =
|
||||||
|
| OpenAIClient
|
||||||
|
| AzureOpenAIClient
|
||||||
|
| AnthropicClient
|
||||||
|
| RawAxiosClient
|
||||||
|
| GeminiClient
|
||||||
|
| MistralClient;
|
||||||
|
|
||||||
|
export interface AiEngine {
|
||||||
|
config: AiEngineConfig;
|
||||||
|
client: Client;
|
||||||
|
generateCommitMessage(
|
||||||
|
messages: Array<OpenAIClient.Chat.Completions.ChatCompletionMessageParam>
|
||||||
|
): Promise<string | null | undefined>;
|
||||||
|
}
|
||||||
47
src/engine/aimlapi.ts
Normal file
47
src/engine/aimlapi.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import OpenAI from 'openai';
|
||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
|
interface AimlApiConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
|
export class AimlApiEngine implements AiEngine {
|
||||||
|
client: AxiosInstance;
|
||||||
|
|
||||||
|
constructor(public config: AimlApiConfig) {
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: config.baseURL || 'https://api.aimlapi.com/v1/chat/completions',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${config.apiKey}`,
|
||||||
|
'HTTP-Referer': 'https://github.com/di-sukharev/opencommit',
|
||||||
|
'X-Title': 'opencommit',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...config.customHeaders
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public generateCommitMessage = async (
|
||||||
|
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||||
|
): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
const response = await this.client.post('', {
|
||||||
|
model: this.config.model,
|
||||||
|
messages
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = response.data.choices?.[0]?.message;
|
||||||
|
return message?.content ?? null;
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
if (
|
||||||
|
axios.isAxiosError<{ error?: { message: string } }>(error) &&
|
||||||
|
error.response?.status === 401
|
||||||
|
) {
|
||||||
|
const apiError = error.response.data.error;
|
||||||
|
if (apiError) throw new Error(apiError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
79
src/engine/anthropic.ts
Normal file
79
src/engine/anthropic.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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 { removeContentTags } from '../utils/removeContentTags';
|
||||||
|
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;
|
||||||
|
let content = message;
|
||||||
|
return removeContentTags(content, 'think');
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
80
src/engine/azure.ts
Normal file
80
src/engine/azure.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
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 { removeContentTags } from '../utils/removeContentTags';
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = message?.content;
|
||||||
|
return removeContentTags(content, 'think');
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
61
src/engine/deepseek.ts
Normal file
61
src/engine/deepseek.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { OpenAI } from 'openai';
|
||||||
|
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
|
import { tokenCount } from '../utils/tokenCount';
|
||||||
|
import { OpenAiEngine, OpenAiConfig } from './openAi';
|
||||||
|
|
||||||
|
export interface DeepseekConfig extends OpenAiConfig {}
|
||||||
|
|
||||||
|
export class DeepseekEngine extends OpenAiEngine {
|
||||||
|
constructor(config: DeepseekConfig) {
|
||||||
|
// Call OpenAIEngine constructor with forced Deepseek baseURL
|
||||||
|
super({
|
||||||
|
...config,
|
||||||
|
baseURL: 'https://api.deepseek.com/v1'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identical method from OpenAiEngine, re-implemented here
|
||||||
|
public generateCommitMessage = async (
|
||||||
|
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||||
|
): Promise<string | null> => {
|
||||||
|
const params = {
|
||||||
|
model: this.config.model,
|
||||||
|
messages,
|
||||||
|
temperature: 0,
|
||||||
|
top_p: 0.1,
|
||||||
|
max_tokens: this.config.maxTokensOutput
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const REQUEST_TOKENS = messages
|
||||||
|
.map((msg) => tokenCount(msg.content as string) + 4)
|
||||||
|
.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
if (
|
||||||
|
REQUEST_TOKENS >
|
||||||
|
this.config.maxTokensInput - this.config.maxTokensOutput
|
||||||
|
)
|
||||||
|
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
||||||
|
|
||||||
|
const completion = await this.client.chat.completions.create(params);
|
||||||
|
|
||||||
|
const message = completion.choices[0].message;
|
||||||
|
let content = message?.content;
|
||||||
|
return removeContentTags(content, 'think');
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
if (
|
||||||
|
axios.isAxiosError<{ error?: { message: string } }>(error) &&
|
||||||
|
error.response?.status === 401
|
||||||
|
) {
|
||||||
|
const openAiError = error.response.data.error;
|
||||||
|
|
||||||
|
if (openAiError) throw new Error(openAiError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
47
src/engine/flowise.ts
Normal file
47
src/engine/flowise.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import { OpenAI } from 'openai';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
|
interface FlowiseAiConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
|
export class FlowiseEngine implements AiEngine {
|
||||||
|
config: FlowiseAiConfig;
|
||||||
|
client: AxiosInstance;
|
||||||
|
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.client = axios.create({
|
||||||
|
url: `${config.baseURL}/${config.apiKey}`,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateCommitMessage(
|
||||||
|
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const gitDiff = (messages[messages.length - 1]?.content as string)
|
||||||
|
.replace(/\\/g, '\\\\')
|
||||||
|
.replace(/"/g, '\\"')
|
||||||
|
.replace(/\n/g, '\\n')
|
||||||
|
.replace(/\r/g, '\\r')
|
||||||
|
.replace(/\t/g, '\\t');
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
question: gitDiff,
|
||||||
|
overrideConfig: {
|
||||||
|
systemMessagePrompt: messages[0]?.content
|
||||||
|
},
|
||||||
|
history: messages.slice(1, -1)
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const response = await this.client.post('', payload);
|
||||||
|
const message = response.data;
|
||||||
|
let content = message?.text;
|
||||||
|
return removeContentTags(content, 'think');
|
||||||
|
} catch (err: any) {
|
||||||
|
const message = err.response?.data?.error ?? err.message;
|
||||||
|
throw new Error('local model issues. details: ' + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/engine/gemini.ts
Normal file
90
src/engine/gemini.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
Content,
|
||||||
|
GoogleGenerativeAI,
|
||||||
|
HarmBlockThreshold,
|
||||||
|
HarmCategory,
|
||||||
|
Part
|
||||||
|
} from '@google/generative-ai';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { OpenAI } from 'openai';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
|
interface GeminiConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
|
export class GeminiEngine implements AiEngine {
|
||||||
|
config: GeminiConfig;
|
||||||
|
client: GoogleGenerativeAI;
|
||||||
|
|
||||||
|
constructor(config) {
|
||||||
|
this.client = new GoogleGenerativeAI(config.apiKey);
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateCommitMessage(
|
||||||
|
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const systemInstruction = messages
|
||||||
|
.filter((m) => m.role === 'system')
|
||||||
|
.map((m) => m.content)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const gemini = this.client.getGenerativeModel({
|
||||||
|
model: this.config.model,
|
||||||
|
systemInstruction
|
||||||
|
});
|
||||||
|
|
||||||
|
const contents = messages
|
||||||
|
.filter((m) => m.role !== 'system')
|
||||||
|
.map(
|
||||||
|
(m) =>
|
||||||
|
({
|
||||||
|
parts: [{ text: m.content } as Part],
|
||||||
|
role: m.role === 'user' ? m.role : 'model'
|
||||||
|
} as Content)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await gemini.generateContent({
|
||||||
|
contents,
|
||||||
|
safetySettings: [
|
||||||
|
{
|
||||||
|
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||||
|
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
|
||||||
|
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
||||||
|
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
||||||
|
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||||
|
}
|
||||||
|
],
|
||||||
|
generationConfig: {
|
||||||
|
maxOutputTokens: this.config.maxTokensOutput,
|
||||||
|
temperature: 0,
|
||||||
|
topP: 0.1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = result.response.text();
|
||||||
|
return removeContentTags(content, 'think');
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/engine/groq.ts
Normal file
10
src/engine/groq.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { OpenAiConfig, OpenAiEngine } from './openAi';
|
||||||
|
|
||||||
|
interface GroqConfig extends OpenAiConfig {}
|
||||||
|
|
||||||
|
export class GroqEngine extends OpenAiEngine {
|
||||||
|
constructor(config: GroqConfig) {
|
||||||
|
config.baseURL = 'https://api.groq.com/openai/v1';
|
||||||
|
super(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/engine/mistral.ts
Normal file
79
src/engine/mistral.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { OpenAI } from 'openai';
|
||||||
|
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
|
import { tokenCount } from '../utils/tokenCount';
|
||||||
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
|
// Using any for Mistral types to avoid type declaration issues
|
||||||
|
export interface MistralAiConfig extends AiEngineConfig {}
|
||||||
|
export type MistralCompletionMessageParam = Array<any>;
|
||||||
|
|
||||||
|
// Import Mistral dynamically to avoid TS errors
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const Mistral = require('@mistralai/mistralai').Mistral;
|
||||||
|
|
||||||
|
export class MistralAiEngine implements AiEngine {
|
||||||
|
config: MistralAiConfig;
|
||||||
|
client: any; // Using any type for Mistral client to avoid TS errors
|
||||||
|
|
||||||
|
constructor(config: MistralAiConfig) {
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
if (!config.baseURL) {
|
||||||
|
this.client = new Mistral({ apiKey: config.apiKey });
|
||||||
|
} else {
|
||||||
|
this.client = new Mistral({
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
serverURL: config.baseURL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public generateCommitMessage = async (
|
||||||
|
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||||
|
): Promise<string | null> => {
|
||||||
|
const params = {
|
||||||
|
model: this.config.model,
|
||||||
|
messages: messages as MistralCompletionMessageParam,
|
||||||
|
topP: 0.1,
|
||||||
|
maxTokens: this.config.maxTokensOutput
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const REQUEST_TOKENS = messages
|
||||||
|
.map((msg) => tokenCount(msg.content as string) + 4)
|
||||||
|
.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
if (
|
||||||
|
REQUEST_TOKENS >
|
||||||
|
this.config.maxTokensInput - this.config.maxTokensOutput
|
||||||
|
)
|
||||||
|
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
||||||
|
|
||||||
|
const completion = await this.client.chat.complete(params);
|
||||||
|
|
||||||
|
if (!completion.choices) throw Error('No completion choice available.');
|
||||||
|
|
||||||
|
const message = completion.choices[0].message;
|
||||||
|
|
||||||
|
if (!message || !message.content)
|
||||||
|
throw Error('No completion choice available.');
|
||||||
|
|
||||||
|
let content = message.content as string;
|
||||||
|
return removeContentTags(content, 'think');
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
if (
|
||||||
|
axios.isAxiosError<{ error?: { message: string } }>(error) &&
|
||||||
|
error.response?.status === 401
|
||||||
|
) {
|
||||||
|
const mistralError = error.response.data.error;
|
||||||
|
|
||||||
|
if (mistralError) throw new Error(mistralError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
47
src/engine/mlx.ts
Normal file
47
src/engine/mlx.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import { OpenAI } from 'openai';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
|
interface MLXConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
|
export class MLXEngine implements AiEngine {
|
||||||
|
config: MLXConfig;
|
||||||
|
client: AxiosInstance;
|
||||||
|
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.client = axios.create({
|
||||||
|
url: config.baseURL
|
||||||
|
? `${config.baseURL}/${config.apiKey}`
|
||||||
|
: 'http://localhost:8080/v1/chat/completions',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateCommitMessage(
|
||||||
|
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const params = {
|
||||||
|
messages,
|
||||||
|
temperature: 0,
|
||||||
|
top_p: 0.1,
|
||||||
|
repetition_penalty: 1.5,
|
||||||
|
stream: false
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const response = await this.client.post(
|
||||||
|
this.client.getUri(this.config),
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
const choices = response.data.choices;
|
||||||
|
const message = choices[0].message;
|
||||||
|
let content = message?.content;
|
||||||
|
return removeContentTags(content, 'think');
|
||||||
|
} catch (err: any) {
|
||||||
|
const message = err.response?.data?.error ?? err.message;
|
||||||
|
throw new Error(`MLX provider error: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/engine/ollama.ts
Normal file
52
src/engine/ollama.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import { OpenAI } from 'openai';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
|
interface OllamaConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
|
export class OllamaEngine implements AiEngine {
|
||||||
|
config: OllamaConfig;
|
||||||
|
client: AxiosInstance;
|
||||||
|
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
// Combine base headers with custom headers
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...config.customHeaders
|
||||||
|
};
|
||||||
|
|
||||||
|
this.client = axios.create({
|
||||||
|
url: config.baseURL
|
||||||
|
? `${config.baseURL}/${config.apiKey}`
|
||||||
|
: 'http://localhost:11434/api/chat',
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
let content = message?.content;
|
||||||
|
return removeContentTags(content, 'think');
|
||||||
|
} catch (err: any) {
|
||||||
|
const message = err.response?.data?.error ?? err.message;
|
||||||
|
throw new Error(`Ollama provider error: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/engine/openAi.ts
Normal file
77
src/engine/openAi.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { OpenAI } from 'openai';
|
||||||
|
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||||
|
import { parseCustomHeaders } from '../utils/engine';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
|
import { tokenCount } from '../utils/tokenCount';
|
||||||
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
|
export interface OpenAiConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
|
export class OpenAiEngine implements AiEngine {
|
||||||
|
config: OpenAiConfig;
|
||||||
|
client: OpenAI;
|
||||||
|
|
||||||
|
constructor(config: OpenAiConfig) {
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
const clientOptions: OpenAI.ClientOptions = {
|
||||||
|
apiKey: config.apiKey
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.baseURL) {
|
||||||
|
clientOptions.baseURL = config.baseURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.customHeaders) {
|
||||||
|
const headers = parseCustomHeaders(config.customHeaders);
|
||||||
|
if (Object.keys(headers).length > 0) {
|
||||||
|
clientOptions.defaultHeaders = headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client = new OpenAI(clientOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public generateCommitMessage = async (
|
||||||
|
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||||
|
): Promise<string | null> => {
|
||||||
|
const params = {
|
||||||
|
model: this.config.model,
|
||||||
|
messages,
|
||||||
|
temperature: 0,
|
||||||
|
top_p: 0.1,
|
||||||
|
max_tokens: this.config.maxTokensOutput
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const REQUEST_TOKENS = messages
|
||||||
|
.map((msg) => tokenCount(msg.content as string) + 4)
|
||||||
|
.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
if (
|
||||||
|
REQUEST_TOKENS >
|
||||||
|
this.config.maxTokensInput - this.config.maxTokensOutput
|
||||||
|
)
|
||||||
|
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
||||||
|
|
||||||
|
const completion = await this.client.chat.completions.create(params);
|
||||||
|
|
||||||
|
const message = completion.choices[0].message;
|
||||||
|
let content = message?.content;
|
||||||
|
return removeContentTags(content, 'think');
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
if (
|
||||||
|
axios.isAxiosError<{ error?: { message: string } }>(error) &&
|
||||||
|
error.response?.status === 401
|
||||||
|
) {
|
||||||
|
const openAiError = error.response.data.error;
|
||||||
|
|
||||||
|
if (openAiError) throw new Error(openAiError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
49
src/engine/openrouter.ts
Normal file
49
src/engine/openrouter.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import OpenAI from 'openai';
|
||||||
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
|
|
||||||
|
interface OpenRouterConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
|
export class OpenRouterEngine implements AiEngine {
|
||||||
|
client: AxiosInstance;
|
||||||
|
|
||||||
|
constructor(public config: OpenRouterConfig) {
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: 'https://openrouter.ai/api/v1/chat/completions',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${config.apiKey}`,
|
||||||
|
'HTTP-Referer': 'https://github.com/di-sukharev/opencommit',
|
||||||
|
'X-Title': 'OpenCommit',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public generateCommitMessage = async (
|
||||||
|
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||||
|
): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
const response = await this.client.post('', {
|
||||||
|
model: this.config.model,
|
||||||
|
messages
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = response.data.choices[0].message;
|
||||||
|
let content = message?.content;
|
||||||
|
return removeContentTags(content, 'think');
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
if (
|
||||||
|
axios.isAxiosError<{ error?: { message: string } }>(error) &&
|
||||||
|
error.response?.status === 401
|
||||||
|
) {
|
||||||
|
const openRouterError = error.response.data.error;
|
||||||
|
|
||||||
|
if (openRouterError) throw new Error(openRouterError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
47
src/engine/testAi.ts
Normal file
47
src/engine/testAi.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,67 +1,28 @@
|
|||||||
import {
|
import { OpenAI } from 'openai';
|
||||||
ChatCompletionRequestMessage,
|
import { DEFAULT_TOKEN_LIMITS, getConfig } from './commands/config';
|
||||||
ChatCompletionRequestMessageRoleEnum
|
import { getMainCommitPrompt } from './prompts';
|
||||||
} from 'openai';
|
import { getEngine } from './utils/engine';
|
||||||
import { api } from './api';
|
|
||||||
import { getConfig } from './commands/config';
|
|
||||||
import { mergeDiffs } from './utils/mergeDiffs';
|
import { mergeDiffs } from './utils/mergeDiffs';
|
||||||
import { i18n, I18nLocals } from './i18n';
|
|
||||||
import { tokenCount } from './utils/tokenCount';
|
import { tokenCount } from './utils/tokenCount';
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
|
const MAX_TOKENS_INPUT = config.OCO_TOKENS_MAX_INPUT;
|
||||||
|
const MAX_TOKENS_OUTPUT = config.OCO_TOKENS_MAX_OUTPUT;
|
||||||
|
|
||||||
const INIT_MESSAGES_PROMPT: Array<ChatCompletionRequestMessage> = [
|
const generateCommitMessageChatCompletionPrompt = async (
|
||||||
{
|
diff: string,
|
||||||
role: ChatCompletionRequestMessageRoleEnum.System,
|
fullGitMojiSpec: boolean,
|
||||||
// prettier-ignore
|
context: string
|
||||||
content: `You are to act as the author of a commit message in git. Your mission is to create clean and comprehensive commit messages in the conventional commit 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.
|
): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => {
|
||||||
${config?.OCO_EMOJI ? 'Use GitMoji convention to preface the commit.': 'Do not preface the commit with anything.'}
|
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(
|
||||||
${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."}
|
fullGitMojiSpec,
|
||||||
Use the present tense. Lines must not be longer than 74 characters. Use ${translation.localLanguage} to answer.`
|
context
|
||||||
},
|
);
|
||||||
{
|
|
||||||
role: ChatCompletionRequestMessageRoleEnum.User,
|
|
||||||
content: `diff --git a/src/server.ts b/src/server.ts
|
|
||||||
index ad4db42..f3b18a9 100644
|
|
||||||
--- a/src/server.ts
|
|
||||||
+++ b/src/server.ts
|
|
||||||
@@ -10,7 +10,7 @@
|
|
||||||
import {
|
|
||||||
initWinstonLogger();
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
-const port = 7799;
|
|
||||||
+const PORT = 7799;
|
|
||||||
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
@@ -34,6 +34,6 @@
|
|
||||||
app.use((_, res, next) => {
|
|
||||||
// ROUTES
|
|
||||||
app.use(PROTECTED_ROUTER_URL, protectedRouter);
|
|
||||||
|
|
||||||
-app.listen(port, () => {
|
|
||||||
- console.log(\`Server listening on port \${port}\`);
|
|
||||||
+app.listen(process.env.PORT || PORT, () => {
|
|
||||||
+ console.log(\`Server listening on port \${PORT}\`);
|
|
||||||
});`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: ChatCompletionRequestMessageRoleEnum.Assistant,
|
|
||||||
content: `${config?.OCO_EMOJI ? '🐛 ' : ''}${translation.commitFix}
|
|
||||||
${config?.OCO_EMOJI ? '✨ ' : ''}${translation.commitFeat}
|
|
||||||
${config?.OCO_DESCRIPTION ? translation.commitDescription : ''}`
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const generateCommitMessageChatCompletionPrompt = (
|
|
||||||
diff: string
|
|
||||||
): Array<ChatCompletionRequestMessage> => {
|
|
||||||
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
|
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
|
||||||
|
|
||||||
chatContextAsCompletionRequest.push({
|
chatContextAsCompletionRequest.push({
|
||||||
role: ChatCompletionRequestMessageRoleEnum.User,
|
role: 'user',
|
||||||
content: diff
|
content: diff
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,42 +32,62 @@ const generateCommitMessageChatCompletionPrompt = (
|
|||||||
export enum GenerateCommitMessageErrorEnum {
|
export enum GenerateCommitMessageErrorEnum {
|
||||||
tooMuchTokens = 'TOO_MUCH_TOKENS',
|
tooMuchTokens = 'TOO_MUCH_TOKENS',
|
||||||
internalError = 'INTERNAL_ERROR',
|
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.`
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GenerateCommitMessageError {
|
const ADJUSTMENT_FACTOR = 20;
|
||||||
error: GenerateCommitMessageErrorEnum;
|
|
||||||
}
|
|
||||||
|
|
||||||
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
|
|
||||||
(msg) => tokenCount(msg.content) + 4
|
|
||||||
).reduce((a, b) => a + b, 0);
|
|
||||||
|
|
||||||
const MAX_REQ_TOKENS = 3000 - INIT_MESSAGES_PROMPT_LENGTH;
|
|
||||||
|
|
||||||
export const generateCommitMessageByDiff = async (
|
export const generateCommitMessageByDiff = async (
|
||||||
diff: string
|
diff: string,
|
||||||
|
fullGitMojiSpec: boolean = false,
|
||||||
|
context: string = ''
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
if (tokenCount(diff) >= MAX_REQ_TOKENS) {
|
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(
|
||||||
const commitMessagePromises = getCommitMsgsPromisesFromFileDiffs(
|
fullGitMojiSpec,
|
||||||
diff,
|
context
|
||||||
MAX_REQ_TOKENS
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const commitMessages = await Promise.all(commitMessagePromises);
|
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
|
||||||
|
(msg) => tokenCount(msg.content as string) + 4
|
||||||
|
).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
const MAX_REQUEST_TOKENS =
|
||||||
|
MAX_TOKENS_INPUT -
|
||||||
|
ADJUSTMENT_FACTOR -
|
||||||
|
INIT_MESSAGES_PROMPT_LENGTH -
|
||||||
|
MAX_TOKENS_OUTPUT;
|
||||||
|
|
||||||
|
if (tokenCount(diff) >= MAX_REQUEST_TOKENS) {
|
||||||
|
const commitMessagePromises = await getCommitMsgsPromisesFromFileDiffs(
|
||||||
|
diff,
|
||||||
|
MAX_REQUEST_TOKENS,
|
||||||
|
fullGitMojiSpec
|
||||||
|
);
|
||||||
|
|
||||||
|
const commitMessages = [] as string[];
|
||||||
|
for (const promise of commitMessagePromises) {
|
||||||
|
commitMessages.push((await promise) as string);
|
||||||
|
await delay(2000);
|
||||||
|
}
|
||||||
|
|
||||||
return commitMessages.join('\n\n');
|
return commitMessages.join('\n\n');
|
||||||
} else {
|
}
|
||||||
const messages = generateCommitMessageChatCompletionPrompt(diff);
|
|
||||||
|
|
||||||
const commitMessage = await api.generateCommitMessage(messages);
|
const messages = await generateCommitMessageChatCompletionPrompt(
|
||||||
|
diff,
|
||||||
|
fullGitMojiSpec,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
const engine = getEngine();
|
||||||
|
const commitMessage = await engine.generateCommitMessage(messages);
|
||||||
|
|
||||||
if (!commitMessage)
|
if (!commitMessage)
|
||||||
throw new Error(GenerateCommitMessageErrorEnum.emptyMessage);
|
throw new Error(GenerateCommitMessageErrorEnum.emptyMessage);
|
||||||
|
|
||||||
return commitMessage;
|
return commitMessage;
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -115,7 +96,8 @@ export const generateCommitMessageByDiff = async (
|
|||||||
function getMessagesPromisesByChangesInFile(
|
function getMessagesPromisesByChangesInFile(
|
||||||
fileDiff: string,
|
fileDiff: string,
|
||||||
separator: string,
|
separator: string,
|
||||||
maxChangeLength: number
|
maxChangeLength: number,
|
||||||
|
fullGitMojiSpec: boolean
|
||||||
) {
|
) {
|
||||||
const hunkHeaderSeparator = '@@ ';
|
const hunkHeaderSeparator = '@@ ';
|
||||||
const [fileHeader, ...fileDiffByLines] = fileDiff.split(hunkHeaderSeparator);
|
const [fileHeader, ...fileDiffByLines] = fileDiff.split(hunkHeaderSeparator);
|
||||||
@@ -126,25 +108,74 @@ function getMessagesPromisesByChangesInFile(
|
|||||||
maxChangeLength
|
maxChangeLength
|
||||||
);
|
);
|
||||||
|
|
||||||
const lineDiffsWithHeader = mergedChanges.map(
|
const lineDiffsWithHeader = [] as string[];
|
||||||
(change) => fileHeader + change
|
for (const change of mergedChanges) {
|
||||||
|
const totalChange = fileHeader + change;
|
||||||
|
if (tokenCount(totalChange) > maxChangeLength) {
|
||||||
|
// If the totalChange is too large, split it into smaller pieces
|
||||||
|
const splitChanges = splitDiff(totalChange, maxChangeLength);
|
||||||
|
lineDiffsWithHeader.push(...splitChanges);
|
||||||
|
} else {
|
||||||
|
lineDiffsWithHeader.push(totalChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const engine = getEngine();
|
||||||
|
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map(
|
||||||
|
async (lineDiff) => {
|
||||||
|
const messages = await generateCommitMessageChatCompletionPrompt(
|
||||||
|
separator + lineDiff,
|
||||||
|
fullGitMojiSpec
|
||||||
);
|
);
|
||||||
|
|
||||||
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map((lineDiff) => {
|
return engine.generateCommitMessage(messages);
|
||||||
const messages = generateCommitMessageChatCompletionPrompt(
|
}
|
||||||
separator + lineDiff
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return api.generateCommitMessage(messages);
|
|
||||||
});
|
|
||||||
|
|
||||||
return commitMsgsFromFileLineDiffs;
|
return commitMsgsFromFileLineDiffs;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCommitMsgsPromisesFromFileDiffs(
|
function splitDiff(diff: string, maxChangeLength: number) {
|
||||||
|
const lines = diff.split('\n');
|
||||||
|
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) {
|
||||||
|
const subLine = line.substring(0, maxChangeLength);
|
||||||
|
line = line.substring(maxChangeLength);
|
||||||
|
splitDiffs.push(subLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the tokenCount of the currentDiff and the line separately
|
||||||
|
if (tokenCount(currentDiff) + tokenCount('\n' + line) > maxChangeLength) {
|
||||||
|
// If adding the next line would exceed the maxChangeLength, start a new diff
|
||||||
|
splitDiffs.push(currentDiff);
|
||||||
|
currentDiff = line;
|
||||||
|
} else {
|
||||||
|
// Otherwise, add the line to the current diff
|
||||||
|
currentDiff += '\n' + line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the last diff
|
||||||
|
if (currentDiff) {
|
||||||
|
splitDiffs.push(currentDiff);
|
||||||
|
}
|
||||||
|
|
||||||
|
return splitDiffs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCommitMsgsPromisesFromFileDiffs = async (
|
||||||
diff: string,
|
diff: string,
|
||||||
maxDiffLength: number
|
maxDiffLength: number,
|
||||||
) {
|
fullGitMojiSpec: boolean
|
||||||
|
) => {
|
||||||
const separator = 'diff --git ';
|
const separator = 'diff --git ';
|
||||||
|
|
||||||
const diffByFiles = diff.split(separator).slice(1);
|
const diffByFiles = diff.split(separator).slice(1);
|
||||||
@@ -152,7 +183,7 @@ export function getCommitMsgsPromisesFromFileDiffs(
|
|||||||
// merge multiple files-diffs into 1 prompt to save tokens
|
// merge multiple files-diffs into 1 prompt to save tokens
|
||||||
const mergedFilesDiffs = mergeDiffs(diffByFiles, maxDiffLength);
|
const mergedFilesDiffs = mergeDiffs(diffByFiles, maxDiffLength);
|
||||||
|
|
||||||
const commitMessagePromises = [];
|
const commitMessagePromises = [] as Promise<string | null | undefined>[];
|
||||||
|
|
||||||
for (const fileDiff of mergedFilesDiffs) {
|
for (const fileDiff of mergedFilesDiffs) {
|
||||||
if (tokenCount(fileDiff) >= maxDiffLength) {
|
if (tokenCount(fileDiff) >= maxDiffLength) {
|
||||||
@@ -160,17 +191,25 @@ export function getCommitMsgsPromisesFromFileDiffs(
|
|||||||
const messagesPromises = getMessagesPromisesByChangesInFile(
|
const messagesPromises = getMessagesPromisesByChangesInFile(
|
||||||
fileDiff,
|
fileDiff,
|
||||||
separator,
|
separator,
|
||||||
maxDiffLength
|
maxDiffLength,
|
||||||
|
fullGitMojiSpec
|
||||||
);
|
);
|
||||||
|
|
||||||
commitMessagePromises.push(...messagesPromises);
|
commitMessagePromises.push(...messagesPromises);
|
||||||
} else {
|
} else {
|
||||||
const messages = generateCommitMessageChatCompletionPrompt(
|
const messages = await generateCommitMessageChatCompletionPrompt(
|
||||||
separator + fileDiff
|
separator + fileDiff,
|
||||||
|
fullGitMojiSpec
|
||||||
);
|
);
|
||||||
|
|
||||||
commitMessagePromises.push(api.generateCommitMessage(messages));
|
const engine = getEngine();
|
||||||
|
commitMessagePromises.push(engine.generateCommitMessage(messages));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return commitMessagePromises;
|
return commitMessagePromises;
|
||||||
|
};
|
||||||
|
|
||||||
|
function delay(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import core from '@actions/core';
|
import core from '@actions/core';
|
||||||
import github from '@actions/github';
|
|
||||||
import exec from '@actions/exec';
|
import exec from '@actions/exec';
|
||||||
|
import github from '@actions/github';
|
||||||
import { intro, outro } from '@clack/prompts';
|
import { intro, outro } from '@clack/prompts';
|
||||||
import { PushEvent } from '@octokit/webhooks-types';
|
import { PushEvent } from '@octokit/webhooks-types';
|
||||||
import { generateCommitMessageByDiff } from './generateCommitMessageFromGitDiff';
|
|
||||||
import { sleep } from './utils/sleep';
|
|
||||||
import { randomIntFromInterval } from './utils/randomIntFromInterval';
|
|
||||||
import { unlinkSync, writeFileSync } from 'fs';
|
import { unlinkSync, writeFileSync } from 'fs';
|
||||||
|
import { generateCommitMessageByDiff } from './generateCommitMessageFromGitDiff';
|
||||||
|
import { randomIntFromInterval } from './utils/randomIntFromInterval';
|
||||||
|
import { sleep } from './utils/sleep';
|
||||||
|
|
||||||
// This should be a token with access to your repository scoped in as a secret.
|
// This should be a token with access to your repository scoped in as a secret.
|
||||||
// The YML workflow will need to set GITHUB_TOKEN with the GitHub Secret Token
|
// The YML workflow will need to set GITHUB_TOKEN with the GitHub Secret Token
|
||||||
@@ -52,7 +52,7 @@ async function improveMessagesInChunks(diffsAndSHAs: DiffAndSHA[]) {
|
|||||||
const chunkSize = diffsAndSHAs!.length % 2 === 0 ? 4 : 3;
|
const chunkSize = diffsAndSHAs!.length % 2 === 0 ? 4 : 3;
|
||||||
outro(`Improving commit messages in chunks of ${chunkSize}.`);
|
outro(`Improving commit messages in chunks of ${chunkSize}.`);
|
||||||
const improvePromises = diffsAndSHAs!.map((commit) =>
|
const improvePromises = diffsAndSHAs!.map((commit) =>
|
||||||
generateCommitMessageByDiff(commit.diff)
|
generateCommitMessageByDiff(commit.diff, false)
|
||||||
);
|
);
|
||||||
|
|
||||||
let improvedMessagesAndSHAs: MsgAndSHA[] = [];
|
let improvedMessagesAndSHAs: MsgAndSHA[] = [];
|
||||||
@@ -133,6 +133,16 @@ async function improveCommitMessages(
|
|||||||
improvedMessagesWithSHAs
|
improvedMessagesWithSHAs
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check if there are actually any changes in the commit messages
|
||||||
|
const messagesChanged = improvedMessagesWithSHAs.some(
|
||||||
|
({ sha, msg }, index) => msg !== commitsToImprove[index].message
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!messagesChanged) {
|
||||||
|
console.log('No changes in commit messages detected, skipping rebase');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const createCommitMessageFile = (message: string, index: number) =>
|
const createCommitMessageFile = (message: string, index: number) =>
|
||||||
writeFileSync(`./commit-${index}.txt`, message);
|
writeFileSync(`./commit-${index}.txt`, message);
|
||||||
improvedMessagesWithSHAs.forEach(({ msg }, i) =>
|
improvedMessagesWithSHAs.forEach(({ msg }, i) =>
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "česky",
|
"localLanguage": "česky",
|
||||||
"commitFix": "fix(server.ts): zlepšení velikosti proměnné port na velká písmena PORT",
|
"commitFix": "fix(server.ts): zlepšení velikosti proměnné port na velká písmena PORT",
|
||||||
"commitFeat": "feat(server.ts): přidání podpory pro proměnnou prostředí process.env.PORT",
|
"commitFeat": "feat(server.ts): přidání podpory pro proměnnou prostředí process.env.PORT",
|
||||||
"commitDescription": "Proměnná port se nyní jmenuje PORT, což odpovídá konvenci pojmenování, protože PORT je konstanta. Podpora proměnné prostředí process.env.PORT umožňuje snadnější správu nastavení při spuštění."
|
"commitDescription": "Proměnná port se nyní jmenuje PORT, což odpovídá konvenci pojmenování, protože PORT je konstanta. Podpora proměnné prostředí process.env.PORT umožňuje snadnější správu nastavení při spuštění.",
|
||||||
|
"commitFixOmitScope": "fix: zlepšení velikosti proměnné port na velká písmena PORT",
|
||||||
|
"commitFeatOmitScope": "feat: přidání podpory pro proměnnou prostředí process.env.PORT"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "Deutsch",
|
"localLanguage": "Deutsch",
|
||||||
"commitFix": "fix(server.ts): Ändere die Groß- und Kleinschreibung der Port-Variable von Kleinbuchstaben auf Großbuchstaben PORT.",
|
"commitFix": "fix(server.ts): Ändere die Groß- und Kleinschreibung der Port-Variable von Kleinbuchstaben auf Großbuchstaben PORT.",
|
||||||
"commitFeat": "Funktion(server.ts): Unterstützung für die Umgebungsvariable process.env.PORT hinzufügen",
|
"commitFeat": "Funktion(server.ts): Unterstützung für die Umgebungsvariable process.env.PORT hinzufügen",
|
||||||
"commitDescription": "Die Port-Variable heißt jetzt PORT, was die Konsistenz mit den Namenskonventionen verbessert, da PORT eine Konstante ist. Die Unterstützung für eine Umgebungsvariable ermöglicht es der Anwendung, flexibler zu sein, da sie jetzt auf jedem verfügbaren Port laufen kann, der über die Umgebungsvariable process.env.PORT angegeben wird."
|
"commitDescription": "Die Port-Variable heißt jetzt PORT, was die Konsistenz mit den Namenskonventionen verbessert, da PORT eine Konstante ist. Die Unterstützung für eine Umgebungsvariable ermöglicht es der Anwendung, flexibler zu sein, da sie jetzt auf jedem verfügbaren Port laufen kann, der über die Umgebungsvariable process.env.PORT angegeben wird.",
|
||||||
|
"commitFixOmitScope": "fix: Ändere die Groß- und Kleinschreibung der Port-Variable von Kleinbuchstaben auf Großbuchstaben PORT.",
|
||||||
|
"commitFeatOmitScope": "Funktion: Unterstützung für die Umgebungsvariable process.env.PORT hinzufügen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "english",
|
"localLanguage": "english",
|
||||||
"commitFix": "fix(server.ts): change port variable case from lowercase port to uppercase PORT to improve semantics",
|
"commitFix": "fix(server.ts): change port variable case from lowercase port to uppercase PORT to improve semantics",
|
||||||
"commitFeat": "feat(server.ts): add support for process.env.PORT environment variable to be able to run app on a configurable port",
|
"commitFeat": "feat(server.ts): add support for process.env.PORT environment variable to be able to run app on a configurable port",
|
||||||
"commitDescription": "The port variable is now named PORT, which improves consistency with the naming conventions as PORT is a constant. Support for an environment variable allows the application to be more flexible as it can now run on any available port specified via the process.env.PORT environment variable."
|
"commitDescription": "The port variable is now named PORT, which improves consistency with the naming conventions as PORT is a constant. Support for an environment variable allows the application to be more flexible as it can now run on any available port specified via the process.env.PORT environment variable.",
|
||||||
|
"commitFixOmitScope": "fix: change port variable case from lowercase port to uppercase PORT to improve semantics",
|
||||||
|
"commitFeatOmitScope": "feat: add support for process.env.PORT environment variable to be able to run app on a configurable port"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "spanish",
|
"localLanguage": "spanish",
|
||||||
"commitFix": "fix(server.ts): cambiar la variable port de minúsculas a mayúsculas PORT",
|
"commitFix": "fix(server.ts): cambiar la variable port de minúsculas a mayúsculas PORT",
|
||||||
"commitFeat": "feat(server.ts): añadir soporte para la variable de entorno process.env.PORT",
|
"commitFeat": "feat(server.ts): añadir soporte para la variable de entorno process.env.PORT",
|
||||||
"commitDescription": "La variable port ahora se llama PORT, lo que mejora la coherencia con las convenciones de nomenclatura, ya que PORT es una constante. El soporte para una variable de entorno permite que la aplicación sea más flexible, ya que ahora puede ejecutarse en cualquier puerto disponible especificado a través de la variable de entorno process.env.PORT."
|
"commitDescription": "La variable port ahora se llama PORT, lo que mejora la coherencia con las convenciones de nomenclatura, ya que PORT es una constante. El soporte para una variable de entorno permite que la aplicación sea más flexible, ya que ahora puede ejecutarse en cualquier puerto disponible especificado a través de la variable de entorno process.env.PORT.",
|
||||||
|
"commitFixOmitScope": "fix: cambiar la variable port de minúsculas a mayúsculas PORT",
|
||||||
|
"commitFeatOmitScope": "feat: añadir soporte para la variable de entorno process.env.PORT"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "française",
|
"localLanguage": "française",
|
||||||
"commitFix": "corriger(server.ts) : changer la casse de la variable de port de minuscules à majuscules (PORT)",
|
"commitFix": "corriger(server.ts) : changer la casse de la variable de port de minuscules à majuscules (PORT)",
|
||||||
"commitFeat": "fonctionnalité(server.ts) : ajouter la prise en charge de la variable d'environnement process.env.PORT",
|
"commitFeat": "fonctionnalité(server.ts) : ajouter la prise en charge de la variable d'environnement process.env.PORT",
|
||||||
"commitDescription": "La variable de port est maintenant nommée PORT, ce qui améliore la cohérence avec les conventions de nommage car PORT est une constante. La prise en charge d'une variable d'environnement permet à l'application d'être plus flexible car elle peut maintenant s'exécuter sur n'importe quel port disponible spécifié via la variable d'environnement process.env.PORT."
|
"commitDescription": "La variable de port est maintenant nommée PORT, ce qui améliore la cohérence avec les conventions de nommage car PORT est une constante. La prise en charge d'une variable d'environnement permet à l'application d'être plus flexible car elle peut maintenant s'exécuter sur n'importe quel port disponible spécifié via la variable d'environnement process.env.PORT.",
|
||||||
|
"commitFixOmitScope": "corriger : changer la casse de la variable de port de minuscules à majuscules (PORT)",
|
||||||
|
"commitFeatOmitScope": "fonctionnalité : ajouter la prise en charge de la variable d'environnement process.env.PORT"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "bahasa",
|
"localLanguage": "bahasa",
|
||||||
"commitFix": "fix(server.ts): mengubah huruf port variable dari huruf kecil ke huruf besar PORT",
|
"commitFix": "fix(server.ts): mengubah huruf port variable dari huruf kecil ke huruf besar PORT",
|
||||||
"commitFeat": "feat(server.ts): menambahkan support di process.env.PORT environment variabel",
|
"commitFeat": "feat(server.ts): menambahkan support di process.env.PORT environment variabel",
|
||||||
"commitDescription": "Port variabel bernama PORT, yang membantu konsistensi dengan memberi nama yaitu PORT yang konstan. Bantuan environment variabel membantu aplikasi lebih fleksibel, dan dapat di jalankan di port manapun yang tertulis pada process.env.PORT"
|
"commitDescription": "Port variabel bernama PORT, yang membantu konsistensi dengan memberi nama yaitu PORT yang konstan. Bantuan environment variabel membantu aplikasi lebih fleksibel, dan dapat di jalankan di port manapun yang tertulis pada process.env.PORT",
|
||||||
|
"commitFixOmitScope": "fix: mengubah huruf port variable dari huruf kecil ke huruf besar PORT",
|
||||||
|
"commitFeatOmitScope": "feat: menambahkan support di process.env.PORT environment variabel"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import en from '../i18n/en.json' assert { type: 'json' };
|
import cs from '../i18n/cs.json';
|
||||||
import cs from '../i18n/cs.json' assert { type: 'json' };
|
import de from '../i18n/de.json';
|
||||||
import de from '../i18n/de.json' assert { type: 'json' };
|
import en from '../i18n/en.json';
|
||||||
import fr from '../i18n/fr.json' assert { type: 'json' };
|
import es_ES from '../i18n/es_ES.json';
|
||||||
import it from '../i18n/it.json' assert { type: 'json' };
|
import fr from '../i18n/fr.json';
|
||||||
import ko from '../i18n/ko.json' assert { type: 'json' };
|
import id_ID from '../i18n/id_ID.json';
|
||||||
import zh_CN from '../i18n/zh_CN.json' assert { type: 'json' };
|
import it from '../i18n/it.json';
|
||||||
import zh_TW from '../i18n/zh_TW.json' assert { type: 'json' };
|
import ja from '../i18n/ja.json';
|
||||||
import ja from '../i18n/ja.json' assert { type: 'json' };
|
import ko from '../i18n/ko.json';
|
||||||
import pt_br from '../i18n/pt_br.json' assert { type: 'json' };
|
import nl from '../i18n/nl.json';
|
||||||
import vi_VN from '../i18n/vi_VN.json' assert { type: 'json' };
|
import pl from '../i18n/pl.json';
|
||||||
import es_ES from '../i18n/es_ES.json' assert { type: 'json' };
|
import pt_br from '../i18n/pt_br.json';
|
||||||
import sv from '../i18n/sv.json' assert { type: 'json' };
|
import ru from '../i18n/ru.json';
|
||||||
import nl from '../i18n/nl.json' assert { type: 'json' };
|
import sv from '../i18n/sv.json';
|
||||||
import ru from '../i18n/ru.json' assert { type: 'json' };
|
import th from '../i18n/th.json';
|
||||||
import id_ID from '../i18n/id_ID.json' assert { type: 'json' };
|
import tr from '../i18n/tr.json';
|
||||||
import pl from '../i18n/pl.json' assert { type: 'json' };
|
import vi_VN from '../i18n/vi_VN.json';
|
||||||
import tr from '../i18n/tr.json' assert { type: 'json' };
|
import zh_CN from '../i18n/zh_CN.json';
|
||||||
import th from '../i18n/th.json' assert { type: 'json' };
|
import zh_TW from '../i18n/zh_TW.json';
|
||||||
|
|
||||||
export enum I18nLocals {
|
export enum I18nLocals {
|
||||||
'en' = 'en',
|
'en' = 'en',
|
||||||
@@ -36,7 +36,7 @@ export enum I18nLocals {
|
|||||||
'id_ID' = 'id_ID',
|
'id_ID' = 'id_ID',
|
||||||
'pl' = 'pl',
|
'pl' = 'pl',
|
||||||
'tr' = 'tr',
|
'tr' = 'tr',
|
||||||
'th' = 'th',
|
'th' = 'th'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const i18n = {
|
export const i18n = {
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "italiano",
|
"localLanguage": "italiano",
|
||||||
"commitFix": "fix(server.ts): cambia la grafia della variabile della porta dal minuscolo port al maiuscolo PORT",
|
"commitFix": "fix(server.ts): cambia la grafia della variabile della porta dal minuscolo port al maiuscolo PORT",
|
||||||
"commitFeat": "feat(server.ts): aggiunge il supporto per la variabile di ambiente process.env.PORT",
|
"commitFeat": "feat(server.ts): aggiunge il supporto per la variabile di ambiente process.env.PORT",
|
||||||
"commitDescription": "La variabile port è ora chiamata PORT, migliorando la coerenza con le convenzioni di denominazione in quanto PORT è una costante. Il supporto per una variabile di ambiente consente all'applicazione di essere più flessibile poiché ora può essere eseguita su qualsiasi porta disponibile specificata tramite la variabile di ambiente process.env.PORT."
|
"commitDescription": "La variabile port è ora chiamata PORT, migliorando la coerenza con le convenzioni di denominazione in quanto PORT è una costante. Il supporto per una variabile di ambiente consente all'applicazione di essere più flessibile poiché ora può essere eseguita su qualsiasi porta disponibile specificata tramite la variabile di ambiente process.env.PORT.",
|
||||||
|
"commitFixOmitScope": "fix: cambia la grafia della variabile della porta dal minuscolo port al maiuscolo PORT",
|
||||||
|
"commitFeatOmitScope": "feat: aggiunge il supporto per la variabile di ambiente process.env.PORT"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "日本語",
|
"localLanguage": "日本語",
|
||||||
"commitFix": "修正(server.ts): ポート変数を小文字のportから大文字のPORTに変更",
|
"commitFix": "修正(server.ts): ポート変数を小文字のportから大文字のPORTに変更",
|
||||||
"commitFeat": "新機能(server.ts): 環境変数process.env.PORTのサポートを追加",
|
"commitFeat": "新機能(server.ts): 環境変数process.env.PORTのサポートを追加",
|
||||||
"commitDescription": "ポート変数は現在PORTという名前になり、定数であるPORTを使うことで命名規則に一貫性が生まれました。環境変数をサポートすることで、環境変数process.env.PORTで指定された任意の利用可能なポートで実行できるようになり、アプリケーションはより柔軟になりました。"
|
"commitDescription": "ポート変数は現在PORTという名前になり、定数であるPORTを使うことで命名規則に一貫性が生まれました。環境変数をサポートすることで、環境変数process.env.PORTで指定された任意の利用可能なポートで実行できるようになり、アプリケーションはより柔軟になりました。",
|
||||||
|
"commitFixOmitScope": "修正: ポート変数を小文字のportから大文字のPORTに変更",
|
||||||
|
"commitFeatOmitScope": "新機能: 環境変数process.env.PORTのサポートを追加"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"localLanguage": "한국어",
|
"localLanguage": "한국어",
|
||||||
"commitFix": "fix(server.ts): 포트 변수를 소문자 port에서 대문자 PORT로 변경",
|
"commitFix": "fix(server.ts): 포트 변수를 소문자 port에서 대문자 PORT로 변경",
|
||||||
"commitFeat": "피트(server.ts): process.env.PORT 환경 변수 지원 추가",
|
"commitFeat": "feat(server.ts): process.env.PORT 환경 변수 지원 추가",
|
||||||
"commitDescription": "포트 변수는 이제 PORT로 이름이 지정되어 상수인 PORT와 일관성 있는 이름 규칙을 따릅니다. 환경 변수 지원을 통해 애플리케이션은 이제 process.env.PORT 환경 변수로 지정된 사용 가능한 모든 포트에서 실행할 수 있으므로 더 유연해졌습니다."
|
"commitDescription": "포트 변수는 이제 PORT로 이름이 지정되어 상수인 PORT와 일관성 있는 이름 규칙을 따릅니다. 환경 변수 지원을 통해 애플리케이션은 이제 process.env.PORT 환경 변수로 지정된 사용 가능한 모든 포트에서 실행할 수 있으므로 더 유연해졌습니다.",
|
||||||
|
"commitFixOmitScope": "fix: 포트 변수를 소문자 port에서 대문자 PORT로 변경",
|
||||||
|
"commitFeatOmitScope": "feat: process.env.PORT 환경 변수 지원 추가"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "Nederlands",
|
"localLanguage": "Nederlands",
|
||||||
"commitFix": "fix(server.ts): verander poortvariabele van kleine letters poort naar hoofdletters PORT",
|
"commitFix": "fix(server.ts): verander poortvariabele van kleine letters poort naar hoofdletters PORT",
|
||||||
"commitFeat": "feat(server.ts): voeg ondersteuning toe voor process.env.PORT omgevingsvariabele",
|
"commitFeat": "feat(server.ts): voeg ondersteuning toe voor process.env.PORT omgevingsvariabele",
|
||||||
"commitDescription": "De poortvariabele heet nu PORT, wat de consistentie met de naamgevingsconventies verbetert omdat PORT een constante is. Ondersteuning voor een omgevingsvariabele maakt de applicatie flexibeler, omdat deze nu kan draaien op elke beschikbare poort die is gespecificeerd via de process.env.PORT omgevingsvariabele."
|
"commitDescription": "De poortvariabele heet nu PORT, wat de consistentie met de naamgevingsconventies verbetert omdat PORT een constante is. Ondersteuning voor een omgevingsvariabele maakt de applicatie flexibeler, omdat deze nu kan draaien op elke beschikbare poort die is gespecificeerd via de process.env.PORT omgevingsvariabele.",
|
||||||
|
"commitFixOmitScope": "fix: verander poortvariabele van kleine letters poort naar hoofdletters PORT",
|
||||||
|
"commitFeatOmitScope": "feat: voeg ondersteuning toe voor process.env.PORT omgevingsvariabele"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "polski",
|
"localLanguage": "polski",
|
||||||
"commitFix": "fix(server.ts): poprawa wielkości zmiennej port na pisane z dużymi literami PORT",
|
"commitFix": "fix(server.ts): poprawa wielkości zmiennej port na pisane z dużymi literami PORT",
|
||||||
"commitFeat": "feat(server.ts): dodanie obsługi zmiennej środowiskowej process.env.PORT",
|
"commitFeat": "feat(server.ts): dodanie obsługi zmiennej środowiskowej process.env.PORT",
|
||||||
"commitDescription": "Zmienna port jest teraz nazwana PORT, co jest zgodne z konwencją nazewniczą ponieważ PORT jest stałą. Obsługa zmiennej środowiskowej process.env.PORT pozwala łatwiej zarządzać ustawieniami przy starcie."
|
"commitDescription": "Zmienna port jest teraz nazwana PORT, co jest zgodne z konwencją nazewniczą ponieważ PORT jest stałą. Obsługa zmiennej środowiskowej process.env.PORT pozwala łatwiej zarządzać ustawieniami przy starcie.",
|
||||||
|
"commitFixOmitScope": "fix: poprawa wielkości zmiennej port na pisane z dużymi literami PORT",
|
||||||
|
"commitFeatOmitScope": "feat: dodanie obsługi zmiennej środowiskowej process.env.PORT"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "português",
|
"localLanguage": "português",
|
||||||
"commitFix": "fix(server.ts): altera o caso da variável de porta de port minúscula para PORT maiúscula",
|
"commitFix": "fix(server.ts): altera o caso da variável de porta de port minúscula para PORT maiúscula",
|
||||||
"commitFeat": "feat(server.ts): adiciona suporte para a variável de ambiente process.env.PORT",
|
"commitFeat": "feat(server.ts): adiciona suporte para a variável de ambiente process.env.PORT",
|
||||||
"commitDescription": "A variável de porta agora é denominada PORT, o que melhora a consistência com as convenções de nomenclatura, pois PORT é uma constante. O suporte para uma variável de ambiente permite que o aplicativo seja mais flexível, pois agora pode ser executado em qualquer porta disponível especificada por meio da variável de ambiente process.env.PORT."
|
"commitDescription": "A variável de porta agora é denominada PORT, o que melhora a consistência com as convenções de nomenclatura, pois PORT é uma constante. O suporte para uma variável de ambiente permite que o aplicativo seja mais flexível, pois agora pode ser executado em qualquer porta disponível especificada por meio da variável de ambiente process.env.PORT.",
|
||||||
|
"commitFixOmitScope": "fix: altera o caso da variável de porta de port minúscula para PORT maiúscula",
|
||||||
|
"commitFeatOmitScope": "feat: adiciona suporte para a variável de ambiente process.env.PORT"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "русский",
|
"localLanguage": "русский",
|
||||||
"commitFix": "fix(server.ts): изменение регистра переменной порта с нижнего регистра port на верхний регистр PORT",
|
"commitFix": "fix(server.ts): изменение регистра переменной порта с нижнего регистра port на верхний регистр PORT",
|
||||||
"commitFeat": "feat(server.ts): добавлена поддержка переменной окружения process.env.PORT",
|
"commitFeat": "feat(server.ts): добавлена поддержка переменной окружения process.env.PORT",
|
||||||
"commitDescription": "Переменная port теперь называется PORT, что улучшает согласованность с соглашениями об именовании констант. Поддержка переменной окружения позволяет приложению быть более гибким, запускаясь на любом доступном порту, указанном с помощью переменной окружения process.env.PORT."
|
"commitDescription": "Переменная port теперь называется PORT, что улучшает согласованность с соглашениями об именовании констант. Поддержка переменной окружения позволяет приложению быть более гибким, запускаясь на любом доступном порту, указанном с помощью переменной окружения process.env.PORT.",
|
||||||
|
"commitFixOmitScope": "fix: изменение регистра переменной порта с нижнего регистра port на верхний регистр PORT",
|
||||||
|
"commitFeatOmitScope": "feat: добавлена поддержка переменной окружения process.env.PORT"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "svenska",
|
"localLanguage": "svenska",
|
||||||
"commitFix": "fixa(server.ts): ändra variabelnamnet för port från små bokstäver till stora bokstäver PORT",
|
"commitFix": "fixa(server.ts): ändra variabelnamnet för port från små bokstäver till stora bokstäver PORT",
|
||||||
"commitFeat": "nyhet(server.ts): lägg till stöd för process.env.PORT miljövariabel",
|
"commitFeat": "nyhet(server.ts): lägg till stöd för process.env.PORT miljövariabel",
|
||||||
"commitDescription": "Variabeln som innehåller portnumret heter nu PORT vilket förbättrar konsekvensen med namngivningskonventionerna eftersom PORT är en konstant. Stöd för en miljövariabel gör att applikationen kan vara mer flexibel då den nu kan köras på vilken port som helst som specificeras via miljövariabeln process.env.PORT."
|
"commitDescription": "Variabeln som innehåller portnumret heter nu PORT vilket förbättrar konsekvensen med namngivningskonventionerna eftersom PORT är en konstant. Stöd för en miljövariabel gör att applikationen kan vara mer flexibel då den nu kan köras på vilken port som helst som specificeras via miljövariabeln process.env.PORT.",
|
||||||
|
"commitFixOmitScope": "fixa: ändra variabelnamnet för port från små bokstäver till stora bokstäver PORT",
|
||||||
|
"commitFeatOmitScope": "nyhet: lägg till stöd för process.env.PORT miljövariabel"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "ไทย",
|
"localLanguage": "ไทย",
|
||||||
"commitFix": "fix(server.ts): เปลี่ยนตัวพิมพ์ของตัวแปร จากตัวพิมพ์เล็ก port เป็นตัวพิมพ์ใหญ่ PORT",
|
"commitFix": "fix(server.ts): เปลี่ยนตัวพิมพ์ของตัวแปร จากตัวพิมพ์เล็ก port เป็นตัวพิมพ์ใหญ่ PORT",
|
||||||
"commitFeat": "feat(server.ts): เพิ่มการรองรับสำหรับตัวแปรสภาพแวดล้อม process.env.PORT",
|
"commitFeat": "feat(server.ts): เพิ่มการรองรับสำหรับตัวแปรสภาพแวดล้อม process.env.PORT",
|
||||||
"commitDescription": "ตอนนี้ตัวแปรพอร์ตมีชื่อว่า PORT, ซึ่งปรับปรุงความสอดคล้องกับหลักการตั้งชื่อเนื่องจาก PORT เป็นค่าคงที่. การสนับสนุนสำหรับตัวแปรสภาพแวดล้อม ช่วยให้แอปพลิเคชันมีความยืดหยุ่นมากขึ้นเนื่องจาก สามารถทำงานบนพอร์ตใด ๆ ตามที่กำหนด ซึ่งระบุผ่านตัวแปรสภาพแวดล้อม process.env.PORT"
|
"commitDescription": "ตอนนี้ตัวแปรพอร์ตมีชื่อว่า PORT, ซึ่งปรับปรุงความสอดคล้องกับหลักการตั้งชื่อเนื่องจาก PORT เป็นค่าคงที่. การสนับสนุนสำหรับตัวแปรสภาพแวดล้อม ช่วยให้แอปพลิเคชันมีความยืดหยุ่นมากขึ้นเนื่องจาก สามารถทำงานบนพอร์ตใด ๆ ตามที่กำหนด ซึ่งระบุผ่านตัวแปรสภาพแวดล้อม process.env.PORT",
|
||||||
|
"commitFixOmitScope": "fix: เปลี่ยนตัวพิมพ์ของตัวแปร จากตัวพิมพ์เล็ก port เป็นตัวพิมพ์ใหญ่ PORT",
|
||||||
|
"commitFeatOmitScope": "feat: เพิ่มการรองรับสำหรับตัวแปรสภาพแวดล้อม process.env.PORT"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "Turkish",
|
"localLanguage": "Turkish",
|
||||||
"commitFix": "fix(server.ts): port değişkeni küçük harfli porttan büyük harfli PORT'a değiştirildi",
|
"commitFix": "fix(server.ts): port değişkeni küçük harfli porttan büyük harfli PORT'a değiştirildi",
|
||||||
"commitFeat": "feat(server.ts): process.env.PORT ortam değişkeni için destek eklendi.",
|
"commitFeat": "feat(server.ts): process.env.PORT ortam değişkeni için destek eklendi.",
|
||||||
"commitDescription": "Bağlantı noktası değişkeni artık PORT olarak adlandırıldı ve PORT bir sabit değişken olduğu için bu adlandırma tutarlılığı artırır. Ortam değişkeni desteği, artık process.env.PORT ortam değişkeni aracılığıyla belirtilen herhangi bir kullanılabilir bağlantı noktasında çalışabileceğinden uygulamanın daha esnek olmasını sağlar."
|
"commitDescription": "Bağlantı noktası değişkeni artık PORT olarak adlandırıldı ve PORT bir sabit değişken olduğu için bu adlandırma tutarlılığı artırır. Ortam değişkeni desteği, artık process.env.PORT ortam değişkeni aracılığıyla belirtilen herhangi bir kullanılabilir bağlantı noktasında çalışabileceğinden uygulamanın daha esnek olmasını sağlar.",
|
||||||
|
"commitFixOmitScope": "fix: port değişkeni küçük harfli porttan büyük harfli PORT'a değiştirildi",
|
||||||
|
"commitFeatOmitScope": "feat: process.env.PORT ortam değişkeni için destek eklendi."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "vietnamese",
|
"localLanguage": "vietnamese",
|
||||||
"commitFix": "fix(server.ts): thay đổi chữ viết thường của biến port thành chữ viết hoa PORT",
|
"commitFix": "fix(server.ts): thay đổi chữ viết thường của biến port thành chữ viết hoa PORT",
|
||||||
"commitFeat": "feat(server.ts): thêm hỗ trợ cho biến môi trường process.env.PORT",
|
"commitFeat": "feat(server.ts): thêm hỗ trợ cho biến môi trường process.env.PORT",
|
||||||
"commitDescription": "Biến port đã được đổi tên thành PORT, giúp cải thiện tính nhất quán trong việc đặt tên theo quy ước vì PORT là một hằng số. Hỗ trợ cho biến môi trường cho phép ứng dụng linh hoạt hơn khi có thể chạy trên bất kỳ cổng nào được chỉ định thông qua biến môi trường process.env.PORT."
|
"commitDescription": "Biến port đã được đổi tên thành PORT, giúp cải thiện tính nhất quán trong việc đặt tên theo quy ước vì PORT là một hằng số. Hỗ trợ cho biến môi trường cho phép ứng dụng linh hoạt hơn khi có thể chạy trên bất kỳ cổng nào được chỉ định thông qua biến môi trường process.env.PORT.",
|
||||||
|
"commitFixOmitScope": "fix: thay đổi chữ viết thường của biến port thành chữ viết hoa PORT",
|
||||||
|
"commitFeatOmitScope": "feat: thêm hỗ trợ cho biến môi trường process.env.PORT"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"localLanguage": "简体中文",
|
"localLanguage": "简体中文",
|
||||||
"commitFix": "修复(server.ts):将端口变量从小写port改为大写PORT",
|
"commitFix": "fix(server.ts):将端口变量从小写port改为大写PORT",
|
||||||
"commitFeat": "功能(server.ts):添加对process.env.PORT环境变量的支持",
|
"commitFeat": "feat(server.ts):添加对process.env.PORT环境变量的支持",
|
||||||
"commitDescription": "现在端口变量被命名为PORT,这提高了命名约定的一致性,因为PORT是一个常量。环境变量的支持使应用程序更加灵活,因为它现在可以通过process.env.PORT环境变量在任何可用端口上运行。"
|
"commitDescription": "现在端口变量被命名为PORT,这提高了命名约定的一致性,因为PORT是一个常量。环境变量的支持使应用程序更加灵活,因为它现在可以通过process.env.PORT环境变量在任何可用端口上运行。",
|
||||||
|
"commitFixOmitScope": "fix:将端口变量从小写port改为大写PORT",
|
||||||
|
"commitFeatOmitScope": "feat:添加对process.env.PORT环境变量的支持"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
"localLanguage": "繁體中文",
|
"localLanguage": "繁體中文",
|
||||||
"commitFix": "修正(server.ts):將端口變數從小寫端口改為大寫PORT",
|
"commitFix": "修正(server.ts):將端口變數從小寫端口改為大寫PORT",
|
||||||
"commitFeat": "功能(server.ts):新增對process.env.PORT環境變數的支援",
|
"commitFeat": "功能(server.ts):新增對process.env.PORT環境變數的支援",
|
||||||
"commitDescription": "現在port變數已更名為PORT,以符合命名慣例,因為PORT是一個常量。支援環境變數可以使應用程序更靈活,因為它現在可以通過process.env.PORT環境變數運行在任何可用端口上。"
|
"commitDescription": "現在port變數已更名為PORT,以符合命名慣例,因為PORT是一個常量。支援環境變數可以使應用程序更靈活,因為它現在可以通過process.env.PORT環境變數運行在任何可用端口上。",
|
||||||
|
"commitFixOmitScope": "修正:將端口變數從小寫端口改為大寫PORT",
|
||||||
|
"commitFeatOmitScope": "功能:新增對process.env.PORT環境變數的支援"
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/migrations/00_use_single_api_key_and_url.ts
Normal file
45
src/migrations/00_use_single_api_key_and_url.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
CONFIG_KEYS,
|
||||||
|
getConfig,
|
||||||
|
OCO_AI_PROVIDER_ENUM,
|
||||||
|
setConfig
|
||||||
|
} from '../commands/config';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const config = getConfig({ setDefaultValues: false });
|
||||||
|
|
||||||
|
const aiProvider = config.OCO_AI_PROVIDER;
|
||||||
|
|
||||||
|
let apiKey: string | undefined;
|
||||||
|
let apiUrl: string | undefined;
|
||||||
|
|
||||||
|
if (aiProvider === OCO_AI_PROVIDER_ENUM.OLLAMA) {
|
||||||
|
apiKey = config['OCO_OLLAMA_API_KEY'];
|
||||||
|
apiUrl = config['OCO_OLLAMA_API_URL'];
|
||||||
|
} else if (aiProvider === OCO_AI_PROVIDER_ENUM.ANTHROPIC) {
|
||||||
|
apiKey = config['OCO_ANTHROPIC_API_KEY'];
|
||||||
|
apiUrl = config['OCO_ANTHROPIC_BASE_PATH'];
|
||||||
|
} else if (aiProvider === OCO_AI_PROVIDER_ENUM.OPENAI) {
|
||||||
|
apiKey = config['OCO_OPENAI_API_KEY'];
|
||||||
|
apiUrl = config['OCO_OPENAI_BASE_PATH'];
|
||||||
|
} else if (aiProvider === OCO_AI_PROVIDER_ENUM.AZURE) {
|
||||||
|
apiKey = config['OCO_AZURE_API_KEY'];
|
||||||
|
apiUrl = config['OCO_AZURE_ENDPOINT'];
|
||||||
|
} else if (aiProvider === OCO_AI_PROVIDER_ENUM.GEMINI) {
|
||||||
|
apiKey = config['OCO_GEMINI_API_KEY'];
|
||||||
|
apiUrl = config['OCO_GEMINI_BASE_PATH'];
|
||||||
|
} else if (aiProvider === OCO_AI_PROVIDER_ENUM.FLOWISE) {
|
||||||
|
apiKey = config['OCO_FLOWISE_API_KEY'];
|
||||||
|
apiUrl = config['OCO_FLOWISE_ENDPOINT'];
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Migration failed, set AI provider first. Run "oco config set OCO_AI_PROVIDER=<provider>", where <provider> is one of: ${Object.values(
|
||||||
|
OCO_AI_PROVIDER_ENUM
|
||||||
|
).join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey) setConfig([[CONFIG_KEYS.OCO_API_KEY, apiKey]]);
|
||||||
|
|
||||||
|
if (apiUrl) setConfig([[CONFIG_KEYS.OCO_API_URL, apiUrl]]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { getGlobalConfig, setGlobalConfig } from '../commands/config';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const obsoleteKeys = [
|
||||||
|
'OCO_OLLAMA_API_KEY',
|
||||||
|
'OCO_OLLAMA_API_URL',
|
||||||
|
'OCO_ANTHROPIC_API_KEY',
|
||||||
|
'OCO_ANTHROPIC_BASE_PATH',
|
||||||
|
'OCO_OPENAI_API_KEY',
|
||||||
|
'OCO_OPENAI_BASE_PATH',
|
||||||
|
'OCO_AZURE_API_KEY',
|
||||||
|
'OCO_AZURE_ENDPOINT',
|
||||||
|
'OCO_GEMINI_API_KEY',
|
||||||
|
'OCO_GEMINI_BASE_PATH',
|
||||||
|
'OCO_FLOWISE_API_KEY',
|
||||||
|
'OCO_FLOWISE_ENDPOINT'
|
||||||
|
];
|
||||||
|
|
||||||
|
const globalConfig = getGlobalConfig();
|
||||||
|
|
||||||
|
const configToOverride = { ...globalConfig };
|
||||||
|
|
||||||
|
for (const key of obsoleteKeys) delete configToOverride[key];
|
||||||
|
|
||||||
|
setGlobalConfig(configToOverride);
|
||||||
|
}
|
||||||
22
src/migrations/02_set_missing_default_values.ts
Normal file
22
src/migrations/02_set_missing_default_values.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import {
|
||||||
|
ConfigType,
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
getGlobalConfig,
|
||||||
|
setConfig
|
||||||
|
} from '../commands/config';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const setDefaultConfigValues = (config: ConfigType) => {
|
||||||
|
const entriesToSet: [key: string, value: string | boolean | number][] = [];
|
||||||
|
for (const entry of Object.entries(DEFAULT_CONFIG)) {
|
||||||
|
const [key, _value] = entry;
|
||||||
|
if (config[key] === 'undefined' || config[key] === undefined)
|
||||||
|
entriesToSet.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entriesToSet.length > 0) setConfig(entriesToSet);
|
||||||
|
console.log(entriesToSet);
|
||||||
|
};
|
||||||
|
|
||||||
|
setDefaultConfigValues(getGlobalConfig());
|
||||||
|
}
|
||||||
18
src/migrations/_migrations.ts
Normal file
18
src/migrations/_migrations.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import migration00 from './00_use_single_api_key_and_url';
|
||||||
|
import migration01 from './01_remove_obsolete_config_keys_from_global_file';
|
||||||
|
import migration02 from './02_set_missing_default_values';
|
||||||
|
|
||||||
|
export const migrations = [
|
||||||
|
{
|
||||||
|
name: '00_use_single_api_key_and_url',
|
||||||
|
run: migration00
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '01_remove_obsolete_config_keys_from_global_file',
|
||||||
|
run: migration01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '02_set_missing_default_values',
|
||||||
|
run: migration02
|
||||||
|
}
|
||||||
|
];
|
||||||
84
src/migrations/_run.ts
Normal file
84
src/migrations/_run.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import { homedir } from 'os';
|
||||||
|
import { join as pathJoin } from 'path';
|
||||||
|
import { migrations } from './_migrations';
|
||||||
|
import { outro } from '@clack/prompts';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import {
|
||||||
|
getConfig,
|
||||||
|
getIsGlobalConfigFileExist,
|
||||||
|
OCO_AI_PROVIDER_ENUM
|
||||||
|
} from '../commands/config';
|
||||||
|
|
||||||
|
const migrationsFile = pathJoin(homedir(), '.opencommit_migrations');
|
||||||
|
|
||||||
|
const getCompletedMigrations = (): string[] => {
|
||||||
|
if (!fs.existsSync(migrationsFile)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const data = fs.readFileSync(migrationsFile, 'utf-8');
|
||||||
|
return data ? JSON.parse(data) : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveCompletedMigration = (migrationName: string) => {
|
||||||
|
const completedMigrations = getCompletedMigrations();
|
||||||
|
completedMigrations.push(migrationName);
|
||||||
|
fs.writeFileSync(
|
||||||
|
migrationsFile,
|
||||||
|
JSON.stringify(completedMigrations, null, 2)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runMigrations = async () => {
|
||||||
|
// if no config file, we assume it's a new installation and no migrations are needed
|
||||||
|
if (!getIsGlobalConfigFileExist()) return;
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
if (config.OCO_AI_PROVIDER === OCO_AI_PROVIDER_ENUM.TEST) return;
|
||||||
|
|
||||||
|
// skip unhandled providers in migration00
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
OCO_AI_PROVIDER_ENUM.DEEPSEEK,
|
||||||
|
OCO_AI_PROVIDER_ENUM.GROQ,
|
||||||
|
OCO_AI_PROVIDER_ENUM.MISTRAL,
|
||||||
|
OCO_AI_PROVIDER_ENUM.MLX,
|
||||||
|
OCO_AI_PROVIDER_ENUM.OPENROUTER
|
||||||
|
].includes(config.OCO_AI_PROVIDER)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const completedMigrations = getCompletedMigrations();
|
||||||
|
|
||||||
|
let isMigrated = false;
|
||||||
|
|
||||||
|
for (const migration of migrations) {
|
||||||
|
if (!completedMigrations.includes(migration.name)) {
|
||||||
|
try {
|
||||||
|
console.log('Applying migration', migration.name);
|
||||||
|
migration.run();
|
||||||
|
console.log('Migration applied successfully', migration.name);
|
||||||
|
saveCompletedMigration(migration.name);
|
||||||
|
} catch (error) {
|
||||||
|
outro(
|
||||||
|
`${chalk.red('Failed to apply migration')} ${
|
||||||
|
migration.name
|
||||||
|
}: ${error}`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
isMigrated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMigrated) {
|
||||||
|
outro(
|
||||||
|
`${chalk.green(
|
||||||
|
'✔'
|
||||||
|
)} Migrations to your config were applied successfully. Please rerun.`
|
||||||
|
);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
93
src/modules/commitlint/config.ts
Normal file
93
src/modules/commitlint/config.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { spinner } from '@clack/prompts';
|
||||||
|
|
||||||
|
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';
|
||||||
|
import { getCommitLintPWDConfig } from './pwd-commitlint';
|
||||||
|
import { CommitlintLLMConfig } from './types';
|
||||||
|
import * as utils from './utils';
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
const translation = i18n[(config.OCO_LANGUAGE as I18nLocals) || 'en'];
|
||||||
|
|
||||||
|
export const configureCommitlintIntegration = async (force = false) => {
|
||||||
|
const spin = spinner();
|
||||||
|
spin.start('Loading @commitlint configuration');
|
||||||
|
|
||||||
|
const fileExists = await utils.commitlintLLMConfigExists();
|
||||||
|
|
||||||
|
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(
|
||||||
|
// `${OPENCOMMIT_COMMITLINT_CONFIG}-commitlint-debug`,
|
||||||
|
// JSON.stringify(commitLintConfig, null, 2)
|
||||||
|
// );
|
||||||
|
|
||||||
|
const hash = await computeHash(JSON.stringify(commitLintConfig));
|
||||||
|
|
||||||
|
spin.stop(`Read @commitlint configuration (hash: ${hash})`);
|
||||||
|
|
||||||
|
if (fileExists) {
|
||||||
|
// Check if we need to update the prompts.
|
||||||
|
const { hash: existingHash } = await utils.getCommitlintLLMConfig();
|
||||||
|
if (hash === existingHash && !force) {
|
||||||
|
spin.stop(
|
||||||
|
'Hashes are the same, no need to update the config. Run "force" command to bypass.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spin.start('Generating consistency with given @commitlint rules');
|
||||||
|
|
||||||
|
const prompts = inferPromptsFromCommitlintConfig(commitLintConfig as any);
|
||||||
|
|
||||||
|
const consistencyPrompts =
|
||||||
|
commitlintPrompts.GEN_COMMITLINT_CONSISTENCY_PROMPT(prompts);
|
||||||
|
|
||||||
|
// debug prompt which will generate a consistency
|
||||||
|
// await fs.writeFile(
|
||||||
|
// `${COMMITLINT_LLM_CONFIG}-debug`,
|
||||||
|
// consistencyPrompts.map((p) => p.content)
|
||||||
|
// );
|
||||||
|
|
||||||
|
const engine = getEngine();
|
||||||
|
let consistency =
|
||||||
|
(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);
|
||||||
|
|
||||||
|
const commitlintLLMConfig: CommitlintLLMConfig = {
|
||||||
|
hash,
|
||||||
|
prompts,
|
||||||
|
consistency: {
|
||||||
|
[translation.localLanguage]: {
|
||||||
|
...JSON.parse(consistency as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await utils.writeCommitlintLLMConfig(commitlintLLMConfig);
|
||||||
|
|
||||||
|
spin.stop(`Done - please review contents of ${COMMITLINT_LLM_CONFIG_PATH}`);
|
||||||
|
};
|
||||||
1
src/modules/commitlint/constants.ts
Normal file
1
src/modules/commitlint/constants.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const COMMITLINT_LLM_CONFIG_PATH = `${process.env.PWD}/.opencommit-commitlint`;
|
||||||
15
src/modules/commitlint/crypto.ts
Normal file
15
src/modules/commitlint/crypto.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
export const computeHash = async (
|
||||||
|
content: string,
|
||||||
|
algorithm: string = 'sha256'
|
||||||
|
): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const hash = crypto.createHash(algorithm);
|
||||||
|
hash.update(content);
|
||||||
|
return hash.digest('hex');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error while computing hash:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
305
src/modules/commitlint/prompts.ts
Normal file
305
src/modules/commitlint/prompts.ts
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
import chalk from 'chalk';
|
||||||
|
import { OpenAI } from 'openai';
|
||||||
|
|
||||||
|
import { outro } from '@clack/prompts';
|
||||||
|
import {
|
||||||
|
PromptConfig,
|
||||||
|
QualifiedConfig,
|
||||||
|
RuleConfigSeverity,
|
||||||
|
RuleConfigTuple
|
||||||
|
} from '@commitlint/types';
|
||||||
|
|
||||||
|
import { getConfig } from '../../commands/config';
|
||||||
|
import { i18n, I18nLocals } from '../../i18n';
|
||||||
|
import { IDENTITY, INIT_DIFF_PROMPT } from '../../prompts';
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
const translation = i18n[(config.OCO_LANGUAGE as I18nLocals) || 'en'];
|
||||||
|
|
||||||
|
type DeepPartial<T> = {
|
||||||
|
[P in keyof T]?: {
|
||||||
|
[K in keyof T[P]]?: T[P][K];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type PromptFunction = (
|
||||||
|
applicable: string,
|
||||||
|
value: any,
|
||||||
|
prompt: DeepPartial<PromptConfig>
|
||||||
|
) => string;
|
||||||
|
|
||||||
|
type PromptResolverFunction = (
|
||||||
|
key: string,
|
||||||
|
applicable: string,
|
||||||
|
value: any,
|
||||||
|
prompt?: DeepPartial<PromptConfig>
|
||||||
|
) => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts more contexte for each type-enum.
|
||||||
|
* IDEA: replicate the concept for scopes and refactor to a generic feature.
|
||||||
|
*/
|
||||||
|
const getTypeRuleExtraDescription = (
|
||||||
|
type: string,
|
||||||
|
prompt?: DeepPartial<PromptConfig>
|
||||||
|
) => prompt?.questions?.type?.enum?.[type]?.description;
|
||||||
|
|
||||||
|
/*
|
||||||
|
IDEA: Compress llm readable prompt for each section of commit message: one line for header, one line for scope, etc.
|
||||||
|
- The type must be in lowercase and should be one of the following values: featuring, fixing, documenting, styling, refactoring, testing, chores, perf, build, ci, revert.
|
||||||
|
- The scope should not be empty and provide context for the change (e.g., module or file changed).
|
||||||
|
- The subject should not be empty, should not end with a period, and should provide a concise description of the change. It should not be in sentence-case, start-case, pascal-case, or upper-case.
|
||||||
|
*/
|
||||||
|
const llmReadableRules: {
|
||||||
|
[ruleName: string]: PromptResolverFunction;
|
||||||
|
} = {
|
||||||
|
blankline: (key, applicable) =>
|
||||||
|
`There should ${applicable} be a blank line at the beginning of the ${key}.`,
|
||||||
|
caseRule: (key, applicable, value: string | Array<string>) =>
|
||||||
|
`The ${key} should ${applicable} be in ${
|
||||||
|
Array.isArray(value)
|
||||||
|
? `one of the following case:
|
||||||
|
- ${value.join('\n - ')}.`
|
||||||
|
: `${value} case.`
|
||||||
|
}`,
|
||||||
|
emptyRule: (key, applicable) => `The ${key} should ${applicable} be empty.`,
|
||||||
|
enumRule: (key, applicable, value: string | Array<string>) =>
|
||||||
|
`The ${key} should ${applicable} be one of the following values:
|
||||||
|
- ${Array.isArray(value) ? value.join('\n - ') : value}.`,
|
||||||
|
enumTypeRule: (key, applicable, value: string | Array<string>, prompt) =>
|
||||||
|
`The ${key} should ${applicable} be one of the following values:
|
||||||
|
- ${
|
||||||
|
Array.isArray(value)
|
||||||
|
? value
|
||||||
|
.map((v) => {
|
||||||
|
const description = getTypeRuleExtraDescription(v, prompt);
|
||||||
|
if (description) {
|
||||||
|
return `${v} (${description})`;
|
||||||
|
} else return v;
|
||||||
|
})
|
||||||
|
.join('\n - ')
|
||||||
|
: value
|
||||||
|
}.`,
|
||||||
|
fullStopRule: (key, applicable, value: string) =>
|
||||||
|
`The ${key} should ${applicable} end with '${value}'.`,
|
||||||
|
maxLengthRule: (key, applicable, value: string) =>
|
||||||
|
`The ${key} should ${applicable} have ${value} characters or less.`,
|
||||||
|
minLengthRule: (key, applicable, value: string) =>
|
||||||
|
`The ${key} should ${applicable} have ${value} characters or more.`
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Validate rules to every rule in the @commitlint configuration.
|
||||||
|
* IDEA: Plugins can extend the list of rule. Provide user with a way to infer or extend when "No prompt handler for rule".
|
||||||
|
*/
|
||||||
|
const rulesPrompts: {
|
||||||
|
[ruleName: string]: PromptFunction;
|
||||||
|
} = {
|
||||||
|
'body-case': (applicable: string, value: string | Array<string>) =>
|
||||||
|
llmReadableRules.caseRule('body', applicable, value),
|
||||||
|
'body-empty': (applicable: string) =>
|
||||||
|
llmReadableRules.emptyRule('body', applicable, undefined),
|
||||||
|
'body-full-stop': (applicable: string, value: string) =>
|
||||||
|
llmReadableRules.fullStopRule('body', applicable, value),
|
||||||
|
'body-leading-blank': (applicable: string) =>
|
||||||
|
llmReadableRules.blankline('body', applicable, undefined),
|
||||||
|
'body-max-length': (applicable: string, value: string) =>
|
||||||
|
llmReadableRules.maxLengthRule('body', applicable, value),
|
||||||
|
'body-max-line-length': (applicable: string, value: string) =>
|
||||||
|
`Each line of the body should ${applicable} have ${value} characters or less.`,
|
||||||
|
'body-min-length': (applicable: string, value: string) =>
|
||||||
|
llmReadableRules.minLengthRule('body', applicable, value),
|
||||||
|
'footer-case': (applicable: string, value: string | Array<string>) =>
|
||||||
|
llmReadableRules.caseRule('footer', applicable, value),
|
||||||
|
'footer-empty': (applicable: string) =>
|
||||||
|
llmReadableRules.emptyRule('footer', applicable, undefined),
|
||||||
|
'footer-leading-blank': (applicable: string) =>
|
||||||
|
llmReadableRules.blankline('footer', applicable, undefined),
|
||||||
|
'footer-max-length': (applicable: string, value: string) =>
|
||||||
|
llmReadableRules.maxLengthRule('footer', applicable, value),
|
||||||
|
'footer-max-line-length': (applicable: string, value: string) =>
|
||||||
|
`Each line of the footer should ${applicable} have ${value} characters or less.`,
|
||||||
|
'footer-min-length': (applicable: string, value: string) =>
|
||||||
|
llmReadableRules.minLengthRule('footer', applicable, value),
|
||||||
|
'header-case': (applicable: string, value: string | Array<string>) =>
|
||||||
|
llmReadableRules.caseRule('header', applicable, value),
|
||||||
|
'header-full-stop': (applicable: string, value: string) =>
|
||||||
|
llmReadableRules.fullStopRule('header', applicable, value),
|
||||||
|
'header-max-length': (applicable: string, value: string) =>
|
||||||
|
llmReadableRules.maxLengthRule('header', applicable, value),
|
||||||
|
'header-min-length': (applicable: string, value: string) =>
|
||||||
|
llmReadableRules.minLengthRule('header', applicable, value),
|
||||||
|
'references-empty': (applicable: string) =>
|
||||||
|
llmReadableRules.emptyRule('references section', applicable, undefined),
|
||||||
|
'scope-case': (applicable: string, value: string | Array<string>) =>
|
||||||
|
llmReadableRules.caseRule('scope', applicable, value),
|
||||||
|
'scope-empty': (applicable: string) =>
|
||||||
|
llmReadableRules.emptyRule('scope', applicable, undefined),
|
||||||
|
'scope-enum': (applicable: string, value: string | Array<string>) =>
|
||||||
|
llmReadableRules.enumRule('type', applicable, value),
|
||||||
|
'scope-max-length': (applicable: string, value: string) =>
|
||||||
|
llmReadableRules.maxLengthRule('scope', applicable, value),
|
||||||
|
'scope-min-length': (applicable: string, value: string) =>
|
||||||
|
llmReadableRules.minLengthRule('scope', applicable, value),
|
||||||
|
'signed-off-by': (applicable: string, value: string) =>
|
||||||
|
`The commit message should ${applicable} have a "Signed-off-by" line with the value "${value}".`,
|
||||||
|
'subject-case': (applicable: string, value: string | Array<string>) =>
|
||||||
|
llmReadableRules.caseRule('subject', applicable, value),
|
||||||
|
'subject-empty': (applicable: string) =>
|
||||||
|
llmReadableRules.emptyRule('subject', applicable, undefined),
|
||||||
|
'subject-full-stop': (applicable: string, value: string) =>
|
||||||
|
llmReadableRules.fullStopRule('subject', applicable, value),
|
||||||
|
'subject-max-length': (applicable: string, value: string) =>
|
||||||
|
llmReadableRules.maxLengthRule('subject', applicable, value),
|
||||||
|
'subject-min-length': (applicable: string, value: string) =>
|
||||||
|
llmReadableRules.minLengthRule('subject', applicable, value),
|
||||||
|
'type-case': (applicable: string, value: string | Array<string>) =>
|
||||||
|
llmReadableRules.caseRule('type', applicable, value),
|
||||||
|
'type-empty': (applicable: string) =>
|
||||||
|
llmReadableRules.emptyRule('type', applicable, undefined),
|
||||||
|
'type-enum': (applicable: string, value: string | Array<string>, prompt) =>
|
||||||
|
llmReadableRules.enumTypeRule('type', applicable, value, prompt),
|
||||||
|
'type-max-length': (applicable: string, value: string) =>
|
||||||
|
llmReadableRules.maxLengthRule('type', applicable, value),
|
||||||
|
'type-min-length': (applicable: string, value: string) =>
|
||||||
|
llmReadableRules.minLengthRule('type', applicable, value)
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPrompt = (
|
||||||
|
ruleName: string,
|
||||||
|
ruleConfig: RuleConfigTuple<unknown>,
|
||||||
|
prompt: DeepPartial<PromptConfig>
|
||||||
|
) => {
|
||||||
|
const [severity, applicable, value] = ruleConfig;
|
||||||
|
|
||||||
|
// Should we exclude "Disabled" properties?
|
||||||
|
// Is this used to disable a subjacent rule when extending presets?
|
||||||
|
if (severity === RuleConfigSeverity.Disabled) return null;
|
||||||
|
|
||||||
|
const promptFn = rulesPrompts[ruleName];
|
||||||
|
if (promptFn) {
|
||||||
|
return promptFn(applicable, value, prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugins may add their custom rules.
|
||||||
|
// We might want to call OpenAI to build this rule's llm-readable prompt.
|
||||||
|
outro(`${chalk.red('✖')} No prompt handler for rule "${ruleName}".`);
|
||||||
|
return `Please manualy set the prompt for rule "${ruleName}".`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inferPromptsFromCommitlintConfig = (
|
||||||
|
config: QualifiedConfig
|
||||||
|
): string[] => {
|
||||||
|
const { rules, prompt } = config;
|
||||||
|
if (!rules) return [];
|
||||||
|
return Object.keys(rules)
|
||||||
|
.map((ruleName) =>
|
||||||
|
getPrompt(ruleName, rules[ruleName] as RuleConfigTuple<unknown>, prompt)
|
||||||
|
)
|
||||||
|
.filter((prompt) => prompt !== null) as string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Breaking down commit message structure for conventional commit, and mapping bits with
|
||||||
|
* ubiquitous language from @commitlint.
|
||||||
|
* While gpt-4 does this on it self, gpt-3.5 can't map this on his own atm.
|
||||||
|
*/
|
||||||
|
const STRUCTURE_OF_COMMIT = config.OCO_OMIT_SCOPE
|
||||||
|
? `
|
||||||
|
- Header of commit is composed of type and subject: <type-of-commit>: <subject-of-commit>
|
||||||
|
- Description of commit is composed of body and footer (optional): <body-of-commit>\n<footer(s)-of-commit>`
|
||||||
|
: `
|
||||||
|
- Header of commit is composed of type, scope, subject: <type-of-commit>(<scope-of-commit>): <subject-of-commit>
|
||||||
|
- Description of commit is composed of body and footer (optional): <body-of-commit>\n<footer(s)-of-commit>`;
|
||||||
|
|
||||||
|
// Prompt to generate LLM-readable rules based on @commitlint rules.
|
||||||
|
const GEN_COMMITLINT_CONSISTENCY_PROMPT = (
|
||||||
|
prompts: string[]
|
||||||
|
): OpenAI.Chat.Completions.ChatCompletionMessageParam[] => [
|
||||||
|
{
|
||||||
|
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:
|
||||||
|
|
||||||
|
Commit Message Conventions:
|
||||||
|
- The commit message consists of three parts: Header, Body, and Footer.
|
||||||
|
- Header:
|
||||||
|
- Format: ${
|
||||||
|
config.OCO_OMIT_SCOPE
|
||||||
|
? '`<type>: <subject>`'
|
||||||
|
: '`<type>(<scope>): <subject>`'
|
||||||
|
}
|
||||||
|
- ${prompts.join('\n- ')}
|
||||||
|
|
||||||
|
JSON Output Format:
|
||||||
|
- The JSON output should contain the commit messages for a bug fix and a new feature in the following format:
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"localLanguage": "${translation.localLanguage}",
|
||||||
|
"commitFix": "<Header of commit for bug fix with scope>",
|
||||||
|
"commitFeat": "<Header of commit for feature with scope>",
|
||||||
|
"commitFixOmitScope": "<Header of commit for bug fix without scope>",
|
||||||
|
"commitFeatOmitScope": "<Header of commit for feature without scope>",
|
||||||
|
"commitDescription": "<Description of commit for both the bug fix and the feature>"
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
- The "commitDescription" should not include the commit message's header, only the description.
|
||||||
|
- Description should not be more than 74 characters.
|
||||||
|
|
||||||
|
Additional Details:
|
||||||
|
- Changing the variable 'port' to uppercase 'PORT' is considered a bug fix.
|
||||||
|
- Allowing the server to listen on a port specified through the environment variable is considered a new feature.
|
||||||
|
|
||||||
|
Example Git Diff is to follow:`
|
||||||
|
},
|
||||||
|
INIT_DIFF_PROMPT
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt to have LLM generate a message using @commitlint rules.
|
||||||
|
*
|
||||||
|
* @param language
|
||||||
|
* @param prompts
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const INIT_MAIN_PROMPT = (
|
||||||
|
language: string,
|
||||||
|
prompts: string[]
|
||||||
|
): 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.'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
${
|
||||||
|
config.OCO_OMIT_SCOPE
|
||||||
|
? 'Do not include a scope in the commit message format. Use the format: <type>: <subject>'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
You will strictly follow the following conventions to generate the content of the commit message:
|
||||||
|
- ${prompts.join('\n- ')}
|
||||||
|
|
||||||
|
The conventions refers to the following structure of commit message:
|
||||||
|
${STRUCTURE_OF_COMMIT}`
|
||||||
|
});
|
||||||
|
|
||||||
|
export const commitlintPrompts = {
|
||||||
|
INIT_MAIN_PROMPT,
|
||||||
|
GEN_COMMITLINT_CONSISTENCY_PROMPT
|
||||||
|
};
|
||||||
74
src/modules/commitlint/pwd-commitlint.ts
Normal file
74
src/modules/commitlint/pwd-commitlint.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const findModulePath = (moduleName: string) => {
|
||||||
|
const searchPaths = [
|
||||||
|
path.join('node_modules', moduleName),
|
||||||
|
path.join('node_modules', '.pnpm'),
|
||||||
|
path.resolve(__dirname, '../..')
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const basePath of searchPaths) {
|
||||||
|
try {
|
||||||
|
const resolvedPath = require.resolve(moduleName, { paths: [basePath] });
|
||||||
|
return resolvedPath;
|
||||||
|
} catch {
|
||||||
|
// Continue to the next search path if the module is not found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Cannot find module ${moduleName}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCommitLintModuleType = async (): Promise<'cjs' | 'esm'> => {
|
||||||
|
const 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
|
||||||
|
* directory (`process.env.PWD`) by requiring the `load` module from the `@commitlint` package.
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
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 = findModulePath('@commitlint/load/lib/load.js');
|
||||||
|
load = (await import(modulePath)).default;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (load && typeof load === 'function') {
|
||||||
|
return await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// @commitlint/load is not a function
|
||||||
|
return null;
|
||||||
|
};
|
||||||
11
src/modules/commitlint/types.ts
Normal file
11
src/modules/commitlint/types.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { i18n } from '../../i18n';
|
||||||
|
|
||||||
|
export type ConsistencyPrompt = (typeof i18n)[keyof typeof i18n];
|
||||||
|
|
||||||
|
export type CommitlintLLMConfig = {
|
||||||
|
hash: string;
|
||||||
|
prompts: string[];
|
||||||
|
consistency: {
|
||||||
|
[key: string]: ConsistencyPrompt;
|
||||||
|
};
|
||||||
|
};
|
||||||
57
src/modules/commitlint/utils.ts
Normal file
57
src/modules/commitlint/utils.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import fs from 'fs/promises';
|
||||||
|
|
||||||
|
import { COMMITLINT_LLM_CONFIG_PATH } from './constants';
|
||||||
|
import { CommitlintLLMConfig } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the "\n" only if occurring twice
|
||||||
|
*/
|
||||||
|
export const removeDoubleNewlines = (input: string): string => {
|
||||||
|
const pattern = /\\n\\n/g;
|
||||||
|
if (pattern.test(input)) {
|
||||||
|
const newInput = input.replace(pattern, '');
|
||||||
|
return removeDoubleNewlines(newInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
await fs.access(COMMITLINT_LLM_CONFIG_PATH);
|
||||||
|
exists = true;
|
||||||
|
} catch (e) {
|
||||||
|
exists = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exists;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const writeCommitlintLLMConfig = async (
|
||||||
|
commitlintLLMConfig: CommitlintLLMConfig
|
||||||
|
): Promise<void> => {
|
||||||
|
await fs.writeFile(
|
||||||
|
COMMITLINT_LLM_CONFIG_PATH,
|
||||||
|
JSON.stringify(commitlintLLMConfig, null, 2)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCommitlintLLMConfig =
|
||||||
|
async (): Promise<CommitlintLLMConfig> => {
|
||||||
|
const content = await fs.readFile(COMMITLINT_LLM_CONFIG_PATH);
|
||||||
|
const commitLintLLMConfig = JSON.parse(
|
||||||
|
content.toString()
|
||||||
|
) as CommitlintLLMConfig;
|
||||||
|
return commitLintLLMConfig;
|
||||||
|
};
|
||||||
265
src/prompts.ts
Normal file
265
src/prompts.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
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'];
|
||||||
|
|
||||||
|
export const IDENTITY =
|
||||||
|
'You are to act as an author of a commit message in git.';
|
||||||
|
|
||||||
|
const GITMOJI_HELP = `Use GitMoji convention to preface the commit. Here are some help to choose the right emoji (emoji, description):
|
||||||
|
🐛, Fix a bug;
|
||||||
|
✨, Introduce new features;
|
||||||
|
📝, Add or update documentation;
|
||||||
|
🚀, Deploy stuff;
|
||||||
|
✅, Add, update, or pass tests;
|
||||||
|
♻️, Refactor code;
|
||||||
|
⬆️, Upgrade dependencies;
|
||||||
|
🔧, Add or update configuration files;
|
||||||
|
🌐, Internationalization and localization;
|
||||||
|
💡, Add or update comments in source code;`;
|
||||||
|
|
||||||
|
const FULL_GITMOJI_SPEC = `${GITMOJI_HELP}
|
||||||
|
🎨, Improve structure / format of the code;
|
||||||
|
⚡️, Improve performance;
|
||||||
|
🔥, Remove code or files;
|
||||||
|
🚑️, Critical hotfix;
|
||||||
|
💄, Add or update the UI and style files;
|
||||||
|
🎉, Begin a project;
|
||||||
|
🔒️, Fix security issues;
|
||||||
|
🔐, Add or update secrets;
|
||||||
|
🔖, Release / Version tags;
|
||||||
|
🚨, Fix compiler / linter warnings;
|
||||||
|
🚧, Work in progress;
|
||||||
|
💚, Fix CI Build;
|
||||||
|
⬇️, Downgrade dependencies;
|
||||||
|
📌, Pin dependencies to specific versions;
|
||||||
|
👷, Add or update CI build system;
|
||||||
|
📈, Add or update analytics or track code;
|
||||||
|
➕, Add a dependency;
|
||||||
|
➖, Remove a dependency;
|
||||||
|
🔨, Add or update development scripts;
|
||||||
|
✏️, Fix typos;
|
||||||
|
💩, Write bad code that needs to be improved;
|
||||||
|
⏪️, Revert changes;
|
||||||
|
🔀, Merge branches;
|
||||||
|
📦️, Add or update compiled files or packages;
|
||||||
|
👽️, Update code due to external API changes;
|
||||||
|
🚚, Move or rename resources (e.g.: files, paths, routes);
|
||||||
|
📄, Add or update license;
|
||||||
|
💥, Introduce breaking changes;
|
||||||
|
🍱, Add or update assets;
|
||||||
|
♿️, Improve accessibility;
|
||||||
|
🍻, Write code drunkenly;
|
||||||
|
💬, Add or update text and literals;
|
||||||
|
🗃️, Perform database related changes;
|
||||||
|
🔊, Add or update logs;
|
||||||
|
🔇, Remove logs;
|
||||||
|
👥, Add or update contributor(s);
|
||||||
|
🚸, Improve user experience / usability;
|
||||||
|
🏗️, Make architectural changes;
|
||||||
|
📱, Work on responsive design;
|
||||||
|
🤡, Mock things;
|
||||||
|
🥚, Add or update an easter egg;
|
||||||
|
🙈, Add or update a .gitignore file;
|
||||||
|
📸, Add or update snapshots;
|
||||||
|
⚗️, Perform experiments;
|
||||||
|
🔍️, Improve SEO;
|
||||||
|
🏷️, Add or update types;
|
||||||
|
🌱, Add or update seed files;
|
||||||
|
🚩, Add, update, or remove feature flags;
|
||||||
|
🥅, Catch errors;
|
||||||
|
💫, Add or update animations and transitions;
|
||||||
|
🗑️, Deprecate code that needs to be cleaned up;
|
||||||
|
🛂, Work on code related to authorization, roles and permissions;
|
||||||
|
🩹, Simple fix for a non-critical issue;
|
||||||
|
🧐, Data exploration/inspection;
|
||||||
|
⚰️, Remove dead code;
|
||||||
|
🧪, Add a failing test;
|
||||||
|
👔, Add or update business logic;
|
||||||
|
🩺, Add or update healthcheck;
|
||||||
|
🧱, Infrastructure related changes;
|
||||||
|
🧑💻, Improve developer experience;
|
||||||
|
💸, Add sponsorships or money related infrastructure;
|
||||||
|
🧵, Add or update code related to multithreading or concurrency;
|
||||||
|
🦺, Add or update code related to validation.`;
|
||||||
|
|
||||||
|
const CONVENTIONAL_COMMIT_KEYWORDS =
|
||||||
|
'Do not preface the commit with anything, except for the conventional commit keywords: fix, feat, build, chore, ci, docs, style, refactor, perf, test.';
|
||||||
|
|
||||||
|
const getCommitConvention = (fullGitMojiSpec: boolean) =>
|
||||||
|
config.OCO_EMOJI
|
||||||
|
? fullGitMojiSpec
|
||||||
|
? FULL_GITMOJI_SPEC
|
||||||
|
: GITMOJI_HELP
|
||||||
|
: CONVENTIONAL_COMMIT_KEYWORDS;
|
||||||
|
|
||||||
|
const getDescriptionInstruction = () =>
|
||||||
|
config.OCO_DESCRIPTION
|
||||||
|
? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.'
|
||||||
|
: "Don't add any descriptions to the commit, only commit message.";
|
||||||
|
|
||||||
|
const getOneLineCommitInstruction = () =>
|
||||||
|
config.OCO_ONE_LINE_COMMIT
|
||||||
|
? 'Craft a concise, single sentence, commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in one single message.'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const getScopeInstruction = () =>
|
||||||
|
config.OCO_OMIT_SCOPE
|
||||||
|
? 'Do not include a scope in the commit message format. Use the format: <type>: <subject>'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the context of the user input
|
||||||
|
* @param extraArgs - The arguments passed to the command line
|
||||||
|
* @example
|
||||||
|
* $ oco -- This is a context used to generate the commit message
|
||||||
|
* @returns - The context of the user input
|
||||||
|
*/
|
||||||
|
const userInputCodeContext = (context: string) => {
|
||||||
|
if (context !== '' && context !== ' ') {
|
||||||
|
return `Additional context provided by the user: <context>${context}</context>\nConsider this context when generating the commit message, incorporating relevant information when appropriate.`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const INIT_MAIN_PROMPT = (
|
||||||
|
language: string,
|
||||||
|
fullGitMojiSpec: boolean,
|
||||||
|
context: string
|
||||||
|
): 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 scopeInstruction = getScopeInstruction();
|
||||||
|
const generalGuidelines = `Use the present tense. Lines must not be longer than 74 characters. Use ${language} for the commit message.`;
|
||||||
|
const userInputContext = userInputCodeContext(context);
|
||||||
|
|
||||||
|
return `${missionStatement}\n${diffInstruction}\n${conventionGuidelines}\n${descriptionGuideline}\n${oneLineCommitGuideline}\n${scopeInstruction}\n${generalGuidelines}\n${userInputContext}`;
|
||||||
|
})()
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
@@ -10,7 +10,7 @@
|
||||||
|
import {
|
||||||
|
initWinstonLogger();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
-const port = 7799;
|
||||||
|
+const PORT = 7799;
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
@@ -34,6 +34,6 @@
|
||||||
|
app.use((_, res, next) => {
|
||||||
|
// ROUTES
|
||||||
|
app.use(PROTECTED_ROUTER_URL, protectedRouter);
|
||||||
|
|
||||||
|
-app.listen(port, () => {
|
||||||
|
- console.log(\`Server listening on port \${port}\`);
|
||||||
|
+app.listen(process.env.PORT || PORT, () => {
|
||||||
|
+ console.log(\`Server listening on port \${PORT}\`);
|
||||||
|
});`
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMMIT_TYPES = {
|
||||||
|
fix: '🐛',
|
||||||
|
feat: '✨'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const generateCommitString = (
|
||||||
|
type: keyof typeof COMMIT_TYPES,
|
||||||
|
message: string
|
||||||
|
): string => {
|
||||||
|
const cleanMessage = removeConventionalCommitWord(message);
|
||||||
|
return config.OCO_EMOJI ? `${COMMIT_TYPES[type]} ${cleanMessage}` : message;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConsistencyContent = (translation: ConsistencyPrompt) => {
|
||||||
|
const fixMessage =
|
||||||
|
config.OCO_OMIT_SCOPE && translation.commitFixOmitScope
|
||||||
|
? translation.commitFixOmitScope
|
||||||
|
: translation.commitFix;
|
||||||
|
|
||||||
|
const featMessage =
|
||||||
|
config.OCO_OMIT_SCOPE && translation.commitFeatOmitScope
|
||||||
|
? translation.commitFeatOmitScope
|
||||||
|
: translation.commitFeat;
|
||||||
|
|
||||||
|
const fix = generateCommitString('fix', fixMessage);
|
||||||
|
const feat = config.OCO_ONE_LINE_COMMIT
|
||||||
|
? ''
|
||||||
|
: generateCommitString('feat', featMessage);
|
||||||
|
|
||||||
|
const description = config.OCO_DESCRIPTION
|
||||||
|
? translation.commitDescription
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return [fix, feat, description].filter(Boolean).join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
const INIT_CONSISTENCY_PROMPT = (
|
||||||
|
translation: ConsistencyPrompt
|
||||||
|
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
|
||||||
|
role: 'assistant',
|
||||||
|
content: getConsistencyContent(translation)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getMainCommitPrompt = async (
|
||||||
|
fullGitMojiSpec: boolean,
|
||||||
|
context: string
|
||||||
|
): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => {
|
||||||
|
switch (config.OCO_PROMPT_MODULE) {
|
||||||
|
case '@commitlint':
|
||||||
|
if (!(await utils.commitlintLLMConfigExists())) {
|
||||||
|
note(
|
||||||
|
`OCO_PROMPT_MODULE is @commitlint but you haven't generated consistency for this project yet.`
|
||||||
|
);
|
||||||
|
await configureCommitlintIntegration();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace example prompt with a prompt that's generated by OpenAI for the commitlint config.
|
||||||
|
const commitLintConfig = await utils.getCommitlintLLMConfig();
|
||||||
|
|
||||||
|
return [
|
||||||
|
commitlintPrompts.INIT_MAIN_PROMPT(
|
||||||
|
translation.localLanguage,
|
||||||
|
commitLintConfig.prompts
|
||||||
|
),
|
||||||
|
INIT_DIFF_PROMPT,
|
||||||
|
INIT_CONSISTENCY_PROMPT(
|
||||||
|
commitLintConfig.consistency[
|
||||||
|
translation.localLanguage
|
||||||
|
] as ConsistencyPrompt
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
default:
|
||||||
|
return [
|
||||||
|
INIT_MAIN_PROMPT(translation.localLanguage, fullGitMojiSpec, context),
|
||||||
|
INIT_DIFF_PROMPT,
|
||||||
|
INIT_CONSISTENCY_PROMPT(translation)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { getOpenCommitLatestVersion } from '../api';
|
|
||||||
import currentPackage from '../../package.json' assert { type: 'json' };
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
import { outro } from '@clack/prompts';
|
||||||
|
|
||||||
|
import currentPackage from '../../package.json';
|
||||||
|
import { getOpenCommitLatestVersion } from '../version';
|
||||||
|
|
||||||
export const checkIsLatestVersion = async () => {
|
export const checkIsLatestVersion = async () => {
|
||||||
const latestVersion = await getOpenCommitLatestVersion();
|
const latestVersion = await getOpenCommitLatestVersion();
|
||||||
|
|
||||||
@@ -9,7 +12,7 @@ export const checkIsLatestVersion = async () => {
|
|||||||
const currentVersion = currentPackage.version;
|
const currentVersion = currentPackage.version;
|
||||||
|
|
||||||
if (currentVersion !== latestVersion) {
|
if (currentVersion !== latestVersion) {
|
||||||
console.warn(
|
outro(
|
||||||
chalk.yellow(
|
chalk.yellow(
|
||||||
`
|
`
|
||||||
You are not using the latest stable version of OpenCommit with new features and bug fixes.
|
You are not using the latest stable version of OpenCommit with new features and bug fixes.
|
||||||
|
|||||||
94
src/utils/engine.ts
Normal file
94
src/utils/engine.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
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 { FlowiseEngine } from '../engine/flowise';
|
||||||
|
import { GeminiEngine } from '../engine/gemini';
|
||||||
|
import { OllamaEngine } from '../engine/ollama';
|
||||||
|
import { OpenAiEngine } from '../engine/openAi';
|
||||||
|
import { MistralAiEngine } from '../engine/mistral';
|
||||||
|
import { TestAi, TestMockType } from '../engine/testAi';
|
||||||
|
import { GroqEngine } from '../engine/groq';
|
||||||
|
import { MLXEngine } from '../engine/mlx';
|
||||||
|
import { DeepseekEngine } from '../engine/deepseek';
|
||||||
|
import { AimlApiEngine } from '../engine/aimlapi';
|
||||||
|
import { OpenRouterEngine } from '../engine/openrouter';
|
||||||
|
|
||||||
|
export function parseCustomHeaders(headers: any): Record<string, string> {
|
||||||
|
let parsedHeaders = {};
|
||||||
|
|
||||||
|
if (!headers) {
|
||||||
|
return parsedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof headers === 'object' && !Array.isArray(headers)) {
|
||||||
|
parsedHeaders = headers;
|
||||||
|
} else {
|
||||||
|
parsedHeaders = JSON.parse(headers);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
'Invalid OCO_API_CUSTOM_HEADERS format, ignoring custom headers'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEngine(): AiEngine {
|
||||||
|
const config = getConfig();
|
||||||
|
const provider = config.OCO_AI_PROVIDER;
|
||||||
|
|
||||||
|
const customHeaders = parseCustomHeaders(config.OCO_API_CUSTOM_HEADERS);
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG = {
|
||||||
|
model: config.OCO_MODEL!,
|
||||||
|
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
|
||||||
|
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
|
||||||
|
baseURL: config.OCO_API_URL!,
|
||||||
|
apiKey: config.OCO_API_KEY!,
|
||||||
|
customHeaders
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (provider) {
|
||||||
|
case OCO_AI_PROVIDER_ENUM.OLLAMA:
|
||||||
|
return new OllamaEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.ANTHROPIC:
|
||||||
|
return new AnthropicEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.TEST:
|
||||||
|
return new TestAi(config.OCO_TEST_MOCK_TYPE as TestMockType);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.GEMINI:
|
||||||
|
return new GeminiEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.AZURE:
|
||||||
|
return new AzureEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.FLOWISE:
|
||||||
|
return new FlowiseEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.GROQ:
|
||||||
|
return new GroqEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.MISTRAL:
|
||||||
|
return new MistralAiEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.MLX:
|
||||||
|
return new MLXEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.DEEPSEEK:
|
||||||
|
return new DeepseekEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.AIMLAPI:
|
||||||
|
return new AimlApiEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
case OCO_AI_PROVIDER_ENUM.OPENROUTER:
|
||||||
|
return new OpenRouterEngine(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return new OpenAiEngine(DEFAULT_CONFIG);
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/utils/git.ts
102
src/utils/git.ts
@@ -1,7 +1,8 @@
|
|||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
import { outro, spinner } from '@clack/prompts';
|
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import ignore, { Ignore } from 'ignore';
|
import ignore, { Ignore } from 'ignore';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { outro, spinner } from '@clack/prompts';
|
||||||
|
|
||||||
export const assertGitRepo = async () => {
|
export const assertGitRepo = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -15,43 +16,44 @@ export const assertGitRepo = async () => {
|
|||||||
// (file) => `:(exclude)${file}`
|
// (file) => `:(exclude)${file}`
|
||||||
// );
|
// );
|
||||||
|
|
||||||
export const getOpenCommitIgnore = (): Ignore => {
|
export const getOpenCommitIgnore = async (): Promise<Ignore> => {
|
||||||
|
const gitDir = await getGitDir();
|
||||||
|
|
||||||
const ig = ignore();
|
const ig = ignore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ig.add(readFileSync('.opencommitignore').toString().split('\n'));
|
ig.add(
|
||||||
|
readFileSync(join(gitDir, '.opencommitignore')).toString().split('\n')
|
||||||
|
);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
return ig;
|
return ig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCoreHooksPath = async(): Promise<string> => {
|
export const getCoreHooksPath = async (): Promise<string> => {
|
||||||
const { stdout } = await execa('git', [
|
const gitDir = await getGitDir();
|
||||||
'config',
|
|
||||||
'core.hooksPath']);
|
const { stdout } = await execa('git', ['config', 'core.hooksPath'], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getStagedFiles = async (): Promise<string[]> => {
|
export const getStagedFiles = async (): Promise<string[]> => {
|
||||||
const { stdout: gitDir } = await execa('git', [
|
const gitDir = await getGitDir();
|
||||||
'rev-parse',
|
|
||||||
'--show-toplevel'
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { stdout: files } = await execa('git', [
|
const { stdout: files } = await execa(
|
||||||
'diff',
|
'git',
|
||||||
'--name-only',
|
['diff', '--name-only', '--cached', '--relative'],
|
||||||
'--cached',
|
{ cwd: gitDir }
|
||||||
'--relative',
|
);
|
||||||
gitDir
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!files) return [];
|
if (!files) return [];
|
||||||
|
|
||||||
const filesList = files.split('\n');
|
const filesList = files.split('\n');
|
||||||
|
|
||||||
const ig = getOpenCommitIgnore();
|
const ig = await getOpenCommitIgnore();
|
||||||
const allowedFiles = filesList.filter((file) => !ig.ignores(file));
|
const allowedFiles = filesList.filter((file) => !ig.ignores(file));
|
||||||
|
|
||||||
if (!allowedFiles) return [];
|
if (!allowedFiles) return [];
|
||||||
@@ -60,12 +62,17 @@ export const getStagedFiles = async (): Promise<string[]> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getChangedFiles = async (): Promise<string[]> => {
|
export const getChangedFiles = async (): Promise<string[]> => {
|
||||||
const { stdout: modified } = await execa('git', ['ls-files', '--modified']);
|
const gitDir = await getGitDir();
|
||||||
const { stdout: others } = await execa('git', [
|
|
||||||
'ls-files',
|
const { stdout: modified } = await execa('git', ['ls-files', '--modified'], {
|
||||||
'--others',
|
cwd: gitDir
|
||||||
'--exclude-standard'
|
});
|
||||||
]);
|
|
||||||
|
const { stdout: others } = await execa(
|
||||||
|
'git',
|
||||||
|
['ls-files', '--others', '--exclude-standard'],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
|
||||||
const files = [...modified.split('\n'), ...others.split('\n')].filter(
|
const files = [...modified.split('\n'), ...others.split('\n')].filter(
|
||||||
(file) => !!file
|
(file) => !!file
|
||||||
@@ -75,20 +82,35 @@ export const getChangedFiles = async (): Promise<string[]> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const gitAdd = async ({ files }: { files: string[] }) => {
|
export const gitAdd = async ({ files }: { files: string[] }) => {
|
||||||
|
const gitDir = await getGitDir();
|
||||||
|
|
||||||
const gitAddSpinner = spinner();
|
const gitAddSpinner = spinner();
|
||||||
|
|
||||||
gitAddSpinner.start('Adding files to commit');
|
gitAddSpinner.start('Adding files to commit');
|
||||||
await execa('git', ['add', ...files]);
|
|
||||||
gitAddSpinner.stop('Done');
|
await execa('git', ['add', ...files], { cwd: gitDir });
|
||||||
|
|
||||||
|
gitAddSpinner.stop(`Staged ${files.length} files`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDiff = async ({ files }: { files: string[] }) => {
|
export const getDiff = async ({ files }: { files: string[] }) => {
|
||||||
|
const gitDir = await getGitDir();
|
||||||
|
|
||||||
const lockFiles = files.filter(
|
const lockFiles = files.filter(
|
||||||
(file) => file.includes('.lock') || file.includes('-lock.')
|
(file) =>
|
||||||
|
file.includes('.lock') ||
|
||||||
|
file.includes('-lock.') ||
|
||||||
|
file.includes('.svg') ||
|
||||||
|
file.includes('.png') ||
|
||||||
|
file.includes('.jpg') ||
|
||||||
|
file.includes('.jpeg') ||
|
||||||
|
file.includes('.webp') ||
|
||||||
|
file.includes('.gif')
|
||||||
);
|
);
|
||||||
|
|
||||||
if (lockFiles.length) {
|
if (lockFiles.length) {
|
||||||
outro(
|
outro(
|
||||||
`Some files are '.lock' files which are excluded by default from 'git diff'. No commit messages are generated for this files:\n${lockFiles.join(
|
`Some files are excluded by default from 'git diff'. No commit messages are generated for this files:\n${lockFiles.join(
|
||||||
'\n'
|
'\n'
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
@@ -98,12 +120,20 @@ export const getDiff = async ({ files }: { files: string[] }) => {
|
|||||||
(file) => !file.includes('.lock') && !file.includes('-lock.')
|
(file) => !file.includes('.lock') && !file.includes('-lock.')
|
||||||
);
|
);
|
||||||
|
|
||||||
const { stdout: diff } = await execa('git', [
|
const { stdout: diff } = await execa(
|
||||||
'diff',
|
'git',
|
||||||
'--staged',
|
['diff', '--staged', '--', ...filesWithoutLocks],
|
||||||
'--',
|
{ cwd: gitDir }
|
||||||
...filesWithoutLocks
|
);
|
||||||
]);
|
|
||||||
|
|
||||||
return diff;
|
return diff;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getGitDir = async (): Promise<string> => {
|
||||||
|
const { stdout: gitDir } = await execa('git', [
|
||||||
|
'rev-parse',
|
||||||
|
'--show-toplevel'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return gitDir;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { tokenCount } from './tokenCount';
|
import { tokenCount } from './tokenCount';
|
||||||
|
|
||||||
export function mergeDiffs(arr: string[], maxStringLength: number): string[] {
|
export function mergeDiffs(arr: string[], maxStringLength: number): string[] {
|
||||||
const mergedArr: string[] = [];
|
const mergedArr: string[] = [];
|
||||||
let currentItem: string = arr[0];
|
let currentItem: string = arr[0];
|
||||||
|
|||||||
57
src/utils/removeContentTags.ts
Normal file
57
src/utils/removeContentTags.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Removes content wrapped in specified tags from a string
|
||||||
|
* @param content The content string to process
|
||||||
|
* @param tag The tag name without angle brackets (e.g., 'think' for '<think></think>')
|
||||||
|
* @returns The content with the specified tags and their contents removed, and trimmed
|
||||||
|
*/
|
||||||
|
export function removeContentTags<T extends string | null | undefined>(
|
||||||
|
content: T,
|
||||||
|
tag: string
|
||||||
|
): T {
|
||||||
|
if (!content || typeof content !== 'string') {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic implementation for other cases
|
||||||
|
const openTag = `<${tag}>`;
|
||||||
|
const closeTag = `</${tag}>`;
|
||||||
|
|
||||||
|
// Parse the content and remove tags
|
||||||
|
let result = '';
|
||||||
|
let skipUntil: number | null = null;
|
||||||
|
let depth = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < content.length; i++) {
|
||||||
|
// Check for opening tag
|
||||||
|
if (content.substring(i, i + openTag.length) === openTag) {
|
||||||
|
depth++;
|
||||||
|
if (depth === 1) {
|
||||||
|
skipUntil = content.indexOf(closeTag, i + openTag.length);
|
||||||
|
i = i + openTag.length - 1; // Skip the opening tag
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check for closing tag
|
||||||
|
else if (
|
||||||
|
content.substring(i, i + closeTag.length) === closeTag &&
|
||||||
|
depth > 0
|
||||||
|
) {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) {
|
||||||
|
i = i + closeTag.length - 1; // Skip the closing tag
|
||||||
|
skipUntil = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add character if not inside a tag
|
||||||
|
if (skipUntil === null) {
|
||||||
|
result += content[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize multiple spaces/tabs into a single space (preserves newlines), then trim.
|
||||||
|
result = result.replace(/[ \t]+/g, ' ').trim();
|
||||||
|
|
||||||
|
return result as unknown as T;
|
||||||
|
}
|
||||||
3
src/utils/removeConventionalCommitWord.ts
Normal file
3
src/utils/removeConventionalCommitWord.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function removeConventionalCommitWord(message: string): string {
|
||||||
|
return message.replace(/^(fix|feat)\((.+?)\):/, '($2):');
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import cl100k_base from '@dqbd/tiktoken/encoders/cl100k_base.json';
|
||||||
import { Tiktoken } from '@dqbd/tiktoken/lite';
|
import { Tiktoken } from '@dqbd/tiktoken/lite';
|
||||||
import cl100k_base from '@dqbd/tiktoken/encoders/cl100k_base.json' assert { type: 'json' };
|
|
||||||
|
|
||||||
export function tokenCount(content: string): number {
|
export function tokenCount(content: string): number {
|
||||||
const encoding = new Tiktoken(
|
const encoding = new Tiktoken(
|
||||||
|
|||||||
14
src/version.ts
Normal file
14
src/version.ts
Normal 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
22
test/Dockerfile
Normal 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
|
||||||
205
test/e2e/gitPush.test.ts
Normal file
205
test/e2e/gitPush.test.ts
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import 'cli-testing-library/extend-expect';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { prepareTempDir } from './utils';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { render } from 'cli-testing-library';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { rm } from 'fs';
|
||||||
|
const fsExec = promisify(exec);
|
||||||
|
const fsRemove = promisify(rm);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* git remote -v
|
||||||
|
*
|
||||||
|
* [no remotes]
|
||||||
|
*/
|
||||||
|
const prepareNoRemoteGitRepository = async (): Promise<{
|
||||||
|
gitDir: string;
|
||||||
|
cleanup: () => Promise<void>;
|
||||||
|
}> => {
|
||||||
|
const tempDir = await prepareTempDir();
|
||||||
|
await fsExec('git init test', { cwd: tempDir });
|
||||||
|
const gitDir = path.resolve(tempDir, 'test');
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
return fsRemove(tempDir, { recursive: true });
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
gitDir,
|
||||||
|
cleanup
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* git remote -v
|
||||||
|
*
|
||||||
|
* origin /tmp/remote.git (fetch)
|
||||||
|
* origin /tmp/remote.git (push)
|
||||||
|
*/
|
||||||
|
const prepareOneRemoteGitRepository = async (): Promise<{
|
||||||
|
gitDir: string;
|
||||||
|
cleanup: () => Promise<void>;
|
||||||
|
}> => {
|
||||||
|
const tempDir = await prepareTempDir();
|
||||||
|
await fsExec('git init --bare remote.git', { cwd: tempDir });
|
||||||
|
await fsExec('git clone remote.git test', { cwd: tempDir });
|
||||||
|
const gitDir = path.resolve(tempDir, 'test');
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
return fsRemove(tempDir, { recursive: true });
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
gitDir,
|
||||||
|
cleanup
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* git remote -v
|
||||||
|
*
|
||||||
|
* origin /tmp/remote.git (fetch)
|
||||||
|
* origin /tmp/remote.git (push)
|
||||||
|
* other ../remote2.git (fetch)
|
||||||
|
* other ../remote2.git (push)
|
||||||
|
*/
|
||||||
|
const prepareTwoRemotesGitRepository = async (): Promise<{
|
||||||
|
gitDir: string;
|
||||||
|
cleanup: () => Promise<void>;
|
||||||
|
}> => {
|
||||||
|
const tempDir = await prepareTempDir();
|
||||||
|
await fsExec('git init --bare remote.git', { cwd: tempDir });
|
||||||
|
await fsExec('git init --bare other.git', { cwd: tempDir });
|
||||||
|
await fsExec('git clone remote.git test', { cwd: tempDir });
|
||||||
|
const gitDir = path.resolve(tempDir, 'test');
|
||||||
|
await fsExec('git remote add other ../other.git', { cwd: gitDir });
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
return fsRemove(tempDir, { recursive: true });
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
gitDir,
|
||||||
|
cleanup
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('cli flow to push git branch', () => {
|
||||||
|
it('do nothing when OCO_GITPUSH is set to false', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareNoRemoteGitRepository();
|
||||||
|
|
||||||
|
await render('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
|
||||||
|
const { queryByText, findByText, userEvent } = await render(
|
||||||
|
`OCO_AI_PROVIDER='test' OCO_GITPUSH='false' node`,
|
||||||
|
[resolve('./out/cli.cjs')],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await queryByText('Choose a remote to push to')
|
||||||
|
).not.toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await queryByText('Do you want to run `git push`?')
|
||||||
|
).not.toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await queryByText('Successfully pushed all commits to origin')
|
||||||
|
).not.toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await queryByText('Command failed with exit code 1')
|
||||||
|
).not.toBeInTheConsole();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('push and cause error when there is no remote', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareNoRemoteGitRepository();
|
||||||
|
|
||||||
|
await render('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
|
||||||
|
const { queryByText, findByText, userEvent } = await render(
|
||||||
|
`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`,
|
||||||
|
[resolve('./out/cli.cjs')],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await queryByText('Choose a remote to push to')
|
||||||
|
).not.toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await queryByText('Do you want to run `git push`?')
|
||||||
|
).not.toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await queryByText('Successfully pushed all commits to origin')
|
||||||
|
).not.toBeInTheConsole();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await findByText('Command failed with exit code 1')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('push when one remote is set', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareOneRemoteGitRepository();
|
||||||
|
|
||||||
|
await render('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
|
||||||
|
const { findByText, userEvent } = await render(
|
||||||
|
`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`,
|
||||||
|
[resolve('./out/cli.cjs')],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await findByText('Do you want to run `git push`?')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await findByText('Successfully pushed all commits to origin')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('push when two remotes are set', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareTwoRemotesGitRepository();
|
||||||
|
|
||||||
|
await render('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
|
||||||
|
const { findByText, userEvent } = await render(
|
||||||
|
`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`,
|
||||||
|
[resolve('./out/cli.cjs')],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(await findByText('Choose a remote to push to')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await findByText('Successfully pushed all commits to origin')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
});
|
||||||
12
test/e2e/noChanges.test.ts
Normal file
12
test/e2e/noChanges.test.ts
Normal 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
55
test/e2e/oneFile.test.ts
Normal 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' OCO_GITPUSH='true' 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('Do you want to run `git push`?')).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' OCO_GITPUSH='true' 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('Do you want to run `git push`?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(await findByText('Successfully pushed all commits to origin')).toBeInTheConsole();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
222
test/e2e/prompt-module/commitlint.test.ts
Normal file
222
test/e2e/prompt-module/commitlint.test.ts
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import { resolve } from 'path';
|
||||||
|
import { render } from 'cli-testing-library';
|
||||||
|
import 'cli-testing-library/extend-expect';
|
||||||
|
import { prepareEnvironment, wait } from '../utils';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
function getAbsolutePath(relativePath: string) {
|
||||||
|
// Use process.cwd() which should be the project root during test execution
|
||||||
|
return path.resolve(process.cwd(), 'test/e2e/prompt-module', 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' OCO_GITPUSH='true' \
|
||||||
|
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' OCO_GITPUSH='true' \
|
||||||
|
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' OCO_GITPUSH='true' \
|
||||||
|
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' OCO_GITPUSH='true' \
|
||||||
|
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' OCO_GITPUSH='true' \
|
||||||
|
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' OCO_GITPUSH='true' \
|
||||||
|
node ${resolve('./out/cli.cjs')} \
|
||||||
|
`,
|
||||||
|
[],
|
||||||
|
{ cwd: gitDir }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await oco.findByText('Generating the commit message')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
expect(
|
||||||
|
await oco.findByText('Confirm the commit message?')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
oco.userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await oco.findByText('Do you want to run `git push`?')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
oco.userEvent.keyboard('[Enter]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await oco.findByText('Successfully pushed all commits to origin')
|
||||||
|
).toBeInTheConsole();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['@commitlint/config-conventional']
|
||||||
|
};
|
||||||
2029
test/e2e/prompt-module/data/commitlint_18/package-lock.json
generated
Normal file
2029
test/e2e/prompt-module/data/commitlint_18/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
test/e2e/prompt-module/data/commitlint_18/package.json
Normal file
15
test/e2e/prompt-module/data/commitlint_18/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "commitlint-test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^18.0.0",
|
||||||
|
"@commitlint/config-conventional": "^18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
extends: ['@commitlint/config-conventional']
|
||||||
|
};
|
||||||
1453
test/e2e/prompt-module/data/commitlint_19/package-lock.json
generated
Normal file
1453
test/e2e/prompt-module/data/commitlint_19/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
test/e2e/prompt-module/data/commitlint_19/package.json
Normal file
16
test/e2e/prompt-module/data/commitlint_19/package.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "commitlint-test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^19.0.0",
|
||||||
|
"@commitlint/config-conventional": "^19.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['@commitlint/config-conventional']
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user