feat(commands): add COMMANDS enum to standardize command names across the application

refactor(commit.ts): restructure generateCommitMessageFromGitDiff function to use an interface for parameters and improve readability
fix(config.ts): update DEFAULT_TOKEN_LIMITS to correct values for max tokens input and output
chore(config.ts): enhance config validation to handle undefined and null values more effectively
style(commit.ts): improve formatting and consistency in the commit confirmation logic
style(config.ts): clean up error messages and improve clarity in config setting process
This commit is contained in:
di-sukharev
2024-08-20 12:01:14 +03:00
parent 5cfa3cded2
commit 4afd7de7a8
6 changed files with 111 additions and 74 deletions

5
src/commands/ENUMS.ts Normal file
View File

@@ -0,0 +1,5 @@
export enum COMMANDS {
config = 'config',
hook = 'hook',
commitlint = 'commitlint'
}

View File

@@ -1,6 +1,3 @@
import chalk from 'chalk';
import { execa } from 'execa';
import {
confirm,
intro,
@@ -10,7 +7,8 @@ import {
select,
spinner
} from '@clack/prompts';
import chalk from 'chalk';
import { execa } from 'execa';
import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
import {
assertGitRepo,
@@ -32,18 +30,25 @@ const getGitRemotes = async () => {
// Check for the presence of message templates
const checkMessageTemplate = (extraArgs: string[]): string | false => {
for (const key in extraArgs) {
if (extraArgs[key].includes(config?.OCO_MESSAGE_TEMPLATE_PLACEHOLDER))
if (extraArgs[key].includes(config.OCO_MESSAGE_TEMPLATE_PLACEHOLDER))
return extraArgs[key];
}
return false;
};
const generateCommitMessageFromGitDiff = async (
diff: string,
extraArgs: string[],
fullGitMojiSpec: boolean,
skipCommitConfirmation: boolean
): Promise<void> => {
interface GenerateCommitMessageFromGitDiffParams {
diff: string;
extraArgs: string[];
fullGitMojiSpec?: boolean;
skipCommitConfirmation?: boolean;
}
const generateCommitMessageFromGitDiff = async ({
diff,
extraArgs,
fullGitMojiSpec = false,
skipCommitConfirmation = false
}: GenerateCommitMessageFromGitDiffParams): Promise<void> => {
await assertGitRepo();
const commitSpinner = spinner();
commitSpinner.start('Generating the commit message');
@@ -56,14 +61,14 @@ const generateCommitMessageFromGitDiff = async (
const messageTemplate = checkMessageTemplate(extraArgs);
if (
config?.OCO_MESSAGE_TEMPLATE_PLACEHOLDER &&
config.OCO_MESSAGE_TEMPLATE_PLACEHOLDER &&
typeof messageTemplate === 'string'
) {
const messageTemplateIndex = extraArgs.indexOf(messageTemplate);
extraArgs.splice(messageTemplateIndex, 1);
commitMessage = messageTemplate.replace(
config?.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
config.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
commitMessage
);
}
@@ -77,9 +82,11 @@ ${commitMessage}
${chalk.grey('——————————————————')}`
);
const isCommitConfirmedByUser = skipCommitConfirmation || await confirm({
message: 'Confirm the commit message?'
});
const isCommitConfirmedByUser =
skipCommitConfirmation ||
(await confirm({
message: 'Confirm the commit message?'
}));
if (isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
const { stdout } = await execa('git', [
@@ -96,8 +103,7 @@ ${chalk.grey('——————————————————')}`
const remotes = await getGitRemotes();
// user isn't pushing, return early
if (config?.OCO_GITPUSH === false)
return
if (config.OCO_GITPUSH === false) return;
if (!remotes.length) {
const { stdout } = await execa('git', ['push']);
@@ -105,7 +111,7 @@ ${chalk.grey('——————————————————')}`
process.exit(0);
}
if (remotes.length === 1 && config?.OCO_GITPUSH !== true) {
if (remotes.length === 1 && config.OCO_GITPUSH !== true) {
const isPushConfirmedByUser = await confirm({
message: 'Do you want to run `git push`?'
});
@@ -157,14 +163,14 @@ ${chalk.grey('——————————————————')}`
}
if (!isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
const regenerateMessage = await confirm({
message: 'Do you want to regenerate the message ?'
message: 'Do you want to regenerate the message?'
});
if (regenerateMessage && !isCancel(isCommitConfirmedByUser)) {
await generateCommitMessageFromGitDiff(
await generateCommitMessageFromGitDiff({
diff,
extraArgs,
fullGitMojiSpec
)
});
}
}
} catch (error) {
@@ -249,12 +255,12 @@ export async function commit(
);
const [, generateCommitError] = await trytm(
generateCommitMessageFromGitDiff(
await getDiff({ files: stagedFiles }),
generateCommitMessageFromGitDiff({
diff: await getDiff({ files: stagedFiles }),
extraArgs,
fullGitMojiSpec,
skipCommitConfirmation
)
})
);
if (generateCommitError) {

View File

@@ -1,11 +1,9 @@
import { intro, outro } from '@clack/prompts';
import chalk from 'chalk';
import { command } from 'cleye';
import { intro, outro } from '@clack/prompts';
import { COMMANDS } from '../ENUMS';
import { configureCommitlintIntegration } from '../modules/commitlint/config';
import { getCommitlintLLMConfig } from '../modules/commitlint/utils';
import { COMMANDS } from './ENUMS';
export enum CONFIG_MODES {
get = 'get',

View File

@@ -6,7 +6,7 @@ import { existsSync, readFileSync, writeFileSync } from 'fs';
import { parse as iniParse, stringify as iniStringify } from 'ini';
import { homedir } from 'os';
import { join as pathJoin, resolve as pathResolve } from 'path';
import { COMMANDS } from '../ENUMS';
import { COMMANDS } from './ENUMS';
import { TEST_MOCK_TYPES } from '../engine/testAi';
import { getI18nLocal, i18n } from '../i18n';
@@ -101,8 +101,8 @@ const getDefaultModel = (provider: string | undefined): string => {
};
export enum DEFAULT_TOKEN_LIMITS {
DEFAULT_MAX_TOKENS_INPUT = 4096,
DEFAULT_MAX_TOKENS_OUTPUT = 500
DEFAULT_MAX_TOKENS_INPUT = 40960,
DEFAULT_MAX_TOKENS_OUTPUT = 4096
}
const validateConfig = (
@@ -111,10 +111,10 @@ const validateConfig = (
validationMessage: string
) => {
if (!condition) {
outro(`${chalk.red('✖')} wrong value for ${key}: ${validationMessage}.`);
outro(
`${chalk.red(
'✖'
)} Unsupported config key ${key}: ${validationMessage}. For more help refer to docs https://github.com/di-sukharev/opencommit`
'For more help refer to docs https://github.com/di-sukharev/opencommit'
);
process.exit(1);
@@ -127,7 +127,13 @@ export const configValidators = {
validateConfig(
'OCO_OPENAI_API_KEY',
!!value,
typeof value === 'string' && value.length > 0,
'Empty value is not allowed'
);
validateConfig(
'OCO_OPENAI_API_KEY',
value,
'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`'
);
@@ -355,6 +361,29 @@ export type ConfigType = {
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)) {
if (!value) continue;
if (typeof value === 'string' && ['null', 'undefined'].includes(value)) {
config[key] = undefined;
continue;
}
try {
const validate = configValidators[key as CONFIG_KEYS];
validate(value, config);
} catch (error) {
outro(`Unknown '${key}' config option or missing validator.`);
outro(
`Manually fix the '.env' file or global '~/.opencommit' config file.`
);
process.exit(1);
}
}
};
export const getConfig = ({
configPath = defaultConfigPath,
envPath = defaultEnvPath
@@ -371,10 +400,10 @@ export const getConfig = ({
OCO_GEMINI_API_KEY: process.env.OCO_GEMINI_API_KEY,
OCO_TOKENS_MAX_INPUT: process.env.OCO_TOKENS_MAX_INPUT
? Number(process.env.OCO_TOKENS_MAX_INPUT)
: undefined,
: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT,
OCO_TOKENS_MAX_OUTPUT: process.env.OCO_TOKENS_MAX_OUTPUT
? Number(process.env.OCO_TOKENS_MAX_OUTPUT)
: undefined,
: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT,
OCO_OPENAI_BASE_PATH: process.env.OCO_OPENAI_BASE_PATH,
OCO_GEMINI_BASE_PATH: process.env.OCO_GEMINI_BASE_PATH,
OCO_DESCRIPTION: process.env.OCO_DESCRIPTION === 'true' ? true : false,
@@ -396,35 +425,18 @@ export const getConfig = ({
OCO_OLLAMA_API_URL: process.env.OCO_OLLAMA_API_URL || undefined
};
const configExists = existsSync(configPath);
const isGlobalConfigFileExist = existsSync(configPath);
if (!configExists) return configFromEnv;
if (!isGlobalConfigFileExist) return configFromEnv;
const configFile = readFileSync(configPath, 'utf8');
const config = iniParse(configFile);
const globalConfig = iniParse(configFile);
for (const configKey of Object.keys(config)) {
if (['null', 'undefined'].includes(config[configKey])) {
config[configKey] = undefined;
continue;
}
try {
const validator = configValidators[configKey as CONFIG_KEYS];
const validValue = validator(
config[configKey] ?? configFromEnv[configKey as CONFIG_KEYS],
config
);
config[configKey] = validValue;
} catch (error) {
outro(`Unknown '${configKey}' config option or missing validator.`);
outro(
`Manually fix the '.env' file or global '~/.opencommit' config file.`
);
process.exit(1);
}
}
// env config takes precedence over global ~/.opencommit config file
const config = Object.keys(globalConfig).reduce((acc, key) => {
acc[key] = configFromEnv[key] || globalConfig[key];
return acc;
}, {} as typeof configFromEnv);
return config;
};
@@ -433,28 +445,40 @@ export const setConfig = (
keyValues: [key: string, value: string][],
configPath: string = defaultConfigPath
) => {
const keysToSet = keyValues
.map(([key, value]) => `${key} to ${value}`)
.join(', ');
const config = getConfig() || {};
for (const [configKey, configValue] of keyValues) {
if (!configValidators.hasOwnProperty(configKey)) {
throw new Error(`Unsupported config key: ${configKey}`);
for (let [key, value] of keyValues) {
if (!configValidators.hasOwnProperty(key)) {
const supportedKeys = Object.keys(configValidators).join('\n');
throw new Error(
`Unsupported config key: ${key}. Expected keys are:\n\n${supportedKeys}.\n\nFor more help refer to our docs: https://github.com/di-sukharev/opencommit`
);
}
let parsedConfigValue;
try {
parsedConfigValue = JSON.parse(configValue);
parsedConfigValue = JSON.parse(value);
} catch (error) {
parsedConfigValue = configValue;
parsedConfigValue = value;
}
const validValue =
configValidators[configKey as CONFIG_KEYS](parsedConfigValue);
config[configKey as CONFIG_KEYS] = validValue;
const validValue = configValidators[key as CONFIG_KEYS](
parsedConfigValue,
config
);
config[key as CONFIG_KEYS] = validValue;
}
writeFileSync(configPath, iniStringify(config), 'utf8');
assertConfigsAreValid(config);
outro(`${chalk.green('✔')} Config successfully set`);
};
@@ -464,9 +488,9 @@ export const configCommand = command(
parameters: ['<mode>', '<key=values...>']
},
async (argv) => {
intro('opencommit — config');
try {
const { mode, keyValues } = argv._;
intro(`COMMAND: config ${mode} ${keyValues}`);
if (mode === CONFIG_MODES.get) {
const config = getConfig() || {};

View File

@@ -4,8 +4,8 @@ import { command } from 'cleye';
import { existsSync } from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { COMMANDS } from '../ENUMS.js';
import { assertGitRepo, getCoreHooksPath } from '../utils/git.js';
import { COMMANDS } from './ENUMS';
const HOOK_NAME = 'prepare-commit-msg';
const DEFAULT_SYMLINK_URL = path.join('.git', 'hooks', HOOK_NAME);

View File

@@ -39,7 +39,11 @@ export const prepareCommitMessageHook = async (
const config = getConfig();
if (!config?.OCO_OPENAI_API_KEY && !config?.OCO_ANTHROPIC_API_KEY && !config?.OCO_AZURE_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 or OCO_ANTHROPIC_API_KEY or OCO_AZURE_API_KEY exists. Set your key in ~/.opencommit'
);