Merge remote-tracking branch 'origin/dev'

This commit is contained in:
di-sukharev
2024-05-25 19:14:41 +03:00
18 changed files with 13662 additions and 3429 deletions
+8 -2
View File
@@ -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
+19 -4
View File
@@ -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?'
});
@@ -154,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');
@@ -166,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();
@@ -238,7 +252,8 @@ export async function commit(
generateCommitMessageFromGitDiff(
await getDiff({ files: stagedFiles }),
extraArgs,
fullGitMojiSpec
fullGitMojiSpec,
skipCommitConfirmation
)
);
+46 -14
View File
@@ -14,6 +14,7 @@ import { getI18nLocal } from '../i18n';
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 +26,8 @@ export enum CONFIG_KEYS {
OCO_PROMPT_MODULE = 'OCO_PROMPT_MODULE',
OCO_AI_PROVIDER = 'OCO_AI_PROVIDER',
OCO_GITPUSH = 'OCO_GITPUSH',
OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT'
OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT',
OCO_AZURE_ENDPOINT = 'OCO_AZURE_ENDPOINT'
}
export enum CONFIG_MODES {
@@ -83,13 +85,23 @@ export const configValidators = {
//need api key unless running locally with ollama
validateConfig(
'OpenAI API_KEY',
value || config.OCO_ANTHROPIC_API_KEY || config.OCO_AI_PROVIDER == 'ollama' || config.OCO_AI_PROVIDER == 'test',
'You need to provide an OpenAI/Anthropic 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(
'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;
@@ -99,7 +111,7 @@ export const configValidators = {
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 API key'
'You need to provide an OpenAI/Anthropic/Azure API key'
);
return value;
@@ -184,9 +196,16 @@ export const configValidators = {
[CONFIG_KEYS.OCO_MODEL](value: any, config: any = {}) {
validateConfig(
CONFIG_KEYS.OCO_MODEL,
[...MODEL_LIST.openai, ...MODEL_LIST.anthropic].includes(value) || config.OCO_AI_PROVIDER == 'ollama' || config.OCO_AI_PROVIDER == 'test',
[...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;
},
@@ -221,13 +240,14 @@ export const configValidators = {
validateConfig(
CONFIG_KEYS.OCO_AI_PROVIDER,
[
'',
'openai',
'',
'openai',
'anthropic',
'ollama',
'azure',
'ollama',
'test'
].includes(value),
`${value} is not supported yet, use 'ollama' 'anthropic' or 'openai' (default)`
].includes(value) || value.startsWith('ollama'),
`${value} is not supported yet, use 'ollama/{model}', 'azure', 'anthropic' or 'openai' (default)`
);
return value;
},
@@ -239,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;
},
};
@@ -261,6 +290,7 @@ export const getConfig = ({
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,
@@ -277,7 +307,9 @@ export const getConfig = ({
OCO_PROMPT_MODULE: process.env.OCO_PROMPT_MODULE || 'conventional-commit',
OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER || 'openai',
OCO_GITPUSH: process.env.OCO_GITPUSH === 'false' ? false : true,
OCO_ONE_LINE_COMMIT: process.env.OCO_ONE_LINE_COMMIT === 'true' ? true : false
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);
@@ -302,7 +334,7 @@ export const getConfig = ({
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.`
);
+2 -2
View File
@@ -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'
);
}
+109
View 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();
+7 -2
View File
@@ -9,10 +9,15 @@ import {
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 = config?.OCO_MODEL || 'mistral';
const model = this.model;
//console.log(messages);
//process.exit()
@@ -21,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 {
+12 -6
View File
@@ -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));
}
}
+2 -2
View File
@@ -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);
+4 -4
View File
@@ -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;
};
};
+1 -1
View File
@@ -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.`
});
+9 -2
View File
@@ -2,18 +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;
}
+3 -3
View File
@@ -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;
}
};
};