mirror of
https://github.com/di-sukharev/opencommit.git
synced 2026-04-20 03:02:51 -04:00
feat: Integrate @commitlint for Enhanced Commit Message Generation and Configuration Support (#209)
* add commitlint support * refactor code * improve readme text
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
export enum COMMANDS {
|
||||
config = 'config',
|
||||
hook = 'hook'
|
||||
hook = 'hook',
|
||||
commitlint = 'commitlint'
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
import axios from 'axios';
|
||||
import chalk from 'chalk';
|
||||
import { execa } from 'execa';
|
||||
import {
|
||||
ChatCompletionRequestMessage,
|
||||
Configuration as OpenAiApiConfiguration,
|
||||
OpenAIApi
|
||||
} from 'openai';
|
||||
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
|
||||
import {
|
||||
CONFIG_MODES,
|
||||
DEFAULT_MODEL_TOKEN_LIMIT,
|
||||
getConfig
|
||||
} from './commands/config';
|
||||
import { tokenCount } from './utils/tokenCount';
|
||||
import { GenerateCommitMessageErrorEnum } from './generateCommitMessageFromGitDiff';
|
||||
import { execa } from 'execa';
|
||||
import { tokenCount } from './utils/tokenCount';
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import packageJSON from '../package.json' assert { type: 'json' };
|
||||
|
||||
import packageJSON from '../package.json';
|
||||
import { commit } from './commands/commit';
|
||||
import { commitlintConfigCommand } from './commands/commitlint';
|
||||
import { configCommand } from './commands/config';
|
||||
import { hookCommand, isHookCalled } from './commands/githook.js';
|
||||
import { prepareCommitMessageHook } from './commands/prepare-commit-msg-hook';
|
||||
import { commit } from './commands/commit';
|
||||
import { checkIsLatestVersion } from './utils/checkIsLatestVersion';
|
||||
|
||||
const extraArgs = process.argv.slice(2);
|
||||
@@ -15,7 +16,7 @@ cli(
|
||||
{
|
||||
version: packageJSON.version,
|
||||
name: 'opencommit',
|
||||
commands: [configCommand, hookCommand],
|
||||
commands: [configCommand, hookCommand, commitlintConfigCommand],
|
||||
flags: {},
|
||||
ignoreArgv: (type) => type === 'unknown-flag' || type === 'argument',
|
||||
help: { description: packageJSON.description }
|
||||
|
||||
9
src/commands/README.md
Normal file
9
src/commands/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# @commitlint Module for opencommit
|
||||
|
||||
1. Load commitlint configuration within tree.
|
||||
2. Generate a commit with commitlint prompt:
|
||||
- Will not run if hash is the same.
|
||||
- Infer a prompt for each commitlint rule.
|
||||
- Ask OpenAI to generate consistency with embedded commitlint rules.
|
||||
- Store configuration close to commitlint configuration.
|
||||
3. Replace conventional-commit prompt with commitlint prompt.
|
||||
@@ -1,4 +1,16 @@
|
||||
import chalk from 'chalk';
|
||||
import { execa } from 'execa';
|
||||
|
||||
import {
|
||||
confirm,
|
||||
intro,
|
||||
isCancel,
|
||||
multiselect,
|
||||
outro,
|
||||
select,
|
||||
spinner
|
||||
} from '@clack/prompts';
|
||||
|
||||
import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
|
||||
import {
|
||||
assertGitRepo,
|
||||
@@ -7,18 +19,8 @@ import {
|
||||
getStagedFiles,
|
||||
gitAdd
|
||||
} from '../utils/git';
|
||||
import {
|
||||
spinner,
|
||||
confirm,
|
||||
outro,
|
||||
isCancel,
|
||||
intro,
|
||||
multiselect,
|
||||
select
|
||||
} from '@clack/prompts';
|
||||
import { getConfig } from '../commands/config';
|
||||
import chalk from 'chalk';
|
||||
import { trytm } from '../utils/trytm';
|
||||
import { getConfig } from './config';
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
|
||||
46
src/commands/commitlint.ts
Normal file
46
src/commands/commitlint.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import chalk from 'chalk';
|
||||
import { command } from 'cleye';
|
||||
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
|
||||
import { COMMANDS } from '../CommandsEnum';
|
||||
import { configureCommitlintIntegration } from '../modules/commitlint/config';
|
||||
import { getCommitlintLLMConfig } from '../modules/commitlint/utils';
|
||||
|
||||
export enum CONFIG_MODES {
|
||||
get = 'get',
|
||||
force = 'force'
|
||||
}
|
||||
|
||||
export const commitlintConfigCommand = command(
|
||||
{
|
||||
name: COMMANDS.commitlint,
|
||||
parameters: ['<mode>']
|
||||
},
|
||||
async (argv) => {
|
||||
intro('opencommit — configure @commitlint');
|
||||
try {
|
||||
const { mode } = argv._;
|
||||
|
||||
if (mode === CONFIG_MODES.get) {
|
||||
const commitLintConfig = await getCommitlintLLMConfig();
|
||||
|
||||
outro(commitLintConfig.toString());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === CONFIG_MODES.force) {
|
||||
await configureCommitlintIntegration(true);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unsupported mode: ${mode}. Valid modes are: "force" and "get"`
|
||||
);
|
||||
} catch (error) {
|
||||
outro(`${chalk.red('✖')} ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1,15 +1,16 @@
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
import chalk from 'chalk';
|
||||
import { command } from 'cleye';
|
||||
import * as dotenv from 'dotenv';
|
||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { parse as iniParse, stringify as iniStringify } from 'ini';
|
||||
import { homedir } from 'os';
|
||||
import { join as pathJoin } from 'path';
|
||||
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
|
||||
import { COMMANDS } from '../CommandsEnum';
|
||||
import { getI18nLocal } from '../i18n';
|
||||
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export enum CONFIG_KEYS {
|
||||
@@ -20,7 +21,8 @@ export enum CONFIG_KEYS {
|
||||
OCO_EMOJI = 'OCO_EMOJI',
|
||||
OCO_MODEL = 'OCO_MODEL',
|
||||
OCO_LANGUAGE = 'OCO_LANGUAGE',
|
||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER = 'OCO_MESSAGE_TEMPLATE_PLACEHOLDER'
|
||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER = 'OCO_MESSAGE_TEMPLATE_PLACEHOLDER',
|
||||
OCO_PROMPT_MODULE = 'OCO_PROMPT_MODULE'
|
||||
}
|
||||
|
||||
export const DEFAULT_MODEL_TOKEN_LIMIT = 4096;
|
||||
@@ -137,6 +139,16 @@ export const configValidators = {
|
||||
value.startsWith('$'),
|
||||
`${value} must start with $, for example: '$msg'`
|
||||
);
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_PROMPT_MODULE](value: any) {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_PROMPT_MODULE,
|
||||
['conventional-commit', '@commitlint'].includes(value),
|
||||
`${value} is not supported yet, use '@commitlint' or 'conventional-commit' (default)`
|
||||
);
|
||||
|
||||
return value;
|
||||
}
|
||||
};
|
||||
@@ -159,7 +171,8 @@ export const getConfig = (): ConfigType | null => {
|
||||
OCO_MODEL: process.env.OCO_MODEL || 'gpt-3.5-turbo-16k',
|
||||
OCO_LANGUAGE: process.env.OCO_LANGUAGE || 'en',
|
||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER:
|
||||
process.env.OCO_MESSAGE_TEMPLATE_PLACEHOLDER || '$msg'
|
||||
process.env.OCO_MESSAGE_TEMPLATE_PLACEHOLDER || '$msg',
|
||||
OCO_PROMPT_MODULE: process.env.OCO_PROMPT_MODULE || 'conventional-commit'
|
||||
};
|
||||
|
||||
const configExists = existsSync(configPath);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import chalk from 'chalk';
|
||||
import { command } from 'cleye';
|
||||
import { existsSync } from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { command } from 'cleye';
|
||||
import { assertGitRepo, getCoreHooksPath } from '../utils/git.js';
|
||||
import { existsSync } from 'fs';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
|
||||
import { COMMANDS } from '../CommandsEnum.js';
|
||||
import { assertGitRepo, getCoreHooksPath } from '../utils/git.js';
|
||||
|
||||
const HOOK_NAME = 'prepare-commit-msg';
|
||||
const DEFAULT_SYMLINK_URL = path.join('.git', 'hooks', HOOK_NAME);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import fs from 'fs/promises';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
import { intro, outro, spinner } from '@clack/prompts';
|
||||
|
||||
import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
|
||||
import { getChangedFiles, getDiff, getStagedFiles, gitAdd } from '../utils/git';
|
||||
import { getConfig } from './config';
|
||||
import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
|
||||
|
||||
const [messageFilePath, commitSource] = process.argv.slice(2);
|
||||
|
||||
|
||||
@@ -2,62 +2,20 @@ import {
|
||||
ChatCompletionRequestMessage,
|
||||
ChatCompletionRequestMessageRoleEnum
|
||||
} from 'openai';
|
||||
|
||||
import { api } from './api';
|
||||
import { DEFAULT_MODEL_TOKEN_LIMIT, getConfig } from './commands/config';
|
||||
import { getMainCommitPrompt } from './prompts';
|
||||
import { mergeDiffs } from './utils/mergeDiffs';
|
||||
import { i18n, I18nLocals } from './i18n';
|
||||
import { tokenCount } from './utils/tokenCount';
|
||||
|
||||
const config = getConfig();
|
||||
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
|
||||
|
||||
const INIT_MESSAGES_PROMPT: Array<ChatCompletionRequestMessage> = [
|
||||
{
|
||||
role: ChatCompletionRequestMessageRoleEnum.System,
|
||||
// prettier-ignore
|
||||
content: `You are to act as the author of a commit message in git. Your mission is to create clean and comprehensive commit messages in the conventional commit convention and explain WHAT were the changes and WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
|
||||
${config?.OCO_EMOJI ? 'Use GitMoji convention to preface the commit.' : 'Do not preface the commit with anything.'}
|
||||
${config?.OCO_DESCRIPTION ? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.' : "Don't add any descriptions to the commit, only commit message."}
|
||||
Use the present tense. Lines must not be longer than 74 characters. Use ${translation.localLanguage} to answer.`
|
||||
},
|
||||
{
|
||||
role: ChatCompletionRequestMessageRoleEnum.User,
|
||||
content: `diff --git a/src/server.ts b/src/server.ts
|
||||
index ad4db42..f3b18a9 100644
|
||||
--- a/src/server.ts
|
||||
+++ b/src/server.ts
|
||||
@@ -10,7 +10,7 @@
|
||||
import {
|
||||
initWinstonLogger();
|
||||
|
||||
const app = express();
|
||||
-const port = 7799;
|
||||
+const PORT = 7799;
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
@@ -34,6 +34,6 @@
|
||||
app.use((_, res, next) => {
|
||||
// ROUTES
|
||||
app.use(PROTECTED_ROUTER_URL, protectedRouter);
|
||||
|
||||
-app.listen(port, () => {
|
||||
- console.log(\`Server listening on port \${port}\`);
|
||||
+app.listen(process.env.PORT || PORT, () => {
|
||||
+ console.log(\`Server listening on port \${PORT}\`);
|
||||
});`
|
||||
},
|
||||
{
|
||||
role: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
content: `${config?.OCO_EMOJI ? '🐛 ' : ''}${translation.commitFix}
|
||||
${config?.OCO_EMOJI ? '✨ ' : ''}${translation.commitFeat}
|
||||
${config?.OCO_DESCRIPTION ? translation.commitDescription : ''}`
|
||||
}
|
||||
];
|
||||
|
||||
const generateCommitMessageChatCompletionPrompt = (
|
||||
const generateCommitMessageChatCompletionPrompt = async (
|
||||
diff: string
|
||||
): Array<ChatCompletionRequestMessage> => {
|
||||
): Promise<Array<ChatCompletionRequestMessage>> => {
|
||||
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt();
|
||||
|
||||
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
|
||||
|
||||
chatContextAsCompletionRequest.push({
|
||||
@@ -74,16 +32,18 @@ export enum GenerateCommitMessageErrorEnum {
|
||||
emptyMessage = 'EMPTY_MESSAGE'
|
||||
}
|
||||
|
||||
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
|
||||
(msg) => tokenCount(msg.content) + 4
|
||||
).reduce((a, b) => a + b, 0);
|
||||
|
||||
const ADJUSTMENT_FACTOR = 20;
|
||||
|
||||
export const generateCommitMessageByDiff = async (
|
||||
diff: string
|
||||
): Promise<string> => {
|
||||
try {
|
||||
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt();
|
||||
|
||||
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
|
||||
(msg) => tokenCount(msg.content) + 4
|
||||
).reduce((a, b) => a + b, 0);
|
||||
|
||||
const MAX_REQUEST_TOKENS =
|
||||
DEFAULT_MODEL_TOKEN_LIMIT -
|
||||
ADJUSTMENT_FACTOR -
|
||||
@@ -91,7 +51,7 @@ export const generateCommitMessageByDiff = async (
|
||||
config?.OCO_OPENAI_MAX_TOKENS;
|
||||
|
||||
if (tokenCount(diff) >= MAX_REQUEST_TOKENS) {
|
||||
const commitMessagePromises = getCommitMsgsPromisesFromFileDiffs(
|
||||
const commitMessagePromises = await getCommitMsgsPromisesFromFileDiffs(
|
||||
diff,
|
||||
MAX_REQUEST_TOKENS
|
||||
);
|
||||
@@ -103,16 +63,16 @@ export const generateCommitMessageByDiff = async (
|
||||
}
|
||||
|
||||
return commitMessages.join('\n\n');
|
||||
} else {
|
||||
const messages = generateCommitMessageChatCompletionPrompt(diff);
|
||||
|
||||
const commitMessage = await api.generateCommitMessage(messages);
|
||||
|
||||
if (!commitMessage)
|
||||
throw new Error(GenerateCommitMessageErrorEnum.emptyMessage);
|
||||
|
||||
return commitMessage;
|
||||
}
|
||||
|
||||
const messages = await generateCommitMessageChatCompletionPrompt(diff);
|
||||
|
||||
const commitMessage = await api.generateCommitMessage(messages);
|
||||
|
||||
if (!commitMessage)
|
||||
throw new Error(GenerateCommitMessageErrorEnum.emptyMessage);
|
||||
|
||||
return commitMessage;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -144,13 +104,15 @@ function getMessagesPromisesByChangesInFile(
|
||||
}
|
||||
}
|
||||
|
||||
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map((lineDiff) => {
|
||||
const messages = generateCommitMessageChatCompletionPrompt(
|
||||
separator + lineDiff
|
||||
);
|
||||
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map(
|
||||
async (lineDiff) => {
|
||||
const messages = await generateCommitMessageChatCompletionPrompt(
|
||||
separator + lineDiff
|
||||
);
|
||||
|
||||
return api.generateCommitMessage(messages);
|
||||
});
|
||||
return api.generateCommitMessage(messages);
|
||||
}
|
||||
);
|
||||
|
||||
return commitMsgsFromFileLineDiffs;
|
||||
}
|
||||
@@ -187,10 +149,10 @@ function splitDiff(diff: string, maxChangeLength: number) {
|
||||
return splitDiffs;
|
||||
}
|
||||
|
||||
export function getCommitMsgsPromisesFromFileDiffs(
|
||||
export const getCommitMsgsPromisesFromFileDiffs = async (
|
||||
diff: string,
|
||||
maxDiffLength: number
|
||||
) {
|
||||
) => {
|
||||
const separator = 'diff --git ';
|
||||
|
||||
const diffByFiles = diff.split(separator).slice(1);
|
||||
@@ -211,7 +173,7 @@ export function getCommitMsgsPromisesFromFileDiffs(
|
||||
|
||||
commitMessagePromises.push(...messagesPromises);
|
||||
} else {
|
||||
const messages = generateCommitMessageChatCompletionPrompt(
|
||||
const messages = await generateCommitMessageChatCompletionPrompt(
|
||||
separator + fileDiff
|
||||
);
|
||||
|
||||
@@ -220,7 +182,7 @@ export function getCommitMsgsPromisesFromFileDiffs(
|
||||
}
|
||||
|
||||
return commitMessagePromises;
|
||||
}
|
||||
};
|
||||
|
||||
function delay(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { unlinkSync, writeFileSync } from 'fs';
|
||||
|
||||
import core from '@actions/core';
|
||||
import github from '@actions/github';
|
||||
import exec from '@actions/exec';
|
||||
import github from '@actions/github';
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
import { PushEvent } from '@octokit/webhooks-types';
|
||||
|
||||
import { generateCommitMessageByDiff } from './generateCommitMessageFromGitDiff';
|
||||
import { sleep } from './utils/sleep';
|
||||
import { randomIntFromInterval } from './utils/randomIntFromInterval';
|
||||
import { unlinkSync, writeFileSync } from 'fs';
|
||||
import { sleep } from './utils/sleep';
|
||||
|
||||
// This should be a token with access to your repository scoped in as a secret.
|
||||
// The YML workflow will need to set GITHUB_TOKEN with the GitHub Secret Token
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import en from '../i18n/en.json' assert { type: 'json' };
|
||||
import cs from '../i18n/cs.json' assert { type: 'json' };
|
||||
import de from '../i18n/de.json' assert { type: 'json' };
|
||||
import fr from '../i18n/fr.json' assert { type: 'json' };
|
||||
import it from '../i18n/it.json' assert { type: 'json' };
|
||||
import ko from '../i18n/ko.json' assert { type: 'json' };
|
||||
import zh_CN from '../i18n/zh_CN.json' assert { type: 'json' };
|
||||
import zh_TW from '../i18n/zh_TW.json' assert { type: 'json' };
|
||||
import ja from '../i18n/ja.json' assert { type: 'json' };
|
||||
import pt_br from '../i18n/pt_br.json' assert { type: 'json' };
|
||||
import vi_VN from '../i18n/vi_VN.json' assert { type: 'json' };
|
||||
import es_ES from '../i18n/es_ES.json' assert { type: 'json' };
|
||||
import sv from '../i18n/sv.json' assert { type: 'json' };
|
||||
import nl from '../i18n/nl.json' assert { type: 'json' };
|
||||
import ru from '../i18n/ru.json' assert { type: 'json' };
|
||||
import id_ID from '../i18n/id_ID.json' assert { type: 'json' };
|
||||
import pl from '../i18n/pl.json' assert { type: 'json' };
|
||||
import tr from '../i18n/tr.json' assert { type: 'json' };
|
||||
import th from '../i18n/th.json' assert { type: 'json' };
|
||||
import cs from '../i18n/cs.json';
|
||||
import de from '../i18n/de.json';
|
||||
import en from '../i18n/en.json';
|
||||
import es_ES from '../i18n/es_ES.json';
|
||||
import fr from '../i18n/fr.json';
|
||||
import id_ID from '../i18n/id_ID.json';
|
||||
import it from '../i18n/it.json';
|
||||
import ja from '../i18n/ja.json';
|
||||
import ko from '../i18n/ko.json';
|
||||
import nl from '../i18n/nl.json';
|
||||
import pl from '../i18n/pl.json';
|
||||
import pt_br from '../i18n/pt_br.json';
|
||||
import ru from '../i18n/ru.json';
|
||||
import sv from '../i18n/sv.json';
|
||||
import th from '../i18n/th.json';
|
||||
import tr from '../i18n/tr.json';
|
||||
import vi_VN from '../i18n/vi_VN.json';
|
||||
import zh_CN from '../i18n/zh_CN.json';
|
||||
import zh_TW from '../i18n/zh_TW.json';
|
||||
|
||||
export enum I18nLocals {
|
||||
'en' = 'en',
|
||||
|
||||
79
src/modules/commitlint/config.ts
Normal file
79
src/modules/commitlint/config.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { spinner } from '@clack/prompts';
|
||||
|
||||
import { api } from '../../api';
|
||||
import { getConfig } from '../../commands/config';
|
||||
import { i18n, I18nLocals } from '../../i18n';
|
||||
import { COMMITLINT_LLM_CONFIG_PATH } from './constants';
|
||||
import { computeHash } from './crypto';
|
||||
import { commitlintPrompts, inferPromptsFromCommitlintConfig } from './prompts';
|
||||
import { getCommitLintPWDConfig } from './pwd-commitlint';
|
||||
import { CommitlintLLMConfig } from './types';
|
||||
import * as utils from './utils';
|
||||
|
||||
const config = getConfig();
|
||||
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
|
||||
|
||||
export const configureCommitlintIntegration = async (force = false) => {
|
||||
const spin = spinner();
|
||||
spin.start('Loading @commitlint configuration');
|
||||
|
||||
const fileExists = await utils.commitlintLLMConfigExists();
|
||||
|
||||
let commitLintConfig = await getCommitLintPWDConfig();
|
||||
|
||||
// debug complete @commitlint configuration
|
||||
// await fs.writeFile(
|
||||
// `${OPENCOMMIT_COMMITLINT_CONFIG}-commitlint-debug`,
|
||||
// JSON.stringify(commitLintConfig, null, 2)
|
||||
// );
|
||||
|
||||
const hash = await computeHash(JSON.stringify(commitLintConfig));
|
||||
|
||||
spin.stop(`Read @commitlint configuration (hash: ${hash})`);
|
||||
|
||||
if (fileExists) {
|
||||
// Check if we need to update the prompts.
|
||||
const { hash: existingHash } = await utils.getCommitlintLLMConfig();
|
||||
if (hash === existingHash && !force) {
|
||||
spin.stop(
|
||||
'Hashes are the same, no need to update the config. Run "force" command to bypass.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
spin.start('Generating consistency with given @commitlint rules');
|
||||
|
||||
const prompts = inferPromptsFromCommitlintConfig(commitLintConfig);
|
||||
|
||||
const consistencyPrompts =
|
||||
commitlintPrompts.GEN_COMMITLINT_CONSISTENCY_PROMPT(prompts);
|
||||
|
||||
// debug prompt which will generate a consistency
|
||||
// await fs.writeFile(
|
||||
// `${COMMITLINT_LLM_CONFIG}-debug`,
|
||||
// consistencyPrompts.map((p) => p.content)
|
||||
// );
|
||||
|
||||
let consistency =
|
||||
(await api.generateCommitMessage(consistencyPrompts)) || '{}';
|
||||
|
||||
// Cleanup the consistency answer. Sometimes 'gpt-3.5-turbo' sends rule's back.
|
||||
prompts.forEach((prompt) => (consistency = consistency.replace(prompt, '')));
|
||||
// ... remaining might be extra set of "\n"
|
||||
consistency = utils.removeDoubleNewlines(consistency);
|
||||
|
||||
const commitlintLLMConfig: CommitlintLLMConfig = {
|
||||
hash,
|
||||
prompts,
|
||||
consistency: {
|
||||
[translation.localLanguage]: {
|
||||
...JSON.parse(consistency as string)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await utils.writeCommitlintLLMConfig(commitlintLLMConfig);
|
||||
|
||||
spin.stop(`Done - please review contents of ${COMMITLINT_LLM_CONFIG_PATH}`);
|
||||
};
|
||||
1
src/modules/commitlint/constants.ts
Normal file
1
src/modules/commitlint/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const COMMITLINT_LLM_CONFIG_PATH = `${process.env.PWD}/.opencommit-commitlint`;
|
||||
15
src/modules/commitlint/crypto.ts
Normal file
15
src/modules/commitlint/crypto.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
export const computeHash = async (
|
||||
content: string,
|
||||
algorithm: string = 'sha256'
|
||||
): Promise<string> => {
|
||||
try {
|
||||
const hash = crypto.createHash(algorithm);
|
||||
hash.update(content);
|
||||
return hash.digest('hex');
|
||||
} catch (error) {
|
||||
console.error('Error while computing hash:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
283
src/modules/commitlint/prompts.ts
Normal file
283
src/modules/commitlint/prompts.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
ChatCompletionRequestMessage,
|
||||
ChatCompletionRequestMessageRoleEnum
|
||||
} from 'openai';
|
||||
|
||||
import { outro } from '@clack/prompts';
|
||||
import {
|
||||
PromptConfig,
|
||||
QualifiedConfig,
|
||||
RuleConfigSeverity,
|
||||
RuleConfigTuple
|
||||
} from '@commitlint/types';
|
||||
|
||||
import { getConfig } from '../../commands/config';
|
||||
import { i18n, I18nLocals } from '../../i18n';
|
||||
import { IDENTITY, INIT_DIFF_PROMPT } from '../../prompts';
|
||||
|
||||
const config = getConfig();
|
||||
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
|
||||
|
||||
type DeepPartial<T> = {
|
||||
[P in keyof T]?: {
|
||||
[K in keyof T[P]]?: T[P][K];
|
||||
};
|
||||
};
|
||||
|
||||
type PromptFunction = (
|
||||
applicable: string,
|
||||
value: any,
|
||||
prompt: DeepPartial<PromptConfig>
|
||||
) => string;
|
||||
|
||||
type PromptResolverFunction = (
|
||||
key: string,
|
||||
applicable: string,
|
||||
value: any,
|
||||
prompt?: DeepPartial<PromptConfig>
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* Extracts more contexte for each type-enum.
|
||||
* IDEA: replicate the concept for scopes and refactor to a generic feature.
|
||||
*/
|
||||
const getTypeRuleExtraDescription = (
|
||||
type: string,
|
||||
prompt?: DeepPartial<PromptConfig>
|
||||
) => prompt?.questions?.type?.enum?.[type]?.description;
|
||||
|
||||
/*
|
||||
IDEA: Compress llm readable prompt for each section of commit message: one line for header, one line for scope, etc.
|
||||
- The type must be in lowercase and should be one of the following values: featuring, fixing, documenting, styling, refactoring, testing, chores, perf, build, ci, revert.
|
||||
- The scope should not be empty and provide context for the change (e.g., module or file changed).
|
||||
- The subject should not be empty, should not end with a period, and should provide a concise description of the change. It should not be in sentence-case, start-case, pascal-case, or upper-case.
|
||||
*/
|
||||
const llmReadableRules: {
|
||||
[ruleName: string]: PromptResolverFunction;
|
||||
} = {
|
||||
blankline: (key, applicable) =>
|
||||
`There should ${applicable} be a blank line at the beginning of the ${key}.`,
|
||||
caseRule: (key, applicable, value: string | Array<string>) =>
|
||||
`The ${key} should ${applicable} be in ${
|
||||
Array.isArray(value)
|
||||
? `one of the following case:
|
||||
- ${value.join('\n - ')}.`
|
||||
: `${value} case.`
|
||||
}`,
|
||||
emptyRule: (key, applicable) => `The ${key} should ${applicable} be empty.`,
|
||||
enumRule: (key, applicable, value: string | Array<string>) =>
|
||||
`The ${key} should ${applicable} be one of the following values:
|
||||
- ${Array.isArray(value) ? value.join('\n - ') : value}.`,
|
||||
enumTypeRule: (key, applicable, value: string | Array<string>, prompt) =>
|
||||
`The ${key} should ${applicable} be one of the following values:
|
||||
- ${
|
||||
Array.isArray(value)
|
||||
? value
|
||||
.map((v) => {
|
||||
const description = getTypeRuleExtraDescription(v, prompt);
|
||||
if (description) {
|
||||
return `${v} (${description})`;
|
||||
} else return v;
|
||||
})
|
||||
.join('\n - ')
|
||||
: value
|
||||
}.`,
|
||||
fullStopRule: (key, applicable, value: string) =>
|
||||
`The ${key} should ${applicable} end with '${value}'.`,
|
||||
maxLengthRule: (key, applicable, value: string) =>
|
||||
`The ${key} should ${applicable} have ${value} characters or less.`,
|
||||
minLengthRule: (key, applicable, value: string) =>
|
||||
`The ${key} should ${applicable} have ${value} characters or more.`
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: Validate rules to every rule in the @commitlint configuration.
|
||||
* IDEA: Plugins can extend the list of rule. Provide user with a way to infer or extend when "No prompt handler for rule".
|
||||
*/
|
||||
const rulesPrompts: {
|
||||
[ruleName: string]: PromptFunction;
|
||||
} = {
|
||||
'body-case': (applicable: string, value: string | Array<string>) =>
|
||||
llmReadableRules.caseRule('body', applicable, value),
|
||||
'body-empty': (applicable: string) =>
|
||||
llmReadableRules.emptyRule('body', applicable, undefined),
|
||||
'body-full-stop': (applicable: string, value: string) =>
|
||||
llmReadableRules.fullStopRule('body', applicable, value),
|
||||
'body-leading-blank': (applicable: string) =>
|
||||
llmReadableRules.blankline('body', applicable, undefined),
|
||||
'body-max-length': (applicable: string, value: string) =>
|
||||
llmReadableRules.maxLengthRule('body', applicable, value),
|
||||
'body-max-line-length': (applicable: string, value: string) =>
|
||||
`Each line of the body should ${applicable} have ${value} characters or less.`,
|
||||
'body-min-length': (applicable: string, value: string) =>
|
||||
llmReadableRules.minLengthRule('body', applicable, value),
|
||||
'footer-case': (applicable: string, value: string | Array<string>) =>
|
||||
llmReadableRules.caseRule('footer', applicable, value),
|
||||
'footer-empty': (applicable: string) =>
|
||||
llmReadableRules.emptyRule('footer', applicable, undefined),
|
||||
'footer-leading-blank': (applicable: string) =>
|
||||
llmReadableRules.blankline('footer', applicable, undefined),
|
||||
'footer-max-length': (applicable: string, value: string) =>
|
||||
llmReadableRules.maxLengthRule('footer', applicable, value),
|
||||
'footer-max-line-length': (applicable: string, value: string) =>
|
||||
`Each line of the footer should ${applicable} have ${value} characters or less.`,
|
||||
'footer-min-length': (applicable: string, value: string) =>
|
||||
llmReadableRules.minLengthRule('footer', applicable, value),
|
||||
'header-case': (applicable: string, value: string | Array<string>) =>
|
||||
llmReadableRules.caseRule('header', applicable, value),
|
||||
'header-full-stop': (applicable: string, value: string) =>
|
||||
llmReadableRules.fullStopRule('header', applicable, value),
|
||||
'header-max-length': (applicable: string, value: string) =>
|
||||
llmReadableRules.maxLengthRule('header', applicable, value),
|
||||
'header-min-length': (applicable: string, value: string) =>
|
||||
llmReadableRules.minLengthRule('header', applicable, value),
|
||||
'references-empty': (applicable: string) =>
|
||||
llmReadableRules.emptyRule('references section', applicable, undefined),
|
||||
'scope-case': (applicable: string, value: string | Array<string>) =>
|
||||
llmReadableRules.caseRule('scope', applicable, value),
|
||||
'scope-empty': (applicable: string) =>
|
||||
llmReadableRules.emptyRule('scope', applicable, undefined),
|
||||
'scope-enum': (applicable: string, value: string | Array<string>) =>
|
||||
llmReadableRules.enumRule('type', applicable, value),
|
||||
'scope-max-length': (applicable: string, value: string) =>
|
||||
llmReadableRules.maxLengthRule('scope', applicable, value),
|
||||
'scope-min-length': (applicable: string, value: string) =>
|
||||
llmReadableRules.minLengthRule('scope', applicable, value),
|
||||
'signed-off-by': (applicable: string, value: string) =>
|
||||
`The commit message should ${applicable} have a "Signed-off-by" line with the value "${value}".`,
|
||||
'subject-case': (applicable: string, value: string | Array<string>) =>
|
||||
llmReadableRules.caseRule('subject', applicable, value),
|
||||
'subject-empty': (applicable: string) =>
|
||||
llmReadableRules.emptyRule('subject', applicable, undefined),
|
||||
'subject-full-stop': (applicable: string, value: string) =>
|
||||
llmReadableRules.fullStopRule('subject', applicable, value),
|
||||
'subject-max-length': (applicable: string, value: string) =>
|
||||
llmReadableRules.maxLengthRule('subject', applicable, value),
|
||||
'subject-min-length': (applicable: string, value: string) =>
|
||||
llmReadableRules.minLengthRule('subject', applicable, value),
|
||||
'type-case': (applicable: string, value: string | Array<string>) =>
|
||||
llmReadableRules.caseRule('type', applicable, value),
|
||||
'type-empty': (applicable: string) =>
|
||||
llmReadableRules.emptyRule('type', applicable, undefined),
|
||||
'type-enum': (applicable: string, value: string | Array<string>, prompt) =>
|
||||
llmReadableRules.enumTypeRule('type', applicable, value, prompt),
|
||||
'type-max-length': (applicable: string, value: string) =>
|
||||
llmReadableRules.maxLengthRule('type', applicable, value),
|
||||
'type-min-length': (applicable: string, value: string) =>
|
||||
llmReadableRules.minLengthRule('type', applicable, value)
|
||||
};
|
||||
|
||||
const getPrompt = (
|
||||
ruleName: string,
|
||||
ruleConfig: RuleConfigTuple<unknown>,
|
||||
prompt: DeepPartial<PromptConfig>
|
||||
) => {
|
||||
const [severity, applicable, value] = ruleConfig;
|
||||
|
||||
// Should we exclude "Disabled" properties?
|
||||
// Is this used to disable a subjacent rule when extending presets?
|
||||
if (severity === RuleConfigSeverity.Disabled) return null;
|
||||
|
||||
const promptFn = rulesPrompts[ruleName];
|
||||
if (promptFn) {
|
||||
return promptFn(applicable, value, prompt);
|
||||
}
|
||||
|
||||
// Plugins may add their custom rules.
|
||||
// We might want to call OpenAI to build this rule's llm-readable prompt.
|
||||
outro(`${chalk.red('✖')} No prompt handler for rule "${ruleName}".`);
|
||||
return `Please manualy set the prompt for rule "${ruleName}".`;
|
||||
};
|
||||
|
||||
export const inferPromptsFromCommitlintConfig = (
|
||||
config: QualifiedConfig
|
||||
): string[] => {
|
||||
const { rules, prompt } = config;
|
||||
if (!rules) return [];
|
||||
return Object.keys(rules)
|
||||
.map((ruleName) =>
|
||||
getPrompt(ruleName, rules[ruleName] as RuleConfigTuple<unknown>, prompt)
|
||||
)
|
||||
.filter((prompt) => prompt !== null) as string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Breaking down commit message structure for conventional commit, and mapping bits with
|
||||
* ubiquitous language from @commitlint.
|
||||
* While gpt-4 does this on it self, gpt-3.5 can't map this on his own atm.
|
||||
*/
|
||||
const STRUCTURE_OF_COMMIT = `
|
||||
- Header of commit is composed of type, scope, subject: <type-of-commit>(<scope-of-commit>): <subject-of-commit>
|
||||
- Description of commit is composed of body and footer (optional): <body-of-commit>\n<footer(s)-of-commit>`;
|
||||
|
||||
// Prompt to generate LLM-readable rules based on @commitlint rules.
|
||||
const GEN_COMMITLINT_CONSISTENCY_PROMPT = (
|
||||
prompts: string[]
|
||||
): ChatCompletionRequestMessage[] => [
|
||||
{
|
||||
role: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
// prettier-ignore
|
||||
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages for two different changes in a single codebase and output them in the provided JSON format: one for a bug fix and another for a new feature.
|
||||
|
||||
Here are the specific requirements and conventions that should be strictly followed:
|
||||
|
||||
Commit Message Conventions:
|
||||
- The commit message consists of three parts: Header, Body, and Footer.
|
||||
- Header:
|
||||
- Format: \`<type>(<scope>): <subject>\`
|
||||
- ${prompts.join('\n- ')}
|
||||
|
||||
JSON Output Format:
|
||||
- The JSON output should contain the commit messages for a bug fix and a new feature in the following format:
|
||||
\`\`\`json
|
||||
{
|
||||
"localLanguage": "${translation.localLanguage}",
|
||||
"commitFix": "<Header of commit for bug fix>",
|
||||
"commitFeat": "<Header of commit for feature>",
|
||||
"commitDescription": "<Description of commit for both the bug fix and the feature>"
|
||||
}
|
||||
\`\`\`
|
||||
- The "commitDescription" should not include the commit message’s header, only the description.
|
||||
- Description should not be more than 74 characters.
|
||||
|
||||
Additional Details:
|
||||
- Changing the variable 'port' to uppercase 'PORT' is considered a bug fix.
|
||||
- Allowing the server to listen on a port specified through the environment variable is considered a new feature.
|
||||
|
||||
Example Git Diff is to follow:`
|
||||
},
|
||||
INIT_DIFF_PROMPT
|
||||
];
|
||||
|
||||
/**
|
||||
* Prompt to have LLM generate a message using @commitlint rules.
|
||||
*
|
||||
* @param language
|
||||
* @param prompts
|
||||
* @returns
|
||||
*/
|
||||
const INIT_MAIN_PROMPT = (
|
||||
language: string,
|
||||
prompts: string[]
|
||||
): ChatCompletionRequestMessage => ({
|
||||
role: ChatCompletionRequestMessageRoleEnum.System,
|
||||
// prettier-ignore
|
||||
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes and WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
|
||||
${config?.OCO_EMOJI ? 'Use GitMoji convention to preface the commit.' : 'Do not preface the commit with anything.'}
|
||||
${config?.OCO_DESCRIPTION ? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.' : "Don't add any descriptions to the commit, only commit message."}
|
||||
Use the present tense. Use ${language} to answer.
|
||||
|
||||
You will strictly follow the following conventions to generate the content of the commit message:
|
||||
- ${prompts.join('\n- ')}
|
||||
|
||||
The conventions refers to the following structure of commit message:
|
||||
${STRUCTURE_OF_COMMIT}
|
||||
|
||||
`
|
||||
});
|
||||
|
||||
export const commitlintPrompts = {
|
||||
INIT_MAIN_PROMPT,
|
||||
GEN_COMMITLINT_CONSISTENCY_PROMPT
|
||||
};
|
||||
25
src/modules/commitlint/pwd-commitlint.ts
Normal file
25
src/modules/commitlint/pwd-commitlint.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import path from 'path';
|
||||
|
||||
const nodeModulesPath = path.join(
|
||||
process.env.PWD as string,
|
||||
'node_modules',
|
||||
'@commitlint',
|
||||
'load'
|
||||
);
|
||||
|
||||
/**
|
||||
* This code is loading the configuration for the `@commitlint` package from the current working
|
||||
* directory (`process.env.PWD`) by requiring the `load` module from the `@commitlint` package.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const getCommitLintPWDConfig = async () => {
|
||||
const load = require(nodeModulesPath).default;
|
||||
|
||||
if (load && typeof load === 'function') {
|
||||
return await load();
|
||||
}
|
||||
|
||||
// @commitlint/load is not a function
|
||||
return null;
|
||||
};
|
||||
11
src/modules/commitlint/types.ts
Normal file
11
src/modules/commitlint/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { i18n } from '../../i18n';
|
||||
|
||||
export type ConsistencyPrompt = (typeof i18n)[keyof typeof i18n];
|
||||
|
||||
export type CommitlintLLMConfig = {
|
||||
hash: string;
|
||||
prompts: string[];
|
||||
consistency: {
|
||||
[key: string]: ConsistencyPrompt;
|
||||
};
|
||||
};
|
||||
47
src/modules/commitlint/utils.ts
Normal file
47
src/modules/commitlint/utils.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import fs from 'fs/promises';
|
||||
|
||||
import { COMMITLINT_LLM_CONFIG_PATH } from './constants';
|
||||
import { CommitlintLLMConfig } from './types';
|
||||
|
||||
/**
|
||||
* Removes the "\n" only if occurring twice
|
||||
*/
|
||||
export const removeDoubleNewlines = (input: string): string => {
|
||||
const pattern = /\\n\\n/g;
|
||||
if (pattern.test(input)) {
|
||||
const newInput = input.replace(pattern, '');
|
||||
return removeDoubleNewlines(newInput);
|
||||
}
|
||||
|
||||
return input;
|
||||
};
|
||||
|
||||
export const commitlintLLMConfigExists = async (): Promise<boolean> => {
|
||||
let exists;
|
||||
try {
|
||||
await fs.access(COMMITLINT_LLM_CONFIG_PATH);
|
||||
exists = true;
|
||||
} catch (e) {
|
||||
exists = false;
|
||||
}
|
||||
|
||||
return exists;
|
||||
};
|
||||
|
||||
export const writeCommitlintLLMConfig = async (
|
||||
commitlintLLMConfig: CommitlintLLMConfig
|
||||
): Promise<void> => {
|
||||
await fs.writeFile(
|
||||
COMMITLINT_LLM_CONFIG_PATH,
|
||||
JSON.stringify(commitlintLLMConfig, null, 2)
|
||||
);
|
||||
};
|
||||
|
||||
export const getCommitlintLLMConfig =
|
||||
async (): Promise<CommitlintLLMConfig> => {
|
||||
const content = await fs.readFile(COMMITLINT_LLM_CONFIG_PATH);
|
||||
const commitLintLLMConfig = JSON.parse(
|
||||
content.toString()
|
||||
) as CommitlintLLMConfig;
|
||||
return commitLintLLMConfig;
|
||||
};
|
||||
103
src/prompts.ts
Normal file
103
src/prompts.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
ChatCompletionRequestMessage,
|
||||
ChatCompletionRequestMessageRoleEnum
|
||||
} from 'openai';
|
||||
|
||||
import { note } from '@clack/prompts';
|
||||
|
||||
import { getConfig } from './commands/config';
|
||||
import { i18n, I18nLocals } from './i18n';
|
||||
import { configureCommitlintIntegration } from './modules/commitlint/config';
|
||||
import { commitlintPrompts } from './modules/commitlint/prompts';
|
||||
import { ConsistencyPrompt } from './modules/commitlint/types';
|
||||
import * as utils from './modules/commitlint/utils';
|
||||
|
||||
const config = getConfig();
|
||||
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
|
||||
|
||||
export const IDENTITY =
|
||||
'You are to act as the author of a commit message in git.';
|
||||
|
||||
const INIT_MAIN_PROMPT = (language: string): ChatCompletionRequestMessage => ({
|
||||
role: ChatCompletionRequestMessageRoleEnum.System,
|
||||
// prettier-ignore
|
||||
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the conventional commit convention and explain WHAT were the changes and WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
|
||||
${config?.OCO_EMOJI ? 'Use GitMoji convention to preface the commit.' : 'Do not preface the commit with anything.'}
|
||||
${config?.OCO_DESCRIPTION ? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.' : "Don't add any descriptions to the commit, only commit message."}
|
||||
Use the present tense. Lines must not be longer than 74 characters. Use ${language} to answer.`
|
||||
});
|
||||
|
||||
export const INIT_DIFF_PROMPT: ChatCompletionRequestMessage = {
|
||||
role: ChatCompletionRequestMessageRoleEnum.User,
|
||||
content: `diff --git a/src/server.ts b/src/server.ts
|
||||
index ad4db42..f3b18a9 100644
|
||||
--- a/src/server.ts
|
||||
+++ b/src/server.ts
|
||||
@@ -10,7 +10,7 @@
|
||||
import {
|
||||
initWinstonLogger();
|
||||
|
||||
const app = express();
|
||||
-const port = 7799;
|
||||
+const PORT = 7799;
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
@@ -34,6 +34,6 @@
|
||||
app.use((_, res, next) => {
|
||||
// ROUTES
|
||||
app.use(PROTECTED_ROUTER_URL, protectedRouter);
|
||||
|
||||
-app.listen(port, () => {
|
||||
- console.log(\`Server listening on port \${port}\`);
|
||||
+app.listen(process.env.PORT || PORT, () => {
|
||||
+ console.log(\`Server listening on port \${PORT}\`);
|
||||
});`
|
||||
};
|
||||
|
||||
const INIT_CONSISTENCY_PROMPT = (
|
||||
translation: ConsistencyPrompt
|
||||
): ChatCompletionRequestMessage => ({
|
||||
role: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
content: `${config?.OCO_EMOJI ? '🐛 ' : ''}${translation.commitFix}
|
||||
${config?.OCO_EMOJI ? '✨ ' : ''}${translation.commitFeat}
|
||||
${config?.OCO_DESCRIPTION ? translation.commitDescription : ''}`
|
||||
});
|
||||
|
||||
export const getMainCommitPrompt = async (): Promise<
|
||||
ChatCompletionRequestMessage[]
|
||||
> => {
|
||||
switch (config?.OCO_PROMPT_MODULE) {
|
||||
case '@commitlint':
|
||||
if (!(await utils.commitlintLLMConfigExists())) {
|
||||
note(
|
||||
`OCO_PROMPT_MODULE is @commitlint but you haven't generated consistency for this project yet.`
|
||||
);
|
||||
await configureCommitlintIntegration();
|
||||
}
|
||||
|
||||
// Replace example prompt with a prompt that's generated by OpenAI for the commitlint config.
|
||||
const commitLintConfig = await utils.getCommitlintLLMConfig();
|
||||
|
||||
return [
|
||||
commitlintPrompts.INIT_MAIN_PROMPT(
|
||||
translation.localLanguage,
|
||||
commitLintConfig.prompts
|
||||
),
|
||||
INIT_DIFF_PROMPT,
|
||||
INIT_CONSISTENCY_PROMPT(
|
||||
commitLintConfig.consistency[
|
||||
translation.localLanguage
|
||||
] as ConsistencyPrompt
|
||||
)
|
||||
];
|
||||
|
||||
default:
|
||||
// conventional-commit
|
||||
return [
|
||||
INIT_MAIN_PROMPT(translation.localLanguage),
|
||||
INIT_DIFF_PROMPT,
|
||||
INIT_CONSISTENCY_PROMPT(translation)
|
||||
];
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,10 @@
|
||||
import { getOpenCommitLatestVersion } from '../api';
|
||||
import currentPackage from '../../package.json' assert { type: 'json' };
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { outro } from '@clack/prompts';
|
||||
|
||||
import currentPackage from '../../package.json';
|
||||
import { getOpenCommitLatestVersion } from '../api';
|
||||
|
||||
export const checkIsLatestVersion = async () => {
|
||||
const latestVersion = await getOpenCommitLatestVersion();
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { execa } from 'execa';
|
||||
import { outro, spinner } from '@clack/prompts';
|
||||
import { readFileSync } from 'fs';
|
||||
import ignore, { Ignore } from 'ignore';
|
||||
|
||||
import { outro, spinner } from '@clack/prompts';
|
||||
|
||||
export const assertGitRepo = async () => {
|
||||
try {
|
||||
await execa('git', ['rev-parse']);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { tokenCount } from './tokenCount';
|
||||
|
||||
export function mergeDiffs(arr: string[], maxStringLength: number): string[] {
|
||||
const mergedArr: string[] = [];
|
||||
let currentItem: string = arr[0];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cl100k_base from '@dqbd/tiktoken/encoders/cl100k_base.json';
|
||||
import { Tiktoken } from '@dqbd/tiktoken/lite';
|
||||
import cl100k_base from '@dqbd/tiktoken/encoders/cl100k_base.json' assert { type: 'json' };
|
||||
|
||||
export function tokenCount(content: string): number {
|
||||
const encoding = new Tiktoken(
|
||||
|
||||
Reference in New Issue
Block a user