diff --git a/out/cli.cjs b/out/cli.cjs index f44d403e..60271183 100755 --- a/out/cli.cjs +++ b/out/cli.cjs @@ -25193,7 +25193,7 @@ function G3(t2, e3) { // package.json var package_default = { name: "opencommit", - version: "3.0.20", + version: "3.1.0", description: "Auto-generate impressive commits in 1 second. Killing lame commits with AI \u{1F92F}\u{1F52B}", keywords: [ "git", @@ -28029,44 +28029,24 @@ var configValidators = { }; var defaultConfigPath = (0, import_path.join)((0, import_os.homedir)(), ".opencommit"); var defaultEnvPath = (0, import_path.resolve)(process.cwd(), ".env"); -var assertConfigsAreValid = (config7) => { - for (const [key, value] of Object.entries(config7)) { - if (!value) - continue; - if (typeof value === "string" && ["null", "undefined"].includes(value)) { - config7[key] = void 0; - continue; - } - try { - const validate = configValidators[key]; - validate(value, config7); - } catch (error) { - ce(`Unknown '${key}' config option or missing validator.`); - ce( - `Manually fix the '.env' file or global '~/.opencommit' config file.` - ); - process.exit(1); - } - } -}; -var initGlobalConfig = () => { - const defaultConfig = { - OCO_TOKENS_MAX_INPUT: 40960 /* DEFAULT_MAX_TOKENS_INPUT */, - OCO_TOKENS_MAX_OUTPUT: 4096 /* DEFAULT_MAX_TOKENS_OUTPUT */, - OCO_DESCRIPTION: false, - OCO_EMOJI: false, - OCO_MODEL: getDefaultModel("openai"), - OCO_LANGUAGE: "en", - OCO_MESSAGE_TEMPLATE_PLACEHOLDER: "$msg", - OCO_PROMPT_MODULE: "conventional-commit" /* CONVENTIONAL_COMMIT */, - OCO_AI_PROVIDER: "openai" /* OPENAI */, - OCO_ONE_LINE_COMMIT: false, - OCO_TEST_MOCK_TYPE: "commit-message", - OCO_FLOWISE_ENDPOINT: ":", - OCO_GITPUSH: true - }; - (0, import_fs.writeFileSync)(defaultConfigPath, (0, import_ini.stringify)(defaultConfig), "utf8"); - return defaultConfig; +var DEFAULT_CONFIG = { + OCO_TOKENS_MAX_INPUT: 40960 /* DEFAULT_MAX_TOKENS_INPUT */, + OCO_TOKENS_MAX_OUTPUT: 4096 /* DEFAULT_MAX_TOKENS_OUTPUT */, + OCO_DESCRIPTION: false, + OCO_EMOJI: false, + OCO_MODEL: getDefaultModel("openai"), + OCO_LANGUAGE: "en", + OCO_MESSAGE_TEMPLATE_PLACEHOLDER: "$msg", + OCO_PROMPT_MODULE: "conventional-commit" /* CONVENTIONAL_COMMIT */, + OCO_AI_PROVIDER: "openai" /* OPENAI */, + OCO_ONE_LINE_COMMIT: false, + OCO_TEST_MOCK_TYPE: "commit-message", + OCO_FLOWISE_ENDPOINT: ":", + OCO_GITPUSH: true +}; +var initGlobalConfig = (configPath = defaultConfigPath) => { + (0, import_fs.writeFileSync)(configPath, (0, import_ini.stringify)(DEFAULT_CONFIG), "utf8"); + return DEFAULT_CONFIG; }; var parseEnvVarValue = (value) => { try { @@ -28075,12 +28055,9 @@ var parseEnvVarValue = (value) => { return value; } }; -var getConfig = ({ - configPath = defaultConfigPath, - envPath = defaultEnvPath -} = {}) => { +var getEnvConfig = (envPath) => { dotenv.config({ path: envPath }); - const envConfig = { + return { OCO_MODEL: process.env.OCO_MODEL, OCO_OPENAI_API_KEY: process.env.OCO_OPENAI_API_KEY, OCO_ANTHROPIC_API_KEY: process.env.OCO_ANTHROPIC_API_KEY, @@ -28104,23 +28081,35 @@ var getConfig = ({ OCO_TEST_MOCK_TYPE: process.env.OCO_TEST_MOCK_TYPE, OCO_GITPUSH: parseEnvVarValue(process.env.OCO_GITPUSH) }; +}; +var getGlobalConfig = (configPath) => { let globalConfig; const isGlobalConfigFileExist = (0, import_fs.existsSync)(configPath); if (!isGlobalConfigFileExist) - globalConfig = initGlobalConfig(); + globalConfig = initGlobalConfig(configPath); else { const configFile = (0, import_fs.readFileSync)(configPath, "utf8"); globalConfig = (0, import_ini.parse)(configFile); } - const mergeObjects = (main, fallback) => Object.keys(CONFIG_KEYS).reduce((acc, key) => { - acc[key] = parseEnvVarValue(main[key] ?? fallback[key]); - return acc; - }, {}); - const config7 = mergeObjects(envConfig, globalConfig); + return globalConfig; +}; +var mergeConfigs = (main, fallback) => Object.keys(CONFIG_KEYS).reduce((acc, key) => { + acc[key] = parseEnvVarValue(main[key] ?? fallback[key]); + return acc; +}, {}); +var getConfig = ({ + envPath = defaultEnvPath, + globalPath = defaultConfigPath +} = {}) => { + const envConfig = getEnvConfig(envPath); + const globalConfig = getGlobalConfig(globalPath); + const config7 = mergeConfigs(envConfig, globalConfig); return config7; }; -var setConfig = (keyValues, configPath = defaultConfigPath) => { - const config7 = getConfig(); +var setConfig = (keyValues, globalConfigPath = defaultConfigPath) => { + const config7 = getConfig({ + globalPath: globalConfigPath + }); for (let [key, value] of keyValues) { if (!configValidators.hasOwnProperty(key)) { const supportedKeys = Object.keys(configValidators).join("\n"); @@ -28144,8 +28133,7 @@ For more help refer to our docs: https://github.com/di-sukharev/opencommit` ); config7[key] = validValue; } - (0, import_fs.writeFileSync)(configPath, (0, import_ini.stringify)(config7), "utf8"); - assertConfigsAreValid(config7); + (0, import_fs.writeFileSync)(globalConfigPath, (0, import_ini.stringify)(config7), "utf8"); ce(`${source_default.green("\u2714")} config successfully set`); }; var configCommand = G3( @@ -42400,7 +42388,7 @@ var OpenAiEngine = class { function getEngine() { const config7 = getConfig(); const provider = config7.OCO_AI_PROVIDER; - const DEFAULT_CONFIG = { + const DEFAULT_CONFIG2 = { model: config7.OCO_MODEL, maxTokensOutput: config7.OCO_TOKENS_MAX_OUTPUT, maxTokensInput: config7.OCO_TOKENS_MAX_INPUT, @@ -42409,37 +42397,37 @@ function getEngine() { switch (provider) { case "ollama" /* OLLAMA */: return new OllamaAi({ - ...DEFAULT_CONFIG, + ...DEFAULT_CONFIG2, apiKey: "", baseURL: config7.OCO_OLLAMA_API_URL }); case "anthropic" /* ANTHROPIC */: return new AnthropicEngine({ - ...DEFAULT_CONFIG, + ...DEFAULT_CONFIG2, apiKey: config7.OCO_ANTHROPIC_API_KEY }); case "test" /* TEST */: return new TestAi(config7.OCO_TEST_MOCK_TYPE); case "gemini" /* GEMINI */: return new Gemini({ - ...DEFAULT_CONFIG, + ...DEFAULT_CONFIG2, apiKey: config7.OCO_GEMINI_API_KEY, baseURL: config7.OCO_GEMINI_BASE_PATH }); case "azure" /* AZURE */: return new AzureEngine({ - ...DEFAULT_CONFIG, + ...DEFAULT_CONFIG2, apiKey: config7.OCO_AZURE_API_KEY }); case "flowise" /* FLOWISE */: return new FlowiseAi({ - ...DEFAULT_CONFIG, - baseURL: config7.OCO_FLOWISE_ENDPOINT || DEFAULT_CONFIG.baseURL, + ...DEFAULT_CONFIG2, + baseURL: config7.OCO_FLOWISE_ENDPOINT || DEFAULT_CONFIG2.baseURL, apiKey: config7.OCO_FLOWISE_API_KEY }); default: return new OpenAiEngine({ - ...DEFAULT_CONFIG, + ...DEFAULT_CONFIG2, apiKey: config7.OCO_OPENAI_API_KEY }); } @@ -43172,8 +43160,8 @@ var generateCommitMessageFromGitDiff = async ({ skipCommitConfirmation = false }) => { await assertGitRepo(); - const commitSpinner = le(); - commitSpinner.start("Generating the commit message"); + const commitGenerationSpinner = le(); + commitGenerationSpinner.start("Generating the commit message"); try { let commitMessage = await generateCommitMessageByDiff( diff, @@ -43188,7 +43176,7 @@ var generateCommitMessageFromGitDiff = async ({ commitMessage ); } - commitSpinner.stop("\u{1F4DD} Commit message generated"); + commitGenerationSpinner.stop("\u{1F4DD} Commit message generated"); ce( `Generated commit message: ${source_default.grey("\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014")} @@ -43198,14 +43186,20 @@ ${source_default.grey("\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2 const isCommitConfirmedByUser = skipCommitConfirmation || await Q3({ message: "Confirm the commit message?" }); - if (isCommitConfirmedByUser && !hD2(isCommitConfirmedByUser)) { + if (hD2(isCommitConfirmedByUser)) + process.exit(1); + if (isCommitConfirmedByUser) { + const committingChangesSpinner = le(); + committingChangesSpinner.start("Committing the changes"); const { stdout } = await execa("git", [ "commit", "-m", commitMessage, ...extraArgs2 ]); - ce(`${source_default.green("\u2714")} Successfully committed`); + committingChangesSpinner.stop( + `${source_default.green("\u2714")} Successfully committed` + ); ce(stdout); const remotes = await getGitRemotes(); if (!remotes.length) { @@ -43218,7 +43212,9 @@ ${source_default.grey("\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2 const isPushConfirmedByUser = await Q3({ message: "Do you want to run `git push`?" }); - if (isPushConfirmedByUser && !hD2(isPushConfirmedByUser)) { + if (hD2(isPushConfirmedByUser)) + process.exit(1); + if (isPushConfirmedByUser) { const pushSpinner = le(); pushSpinner.start(`Running 'git push ${remotes[0]}'`); const { stdout: stdout2 } = await execa("git", [ @@ -43240,26 +43236,26 @@ ${source_default.grey("\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2 message: "Choose a remote to push to", options: remotes.map((remote) => ({ value: remote, label: remote })) }); - if (!hD2(selectedRemote)) { - const pushSpinner = le(); - pushSpinner.start(`Running 'git push ${selectedRemote}'`); - const { stdout: stdout2 } = await execa("git", ["push", selectedRemote]); - pushSpinner.stop( - `${source_default.green( - "\u2714" - )} Successfully pushed all commits to ${selectedRemote}` - ); - if (stdout2) - ce(stdout2); - } else - ce(`${source_default.gray("\u2716")} process cancelled`); + if (hD2(selectedRemote)) + process.exit(1); + const pushSpinner = le(); + pushSpinner.start(`Running 'git push ${selectedRemote}'`); + const { stdout: stdout2 } = await execa("git", ["push", selectedRemote]); + pushSpinner.stop( + `${source_default.green( + "\u2714" + )} Successfully pushed all commits to ${selectedRemote}` + ); + if (stdout2) + ce(stdout2); } - } - if (!isCommitConfirmedByUser && !hD2(isCommitConfirmedByUser)) { + } else { const regenerateMessage = await Q3({ message: "Do you want to regenerate the message?" }); - if (regenerateMessage && !hD2(isCommitConfirmedByUser)) { + if (hD2(regenerateMessage)) + process.exit(1); + if (regenerateMessage) { await generateCommitMessageFromGitDiff({ diff, extraArgs: extraArgs2, @@ -43268,7 +43264,7 @@ ${source_default.grey("\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2 } } } catch (error) { - commitSpinner.stop("\u{1F4DD} Commit message generated"); + commitGenerationSpinner.stop("\u{1F4DD} Commit message generated"); const err = error; ce(`${source_default.red("\u2716")} ${err?.message || err}`); process.exit(1); @@ -43302,7 +43298,9 @@ async function commit(extraArgs2 = [], isStageAllFlag = false, fullGitMojiSpec = const isStageAllAndCommitConfirmedByUser = await Q3({ message: "Do you want to stage all files and generate commit message?" }); - if (isStageAllAndCommitConfirmedByUser && !hD2(isStageAllAndCommitConfirmedByUser)) { + if (hD2(isStageAllAndCommitConfirmedByUser)) + process.exit(1); + if (isStageAllAndCommitConfirmedByUser) { await commit(extraArgs2, true, fullGitMojiSpec); process.exit(1); } diff --git a/out/github-action.cjs b/out/github-action.cjs index 5fef6324..56759984 100644 --- a/out/github-action.cjs +++ b/out/github-action.cjs @@ -46841,44 +46841,24 @@ var configValidators = { }; var defaultConfigPath = (0, import_path.join)((0, import_os.homedir)(), ".opencommit"); var defaultEnvPath = (0, import_path.resolve)(process.cwd(), ".env"); -var assertConfigsAreValid = (config6) => { - for (const [key, value] of Object.entries(config6)) { - if (!value) - continue; - if (typeof value === "string" && ["null", "undefined"].includes(value)) { - config6[key] = void 0; - continue; - } - try { - const validate = configValidators[key]; - validate(value, config6); - } catch (error) { - ce(`Unknown '${key}' config option or missing validator.`); - ce( - `Manually fix the '.env' file or global '~/.opencommit' config file.` - ); - process.exit(1); - } - } -}; -var initGlobalConfig = () => { - const defaultConfig = { - OCO_TOKENS_MAX_INPUT: 40960 /* DEFAULT_MAX_TOKENS_INPUT */, - OCO_TOKENS_MAX_OUTPUT: 4096 /* DEFAULT_MAX_TOKENS_OUTPUT */, - OCO_DESCRIPTION: false, - OCO_EMOJI: false, - OCO_MODEL: getDefaultModel("openai"), - OCO_LANGUAGE: "en", - OCO_MESSAGE_TEMPLATE_PLACEHOLDER: "$msg", - OCO_PROMPT_MODULE: "conventional-commit" /* CONVENTIONAL_COMMIT */, - OCO_AI_PROVIDER: "openai" /* OPENAI */, - OCO_ONE_LINE_COMMIT: false, - OCO_TEST_MOCK_TYPE: "commit-message", - OCO_FLOWISE_ENDPOINT: ":", - OCO_GITPUSH: true - }; - (0, import_fs.writeFileSync)(defaultConfigPath, (0, import_ini.stringify)(defaultConfig), "utf8"); - return defaultConfig; +var DEFAULT_CONFIG = { + OCO_TOKENS_MAX_INPUT: 40960 /* DEFAULT_MAX_TOKENS_INPUT */, + OCO_TOKENS_MAX_OUTPUT: 4096 /* DEFAULT_MAX_TOKENS_OUTPUT */, + OCO_DESCRIPTION: false, + OCO_EMOJI: false, + OCO_MODEL: getDefaultModel("openai"), + OCO_LANGUAGE: "en", + OCO_MESSAGE_TEMPLATE_PLACEHOLDER: "$msg", + OCO_PROMPT_MODULE: "conventional-commit" /* CONVENTIONAL_COMMIT */, + OCO_AI_PROVIDER: "openai" /* OPENAI */, + OCO_ONE_LINE_COMMIT: false, + OCO_TEST_MOCK_TYPE: "commit-message", + OCO_FLOWISE_ENDPOINT: ":", + OCO_GITPUSH: true +}; +var initGlobalConfig = (configPath = defaultConfigPath) => { + (0, import_fs.writeFileSync)(configPath, (0, import_ini.stringify)(DEFAULT_CONFIG), "utf8"); + return DEFAULT_CONFIG; }; var parseEnvVarValue = (value) => { try { @@ -46887,12 +46867,9 @@ var parseEnvVarValue = (value) => { return value; } }; -var getConfig = ({ - configPath = defaultConfigPath, - envPath = defaultEnvPath -} = {}) => { +var getEnvConfig = (envPath) => { dotenv.config({ path: envPath }); - const envConfig = { + return { OCO_MODEL: process.env.OCO_MODEL, OCO_OPENAI_API_KEY: process.env.OCO_OPENAI_API_KEY, OCO_ANTHROPIC_API_KEY: process.env.OCO_ANTHROPIC_API_KEY, @@ -46916,23 +46893,35 @@ var getConfig = ({ OCO_TEST_MOCK_TYPE: process.env.OCO_TEST_MOCK_TYPE, OCO_GITPUSH: parseEnvVarValue(process.env.OCO_GITPUSH) }; +}; +var getGlobalConfig = (configPath) => { let globalConfig; const isGlobalConfigFileExist = (0, import_fs.existsSync)(configPath); if (!isGlobalConfigFileExist) - globalConfig = initGlobalConfig(); + globalConfig = initGlobalConfig(configPath); else { const configFile = (0, import_fs.readFileSync)(configPath, "utf8"); globalConfig = (0, import_ini.parse)(configFile); } - const mergeObjects = (main, fallback) => Object.keys(CONFIG_KEYS).reduce((acc, key) => { - acc[key] = parseEnvVarValue(main[key] ?? fallback[key]); - return acc; - }, {}); - const config6 = mergeObjects(envConfig, globalConfig); + return globalConfig; +}; +var mergeConfigs = (main, fallback) => Object.keys(CONFIG_KEYS).reduce((acc, key) => { + acc[key] = parseEnvVarValue(main[key] ?? fallback[key]); + return acc; +}, {}); +var getConfig = ({ + envPath = defaultEnvPath, + globalPath = defaultConfigPath +} = {}) => { + const envConfig = getEnvConfig(envPath); + const globalConfig = getGlobalConfig(globalPath); + const config6 = mergeConfigs(envConfig, globalConfig); return config6; }; -var setConfig = (keyValues, configPath = defaultConfigPath) => { - const config6 = getConfig(); +var setConfig = (keyValues, globalConfigPath = defaultConfigPath) => { + const config6 = getConfig({ + globalPath: globalConfigPath + }); for (let [key, value] of keyValues) { if (!configValidators.hasOwnProperty(key)) { const supportedKeys = Object.keys(configValidators).join("\n"); @@ -46956,8 +46945,7 @@ For more help refer to our docs: https://github.com/di-sukharev/opencommit` ); config6[key] = validValue; } - (0, import_fs.writeFileSync)(configPath, (0, import_ini.stringify)(config6), "utf8"); - assertConfigsAreValid(config6); + (0, import_fs.writeFileSync)(globalConfigPath, (0, import_ini.stringify)(config6), "utf8"); ce(`${source_default.green("\u2714")} config successfully set`); }; var configCommand = G2( @@ -61212,7 +61200,7 @@ var OpenAiEngine = class { function getEngine() { const config6 = getConfig(); const provider = config6.OCO_AI_PROVIDER; - const DEFAULT_CONFIG = { + const DEFAULT_CONFIG2 = { model: config6.OCO_MODEL, maxTokensOutput: config6.OCO_TOKENS_MAX_OUTPUT, maxTokensInput: config6.OCO_TOKENS_MAX_INPUT, @@ -61221,37 +61209,37 @@ function getEngine() { switch (provider) { case "ollama" /* OLLAMA */: return new OllamaAi({ - ...DEFAULT_CONFIG, + ...DEFAULT_CONFIG2, apiKey: "", baseURL: config6.OCO_OLLAMA_API_URL }); case "anthropic" /* ANTHROPIC */: return new AnthropicEngine({ - ...DEFAULT_CONFIG, + ...DEFAULT_CONFIG2, apiKey: config6.OCO_ANTHROPIC_API_KEY }); case "test" /* TEST */: return new TestAi(config6.OCO_TEST_MOCK_TYPE); case "gemini" /* GEMINI */: return new Gemini({ - ...DEFAULT_CONFIG, + ...DEFAULT_CONFIG2, apiKey: config6.OCO_GEMINI_API_KEY, baseURL: config6.OCO_GEMINI_BASE_PATH }); case "azure" /* AZURE */: return new AzureEngine({ - ...DEFAULT_CONFIG, + ...DEFAULT_CONFIG2, apiKey: config6.OCO_AZURE_API_KEY }); case "flowise" /* FLOWISE */: return new FlowiseAi({ - ...DEFAULT_CONFIG, - baseURL: config6.OCO_FLOWISE_ENDPOINT || DEFAULT_CONFIG.baseURL, + ...DEFAULT_CONFIG2, + baseURL: config6.OCO_FLOWISE_ENDPOINT || DEFAULT_CONFIG2.baseURL, apiKey: config6.OCO_FLOWISE_API_KEY }); default: return new OpenAiEngine({ - ...DEFAULT_CONFIG, + ...DEFAULT_CONFIG2, apiKey: config6.OCO_OPENAI_API_KEY }); } diff --git a/src/commands/commit.ts b/src/commands/commit.ts index ff101387..5bb9de85 100644 --- a/src/commands/commit.ts +++ b/src/commands/commit.ts @@ -50,8 +50,8 @@ const generateCommitMessageFromGitDiff = async ({ skipCommitConfirmation = false }: GenerateCommitMessageFromGitDiffParams): Promise => { await assertGitRepo(); - const commitSpinner = spinner(); - commitSpinner.start('Generating the commit message'); + const commitGenerationSpinner = spinner(); + commitGenerationSpinner.start('Generating the commit message'); try { let commitMessage = await generateCommitMessageByDiff( @@ -73,7 +73,7 @@ const generateCommitMessageFromGitDiff = async ({ ); } - commitSpinner.stop('📝 Commit message generated'); + commitGenerationSpinner.stop('📝 Commit message generated'); outro( `Generated commit message: @@ -88,15 +88,20 @@ ${chalk.grey('——————————————————')}` message: 'Confirm the commit message?' })); - if (isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) { + if (isCancel(isCommitConfirmedByUser)) process.exit(1); + + if (isCommitConfirmedByUser) { + const committingChangesSpinner = spinner(); + committingChangesSpinner.start('Committing the changes'); const { stdout } = await execa('git', [ 'commit', '-m', commitMessage, ...extraArgs ]); - - outro(`${chalk.green('✔')} Successfully committed`); + committingChangesSpinner.stop( + `${chalk.green('✔')} Successfully committed` + ); outro(stdout); @@ -113,7 +118,9 @@ ${chalk.grey('——————————————————')}` message: 'Do you want to run `git push`?' }); - if (isPushConfirmedByUser && !isCancel(isPushConfirmedByUser)) { + if (isCancel(isPushConfirmedByUser)) process.exit(1); + + if (isPushConfirmedByUser) { const pushSpinner = spinner(); pushSpinner.start(`Running 'git push ${remotes[0]}'`); @@ -141,28 +148,30 @@ ${chalk.grey('——————————————————')}` options: remotes.map((remote) => ({ value: remote, label: remote })) })) as string; - if (!isCancel(selectedRemote)) { - const pushSpinner = spinner(); + if (isCancel(selectedRemote)) process.exit(1); - pushSpinner.start(`Running 'git push ${selectedRemote}'`); + const pushSpinner = spinner(); - const { stdout } = await execa('git', ['push', selectedRemote]); + pushSpinner.start(`Running 'git push ${selectedRemote}'`); - pushSpinner.stop( - `${chalk.green( - '✔' - )} Successfully pushed all commits to ${selectedRemote}` - ); + const { stdout } = await execa('git', ['push', selectedRemote]); - if (stdout) outro(stdout); - } else outro(`${chalk.gray('✖')} process cancelled`); + pushSpinner.stop( + `${chalk.green( + '✔' + )} Successfully pushed all commits to ${selectedRemote}` + ); + + if (stdout) outro(stdout); } - } - if (!isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) { + } else { const regenerateMessage = await confirm({ message: 'Do you want to regenerate the message?' }); - if (regenerateMessage && !isCancel(isCommitConfirmedByUser)) { + + if (isCancel(regenerateMessage)) process.exit(1); + + if (regenerateMessage) { await generateCommitMessageFromGitDiff({ diff, extraArgs, @@ -171,7 +180,7 @@ ${chalk.grey('——————————————————')}` } } } catch (error) { - commitSpinner.stop('📝 Commit message generated'); + commitGenerationSpinner.stop('📝 Commit message generated'); const err = error as Error; outro(`${chalk.red('✖')} ${err?.message || err}`); @@ -219,10 +228,9 @@ export async function commit( message: 'Do you want to stage all files and generate commit message?' }); - if ( - isStageAllAndCommitConfirmedByUser && - !isCancel(isStageAllAndCommitConfirmedByUser) - ) { + if (isCancel(isStageAllAndCommitConfirmedByUser)) process.exit(1); + + if (isStageAllAndCommitConfirmedByUser) { await commit(extraArgs, true, fullGitMojiSpec); process.exit(1); } diff --git a/src/commands/config.ts b/src/commands/config.ts index b46ac822..78b435aa 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -422,25 +422,25 @@ enum OCO_PROMPT_MODULE_ENUM { COMMITLINT = '@commitlint' } -const initGlobalConfig = () => { - const defaultConfig = { - OCO_TOKENS_MAX_INPUT: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT, - OCO_TOKENS_MAX_OUTPUT: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT, - OCO_DESCRIPTION: false, - OCO_EMOJI: false, - OCO_MODEL: getDefaultModel('openai'), - OCO_LANGUAGE: 'en', - OCO_MESSAGE_TEMPLATE_PLACEHOLDER: '$msg', - OCO_PROMPT_MODULE: OCO_PROMPT_MODULE_ENUM.CONVENTIONAL_COMMIT, - OCO_AI_PROVIDER: OCO_AI_PROVIDER_ENUM.OPENAI, - OCO_ONE_LINE_COMMIT: false, - OCO_TEST_MOCK_TYPE: 'commit-message', - OCO_FLOWISE_ENDPOINT: ':', - OCO_GITPUSH: true // todo: deprecate - }; +export const DEFAULT_CONFIG = { + OCO_TOKENS_MAX_INPUT: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT, + OCO_TOKENS_MAX_OUTPUT: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT, + OCO_DESCRIPTION: false, + OCO_EMOJI: false, + OCO_MODEL: getDefaultModel('openai'), + OCO_LANGUAGE: 'en', + OCO_MESSAGE_TEMPLATE_PLACEHOLDER: '$msg', + OCO_PROMPT_MODULE: OCO_PROMPT_MODULE_ENUM.CONVENTIONAL_COMMIT, + OCO_AI_PROVIDER: OCO_AI_PROVIDER_ENUM.OPENAI, + OCO_ONE_LINE_COMMIT: false, + OCO_TEST_MOCK_TYPE: 'commit-message', + OCO_FLOWISE_ENDPOINT: ':', + OCO_GITPUSH: true // todo: deprecate +}; - writeFileSync(defaultConfigPath, iniStringify(defaultConfig), 'utf8'); - return defaultConfig; +const initGlobalConfig = (configPath: string = defaultConfigPath) => { + writeFileSync(configPath, iniStringify(DEFAULT_CONFIG), 'utf8'); + return DEFAULT_CONFIG; }; const parseEnvVarValue = (value?: any) => { @@ -451,16 +451,10 @@ const parseEnvVarValue = (value?: any) => { } }; -export const getConfig = ({ - configPath = defaultConfigPath, - envPath = defaultEnvPath -}: { - configPath?: string; - envPath?: string; -} = {}): ConfigType => { +const getEnvConfig = (envPath: string) => { dotenv.config({ path: envPath }); - const envConfig = { + return { OCO_MODEL: process.env.OCO_MODEL, OCO_OPENAI_API_KEY: process.env.OCO_OPENAI_API_KEY, @@ -491,33 +485,59 @@ export const getConfig = ({ OCO_GITPUSH: parseEnvVarValue(process.env.OCO_GITPUSH) // todo: deprecate }; +}; +const getGlobalConfig = (configPath: string) => { let globalConfig: ConfigType; + const isGlobalConfigFileExist = existsSync(configPath); - if (!isGlobalConfigFileExist) globalConfig = initGlobalConfig(); + if (!isGlobalConfigFileExist) globalConfig = initGlobalConfig(configPath); else { const configFile = readFileSync(configPath, 'utf8'); globalConfig = iniParse(configFile) as ConfigType; } - const mergeObjects = (main: Partial, fallback: ConfigType) => - Object.keys(CONFIG_KEYS).reduce((acc, key) => { - acc[key] = parseEnvVarValue(main[key] ?? fallback[key]); + return globalConfig; +}; + +/** + * Merges two configs. + * Env config takes precedence over global ~/.opencommit config file + * @param main - env config + * @param fallback - global ~/.opencommit config file + * @returns merged config + */ +const mergeConfigs = (main: Partial, 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; +} - return acc; - }, {} as ConfigType); +export const getConfig = ({ + envPath = defaultEnvPath, + globalPath = defaultConfigPath +}: GetConfigOptions = {}): ConfigType => { + const envConfig = getEnvConfig(envPath); + const globalConfig = getGlobalConfig(globalPath); - // env config takes precedence over global ~/.opencommit config file - const config = mergeObjects(envConfig, globalConfig); + const config = mergeConfigs(envConfig, globalConfig); return config; }; export const setConfig = ( keyValues: [key: string, value: string][], - configPath: string = defaultConfigPath + globalConfigPath: string = defaultConfigPath ) => { - const config = getConfig(); + const config = getConfig({ + globalPath: globalConfigPath + }); for (let [key, value] of keyValues) { if (!configValidators.hasOwnProperty(key)) { @@ -543,9 +563,7 @@ export const setConfig = ( config[key] = validValue; } - writeFileSync(configPath, iniStringify(config), 'utf8'); - - assertConfigsAreValid(config); + writeFileSync(globalConfigPath, iniStringify(config), 'utf8'); outro(`${chalk.green('✔')} config successfully set`); }; diff --git a/test/unit/config.test.ts b/test/unit/config.test.ts index d8ebdff4..35c15528 100644 --- a/test/unit/config.test.ts +++ b/test/unit/config.test.ts @@ -1,10 +1,16 @@ -import { getConfig } from '../../src/commands/config'; +import { existsSync, readFileSync, rmSync } from 'fs'; +import { + DEFAULT_CONFIG, + getConfig, + setConfig +} from '../../src/commands/config'; import { prepareFile } from './utils'; +import { dirname } from 'path'; -describe('getConfig', () => { +describe('config', () => { const originalEnv = { ...process.env }; let globalConfigFile: { filePath: string; cleanup: () => Promise }; - let localEnvFile: { filePath: string; cleanup: () => Promise }; + let envConfigFile: { filePath: string; cleanup: () => Promise }; function resetEnv(env: NodeJS.ProcessEnv) { Object.keys(process.env).forEach((key) => { @@ -19,7 +25,12 @@ describe('getConfig', () => { beforeEach(async () => { resetEnv(originalEnv); if (globalConfigFile) await globalConfigFile.cleanup(); - if (localEnvFile) await localEnvFile.cleanup(); + if (envConfigFile) await envConfigFile.cleanup(); + }); + + afterEach(async () => { + if (globalConfigFile) await globalConfigFile.cleanup(); + if (envConfigFile) await envConfigFile.cleanup(); }); afterAll(() => { @@ -36,115 +47,249 @@ describe('getConfig', () => { return await prepareFile(fileName, fileContent); }; - it('should prioritize local .env over global .opencommit config', async () => { - globalConfigFile = await generateConfig('.opencommit', { - OCO_OPENAI_API_KEY: 'global-key', - OCO_MODEL: 'gpt-3.5-turbo', - OCO_LANGUAGE: 'en' - }); + describe('getConfig', () => { + it('should prioritize local .env over global .opencommit config', async () => { + globalConfigFile = await generateConfig('.opencommit', { + OCO_OPENAI_API_KEY: 'global-key', + OCO_MODEL: 'gpt-3.5-turbo', + OCO_LANGUAGE: 'en' + }); + + envConfigFile = await generateConfig('.env', { + OCO_OPENAI_API_KEY: 'local-key', + OCO_ANTHROPIC_API_KEY: 'local-anthropic-key', + OCO_LANGUAGE: 'fr' + }); - localEnvFile = await generateConfig('.env', { - OCO_OPENAI_API_KEY: 'local-key', - OCO_ANTHROPIC_API_KEY: 'local-anthropic-key', - OCO_LANGUAGE: 'fr' + const config = getConfig({ + globalPath: globalConfigFile.filePath, + envPath: envConfigFile.filePath + }); + + expect(config).not.toEqual(null); + 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'); }); - const config = getConfig({ - configPath: globalConfigFile.filePath, - envPath: localEnvFile.filePath + it('should fallback to global config when local config is not set', async () => { + globalConfigFile = await generateConfig('.opencommit', { + OCO_OPENAI_API_KEY: 'global-key', + OCO_MODEL: 'gpt-4', + OCO_LANGUAGE: 'de', + OCO_DESCRIPTION: 'true' + }); + + envConfigFile = await generateConfig('.env', { + OCO_ANTHROPIC_API_KEY: 'local-anthropic-key' + }); + + const config = getConfig({ + globalPath: globalConfigFile.filePath, + envPath: envConfigFile.filePath + }); + + expect(config).not.toEqual(null); + 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); }); - expect(config).not.toEqual(null); - 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 handle boolean and numeric values correctly', async () => { + globalConfigFile = await generateConfig('.opencommit', { + OCO_TOKENS_MAX_INPUT: '4096', + OCO_TOKENS_MAX_OUTPUT: '500', + OCO_GITPUSH: 'true' + }); + + envConfigFile = await generateConfig('.env', { + OCO_TOKENS_MAX_INPUT: '8192', + OCO_ONE_LINE_COMMIT: 'false' + }); - it('should fallback to global config when local config is not set', async () => { - globalConfigFile = await generateConfig('.opencommit', { - OCO_OPENAI_API_KEY: 'global-key', - OCO_MODEL: 'gpt-4', - OCO_LANGUAGE: 'de', - OCO_DESCRIPTION: 'true' + const config = getConfig({ + globalPath: globalConfigFile.filePath, + envPath: envConfigFile.filePath + }); + + expect(config).not.toEqual(null); + expect(config.OCO_TOKENS_MAX_INPUT).toEqual(8192); + expect(config.OCO_TOKENS_MAX_OUTPUT).toEqual(500); + expect(config.OCO_GITPUSH).toEqual(true); + expect(config.OCO_ONE_LINE_COMMIT).toEqual(false); }); - localEnvFile = await generateConfig('.env', { - OCO_ANTHROPIC_API_KEY: 'local-anthropic-key' + it('should handle empty local config correctly', async () => { + globalConfigFile = await generateConfig('.opencommit', { + OCO_OPENAI_API_KEY: 'global-key', + OCO_MODEL: 'gpt-4', + OCO_LANGUAGE: 'es' + }); + + envConfigFile = await generateConfig('.env', {}); + + const config = getConfig({ + globalPath: globalConfigFile.filePath, + envPath: envConfigFile.filePath + }); + + expect(config).not.toEqual(null); + expect(config.OCO_OPENAI_API_KEY).toEqual('global-key'); + expect(config.OCO_MODEL).toEqual('gpt-4'); + expect(config.OCO_LANGUAGE).toEqual('es'); }); - const config = getConfig({ - configPath: globalConfigFile.filePath, - envPath: localEnvFile.filePath + it('should override global config with null values in local .env', async () => { + globalConfigFile = await generateConfig('.opencommit', { + OCO_OPENAI_API_KEY: 'global-key', + OCO_MODEL: 'gpt-4', + OCO_LANGUAGE: 'es' + }); + + envConfigFile = await generateConfig('.env', { + OCO_OPENAI_API_KEY: 'null' + }); + + const config = getConfig({ + globalPath: globalConfigFile.filePath, + envPath: envConfigFile.filePath + }); + + expect(config).not.toEqual(null); + expect(config.OCO_OPENAI_API_KEY).toEqual(null); }); - expect(config).not.toEqual(null); - 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); + it('should handle empty global config', async () => { + globalConfigFile = await generateConfig('.opencommit', {}); + envConfigFile = await generateConfig('.env', {}); + + const config = getConfig({ + globalPath: globalConfigFile.filePath, + envPath: envConfigFile.filePath + }); + + expect(config).not.toEqual(null); + expect(config.OCO_OPENAI_API_KEY).toEqual(undefined); + }); }); - it('should handle boolean and numeric values correctly', async () => { - globalConfigFile = await generateConfig('.opencommit', { - OCO_TOKENS_MAX_INPUT: '4096', - OCO_TOKENS_MAX_OUTPUT: '500', - OCO_GITPUSH: 'true' + describe('setConfig', () => { + beforeEach(async () => { + // we create and delete the file to have the parent directory, but not the file, to test the creation of the file + globalConfigFile = await generateConfig('.opencommit', {}); + rmSync(globalConfigFile.filePath); }); - localEnvFile = await generateConfig('.env', { - OCO_TOKENS_MAX_INPUT: '8192', - OCO_ONE_LINE_COMMIT: 'false' + it('should create .opencommit file with DEFAULT CONFIG if it does not exist on first setConfig run', async () => { + const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath); + expect(isGlobalConfigFileExist).toBe(false); + + await setConfig( + [['OCO_OPENAI_API_KEY', 'persisted-key_1']], + globalConfigFile.filePath + ); + + const fileContent = readFileSync(globalConfigFile.filePath, 'utf8'); + expect(fileContent).toContain('OCO_OPENAI_API_KEY=persisted-key_1'); + Object.entries(DEFAULT_CONFIG).forEach(([key, value]) => { + expect(fileContent).toContain(`${key}=${value}`); + }); }); - const config = getConfig({ - configPath: globalConfigFile.filePath, - envPath: localEnvFile.filePath + it('should set new config values', async () => { + globalConfigFile = await generateConfig('.opencommit', {}); + await setConfig( + [ + ['OCO_OPENAI_API_KEY', 'new-key'], + ['OCO_MODEL', 'gpt-4'] + ], + globalConfigFile.filePath + ); + + const config = getConfig({ globalPath: globalConfigFile.filePath }); + expect(config.OCO_OPENAI_API_KEY).toEqual('new-key'); + expect(config.OCO_MODEL).toEqual('gpt-4'); }); - expect(config).not.toEqual(null); - expect(config.OCO_TOKENS_MAX_INPUT).toEqual(8192); - expect(config.OCO_TOKENS_MAX_OUTPUT).toEqual(500); - expect(config.OCO_GITPUSH).toEqual(true); - expect(config.OCO_ONE_LINE_COMMIT).toEqual(false); - }); + it('should update existing config values', async () => { + globalConfigFile = await generateConfig('.opencommit', { + OCO_OPENAI_API_KEY: 'initial-key' + }); + await setConfig( + [['OCO_OPENAI_API_KEY', 'updated-key']], + globalConfigFile.filePath + ); - it('should handle empty local config correctly', async () => { - globalConfigFile = await generateConfig('.opencommit', { - OCO_OPENAI_API_KEY: 'global-key', - OCO_MODEL: 'gpt-4', - OCO_LANGUAGE: 'es' + const config = getConfig({ globalPath: globalConfigFile.filePath }); + expect(config.OCO_OPENAI_API_KEY).toEqual('updated-key'); }); - localEnvFile = await generateConfig('.env', {}); + it('should handle boolean and numeric values correctly', async () => { + globalConfigFile = await generateConfig('.opencommit', {}); + await setConfig( + [ + ['OCO_TOKENS_MAX_INPUT', '8192'], + ['OCO_DESCRIPTION', 'true'], + ['OCO_ONE_LINE_COMMIT', 'false'] + ], + globalConfigFile.filePath + ); - const config = getConfig({ - configPath: globalConfigFile.filePath, - envPath: localEnvFile.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); }); - expect(config).not.toEqual(null); - expect(config.OCO_OPENAI_API_KEY).toEqual('global-key'); - expect(config.OCO_MODEL).toEqual('gpt-4'); - expect(config.OCO_LANGUAGE).toEqual('es'); - }); + it('should throw an error for unsupported config keys', async () => { + globalConfigFile = await generateConfig('.opencommit', {}); - it('should override global config with null values in local .env', async () => { - globalConfigFile = await generateConfig('.opencommit', { - OCO_OPENAI_API_KEY: 'global-key', - OCO_MODEL: 'gpt-4', - OCO_LANGUAGE: 'es' + try { + await setConfig( + [['UNSUPPORTED_KEY', 'value']], + globalConfigFile.filePath + ); + throw new Error('NEVER_REACHED'); + } catch (error) { + expect(error.message).toContain( + 'Unsupported config key: UNSUPPORTED_KEY' + ); + expect(error.message).not.toContain('NEVER_REACHED'); + } }); - localEnvFile = await generateConfig('.env', { OCO_OPENAI_API_KEY: 'null' }); + it('should persist changes to the config file', async () => { + const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath); + expect(isGlobalConfigFileExist).toBe(false); - const config = getConfig({ - configPath: globalConfigFile.filePath, - envPath: localEnvFile.filePath + await setConfig( + [['OCO_OPENAI_API_KEY', 'persisted-key']], + globalConfigFile.filePath + ); + + const fileContent = readFileSync(globalConfigFile.filePath, 'utf8'); + expect(fileContent).toContain('OCO_OPENAI_API_KEY=persisted-key'); }); - expect(config).not.toEqual(null); - expect(config.OCO_OPENAI_API_KEY).toEqual(null); + it('should set multiple configs in a row and keep the changes', async () => { + const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath); + expect(isGlobalConfigFileExist).toBe(false); + + await setConfig( + [['OCO_OPENAI_API_KEY', 'persisted-key']], + globalConfigFile.filePath + ); + + const fileContent1 = readFileSync(globalConfigFile.filePath, 'utf8'); + expect(fileContent1).toContain('OCO_OPENAI_API_KEY=persisted-key'); + + await setConfig([['OCO_MODEL', 'gpt-4']], globalConfigFile.filePath); + + const fileContent2 = readFileSync(globalConfigFile.filePath, 'utf8'); + expect(fileContent2).toContain('OCO_MODEL=gpt-4'); + }); }); }); diff --git a/test/unit/utils.ts b/test/unit/utils.ts index 949202b4..f1b6268f 100644 --- a/test/unit/utils.ts +++ b/test/unit/utils.ts @@ -1,7 +1,7 @@ +import { existsSync, mkdtemp, rm, writeFile } from 'fs'; +import { tmpdir } from 'os'; import path from 'path'; -import { mkdtemp, rm, writeFile } from 'fs'; import { promisify } from 'util'; -import { tmpdir } from 'os'; const fsMakeTempDir = promisify(mkdtemp); const fsRemove = promisify(rm); const fsWriteFile = promisify(writeFile); @@ -20,7 +20,9 @@ export async function prepareFile( const filePath = path.resolve(tempDir, fileName); await fsWriteFile(filePath, content); const cleanup = async () => { - return fsRemove(tempDir, { recursive: true }); + if (existsSync(tempDir)) { + await fsRemove(tempDir, { recursive: true }); + } }; return { diff --git a/tsconfig.json b/tsconfig.json index 485bd047..d99dc94e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "lib": ["ES6", "ES2020"], "module": "CommonJS", - + "resolveJsonModule": true, "moduleResolution": "Node", @@ -21,9 +21,7 @@ "skipLibCheck": true }, - "include": [ - "test/jest-setup.ts" - ], + "include": ["test/jest-setup.ts"], "exclude": ["node_modules"], "ts-node": { "esm": true,