mirror of
https://github.com/di-sukharev/opencommit.git
synced 2026-01-13 07:38:01 -05:00
Compare commits
26 Commits
v3.2.4
...
oco_find_v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca4be719b2 | ||
|
|
5e37fd29b7 | ||
|
|
7286456a04 | ||
|
|
85468823f9 | ||
|
|
7eb9a1b45c | ||
|
|
825c2fe825 | ||
|
|
9dcb264420 | ||
|
|
dd7fdba94e | ||
|
|
5fa12e2d4a | ||
|
|
42a36492ad | ||
|
|
443d27fc8d | ||
|
|
04991dd00f | ||
|
|
3ded6062c1 | ||
|
|
f8584e7b78 | ||
|
|
94faceefd3 | ||
|
|
720cd6f9c1 | ||
|
|
b6a92d557f | ||
|
|
71354e4687 | ||
|
|
8f85ee8f8e | ||
|
|
f9103a3c6a | ||
|
|
4afd7de7a8 | ||
|
|
5cfa3cded2 | ||
|
|
bb0b0e804e | ||
|
|
5d87cc514b | ||
|
|
6f4e8fde93 | ||
|
|
745bb5218f |
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
@@ -18,7 +18,7 @@ To get started, follow these steps:
|
||||
1. Clone the project repository locally.
|
||||
2. Install dependencies with `npm install`.
|
||||
3. Run the project with `npm run dev`.
|
||||
4. See [issues](https://github.com/di-sukharev/opencommit/issues) or [TODO.md](TODO.md) to help the project.
|
||||
4. See [issues](https://github.com/di-sukharev/open-commit/issues) or [TODO.md](../TODO.md) to help the project.
|
||||
|
||||
## Commit message guidelines
|
||||
|
||||
@@ -30,7 +30,7 @@ If you encounter any issues while using the project, please report them on the G
|
||||
|
||||
## Contacts
|
||||
|
||||
If you have any questions about contributing to the project, please contact by [creating an issue](https://github.com/di-sukharev/opencommit/issues) on the GitHub issue tracker.
|
||||
If you have any questions about contributing to the project, please contact by [creating an issue](https://github.com/di-sukharev/open-commit/issues) on the GitHub issue tracker.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,4 +11,6 @@ uncaughtExceptions.log
|
||||
src/*.json
|
||||
.idea
|
||||
test.ts
|
||||
notes.md
|
||||
notes.md
|
||||
*.excalidraw
|
||||
*.tldr
|
||||
|
||||
60
README.md
60
README.md
@@ -28,19 +28,28 @@ You can use OpenCommit by simply running it via the CLI like this `oco`. 2 secon
|
||||
npm install -g opencommit
|
||||
```
|
||||
|
||||
2. Get your API key from [OpenAI](https://platform.openai.com/account/api-keys) or other supported LLM providers (we support them all). Make sure that you add your OpenAI payment details to your account, so the API works.
|
||||
Alternatively run it via `npx opencommit` or `bunx opencommit`, but you need to create ~/.opencommit config file in place.
|
||||
|
||||
2. Get your API key from [OpenAI](https://platform.openai.com/account/api-keys). Make sure that you add your payment details, so the API works.
|
||||
|
||||
3. Set the key to OpenCommit config:
|
||||
|
||||
```sh
|
||||
oco config set OCO_API_KEY=<your_api_key>
|
||||
oco config set OCO_OPENAI_API_KEY=<your_api_key>
|
||||
```
|
||||
|
||||
Your API key is stored locally in the `~/.opencommit` config file.
|
||||
|
||||
## Usage
|
||||
|
||||
You can call OpenCommit with `oco` command to generate a commit message for your staged changes:
|
||||
You can call OpenCommit directly to generate a commit message for your staged changes:
|
||||
|
||||
```sh
|
||||
git add <files...>
|
||||
opencommit
|
||||
```
|
||||
|
||||
You can also use the `oco` shortcut:
|
||||
|
||||
```sh
|
||||
git add <files...>
|
||||
@@ -59,17 +68,21 @@ You can also run it with local model through ollama:
|
||||
|
||||
```sh
|
||||
git add <files...>
|
||||
oco config set OCO_AI_PROVIDER='ollama' OCO_MODEL='llama3:8b'
|
||||
oco config set OCO_AI_PROVIDER='ollama'
|
||||
```
|
||||
|
||||
Default model is `mistral`.
|
||||
If you want to use a model other than mistral (default), you can do so by setting the `OCO_AI_PROVIDER` environment variable as follows:
|
||||
|
||||
```sh
|
||||
oco config set OCO_AI_PROVIDER='ollama/llama3:8b'
|
||||
```
|
||||
|
||||
If you have ollama that is set up in docker/ on another machine with GPUs (not locally), you can change the default endpoint url.
|
||||
|
||||
You can do so by setting the `OCO_API_URL` environment variable as follows:
|
||||
You can do so by setting the `OCO_OLLAMA_API_URL` environment variable as follows:
|
||||
|
||||
```sh
|
||||
oco config set OCO_API_URL='http://192.168.1.10:11434/api/chat'
|
||||
oco config set OCO_OLLAMA_API_URL='http://192.168.1.10:11434/api/chat'
|
||||
```
|
||||
|
||||
where 192.168.1.10 is example of endpoint URL, where you have ollama set up.
|
||||
@@ -106,21 +119,22 @@ Create a `.env` file and add OpenCommit config variables there like this:
|
||||
|
||||
```env
|
||||
...
|
||||
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama, gemini, flowise>
|
||||
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_OPENAI_API_KEY=<your OpenAI API token>
|
||||
OCO_TOKENS_MAX_INPUT=<max model token limit (default: 4096)>
|
||||
OCO_TOKENS_MAX_OUTPUT=<max response tokens (default: 500)>
|
||||
OCO_OPENAI_BASE_PATH=<may be used to set proxy path to OpenAI api>
|
||||
OCO_DESCRIPTION=<postface a message with ~3 sentences description of the changes>
|
||||
OCO_EMOJI=<boolean, add GitMoji>
|
||||
OCO_MODEL=<either 'gpt-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', 'gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo' (default), 'gpt-3.5-turbo-0125', 'gpt-4-1106-preview', 'gpt-4-turbo-preview' or 'gpt-4-0125-preview' or any string basically, but it should be a valid model name>
|
||||
OCO_LANGUAGE=<locale, scroll to the bottom to see options>
|
||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER=<message template placeholder, default: '$msg'>
|
||||
OCO_PROMPT_MODULE=<either conventional-commit or @commitlint, default: conventional-commit>
|
||||
OCO_ONE_LINE_COMMIT=<one line commit message, default: false>
|
||||
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama or ollama/model>
|
||||
...
|
||||
```
|
||||
|
||||
Global configs are same as local configs, but they are stored in the global `~/.opencommit` config file and set with `oco config set` command, e.g. `oco config set OCO_MODEL=gpt-4o`.
|
||||
This are not all the config options, but you get the point.
|
||||
|
||||
### Global config for all repos
|
||||
|
||||
@@ -172,26 +186,26 @@ or for as a cheaper option:
|
||||
oco config set OCO_MODEL=gpt-3.5-turbo
|
||||
```
|
||||
|
||||
### Switch to other LLM providers with a custom URL
|
||||
### Switch to Azure OpenAI
|
||||
|
||||
By default OpenCommit uses [OpenAI](https://openai.com).
|
||||
|
||||
You could switch to [Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/) or Flowise or Ollama.
|
||||
You could switch to [Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/)🚀
|
||||
|
||||
```sh
|
||||
oco config set OCO_AI_PROVIDER=azure OCO_API_KEY=<your_azure_api_key> OCO_API_URL=<your_azure_endpoint>
|
||||
|
||||
oco config set OCO_AI_PROVIDER=flowise OCO_API_KEY=<your_flowise_api_key> OCO_API_URL=<your_flowise_endpoint>
|
||||
|
||||
oco config set OCO_AI_PROVIDER=ollama OCO_API_KEY=<your_ollama_api_key> OCO_API_URL=<your_ollama_endpoint>
|
||||
opencommit config set OCO_AI_PROVIDER=azure
|
||||
```
|
||||
|
||||
Of course need to set 'OCO_OPENAI_API_KEY'. And also need to set the
|
||||
'OPENAI_BASE_PATH' for the endpoint and set the deployment name to
|
||||
'model'.
|
||||
|
||||
### Locale configuration
|
||||
|
||||
To globally specify the language used to generate commit messages:
|
||||
|
||||
```sh
|
||||
# de, German, Deutsch
|
||||
# de, German ,Deutsch
|
||||
oco config set OCO_LANGUAGE=de
|
||||
oco config set OCO_LANGUAGE=German
|
||||
oco config set OCO_LANGUAGE=Deutsch
|
||||
@@ -207,14 +221,12 @@ All available languages are currently listed in the [i18n](https://github.com/di
|
||||
|
||||
### Push to git (gonna be deprecated)
|
||||
|
||||
A prompt for pushing to git is on by default but if you would like to turn it off just use:
|
||||
A prompt to ushing to git is on by default but if you would like to turn it off just use:
|
||||
|
||||
```sh
|
||||
oco config set OCO_GITPUSH=false
|
||||
```
|
||||
|
||||
and it will exit right after commit is confirmed without asking if you would like to push to remote.
|
||||
|
||||
### Switch to `@commitlint`
|
||||
|
||||
OpenCommit allows you to choose the prompt module used to generate commit messages. By default, OpenCommit uses its conventional-commit message generator. However, you can switch to using the `@commitlint` prompt module if you prefer. This option lets you generate commit messages in respect with the local config.
|
||||
@@ -389,7 +401,7 @@ jobs:
|
||||
# set openAI api key in repo actions secrets,
|
||||
# for openAI keys go to: https://platform.openai.com/account/api-keys
|
||||
# for repo secret go to: <your_repo_url>/settings/secrets/actions
|
||||
OCO_API_KEY: ${{ secrets.OCO_API_KEY }}
|
||||
OCO_OPENAI_API_KEY: ${{ secrets.OCO_OPENAI_API_KEY }}
|
||||
|
||||
# customization
|
||||
OCO_TOKENS_MAX_INPUT: 4096
|
||||
|
||||
3116
out/cli.cjs
3116
out/cli.cjs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
985
package-lock.json
generated
985
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.2.4",
|
||||
"version": "3.1.1",
|
||||
"description": "Auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
|
||||
"keywords": [
|
||||
"git",
|
||||
@@ -46,9 +46,8 @@
|
||||
"dev:gemini": "OCO_AI_PROVIDER='gemini' ts-node ./src/cli.ts",
|
||||
"build": "rimraf out && node esbuild.config.js",
|
||||
"build:push": "npm run build && git add . && git commit -m 'build' && git push",
|
||||
"deploy": "npm publish --tag latest",
|
||||
"deploy:build": "npm run build:push && git push --tags && npm run deploy",
|
||||
"deploy:patch": "npm version patch && npm run deploy:build",
|
||||
"deploy": "npm run build:push && git push --tags && npm publish --tag latest",
|
||||
"deploy:patch": "npm version patch && npm run deploy",
|
||||
"lint": "eslint src --ext ts && tsc --noEmit",
|
||||
"format": "prettier --write src",
|
||||
"test": "node --no-warnings --experimental-vm-modules $( [ -f ./node_modules/.bin/jest ] && echo ./node_modules/.bin/jest || which jest ) test/unit",
|
||||
@@ -58,8 +57,7 @@
|
||||
"test:unit:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:unit",
|
||||
"test:e2e": "npm run test:e2e:setup && jest test/e2e",
|
||||
"test:e2e:setup": "sh test/e2e/setup.sh",
|
||||
"test:e2e:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:e2e",
|
||||
"mlx:start": "OCO_AI_PROVIDER='mlx' node ./out/cli.cjs"
|
||||
"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",
|
||||
@@ -88,9 +86,9 @@
|
||||
"@clack/prompts": "^0.6.1",
|
||||
"@dqbd/tiktoken": "^1.0.2",
|
||||
"@google/generative-ai": "^0.11.4",
|
||||
"@mistralai/mistralai": "^1.3.5",
|
||||
"@octokit/webhooks-schemas": "^6.11.0",
|
||||
"@octokit/webhooks-types": "^6.11.0",
|
||||
"ai": "^2.2.14",
|
||||
"axios": "^1.3.4",
|
||||
"chalk": "^5.2.0",
|
||||
"cleye": "^1.3.2",
|
||||
@@ -99,8 +97,6 @@
|
||||
"ignore": "^5.2.4",
|
||||
"ini": "^3.0.1",
|
||||
"inquirer": "^9.1.4",
|
||||
"openai": "^4.57.0",
|
||||
"punycode": "^2.3.1",
|
||||
"zod": "^3.23.8"
|
||||
"openai": "^4.56.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export enum COMMANDS {
|
||||
config = 'config',
|
||||
hook = 'hook',
|
||||
commitlint = 'commitlint'
|
||||
}
|
||||
18
src/cli.ts
18
src/cli.ts
@@ -9,7 +9,7 @@ import { configCommand } from './commands/config';
|
||||
import { hookCommand, isHookCalled } from './commands/githook.js';
|
||||
import { prepareCommitMessageHook } from './commands/prepare-commit-msg-hook';
|
||||
import { checkIsLatestVersion } from './utils/checkIsLatestVersion';
|
||||
import { runMigrations } from './migrations/_run.js';
|
||||
import { findCommand } from './commands/find';
|
||||
|
||||
const extraArgs = process.argv.slice(2);
|
||||
|
||||
@@ -17,15 +17,14 @@ cli(
|
||||
{
|
||||
version: packageJSON.version,
|
||||
name: 'opencommit',
|
||||
commands: [configCommand, hookCommand, commitlintConfigCommand],
|
||||
commands: [
|
||||
configCommand,
|
||||
hookCommand,
|
||||
commitlintConfigCommand,
|
||||
findCommand
|
||||
],
|
||||
flags: {
|
||||
fgm: Boolean,
|
||||
context: {
|
||||
type: String,
|
||||
alias: 'c',
|
||||
description: 'Additional user input context for the commit message',
|
||||
default: ''
|
||||
},
|
||||
yes: {
|
||||
type: Boolean,
|
||||
alias: 'y',
|
||||
@@ -37,13 +36,12 @@ cli(
|
||||
help: { description: packageJSON.description }
|
||||
},
|
||||
async ({ flags }) => {
|
||||
await runMigrations();
|
||||
await checkIsLatestVersion();
|
||||
|
||||
if (await isHookCalled()) {
|
||||
prepareCommitMessageHook();
|
||||
} else {
|
||||
commit(extraArgs, flags.context, false, flags.fgm, flags.yes);
|
||||
commit(extraArgs, false, flags.fgm, flags.yes);
|
||||
}
|
||||
},
|
||||
extraArgs
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export enum COMMANDS {
|
||||
config = 'config',
|
||||
hook = 'hook',
|
||||
commitlint = 'commitlint'
|
||||
commitlint = 'commitlint',
|
||||
find = 'find'
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ const checkMessageTemplate = (extraArgs: string[]): string | false => {
|
||||
interface GenerateCommitMessageFromGitDiffParams {
|
||||
diff: string;
|
||||
extraArgs: string[];
|
||||
context?: string;
|
||||
fullGitMojiSpec?: boolean;
|
||||
skipCommitConfirmation?: boolean;
|
||||
}
|
||||
@@ -47,7 +46,6 @@ interface GenerateCommitMessageFromGitDiffParams {
|
||||
const generateCommitMessageFromGitDiff = async ({
|
||||
diff,
|
||||
extraArgs,
|
||||
context = '',
|
||||
fullGitMojiSpec = false,
|
||||
skipCommitConfirmation = false
|
||||
}: GenerateCommitMessageFromGitDiffParams): Promise<void> => {
|
||||
@@ -58,8 +56,7 @@ const generateCommitMessageFromGitDiff = async ({
|
||||
try {
|
||||
let commitMessage = await generateCommitMessageByDiff(
|
||||
diff,
|
||||
fullGitMojiSpec,
|
||||
context
|
||||
fullGitMojiSpec
|
||||
);
|
||||
|
||||
const messageTemplate = checkMessageTemplate(extraArgs);
|
||||
@@ -110,16 +107,13 @@ ${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`?'
|
||||
});
|
||||
@@ -138,7 +132,8 @@ ${chalk.grey('——————————————————')}`
|
||||
]);
|
||||
|
||||
pushSpinner.stop(
|
||||
`${chalk.green('✔')} Successfully pushed all commits to ${remotes[0]
|
||||
`${chalk.green('✔')} Successfully pushed all commits to ${
|
||||
remotes[0]
|
||||
}`
|
||||
);
|
||||
|
||||
@@ -148,29 +143,26 @@ ${chalk.grey('——————————————————')}`
|
||||
process.exit(0);
|
||||
}
|
||||
} else {
|
||||
const skipOption = `don't push`
|
||||
const selectedRemote = (await select({
|
||||
message: 'Choose a remote to push to',
|
||||
options: [...remotes, skipOption].map((remote) => ({ value: remote, label: remote })),
|
||||
options: remotes.map((remote) => ({ value: remote, label: remote }))
|
||||
})) as string;
|
||||
|
||||
if (isCancel(selectedRemote)) process.exit(1);
|
||||
|
||||
if (selectedRemote !== skipOption) {
|
||||
const pushSpinner = spinner();
|
||||
|
||||
pushSpinner.start(`Running 'git push ${selectedRemote}'`);
|
||||
|
||||
const { stdout } = await execa('git', ['push', selectedRemote]);
|
||||
|
||||
if (stdout) outro(stdout);
|
||||
|
||||
pushSpinner.stop(
|
||||
`${chalk.green(
|
||||
'✔'
|
||||
)} successfully pushed all commits to ${selectedRemote}`
|
||||
);
|
||||
}
|
||||
const pushSpinner = spinner();
|
||||
|
||||
pushSpinner.start(`Running 'git push ${selectedRemote}'`);
|
||||
|
||||
const { stdout } = await execa('git', ['push', selectedRemote]);
|
||||
|
||||
pushSpinner.stop(
|
||||
`${chalk.green(
|
||||
'✔'
|
||||
)} Successfully pushed all commits to ${selectedRemote}`
|
||||
);
|
||||
|
||||
if (stdout) outro(stdout);
|
||||
}
|
||||
} else {
|
||||
const regenerateMessage = await confirm({
|
||||
@@ -188,11 +180,7 @@ ${chalk.grey('——————————————————')}`
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
commitGenerationSpinner.stop(
|
||||
`${chalk.red('✖')} Failed to generate the commit message`
|
||||
);
|
||||
|
||||
console.log(error);
|
||||
commitGenerationSpinner.stop('📝 Commit message generated');
|
||||
|
||||
const err = error as Error;
|
||||
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
||||
@@ -202,7 +190,6 @@ ${chalk.grey('——————————————————')}`
|
||||
|
||||
export async function commit(
|
||||
extraArgs: string[] = [],
|
||||
context: string = '',
|
||||
isStageAllFlag: Boolean = false,
|
||||
fullGitMojiSpec: boolean = false,
|
||||
skipCommitConfirmation: boolean = false
|
||||
@@ -244,7 +231,7 @@ export async function commit(
|
||||
if (isCancel(isStageAllAndCommitConfirmedByUser)) process.exit(1);
|
||||
|
||||
if (isStageAllAndCommitConfirmedByUser) {
|
||||
await commit(extraArgs, context, true, fullGitMojiSpec);
|
||||
await commit(extraArgs, true, fullGitMojiSpec);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -262,7 +249,7 @@ export async function commit(
|
||||
await gitAdd({ files });
|
||||
}
|
||||
|
||||
await commit(extraArgs, context, false, fullGitMojiSpec);
|
||||
await commit(extraArgs, false, fullGitMojiSpec);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -276,7 +263,6 @@ export async function commit(
|
||||
generateCommitMessageFromGitDiff({
|
||||
diff: await getDiff({ files: stagedFiles }),
|
||||
extraArgs,
|
||||
context,
|
||||
fullGitMojiSpec,
|
||||
skipCommitConfirmation
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ export const commitlintConfigCommand = command(
|
||||
parameters: ['<mode>']
|
||||
},
|
||||
async (argv) => {
|
||||
intro('opencommit — configure @commitlint');
|
||||
intro('OpenCommit — configure @commitlint');
|
||||
try {
|
||||
const { mode } = argv._;
|
||||
|
||||
|
||||
@@ -11,9 +11,14 @@ import { TEST_MOCK_TYPES } from '../engine/testAi';
|
||||
import { getI18nLocal, i18n } from '../i18n';
|
||||
|
||||
export enum CONFIG_KEYS {
|
||||
OCO_API_KEY = 'OCO_API_KEY',
|
||||
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_GEMINI_API_KEY = 'OCO_GEMINI_API_KEY',
|
||||
OCO_GEMINI_BASE_PATH = 'OCO_GEMINI_BASE_PATH',
|
||||
OCO_TOKENS_MAX_INPUT = 'OCO_TOKENS_MAX_INPUT',
|
||||
OCO_TOKENS_MAX_OUTPUT = 'OCO_TOKENS_MAX_OUTPUT',
|
||||
OCO_OPENAI_BASE_PATH = 'OCO_OPENAI_BASE_PATH',
|
||||
OCO_DESCRIPTION = 'OCO_DESCRIPTION',
|
||||
OCO_EMOJI = 'OCO_EMOJI',
|
||||
OCO_MODEL = 'OCO_MODEL',
|
||||
@@ -22,10 +27,14 @@ 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_GITPUSH = 'OCO_GITPUSH', // todo: deprecate
|
||||
OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT',
|
||||
OCO_AZURE_ENDPOINT = 'OCO_AZURE_ENDPOINT',
|
||||
OCO_TEST_MOCK_TYPE = 'OCO_TEST_MOCK_TYPE',
|
||||
OCO_API_URL = 'OCO_API_URL',
|
||||
OCO_GITPUSH = 'OCO_GITPUSH' // todo: deprecate
|
||||
OCO_OLLAMA_API_URL = 'OCO_OLLAMA_API_URL',
|
||||
OCO_FLOWISE_ENDPOINT = 'OCO_FLOWISE_ENDPOINT',
|
||||
OCO_FLOWISE_API_KEY = 'OCO_FLOWISE_API_KEY'
|
||||
}
|
||||
|
||||
export enum CONFIG_MODES {
|
||||
@@ -76,58 +85,6 @@ export const MODEL_LIST = {
|
||||
'gemini-1.0-pro',
|
||||
'gemini-pro-vision',
|
||||
'text-embedding-004'
|
||||
],
|
||||
|
||||
groq: [
|
||||
'llama3-70b-8192', // Meta Llama 3 70B (default one, no daily token limit and 14 400 reqs/day)
|
||||
'llama3-8b-8192', // Meta Llama 3 8B
|
||||
'llama-guard-3-8b', // Llama Guard 3 8B
|
||||
'llama-3.1-8b-instant', // Llama 3.1 8B (Preview)
|
||||
'llama-3.1-70b-versatile', // Llama 3.1 70B (Preview)
|
||||
'gemma-7b-it', // Gemma 7B
|
||||
'gemma2-9b-it' // Gemma 2 9B
|
||||
],
|
||||
|
||||
mistral: [
|
||||
'ministral-3b-2410',
|
||||
'ministral-3b-latest',
|
||||
'ministral-8b-2410',
|
||||
'ministral-8b-latest',
|
||||
'open-mistral-7b',
|
||||
'mistral-tiny',
|
||||
'mistral-tiny-2312',
|
||||
'open-mistral-nemo',
|
||||
'open-mistral-nemo-2407',
|
||||
'mistral-tiny-2407',
|
||||
'mistral-tiny-latest',
|
||||
'open-mixtral-8x7b',
|
||||
'mistral-small',
|
||||
'mistral-small-2312',
|
||||
'open-mixtral-8x22b',
|
||||
'open-mixtral-8x22b-2404',
|
||||
'mistral-small-2402',
|
||||
'mistral-small-2409',
|
||||
'mistral-small-latest',
|
||||
'mistral-medium-2312',
|
||||
'mistral-medium',
|
||||
'mistral-medium-latest',
|
||||
'mistral-large-2402',
|
||||
'mistral-large-2407',
|
||||
'mistral-large-2411',
|
||||
'mistral-large-latest',
|
||||
'pixtral-large-2411',
|
||||
'pixtral-large-latest',
|
||||
'codestral-2405',
|
||||
'codestral-latest',
|
||||
'codestral-mamba-2407',
|
||||
'open-codestral-mamba',
|
||||
'codestral-mamba-latest',
|
||||
'pixtral-12b-2409',
|
||||
'pixtral-12b',
|
||||
'pixtral-12b-latest',
|
||||
'mistral-embed',
|
||||
'mistral-moderation-2411',
|
||||
'mistral-moderation-latest',
|
||||
]
|
||||
};
|
||||
|
||||
@@ -135,16 +92,10 @@ const getDefaultModel = (provider: string | undefined): string => {
|
||||
switch (provider) {
|
||||
case 'ollama':
|
||||
return '';
|
||||
case 'mlx':
|
||||
return '';
|
||||
case 'anthropic':
|
||||
return MODEL_LIST.anthropic[0];
|
||||
case 'gemini':
|
||||
return MODEL_LIST.gemini[0];
|
||||
case 'groq':
|
||||
return MODEL_LIST.groq[0];
|
||||
case 'mistral':
|
||||
return MODEL_LIST.mistral[0];
|
||||
default:
|
||||
return MODEL_LIST.openai[0];
|
||||
}
|
||||
@@ -172,19 +123,65 @@ const validateConfig = (
|
||||
};
|
||||
|
||||
export const configValidators = {
|
||||
[CONFIG_KEYS.OCO_API_KEY](value: any, config: any = {}) {
|
||||
[CONFIG_KEYS.OCO_OPENAI_API_KEY](value: any, config: any = {}) {
|
||||
if (config.OCO_AI_PROVIDER !== 'openai') return value;
|
||||
|
||||
validateConfig(
|
||||
'OCO_API_KEY',
|
||||
'OCO_OPENAI_API_KEY',
|
||||
typeof value === 'string' && value.length > 0,
|
||||
'Empty value is not allowed'
|
||||
);
|
||||
|
||||
validateConfig(
|
||||
'OCO_API_KEY',
|
||||
'OCO_OPENAI_API_KEY',
|
||||
value,
|
||||
'You need to provide the OCO_API_KEY when OCO_AI_PROVIDER set to "openai" (default) or "ollama" or "mlx" or "azure" or "gemini" or "flowise" or "anthropic". Run `oco config set OCO_API_KEY=your_key OCO_AI_PROVIDER=openai`'
|
||||
'You need to provide the OCO_OPENAI_API_KEY when OCO_AI_PROVIDER is set to "openai" (default). Run `oco config set OCO_OPENAI_API_KEY=your_key`'
|
||||
);
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_AZURE_API_KEY](value: any, config: any = {}) {
|
||||
if (config.OCO_AI_PROVIDER !== 'azure') return value;
|
||||
|
||||
validateConfig(
|
||||
'OCO_AZURE_API_KEY',
|
||||
!!value,
|
||||
'You need to provide the OCO_AZURE_API_KEY when OCO_AI_PROVIDER is set to "azure". Run: `oco config set OCO_AZURE_API_KEY=your_key`'
|
||||
);
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_GEMINI_API_KEY](value: any, config: any = {}) {
|
||||
if (config.OCO_AI_PROVIDER !== 'gemini') return value;
|
||||
|
||||
validateConfig(
|
||||
'OCO_GEMINI_API_KEY',
|
||||
value || config.OCO_GEMINI_API_KEY || config.OCO_AI_PROVIDER === 'test',
|
||||
'You need to provide the OCO_GEMINI_API_KEY when OCO_AI_PROVIDER is set to "gemini". Run: `oco config set OCO_GEMINI_API_KEY=your_key`'
|
||||
);
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_ANTHROPIC_API_KEY](value: any, config: any = {}) {
|
||||
if (config.OCO_AI_PROVIDER !== 'anthropic') return value;
|
||||
|
||||
validateConfig(
|
||||
'ANTHROPIC_API_KEY',
|
||||
!!value,
|
||||
'You need to provide the OCO_ANTHROPIC_API_KEY key when OCO_AI_PROVIDER is set to "anthropic". Run: `oco config set OCO_ANTHROPIC_API_KEY=your_key`'
|
||||
);
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_FLOWISE_API_KEY](value: any, config: any = {}) {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_FLOWISE_API_KEY,
|
||||
value || config.OCO_AI_PROVIDER !== 'flowise',
|
||||
'You need to provide the OCO_FLOWISE_API_KEY when OCO_AI_PROVIDER is set to "flowise". Run: `oco config set OCO_FLOWISE_API_KEY=your_key`'
|
||||
);
|
||||
|
||||
return value;
|
||||
@@ -244,11 +241,11 @@ export const configValidators = {
|
||||
return getI18nLocal(value);
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_API_URL](value: any) {
|
||||
[CONFIG_KEYS.OCO_OPENAI_BASE_PATH](value: any) {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_API_URL,
|
||||
CONFIG_KEYS.OCO_OPENAI_BASE_PATH,
|
||||
typeof value === 'string',
|
||||
`${value} is not a valid URL. It should start with 'http://' or 'https://'.`
|
||||
'Must be string'
|
||||
);
|
||||
return value;
|
||||
},
|
||||
@@ -299,17 +296,10 @@ export const configValidators = {
|
||||
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_AI_PROVIDER,
|
||||
[
|
||||
'openai',
|
||||
'mistral',
|
||||
'anthropic',
|
||||
'gemini',
|
||||
'azure',
|
||||
'test',
|
||||
'flowise',
|
||||
'groq'
|
||||
].includes(value) || value.startsWith('ollama'),
|
||||
`${value} is not supported yet, use 'ollama', 'mlx', 'anthropic', 'azure', 'gemini', 'flowise', 'mistral' or 'openai' (default)`
|
||||
['openai', 'anthropic', 'gemini', 'azure', 'test', 'flowise'].includes(
|
||||
value
|
||||
) || value.startsWith('ollama'),
|
||||
`${value} is not supported yet, use 'ollama', 'anthropic', 'azure', 'gemini', 'flowise' or 'openai' (default)`
|
||||
);
|
||||
|
||||
return value;
|
||||
@@ -325,6 +315,26 @@ export const configValidators = {
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_AZURE_ENDPOINT](value: any) {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_AZURE_ENDPOINT,
|
||||
value.includes('openai.azure.com'),
|
||||
'Must be in format "https://<resource name>.openai.azure.com/"'
|
||||
);
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_FLOWISE_ENDPOINT](value: any) {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_FLOWISE_ENDPOINT,
|
||||
typeof value === 'string' && value.includes(':'),
|
||||
'Value must be string and should include both I.P. and port number' // Considering the possibility of DNS lookup or feeding the I.P. explicitly, there is no pattern to verify, except a column for the port number
|
||||
);
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_TEST_MOCK_TYPE](value: any) {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_TEST_MOCK_TYPE,
|
||||
@@ -336,11 +346,11 @@ export const configValidators = {
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_WHY](value: any) {
|
||||
[CONFIG_KEYS.OCO_OLLAMA_API_URL](value: any) {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_WHY,
|
||||
typeof value === 'boolean',
|
||||
'Must be true or false'
|
||||
CONFIG_KEYS.OCO_OLLAMA_API_URL,
|
||||
typeof value === 'string' && value.startsWith('http'),
|
||||
`${value} is not a valid URL. It should start with 'http://' or 'https://'.`
|
||||
);
|
||||
return value;
|
||||
}
|
||||
@@ -353,17 +363,18 @@ export enum OCO_AI_PROVIDER_ENUM {
|
||||
GEMINI = 'gemini',
|
||||
AZURE = 'azure',
|
||||
TEST = 'test',
|
||||
FLOWISE = 'flowise',
|
||||
GROQ = 'groq',
|
||||
MISTRAL = 'mistral',
|
||||
MLX = 'mlx'
|
||||
FLOWISE = 'flowise'
|
||||
}
|
||||
|
||||
export type ConfigType = {
|
||||
[CONFIG_KEYS.OCO_API_KEY]?: string;
|
||||
[CONFIG_KEYS.OCO_OPENAI_API_KEY]?: string;
|
||||
[CONFIG_KEYS.OCO_ANTHROPIC_API_KEY]?: string;
|
||||
[CONFIG_KEYS.OCO_AZURE_API_KEY]?: string;
|
||||
[CONFIG_KEYS.OCO_GEMINI_API_KEY]?: string;
|
||||
[CONFIG_KEYS.OCO_GEMINI_BASE_PATH]?: string;
|
||||
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT]: number;
|
||||
[CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT]: number;
|
||||
[CONFIG_KEYS.OCO_API_URL]?: string;
|
||||
[CONFIG_KEYS.OCO_OPENAI_BASE_PATH]?: string;
|
||||
[CONFIG_KEYS.OCO_DESCRIPTION]: boolean;
|
||||
[CONFIG_KEYS.OCO_EMOJI]: boolean;
|
||||
[CONFIG_KEYS.OCO_WHY]: boolean;
|
||||
@@ -374,11 +385,16 @@ export type ConfigType = {
|
||||
[CONFIG_KEYS.OCO_AI_PROVIDER]: OCO_AI_PROVIDER_ENUM;
|
||||
[CONFIG_KEYS.OCO_GITPUSH]: boolean;
|
||||
[CONFIG_KEYS.OCO_ONE_LINE_COMMIT]: boolean;
|
||||
[CONFIG_KEYS.OCO_AZURE_ENDPOINT]?: string;
|
||||
[CONFIG_KEYS.OCO_TEST_MOCK_TYPE]: string;
|
||||
[CONFIG_KEYS.OCO_API_URL]?: string;
|
||||
[CONFIG_KEYS.OCO_OLLAMA_API_URL]?: string;
|
||||
[CONFIG_KEYS.OCO_FLOWISE_ENDPOINT]: string;
|
||||
[CONFIG_KEYS.OCO_FLOWISE_API_KEY]?: string;
|
||||
};
|
||||
|
||||
export const defaultConfigPath = pathJoin(homedir(), '.opencommit');
|
||||
export const defaultEnvPath = pathResolve(process.cwd(), '.env');
|
||||
const defaultConfigPath = pathJoin(homedir(), '.opencommit');
|
||||
const defaultEnvPath = pathResolve(process.cwd(), '.env');
|
||||
|
||||
const assertConfigsAreValid = (config: Record<string, any>) => {
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
@@ -420,6 +436,7 @@ export const DEFAULT_CONFIG = {
|
||||
OCO_AI_PROVIDER: OCO_AI_PROVIDER_ENUM.OPENAI,
|
||||
OCO_ONE_LINE_COMMIT: false,
|
||||
OCO_TEST_MOCK_TYPE: 'commit-message',
|
||||
OCO_FLOWISE_ENDPOINT: ':',
|
||||
OCO_WHY: false,
|
||||
OCO_GITPUSH: true // todo: deprecate
|
||||
};
|
||||
@@ -429,7 +446,7 @@ const initGlobalConfig = (configPath: string = defaultConfigPath) => {
|
||||
return DEFAULT_CONFIG;
|
||||
};
|
||||
|
||||
const parseConfigVarValue = (value?: any) => {
|
||||
const parseEnvVarValue = (value?: any) => {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
@@ -442,45 +459,41 @@ const getEnvConfig = (envPath: string) => {
|
||||
|
||||
return {
|
||||
OCO_MODEL: process.env.OCO_MODEL,
|
||||
OCO_API_URL: process.env.OCO_API_URL,
|
||||
OCO_API_KEY: process.env.OCO_API_KEY,
|
||||
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_OUTPUT: parseConfigVarValue(
|
||||
process.env.OCO_TOKENS_MAX_OUTPUT
|
||||
),
|
||||
OCO_OPENAI_API_KEY: process.env.OCO_OPENAI_API_KEY,
|
||||
OCO_ANTHROPIC_API_KEY: process.env.OCO_ANTHROPIC_API_KEY,
|
||||
OCO_AZURE_API_KEY: process.env.OCO_AZURE_API_KEY,
|
||||
OCO_GEMINI_API_KEY: process.env.OCO_GEMINI_API_KEY,
|
||||
OCO_FLOWISE_API_KEY: process.env.OCO_FLOWISE_API_KEY,
|
||||
|
||||
OCO_DESCRIPTION: parseConfigVarValue(process.env.OCO_DESCRIPTION),
|
||||
OCO_EMOJI: parseConfigVarValue(process.env.OCO_EMOJI),
|
||||
OCO_TOKENS_MAX_INPUT: parseEnvVarValue(process.env.OCO_TOKENS_MAX_INPUT),
|
||||
OCO_TOKENS_MAX_OUTPUT: parseEnvVarValue(process.env.OCO_TOKENS_MAX_OUTPUT),
|
||||
|
||||
OCO_OPENAI_BASE_PATH: process.env.OCO_OPENAI_BASE_PATH,
|
||||
OCO_GEMINI_BASE_PATH: process.env.OCO_GEMINI_BASE_PATH,
|
||||
|
||||
OCO_AZURE_ENDPOINT: process.env.OCO_AZURE_ENDPOINT,
|
||||
OCO_FLOWISE_ENDPOINT: process.env.OCO_FLOWISE_ENDPOINT,
|
||||
OCO_OLLAMA_API_URL: process.env.OCO_OLLAMA_API_URL,
|
||||
|
||||
OCO_DESCRIPTION: parseEnvVarValue(process.env.OCO_DESCRIPTION),
|
||||
OCO_EMOJI: parseEnvVarValue(process.env.OCO_EMOJI),
|
||||
OCO_LANGUAGE: process.env.OCO_LANGUAGE,
|
||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER:
|
||||
process.env.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
|
||||
OCO_PROMPT_MODULE: process.env.OCO_PROMPT_MODULE as OCO_PROMPT_MODULE_ENUM,
|
||||
OCO_ONE_LINE_COMMIT: parseConfigVarValue(process.env.OCO_ONE_LINE_COMMIT),
|
||||
OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER as OCO_AI_PROVIDER_ENUM,
|
||||
OCO_ONE_LINE_COMMIT: parseEnvVarValue(process.env.OCO_ONE_LINE_COMMIT),
|
||||
OCO_TEST_MOCK_TYPE: process.env.OCO_TEST_MOCK_TYPE,
|
||||
|
||||
OCO_GITPUSH: parseConfigVarValue(process.env.OCO_GITPUSH) // todo: deprecate
|
||||
OCO_GITPUSH: parseEnvVarValue(process.env.OCO_GITPUSH) // todo: deprecate
|
||||
};
|
||||
};
|
||||
|
||||
export const setGlobalConfig = (
|
||||
config: ConfigType,
|
||||
configPath: string = defaultConfigPath
|
||||
) => {
|
||||
writeFileSync(configPath, iniStringify(config), 'utf8');
|
||||
};
|
||||
|
||||
export const getIsGlobalConfigFileExist = (
|
||||
configPath: string = defaultConfigPath
|
||||
) => {
|
||||
return existsSync(configPath);
|
||||
};
|
||||
|
||||
export const getGlobalConfig = (configPath: string = defaultConfigPath) => {
|
||||
const getGlobalConfig = (configPath: string) => {
|
||||
let globalConfig: ConfigType;
|
||||
|
||||
const isGlobalConfigFileExist = getIsGlobalConfigFileExist(configPath);
|
||||
const isGlobalConfigFileExist = existsSync(configPath);
|
||||
if (!isGlobalConfigFileExist) globalConfig = initGlobalConfig(configPath);
|
||||
else {
|
||||
const configFile = readFileSync(configPath, 'utf8');
|
||||
@@ -497,39 +510,18 @@ export const getGlobalConfig = (configPath: string = defaultConfigPath) => {
|
||||
* @param fallback - global ~/.opencommit config file
|
||||
* @returns merged config
|
||||
*/
|
||||
const mergeConfigs = (main: Partial<ConfigType>, fallback: ConfigType) => {
|
||||
const allKeys = new Set([...Object.keys(main), ...Object.keys(fallback)]);
|
||||
return Array.from(allKeys).reduce((acc, key) => {
|
||||
acc[key] = parseConfigVarValue(main[key] ?? fallback[key]);
|
||||
const mergeConfigs = (main: Partial<ConfigType>, fallback: ConfigType) =>
|
||||
Object.keys(CONFIG_KEYS).reduce((acc, key) => {
|
||||
acc[key] = parseEnvVarValue(main[key] ?? fallback[key]);
|
||||
|
||||
return acc;
|
||||
}, {} as ConfigType);
|
||||
};
|
||||
|
||||
interface GetConfigOptions {
|
||||
globalPath?: string;
|
||||
envPath?: string;
|
||||
setDefaultValues?: boolean;
|
||||
}
|
||||
|
||||
const cleanUndefinedValues = (config: ConfigType) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(config).map(([_, v]) => {
|
||||
try {
|
||||
if (typeof v === 'string') {
|
||||
if (v === 'undefined') return [_, undefined];
|
||||
if (v === 'null') return [_, null];
|
||||
|
||||
const parsedValue = JSON.parse(v);
|
||||
return [_, parsedValue];
|
||||
}
|
||||
return [_, v];
|
||||
} catch (error) {
|
||||
return [_, v];
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const getConfig = ({
|
||||
envPath = defaultEnvPath,
|
||||
globalPath = defaultConfigPath
|
||||
@@ -539,21 +531,17 @@ export const getConfig = ({
|
||||
|
||||
const config = mergeConfigs(envConfig, globalConfig);
|
||||
|
||||
const cleanConfig = cleanUndefinedValues(config);
|
||||
|
||||
return cleanConfig as ConfigType;
|
||||
return config;
|
||||
};
|
||||
|
||||
export const setConfig = (
|
||||
keyValues: [key: string, value: string | boolean | number | null][],
|
||||
keyValues: [key: string, value: string][],
|
||||
globalConfigPath: string = defaultConfigPath
|
||||
) => {
|
||||
const config = getConfig({
|
||||
globalPath: globalConfigPath
|
||||
});
|
||||
|
||||
const configToSet = {};
|
||||
|
||||
for (let [key, value] of keyValues) {
|
||||
if (!configValidators.hasOwnProperty(key)) {
|
||||
const supportedKeys = Object.keys(configValidators).join('\n');
|
||||
@@ -565,8 +553,7 @@ export const setConfig = (
|
||||
let parsedConfigValue;
|
||||
|
||||
try {
|
||||
if (typeof value === 'string') parsedConfigValue = JSON.parse(value);
|
||||
else parsedConfigValue = value;
|
||||
parsedConfigValue = JSON.parse(value);
|
||||
} catch (error) {
|
||||
parsedConfigValue = value;
|
||||
}
|
||||
@@ -576,10 +563,10 @@ export const setConfig = (
|
||||
config
|
||||
);
|
||||
|
||||
configToSet[key] = validValue;
|
||||
config[key] = validValue;
|
||||
}
|
||||
|
||||
setGlobalConfig(mergeConfigs(configToSet, config), globalConfigPath);
|
||||
writeFileSync(globalConfigPath, iniStringify(config), 'utf8');
|
||||
|
||||
outro(`${chalk.green('✔')} config successfully set`);
|
||||
};
|
||||
|
||||
372
src/commands/find.ts
Normal file
372
src/commands/find.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
import {
|
||||
confirm,
|
||||
intro,
|
||||
isCancel,
|
||||
note,
|
||||
outro,
|
||||
select,
|
||||
spinner
|
||||
} from '@clack/prompts';
|
||||
import chalk from 'chalk';
|
||||
import { command } from 'cleye';
|
||||
import { execa } from 'execa';
|
||||
import { getIgnoredFolders } from '../utils/git';
|
||||
import { COMMANDS } from './ENUMS';
|
||||
import { OpenAiEngine } from '../engine/openAi';
|
||||
import { getConfig } from './config';
|
||||
|
||||
type Occurrence = {
|
||||
fileName: string;
|
||||
context: {
|
||||
number: number;
|
||||
content: string;
|
||||
};
|
||||
matches: {
|
||||
number: number;
|
||||
content: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
/*
|
||||
TODO:
|
||||
- [ ] format declarations as file:line => context -> declaration
|
||||
- [ ] format usages as file:line => context -> usage
|
||||
- [ ] expand on usage to see it's call hierarchy
|
||||
- [ ] generate Mermaid diagram
|
||||
*/
|
||||
|
||||
const generateMermaid = async (stdout: string) => {
|
||||
const config = getConfig();
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
model: config.OCO_MODEL!,
|
||||
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
|
||||
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
|
||||
baseURL: config.OCO_OPENAI_BASE_PATH!
|
||||
};
|
||||
const engine = new OpenAiEngine({
|
||||
...DEFAULT_CONFIG,
|
||||
apiKey: config.OCO_OPENAI_API_KEY!
|
||||
});
|
||||
|
||||
const diagram = await engine.generateCommitMessage([
|
||||
{
|
||||
role: 'system',
|
||||
content: `You are to generate a mermaid diagram from the given function. Strictly answer in this json format: { "mermaid": "<mermaid diagram>" }. Where <mermaid diagram> is a valid mermaid diagram, e.g:
|
||||
graph TD
|
||||
A[Start] --> B[Generate Commit Message]
|
||||
B --> C{Token count >= Max?}
|
||||
C -->|Yes| D[Process file diffs]
|
||||
C -->|No| E[Generate single message]
|
||||
D --> F[Join messages]
|
||||
E --> G[Generate message]
|
||||
F --> H[End]
|
||||
G --> H
|
||||
B --> I{Error occurred?}
|
||||
I -->|Yes| J[Handle error]
|
||||
J --> H
|
||||
I -->|No| H
|
||||
`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: stdout
|
||||
}
|
||||
]);
|
||||
|
||||
return JSON.parse(diagram as string);
|
||||
};
|
||||
|
||||
export function extractFuncName(line: string) {
|
||||
const regex =
|
||||
/(?:function|export\s+const|const|let|var)?\s*(?:async\s+)?(\w+)\s*(?:=\s*(?:async\s*)?\(|\()/;
|
||||
const match = line.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function extractSingle(lineContent: string): string | null {
|
||||
const match = lineContent.match(/\s*(?:public\s+)?(?:async\s+)?(\w+)\s*=/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function mapLinesToOccurrences(input: string[], step: number = 3) {
|
||||
const occurrences: Occurrence[] = [];
|
||||
let single;
|
||||
|
||||
for (let i = 0; i < input.length; i += step) {
|
||||
if (i + 1 >= input.length) break;
|
||||
|
||||
const [fileName, callerLineNumber, ...callerLineContent] =
|
||||
input[i].split(/[=:]/);
|
||||
const [, definitionLineNumber, ...definitionLineContent] =
|
||||
input[i + 1].split(/[:]/);
|
||||
|
||||
if (!single) single = extractSingle(definitionLineContent.join(':'));
|
||||
|
||||
occurrences.push({
|
||||
fileName,
|
||||
context: {
|
||||
number: parseInt(callerLineNumber, 10),
|
||||
content: callerLineContent.join('=').trim()
|
||||
},
|
||||
matches: [
|
||||
{
|
||||
number: parseInt(definitionLineNumber, 10),
|
||||
content: definitionLineContent.join(':').trim()
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
return { occurrences, single };
|
||||
}
|
||||
|
||||
const findDeclarations = async (query: string[], ignoredFolders: string[]) => {
|
||||
const searchQuery = `(async|function|public).*${query.join('[^ \\n]*')}`;
|
||||
|
||||
outro(`Searching: ${searchQuery}`);
|
||||
|
||||
const occurrences = await findInFiles({ query: searchQuery, ignoredFolders });
|
||||
|
||||
if (!occurrences) return null;
|
||||
|
||||
const declarations = mapLinesToOccurrences(occurrences.split('\n'));
|
||||
|
||||
return declarations;
|
||||
};
|
||||
|
||||
const findUsagesByDeclaration = async (
|
||||
declaration: string,
|
||||
ignoredFolders: string[]
|
||||
) => {
|
||||
const searchQuery = `${declaration}\\(.*\\)`;
|
||||
|
||||
const occurrences = await findInFiles({
|
||||
query: searchQuery,
|
||||
ignoredFolders
|
||||
// grepOptions: ['--function-context']
|
||||
});
|
||||
|
||||
if (!occurrences) return null;
|
||||
|
||||
const usages = mapLinesToOccurrences(
|
||||
occurrences.split('\n').filter(Boolean),
|
||||
2
|
||||
);
|
||||
|
||||
return usages;
|
||||
};
|
||||
|
||||
const buildCallHierarchy = async (
|
||||
query: string[],
|
||||
ignoredFolders: string[]
|
||||
) => {};
|
||||
|
||||
const findInFiles = async ({
|
||||
query,
|
||||
ignoredFolders,
|
||||
grepOptions = []
|
||||
}: {
|
||||
query: string;
|
||||
ignoredFolders: string[];
|
||||
grepOptions?: string[];
|
||||
}): Promise<string | null> => {
|
||||
const withIgnoredFolders =
|
||||
ignoredFolders.length > 0
|
||||
? [
|
||||
'--',
|
||||
' ',
|
||||
'.',
|
||||
' ',
|
||||
ignoredFolders.map((folder) => `:^${folder}`).join(' ')
|
||||
]
|
||||
: [];
|
||||
|
||||
const params = [
|
||||
'--no-pager',
|
||||
'grep',
|
||||
'--show-function', // show function caller
|
||||
'-n',
|
||||
'-i',
|
||||
...grepOptions,
|
||||
'--break',
|
||||
'--color=never',
|
||||
|
||||
// '-C',
|
||||
// '1',
|
||||
|
||||
// '--full-name',
|
||||
// '--heading',
|
||||
'--threads',
|
||||
'10',
|
||||
'-E',
|
||||
query,
|
||||
...withIgnoredFolders
|
||||
];
|
||||
|
||||
try {
|
||||
const { stdout } = await execa('git', params);
|
||||
return stdout;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const generatePermutations = (arr: string[]): string[][] => {
|
||||
const n = arr.length;
|
||||
const result: string[][] = [];
|
||||
const indices = new Int32Array(n);
|
||||
|
||||
const current = new Array(n);
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
indices[i] = i;
|
||||
current[i] = arr[i];
|
||||
}
|
||||
result.push([...current]);
|
||||
|
||||
let i = 1;
|
||||
while (i < n) {
|
||||
if (indices[i] > 0) {
|
||||
const j = indices[i] % 2 === 1 ? 0 : indices[i];
|
||||
|
||||
[current[i], current[j]] = [current[j], current[i]];
|
||||
result.push([...current]);
|
||||
indices[i]--;
|
||||
i = 1;
|
||||
} else {
|
||||
indices[i] = i;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const shuffleQuery = (query: string[]): string[][] => {
|
||||
return generatePermutations(query);
|
||||
};
|
||||
|
||||
export const findCommand = command(
|
||||
{
|
||||
name: COMMANDS.find,
|
||||
parameters: ['<query...>']
|
||||
},
|
||||
async (argv) => {
|
||||
const query = argv._;
|
||||
|
||||
intro(`OpenCommit — 🔦 find`);
|
||||
const ignoredFolders = getIgnoredFolders();
|
||||
|
||||
const searchSpinner = spinner();
|
||||
let declarations = await findDeclarations(query, ignoredFolders);
|
||||
|
||||
outro(`No matches found. Searching semantically similar queries.`);
|
||||
|
||||
searchSpinner.start(`Searching for matches...`);
|
||||
|
||||
if (!declarations?.occurrences.length) {
|
||||
const allPossibleQueries = shuffleQuery(query).reverse();
|
||||
for (const possibleQuery of allPossibleQueries) {
|
||||
declarations = await findDeclarations(possibleQuery, ignoredFolders);
|
||||
if (declarations?.occurrences.length) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!declarations?.occurrences.length) {
|
||||
searchSpinner.stop(`${chalk.red('✘')} No function declarations found.`);
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
const usages = await findUsagesByDeclaration(
|
||||
declarations.single,
|
||||
ignoredFolders
|
||||
);
|
||||
|
||||
searchSpinner.stop(
|
||||
`${chalk.green('✔')} Found ${chalk.green(
|
||||
declarations.single
|
||||
)} definition and ${usages?.occurrences.length} usages.`
|
||||
);
|
||||
|
||||
note(
|
||||
declarations.occurrences
|
||||
.map((o) =>
|
||||
o.matches
|
||||
.map(
|
||||
(m) =>
|
||||
`${o.fileName}:${m.number} ${chalk.cyan(
|
||||
'==>'
|
||||
)} ${m.content.replace(
|
||||
declarations.single,
|
||||
chalk.green(declarations.single)
|
||||
)}`
|
||||
)
|
||||
.join('\n')
|
||||
)
|
||||
.join('\n'),
|
||||
'⍜ DECLARATIONS ⍜'
|
||||
);
|
||||
|
||||
note(
|
||||
usages?.occurrences
|
||||
.map((o) =>
|
||||
o.matches.map(
|
||||
(m) =>
|
||||
`${o.fileName}:${m.number} ${chalk.cyan(
|
||||
'==>'
|
||||
)} ${m.content.replace(
|
||||
declarations.single,
|
||||
chalk.green(declarations.single)
|
||||
)}`
|
||||
)
|
||||
)
|
||||
.join('\n'),
|
||||
'⌾ USAGES ⌾'
|
||||
);
|
||||
|
||||
const usage = (await select({
|
||||
message: chalk.cyan('Expand usage:'),
|
||||
options: usages!.occurrences
|
||||
.map((o) =>
|
||||
o.matches.map((m) => ({
|
||||
value: { o, m },
|
||||
label: `${chalk.yellow(`${o.fileName}:${m.number}`)} ${chalk.cyan(
|
||||
'==>'
|
||||
)} ${m.content.replace(
|
||||
declarations.single,
|
||||
chalk.green(declarations.single)
|
||||
)}`,
|
||||
hint: `parent: ${extractFuncName(o.context.content) ?? '404'}`
|
||||
}))
|
||||
)
|
||||
.flat()
|
||||
})) as { o: Occurrence; m: any };
|
||||
|
||||
if (isCancel(usage)) process.exit(1);
|
||||
|
||||
const { stdout } = await execa('git', [
|
||||
'--no-pager',
|
||||
'grep',
|
||||
'--function-context',
|
||||
'--heading',
|
||||
'-E',
|
||||
usage.m.content.replace('(', '\\(').replace(')', '\\)'),
|
||||
usage.o.fileName
|
||||
]);
|
||||
|
||||
const mermaidSpinner = spinner();
|
||||
mermaidSpinner.start('Generating mermaid diagram...');
|
||||
const mermaid: any = await generateMermaid(stdout);
|
||||
mermaidSpinner.stop();
|
||||
if (mermaid) console.log(mermaid.mermaid);
|
||||
else note('No mermaid diagram found.');
|
||||
|
||||
const isCommitConfirmedByUser = await confirm({
|
||||
message: 'Create Excalidraw file?'
|
||||
});
|
||||
|
||||
if (isCommitConfirmedByUser) outro('created diagram.excalidraw');
|
||||
else outro('Excalidraw file not created.');
|
||||
}
|
||||
);
|
||||
@@ -35,15 +35,18 @@ export const prepareCommitMessageHook = async (
|
||||
|
||||
if (!staged) return;
|
||||
|
||||
intro('opencommit');
|
||||
intro('OpenCommit');
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
if (!config.OCO_API_KEY) {
|
||||
outro(
|
||||
'No OCO_API_KEY is set. Set your key via `oco config set OCO_API_KEY=<value>. For more info see https://github.com/di-sukharev/opencommit'
|
||||
if (
|
||||
!config.OCO_OPENAI_API_KEY &&
|
||||
!config.OCO_ANTHROPIC_API_KEY &&
|
||||
!config.OCO_AZURE_API_KEY
|
||||
) {
|
||||
throw new Error(
|
||||
'No OPEN_AI_API or OCO_ANTHROPIC_API_KEY or OCO_AZURE_API_KEY exists. Set your key in ~/.opencommit'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const spin = spinner();
|
||||
|
||||
@@ -3,7 +3,6 @@ import { OpenAIClient as AzureOpenAIClient } from '@azure/openai';
|
||||
import { GoogleGenerativeAI as GeminiClient } from '@google/generative-ai';
|
||||
import { AxiosInstance as RawAxiosClient } from 'axios';
|
||||
import { OpenAI as OpenAIClient } from 'openai';
|
||||
import { Mistral as MistralClient } from '@mistralai/mistralai';
|
||||
|
||||
export interface AiEngineConfig {
|
||||
apiKey: string;
|
||||
@@ -18,8 +17,7 @@ type Client =
|
||||
| AzureOpenAIClient
|
||||
| AnthropicClient
|
||||
| RawAxiosClient
|
||||
| GeminiClient
|
||||
| MistralClient;
|
||||
| GeminiClient;
|
||||
|
||||
export interface AiEngine {
|
||||
config: AiEngineConfig;
|
||||
|
||||
@@ -27,9 +27,9 @@ export class AzureEngine implements AiEngine {
|
||||
);
|
||||
}
|
||||
|
||||
generateCommitMessage = async (
|
||||
async generateCommitMessage(
|
||||
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||
): Promise<string | undefined> => {
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const REQUEST_TOKENS = messages
|
||||
.map((msg) => tokenCount(msg.content as string) + 4)
|
||||
@@ -73,5 +73,5 @@ export class AzureEngine implements AiEngine {
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AiEngine, AiEngineConfig } from './Engine';
|
||||
|
||||
interface FlowiseAiConfig extends AiEngineConfig {}
|
||||
|
||||
export class FlowiseEngine implements AiEngine {
|
||||
export class FlowiseAi implements AiEngine {
|
||||
config: FlowiseAiConfig;
|
||||
client: AxiosInstance;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AiEngine, AiEngineConfig } from './Engine';
|
||||
|
||||
interface GeminiConfig extends AiEngineConfig {}
|
||||
|
||||
export class GeminiEngine implements AiEngine {
|
||||
export class Gemini implements AiEngine {
|
||||
config: GeminiConfig;
|
||||
client: GoogleGenerativeAI;
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { OpenAiConfig, OpenAiEngine } from './openAi';
|
||||
|
||||
interface GroqConfig extends OpenAiConfig {}
|
||||
|
||||
export class GroqEngine extends OpenAiEngine {
|
||||
constructor(config: GroqConfig) {
|
||||
config.baseURL = 'https://api.groq.com/openai/v1';
|
||||
super(config);
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import { Mistral } from '@mistralai/mistralai';
|
||||
import { OpenAI } from 'openai';
|
||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||
import { tokenCount } from '../utils/tokenCount';
|
||||
import { AiEngine, AiEngineConfig } from './Engine';
|
||||
import {
|
||||
AssistantMessage as MistralAssistantMessage,
|
||||
SystemMessage as MistralSystemMessage,
|
||||
ToolMessage as MistralToolMessage,
|
||||
UserMessage as MistralUserMessage
|
||||
} from '@mistralai/mistralai/models/components';
|
||||
|
||||
export interface MistralAiConfig extends AiEngineConfig {}
|
||||
export type MistralCompletionMessageParam = Array<
|
||||
| (MistralSystemMessage & { role: "system" })
|
||||
| (MistralUserMessage & { role: "user" })
|
||||
| (MistralAssistantMessage & { role: "assistant" })
|
||||
| (MistralToolMessage & { role: "tool" })
|
||||
>
|
||||
|
||||
export class MistralAiEngine implements AiEngine {
|
||||
config: MistralAiConfig;
|
||||
client: Mistral;
|
||||
|
||||
constructor(config: MistralAiConfig) {
|
||||
this.config = config;
|
||||
|
||||
if (!config.baseURL) {
|
||||
this.client = new Mistral({ apiKey: config.apiKey });
|
||||
} else {
|
||||
this.client = new Mistral({ apiKey: config.apiKey, serverURL: config.baseURL });
|
||||
}
|
||||
}
|
||||
|
||||
public generateCommitMessage = async (
|
||||
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||
): Promise<string | null> => {
|
||||
const params = {
|
||||
model: this.config.model,
|
||||
messages: messages as MistralCompletionMessageParam,
|
||||
topP: 0.1,
|
||||
maxTokens: this.config.maxTokensOutput
|
||||
};
|
||||
|
||||
try {
|
||||
const REQUEST_TOKENS = messages
|
||||
.map((msg) => tokenCount(msg.content as string) + 4)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
if (
|
||||
REQUEST_TOKENS >
|
||||
this.config.maxTokensInput - this.config.maxTokensOutput
|
||||
)
|
||||
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
||||
|
||||
const completion = await this.client.chat.complete(params);
|
||||
|
||||
if (!completion.choices)
|
||||
throw Error('No completion choice available.')
|
||||
|
||||
const message = completion.choices[0].message;
|
||||
|
||||
if (!message || !message.content)
|
||||
throw Error('No completion choice available.')
|
||||
|
||||
return message.content as string;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
if (
|
||||
axios.isAxiosError<{ error?: { message: string } }>(error) &&
|
||||
error.response?.status === 401
|
||||
) {
|
||||
const mistralError = error.response.data.error;
|
||||
|
||||
if (mistralError) throw new Error(mistralError.message);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { OpenAI } from 'openai';
|
||||
import { AiEngine, AiEngineConfig } from './Engine';
|
||||
import { chown } from 'fs';
|
||||
|
||||
interface MLXConfig extends AiEngineConfig {}
|
||||
|
||||
export class MLXEngine implements AiEngine {
|
||||
config: MLXConfig;
|
||||
client: AxiosInstance;
|
||||
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.client = axios.create({
|
||||
url: config.baseURL
|
||||
? `${config.baseURL}/${config.apiKey}`
|
||||
: 'http://localhost:8080/v1/chat/completions',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
async generateCommitMessage(
|
||||
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>):
|
||||
Promise<string | undefined> {
|
||||
const params = {
|
||||
messages,
|
||||
temperature: 0,
|
||||
top_p: 0.1,
|
||||
repetition_penalty: 1.5,
|
||||
stream: false
|
||||
};
|
||||
try {
|
||||
const response = await this.client.post(
|
||||
this.client.getUri(this.config),
|
||||
params
|
||||
);
|
||||
|
||||
const choices = response.data.choices;
|
||||
const message = choices[0].message;
|
||||
|
||||
return message?.content;
|
||||
} catch (err: any) {
|
||||
const message = err.response?.data?.error ?? err.message;
|
||||
throw new Error(`MLX provider error: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { AiEngine, AiEngineConfig } from './Engine';
|
||||
|
||||
interface OllamaConfig extends AiEngineConfig {}
|
||||
|
||||
export class OllamaEngine implements AiEngine {
|
||||
export class OllamaAi implements AiEngine {
|
||||
config: OllamaConfig;
|
||||
client: AxiosInstance;
|
||||
|
||||
@@ -28,10 +28,7 @@ export class OllamaEngine implements AiEngine {
|
||||
stream: false
|
||||
};
|
||||
try {
|
||||
const response = await this.client.post(
|
||||
this.client.getUri(this.config),
|
||||
params
|
||||
);
|
||||
const response = await this.client.post('', params);
|
||||
|
||||
const message = response.data.message;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitD
|
||||
import { tokenCount } from '../utils/tokenCount';
|
||||
import { AiEngine, AiEngineConfig } from './Engine';
|
||||
|
||||
export interface OpenAiConfig extends AiEngineConfig {}
|
||||
interface OpenAiConfig extends AiEngineConfig {}
|
||||
|
||||
export class OpenAiEngine implements AiEngine {
|
||||
config: OpenAiConfig;
|
||||
@@ -12,12 +12,7 @@ export class OpenAiEngine implements AiEngine {
|
||||
|
||||
constructor(config: OpenAiConfig) {
|
||||
this.config = config;
|
||||
|
||||
if (!config.baseURL) {
|
||||
this.client = new OpenAI({ apiKey: config.apiKey });
|
||||
} else {
|
||||
this.client = new OpenAI({ apiKey: config.apiKey, baseURL: config.baseURL });
|
||||
}
|
||||
this.client = new OpenAI({ apiKey: config.apiKey });
|
||||
}
|
||||
|
||||
public generateCommitMessage = async (
|
||||
|
||||
@@ -6,15 +6,17 @@ import { mergeDiffs } from './utils/mergeDiffs';
|
||||
import { tokenCount } from './utils/tokenCount';
|
||||
|
||||
const config = getConfig();
|
||||
const MAX_TOKENS_INPUT = config.OCO_TOKENS_MAX_INPUT;
|
||||
const MAX_TOKENS_OUTPUT = config.OCO_TOKENS_MAX_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,
|
||||
fullGitMojiSpec: boolean,
|
||||
context: string
|
||||
fullGitMojiSpec: boolean
|
||||
): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => {
|
||||
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec, context);
|
||||
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec);
|
||||
|
||||
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
|
||||
|
||||
@@ -37,14 +39,10 @@ const ADJUSTMENT_FACTOR = 20;
|
||||
|
||||
export const generateCommitMessageByDiff = async (
|
||||
diff: string,
|
||||
fullGitMojiSpec: boolean = false,
|
||||
context: string = ""
|
||||
fullGitMojiSpec: boolean = false
|
||||
): Promise<string> => {
|
||||
try {
|
||||
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(
|
||||
fullGitMojiSpec,
|
||||
context
|
||||
);
|
||||
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec);
|
||||
|
||||
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
|
||||
(msg) => tokenCount(msg.content as string) + 4
|
||||
@@ -74,8 +72,7 @@ export const generateCommitMessageByDiff = async (
|
||||
|
||||
const messages = await generateCommitMessageChatCompletionPrompt(
|
||||
diff,
|
||||
fullGitMojiSpec,
|
||||
context,
|
||||
fullGitMojiSpec
|
||||
);
|
||||
|
||||
const engine = getEngine();
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import {
|
||||
CONFIG_KEYS,
|
||||
getConfig,
|
||||
OCO_AI_PROVIDER_ENUM,
|
||||
setConfig
|
||||
} from '../commands/config';
|
||||
|
||||
export default function () {
|
||||
const config = getConfig({ setDefaultValues: false });
|
||||
|
||||
const aiProvider = config.OCO_AI_PROVIDER;
|
||||
|
||||
let apiKey: string | undefined;
|
||||
let apiUrl: string | undefined;
|
||||
|
||||
if (aiProvider === OCO_AI_PROVIDER_ENUM.OLLAMA) {
|
||||
apiKey = config['OCO_OLLAMA_API_KEY'];
|
||||
apiUrl = config['OCO_OLLAMA_API_URL'];
|
||||
} else if (aiProvider === OCO_AI_PROVIDER_ENUM.ANTHROPIC) {
|
||||
apiKey = config['OCO_ANTHROPIC_API_KEY'];
|
||||
apiUrl = config['OCO_ANTHROPIC_BASE_PATH'];
|
||||
} else if (aiProvider === OCO_AI_PROVIDER_ENUM.OPENAI) {
|
||||
apiKey = config['OCO_OPENAI_API_KEY'];
|
||||
apiUrl = config['OCO_OPENAI_BASE_PATH'];
|
||||
} else if (aiProvider === OCO_AI_PROVIDER_ENUM.AZURE) {
|
||||
apiKey = config['OCO_AZURE_API_KEY'];
|
||||
apiUrl = config['OCO_AZURE_ENDPOINT'];
|
||||
} else if (aiProvider === OCO_AI_PROVIDER_ENUM.GEMINI) {
|
||||
apiKey = config['OCO_GEMINI_API_KEY'];
|
||||
apiUrl = config['OCO_GEMINI_BASE_PATH'];
|
||||
} else if (aiProvider === OCO_AI_PROVIDER_ENUM.FLOWISE) {
|
||||
apiKey = config['OCO_FLOWISE_API_KEY'];
|
||||
apiUrl = config['OCO_FLOWISE_ENDPOINT'];
|
||||
} else {
|
||||
throw new Error(
|
||||
`Migration failed, set AI provider first. Run "oco config set OCO_AI_PROVIDER=<provider>", where <provider> is one of: ${Object.values(
|
||||
OCO_AI_PROVIDER_ENUM
|
||||
).join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (apiKey) setConfig([[CONFIG_KEYS.OCO_API_KEY, apiKey]]);
|
||||
|
||||
if (apiUrl) setConfig([[CONFIG_KEYS.OCO_API_URL, apiUrl]]);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { getGlobalConfig, setGlobalConfig } from '../commands/config';
|
||||
|
||||
export default function () {
|
||||
const obsoleteKeys = [
|
||||
'OCO_OLLAMA_API_KEY',
|
||||
'OCO_OLLAMA_API_URL',
|
||||
'OCO_ANTHROPIC_API_KEY',
|
||||
'OCO_ANTHROPIC_BASE_PATH',
|
||||
'OCO_OPENAI_API_KEY',
|
||||
'OCO_OPENAI_BASE_PATH',
|
||||
'OCO_AZURE_API_KEY',
|
||||
'OCO_AZURE_ENDPOINT',
|
||||
'OCO_GEMINI_API_KEY',
|
||||
'OCO_GEMINI_BASE_PATH',
|
||||
'OCO_FLOWISE_API_KEY',
|
||||
'OCO_FLOWISE_ENDPOINT'
|
||||
];
|
||||
|
||||
const globalConfig = getGlobalConfig();
|
||||
|
||||
const configToOverride = { ...globalConfig };
|
||||
|
||||
for (const key of obsoleteKeys) delete configToOverride[key];
|
||||
|
||||
setGlobalConfig(configToOverride);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import {
|
||||
ConfigType,
|
||||
DEFAULT_CONFIG,
|
||||
getGlobalConfig,
|
||||
setConfig
|
||||
} from '../commands/config';
|
||||
|
||||
export default function () {
|
||||
const setDefaultConfigValues = (config: ConfigType) => {
|
||||
const entriesToSet: [key: string, value: string | boolean | number][] = [];
|
||||
for (const entry of Object.entries(DEFAULT_CONFIG)) {
|
||||
const [key, _value] = entry;
|
||||
if (config[key] === 'undefined' || config[key] === undefined)
|
||||
entriesToSet.push(entry);
|
||||
}
|
||||
|
||||
if (entriesToSet.length > 0) setConfig(entriesToSet);
|
||||
console.log(entriesToSet);
|
||||
};
|
||||
|
||||
setDefaultConfigValues(getGlobalConfig());
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import migration00 from './00_use_single_api_key_and_url';
|
||||
import migration01 from './01_remove_obsolete_config_keys_from_global_file';
|
||||
import migration02 from './02_set_missing_default_values';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
name: '00_use_single_api_key_and_url',
|
||||
run: migration00
|
||||
},
|
||||
{
|
||||
name: '01_remove_obsolete_config_keys_from_global_file',
|
||||
run: migration01
|
||||
},
|
||||
{
|
||||
name: '02_set_missing_default_values',
|
||||
run: migration02
|
||||
}
|
||||
];
|
||||
@@ -1,71 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join as pathJoin } from 'path';
|
||||
import { migrations } from './_migrations';
|
||||
import { outro } from '@clack/prompts';
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
getConfig,
|
||||
getIsGlobalConfigFileExist,
|
||||
OCO_AI_PROVIDER_ENUM
|
||||
} from '../commands/config';
|
||||
|
||||
const migrationsFile = pathJoin(homedir(), '.opencommit_migrations');
|
||||
|
||||
const getCompletedMigrations = (): string[] => {
|
||||
if (!fs.existsSync(migrationsFile)) {
|
||||
return [];
|
||||
}
|
||||
const data = fs.readFileSync(migrationsFile, 'utf-8');
|
||||
return data ? JSON.parse(data) : [];
|
||||
};
|
||||
|
||||
const saveCompletedMigration = (migrationName: string) => {
|
||||
const completedMigrations = getCompletedMigrations();
|
||||
completedMigrations.push(migrationName);
|
||||
fs.writeFileSync(
|
||||
migrationsFile,
|
||||
JSON.stringify(completedMigrations, null, 2)
|
||||
);
|
||||
};
|
||||
|
||||
export const runMigrations = async () => {
|
||||
// if no config file, we assume it's a new installation and no migrations are needed
|
||||
if (!getIsGlobalConfigFileExist()) return;
|
||||
|
||||
const config = getConfig();
|
||||
if (config.OCO_AI_PROVIDER === OCO_AI_PROVIDER_ENUM.TEST) return;
|
||||
|
||||
const completedMigrations = getCompletedMigrations();
|
||||
|
||||
let isMigrated = false;
|
||||
|
||||
for (const migration of migrations) {
|
||||
if (!completedMigrations.includes(migration.name)) {
|
||||
try {
|
||||
console.log('Applying migration', migration.name);
|
||||
migration.run();
|
||||
console.log('Migration applied successfully', migration.name);
|
||||
saveCompletedMigration(migration.name);
|
||||
} catch (error) {
|
||||
outro(
|
||||
`${chalk.red('Failed to apply migration')} ${
|
||||
migration.name
|
||||
}: ${error}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
isMigrated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMigrated) {
|
||||
outro(
|
||||
`${chalk.green(
|
||||
'✔'
|
||||
)} Migrations to your config were applied successfully. Please rerun.`
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
@@ -111,24 +111,9 @@ const getOneLineCommitInstruction = () =>
|
||||
? '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.'
|
||||
: '';
|
||||
|
||||
/**
|
||||
* Get the context of the user input
|
||||
* @param extraArgs - The arguments passed to the command line
|
||||
* @example
|
||||
* $ oco -- This is a context used to generate the commit message
|
||||
* @returns - The context of the user input
|
||||
*/
|
||||
const userInputCodeContext = (context: string) => {
|
||||
if (context !== '' && context !== ' ') {
|
||||
return `Additional context provided by the user: <context>${context}</context>\nConsider this context when generating the commit message, incorporating relevant information when appropriate.`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const INIT_MAIN_PROMPT = (
|
||||
language: string,
|
||||
fullGitMojiSpec: boolean,
|
||||
context: string
|
||||
fullGitMojiSpec: boolean
|
||||
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
|
||||
role: 'system',
|
||||
content: (() => {
|
||||
@@ -142,16 +127,15 @@ const INIT_MAIN_PROMPT = (
|
||||
const descriptionGuideline = getDescriptionInstruction();
|
||||
const oneLineCommitGuideline = getOneLineCommitInstruction();
|
||||
const generalGuidelines = `Use the present tense. Lines must not be longer than 74 characters. Use ${language} for the commit message.`;
|
||||
const userInputContext = userInputCodeContext(context);
|
||||
|
||||
return `${missionStatement}\n${diffInstruction}\n${conventionGuidelines}\n${descriptionGuideline}\n${oneLineCommitGuideline}\n${generalGuidelines}\n${userInputContext}`;
|
||||
return `${missionStatement}\n${diffInstruction}\n${conventionGuidelines}\n${descriptionGuideline}\n${oneLineCommitGuideline}\n${generalGuidelines}`;
|
||||
})()
|
||||
});
|
||||
|
||||
export const INIT_DIFF_PROMPT: OpenAI.Chat.Completions.ChatCompletionMessageParam =
|
||||
{
|
||||
role: 'user',
|
||||
content: `diff --git a/src/server.ts b/src/server.ts
|
||||
{
|
||||
role: 'user',
|
||||
content: `diff --git a/src/server.ts b/src/server.ts
|
||||
index ad4db42..f3b18a9 100644
|
||||
--- a/src/server.ts
|
||||
+++ b/src/server.ts
|
||||
@@ -175,7 +159,7 @@ export const INIT_DIFF_PROMPT: OpenAI.Chat.Completions.ChatCompletionMessagePara
|
||||
+app.listen(process.env.PORT || PORT, () => {
|
||||
+ console.log(\`Server listening on port \${PORT}\`);
|
||||
});`
|
||||
};
|
||||
};
|
||||
|
||||
const getContent = (translation: ConsistencyPrompt) => {
|
||||
const fix = config.OCO_EMOJI
|
||||
@@ -201,8 +185,7 @@ const INIT_CONSISTENCY_PROMPT = (
|
||||
});
|
||||
|
||||
export const getMainCommitPrompt = async (
|
||||
fullGitMojiSpec: boolean,
|
||||
context: string
|
||||
fullGitMojiSpec: boolean
|
||||
): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => {
|
||||
switch (config.OCO_PROMPT_MODULE) {
|
||||
case '@commitlint':
|
||||
@@ -224,14 +207,14 @@ export const getMainCommitPrompt = async (
|
||||
INIT_DIFF_PROMPT,
|
||||
INIT_CONSISTENCY_PROMPT(
|
||||
commitLintConfig.consistency[
|
||||
translation.localLanguage
|
||||
translation.localLanguage
|
||||
] as ConsistencyPrompt
|
||||
)
|
||||
];
|
||||
|
||||
default:
|
||||
return [
|
||||
INIT_MAIN_PROMPT(translation.localLanguage, fullGitMojiSpec, context),
|
||||
INIT_MAIN_PROMPT(translation.localLanguage, fullGitMojiSpec),
|
||||
INIT_DIFF_PROMPT,
|
||||
INIT_CONSISTENCY_PROMPT(translation)
|
||||
];
|
||||
|
||||
@@ -2,14 +2,11 @@ import { getConfig, OCO_AI_PROVIDER_ENUM } from '../commands/config';
|
||||
import { AnthropicEngine } from '../engine/anthropic';
|
||||
import { AzureEngine } from '../engine/azure';
|
||||
import { AiEngine } from '../engine/Engine';
|
||||
import { FlowiseEngine } from '../engine/flowise';
|
||||
import { GeminiEngine } from '../engine/gemini';
|
||||
import { OllamaEngine } from '../engine/ollama';
|
||||
import { FlowiseAi } from '../engine/flowise';
|
||||
import { Gemini } from '../engine/gemini';
|
||||
import { OllamaAi } from '../engine/ollama';
|
||||
import { OpenAiEngine } from '../engine/openAi';
|
||||
import { MistralAiEngine } from '../engine/mistral';
|
||||
import { TestAi, TestMockType } from '../engine/testAi';
|
||||
import { GroqEngine } from '../engine/groq';
|
||||
import { MLXEngine } from '../engine/mlx';
|
||||
|
||||
export function getEngine(): AiEngine {
|
||||
const config = getConfig();
|
||||
@@ -19,39 +16,50 @@ export function getEngine(): AiEngine {
|
||||
model: config.OCO_MODEL!,
|
||||
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
|
||||
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
|
||||
baseURL: config.OCO_API_URL!,
|
||||
apiKey: config.OCO_API_KEY!
|
||||
baseURL: config.OCO_OPENAI_BASE_PATH!
|
||||
};
|
||||
|
||||
switch (provider) {
|
||||
case OCO_AI_PROVIDER_ENUM.OLLAMA:
|
||||
return new OllamaEngine(DEFAULT_CONFIG);
|
||||
return new OllamaAi({
|
||||
...DEFAULT_CONFIG,
|
||||
apiKey: '',
|
||||
baseURL: config.OCO_OLLAMA_API_URL!
|
||||
});
|
||||
|
||||
case OCO_AI_PROVIDER_ENUM.ANTHROPIC:
|
||||
return new AnthropicEngine(DEFAULT_CONFIG);
|
||||
return new AnthropicEngine({
|
||||
...DEFAULT_CONFIG,
|
||||
apiKey: config.OCO_ANTHROPIC_API_KEY!
|
||||
});
|
||||
|
||||
case OCO_AI_PROVIDER_ENUM.TEST:
|
||||
return new TestAi(config.OCO_TEST_MOCK_TYPE as TestMockType);
|
||||
|
||||
case OCO_AI_PROVIDER_ENUM.GEMINI:
|
||||
return new GeminiEngine(DEFAULT_CONFIG);
|
||||
return new Gemini({
|
||||
...DEFAULT_CONFIG,
|
||||
apiKey: config.OCO_GEMINI_API_KEY!,
|
||||
baseURL: config.OCO_GEMINI_BASE_PATH!
|
||||
});
|
||||
|
||||
case OCO_AI_PROVIDER_ENUM.AZURE:
|
||||
return new AzureEngine(DEFAULT_CONFIG);
|
||||
return new AzureEngine({
|
||||
...DEFAULT_CONFIG,
|
||||
apiKey: config.OCO_AZURE_API_KEY!
|
||||
});
|
||||
|
||||
case OCO_AI_PROVIDER_ENUM.FLOWISE:
|
||||
return new FlowiseEngine(DEFAULT_CONFIG);
|
||||
|
||||
case OCO_AI_PROVIDER_ENUM.GROQ:
|
||||
return new GroqEngine(DEFAULT_CONFIG);
|
||||
|
||||
case OCO_AI_PROVIDER_ENUM.MISTRAL:
|
||||
return new MistralAiEngine(DEFAULT_CONFIG);
|
||||
|
||||
case OCO_AI_PROVIDER_ENUM.MLX:
|
||||
return new MLXEngine(DEFAULT_CONFIG);
|
||||
return new FlowiseAi({
|
||||
...DEFAULT_CONFIG,
|
||||
baseURL: config.OCO_FLOWISE_ENDPOINT || DEFAULT_CONFIG.baseURL,
|
||||
apiKey: config.OCO_FLOWISE_API_KEY!
|
||||
});
|
||||
|
||||
default:
|
||||
return new OpenAiEngine(DEFAULT_CONFIG);
|
||||
return new OpenAiEngine({
|
||||
...DEFAULT_CONFIG,
|
||||
apiKey: config.OCO_OPENAI_API_KEY!
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,18 @@ export const assertGitRepo = async () => {
|
||||
// (file) => `:(exclude)${file}`
|
||||
// );
|
||||
|
||||
export const getIgnoredFolders = (): string[] => {
|
||||
try {
|
||||
return readFileSync('.opencommitignore').toString().split('\n');
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getOpenCommitIgnore = (): Ignore => {
|
||||
const ig = ignore();
|
||||
|
||||
try {
|
||||
ig.add(readFileSync('.opencommitignore').toString().split('\n'));
|
||||
} catch (e) {}
|
||||
|
||||
const ignorePatterns = getIgnoredFolders();
|
||||
ig.add(ignorePatterns);
|
||||
return ig;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
import path from 'path';
|
||||
import 'cli-testing-library/extend-expect';
|
||||
import { exec } from 'child_process';
|
||||
import { prepareTempDir } from './utils';
|
||||
import { promisify } from 'util';
|
||||
import { render } from 'cli-testing-library';
|
||||
import { resolve } from 'path';
|
||||
import { rm } from 'fs';
|
||||
const fsExec = promisify(exec);
|
||||
const fsRemove = promisify(rm);
|
||||
|
||||
/**
|
||||
* git remote -v
|
||||
*
|
||||
* [no remotes]
|
||||
*/
|
||||
const prepareNoRemoteGitRepository = async (): Promise<{
|
||||
gitDir: string;
|
||||
cleanup: () => Promise<void>;
|
||||
}> => {
|
||||
const tempDir = await prepareTempDir();
|
||||
await fsExec('git init test', { cwd: tempDir });
|
||||
const gitDir = path.resolve(tempDir, 'test');
|
||||
|
||||
const cleanup = async () => {
|
||||
return fsRemove(tempDir, { recursive: true });
|
||||
};
|
||||
return {
|
||||
gitDir,
|
||||
cleanup
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* git remote -v
|
||||
*
|
||||
* origin /tmp/remote.git (fetch)
|
||||
* origin /tmp/remote.git (push)
|
||||
*/
|
||||
const prepareOneRemoteGitRepository = async (): Promise<{
|
||||
gitDir: string;
|
||||
cleanup: () => Promise<void>;
|
||||
}> => {
|
||||
const tempDir = await prepareTempDir();
|
||||
await fsExec('git init --bare remote.git', { cwd: tempDir });
|
||||
await fsExec('git clone remote.git test', { cwd: tempDir });
|
||||
const gitDir = path.resolve(tempDir, 'test');
|
||||
|
||||
const cleanup = async () => {
|
||||
return fsRemove(tempDir, { recursive: true });
|
||||
};
|
||||
return {
|
||||
gitDir,
|
||||
cleanup
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* git remote -v
|
||||
*
|
||||
* origin /tmp/remote.git (fetch)
|
||||
* origin /tmp/remote.git (push)
|
||||
* other ../remote2.git (fetch)
|
||||
* other ../remote2.git (push)
|
||||
*/
|
||||
const prepareTwoRemotesGitRepository = async (): Promise<{
|
||||
gitDir: string;
|
||||
cleanup: () => Promise<void>;
|
||||
}> => {
|
||||
const tempDir = await prepareTempDir();
|
||||
await fsExec('git init --bare remote.git', { cwd: tempDir });
|
||||
await fsExec('git init --bare other.git', { cwd: tempDir });
|
||||
await fsExec('git clone remote.git test', { cwd: tempDir });
|
||||
const gitDir = path.resolve(tempDir, 'test');
|
||||
await fsExec('git remote add other ../other.git', { cwd: gitDir });
|
||||
|
||||
const cleanup = async () => {
|
||||
return fsRemove(tempDir, { recursive: true });
|
||||
};
|
||||
return {
|
||||
gitDir,
|
||||
cleanup
|
||||
};
|
||||
};
|
||||
|
||||
describe('cli flow to push git branch', () => {
|
||||
it('do nothing when OCO_GITPUSH is set to false', async () => {
|
||||
const { gitDir, cleanup } = await prepareNoRemoteGitRepository();
|
||||
|
||||
await render('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||
cwd: gitDir
|
||||
});
|
||||
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||
|
||||
const { queryByText, findByText, userEvent } = await render(
|
||||
`OCO_AI_PROVIDER='test' OCO_GITPUSH='false' node`,
|
||||
[resolve('./out/cli.cjs')],
|
||||
{ cwd: gitDir }
|
||||
);
|
||||
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||
userEvent.keyboard('[Enter]');
|
||||
|
||||
expect(
|
||||
await queryByText('Choose a remote to push to')
|
||||
).not.toBeInTheConsole();
|
||||
expect(
|
||||
await queryByText('Do you want to run `git push`?')
|
||||
).not.toBeInTheConsole();
|
||||
expect(
|
||||
await queryByText('Successfully pushed all commits to origin')
|
||||
).not.toBeInTheConsole();
|
||||
expect(
|
||||
await queryByText('Command failed with exit code 1')
|
||||
).not.toBeInTheConsole();
|
||||
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
it('push and cause error when there is no remote', async () => {
|
||||
const { gitDir, cleanup } = await prepareNoRemoteGitRepository();
|
||||
|
||||
await render('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||
cwd: gitDir
|
||||
});
|
||||
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||
|
||||
const { queryByText, findByText, userEvent } = await render(
|
||||
`OCO_AI_PROVIDER='test' node`,
|
||||
[resolve('./out/cli.cjs')],
|
||||
{ cwd: gitDir }
|
||||
);
|
||||
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||
userEvent.keyboard('[Enter]');
|
||||
|
||||
expect(
|
||||
await queryByText('Choose a remote to push to')
|
||||
).not.toBeInTheConsole();
|
||||
expect(
|
||||
await queryByText('Do you want to run `git push`?')
|
||||
).not.toBeInTheConsole();
|
||||
expect(
|
||||
await queryByText('Successfully pushed all commits to origin')
|
||||
).not.toBeInTheConsole();
|
||||
|
||||
expect(
|
||||
await findByText('Command failed with exit code 1')
|
||||
).toBeInTheConsole();
|
||||
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
it('push when one remote is set', async () => {
|
||||
const { gitDir, cleanup } = await prepareOneRemoteGitRepository();
|
||||
|
||||
await render('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||
cwd: gitDir
|
||||
});
|
||||
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||
|
||||
const { findByText, userEvent } = await render(
|
||||
`OCO_AI_PROVIDER='test' node`,
|
||||
[resolve('./out/cli.cjs')],
|
||||
{ cwd: gitDir }
|
||||
);
|
||||
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||
userEvent.keyboard('[Enter]');
|
||||
|
||||
expect(
|
||||
await findByText('Do you want to run `git push`?')
|
||||
).toBeInTheConsole();
|
||||
userEvent.keyboard('[Enter]');
|
||||
|
||||
expect(
|
||||
await findByText('Successfully pushed all commits to origin')
|
||||
).toBeInTheConsole();
|
||||
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
it('push when two remotes are set', async () => {
|
||||
const { gitDir, cleanup } = await prepareTwoRemotesGitRepository();
|
||||
|
||||
await render('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||
cwd: gitDir
|
||||
});
|
||||
await render('git', ['add index.ts'], { cwd: gitDir });
|
||||
|
||||
const { findByText, userEvent } = await render(
|
||||
`OCO_AI_PROVIDER='test' node`,
|
||||
[resolve('./out/cli.cjs')],
|
||||
{ cwd: gitDir }
|
||||
);
|
||||
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||
userEvent.keyboard('[Enter]');
|
||||
|
||||
expect(await findByText('Choose a remote to push to')).toBeInTheConsole();
|
||||
userEvent.keyboard('[Enter]');
|
||||
|
||||
expect(
|
||||
await findByText('Successfully pushed all commits to origin')
|
||||
).toBeInTheConsole();
|
||||
|
||||
await cleanup();
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ it('cli flow to generate commit message for 1 new file (staged)', async () => {
|
||||
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||
userEvent.keyboard('[Enter]');
|
||||
|
||||
expect(await findByText('Do you want to run `git push`?')).toBeInTheConsole();
|
||||
expect(await findByText('Choose a remote to push to')).toBeInTheConsole();
|
||||
userEvent.keyboard('[Enter]');
|
||||
|
||||
expect(await findByText('Successfully pushed all commits to origin')).toBeInTheConsole();
|
||||
@@ -46,7 +46,7 @@ it('cli flow to generate commit message for 1 changed file (not staged)', async
|
||||
|
||||
expect(await findByText('Successfully committed')).toBeInTheConsole();
|
||||
|
||||
expect(await findByText('Do you want to run `git push`?')).toBeInTheConsole();
|
||||
expect(await findByText('Choose a remote to push to')).toBeInTheConsole();
|
||||
userEvent.keyboard('[Enter]');
|
||||
|
||||
expect(await findByText('Successfully pushed all commits to origin')).toBeInTheConsole();
|
||||
|
||||
@@ -209,7 +209,7 @@ describe('cli flow to generate commit message using @commitlint prompt-module',
|
||||
oco.userEvent.keyboard('[Enter]');
|
||||
|
||||
expect(
|
||||
await oco.findByText('Do you want to run `git push`?')
|
||||
await oco.findByText('Choose a remote to push to')
|
||||
).toBeInTheConsole();
|
||||
oco.userEvent.keyboard('[Enter]');
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const prepareEnvironment = async (): Promise<{
|
||||
gitDir: string;
|
||||
cleanup: () => Promise<void>;
|
||||
}> => {
|
||||
const tempDir = await prepareTempDir();
|
||||
const tempDir = await fsMakeTempDir(path.join(tmpdir(), 'opencommit-test-'));
|
||||
// Create a remote git repository int the temp directory. This is necessary to execute the `git push` command
|
||||
await fsExec('git init --bare remote.git', { cwd: tempDir });
|
||||
await fsExec('git clone remote.git test', { cwd: tempDir });
|
||||
@@ -30,8 +30,4 @@ export const prepareEnvironment = async (): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
export const prepareTempDir = async(): Promise<string> => {
|
||||
return await fsMakeTempDir(path.join(tmpdir(), 'opencommit-test-'));
|
||||
}
|
||||
|
||||
export const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { existsSync, readFileSync, rmSync } from 'fs';
|
||||
import {
|
||||
CONFIG_KEYS,
|
||||
DEFAULT_CONFIG,
|
||||
getConfig,
|
||||
setConfig
|
||||
@@ -51,13 +50,14 @@ describe('config', () => {
|
||||
describe('getConfig', () => {
|
||||
it('should prioritize local .env over global .opencommit config', async () => {
|
||||
globalConfigFile = await generateConfig('.opencommit', {
|
||||
OCO_API_KEY: 'global-key',
|
||||
OCO_OPENAI_API_KEY: 'global-key',
|
||||
OCO_MODEL: 'gpt-3.5-turbo',
|
||||
OCO_LANGUAGE: 'en'
|
||||
});
|
||||
|
||||
envConfigFile = await generateConfig('.env', {
|
||||
OCO_API_KEY: 'local-key',
|
||||
OCO_OPENAI_API_KEY: 'local-key',
|
||||
OCO_ANTHROPIC_API_KEY: 'local-anthropic-key',
|
||||
OCO_LANGUAGE: 'fr'
|
||||
});
|
||||
|
||||
@@ -67,21 +67,22 @@ describe('config', () => {
|
||||
});
|
||||
|
||||
expect(config).not.toEqual(null);
|
||||
expect(config.OCO_API_KEY).toEqual('local-key');
|
||||
expect(config.OCO_OPENAI_API_KEY).toEqual('local-key');
|
||||
expect(config.OCO_MODEL).toEqual('gpt-3.5-turbo');
|
||||
expect(config.OCO_LANGUAGE).toEqual('fr');
|
||||
expect(config.OCO_ANTHROPIC_API_KEY).toEqual('local-anthropic-key');
|
||||
});
|
||||
|
||||
it('should fallback to global config when local config is not set', async () => {
|
||||
globalConfigFile = await generateConfig('.opencommit', {
|
||||
OCO_API_KEY: 'global-key',
|
||||
OCO_OPENAI_API_KEY: 'global-key',
|
||||
OCO_MODEL: 'gpt-4',
|
||||
OCO_LANGUAGE: 'de',
|
||||
OCO_DESCRIPTION: 'true'
|
||||
});
|
||||
|
||||
envConfigFile = await generateConfig('.env', {
|
||||
OCO_API_URL: 'local-api-url'
|
||||
OCO_ANTHROPIC_API_KEY: 'local-anthropic-key'
|
||||
});
|
||||
|
||||
const config = getConfig({
|
||||
@@ -90,8 +91,8 @@ describe('config', () => {
|
||||
});
|
||||
|
||||
expect(config).not.toEqual(null);
|
||||
expect(config.OCO_API_KEY).toEqual('global-key');
|
||||
expect(config.OCO_API_URL).toEqual('local-api-url');
|
||||
expect(config.OCO_OPENAI_API_KEY).toEqual('global-key');
|
||||
expect(config.OCO_ANTHROPIC_API_KEY).toEqual('local-anthropic-key');
|
||||
expect(config.OCO_MODEL).toEqual('gpt-4');
|
||||
expect(config.OCO_LANGUAGE).toEqual('de');
|
||||
expect(config.OCO_DESCRIPTION).toEqual(true);
|
||||
@@ -123,7 +124,7 @@ describe('config', () => {
|
||||
|
||||
it('should handle empty local config correctly', async () => {
|
||||
globalConfigFile = await generateConfig('.opencommit', {
|
||||
OCO_API_KEY: 'global-key',
|
||||
OCO_OPENAI_API_KEY: 'global-key',
|
||||
OCO_MODEL: 'gpt-4',
|
||||
OCO_LANGUAGE: 'es'
|
||||
});
|
||||
@@ -136,20 +137,20 @@ describe('config', () => {
|
||||
});
|
||||
|
||||
expect(config).not.toEqual(null);
|
||||
expect(config.OCO_API_KEY).toEqual('global-key');
|
||||
expect(config.OCO_OPENAI_API_KEY).toEqual('global-key');
|
||||
expect(config.OCO_MODEL).toEqual('gpt-4');
|
||||
expect(config.OCO_LANGUAGE).toEqual('es');
|
||||
});
|
||||
|
||||
it('should override global config with null values in local .env', async () => {
|
||||
globalConfigFile = await generateConfig('.opencommit', {
|
||||
OCO_API_KEY: 'global-key',
|
||||
OCO_OPENAI_API_KEY: 'global-key',
|
||||
OCO_MODEL: 'gpt-4',
|
||||
OCO_LANGUAGE: 'es'
|
||||
});
|
||||
|
||||
envConfigFile = await generateConfig('.env', {
|
||||
OCO_API_KEY: 'null'
|
||||
OCO_OPENAI_API_KEY: 'null'
|
||||
});
|
||||
|
||||
const config = getConfig({
|
||||
@@ -158,7 +159,7 @@ describe('config', () => {
|
||||
});
|
||||
|
||||
expect(config).not.toEqual(null);
|
||||
expect(config.OCO_API_KEY).toEqual(null);
|
||||
expect(config.OCO_OPENAI_API_KEY).toEqual(null);
|
||||
});
|
||||
|
||||
it('should handle empty global config', async () => {
|
||||
@@ -171,7 +172,7 @@ describe('config', () => {
|
||||
});
|
||||
|
||||
expect(config).not.toEqual(null);
|
||||
expect(config.OCO_API_KEY).toEqual(undefined);
|
||||
expect(config.OCO_OPENAI_API_KEY).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -187,12 +188,12 @@ describe('config', () => {
|
||||
expect(isGlobalConfigFileExist).toBe(false);
|
||||
|
||||
await setConfig(
|
||||
[[CONFIG_KEYS.OCO_API_KEY, 'persisted-key_1']],
|
||||
[['OCO_OPENAI_API_KEY', 'persisted-key_1']],
|
||||
globalConfigFile.filePath
|
||||
);
|
||||
|
||||
const fileContent = readFileSync(globalConfigFile.filePath, 'utf8');
|
||||
expect(fileContent).toContain('OCO_API_KEY=persisted-key_1');
|
||||
expect(fileContent).toContain('OCO_OPENAI_API_KEY=persisted-key_1');
|
||||
Object.entries(DEFAULT_CONFIG).forEach(([key, value]) => {
|
||||
expect(fileContent).toContain(`${key}=${value}`);
|
||||
});
|
||||
@@ -202,48 +203,42 @@ describe('config', () => {
|
||||
globalConfigFile = await generateConfig('.opencommit', {});
|
||||
await setConfig(
|
||||
[
|
||||
[CONFIG_KEYS.OCO_API_KEY, 'new-key'],
|
||||
[CONFIG_KEYS.OCO_MODEL, 'gpt-4']
|
||||
['OCO_OPENAI_API_KEY', 'new-key'],
|
||||
['OCO_MODEL', 'gpt-4']
|
||||
],
|
||||
globalConfigFile.filePath
|
||||
);
|
||||
|
||||
const config = getConfig({
|
||||
globalPath: globalConfigFile.filePath
|
||||
});
|
||||
expect(config.OCO_API_KEY).toEqual('new-key');
|
||||
const config = getConfig({ globalPath: globalConfigFile.filePath });
|
||||
expect(config.OCO_OPENAI_API_KEY).toEqual('new-key');
|
||||
expect(config.OCO_MODEL).toEqual('gpt-4');
|
||||
});
|
||||
|
||||
it('should update existing config values', async () => {
|
||||
globalConfigFile = await generateConfig('.opencommit', {
|
||||
OCO_API_KEY: 'initial-key'
|
||||
OCO_OPENAI_API_KEY: 'initial-key'
|
||||
});
|
||||
await setConfig(
|
||||
[[CONFIG_KEYS.OCO_API_KEY, 'updated-key']],
|
||||
[['OCO_OPENAI_API_KEY', 'updated-key']],
|
||||
globalConfigFile.filePath
|
||||
);
|
||||
|
||||
const config = getConfig({
|
||||
globalPath: globalConfigFile.filePath
|
||||
});
|
||||
expect(config.OCO_API_KEY).toEqual('updated-key');
|
||||
const config = getConfig({ globalPath: globalConfigFile.filePath });
|
||||
expect(config.OCO_OPENAI_API_KEY).toEqual('updated-key');
|
||||
});
|
||||
|
||||
it('should handle boolean and numeric values correctly', async () => {
|
||||
globalConfigFile = await generateConfig('.opencommit', {});
|
||||
await setConfig(
|
||||
[
|
||||
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT, '8192'],
|
||||
[CONFIG_KEYS.OCO_DESCRIPTION, 'true'],
|
||||
[CONFIG_KEYS.OCO_ONE_LINE_COMMIT, 'false']
|
||||
['OCO_TOKENS_MAX_INPUT', '8192'],
|
||||
['OCO_DESCRIPTION', 'true'],
|
||||
['OCO_ONE_LINE_COMMIT', 'false']
|
||||
],
|
||||
globalConfigFile.filePath
|
||||
);
|
||||
|
||||
const config = getConfig({
|
||||
globalPath: globalConfigFile.filePath
|
||||
});
|
||||
const config = getConfig({ globalPath: globalConfigFile.filePath });
|
||||
expect(config.OCO_TOKENS_MAX_INPUT).toEqual(8192);
|
||||
expect(config.OCO_DESCRIPTION).toEqual(true);
|
||||
expect(config.OCO_ONE_LINE_COMMIT).toEqual(false);
|
||||
@@ -271,12 +266,12 @@ describe('config', () => {
|
||||
expect(isGlobalConfigFileExist).toBe(false);
|
||||
|
||||
await setConfig(
|
||||
[[CONFIG_KEYS.OCO_API_KEY, 'persisted-key']],
|
||||
[['OCO_OPENAI_API_KEY', 'persisted-key']],
|
||||
globalConfigFile.filePath
|
||||
);
|
||||
|
||||
const fileContent = readFileSync(globalConfigFile.filePath, 'utf8');
|
||||
expect(fileContent).toContain('OCO_API_KEY=persisted-key');
|
||||
expect(fileContent).toContain('OCO_OPENAI_API_KEY=persisted-key');
|
||||
});
|
||||
|
||||
it('should set multiple configs in a row and keep the changes', async () => {
|
||||
@@ -284,17 +279,14 @@ describe('config', () => {
|
||||
expect(isGlobalConfigFileExist).toBe(false);
|
||||
|
||||
await setConfig(
|
||||
[[CONFIG_KEYS.OCO_API_KEY, 'persisted-key']],
|
||||
[['OCO_OPENAI_API_KEY', 'persisted-key']],
|
||||
globalConfigFile.filePath
|
||||
);
|
||||
|
||||
const fileContent1 = readFileSync(globalConfigFile.filePath, 'utf8');
|
||||
expect(fileContent1).toContain('OCO_API_KEY=persisted-key');
|
||||
expect(fileContent1).toContain('OCO_OPENAI_API_KEY=persisted-key');
|
||||
|
||||
await setConfig(
|
||||
[[CONFIG_KEYS.OCO_MODEL, 'gpt-4']],
|
||||
globalConfigFile.filePath
|
||||
);
|
||||
await setConfig([['OCO_MODEL', 'gpt-4']], globalConfigFile.filePath);
|
||||
|
||||
const fileContent2 = readFileSync(globalConfigFile.filePath, 'utf8');
|
||||
expect(fileContent2).toContain('OCO_MODEL=gpt-4');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GeminiEngine } from '../../src/engine/gemini';
|
||||
import { Gemini } from '../../src/engine/gemini';
|
||||
|
||||
import { GenerativeModel, GoogleGenerativeAI } from '@google/generative-ai';
|
||||
import {
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { OpenAI } from 'openai';
|
||||
|
||||
describe('Gemini', () => {
|
||||
let gemini: GeminiEngine;
|
||||
let gemini: Gemini;
|
||||
let mockConfig: ConfigType;
|
||||
let mockGoogleGenerativeAi: GoogleGenerativeAI;
|
||||
let mockGenerativeModel: GenerativeModel;
|
||||
@@ -20,8 +20,8 @@ describe('Gemini', () => {
|
||||
const mockGemini = () => {
|
||||
mockConfig = getConfig() as ConfigType;
|
||||
|
||||
gemini = new GeminiEngine({
|
||||
apiKey: mockConfig.OCO_API_KEY,
|
||||
gemini = new Gemini({
|
||||
apiKey: mockConfig.OCO_GEMINI_API_KEY,
|
||||
model: mockConfig.OCO_MODEL
|
||||
});
|
||||
};
|
||||
@@ -45,10 +45,12 @@ describe('Gemini', () => {
|
||||
mockConfig = getConfig() as ConfigType;
|
||||
|
||||
mockConfig.OCO_AI_PROVIDER = OCO_AI_PROVIDER_ENUM.GEMINI;
|
||||
mockConfig.OCO_API_KEY = 'mock-api-key';
|
||||
mockConfig.OCO_GEMINI_API_KEY = 'mock-api-key';
|
||||
mockConfig.OCO_MODEL = 'gemini-1.5-flash';
|
||||
|
||||
mockGoogleGenerativeAi = new GoogleGenerativeAI(mockConfig.OCO_API_KEY);
|
||||
mockGoogleGenerativeAi = new GoogleGenerativeAI(
|
||||
mockConfig.OCO_GEMINI_API_KEY
|
||||
);
|
||||
mockGenerativeModel = mockGoogleGenerativeAi.getGenerativeModel({
|
||||
model: mockConfig.OCO_MODEL
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user