mirror of
https://github.com/di-sukharev/opencommit.git
synced 2026-01-14 16:18:02 -05:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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
|
||||||
|
|||||||
17
.github/workflows/test.yml
vendored
17
.github/workflows/test.yml
vendored
@@ -1,6 +1,11 @@
|
|||||||
name: Testing
|
name: Testing
|
||||||
|
|
||||||
on: [pull_request]
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
unit-test:
|
unit-test:
|
||||||
@@ -9,11 +14,12 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
node-version: [20.x]
|
node-version: [20.x]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
- name: Run Unit Tests
|
- name: Run Unit Tests
|
||||||
@@ -24,11 +30,12 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
node-version: [20.x]
|
node-version: [20.x]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
- name: Install git
|
- name: Install git
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,3 +12,4 @@ src/*.json
|
|||||||
.idea
|
.idea
|
||||||
test.ts
|
test.ts
|
||||||
notes.md
|
notes.md
|
||||||
|
.nvmrc
|
||||||
15
README.md
15
README.md
@@ -109,11 +109,12 @@ Create a `.env` file and add OpenCommit config variables there like this:
|
|||||||
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama, gemini, flowise, deepseek>
|
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama, gemini, flowise, deepseek>
|
||||||
OCO_API_KEY=<your OpenAI API token> // or other LLM provider API token
|
OCO_API_KEY=<your OpenAI API token> // or other LLM provider API token
|
||||||
OCO_API_URL=<may be used to set proxy path to OpenAI api>
|
OCO_API_URL=<may be used to set proxy path to OpenAI api>
|
||||||
|
OCO_API_CUSTOM_HEADERS=<JSON string of custom HTTP headers to include in API requests>
|
||||||
OCO_TOKENS_MAX_INPUT=<max model token limit (default: 4096)>
|
OCO_TOKENS_MAX_INPUT=<max model token limit (default: 4096)>
|
||||||
OCO_TOKENS_MAX_OUTPUT=<max response tokens (default: 500)>
|
OCO_TOKENS_MAX_OUTPUT=<max response tokens (default: 500)>
|
||||||
OCO_DESCRIPTION=<postface a message with ~3 sentences description of the changes>
|
OCO_DESCRIPTION=<postface a message with ~3 sentences description of the changes>
|
||||||
OCO_EMOJI=<boolean, add GitMoji>
|
OCO_EMOJI=<boolean, add GitMoji>
|
||||||
OCO_MODEL=<either 'gpt-4o', 'gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo' (default), 'gpt-3.5-turbo-0125', 'gpt-4-1106-preview', 'gpt-4-turbo-preview' or 'gpt-4-0125-preview' or any Anthropic or Ollama model or any string basically, but it should be a valid model name>
|
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_MESSAGE_TEMPLATE_PLACEHOLDER=<message template placeholder, default: '$msg'>
|
||||||
OCO_PROMPT_MODULE=<either conventional-commit or @commitlint, default: conventional-commit>
|
OCO_PROMPT_MODULE=<either conventional-commit or @commitlint, default: conventional-commit>
|
||||||
@@ -132,6 +133,18 @@ Simply set any of the variables above like this:
|
|||||||
oco config set OCO_MODEL=gpt-4o-mini
|
oco config set OCO_MODEL=gpt-4o-mini
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To see all available configuration parameters and their accepted values:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco config describe
|
||||||
|
```
|
||||||
|
|
||||||
|
To see details for a specific parameter:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
oco config describe OCO_MODEL
|
||||||
|
```
|
||||||
|
|
||||||
Configure [GitMoji](https://gitmoji.dev/) to preface a message.
|
Configure [GitMoji](https://gitmoji.dev/) to preface a message.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -9,19 +9,33 @@ const config: Config = {
|
|||||||
testTimeout: 100_000,
|
testTimeout: 100_000,
|
||||||
coverageProvider: 'v8',
|
coverageProvider: 'v8',
|
||||||
moduleDirectories: ['node_modules', 'src'],
|
moduleDirectories: ['node_modules', 'src'],
|
||||||
preset: 'ts-jest/presets/js-with-ts-esm',
|
preset: 'ts-jest/presets/default-esm',
|
||||||
setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'],
|
setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'],
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
testRegex: ['.*\\.test\\.ts$'],
|
testRegex: ['.*\\.test\\.ts$'],
|
||||||
transformIgnorePatterns: ['node_modules/(?!cli-testing-library)'],
|
// 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: {
|
transform: {
|
||||||
'^.+\\.(ts|tsx)$': [
|
'^.+\\.(ts|tsx|js|jsx|mjs)$': [
|
||||||
'ts-jest',
|
'ts-jest',
|
||||||
{
|
{
|
||||||
diagnostics: false,
|
diagnostics: false,
|
||||||
useESM: true
|
useESM: true,
|
||||||
|
tsconfig: {
|
||||||
|
module: 'ESNext',
|
||||||
|
target: 'ES2022'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^(\\.{1,2}/.*)\\.js$': '$1'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
17687
out/cli.cjs
17687
out/cli.cjs
File diff suppressed because one or more lines are too long
27228
out/github-action.cjs
27228
out/github-action.cjs
File diff suppressed because one or more lines are too long
4345
package-lock.json
generated
4345
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "opencommit",
|
"name": "opencommit",
|
||||||
"version": "3.2.7",
|
"version": "3.2.8",
|
||||||
"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",
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"ollama:start": "OCO_AI_PROVIDER='ollama' node ./out/cli.cjs",
|
"ollama:start": "OCO_AI_PROVIDER='ollama' node ./out/cli.cjs",
|
||||||
"dev": "ts-node ./src/cli.ts",
|
"dev": "ts-node ./src/cli.ts",
|
||||||
"dev:gemini": "OCO_AI_PROVIDER='gemini' ts-node ./src/cli.ts",
|
"dev:gemini": "OCO_AI_PROVIDER='gemini' ts-node ./src/cli.ts",
|
||||||
"build": "rimraf out && node esbuild.config.js",
|
"build": "npx rimraf out && node esbuild.config.js",
|
||||||
"build:push": "npm run build && git add . && git commit -m 'build' && git push",
|
"build:push": "npm run build && git add . && git commit -m 'build' && git push",
|
||||||
"deploy": "npm publish --tag latest",
|
"deploy": "npm publish --tag latest",
|
||||||
"deploy:build": "npm run build:push && git push --tags && npm run deploy",
|
"deploy:build": "npm run build:push && git push --tags && npm run deploy",
|
||||||
@@ -67,14 +67,15 @@
|
|||||||
"@types/inquirer": "^9.0.3",
|
"@types/inquirer": "^9.0.3",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^16.18.14",
|
"@types/node": "^16.18.14",
|
||||||
"@typescript-eslint/eslint-plugin": "^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",
|
"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",
|
"jest": "^29.7.0",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.4",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
"ts-jest": "^29.1.2",
|
"ts-jest": "^29.1.2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.9.3"
|
||||||
@@ -82,7 +83,7 @@
|
|||||||
"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",
|
"@anthropic-ai/sdk": "^0.19.2",
|
||||||
"@azure/openai": "^1.0.0-beta.12",
|
"@azure/openai": "^1.0.0-beta.12",
|
||||||
"@clack/prompts": "^0.6.1",
|
"@clack/prompts": "^0.6.1",
|
||||||
@@ -102,5 +103,9 @@
|
|||||||
"openai": "^4.57.0",
|
"openai": "^4.57.0",
|
||||||
"punycode": "^2.3.1",
|
"punycode": "^2.3.1",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"ajv": "^8.17.1",
|
||||||
|
"whatwg-url": "^14.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ cli(
|
|||||||
name: 'opencommit',
|
name: 'opencommit',
|
||||||
commands: [configCommand, hookCommand, commitlintConfigCommand],
|
commands: [configCommand, hookCommand, commitlintConfigCommand],
|
||||||
flags: {
|
flags: {
|
||||||
fgm: Boolean,
|
fgm: {
|
||||||
|
type: Boolean,
|
||||||
|
description: 'Use full GitMoji specification',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
context: {
|
context: {
|
||||||
type: String,
|
type: String,
|
||||||
alias: 'c',
|
alias: 'c',
|
||||||
|
|||||||
@@ -138,7 +138,8 @@ ${chalk.grey('——————————————————')}`
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
pushSpinner.stop(
|
pushSpinner.stop(
|
||||||
`${chalk.green('✔')} Successfully pushed all commits to ${remotes[0]
|
`${chalk.green('✔')} Successfully pushed all commits to ${
|
||||||
|
remotes[0]
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -148,10 +149,13 @@ ${chalk.grey('——————————————————')}`
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const skipOption = `don't push`
|
const skipOption = `don't push`;
|
||||||
const selectedRemote = (await select({
|
const selectedRemote = (await select({
|
||||||
message: 'Choose a remote to push to',
|
message: 'Choose a remote to push to',
|
||||||
options: [...remotes, skipOption].map((remote) => ({ value: remote, label: remote })),
|
options: [...remotes, skipOption].map((remote) => ({
|
||||||
|
value: remote,
|
||||||
|
label: remote
|
||||||
|
}))
|
||||||
})) as string;
|
})) as string;
|
||||||
|
|
||||||
if (isCancel(selectedRemote)) process.exit(1);
|
if (isCancel(selectedRemote)) process.exit(1);
|
||||||
@@ -235,8 +239,9 @@ 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?'
|
||||||
});
|
});
|
||||||
@@ -245,7 +250,7 @@ export async function commit(
|
|||||||
|
|
||||||
if (isStageAllAndCommitConfirmedByUser) {
|
if (isStageAllAndCommitConfirmedByUser) {
|
||||||
await commit(extraArgs, context, true, fullGitMojiSpec);
|
await commit(extraArgs, context, true, fullGitMojiSpec);
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stagedFiles.length === 0 && changedFiles.length > 0) {
|
if (stagedFiles.length === 0 && changedFiles.length > 0) {
|
||||||
@@ -257,13 +262,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, context, false, fullGitMojiSpec);
|
await commit(extraArgs, context, false, fullGitMojiSpec);
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
stagedFilesSpinner.stop(
|
stagedFilesSpinner.stop(
|
||||||
|
|||||||
@@ -25,13 +25,15 @@ export enum CONFIG_KEYS {
|
|||||||
OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT',
|
OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT',
|
||||||
OCO_TEST_MOCK_TYPE = 'OCO_TEST_MOCK_TYPE',
|
OCO_TEST_MOCK_TYPE = 'OCO_TEST_MOCK_TYPE',
|
||||||
OCO_API_URL = 'OCO_API_URL',
|
OCO_API_URL = 'OCO_API_URL',
|
||||||
|
OCO_API_CUSTOM_HEADERS = 'OCO_API_CUSTOM_HEADERS',
|
||||||
OCO_OMIT_SCOPE = 'OCO_OMIT_SCOPE',
|
OCO_OMIT_SCOPE = 'OCO_OMIT_SCOPE',
|
||||||
OCO_GITPUSH = 'OCO_GITPUSH' // todo: deprecate
|
OCO_GITPUSH = 'OCO_GITPUSH' // todo: deprecate
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CONFIG_MODES {
|
export enum CONFIG_MODES {
|
||||||
get = 'get',
|
get = 'get',
|
||||||
set = 'set'
|
set = 'set',
|
||||||
|
describe = 'describe'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MODEL_LIST = {
|
export const MODEL_LIST = {
|
||||||
@@ -155,8 +157,8 @@ const getDefaultModel = (provider: string | undefined): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export enum DEFAULT_TOKEN_LIMITS {
|
export enum DEFAULT_TOKEN_LIMITS {
|
||||||
DEFAULT_MAX_TOKENS_INPUT = 40960,
|
DEFAULT_MAX_TOKENS_INPUT = 4096,
|
||||||
DEFAULT_MAX_TOKENS_OUTPUT = 4096
|
DEFAULT_MAX_TOKENS_OUTPUT = 500
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateConfig = (
|
const validateConfig = (
|
||||||
@@ -204,6 +206,22 @@ export const configValidators = {
|
|||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[CONFIG_KEYS.OCO_API_CUSTOM_HEADERS](value) {
|
||||||
|
try {
|
||||||
|
// Custom headers must be a valid JSON string
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
JSON.parse(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
} catch (error) {
|
||||||
|
validateConfig(
|
||||||
|
CONFIG_KEYS.OCO_API_CUSTOM_HEADERS,
|
||||||
|
false,
|
||||||
|
'Must be a valid JSON string of headers'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT](value: any) {
|
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT](value: any) {
|
||||||
value = parseInt(value);
|
value = parseInt(value);
|
||||||
validateConfig(
|
validateConfig(
|
||||||
@@ -380,6 +398,7 @@ export type ConfigType = {
|
|||||||
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT]: number;
|
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT]: number;
|
||||||
[CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT]: number;
|
[CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT]: number;
|
||||||
[CONFIG_KEYS.OCO_API_URL]?: string;
|
[CONFIG_KEYS.OCO_API_URL]?: string;
|
||||||
|
[CONFIG_KEYS.OCO_API_CUSTOM_HEADERS]?: string;
|
||||||
[CONFIG_KEYS.OCO_DESCRIPTION]: boolean;
|
[CONFIG_KEYS.OCO_DESCRIPTION]: boolean;
|
||||||
[CONFIG_KEYS.OCO_EMOJI]: boolean;
|
[CONFIG_KEYS.OCO_EMOJI]: boolean;
|
||||||
[CONFIG_KEYS.OCO_WHY]: boolean;
|
[CONFIG_KEYS.OCO_WHY]: boolean;
|
||||||
@@ -462,6 +481,7 @@ const getEnvConfig = (envPath: string) => {
|
|||||||
OCO_MODEL: process.env.OCO_MODEL,
|
OCO_MODEL: process.env.OCO_MODEL,
|
||||||
OCO_API_URL: process.env.OCO_API_URL,
|
OCO_API_URL: process.env.OCO_API_URL,
|
||||||
OCO_API_KEY: process.env.OCO_API_KEY,
|
OCO_API_KEY: process.env.OCO_API_KEY,
|
||||||
|
OCO_API_CUSTOM_HEADERS: process.env.OCO_API_CUSTOM_HEADERS,
|
||||||
OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER as OCO_AI_PROVIDER_ENUM,
|
OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER as OCO_AI_PROVIDER_ENUM,
|
||||||
|
|
||||||
OCO_TOKENS_MAX_INPUT: parseConfigVarValue(process.env.OCO_TOKENS_MAX_INPUT),
|
OCO_TOKENS_MAX_INPUT: parseConfigVarValue(process.env.OCO_TOKENS_MAX_INPUT),
|
||||||
@@ -603,28 +623,208 @@ export const setConfig = (
|
|||||||
outro(`${chalk.green('✔')} config successfully set`);
|
outro(`${chalk.green('✔')} config successfully set`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- HELP MESSAGE GENERATION ---
|
||||||
|
function getConfigKeyDetails(key) {
|
||||||
|
switch (key) {
|
||||||
|
case CONFIG_KEYS.OCO_MODEL:
|
||||||
|
return {
|
||||||
|
description: 'The AI model to use for generating commit messages',
|
||||||
|
values: MODEL_LIST
|
||||||
|
};
|
||||||
|
case CONFIG_KEYS.OCO_AI_PROVIDER:
|
||||||
|
return {
|
||||||
|
description: 'The AI provider to use',
|
||||||
|
values: Object.values(OCO_AI_PROVIDER_ENUM)
|
||||||
|
};
|
||||||
|
case CONFIG_KEYS.OCO_PROMPT_MODULE:
|
||||||
|
return {
|
||||||
|
description: 'The prompt module to use for commit message generation',
|
||||||
|
values: Object.values(OCO_PROMPT_MODULE_ENUM)
|
||||||
|
};
|
||||||
|
case CONFIG_KEYS.OCO_LANGUAGE:
|
||||||
|
return {
|
||||||
|
description: 'The locale to use for commit messages',
|
||||||
|
values: Object.keys(i18n)
|
||||||
|
};
|
||||||
|
case CONFIG_KEYS.OCO_TEST_MOCK_TYPE:
|
||||||
|
return {
|
||||||
|
description: 'The type of test mock to use',
|
||||||
|
values: ['commit-message', 'prompt-module-commitlint-config']
|
||||||
|
};
|
||||||
|
case CONFIG_KEYS.OCO_ONE_LINE_COMMIT:
|
||||||
|
return {
|
||||||
|
description: 'One line commit message',
|
||||||
|
values: ['true', 'false']
|
||||||
|
};
|
||||||
|
case CONFIG_KEYS.OCO_DESCRIPTION:
|
||||||
|
return {
|
||||||
|
description: 'Postface a message with ~3 sentences description of the changes',
|
||||||
|
values: ['true', 'false']
|
||||||
|
};
|
||||||
|
case CONFIG_KEYS.OCO_EMOJI:
|
||||||
|
return {
|
||||||
|
description: 'Preface a message with GitMoji',
|
||||||
|
values: ['true', 'false']
|
||||||
|
};
|
||||||
|
case CONFIG_KEYS.OCO_WHY:
|
||||||
|
return {
|
||||||
|
description: 'Output a short description of why the changes were done after the commit message (default: false)',
|
||||||
|
values: ['true', 'false']
|
||||||
|
}
|
||||||
|
case CONFIG_KEYS.OCO_OMIT_SCOPE:
|
||||||
|
return {
|
||||||
|
description: 'Do not include a scope in the commit message',
|
||||||
|
values: ['true', 'false']
|
||||||
|
};
|
||||||
|
case CONFIG_KEYS.OCO_GITPUSH:
|
||||||
|
return {
|
||||||
|
description: 'Push to git after commit (deprecated). If false, oco will exit after committing',
|
||||||
|
values: ['true', 'false']
|
||||||
|
};
|
||||||
|
case CONFIG_KEYS.OCO_TOKENS_MAX_INPUT:
|
||||||
|
return {
|
||||||
|
description: 'Max model token limit',
|
||||||
|
values: ['Any positive integer']
|
||||||
|
};
|
||||||
|
case CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT:
|
||||||
|
return {
|
||||||
|
description: 'Max response tokens',
|
||||||
|
values: ['Any positive integer']
|
||||||
|
};
|
||||||
|
case CONFIG_KEYS.OCO_API_KEY:
|
||||||
|
return {
|
||||||
|
description: 'API key for the selected provider',
|
||||||
|
values: ['String (required for most providers)']
|
||||||
|
};
|
||||||
|
case CONFIG_KEYS.OCO_API_URL:
|
||||||
|
return {
|
||||||
|
description: 'Custom API URL - may be used to set proxy path to OpenAI API',
|
||||||
|
values: ["URL string (must start with 'http://' or 'https://')"]
|
||||||
|
};
|
||||||
|
case CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER:
|
||||||
|
return {
|
||||||
|
description: 'Message template placeholder',
|
||||||
|
values: ["String (must start with $)"]
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
description: 'String value',
|
||||||
|
values: ['Any string']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printConfigKeyHelp(param) {
|
||||||
|
if (!Object.values(CONFIG_KEYS).includes(param)) {
|
||||||
|
console.log(chalk.red(`Unknown config parameter: ${param}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const details = getConfigKeyDetails(param as CONFIG_KEYS);
|
||||||
|
|
||||||
|
let desc = details.description;
|
||||||
|
let defaultValue = undefined;
|
||||||
|
if (param in DEFAULT_CONFIG) {
|
||||||
|
defaultValue = DEFAULT_CONFIG[param];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
console.log(chalk.bold(`\n${param}:`));
|
||||||
|
console.log(chalk.gray(` Description: ${desc}`));
|
||||||
|
if (defaultValue !== undefined) {
|
||||||
|
// Print booleans and numbers as-is, strings without quotes
|
||||||
|
if (typeof defaultValue === 'string') {
|
||||||
|
console.log(chalk.gray(` Default: ${defaultValue}`));
|
||||||
|
} else {
|
||||||
|
console.log(chalk.gray(` Default: ${defaultValue}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(details.values)) {
|
||||||
|
console.log(chalk.gray(' Accepted values:'));
|
||||||
|
details.values.forEach(value => {
|
||||||
|
console.log(chalk.gray(` - ${value}`));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(chalk.gray(' Accepted values by provider:'));
|
||||||
|
Object.entries(details.values).forEach(([provider, values]) => {
|
||||||
|
console.log(chalk.gray(` ${provider}:`));
|
||||||
|
(values as string[]).forEach(value => {
|
||||||
|
console.log(chalk.gray(` - ${value}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printAllConfigHelp() {
|
||||||
|
console.log(chalk.bold('Available config parameters:'));
|
||||||
|
for (const key of Object.values(CONFIG_KEYS).sort()) {
|
||||||
|
const details = getConfigKeyDetails(key);
|
||||||
|
// Try to get the default value from DEFAULT_CONFIG
|
||||||
|
let defaultValue = undefined;
|
||||||
|
if (key in DEFAULT_CONFIG) {
|
||||||
|
defaultValue = DEFAULT_CONFIG[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.bold(`\n${key}:`));
|
||||||
|
console.log(chalk.gray(` Description: ${details.description}`));
|
||||||
|
if (defaultValue !== undefined) {
|
||||||
|
if (typeof defaultValue === 'string') {
|
||||||
|
console.log(chalk.gray(` Default: ${defaultValue}`));
|
||||||
|
} else {
|
||||||
|
console.log(chalk.gray(` Default: ${defaultValue}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(chalk.yellow('\nUse "oco config describe [PARAMETER]" to see accepted values and more details for a specific config parameter.'));
|
||||||
|
}
|
||||||
|
|
||||||
export const configCommand = command(
|
export const configCommand = command(
|
||||||
{
|
{
|
||||||
name: COMMANDS.config,
|
name: COMMANDS.config,
|
||||||
parameters: ['<mode>', '<key=values...>']
|
parameters: ['<mode>', '[key=values...]'],
|
||||||
|
help: {
|
||||||
|
description: 'Configure opencommit settings',
|
||||||
|
examples: [
|
||||||
|
'Describe all config parameters: oco config describe',
|
||||||
|
'Describe a specific parameter: oco config describe OCO_MODEL',
|
||||||
|
'Get a config value: oco config get OCO_MODEL',
|
||||||
|
'Set a config value: oco config set OCO_MODEL=gpt-4'
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async (argv) => {
|
async (argv) => {
|
||||||
try {
|
try {
|
||||||
const { mode, keyValues } = argv._;
|
const { mode, keyValues } = argv._;
|
||||||
intro(`COMMAND: config ${mode} ${keyValues}`);
|
intro(`COMMAND: config ${mode} ${keyValues}`);
|
||||||
|
|
||||||
if (mode === CONFIG_MODES.get) {
|
if (mode === CONFIG_MODES.describe) {
|
||||||
|
if (!keyValues || keyValues.length === 0) {
|
||||||
|
printAllConfigHelp();
|
||||||
|
} else {
|
||||||
|
for (const key of keyValues) {
|
||||||
|
printConfigKeyHelp(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
} else if (mode === CONFIG_MODES.get) {
|
||||||
|
if (!keyValues || keyValues.length === 0) {
|
||||||
|
throw new Error('No config keys specified for get mode');
|
||||||
|
}
|
||||||
const config = getConfig() || {};
|
const config = getConfig() || {};
|
||||||
for (const key of keyValues) {
|
for (const key of keyValues) {
|
||||||
outro(`${key}=${config[key as keyof typeof config]}`);
|
outro(`${key}=${config[key as keyof typeof config]}`);
|
||||||
}
|
}
|
||||||
} else if (mode === CONFIG_MODES.set) {
|
} else if (mode === CONFIG_MODES.set) {
|
||||||
|
if (!keyValues || keyValues.length === 0) {
|
||||||
|
throw new Error('No config keys specified for set mode');
|
||||||
|
}
|
||||||
await setConfig(
|
await setConfig(
|
||||||
keyValues.map((keyValue) => keyValue.split('=') as [string, string])
|
keyValues.map((keyValue) => keyValue.split('=') as [string, string])
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unsupported mode: ${mode}. Valid modes are: "set" and "get"`
|
`Unsupported mode: ${mode}. Valid modes are: "set", "get", and "describe"`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface AiEngineConfig {
|
|||||||
maxTokensOutput: number;
|
maxTokensOutput: number;
|
||||||
maxTokensInput: number;
|
maxTokensInput: number;
|
||||||
baseURL?: string;
|
baseURL?: string;
|
||||||
|
customHeaders?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client =
|
type Client =
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import axios from 'axios';
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { OpenAI } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
import { tokenCount } from '../utils/tokenCount';
|
import { tokenCount } from '../utils/tokenCount';
|
||||||
import { AiEngine, AiEngineConfig } from './Engine';
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
@@ -54,8 +55,8 @@ export class AnthropicEngine implements AiEngine {
|
|||||||
const data = await this.client.messages.create(params);
|
const data = await this.client.messages.create(params);
|
||||||
|
|
||||||
const message = data?.content[0].text;
|
const message = data?.content[0].text;
|
||||||
|
let content = message;
|
||||||
return message;
|
return removeContentTags(content, 'think');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import axios from 'axios';
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { OpenAI } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
import { tokenCount } from '../utils/tokenCount';
|
import { tokenCount } from '../utils/tokenCount';
|
||||||
import { AiEngine, AiEngineConfig } from './Engine';
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
@@ -52,7 +53,9 @@ export class AzureEngine implements AiEngine {
|
|||||||
if (message?.content === null) {
|
if (message?.content === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return message?.content;
|
|
||||||
|
let content = message?.content;
|
||||||
|
return removeContentTags(content, 'think');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
outro(`${chalk.red('✖')} ${this.config.model}`);
|
outro(`${chalk.red('✖')} ${this.config.model}`);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { OpenAI } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
import { tokenCount } from '../utils/tokenCount';
|
import { tokenCount } from '../utils/tokenCount';
|
||||||
import { OpenAiEngine, OpenAiConfig } from './openAI';
|
import { OpenAiEngine, OpenAiConfig } from './openAi';
|
||||||
|
|
||||||
export interface DeepseekConfig extends OpenAiConfig {}
|
export interface DeepseekConfig extends OpenAiConfig {}
|
||||||
|
|
||||||
@@ -41,8 +42,8 @@ export class DeepseekEngine extends OpenAiEngine {
|
|||||||
const completion = await this.client.chat.completions.create(params);
|
const completion = await this.client.chat.completions.create(params);
|
||||||
|
|
||||||
const message = completion.choices[0].message;
|
const message = completion.choices[0].message;
|
||||||
|
let content = message?.content;
|
||||||
return message?.content;
|
return removeContentTags(content, 'think');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
import { OpenAI } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
import { AiEngine, AiEngineConfig } from './Engine';
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
interface FlowiseAiConfig extends AiEngineConfig {}
|
interface FlowiseAiConfig extends AiEngineConfig {}
|
||||||
@@ -36,7 +37,8 @@ export class FlowiseEngine implements AiEngine {
|
|||||||
try {
|
try {
|
||||||
const response = await this.client.post('', payload);
|
const response = await this.client.post('', payload);
|
||||||
const message = response.data;
|
const message = response.data;
|
||||||
return message?.text;
|
let content = message?.text;
|
||||||
|
return removeContentTags(content, 'think');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const message = err.response?.data?.error ?? err.message;
|
const message = err.response?.data?.error ?? err.message;
|
||||||
throw new Error('local model issues. details: ' + message);
|
throw new Error('local model issues. details: ' + message);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from '@google/generative-ai';
|
} from '@google/generative-ai';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { OpenAI } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
import { AiEngine, AiEngineConfig } from './Engine';
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
interface GeminiConfig extends AiEngineConfig {}
|
interface GeminiConfig extends AiEngineConfig {}
|
||||||
@@ -71,7 +72,8 @@ export class GeminiEngine implements AiEngine {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.response.text();
|
const content = result.response.text();
|
||||||
|
return removeContentTags(content, 'think');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,27 +1,21 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Mistral } from '@mistralai/mistralai';
|
|
||||||
import { OpenAI } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
import { tokenCount } from '../utils/tokenCount';
|
import { tokenCount } from '../utils/tokenCount';
|
||||||
import { AiEngine, AiEngineConfig } from './Engine';
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
import {
|
|
||||||
AssistantMessage as MistralAssistantMessage,
|
|
||||||
SystemMessage as MistralSystemMessage,
|
|
||||||
ToolMessage as MistralToolMessage,
|
|
||||||
UserMessage as MistralUserMessage
|
|
||||||
} from '@mistralai/mistralai/models/components';
|
|
||||||
|
|
||||||
|
// Using any for Mistral types to avoid type declaration issues
|
||||||
export interface MistralAiConfig extends AiEngineConfig {}
|
export interface MistralAiConfig extends AiEngineConfig {}
|
||||||
export type MistralCompletionMessageParam = Array<
|
export type MistralCompletionMessageParam = Array<any>;
|
||||||
| (MistralSystemMessage & { role: "system" })
|
|
||||||
| (MistralUserMessage & { role: "user" })
|
// Import Mistral dynamically to avoid TS errors
|
||||||
| (MistralAssistantMessage & { role: "assistant" })
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
| (MistralToolMessage & { role: "tool" })
|
const Mistral = require('@mistralai/mistralai').Mistral;
|
||||||
>
|
|
||||||
|
|
||||||
export class MistralAiEngine implements AiEngine {
|
export class MistralAiEngine implements AiEngine {
|
||||||
config: MistralAiConfig;
|
config: MistralAiConfig;
|
||||||
client: Mistral;
|
client: any; // Using any type for Mistral client to avoid TS errors
|
||||||
|
|
||||||
constructor(config: MistralAiConfig) {
|
constructor(config: MistralAiConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
@@ -64,7 +58,8 @@ export class MistralAiEngine implements AiEngine {
|
|||||||
if (!message || !message.content)
|
if (!message || !message.content)
|
||||||
throw Error('No completion choice available.')
|
throw Error('No completion choice available.')
|
||||||
|
|
||||||
return message.content as string;
|
let content = message.content as string;
|
||||||
|
return removeContentTags(content, 'think');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
import { OpenAI } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
import { AiEngine, AiEngineConfig } from './Engine';
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
import { chown } from 'fs';
|
|
||||||
|
|
||||||
interface MLXConfig extends AiEngineConfig {}
|
interface MLXConfig extends AiEngineConfig {}
|
||||||
|
|
||||||
@@ -37,8 +37,8 @@ export class MLXEngine implements AiEngine {
|
|||||||
|
|
||||||
const choices = response.data.choices;
|
const choices = response.data.choices;
|
||||||
const message = choices[0].message;
|
const message = choices[0].message;
|
||||||
|
let content = message?.content;
|
||||||
return message?.content;
|
return removeContentTags(content, 'think');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const message = err.response?.data?.error ?? err.message;
|
const message = err.response?.data?.error ?? err.message;
|
||||||
throw new Error(`MLX provider error: ${message}`);
|
throw new Error(`MLX provider error: ${message}`);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
import { OpenAI } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
import { AiEngine, AiEngineConfig } from './Engine';
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
interface OllamaConfig extends AiEngineConfig {}
|
interface OllamaConfig extends AiEngineConfig {}
|
||||||
@@ -10,11 +11,18 @@ export class OllamaEngine implements AiEngine {
|
|||||||
|
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
|
// Combine base headers with custom headers
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...config.customHeaders
|
||||||
|
};
|
||||||
|
|
||||||
this.client = axios.create({
|
this.client = axios.create({
|
||||||
url: config.baseURL
|
url: config.baseURL
|
||||||
? `${config.baseURL}/${config.apiKey}`
|
? `${config.baseURL}/${config.apiKey}`
|
||||||
: 'http://localhost:11434/api/chat',
|
: 'http://localhost:11434/api/chat',
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,12 +43,7 @@ export class OllamaEngine implements AiEngine {
|
|||||||
|
|
||||||
const { message } = response.data;
|
const { message } = response.data;
|
||||||
let content = message?.content;
|
let content = message?.content;
|
||||||
|
return removeContentTags(content, 'think');
|
||||||
if (content && content.includes('<think>')) {
|
|
||||||
return content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const message = err.response?.data?.error ?? err.message;
|
const message = err.response?.data?.error ?? err.message;
|
||||||
throw new Error(`Ollama provider error: ${message}`);
|
throw new Error(`Ollama provider error: ${message}`);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { OpenAI } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||||
|
import { parseCustomHeaders } from '../utils/engine';
|
||||||
|
import { removeContentTags } from '../utils/removeContentTags';
|
||||||
import { tokenCount } from '../utils/tokenCount';
|
import { tokenCount } from '../utils/tokenCount';
|
||||||
import { AiEngine, AiEngineConfig } from './Engine';
|
import { AiEngine, AiEngineConfig } from './Engine';
|
||||||
|
|
||||||
@@ -13,11 +15,22 @@ export class OpenAiEngine implements AiEngine {
|
|||||||
constructor(config: OpenAiConfig) {
|
constructor(config: OpenAiConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
if (!config.baseURL) {
|
const clientOptions: OpenAI.ClientOptions = {
|
||||||
this.client = new OpenAI({ apiKey: config.apiKey });
|
apiKey: config.apiKey
|
||||||
} else {
|
};
|
||||||
this.client = new OpenAI({ apiKey: config.apiKey, baseURL: config.baseURL });
|
|
||||||
|
if (config.baseURL) {
|
||||||
|
clientOptions.baseURL = config.baseURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.customHeaders) {
|
||||||
|
const headers = parseCustomHeaders(config.customHeaders);
|
||||||
|
if (Object.keys(headers).length > 0) {
|
||||||
|
clientOptions.defaultHeaders = headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client = new OpenAI(clientOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateCommitMessage = async (
|
public generateCommitMessage = async (
|
||||||
@@ -45,8 +58,8 @@ export class OpenAiEngine implements AiEngine {
|
|||||||
const completion = await this.client.chat.completions.create(params);
|
const completion = await this.client.chat.completions.create(params);
|
||||||
|
|
||||||
const message = completion.choices[0].message;
|
const message = completion.choices[0].message;
|
||||||
|
let content = message?.content;
|
||||||
return message?.content;
|
return removeContentTags(content, 'think');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const configureCommitlintIntegration = async (force = false) => {
|
|||||||
|
|
||||||
spin.start('Generating consistency with given @commitlint rules');
|
spin.start('Generating consistency with given @commitlint rules');
|
||||||
|
|
||||||
const prompts = inferPromptsFromCommitlintConfig(commitLintConfig);
|
const prompts = inferPromptsFromCommitlintConfig(commitLintConfig as any);
|
||||||
|
|
||||||
const consistencyPrompts =
|
const consistencyPrompts =
|
||||||
commitlintPrompts.GEN_COMMITLINT_CONSISTENCY_PROMPT(prompts);
|
commitlintPrompts.GEN_COMMITLINT_CONSISTENCY_PROMPT(prompts);
|
||||||
|
|||||||
@@ -56,11 +56,10 @@ const llmReadableRules: {
|
|||||||
blankline: (key, applicable) =>
|
blankline: (key, applicable) =>
|
||||||
`There should ${applicable} be a blank line at the beginning of the ${key}.`,
|
`There should ${applicable} be a blank line at the beginning of the ${key}.`,
|
||||||
caseRule: (key, applicable, value: string | Array<string>) =>
|
caseRule: (key, applicable, value: string | Array<string>) =>
|
||||||
`The ${key} should ${applicable} be in ${
|
`The ${key} should ${applicable} be in ${Array.isArray(value)
|
||||||
Array.isArray(value)
|
? `one of the following case:
|
||||||
? `one of the following case:
|
|
||||||
- ${value.join('\n - ')}.`
|
- ${value.join('\n - ')}.`
|
||||||
: `${value} case.`
|
: `${value} case.`
|
||||||
}`,
|
}`,
|
||||||
emptyRule: (key, applicable) => `The ${key} should ${applicable} be empty.`,
|
emptyRule: (key, applicable) => `The ${key} should ${applicable} be empty.`,
|
||||||
enumRule: (key, applicable, value: string | Array<string>) =>
|
enumRule: (key, applicable, value: string | Array<string>) =>
|
||||||
@@ -68,18 +67,17 @@ const llmReadableRules: {
|
|||||||
- ${Array.isArray(value) ? value.join('\n - ') : value}.`,
|
- ${Array.isArray(value) ? value.join('\n - ') : value}.`,
|
||||||
enumTypeRule: (key, applicable, value: string | Array<string>, prompt) =>
|
enumTypeRule: (key, applicable, value: string | Array<string>, prompt) =>
|
||||||
`The ${key} should ${applicable} be one of the following values:
|
`The ${key} should ${applicable} be one of the following values:
|
||||||
- ${
|
- ${Array.isArray(value)
|
||||||
Array.isArray(value)
|
|
||||||
? value
|
? value
|
||||||
.map((v) => {
|
.map((v) => {
|
||||||
const description = getTypeRuleExtraDescription(v, prompt);
|
const description = getTypeRuleExtraDescription(v, prompt);
|
||||||
if (description) {
|
if (description) {
|
||||||
return `${v} (${description})`;
|
return `${v} (${description})`;
|
||||||
} else return v;
|
} else return v;
|
||||||
})
|
})
|
||||||
.join('\n - ')
|
.join('\n - ')
|
||||||
: value
|
: value
|
||||||
}.`,
|
}.`,
|
||||||
fullStopRule: (key, applicable, value: string) =>
|
fullStopRule: (key, applicable, value: string) =>
|
||||||
`The ${key} should ${applicable} end with '${value}'.`,
|
`The ${key} should ${applicable} end with '${value}'.`,
|
||||||
maxLengthRule: (key, applicable, value: string) =>
|
maxLengthRule: (key, applicable, value: string) =>
|
||||||
@@ -216,9 +214,9 @@ const STRUCTURE_OF_COMMIT = config.OCO_OMIT_SCOPE
|
|||||||
const GEN_COMMITLINT_CONSISTENCY_PROMPT = (
|
const GEN_COMMITLINT_CONSISTENCY_PROMPT = (
|
||||||
prompts: string[]
|
prompts: string[]
|
||||||
): OpenAI.Chat.Completions.ChatCompletionMessageParam[] => [
|
): OpenAI.Chat.Completions.ChatCompletionMessageParam[] => [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages for two different changes in a single codebase and output them in the provided JSON format: one for a bug fix and another for a new feature.
|
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages for two different changes in a single codebase and output them in the provided JSON format: one for a bug fix and another for a new feature.
|
||||||
|
|
||||||
Here are the specific requirements and conventions that should be strictly followed:
|
Here are the specific requirements and conventions that should be strictly followed:
|
||||||
|
|
||||||
@@ -240,7 +238,7 @@ JSON Output Format:
|
|||||||
"commitDescription": "<Description of commit for both the bug fix and the feature>"
|
"commitDescription": "<Description of commit for both the bug fix and the feature>"
|
||||||
}
|
}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
- The "commitDescription" should not include the commit message’s header, only the description.
|
- The "commitDescription" should not include the commit message's header, only the description.
|
||||||
- Description should not be more than 74 characters.
|
- Description should not be more than 74 characters.
|
||||||
|
|
||||||
Additional Details:
|
Additional Details:
|
||||||
@@ -248,9 +246,9 @@ Additional Details:
|
|||||||
- Allowing the server to listen on a port specified through the environment variable is considered a new feature.
|
- Allowing the server to listen on a port specified through the environment variable is considered a new feature.
|
||||||
|
|
||||||
Example Git Diff is to follow:`
|
Example Git Diff is to follow:`
|
||||||
},
|
},
|
||||||
INIT_DIFF_PROMPT
|
INIT_DIFF_PROMPT
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompt to have LLM generate a message using @commitlint rules.
|
* Prompt to have LLM generate a message using @commitlint rules.
|
||||||
@@ -264,30 +262,25 @@ const INIT_MAIN_PROMPT = (
|
|||||||
prompts: string[]
|
prompts: string[]
|
||||||
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
|
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes ${
|
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes ${config.OCO_WHY ? 'and WHY the changes were done' : ''
|
||||||
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.
|
||||||
}. 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.'
|
||||||
config.OCO_EMOJI
|
: 'Do not preface the commit with anything.'
|
||||||
? '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."
|
||||||
config.OCO_DESCRIPTION
|
}
|
||||||
? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.'
|
|
||||||
: "Don't add any descriptions to the commit, only commit message."
|
|
||||||
}
|
|
||||||
Use the present tense. Use ${language} to answer.
|
Use the present tense. Use ${language} to answer.
|
||||||
${
|
${config.OCO_ONE_LINE_COMMIT
|
||||||
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.'
|
||||||
? '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>'
|
||||||
config.OCO_OMIT_SCOPE
|
: ''
|
||||||
? 'Do not include a scope in the commit message format. Use the format: <type>: <subject>'
|
}
|
||||||
: ''
|
|
||||||
}
|
|
||||||
You will strictly follow the following conventions to generate the content of the commit message:
|
You will strictly follow the following conventions to generate the content of the commit message:
|
||||||
- ${prompts.join('\n- ')}
|
- ${prompts.join('\n- ')}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import path from 'path';
|
|||||||
const findModulePath = (moduleName: string) => {
|
const findModulePath = (moduleName: string) => {
|
||||||
const searchPaths = [
|
const searchPaths = [
|
||||||
path.join('node_modules', moduleName),
|
path.join('node_modules', moduleName),
|
||||||
path.join('node_modules', '.pnpm')
|
path.join('node_modules', '.pnpm'),
|
||||||
|
path.resolve(__dirname, '../..')
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const basePath of searchPaths) {
|
for (const basePath of searchPaths) {
|
||||||
@@ -59,7 +60,7 @@ export const getCommitLintPWDConfig =
|
|||||||
* ES Module (commitlint@v19.x.x. <= )
|
* ES Module (commitlint@v19.x.x. <= )
|
||||||
* Directory import is not supported in ES Module resolution, so import the file directly
|
* Directory import is not supported in ES Module resolution, so import the file directly
|
||||||
*/
|
*/
|
||||||
modulePath = await findModulePath('@commitlint/load/lib/load.js');
|
modulePath = findModulePath('@commitlint/load/lib/load.js');
|
||||||
load = (await import(modulePath)).default;
|
load = (await import(modulePath)).default;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ const getDescriptionInstruction = () =>
|
|||||||
|
|
||||||
const getOneLineCommitInstruction = () =>
|
const getOneLineCommitInstruction = () =>
|
||||||
config.OCO_ONE_LINE_COMMIT
|
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.'
|
? '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 = () =>
|
const getScopeInstruction = () =>
|
||||||
@@ -183,43 +183,47 @@ export const INIT_DIFF_PROMPT: OpenAI.Chat.Completions.ChatCompletionMessagePara
|
|||||||
});`
|
});`
|
||||||
};
|
};
|
||||||
|
|
||||||
const getContent = (translation: ConsistencyPrompt) => {
|
const COMMIT_TYPES = {
|
||||||
const getCommitString = (commitWithScope: string, commitWithoutScope?: string) => {
|
fix: '🐛',
|
||||||
if (config.OCO_OMIT_SCOPE && commitWithoutScope) {
|
feat: '✨'
|
||||||
return config.OCO_EMOJI
|
} as const;
|
||||||
? `🐛 ${removeConventionalCommitWord(commitWithoutScope)}`
|
|
||||||
: commitWithoutScope;
|
|
||||||
}
|
|
||||||
return config.OCO_EMOJI
|
|
||||||
? `🐛 ${removeConventionalCommitWord(commitWithScope)}`
|
|
||||||
: commitWithScope;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fix = getCommitString(
|
const generateCommitString = (
|
||||||
translation.commitFix,
|
type: keyof typeof COMMIT_TYPES,
|
||||||
translation.commitFixOmitScope
|
message: string
|
||||||
);
|
): string => {
|
||||||
|
const cleanMessage = removeConventionalCommitWord(message);
|
||||||
|
return config.OCO_EMOJI
|
||||||
|
? `${COMMIT_TYPES[type]} ${cleanMessage}`
|
||||||
|
: message;
|
||||||
|
};
|
||||||
|
|
||||||
const feat = config.OCO_OMIT_SCOPE && translation.commitFeatOmitScope
|
const getConsistencyContent = (translation: ConsistencyPrompt) => {
|
||||||
? (config.OCO_EMOJI
|
const fixMessage = config.OCO_OMIT_SCOPE && translation.commitFixOmitScope
|
||||||
? `✨ ${removeConventionalCommitWord(translation.commitFeatOmitScope)}`
|
? translation.commitFixOmitScope
|
||||||
: translation.commitFeatOmitScope)
|
: translation.commitFix;
|
||||||
: (config.OCO_EMOJI
|
|
||||||
? `✨ ${removeConventionalCommitWord(translation.commitFeat)}`
|
const featMessage = config.OCO_OMIT_SCOPE && translation.commitFeatOmitScope
|
||||||
: translation.commitFeat);
|
? translation.commitFeatOmitScope
|
||||||
|
: translation.commitFeat;
|
||||||
|
|
||||||
|
const fix = generateCommitString('fix', fixMessage);
|
||||||
|
const feat = config.OCO_ONE_LINE_COMMIT
|
||||||
|
? ''
|
||||||
|
: generateCommitString('feat', featMessage);
|
||||||
|
|
||||||
const description = config.OCO_DESCRIPTION
|
const description = config.OCO_DESCRIPTION
|
||||||
? translation.commitDescription
|
? translation.commitDescription
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return `${fix}\n${feat}\n${description}`;
|
return [fix, feat, description].filter(Boolean).join('\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
const INIT_CONSISTENCY_PROMPT = (
|
const INIT_CONSISTENCY_PROMPT = (
|
||||||
translation: ConsistencyPrompt
|
translation: ConsistencyPrompt
|
||||||
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
|
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: getContent(translation)
|
content: getConsistencyContent(translation)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getMainCommitPrompt = async (
|
export const getMainCommitPrompt = async (
|
||||||
|
|||||||
@@ -12,16 +12,39 @@ import { GroqEngine } from '../engine/groq';
|
|||||||
import { MLXEngine } from '../engine/mlx';
|
import { MLXEngine } from '../engine/mlx';
|
||||||
import { DeepseekEngine } from '../engine/deepseek';
|
import { DeepseekEngine } from '../engine/deepseek';
|
||||||
|
|
||||||
|
export function parseCustomHeaders(headers: any): Record<string, string> {
|
||||||
|
let parsedHeaders = {};
|
||||||
|
|
||||||
|
if (!headers) {
|
||||||
|
return parsedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof headers === 'object' && !Array.isArray(headers)) {
|
||||||
|
parsedHeaders = headers;
|
||||||
|
} else {
|
||||||
|
parsedHeaders = JSON.parse(headers);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Invalid OCO_API_CUSTOM_HEADERS format, ignoring custom headers');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
export function getEngine(): AiEngine {
|
export function getEngine(): AiEngine {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
const provider = config.OCO_AI_PROVIDER;
|
const provider = config.OCO_AI_PROVIDER;
|
||||||
|
|
||||||
|
const customHeaders = parseCustomHeaders(config.OCO_API_CUSTOM_HEADERS);
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
model: config.OCO_MODEL!,
|
model: config.OCO_MODEL!,
|
||||||
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
|
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
|
||||||
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
|
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
|
||||||
baseURL: config.OCO_API_URL!,
|
baseURL: config.OCO_API_URL!,
|
||||||
apiKey: config.OCO_API_KEY!
|
apiKey: config.OCO_API_KEY!,
|
||||||
|
customHeaders
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const gitAdd = async ({ files }: { files: string[] }) => {
|
|||||||
|
|
||||||
await execa('git', ['add', ...files]);
|
await execa('git', ['add', ...files]);
|
||||||
|
|
||||||
gitAddSpinner.stop('Done');
|
gitAddSpinner.stop(`Staged ${files.length} files`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDiff = async ({ files }: { files: string[] }) => {
|
export const getDiff = async ({ files }: { files: string[] }) => {
|
||||||
|
|||||||
51
src/utils/removeContentTags.ts
Normal file
51
src/utils/removeContentTags.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
@@ -125,7 +125,7 @@ describe('cli flow to push git branch', () => {
|
|||||||
await render('git', ['add index.ts'], { cwd: gitDir });
|
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
|
||||||
const { queryByText, findByText, userEvent } = await render(
|
const { queryByText, findByText, userEvent } = await render(
|
||||||
`OCO_AI_PROVIDER='test' node`,
|
`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`,
|
||||||
[resolve('./out/cli.cjs')],
|
[resolve('./out/cli.cjs')],
|
||||||
{ cwd: gitDir }
|
{ cwd: gitDir }
|
||||||
);
|
);
|
||||||
@@ -158,7 +158,7 @@ describe('cli flow to push git branch', () => {
|
|||||||
await render('git', ['add index.ts'], { cwd: gitDir });
|
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
|
||||||
const { findByText, userEvent } = await render(
|
const { findByText, userEvent } = await render(
|
||||||
`OCO_AI_PROVIDER='test' node`,
|
`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`,
|
||||||
[resolve('./out/cli.cjs')],
|
[resolve('./out/cli.cjs')],
|
||||||
{ cwd: gitDir }
|
{ cwd: gitDir }
|
||||||
);
|
);
|
||||||
@@ -186,7 +186,7 @@ describe('cli flow to push git branch', () => {
|
|||||||
await render('git', ['add index.ts'], { cwd: gitDir });
|
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
|
||||||
const { findByText, userEvent } = await render(
|
const { findByText, userEvent } = await render(
|
||||||
`OCO_AI_PROVIDER='test' node`,
|
`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`,
|
||||||
[resolve('./out/cli.cjs')],
|
[resolve('./out/cli.cjs')],
|
||||||
{ cwd: gitDir }
|
{ cwd: gitDir }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ it('cli flow to generate commit message for 1 new file (staged)', async () => {
|
|||||||
await render('echo' ,[`'console.log("Hello World");' > index.ts`], { cwd: gitDir });
|
await render('echo' ,[`'console.log("Hello World");' > index.ts`], { cwd: gitDir });
|
||||||
await render('git' ,['add index.ts'], { cwd: gitDir });
|
await render('git' ,['add index.ts'], { cwd: gitDir });
|
||||||
|
|
||||||
const { queryByText, findByText, userEvent } = await render(`OCO_AI_PROVIDER='test' node`, [resolve('./out/cli.cjs')], { 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('No files are staged')).not.toBeInTheConsole();
|
||||||
expect(await queryByText('Do you want to stage all files and generate commit message?')).not.toBeInTheConsole();
|
expect(await queryByText('Do you want to stage all files and generate commit message?')).not.toBeInTheConsole();
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ it('cli flow to generate commit message for 1 changed file (not staged)', async
|
|||||||
|
|
||||||
await render('echo' ,[`'console.log("Good night World");' >> index.ts`], { cwd: gitDir });
|
await render('echo' ,[`'console.log("Good night World");' >> index.ts`], { cwd: gitDir });
|
||||||
|
|
||||||
const { findByText, userEvent } = await render(`OCO_AI_PROVIDER='test' node`, [resolve('./out/cli.cjs')], { 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('No files are staged')).toBeInTheConsole();
|
||||||
expect(await findByText('Do you want to stage all files and generate commit message?')).toBeInTheConsole();
|
expect(await findByText('Do you want to stage all files and generate commit message?')).toBeInTheConsole();
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { prepareEnvironment, wait } from '../utils';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
function getAbsolutePath(relativePath: string) {
|
function getAbsolutePath(relativePath: string) {
|
||||||
const scriptDir = path.dirname(__filename);
|
// Use process.cwd() which should be the project root during test execution
|
||||||
return path.resolve(scriptDir, relativePath);
|
return path.resolve(process.cwd(), 'test/e2e/prompt-module', relativePath);
|
||||||
}
|
}
|
||||||
async function setupCommitlint(dir: string, ver: 9 | 18 | 19) {
|
async function setupCommitlint(dir: string, ver: 9 | 18 | 19) {
|
||||||
let packagePath, packageJsonPath, configPath;
|
let packagePath, packageJsonPath, configPath;
|
||||||
@@ -47,7 +47,7 @@ describe('cli flow to run "oco commitlint force"', () => {
|
|||||||
`
|
`
|
||||||
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
||||||
OCO_PROMPT_MODULE='@commitlint' \
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
OCO_AI_PROVIDER='test' \
|
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
|
||||||
node ${resolve('./out/cli.cjs')} commitlint force \
|
node ${resolve('./out/cli.cjs')} commitlint force \
|
||||||
`,
|
`,
|
||||||
[],
|
[],
|
||||||
@@ -83,7 +83,7 @@ describe('cli flow to run "oco commitlint force"', () => {
|
|||||||
`
|
`
|
||||||
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
||||||
OCO_PROMPT_MODULE='@commitlint' \
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
OCO_AI_PROVIDER='test' \
|
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
|
||||||
node ${resolve('./out/cli.cjs')} commitlint force \
|
node ${resolve('./out/cli.cjs')} commitlint force \
|
||||||
`,
|
`,
|
||||||
[],
|
[],
|
||||||
@@ -119,7 +119,7 @@ describe('cli flow to run "oco commitlint force"', () => {
|
|||||||
`
|
`
|
||||||
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
||||||
OCO_PROMPT_MODULE='@commitlint' \
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
OCO_AI_PROVIDER='test' \
|
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
|
||||||
node ${resolve('./out/cli.cjs')} commitlint force \
|
node ${resolve('./out/cli.cjs')} commitlint force \
|
||||||
`,
|
`,
|
||||||
[],
|
[],
|
||||||
@@ -160,7 +160,7 @@ describe('cli flow to generate commit message using @commitlint prompt-module',
|
|||||||
`
|
`
|
||||||
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
||||||
OCO_PROMPT_MODULE='@commitlint' \
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
OCO_AI_PROVIDER='test' \
|
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
|
||||||
node ${resolve('./out/cli.cjs')} commitlint force \
|
node ${resolve('./out/cli.cjs')} commitlint force \
|
||||||
`,
|
`,
|
||||||
[],
|
[],
|
||||||
@@ -175,7 +175,7 @@ describe('cli flow to generate commit message using @commitlint prompt-module',
|
|||||||
`
|
`
|
||||||
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
||||||
OCO_PROMPT_MODULE='@commitlint' \
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
OCO_AI_PROVIDER='test' \
|
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
|
||||||
node ${resolve('./out/cli.cjs')} commitlint get \
|
node ${resolve('./out/cli.cjs')} commitlint get \
|
||||||
`,
|
`,
|
||||||
[],
|
[],
|
||||||
@@ -193,7 +193,7 @@ describe('cli flow to generate commit message using @commitlint prompt-module',
|
|||||||
`
|
`
|
||||||
OCO_TEST_MOCK_TYPE='commit-message' \
|
OCO_TEST_MOCK_TYPE='commit-message' \
|
||||||
OCO_PROMPT_MODULE='@commitlint' \
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
OCO_AI_PROVIDER='test' \
|
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
|
||||||
node ${resolve('./out/cli.cjs')} \
|
node ${resolve('./out/cli.cjs')} \
|
||||||
`,
|
`,
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'cli-testing-library/extend-expect'
|
|
||||||
import { configure } from 'cli-testing-library'
|
|
||||||
import { jest } from '@jest/globals';
|
import { jest } from '@jest/globals';
|
||||||
|
import 'cli-testing-library/extend-expect';
|
||||||
|
import { configure } from 'cli-testing-library';
|
||||||
|
|
||||||
|
// Make Jest available globally
|
||||||
global.jest = jest;
|
global.jest = jest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adjusted the wait time for waitFor/findByText to 2000ms, because the default 1000ms makes the test results flaky
|
* Adjusted the wait time for waitFor/findByText to 2000ms, because the default 1000ms makes the test results flaky
|
||||||
*/
|
*/
|
||||||
configure({ asyncUtilTimeout: 2000 })
|
configure({ asyncUtilTimeout: 2000 });
|
||||||
|
|||||||
@@ -123,6 +123,30 @@ describe('config', () => {
|
|||||||
expect(config.OCO_OMIT_SCOPE).toEqual(true);
|
expect(config.OCO_OMIT_SCOPE).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle custom HTTP headers correctly', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
|
OCO_API_CUSTOM_HEADERS: '{"X-Global-Header": "global-value"}'
|
||||||
|
});
|
||||||
|
|
||||||
|
envConfigFile = await generateConfig('.env', {
|
||||||
|
OCO_API_CUSTOM_HEADERS: '{"Authorization": "Bearer token123", "X-Custom-Header": "test-value"}'
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = getConfig({
|
||||||
|
globalPath: globalConfigFile.filePath,
|
||||||
|
envPath: envConfigFile.filePath
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config).not.toEqual(null);
|
||||||
|
expect(config.OCO_API_CUSTOM_HEADERS).toEqual({"Authorization": "Bearer token123", "X-Custom-Header": "test-value"});
|
||||||
|
|
||||||
|
// No need to parse JSON again since it's already an object
|
||||||
|
const parsedHeaders = config.OCO_API_CUSTOM_HEADERS;
|
||||||
|
expect(parsedHeaders).toHaveProperty('Authorization', 'Bearer token123');
|
||||||
|
expect(parsedHeaders).toHaveProperty('X-Custom-Header', 'test-value');
|
||||||
|
expect(parsedHeaders).not.toHaveProperty('X-Global-Header');
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle empty local config correctly', async () => {
|
it('should handle empty local config correctly', async () => {
|
||||||
globalConfigFile = await generateConfig('.opencommit', {
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
OCO_API_KEY: 'global-key',
|
OCO_API_KEY: 'global-key',
|
||||||
|
|||||||
57
test/unit/removeContentTags.test.ts
Normal file
57
test/unit/removeContentTags.test.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { removeContentTags } from '../../src/utils/removeContentTags';
|
||||||
|
|
||||||
|
describe('removeContentTags', () => {
|
||||||
|
it('should remove content wrapped in specified tags', () => {
|
||||||
|
const content = 'This is <think>something to hide</think> visible content';
|
||||||
|
const result = removeContentTags(content, 'think');
|
||||||
|
expect(result).toBe('This is visible content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple tag occurrences', () => {
|
||||||
|
const content = '<think>hidden</think> visible <think>also hidden</think> text';
|
||||||
|
const result = removeContentTags(content, 'think');
|
||||||
|
expect(result).toBe('visible text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiline content within tags', () => {
|
||||||
|
const content = 'Start <think>hidden\nover multiple\nlines</think> End';
|
||||||
|
const result = removeContentTags(content, 'think');
|
||||||
|
expect(result).toBe('Start End');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return content as is when tag is not found', () => {
|
||||||
|
const content = 'Content without any tags';
|
||||||
|
const result = removeContentTags(content, 'think');
|
||||||
|
expect(result).toBe('Content without any tags');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with different tag names', () => {
|
||||||
|
const content = 'This is <custom>something to hide</custom> visible content';
|
||||||
|
const result = removeContentTags(content, 'custom');
|
||||||
|
expect(result).toBe('This is visible content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null content', () => {
|
||||||
|
const content = null;
|
||||||
|
const result = removeContentTags(content, 'think');
|
||||||
|
expect(result).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle undefined content', () => {
|
||||||
|
const content = undefined;
|
||||||
|
const result = removeContentTags(content, 'think');
|
||||||
|
expect(result).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trim the result', () => {
|
||||||
|
const content = ' <think>hidden</think> visible ';
|
||||||
|
const result = removeContentTags(content, 'think');
|
||||||
|
expect(result).toBe('visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle nested tags correctly', () => {
|
||||||
|
const content = 'Outside <think>Inside <think>Nested</think></think> End';
|
||||||
|
const result = removeContentTags(content, 'think');
|
||||||
|
expect(result).toBe('Outside End');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,10 +3,10 @@
|
|||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"lib": ["ES6", "ES2020"],
|
"lib": ["ES6", "ES2020"],
|
||||||
|
|
||||||
"module": "CommonJS",
|
"module": "NodeNext",
|
||||||
|
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "NodeNext",
|
||||||
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user