mirror of
https://github.com/di-sukharev/opencommit.git
synced 2026-01-13 07:38:01 -05:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf578da16a | ||
|
|
41330d5517 | ||
|
|
a703fde7b2 | ||
|
|
c5ee5cd8df | ||
|
|
312540456a | ||
|
|
7ddbaf477a | ||
|
|
9a0f412fff | ||
|
|
7cd3ef09cb | ||
|
|
f814c6b89d | ||
|
|
74024a4997 | ||
|
|
cb7f5dd44d | ||
|
|
9cf2db84a9 | ||
|
|
ec307d561f | ||
|
|
058bad95cd | ||
|
|
7469633e3d | ||
|
|
278e4cb4c2 | ||
|
|
e19305dee2 | ||
|
|
673eee209d | ||
|
|
91399a0c68 | ||
|
|
a4480893cb | ||
|
|
c410486e30 | ||
|
|
5cda8b1b03 | ||
|
|
0ac7211ff7 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
.env
|
||||
46
.github/workflows/test.yml
vendored
Normal file
46
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Testing
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
- name: Run Unit Tests
|
||||
run: npm run test:unit
|
||||
e2e-test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install git
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git
|
||||
git --version
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.email "test@example.com"
|
||||
git config --global user.name "Test User"
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Run E2E Tests
|
||||
run: npm run test:e2e
|
||||
32
README.md
32
README.md
@@ -84,6 +84,14 @@ This is due to limit the number of tokens sent in each request. However, if you
|
||||
oco --fgm
|
||||
```
|
||||
|
||||
#### Skip Commit Confirmation
|
||||
|
||||
This flag allows users to automatically commit the changes without having to manually confirm the commit message. This is useful for users who want to streamline the commit process and avoid additional steps. To use this flag, you can run the following command:
|
||||
|
||||
```
|
||||
oco --yes
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Local per repo configuration
|
||||
@@ -97,7 +105,7 @@ OCO_TOKENS_MAX_OUTPUT=<max response tokens (default: 500)>
|
||||
OCO_OPENAI_BASE_PATH=<may be used to set proxy path to OpenAI api>
|
||||
OCO_DESCRIPTION=<postface a message with ~3 sentences description of the changes>
|
||||
OCO_EMOJI=<boolean, add GitMoji>
|
||||
OCO_MODEL=<either 'gpt-4', 'gpt-3.5-turbo' (default), 'gpt-3.5-turbo-0125', 'gpt-4-1106-preview', 'gpt-4-turbo-preview' or 'gpt-4-0125-preview'>
|
||||
OCO_MODEL=<either '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'>
|
||||
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>
|
||||
@@ -150,6 +158,20 @@ oco config set OCO_MODEL=gpt-4-0125-preview
|
||||
|
||||
Make sure that you spell it `gpt-4` (lowercase) and that you have API access to the 4th model. Even if you have ChatGPT+, that doesn't necessarily mean that you have API access to GPT-4.
|
||||
|
||||
### Switch to Azure OpenAI
|
||||
|
||||
By default OpenCommit uses [OpenAI](https://openai.com).
|
||||
|
||||
You could switch to [Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/)🚀
|
||||
|
||||
```sh
|
||||
opencommit config set OCO_AI_PROVIDER=azure
|
||||
```
|
||||
|
||||
Of course need to set 'OPENAI_API_KEY'. And also need to set the
|
||||
'OPENAI_BASE_PATH' for the endpoint and set the deployment name to
|
||||
'model'.
|
||||
|
||||
### Locale configuration
|
||||
|
||||
To globally specify the language used to generate commit messages:
|
||||
@@ -169,6 +191,14 @@ oco config set OCO_LANGUAGE=française
|
||||
The default language setting is **English**
|
||||
All available languages are currently listed in the [i18n](https://github.com/di-sukharev/opencommit/tree/master/src/i18n) folder
|
||||
|
||||
### Push to git
|
||||
|
||||
Pushing to git is on by default but if you would like to turn it off just use:
|
||||
|
||||
```sh
|
||||
oco config set OCO_GITPUSH=false
|
||||
```
|
||||
|
||||
### Switch to `@commitlint`
|
||||
|
||||
OpenCommit allows you to choose the prompt module used to generate commit messages. By default, OpenCommit uses its conventional-commit message generator. However, you can switch to using the `@commitlint` prompt module if you prefer. This option lets you generate commit messages in respect with the local config.
|
||||
|
||||
28
jest.config.ts
Normal file
28
jest.config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* For a detailed explanation regarding each configuration property, visit:
|
||||
* https://jestjs.io/docs/configuration
|
||||
*/
|
||||
|
||||
import type { Config } from 'jest';
|
||||
|
||||
const config: Config = {
|
||||
testTimeout: 100_000,
|
||||
coverageProvider: 'v8',
|
||||
moduleDirectories: ['node_modules', 'src'],
|
||||
preset: 'ts-jest/presets/js-with-ts-esm',
|
||||
setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'],
|
||||
testEnvironment: 'node',
|
||||
testRegex: ['.*\\.test\\.ts$'],
|
||||
transformIgnorePatterns: ['node_modules/(?!cli-testing-library)'],
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': [
|
||||
'ts-jest',
|
||||
{
|
||||
diagnostics: false,
|
||||
useESM: true
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
23092
out/cli.cjs
23092
out/cli.cjs
File diff suppressed because one or more lines are too long
36815
out/github-action.cjs
36815
out/github-action.cjs
File diff suppressed because one or more lines are too long
Binary file not shown.
6474
package-lock.json
generated
6474
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "opencommit",
|
||||
"version": "3.0.12",
|
||||
"version": "3.0.16",
|
||||
"description": "Auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
|
||||
"keywords": [
|
||||
"git",
|
||||
@@ -47,19 +47,29 @@
|
||||
"build:push": "npm run build && git add . && git commit -m 'build' && git push",
|
||||
"deploy": "npm version patch && npm run build:push && git push --tags && npm publish --tag latest",
|
||||
"lint": "eslint src --ext ts && tsc --noEmit",
|
||||
"format": "prettier --write src"
|
||||
"format": "prettier --write src",
|
||||
"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": "jest test/e2e",
|
||||
"test:e2e:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:e2e"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/types": "^17.4.4",
|
||||
"@types/ini": "^1.3.31",
|
||||
"@types/inquirer": "^9.0.3",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^16.18.14",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"cli-testing-library": "^2.0.2",
|
||||
"dotenv": "^16.0.3",
|
||||
"esbuild": "^0.15.18",
|
||||
"eslint": "^8.28.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^2.8.4",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
@@ -67,6 +77,8 @@
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/github": "^5.1.1",
|
||||
"@azure/openai": "^1.0.0-beta.12",
|
||||
"@anthropic-ai/sdk": "^0.19.2",
|
||||
"@clack/prompts": "^0.6.1",
|
||||
"@dqbd/tiktoken": "^1.0.2",
|
||||
"@octokit/webhooks-schemas": "^6.11.0",
|
||||
|
||||
10
src/cli.ts
10
src/cli.ts
@@ -18,7 +18,13 @@ cli(
|
||||
name: 'opencommit',
|
||||
commands: [configCommand, hookCommand, commitlintConfigCommand],
|
||||
flags: {
|
||||
fgm: Boolean
|
||||
fgm: Boolean,
|
||||
yes: {
|
||||
type: Boolean,
|
||||
alias: 'y',
|
||||
description: 'Skip commit confirmation prompt',
|
||||
default: false
|
||||
}
|
||||
},
|
||||
ignoreArgv: (type) => type === 'unknown-flag' || type === 'argument',
|
||||
help: { description: packageJSON.description }
|
||||
@@ -29,7 +35,7 @@ cli(
|
||||
if (await isHookCalled()) {
|
||||
prepareCommitMessageHook();
|
||||
} else {
|
||||
commit(extraArgs, false, flags.fgm);
|
||||
commit(extraArgs, false, flags.fgm, flags.yes);
|
||||
}
|
||||
},
|
||||
extraArgs
|
||||
|
||||
@@ -41,7 +41,8 @@ const checkMessageTemplate = (extraArgs: string[]): string | false => {
|
||||
const generateCommitMessageFromGitDiff = async (
|
||||
diff: string,
|
||||
extraArgs: string[],
|
||||
fullGitMojiSpec: boolean
|
||||
fullGitMojiSpec: boolean,
|
||||
skipCommitConfirmation: boolean
|
||||
): Promise<void> => {
|
||||
await assertGitRepo();
|
||||
const commitSpinner = spinner();
|
||||
@@ -76,7 +77,7 @@ ${commitMessage}
|
||||
${chalk.grey('——————————————————')}`
|
||||
);
|
||||
|
||||
const isCommitConfirmedByUser = await confirm({
|
||||
const isCommitConfirmedByUser = skipCommitConfirmation || await confirm({
|
||||
message: 'Confirm the commit message?'
|
||||
});
|
||||
|
||||
@@ -94,13 +95,17 @@ ${chalk.grey('——————————————————')}`
|
||||
|
||||
const remotes = await getGitRemotes();
|
||||
|
||||
// user isn't pushing, return early
|
||||
if (config?.OCO_GITPUSH === false)
|
||||
return
|
||||
|
||||
if (!remotes.length) {
|
||||
const { stdout } = await execa('git', ['push']);
|
||||
if (stdout) outro(stdout);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (remotes.length === 1) {
|
||||
if (remotes.length === 1 && config?.OCO_GITPUSH !== true) {
|
||||
const isPushConfirmedByUser = await confirm({
|
||||
message: 'Do you want to run `git push`?'
|
||||
});
|
||||
@@ -150,6 +155,18 @@ ${chalk.grey('——————————————————')}`
|
||||
} else outro(`${chalk.gray('✖')} process cancelled`);
|
||||
}
|
||||
}
|
||||
if (!isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
|
||||
const regenerateMessage = await confirm({
|
||||
message: 'Do you want to regenerate the message ?'
|
||||
});
|
||||
if (regenerateMessage && !isCancel(isCommitConfirmedByUser)) {
|
||||
await generateCommitMessageFromGitDiff(
|
||||
diff,
|
||||
extraArgs,
|
||||
fullGitMojiSpec
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
commitSpinner.stop('📝 Commit message generated');
|
||||
|
||||
@@ -162,7 +179,8 @@ ${chalk.grey('——————————————————')}`
|
||||
export async function commit(
|
||||
extraArgs: string[] = [],
|
||||
isStageAllFlag: Boolean = false,
|
||||
fullGitMojiSpec: boolean = false
|
||||
fullGitMojiSpec: boolean = false,
|
||||
skipCommitConfirmation: boolean = false
|
||||
) {
|
||||
if (isStageAllFlag) {
|
||||
const changedFiles = await getChangedFiles();
|
||||
@@ -234,7 +252,8 @@ export async function commit(
|
||||
generateCommitMessageFromGitDiff(
|
||||
await getDiff({ files: stagedFiles }),
|
||||
extraArgs,
|
||||
fullGitMojiSpec
|
||||
fullGitMojiSpec,
|
||||
skipCommitConfirmation
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -4,17 +4,17 @@ import * as dotenv from 'dotenv';
|
||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { parse as iniParse, stringify as iniStringify } from 'ini';
|
||||
import { homedir } from 'os';
|
||||
import { join as pathJoin } from 'path';
|
||||
import { join as pathJoin, resolve as pathResolve } from 'path';
|
||||
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
|
||||
import { COMMANDS } from '../CommandsEnum';
|
||||
import { getI18nLocal } from '../i18n';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export enum CONFIG_KEYS {
|
||||
OCO_OPENAI_API_KEY = 'OCO_OPENAI_API_KEY',
|
||||
OCO_ANTHROPIC_API_KEY = 'OCO_ANTHROPIC_API_KEY',
|
||||
OCO_AZURE_API_KEY = 'OCO_AZURE_API_KEY',
|
||||
OCO_TOKENS_MAX_INPUT = 'OCO_TOKENS_MAX_INPUT',
|
||||
OCO_TOKENS_MAX_OUTPUT = 'OCO_TOKENS_MAX_OUTPUT',
|
||||
OCO_OPENAI_BASE_PATH = 'OCO_OPENAI_BASE_PATH',
|
||||
@@ -25,7 +25,9 @@ export enum CONFIG_KEYS {
|
||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER = 'OCO_MESSAGE_TEMPLATE_PLACEHOLDER',
|
||||
OCO_PROMPT_MODULE = 'OCO_PROMPT_MODULE',
|
||||
OCO_AI_PROVIDER = 'OCO_AI_PROVIDER',
|
||||
OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT'
|
||||
OCO_GITPUSH = 'OCO_GITPUSH',
|
||||
OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT',
|
||||
OCO_AZURE_ENDPOINT = 'OCO_AZURE_ENDPOINT'
|
||||
}
|
||||
|
||||
export enum CONFIG_MODES {
|
||||
@@ -33,6 +35,32 @@ export enum CONFIG_MODES {
|
||||
set = 'set'
|
||||
}
|
||||
|
||||
export const MODEL_LIST = {
|
||||
openai: ['gpt-3.5-turbo',
|
||||
'gpt-3.5-turbo-0125',
|
||||
'gpt-4',
|
||||
'gpt-4-turbo',
|
||||
'gpt-4-1106-preview',
|
||||
'gpt-4-turbo-preview',
|
||||
'gpt-4-0125-preview',
|
||||
'gpt-4o'],
|
||||
|
||||
anthropic: ['claude-3-haiku-20240307',
|
||||
'claude-3-sonnet-20240229',
|
||||
'claude-3-opus-20240229']
|
||||
}
|
||||
|
||||
const getDefaultModel = (provider: string | undefined): string => {
|
||||
switch (provider) {
|
||||
case 'ollama':
|
||||
return '';
|
||||
case 'anthropic':
|
||||
return MODEL_LIST.anthropic[0];
|
||||
default:
|
||||
return MODEL_LIST.openai[0];
|
||||
}
|
||||
};
|
||||
|
||||
export enum DEFAULT_TOKEN_LIMITS {
|
||||
DEFAULT_MAX_TOKENS_INPUT = 4096,
|
||||
DEFAULT_MAX_TOKENS_OUTPUT = 500
|
||||
@@ -56,19 +84,34 @@ export const configValidators = {
|
||||
[CONFIG_KEYS.OCO_OPENAI_API_KEY](value: any, config: any = {}) {
|
||||
//need api key unless running locally with ollama
|
||||
validateConfig(
|
||||
'API_KEY',
|
||||
value || config.OCO_AI_PROVIDER == 'ollama',
|
||||
'You need to provide an API key'
|
||||
'OpenAI API_KEY',
|
||||
value || config.OCO_ANTHROPIC_API_KEY || config.OCO_AI_PROVIDER.startsWith('ollama') || config.OCO_AZURE_API_KEY || config.OCO_AI_PROVIDER == 'test' ,
|
||||
'You need to provide an OpenAI/Anthropic/Azure API key'
|
||||
);
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_OPENAI_API_KEY,
|
||||
value.startsWith('sk-'),
|
||||
'Must start with "sk-"'
|
||||
value.startsWith('sk-') || config.OCO_AI_PROVIDER != 'openai',
|
||||
'Must start with "sk-" for openai provider'
|
||||
);
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_AZURE_API_KEY](value: any, config: any = {}) {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_OPENAI_API_KEY,
|
||||
config[CONFIG_KEYS.OCO_OPENAI_BASE_PATH] || value.length === 51,
|
||||
'Must be 51 characters long'
|
||||
'ANTHROPIC_API_KEY',
|
||||
value || config.OCO_OPENAI_API_KEY || config.OCO_AZURE_API_KEY || config.OCO_AI_PROVIDER == 'ollama' || config.OCO_AI_PROVIDER == 'test',
|
||||
'You need to provide an OpenAI/Anthropic/Azure API key'
|
||||
);
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_ANTHROPIC_API_KEY](value: any, config: any = {}) {
|
||||
validateConfig(
|
||||
'ANTHROPIC_API_KEY',
|
||||
value || config.OCO_OPENAI_API_KEY || config.OCO_AI_PROVIDER == 'ollama' || config.OCO_AI_PROVIDER == 'test',
|
||||
'You need to provide an OpenAI/Anthropic/Azure API key'
|
||||
);
|
||||
|
||||
return value;
|
||||
@@ -150,21 +193,22 @@ export const configValidators = {
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_MODEL](value: any) {
|
||||
[CONFIG_KEYS.OCO_MODEL](value: any, config: any = {}) {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_MODEL,
|
||||
[
|
||||
'gpt-3.5-turbo',
|
||||
'gpt-3.5-turbo-0125',
|
||||
'gpt-4',
|
||||
'gpt-4-1106-preview',
|
||||
'gpt-4-turbo-preview',
|
||||
'gpt-4-0125-preview'
|
||||
].includes(value),
|
||||
`${value} is not supported yet, use 'gpt-4', 'gpt-3.5-turbo' (default), 'gpt-3.5-turbo-0125', 'gpt-4-1106-preview', 'gpt-4-turbo-preview' or 'gpt-4-0125-preview'`
|
||||
[...MODEL_LIST.openai, ...MODEL_LIST.anthropic].includes(value) || config.OCO_AI_PROVIDER == 'ollama' || config.OCO_AI_PROVIDER == 'test'|| config.OCO_AI_PROVIDER == 'azure',
|
||||
`${value} is not supported yet, use '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', 'gpt-4-0125-preview', 'claude-3-opus-20240229', 'claude-3-sonnet-20240229' or 'claude-3-haiku-20240307'`
|
||||
);
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_MODEL,
|
||||
typeof value === 'string' &&
|
||||
value.match(/^[a-zA-Z0-9~\-]{1,63}[a-zA-Z0-9]$/) ||
|
||||
config.OCO_AI_PROVIDER != 'azure',
|
||||
`${value} is not model deployed name.`
|
||||
);
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER](value: any) {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
|
||||
@@ -180,7 +224,15 @@ export const configValidators = {
|
||||
['conventional-commit', '@commitlint'].includes(value),
|
||||
`${value} is not supported yet, use '@commitlint' or 'conventional-commit' (default)`
|
||||
);
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_GITPUSH](value: any) {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_GITPUSH,
|
||||
typeof value === 'boolean',
|
||||
'Must be true or false'
|
||||
);
|
||||
return value;
|
||||
},
|
||||
|
||||
@@ -188,11 +240,14 @@ export const configValidators = {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_AI_PROVIDER,
|
||||
[
|
||||
'',
|
||||
'openai',
|
||||
'ollama'
|
||||
].includes(value),
|
||||
`${value} is not supported yet, use 'ollama' or 'openai' (default)`
|
||||
'',
|
||||
'openai',
|
||||
'anthropic',
|
||||
'azure',
|
||||
'ollama',
|
||||
'test'
|
||||
].includes(value) || value.startsWith('ollama'),
|
||||
`${value} is not supported yet, use 'ollama/{model}', 'azure', 'anthropic' or 'openai' (default)`
|
||||
);
|
||||
return value;
|
||||
},
|
||||
@@ -204,6 +259,15 @@ export const configValidators = {
|
||||
'Must be true or false'
|
||||
);
|
||||
|
||||
return value;
|
||||
},
|
||||
[CONFIG_KEYS.OCO_AZURE_ENDPOINT](value: any) {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_AZURE_ENDPOINT,
|
||||
value.includes('openai.azure.com'),
|
||||
'Must be in format "https://<resource name>.openai.azure.com/"'
|
||||
);
|
||||
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -212,11 +276,21 @@ export type ConfigType = {
|
||||
[key in CONFIG_KEYS]?: any;
|
||||
};
|
||||
|
||||
const configPath = pathJoin(homedir(), '.opencommit');
|
||||
const defaultConfigPath = pathJoin(homedir(), '.opencommit');
|
||||
const defaultEnvPath = pathResolve(process.cwd(), '.env');
|
||||
|
||||
export const getConfig = (): ConfigType | null => {
|
||||
export const getConfig = ({
|
||||
configPath = defaultConfigPath,
|
||||
envPath = defaultEnvPath
|
||||
}: {
|
||||
configPath?: string
|
||||
envPath?: string
|
||||
} = {}): ConfigType | null => {
|
||||
dotenv.config({ path: envPath });
|
||||
const configFromEnv = {
|
||||
OCO_OPENAI_API_KEY: process.env.OCO_OPENAI_API_KEY,
|
||||
OCO_ANTHROPIC_API_KEY: process.env.OCO_ANTHROPIC_API_KEY,
|
||||
OCO_AZURE_API_KEY: process.env.OCO_AZURE_API_KEY,
|
||||
OCO_TOKENS_MAX_INPUT: process.env.OCO_TOKENS_MAX_INPUT
|
||||
? Number(process.env.OCO_TOKENS_MAX_INPUT)
|
||||
: undefined,
|
||||
@@ -226,13 +300,16 @@ export const getConfig = (): ConfigType | null => {
|
||||
OCO_OPENAI_BASE_PATH: process.env.OCO_OPENAI_BASE_PATH,
|
||||
OCO_DESCRIPTION: process.env.OCO_DESCRIPTION === 'true' ? true : false,
|
||||
OCO_EMOJI: process.env.OCO_EMOJI === 'true' ? true : false,
|
||||
OCO_MODEL: process.env.OCO_MODEL || 'gpt-3.5-turbo',
|
||||
OCO_MODEL: process.env.OCO_MODEL || getDefaultModel(process.env.OCO_AI_PROVIDER),
|
||||
OCO_LANGUAGE: process.env.OCO_LANGUAGE || 'en',
|
||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER:
|
||||
process.env.OCO_MESSAGE_TEMPLATE_PLACEHOLDER || '$msg',
|
||||
OCO_PROMPT_MODULE: process.env.OCO_PROMPT_MODULE || 'conventional-commit',
|
||||
OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER || 'openai',
|
||||
OCO_ONE_LINE_COMMIT: process.env.OCO_ONE_LINE_COMMIT === 'true' ? true : false
|
||||
OCO_GITPUSH: process.env.OCO_GITPUSH === 'false' ? false : true,
|
||||
OCO_ONE_LINE_COMMIT:
|
||||
process.env.OCO_ONE_LINE_COMMIT === 'true' ? true : false,
|
||||
OCO_AZURE_ENDPOINT: process.env.OCO_AZURE_ENDPOINT || '',
|
||||
};
|
||||
|
||||
const configExists = existsSync(configPath);
|
||||
@@ -243,7 +320,6 @@ export const getConfig = (): ConfigType | null => {
|
||||
|
||||
for (const configKey of Object.keys(config)) {
|
||||
if (
|
||||
!config[configKey] ||
|
||||
['null', 'undefined'].includes(config[configKey])
|
||||
) {
|
||||
config[configKey] = undefined;
|
||||
@@ -258,7 +334,7 @@ export const getConfig = (): ConfigType | null => {
|
||||
|
||||
config[configKey] = validValue;
|
||||
} catch (error) {
|
||||
outro(`Unknown '${configKey}' config option.`);
|
||||
outro(`Unknown '${configKey}' config option or missing validator.`);
|
||||
outro(
|
||||
`Manually fix the '.env' file or global '~/.opencommit' config file.`
|
||||
);
|
||||
@@ -269,7 +345,7 @@ export const getConfig = (): ConfigType | null => {
|
||||
return config;
|
||||
};
|
||||
|
||||
export const setConfig = (keyValues: [key: string, value: string][]) => {
|
||||
export const setConfig = (keyValues: [key: string, value: string][], configPath: string = defaultConfigPath) => {
|
||||
const config = getConfig() || {};
|
||||
|
||||
for (const [configKey, configValue] of keyValues) {
|
||||
|
||||
@@ -39,9 +39,9 @@ export const prepareCommitMessageHook = async (
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
if (!config?.OCO_OPENAI_API_KEY) {
|
||||
if (!config?.OCO_OPENAI_API_KEY && !config?.OCO_ANTHROPIC_API_KEY && !config?.OCO_AZURE_API_KEY) {
|
||||
throw new Error(
|
||||
'No OPEN_AI_API exists. Set your OPEN_AI_API=<key> in ~/.opencommit'
|
||||
'No OPEN_AI_API or OCO_ANTHROPIC_API_KEY or OCO_AZURE_API_KEY exists. Set your key in ~/.opencommit'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
124
src/engine/anthropic.ts
Normal file
124
src/engine/anthropic.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import axios from 'axios';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import {ChatCompletionRequestMessage} from 'openai'
|
||||
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources';
|
||||
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
|
||||
import {
|
||||
CONFIG_MODES,
|
||||
DEFAULT_TOKEN_LIMITS,
|
||||
getConfig
|
||||
} from '../commands/config';
|
||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||
import { tokenCount } from '../utils/tokenCount';
|
||||
import { AiEngine } from './Engine';
|
||||
import { MODEL_LIST } from '../commands/config';
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
const MAX_TOKENS_OUTPUT =
|
||||
config?.OCO_TOKENS_MAX_OUTPUT ||
|
||||
DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
||||
const MAX_TOKENS_INPUT =
|
||||
config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
||||
|
||||
let provider = config?.OCO_AI_PROVIDER;
|
||||
let apiKey = config?.OCO_ANTHROPIC_API_KEY;
|
||||
const [command, mode] = process.argv.slice(2);
|
||||
if (
|
||||
provider === 'anthropic' &&
|
||||
!apiKey &&
|
||||
command !== 'config' &&
|
||||
mode !== CONFIG_MODES.set
|
||||
) {
|
||||
intro('opencommit');
|
||||
|
||||
outro(
|
||||
'OCO_ANTHROPIC_API_KEY is not set, please run `oco config set OCO_ANTHROPIC_API_KEY=<your token> . If you are using Claude, 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;
|
||||
if (provider === 'anthropic' &&
|
||||
!MODEL_LIST.anthropic.includes(MODEL) &&
|
||||
command !== 'config' &&
|
||||
mode !== CONFIG_MODES.set) {
|
||||
outro(
|
||||
`${chalk.red('✖')} Unsupported model ${MODEL} for Anthropic. Supported models are: ${MODEL_LIST.anthropic.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
class AnthropicAi implements AiEngine {
|
||||
private anthropicAiApiConfiguration = {
|
||||
apiKey: apiKey
|
||||
};
|
||||
private anthropicAI!: Anthropic;
|
||||
|
||||
constructor() {
|
||||
this.anthropicAI = new Anthropic(this.anthropicAiApiConfiguration);
|
||||
}
|
||||
|
||||
public generateCommitMessage = async (
|
||||
messages: Array<ChatCompletionRequestMessage>
|
||||
): 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: MODEL,
|
||||
system: systemMessage,
|
||||
messages: restMessages,
|
||||
temperature: 0,
|
||||
top_p: 0.1,
|
||||
max_tokens: MAX_TOKENS_OUTPUT
|
||||
};
|
||||
try {
|
||||
const REQUEST_TOKENS = messages
|
||||
.map((msg) => tokenCount(msg.content as string) + 4)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
if (REQUEST_TOKENS > MAX_TOKENS_INPUT - MAX_TOKENS_OUTPUT) {
|
||||
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
||||
}
|
||||
|
||||
const data = await this.anthropicAI.messages.create(params);
|
||||
|
||||
const message = data?.content[0].text;
|
||||
|
||||
return message;
|
||||
} 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 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const anthropicAi = new AnthropicAi();
|
||||
109
src/engine/azure.ts
Normal file
109
src/engine/azure.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import axios from 'axios';
|
||||
import chalk from 'chalk';
|
||||
import { execa } from 'execa';
|
||||
import {
|
||||
ChatCompletionRequestMessage,
|
||||
} from 'openai';
|
||||
|
||||
import { OpenAIClient, AzureKeyCredential } from '@azure/openai';
|
||||
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
|
||||
import {
|
||||
CONFIG_MODES,
|
||||
DEFAULT_TOKEN_LIMITS,
|
||||
getConfig
|
||||
} from '../commands/config';
|
||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||
import { tokenCount } from '../utils/tokenCount';
|
||||
import { AiEngine } from './Engine';
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
const MAX_TOKENS_OUTPUT =
|
||||
config?.OCO_TOKENS_MAX_OUTPUT ||
|
||||
DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
||||
const MAX_TOKENS_INPUT =
|
||||
config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
||||
let basePath = config?.OCO_OPENAI_BASE_PATH;
|
||||
let apiKey = config?.OCO_AZURE_API_KEY;
|
||||
let apiEndpoint = config?.OCO_AZURE_ENDPOINT;
|
||||
|
||||
const [command, mode] = process.argv.slice(2);
|
||||
|
||||
const provider = config?.OCO_AI_PROVIDER;
|
||||
|
||||
if (
|
||||
provider === 'azure' &&
|
||||
!apiKey &&
|
||||
!apiEndpoint &&
|
||||
command !== 'config' &&
|
||||
mode !== CONFIG_MODES.set
|
||||
) {
|
||||
intro('opencommit');
|
||||
|
||||
outro(
|
||||
'OCO_AZURE_API_KEY or OCO_AZURE_ENDPOINT are not set, please run `oco config set OCO_AZURE_API_KEY=<your token> . If you are using GPT, 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 Azure implements AiEngine {
|
||||
private openAI!: OpenAIClient;
|
||||
|
||||
constructor() {
|
||||
if (provider === 'azure') {
|
||||
this.openAI = new OpenAIClient(apiEndpoint, new AzureKeyCredential(apiKey));
|
||||
}
|
||||
}
|
||||
|
||||
public generateCommitMessage = async (
|
||||
messages: Array<ChatCompletionRequestMessage>
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
const REQUEST_TOKENS = messages
|
||||
.map((msg) => tokenCount(msg.content) + 4)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
if (REQUEST_TOKENS > MAX_TOKENS_INPUT - MAX_TOKENS_OUTPUT) {
|
||||
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
||||
}
|
||||
|
||||
const data = await this.openAI.getChatCompletions(MODEL, messages);
|
||||
|
||||
const message = data.choices[0].message;
|
||||
|
||||
if (message?.content === null) {
|
||||
return undefined;
|
||||
}
|
||||
return message?.content;
|
||||
} catch (error) {
|
||||
outro(`${chalk.red('✖')} ${MODEL}`);
|
||||
|
||||
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 azure = new Azure();
|
||||
@@ -2,11 +2,22 @@ import axios, { AxiosError } from 'axios';
|
||||
import { ChatCompletionRequestMessage } from 'openai';
|
||||
import { AiEngine } from './Engine';
|
||||
|
||||
import {
|
||||
getConfig
|
||||
} from '../commands/config';
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
export class OllamaAi implements AiEngine {
|
||||
private model = "mistral"; // as default model of Ollama
|
||||
|
||||
setModel(model: string) {
|
||||
this.model = model ?? config?.OCO_MODEL ?? 'mistral';
|
||||
}
|
||||
async generateCommitMessage(
|
||||
messages: Array<ChatCompletionRequestMessage>
|
||||
): Promise<string | undefined> {
|
||||
const model = 'mistral'; // todo: allow other models
|
||||
const model = this.model;
|
||||
|
||||
//console.log(messages);
|
||||
//process.exit()
|
||||
@@ -15,7 +26,7 @@ export class OllamaAi implements AiEngine {
|
||||
const p = {
|
||||
model,
|
||||
messages,
|
||||
options: {temperature: 0, top_p: 0.1},
|
||||
options: { temperature: 0, top_p: 0.1 },
|
||||
stream: false
|
||||
};
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import chalk from 'chalk';
|
||||
import { execa } from 'execa';
|
||||
|
||||
import {
|
||||
ChatCompletionRequestMessage,
|
||||
Configuration as OpenAiApiConfiguration,
|
||||
@@ -17,20 +18,28 @@ import {
|
||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||
import { tokenCount } from '../utils/tokenCount';
|
||||
import { AiEngine } from './Engine';
|
||||
import { MODEL_LIST } from '../commands/config';
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
const MAX_TOKENS_OUTPUT = config?.OCO_TOKENS_MAX_OUTPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
||||
const MAX_TOKENS_INPUT = config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
||||
const MAX_TOKENS_OUTPUT =
|
||||
config?.OCO_TOKENS_MAX_OUTPUT ||
|
||||
DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
||||
const MAX_TOKENS_INPUT =
|
||||
config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
||||
let basePath = config?.OCO_OPENAI_BASE_PATH;
|
||||
let apiKey = config?.OCO_OPENAI_API_KEY
|
||||
let apiKey = config?.OCO_OPENAI_API_KEY;
|
||||
|
||||
const [command, mode] = process.argv.slice(2);
|
||||
|
||||
const isLocalModel = config?.OCO_AI_PROVIDER == 'ollama'
|
||||
const provider = config?.OCO_AI_PROVIDER;
|
||||
|
||||
|
||||
if (!apiKey && command !== 'config' && mode !== CONFIG_MODES.set && !isLocalModel) {
|
||||
if (
|
||||
provider === 'openai' &&
|
||||
!apiKey &&
|
||||
command !== 'config' &&
|
||||
mode !== CONFIG_MODES.set
|
||||
) {
|
||||
intro('opencommit');
|
||||
|
||||
outro(
|
||||
@@ -44,6 +53,18 @@ if (!apiKey && command !== 'config' && mode !== CONFIG_MODES.set && !isLocalMode
|
||||
}
|
||||
|
||||
const MODEL = config?.OCO_MODEL || 'gpt-3.5-turbo';
|
||||
if (provider === 'openai' &&
|
||||
!MODEL_LIST.openai.includes(MODEL) &&
|
||||
command !== 'config' &&
|
||||
mode !== CONFIG_MODES.set) {
|
||||
outro(
|
||||
`${chalk.red('✖')} Unsupported model ${MODEL} for OpenAI. Supported models are: ${MODEL_LIST.openai.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
class OpenAi implements AiEngine {
|
||||
private openAiApiConfiguration = new OpenAiApiConfiguration({
|
||||
@@ -105,6 +126,4 @@ class OpenAi implements AiEngine {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const api = new OpenAi();
|
||||
|
||||
12
src/engine/testAi.ts
Normal file
12
src/engine/testAi.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ChatCompletionRequestMessage } from 'openai';
|
||||
import { AiEngine } from './Engine';
|
||||
|
||||
export class TestAi implements AiEngine {
|
||||
async generateCommitMessage(
|
||||
messages: Array<ChatCompletionRequestMessage>
|
||||
): Promise<string | undefined> {
|
||||
return 'test commit message';
|
||||
}
|
||||
}
|
||||
|
||||
export const testAi = new TestAi();
|
||||
@@ -10,8 +10,11 @@ import { tokenCount } from './utils/tokenCount';
|
||||
import { getEngine } from './utils/engine';
|
||||
|
||||
const config = getConfig();
|
||||
const MAX_TOKENS_INPUT = config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
||||
const MAX_TOKENS_OUTPUT = config?.OCO_TOKENS_MAX_OUTPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
||||
const MAX_TOKENS_INPUT =
|
||||
config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
||||
const MAX_TOKENS_OUTPUT =
|
||||
config?.OCO_TOKENS_MAX_OUTPUT ||
|
||||
DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
||||
|
||||
const generateCommitMessageChatCompletionPrompt = async (
|
||||
diff: string,
|
||||
@@ -71,9 +74,12 @@ export const generateCommitMessageByDiff = async (
|
||||
return commitMessages.join('\n\n');
|
||||
}
|
||||
|
||||
const messages = await generateCommitMessageChatCompletionPrompt(diff, fullGitMojiSpec);
|
||||
const messages = await generateCommitMessageChatCompletionPrompt(
|
||||
diff,
|
||||
fullGitMojiSpec
|
||||
);
|
||||
|
||||
const engine = getEngine()
|
||||
const engine = getEngine();
|
||||
const commitMessage = await engine.generateCommitMessage(messages);
|
||||
|
||||
if (!commitMessage)
|
||||
@@ -112,7 +118,7 @@ function getMessagesPromisesByChangesInFile(
|
||||
}
|
||||
}
|
||||
|
||||
const engine = getEngine()
|
||||
const engine = getEngine();
|
||||
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map(
|
||||
async (lineDiff) => {
|
||||
const messages = await generateCommitMessageChatCompletionPrompt(
|
||||
@@ -194,7 +200,7 @@ export const getCommitMsgsPromisesFromFileDiffs = async (
|
||||
fullGitMojiSpec
|
||||
);
|
||||
|
||||
const engine = getEngine()
|
||||
const engine = getEngine();
|
||||
commitMessagePromises.push(engine.generateCommitMessage(messages));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export const configureCommitlintIntegration = async (force = false) => {
|
||||
// consistencyPrompts.map((p) => p.content)
|
||||
// );
|
||||
|
||||
const engine = getEngine()
|
||||
const engine = getEngine();
|
||||
let consistency =
|
||||
(await engine.generateCommitMessage(consistencyPrompts)) || '{}';
|
||||
|
||||
@@ -64,7 +64,7 @@ export const configureCommitlintIntegration = async (force = false) => {
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ export const removeDoubleNewlines = (input: string): string => {
|
||||
|
||||
export const getJSONBlock = (input: string): string => {
|
||||
const jsonIndex = input.search('```json');
|
||||
if(jsonIndex > -1) {
|
||||
if (jsonIndex > -1) {
|
||||
input = input.slice(jsonIndex + 8);
|
||||
const endJsonIndex = consistency.search('```');
|
||||
input = input.slice(0, endJsonIndex);
|
||||
input = input.slice(0, endJsonIndex);
|
||||
}
|
||||
return input;
|
||||
};
|
||||
|
||||
|
||||
export const commitlintLLMConfigExists = async (): Promise<boolean> => {
|
||||
let exists;
|
||||
try {
|
||||
@@ -54,4 +54,4 @@ export const getCommitlintLLMConfig =
|
||||
content.toString()
|
||||
) as CommitlintLLMConfig;
|
||||
return commitLintLLMConfig;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -118,7 +118,7 @@ const INIT_MAIN_PROMPT = (
|
||||
${
|
||||
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.'
|
||||
: ""
|
||||
: ''
|
||||
}
|
||||
Use the present tense. Lines must not be longer than 74 characters. Use ${language} for the commit message.`
|
||||
});
|
||||
|
||||
@@ -2,12 +2,25 @@ import { AiEngine } from '../engine/Engine';
|
||||
import { api } from '../engine/openAi';
|
||||
import { getConfig } from '../commands/config';
|
||||
import { ollamaAi } from '../engine/ollama';
|
||||
import { azure } from '../engine/azure';
|
||||
import { anthropicAi } from '../engine/anthropic'
|
||||
import { testAi } from '../engine/testAi';
|
||||
|
||||
export function getEngine(): AiEngine {
|
||||
const config = getConfig();
|
||||
if (config?.OCO_AI_PROVIDER == 'ollama') {
|
||||
const provider = config?.OCO_AI_PROVIDER;
|
||||
if (provider?.startsWith('ollama')) {
|
||||
const model = provider.split('/')[1];
|
||||
if (model) ollamaAi.setModel(model);
|
||||
|
||||
return ollamaAi;
|
||||
} else if (config?.OCO_AI_PROVIDER == 'anthropic') {
|
||||
return anthropicAi;
|
||||
} else if (config?.OCO_AI_PROVIDER == 'test') {
|
||||
return testAi;
|
||||
} else if (config?.OCO_AI_PROVIDER == 'azure') {
|
||||
return azure;
|
||||
}
|
||||
//open ai gpt by default
|
||||
// open ai gpt by default
|
||||
return api;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { outro } from "@clack/prompts";
|
||||
import { execa } from "execa";
|
||||
import { outro } from '@clack/prompts';
|
||||
import { execa } from 'execa';
|
||||
|
||||
export const getOpenCommitLatestVersion = async (): Promise<
|
||||
string | undefined
|
||||
@@ -11,4 +11,4 @@ export const getOpenCommitLatestVersion = async (): Promise<
|
||||
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
|
||||
13
test/e2e/noChanges.test.ts
Normal file
13
test/e2e/noChanges.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
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();
|
||||
});
|
||||
56
test/e2e/oneFile.test.ts
Normal file
56
test/e2e/oneFile.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { resolve } from 'path'
|
||||
import { render } from 'cli-testing-library'
|
||||
import 'cli-testing-library/extend-expect';
|
||||
import { prepareEnvironment } from './utils';
|
||||
|
||||
it('cli flow to generate commit message for 1 new file (staged)', async () => {
|
||||
const { gitDir, cleanup } = await prepareEnvironment();
|
||||
|
||||
await render('echo' ,[`'console.log("Hello World");' > index.ts`], { cwd: gitDir });
|
||||
await render('git' ,['add index.ts'], { cwd: gitDir });
|
||||
|
||||
const { queryByText, findByText, userEvent } = await render(`OCO_AI_PROVIDER='test' node`, [resolve('./out/cli.cjs')], { cwd: gitDir });
|
||||
|
||||
expect(await queryByText('No files are staged')).not.toBeInTheConsole();
|
||||
expect(await queryByText('Do you want to stage all files and generate commit message?')).not.toBeInTheConsole();
|
||||
|
||||
expect(await findByText('Generating the commit message')).toBeInTheConsole();
|
||||
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||
userEvent.keyboard('[Enter]');
|
||||
|
||||
expect(await findByText('Choose a remote to push to')).toBeInTheConsole();
|
||||
userEvent.keyboard('[Enter]');
|
||||
|
||||
expect(await findByText('Successfully pushed all commits to origin')).toBeInTheConsole();
|
||||
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
it('cli flow to generate commit message for 1 changed file (not staged)', async () => {
|
||||
const { gitDir, cleanup } = await prepareEnvironment();
|
||||
|
||||
await render('echo' ,[`'console.log("Hello World");' > index.ts`], { cwd: gitDir });
|
||||
await render('git' ,['add index.ts'], { cwd: gitDir });
|
||||
await render('git' ,[`commit -m 'add new file'`], { cwd: gitDir });
|
||||
|
||||
await render('echo' ,[`'console.log("Good night World");' >> index.ts`], { cwd: gitDir });
|
||||
|
||||
const { findByText, userEvent } = await render(`OCO_AI_PROVIDER='test' node`, [resolve('./out/cli.cjs')], { cwd: gitDir });
|
||||
|
||||
expect(await findByText('No files are staged')).toBeInTheConsole();
|
||||
expect(await findByText('Do you want to stage all files and generate commit message?')).toBeInTheConsole();
|
||||
userEvent.keyboard('[Enter]');
|
||||
|
||||
expect(await findByText('Generating the commit message')).toBeInTheConsole();
|
||||
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||
userEvent.keyboard('[Enter]');
|
||||
|
||||
expect(await findByText('Successfully committed')).toBeInTheConsole();
|
||||
|
||||
expect(await findByText('Choose a remote to push to')).toBeInTheConsole();
|
||||
userEvent.keyboard('[Enter]');
|
||||
|
||||
expect(await findByText('Successfully pushed all commits to origin')).toBeInTheConsole();
|
||||
|
||||
await cleanup();
|
||||
});
|
||||
31
test/e2e/utils.ts
Normal file
31
test/e2e/utils.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import path from 'path'
|
||||
import { mkdtemp, rm } from 'fs'
|
||||
import { promisify } from 'util';
|
||||
import { tmpdir } from 'os';
|
||||
import { exec } from 'child_process';
|
||||
const fsMakeTempDir = promisify(mkdtemp);
|
||||
const fsExec = promisify(exec);
|
||||
const fsRemove = promisify(rm);
|
||||
|
||||
/**
|
||||
* Prepare the environment for the test
|
||||
* Create a temporary git repository in the temp directory
|
||||
*/
|
||||
export const prepareEnvironment = async (): Promise<{
|
||||
gitDir: string;
|
||||
cleanup: () => Promise<void>;
|
||||
}> => {
|
||||
const tempDir = await fsMakeTempDir(path.join(tmpdir(), 'opencommit-test-'));
|
||||
// Create a remote git repository int the temp directory. This is necessary to execute the `git push` command
|
||||
await fsExec('git init --bare remote.git', { cwd: tempDir });
|
||||
await fsExec('git clone remote.git test', { cwd: tempDir });
|
||||
const gitDir = path.resolve(tempDir, 'test');
|
||||
|
||||
const cleanup = async () => {
|
||||
return fsRemove(tempDir, { recursive: true });
|
||||
}
|
||||
return {
|
||||
gitDir,
|
||||
cleanup,
|
||||
}
|
||||
}
|
||||
7
test/jest-setup.ts
Normal file
7
test/jest-setup.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'cli-testing-library/extend-expect'
|
||||
import { configure } from 'cli-testing-library'
|
||||
|
||||
/**
|
||||
* Adjusted the wait time for waitFor/findByText to 2000ms, because the default 1000ms makes the test results flaky
|
||||
*/
|
||||
configure({ asyncUtilTimeout: 2000 })
|
||||
105
test/unit/config.test.ts
Normal file
105
test/unit/config.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { getConfig } from '../../src/commands/config';
|
||||
import { prepareFile } from './utils';
|
||||
|
||||
describe('getConfig', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
function resetEnv(env: NodeJS.ProcessEnv) {
|
||||
Object.keys(process.env).forEach((key) => {
|
||||
if (!(key in env)) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = env[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetEnv(originalEnv);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
resetEnv(originalEnv);
|
||||
});
|
||||
|
||||
it('return config values from the global config file', async () => {
|
||||
const configFile = await prepareFile(
|
||||
'.opencommit',
|
||||
`
|
||||
OCO_OPENAI_API_KEY="sk-key"
|
||||
OCO_ANTHROPIC_API_KEY="secret-key"
|
||||
OCO_TOKENS_MAX_INPUT="8192"
|
||||
OCO_TOKENS_MAX_OUTPUT="1000"
|
||||
OCO_OPENAI_BASE_PATH="/openai/api"
|
||||
OCO_DESCRIPTION="true"
|
||||
OCO_EMOJI="true"
|
||||
OCO_MODEL="gpt-4"
|
||||
OCO_LANGUAGE="de"
|
||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER="$m"
|
||||
OCO_PROMPT_MODULE="@commitlint"
|
||||
OCO_AI_PROVIDER="ollama"
|
||||
OCO_GITPUSH="false"
|
||||
OCO_ONE_LINE_COMMIT="true"
|
||||
`
|
||||
);
|
||||
const config = getConfig({ configPath: configFile.filePath, envPath: '' });
|
||||
|
||||
expect(config).not.toEqual(null);
|
||||
expect(config!['OCO_OPENAI_API_KEY']).toEqual('sk-key');
|
||||
expect(config!['OCO_ANTHROPIC_API_KEY']).toEqual('secret-key');
|
||||
expect(config!['OCO_TOKENS_MAX_INPUT']).toEqual(8192);
|
||||
expect(config!['OCO_TOKENS_MAX_OUTPUT']).toEqual(1000);
|
||||
expect(config!['OCO_OPENAI_BASE_PATH']).toEqual('/openai/api');
|
||||
expect(config!['OCO_DESCRIPTION']).toEqual(true);
|
||||
expect(config!['OCO_EMOJI']).toEqual(true);
|
||||
expect(config!['OCO_MODEL']).toEqual('gpt-4');
|
||||
expect(config!['OCO_LANGUAGE']).toEqual('de');
|
||||
expect(config!['OCO_MESSAGE_TEMPLATE_PLACEHOLDER']).toEqual('$m');
|
||||
expect(config!['OCO_PROMPT_MODULE']).toEqual('@commitlint');
|
||||
expect(config!['OCO_AI_PROVIDER']).toEqual('ollama');
|
||||
expect(config!['OCO_GITPUSH']).toEqual(false);
|
||||
expect(config!['OCO_ONE_LINE_COMMIT']).toEqual(true);
|
||||
|
||||
await configFile.cleanup();
|
||||
});
|
||||
|
||||
it('return config values from the local env file', async () => {
|
||||
const envFile = await prepareFile(
|
||||
'.env',
|
||||
`
|
||||
OCO_OPENAI_API_KEY="sk-key"
|
||||
OCO_ANTHROPIC_API_KEY="secret-key"
|
||||
OCO_TOKENS_MAX_INPUT="8192"
|
||||
OCO_TOKENS_MAX_OUTPUT="1000"
|
||||
OCO_OPENAI_BASE_PATH="/openai/api"
|
||||
OCO_DESCRIPTION="true"
|
||||
OCO_EMOJI="true"
|
||||
OCO_MODEL="gpt-4"
|
||||
OCO_LANGUAGE="de"
|
||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER="$m"
|
||||
OCO_PROMPT_MODULE="@commitlint"
|
||||
OCO_AI_PROVIDER="ollama"
|
||||
OCO_GITPUSH="false"
|
||||
OCO_ONE_LINE_COMMIT="true"
|
||||
`
|
||||
);
|
||||
const config = getConfig({ configPath: '', envPath: envFile.filePath });
|
||||
|
||||
expect(config).not.toEqual(null);
|
||||
expect(config!['OCO_OPENAI_API_KEY']).toEqual('sk-key');
|
||||
expect(config!['OCO_ANTHROPIC_API_KEY']).toEqual('secret-key');
|
||||
expect(config!['OCO_TOKENS_MAX_INPUT']).toEqual(8192);
|
||||
expect(config!['OCO_TOKENS_MAX_OUTPUT']).toEqual(1000);
|
||||
expect(config!['OCO_OPENAI_BASE_PATH']).toEqual('/openai/api');
|
||||
expect(config!['OCO_DESCRIPTION']).toEqual(true);
|
||||
expect(config!['OCO_EMOJI']).toEqual(true);
|
||||
expect(config!['OCO_MODEL']).toEqual('gpt-4');
|
||||
expect(config!['OCO_LANGUAGE']).toEqual('de');
|
||||
expect(config!['OCO_MESSAGE_TEMPLATE_PLACEHOLDER']).toEqual('$m');
|
||||
expect(config!['OCO_PROMPT_MODULE']).toEqual('@commitlint');
|
||||
expect(config!['OCO_AI_PROVIDER']).toEqual('ollama');
|
||||
expect(config!['OCO_GITPUSH']).toEqual(false);
|
||||
expect(config!['OCO_ONE_LINE_COMMIT']).toEqual(true);
|
||||
|
||||
await envFile.cleanup();
|
||||
});
|
||||
});
|
||||
29
test/unit/utils.ts
Normal file
29
test/unit/utils.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import path from 'path';
|
||||
import { mkdtemp, rm, writeFile } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { tmpdir } from 'os';
|
||||
const fsMakeTempDir = promisify(mkdtemp);
|
||||
const fsRemove = promisify(rm);
|
||||
const fsWriteFile = promisify(writeFile);
|
||||
|
||||
/**
|
||||
* Prepare tmp file for the test
|
||||
*/
|
||||
export async function prepareFile(
|
||||
fileName: string,
|
||||
content: string
|
||||
): Promise<{
|
||||
filePath: string;
|
||||
cleanup: () => Promise<void>;
|
||||
}> {
|
||||
const tempDir = await fsMakeTempDir(path.join(tmpdir(), 'opencommit-test-'));
|
||||
const filePath = path.resolve(tempDir, fileName);
|
||||
await fsWriteFile(filePath, content);
|
||||
const cleanup = async () => {
|
||||
return fsRemove(tempDir, { recursive: true });
|
||||
};
|
||||
return {
|
||||
filePath,
|
||||
cleanup
|
||||
};
|
||||
}
|
||||
@@ -21,6 +21,9 @@
|
||||
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"test/jest-setup.ts"
|
||||
],
|
||||
"exclude": ["node_modules"],
|
||||
"ts-node": {
|
||||
"esm": true,
|
||||
|
||||
Reference in New Issue
Block a user