diff --git a/README.md b/README.md index 1b27b530..d6ae7685 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ OCO_LANGUAGE= OCO_MESSAGE_TEMPLATE_PLACEHOLDER= OCO_PROMPT_MODULE= OCO_ONE_LINE_COMMIT= +OCO_CUSTOM_PROMPT="" ``` Global configs are same as local configs, but they are stored in the global `~/.opencommit` config file and set with `oco config set` command, e.g. `oco config set OCO_MODEL=gpt-4o`. diff --git a/out/cli.cjs b/out/cli.cjs index 8a79a122..a5665ce6 100755 --- a/out/cli.cjs +++ b/out/cli.cjs @@ -1332,185 +1332,6 @@ var require_main = __commonJS({ } }); -// node_modules/ini/lib/ini.js -var require_ini = __commonJS({ - "node_modules/ini/lib/ini.js"(exports2, module2) { - var { hasOwnProperty: hasOwnProperty2 } = Object.prototype; - var eol = typeof process !== "undefined" && process.platform === "win32" ? "\r\n" : "\n"; - var encode3 = (obj, opt) => { - const children = []; - let out = ""; - if (typeof opt === "string") { - opt = { - section: opt, - whitespace: false - }; - } else { - opt = opt || /* @__PURE__ */ Object.create(null); - opt.whitespace = opt.whitespace === true; - } - const separator = opt.whitespace ? " = " : "="; - for (const k7 of Object.keys(obj)) { - const val = obj[k7]; - if (val && Array.isArray(val)) { - for (const item of val) { - out += safe(k7 + "[]") + separator + safe(item) + eol; - } - } else if (val && typeof val === "object") { - children.push(k7); - } else { - out += safe(k7) + separator + safe(val) + eol; - } - } - if (opt.section && out.length) { - out = "[" + safe(opt.section) + "]" + eol + out; - } - for (const k7 of children) { - const nk = dotSplit(k7).join("\\."); - const section = (opt.section ? opt.section + "." : "") + nk; - const { whitespace } = opt; - const child = encode3(obj[k7], { - section, - whitespace - }); - if (out.length && child.length) { - out += eol; - } - out += child; - } - return out; - }; - var dotSplit = (str2) => str2.replace(/\1/g, "LITERAL\\1LITERAL").replace(/\\\./g, "").split(/\./).map((part) => part.replace(/\1/g, "\\.").replace(/\2LITERAL\\1LITERAL\2/g, "")); - var decode = (str2) => { - const out = /* @__PURE__ */ Object.create(null); - let p4 = out; - let section = null; - const re3 = /^\[([^\]]*)\]$|^([^=]+)(=(.*))?$/i; - const lines = str2.split(/[\r\n]+/g); - for (const line of lines) { - if (!line || line.match(/^\s*[;#]/)) { - continue; - } - const match = line.match(re3); - if (!match) { - continue; - } - if (match[1] !== void 0) { - section = unsafe(match[1]); - if (section === "__proto__") { - p4 = /* @__PURE__ */ Object.create(null); - continue; - } - p4 = out[section] = out[section] || /* @__PURE__ */ Object.create(null); - continue; - } - const keyRaw = unsafe(match[2]); - const isArray2 = keyRaw.length > 2 && keyRaw.slice(-2) === "[]"; - const key = isArray2 ? keyRaw.slice(0, -2) : keyRaw; - if (key === "__proto__") { - continue; - } - const valueRaw = match[3] ? unsafe(match[4]) : true; - const value = valueRaw === "true" || valueRaw === "false" || valueRaw === "null" ? JSON.parse(valueRaw) : valueRaw; - if (isArray2) { - if (!hasOwnProperty2.call(p4, key)) { - p4[key] = []; - } else if (!Array.isArray(p4[key])) { - p4[key] = [p4[key]]; - } - } - if (Array.isArray(p4[key])) { - p4[key].push(value); - } else { - p4[key] = value; - } - } - const remove = []; - for (const k7 of Object.keys(out)) { - if (!hasOwnProperty2.call(out, k7) || typeof out[k7] !== "object" || Array.isArray(out[k7])) { - continue; - } - const parts = dotSplit(k7); - p4 = out; - const l3 = parts.pop(); - const nl = l3.replace(/\\\./g, "."); - for (const part of parts) { - if (part === "__proto__") { - continue; - } - if (!hasOwnProperty2.call(p4, part) || typeof p4[part] !== "object") { - p4[part] = /* @__PURE__ */ Object.create(null); - } - p4 = p4[part]; - } - if (p4 === out && nl === l3) { - continue; - } - p4[nl] = out[k7]; - remove.push(k7); - } - for (const del of remove) { - delete out[del]; - } - return out; - }; - var isQuoted = (val) => { - return val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'"); - }; - var safe = (val) => { - if (typeof val !== "string" || val.match(/[=\r\n]/) || val.match(/^\[/) || val.length > 1 && isQuoted(val) || val !== val.trim()) { - return JSON.stringify(val); - } - return val.split(";").join("\\;").split("#").join("\\#"); - }; - var unsafe = (val, doUnesc) => { - val = (val || "").trim(); - if (isQuoted(val)) { - if (val.charAt(0) === "'") { - val = val.slice(1, -1); - } - try { - val = JSON.parse(val); - } catch { - } - } else { - let esc = false; - let unesc = ""; - for (let i3 = 0, l3 = val.length; i3 < l3; i3++) { - const c4 = val.charAt(i3); - if (esc) { - if ("\\;#".indexOf(c4) !== -1) { - unesc += c4; - } else { - unesc += "\\" + c4; - } - esc = false; - } else if (";#".indexOf(c4) !== -1) { - break; - } else if (c4 === "\\") { - esc = true; - } else { - unesc += c4; - } - } - if (esc) { - unesc += "\\"; - } - return unesc.trim(); - } - return val; - }; - module2.exports = { - parse: decode, - decode, - stringify: encode3, - encode: encode3, - safe, - unsafe - }; - } -}); - // node_modules/webidl-conversions/lib/index.js var require_lib = __commonJS({ "node_modules/webidl-conversions/lib/index.js"(exports2) { @@ -47736,7 +47557,6 @@ var package_default = { }, devDependencies: { "@commitlint/types": "^17.4.4", - "@types/ini": "^1.3.31", "@types/inquirer": "^9.0.3", "@types/jest": "^29.5.12", "@types/node": "^16.18.14", @@ -47771,7 +47591,6 @@ var package_default = { crypto: "^1.0.1", execa: "^7.0.0", ignore: "^5.2.4", - ini: "^3.0.1", inquirer: "^9.1.4", openai: "^4.57.0", punycode: "^2.3.1", @@ -50019,7 +49838,6 @@ var $4 = create$(); // src/commands/config.ts var dotenv = __toESM(require_main(), 1); var import_fs = require("fs"); -var import_ini = __toESM(require_ini(), 1); var import_os = require("os"); var import_path = require("path"); @@ -50313,6 +50131,7 @@ var CONFIG_KEYS = /* @__PURE__ */ ((CONFIG_KEYS2) => { CONFIG_KEYS2["OCO_OMIT_SCOPE"] = "OCO_OMIT_SCOPE"; CONFIG_KEYS2["OCO_GITPUSH"] = "OCO_GITPUSH"; CONFIG_KEYS2["OCO_HOOK_AUTO_UNCOMMENT"] = "OCO_HOOK_AUTO_UNCOMMENT"; + CONFIG_KEYS2["OCO_CUSTOM_PROMPT"] = "OCO_CUSTOM_PROMPT"; return CONFIG_KEYS2; })(CONFIG_KEYS || {}); var MODEL_LIST = { @@ -51064,6 +50883,15 @@ var configValidators = { typeof value === "boolean", "Must be true or false" ); + }, + ["OCO_CUSTOM_PROMPT" /* OCO_CUSTOM_PROMPT */](value) { + if (value === void 0 || value === null) return value; + validateConfig( + "OCO_CUSTOM_PROMPT" /* OCO_CUSTOM_PROMPT */, + typeof value === "string", + "Must be a string" + ); + return value; } }; var OCO_AI_PROVIDER_ENUM = /* @__PURE__ */ ((OCO_AI_PROVIDER_ENUM2) => { @@ -51105,10 +50933,12 @@ var DEFAULT_CONFIG = { OCO_OMIT_SCOPE: false, OCO_GITPUSH: true, // todo: deprecate - OCO_HOOK_AUTO_UNCOMMENT: false + OCO_HOOK_AUTO_UNCOMMENT: false, + OCO_CUSTOM_PROMPT: void 0 }; var initGlobalConfig = (configPath = defaultConfigPath) => { - (0, import_fs.writeFileSync)(configPath, (0, import_ini.stringify)(DEFAULT_CONFIG), "utf8"); + const configContent = Object.entries(DEFAULT_CONFIG).map(([key, value]) => `${key}=${value}`).join("\n"); + (0, import_fs.writeFileSync)(configPath, configContent, "utf8"); return DEFAULT_CONFIG; }; var parseConfigVarValue = (value) => { @@ -51138,12 +50968,22 @@ var getEnvConfig = (envPath) => { OCO_ONE_LINE_COMMIT: parseConfigVarValue(process.env.OCO_ONE_LINE_COMMIT), OCO_TEST_MOCK_TYPE: process.env.OCO_TEST_MOCK_TYPE, OCO_OMIT_SCOPE: parseConfigVarValue(process.env.OCO_OMIT_SCOPE), + OCO_CUSTOM_PROMPT: process.env.OCO_CUSTOM_PROMPT, OCO_GITPUSH: parseConfigVarValue(process.env.OCO_GITPUSH) // todo: deprecate }; }; var setGlobalConfig = (config7, configPath = defaultConfigPath) => { - (0, import_fs.writeFileSync)(configPath, (0, import_ini.stringify)(config7), "utf8"); + const configContent = Object.entries(config7).filter(([_7, value]) => value !== void 0 && value !== null).map(([key, value]) => { + if (typeof value === "string" && value.includes("\n")) { + return `${key}="${value.replace(/\n/g, "\\n")}"`; + } + if (typeof value === "string" && (value.includes(" ") || value.includes('"') || value.includes("'"))) { + return `${key}="${value.replace(/"/g, '\\"')}"`; + } + return `${key}=${value}`; + }).join("\n"); + (0, import_fs.writeFileSync)(configPath, configContent, "utf8"); }; var getIsGlobalConfigFileExist = (configPath = defaultConfigPath) => { return (0, import_fs.existsSync)(configPath); @@ -51153,8 +50993,28 @@ var getGlobalConfig = (configPath = defaultConfigPath) => { const isGlobalConfigFileExist = getIsGlobalConfigFileExist(configPath); if (!isGlobalConfigFileExist) globalConfig = initGlobalConfig(configPath); else { - const configFile = (0, import_fs.readFileSync)(configPath, "utf8"); - globalConfig = (0, import_ini.parse)(configFile); + dotenv.config({ path: configPath }); + globalConfig = { + OCO_API_KEY: process.env.OCO_API_KEY, + OCO_TOKENS_MAX_INPUT: parseConfigVarValue(process.env.OCO_TOKENS_MAX_INPUT) || DEFAULT_CONFIG.OCO_TOKENS_MAX_INPUT, + OCO_TOKENS_MAX_OUTPUT: parseConfigVarValue(process.env.OCO_TOKENS_MAX_OUTPUT) || DEFAULT_CONFIG.OCO_TOKENS_MAX_OUTPUT, + OCO_API_URL: process.env.OCO_API_URL, + OCO_API_CUSTOM_HEADERS: process.env.OCO_API_CUSTOM_HEADERS, + OCO_DESCRIPTION: parseConfigVarValue(process.env.OCO_DESCRIPTION) || DEFAULT_CONFIG.OCO_DESCRIPTION, + OCO_EMOJI: parseConfigVarValue(process.env.OCO_EMOJI) || DEFAULT_CONFIG.OCO_EMOJI, + OCO_WHY: parseConfigVarValue(process.env.OCO_WHY) || DEFAULT_CONFIG.OCO_WHY, + OCO_MODEL: process.env.OCO_MODEL || DEFAULT_CONFIG.OCO_MODEL, + OCO_LANGUAGE: process.env.OCO_LANGUAGE || DEFAULT_CONFIG.OCO_LANGUAGE, + OCO_MESSAGE_TEMPLATE_PLACEHOLDER: process.env.OCO_MESSAGE_TEMPLATE_PLACEHOLDER || DEFAULT_CONFIG.OCO_MESSAGE_TEMPLATE_PLACEHOLDER, + OCO_PROMPT_MODULE: process.env.OCO_PROMPT_MODULE || DEFAULT_CONFIG.OCO_PROMPT_MODULE, + OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER || DEFAULT_CONFIG.OCO_AI_PROVIDER, + OCO_GITPUSH: parseConfigVarValue(process.env.OCO_GITPUSH) || DEFAULT_CONFIG.OCO_GITPUSH, + OCO_ONE_LINE_COMMIT: parseConfigVarValue(process.env.OCO_ONE_LINE_COMMIT) || DEFAULT_CONFIG.OCO_ONE_LINE_COMMIT, + OCO_OMIT_SCOPE: parseConfigVarValue(process.env.OCO_OMIT_SCOPE) || DEFAULT_CONFIG.OCO_OMIT_SCOPE, + OCO_TEST_MOCK_TYPE: process.env.OCO_TEST_MOCK_TYPE || DEFAULT_CONFIG.OCO_TEST_MOCK_TYPE, + OCO_HOOK_AUTO_UNCOMMENT: parseConfigVarValue(process.env.OCO_HOOK_AUTO_UNCOMMENT) || DEFAULT_CONFIG.OCO_HOOK_AUTO_UNCOMMENT, + OCO_CUSTOM_PROMPT: process.env.OCO_CUSTOM_PROMPT || DEFAULT_CONFIG.OCO_CUSTOM_PROMPT + }; } return globalConfig; }; @@ -51311,6 +51171,11 @@ function getConfigKeyDetails(key) { description: "Automatically uncomment the commit message in the hook", values: ["true", "false"] }; + case "OCO_CUSTOM_PROMPT" /* OCO_CUSTOM_PROMPT */: + return { + description: "Custom prompt to use instead of the default prompt template", + values: ["Any string"] + }; default: return { description: "String value", @@ -67206,6 +67071,11 @@ Consider this context when generating the commit message, incorporating relevant var INIT_MAIN_PROMPT2 = (language, fullGitMojiSpec, context) => ({ role: "system", content: (() => { + if (config4.OCO_CUSTOM_PROMPT) { + const userInputContext2 = userInputCodeContext(context); + return userInputContext2 ? `${config4.OCO_CUSTOM_PROMPT} +${userInputContext2}` : config4.OCO_CUSTOM_PROMPT; + } const commitConvention = fullGitMojiSpec ? "GitMoji specification" : "Conventional Commit Convention"; const missionStatement = `${IDENTITY} Your mission is to create clean and comprehensive commit messages as per the ${commitConvention} and explain WHAT were the changes and mainly WHY the changes were done.`; const diffInstruction = "I'll send you an output of 'git diff --staged' command, and you are to convert it into a commit message."; @@ -67715,7 +67585,7 @@ ${source_default.grey("\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2 process.exit(1); } }; -async function commit(extraArgs2 = [], context = "", isStageAllFlag = false, fullGitMojiSpec = false, skipCommitConfirmation = false) { +async function commit(extraArgs2 = [], context = "", isStageAllFlag = false, fullGitMojiSpec = false, skipCommitConfirmation = false, showPrompt = false) { if (isStageAllFlag) { const changedFiles2 = await getChangedFiles(); if (changedFiles2) await gitAdd({ files: changedFiles2 }); @@ -67730,6 +67600,22 @@ async function commit(extraArgs2 = [], context = "", isStageAllFlag = false, ful ce(source_default.red("No changes detected")); process.exit(1); } + if (showPrompt) { + try { + const messages = await getMainCommitPrompt(fullGitMojiSpec, context); + const systemMessage = messages.find((msg) => msg.role === "system"); + if (systemMessage) { + console.log(systemMessage.content); + } else { + console.error("No system prompt found"); + } + process.exit(0); + } catch (error) { + const err = error; + console.error(`Failed to generate prompt: ${err?.message || err}`); + process.exit(1); + } + } ae("open-commit"); if (errorChangedFiles ?? errorStagedFiles) { ce(`${source_default.red("\u2716")} ${errorChangedFiles ?? errorStagedFiles}`); @@ -68151,6 +68037,11 @@ Z2( alias: "y", description: "Skip commit confirmation prompt", default: false + }, + "show-prompt": { + type: Boolean, + description: "Show the instructional prompt that would be given to the LLM", + default: false } }, ignoreArgv: (type2) => type2 === "unknown-flag" || type2 === "argument", @@ -68162,7 +68053,7 @@ Z2( if (await isHookCalled()) { prepareCommitMessageHook(); } else { - commit(extraArgs, flags.context, false, flags.fgm, flags.yes); + commit(extraArgs, flags.context, false, flags.fgm, flags.yes, flags["show-prompt"]); } }, extraArgs diff --git a/out/github-action.cjs b/out/github-action.cjs index 39b526bb..0545ade9 100644 --- a/out/github-action.cjs +++ b/out/github-action.cjs @@ -24528,185 +24528,6 @@ var require_main2 = __commonJS({ } }); -// node_modules/ini/lib/ini.js -var require_ini = __commonJS({ - "node_modules/ini/lib/ini.js"(exports2, module2) { - var { hasOwnProperty: hasOwnProperty2 } = Object.prototype; - var eol = typeof process !== "undefined" && process.platform === "win32" ? "\r\n" : "\n"; - var encode3 = (obj, opt) => { - const children = []; - let out = ""; - if (typeof opt === "string") { - opt = { - section: opt, - whitespace: false - }; - } else { - opt = opt || /* @__PURE__ */ Object.create(null); - opt.whitespace = opt.whitespace === true; - } - const separator = opt.whitespace ? " = " : "="; - for (const k4 of Object.keys(obj)) { - const val = obj[k4]; - if (val && Array.isArray(val)) { - for (const item of val) { - out += safe(k4 + "[]") + separator + safe(item) + eol; - } - } else if (val && typeof val === "object") { - children.push(k4); - } else { - out += safe(k4) + separator + safe(val) + eol; - } - } - if (opt.section && out.length) { - out = "[" + safe(opt.section) + "]" + eol + out; - } - for (const k4 of children) { - const nk = dotSplit(k4).join("\\."); - const section = (opt.section ? opt.section + "." : "") + nk; - const { whitespace } = opt; - const child = encode3(obj[k4], { - section, - whitespace - }); - if (out.length && child.length) { - out += eol; - } - out += child; - } - return out; - }; - var dotSplit = (str2) => str2.replace(/\1/g, "LITERAL\\1LITERAL").replace(/\\\./g, "").split(/\./).map((part) => part.replace(/\1/g, "\\.").replace(/\2LITERAL\\1LITERAL\2/g, "")); - var decode = (str2) => { - const out = /* @__PURE__ */ Object.create(null); - let p3 = out; - let section = null; - const re2 = /^\[([^\]]*)\]$|^([^=]+)(=(.*))?$/i; - const lines = str2.split(/[\r\n]+/g); - for (const line of lines) { - if (!line || line.match(/^\s*[;#]/)) { - continue; - } - const match = line.match(re2); - if (!match) { - continue; - } - if (match[1] !== void 0) { - section = unsafe(match[1]); - if (section === "__proto__") { - p3 = /* @__PURE__ */ Object.create(null); - continue; - } - p3 = out[section] = out[section] || /* @__PURE__ */ Object.create(null); - continue; - } - const keyRaw = unsafe(match[2]); - const isArray2 = keyRaw.length > 2 && keyRaw.slice(-2) === "[]"; - const key = isArray2 ? keyRaw.slice(0, -2) : keyRaw; - if (key === "__proto__") { - continue; - } - const valueRaw = match[3] ? unsafe(match[4]) : true; - const value = valueRaw === "true" || valueRaw === "false" || valueRaw === "null" ? JSON.parse(valueRaw) : valueRaw; - if (isArray2) { - if (!hasOwnProperty2.call(p3, key)) { - p3[key] = []; - } else if (!Array.isArray(p3[key])) { - p3[key] = [p3[key]]; - } - } - if (Array.isArray(p3[key])) { - p3[key].push(value); - } else { - p3[key] = value; - } - } - const remove = []; - for (const k4 of Object.keys(out)) { - if (!hasOwnProperty2.call(out, k4) || typeof out[k4] !== "object" || Array.isArray(out[k4])) { - continue; - } - const parts = dotSplit(k4); - p3 = out; - const l3 = parts.pop(); - const nl = l3.replace(/\\\./g, "."); - for (const part of parts) { - if (part === "__proto__") { - continue; - } - if (!hasOwnProperty2.call(p3, part) || typeof p3[part] !== "object") { - p3[part] = /* @__PURE__ */ Object.create(null); - } - p3 = p3[part]; - } - if (p3 === out && nl === l3) { - continue; - } - p3[nl] = out[k4]; - remove.push(k4); - } - for (const del of remove) { - delete out[del]; - } - return out; - }; - var isQuoted = (val) => { - return val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'"); - }; - var safe = (val) => { - if (typeof val !== "string" || val.match(/[=\r\n]/) || val.match(/^\[/) || val.length > 1 && isQuoted(val) || val !== val.trim()) { - return JSON.stringify(val); - } - return val.split(";").join("\\;").split("#").join("\\#"); - }; - var unsafe = (val, doUnesc) => { - val = (val || "").trim(); - if (isQuoted(val)) { - if (val.charAt(0) === "'") { - val = val.slice(1, -1); - } - try { - val = JSON.parse(val); - } catch { - } - } else { - let esc = false; - let unesc = ""; - for (let i3 = 0, l3 = val.length; i3 < l3; i3++) { - const c2 = val.charAt(i3); - if (esc) { - if ("\\;#".indexOf(c2) !== -1) { - unesc += c2; - } else { - unesc += "\\" + c2; - } - esc = false; - } else if (";#".indexOf(c2) !== -1) { - break; - } else if (c2 === "\\") { - esc = true; - } else { - unesc += c2; - } - } - if (esc) { - unesc += "\\"; - } - return unesc.trim(); - } - return val; - }; - module2.exports = { - parse: decode, - decode, - stringify: encode3, - encode: encode3, - safe, - unsafe - }; - } -}); - // node_modules/webidl-conversions/lib/index.js var require_lib2 = __commonJS({ "node_modules/webidl-conversions/lib/index.js"(exports2) { @@ -70590,7 +70411,6 @@ function G2(t2, e3) { // src/commands/config.ts var dotenv = __toESM(require_main2(), 1); var import_fs = require("fs"); -var import_ini = __toESM(require_ini(), 1); var import_os = require("os"); var import_path = require("path"); @@ -70884,6 +70704,7 @@ var CONFIG_KEYS = /* @__PURE__ */ ((CONFIG_KEYS2) => { CONFIG_KEYS2["OCO_OMIT_SCOPE"] = "OCO_OMIT_SCOPE"; CONFIG_KEYS2["OCO_GITPUSH"] = "OCO_GITPUSH"; CONFIG_KEYS2["OCO_HOOK_AUTO_UNCOMMENT"] = "OCO_HOOK_AUTO_UNCOMMENT"; + CONFIG_KEYS2["OCO_CUSTOM_PROMPT"] = "OCO_CUSTOM_PROMPT"; return CONFIG_KEYS2; })(CONFIG_KEYS || {}); var MODEL_LIST = { @@ -71635,6 +71456,15 @@ var configValidators = { typeof value === "boolean", "Must be true or false" ); + }, + ["OCO_CUSTOM_PROMPT" /* OCO_CUSTOM_PROMPT */](value) { + if (value === void 0 || value === null) return value; + validateConfig( + "OCO_CUSTOM_PROMPT" /* OCO_CUSTOM_PROMPT */, + typeof value === "string", + "Must be a string" + ); + return value; } }; var OCO_AI_PROVIDER_ENUM = /* @__PURE__ */ ((OCO_AI_PROVIDER_ENUM2) => { @@ -71676,10 +71506,12 @@ var DEFAULT_CONFIG = { OCO_OMIT_SCOPE: false, OCO_GITPUSH: true, // todo: deprecate - OCO_HOOK_AUTO_UNCOMMENT: false + OCO_HOOK_AUTO_UNCOMMENT: false, + OCO_CUSTOM_PROMPT: void 0 }; var initGlobalConfig = (configPath = defaultConfigPath) => { - (0, import_fs.writeFileSync)(configPath, (0, import_ini.stringify)(DEFAULT_CONFIG), "utf8"); + const configContent = Object.entries(DEFAULT_CONFIG).map(([key, value]) => `${key}=${value}`).join("\n"); + (0, import_fs.writeFileSync)(configPath, configContent, "utf8"); return DEFAULT_CONFIG; }; var parseConfigVarValue = (value) => { @@ -71709,12 +71541,22 @@ var getEnvConfig = (envPath) => { OCO_ONE_LINE_COMMIT: parseConfigVarValue(process.env.OCO_ONE_LINE_COMMIT), OCO_TEST_MOCK_TYPE: process.env.OCO_TEST_MOCK_TYPE, OCO_OMIT_SCOPE: parseConfigVarValue(process.env.OCO_OMIT_SCOPE), + OCO_CUSTOM_PROMPT: process.env.OCO_CUSTOM_PROMPT, OCO_GITPUSH: parseConfigVarValue(process.env.OCO_GITPUSH) // todo: deprecate }; }; var setGlobalConfig = (config6, configPath = defaultConfigPath) => { - (0, import_fs.writeFileSync)(configPath, (0, import_ini.stringify)(config6), "utf8"); + const configContent = Object.entries(config6).filter(([_3, value]) => value !== void 0 && value !== null).map(([key, value]) => { + if (typeof value === "string" && value.includes("\n")) { + return `${key}="${value.replace(/\n/g, "\\n")}"`; + } + if (typeof value === "string" && (value.includes(" ") || value.includes('"') || value.includes("'"))) { + return `${key}="${value.replace(/"/g, '\\"')}"`; + } + return `${key}=${value}`; + }).join("\n"); + (0, import_fs.writeFileSync)(configPath, configContent, "utf8"); }; var getIsGlobalConfigFileExist = (configPath = defaultConfigPath) => { return (0, import_fs.existsSync)(configPath); @@ -71724,8 +71566,28 @@ var getGlobalConfig = (configPath = defaultConfigPath) => { const isGlobalConfigFileExist = getIsGlobalConfigFileExist(configPath); if (!isGlobalConfigFileExist) globalConfig = initGlobalConfig(configPath); else { - const configFile = (0, import_fs.readFileSync)(configPath, "utf8"); - globalConfig = (0, import_ini.parse)(configFile); + dotenv.config({ path: configPath }); + globalConfig = { + OCO_API_KEY: process.env.OCO_API_KEY, + OCO_TOKENS_MAX_INPUT: parseConfigVarValue(process.env.OCO_TOKENS_MAX_INPUT) || DEFAULT_CONFIG.OCO_TOKENS_MAX_INPUT, + OCO_TOKENS_MAX_OUTPUT: parseConfigVarValue(process.env.OCO_TOKENS_MAX_OUTPUT) || DEFAULT_CONFIG.OCO_TOKENS_MAX_OUTPUT, + OCO_API_URL: process.env.OCO_API_URL, + OCO_API_CUSTOM_HEADERS: process.env.OCO_API_CUSTOM_HEADERS, + OCO_DESCRIPTION: parseConfigVarValue(process.env.OCO_DESCRIPTION) || DEFAULT_CONFIG.OCO_DESCRIPTION, + OCO_EMOJI: parseConfigVarValue(process.env.OCO_EMOJI) || DEFAULT_CONFIG.OCO_EMOJI, + OCO_WHY: parseConfigVarValue(process.env.OCO_WHY) || DEFAULT_CONFIG.OCO_WHY, + OCO_MODEL: process.env.OCO_MODEL || DEFAULT_CONFIG.OCO_MODEL, + OCO_LANGUAGE: process.env.OCO_LANGUAGE || DEFAULT_CONFIG.OCO_LANGUAGE, + OCO_MESSAGE_TEMPLATE_PLACEHOLDER: process.env.OCO_MESSAGE_TEMPLATE_PLACEHOLDER || DEFAULT_CONFIG.OCO_MESSAGE_TEMPLATE_PLACEHOLDER, + OCO_PROMPT_MODULE: process.env.OCO_PROMPT_MODULE || DEFAULT_CONFIG.OCO_PROMPT_MODULE, + OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER || DEFAULT_CONFIG.OCO_AI_PROVIDER, + OCO_GITPUSH: parseConfigVarValue(process.env.OCO_GITPUSH) || DEFAULT_CONFIG.OCO_GITPUSH, + OCO_ONE_LINE_COMMIT: parseConfigVarValue(process.env.OCO_ONE_LINE_COMMIT) || DEFAULT_CONFIG.OCO_ONE_LINE_COMMIT, + OCO_OMIT_SCOPE: parseConfigVarValue(process.env.OCO_OMIT_SCOPE) || DEFAULT_CONFIG.OCO_OMIT_SCOPE, + OCO_TEST_MOCK_TYPE: process.env.OCO_TEST_MOCK_TYPE || DEFAULT_CONFIG.OCO_TEST_MOCK_TYPE, + OCO_HOOK_AUTO_UNCOMMENT: parseConfigVarValue(process.env.OCO_HOOK_AUTO_UNCOMMENT) || DEFAULT_CONFIG.OCO_HOOK_AUTO_UNCOMMENT, + OCO_CUSTOM_PROMPT: process.env.OCO_CUSTOM_PROMPT || DEFAULT_CONFIG.OCO_CUSTOM_PROMPT + }; } return globalConfig; }; @@ -71882,6 +71744,11 @@ function getConfigKeyDetails(key) { description: "Automatically uncomment the commit message in the hook", values: ["true", "false"] }; + case "OCO_CUSTOM_PROMPT" /* OCO_CUSTOM_PROMPT */: + return { + description: "Custom prompt to use instead of the default prompt template", + values: ["Any string"] + }; default: return { description: "String value", @@ -87777,6 +87644,11 @@ Consider this context when generating the commit message, incorporating relevant var INIT_MAIN_PROMPT2 = (language, fullGitMojiSpec, context2) => ({ role: "system", content: (() => { + if (config4.OCO_CUSTOM_PROMPT) { + const userInputContext2 = userInputCodeContext(context2); + return userInputContext2 ? `${config4.OCO_CUSTOM_PROMPT} +${userInputContext2}` : config4.OCO_CUSTOM_PROMPT; + } const commitConvention = fullGitMojiSpec ? "GitMoji specification" : "Conventional Commit Convention"; const missionStatement = `${IDENTITY} Your mission is to create clean and comprehensive commit messages as per the ${commitConvention} and explain WHAT were the changes and mainly WHY the changes were done.`; const diffInstruction = "I'll send you an output of 'git diff --staged' command, and you are to convert it into a commit message."; diff --git a/package.json b/package.json index 095b67ba..5fc1fdc5 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ }, "devDependencies": { "@commitlint/types": "^17.4.4", - "@types/ini": "^1.3.31", "@types/inquirer": "^9.0.3", "@types/jest": "^29.5.12", "@types/node": "^16.18.14", @@ -99,7 +98,6 @@ "crypto": "^1.0.1", "execa": "^7.0.0", "ignore": "^5.2.4", - "ini": "^3.0.1", "inquirer": "^9.1.4", "openai": "^4.57.0", "punycode": "^2.3.1", @@ -109,4 +107,4 @@ "ajv": "^8.17.1", "whatwg-url": "^14.0.0" } -} +} \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index 215c975c..79520fbe 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -35,6 +35,11 @@ cli( alias: 'y', description: 'Skip commit confirmation prompt', default: false + }, + 'show-prompt': { + type: Boolean, + description: 'Show the instructional prompt that would be given to the LLM', + default: false } }, ignoreArgv: (type) => type === 'unknown-flag' || type === 'argument', @@ -47,7 +52,7 @@ cli( if (await isHookCalled()) { prepareCommitMessageHook(); } else { - commit(extraArgs, flags.context, false, flags.fgm, flags.yes); + commit(extraArgs, flags.context, false, flags.fgm, flags.yes, flags['show-prompt']); } }, extraArgs diff --git a/src/commands/commit.ts b/src/commands/commit.ts index f86016e4..2de8f57c 100644 --- a/src/commands/commit.ts +++ b/src/commands/commit.ts @@ -11,6 +11,7 @@ import { import chalk from 'chalk'; import { execa } from 'execa'; import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff'; +import { getMainCommitPrompt } from '../prompts'; import { assertGitRepo, getChangedFiles, @@ -224,7 +225,8 @@ export async function commit( context: string = '', isStageAllFlag: Boolean = false, fullGitMojiSpec: boolean = false, - skipCommitConfirmation: boolean = false + skipCommitConfirmation: boolean = false, + showPrompt: boolean = false ) { if (isStageAllFlag) { const changedFiles = await getChangedFiles(); @@ -244,6 +246,26 @@ export async function commit( process.exit(1); } + // If showPrompt flag is set, display only the system prompt and exit + if (showPrompt) { + try { + const messages = await getMainCommitPrompt(fullGitMojiSpec, context); + const systemMessage = messages.find(msg => msg.role === 'system'); + + if (systemMessage) { + console.log(systemMessage.content); + } else { + console.error('No system prompt found'); + } + + process.exit(0); + } catch (error) { + const err = error as Error; + console.error(`Failed to generate prompt: ${err?.message || err}`); + process.exit(1); + } + } + intro('open-commit'); if (errorChangedFiles ?? errorStagedFiles) { outro(`${chalk.red('✖')} ${errorChangedFiles ?? errorStagedFiles}`); diff --git a/src/commands/config.ts b/src/commands/config.ts index 8bf98c36..9719ba73 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -3,7 +3,7 @@ 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'; +// Removed ini import - now using dotenv format for global config import { homedir } from 'os'; import { join as pathJoin, resolve as pathResolve } from 'path'; import { COMMANDS } from './ENUMS'; @@ -28,7 +28,8 @@ export enum CONFIG_KEYS { OCO_API_CUSTOM_HEADERS = 'OCO_API_CUSTOM_HEADERS', OCO_OMIT_SCOPE = 'OCO_OMIT_SCOPE', OCO_GITPUSH = 'OCO_GITPUSH', // todo: deprecate - OCO_HOOK_AUTO_UNCOMMENT = 'OCO_HOOK_AUTO_UNCOMMENT' + OCO_HOOK_AUTO_UNCOMMENT = 'OCO_HOOK_AUTO_UNCOMMENT', + OCO_CUSTOM_PROMPT = 'OCO_CUSTOM_PROMPT' } export enum CONFIG_MODES { @@ -827,6 +828,18 @@ export const configValidators = { typeof value === 'boolean', 'Must be true or false' ); + }, + + [CONFIG_KEYS.OCO_CUSTOM_PROMPT](value: any) { + if (value === undefined || value === null) return value; + + validateConfig( + CONFIG_KEYS.OCO_CUSTOM_PROMPT, + typeof value === 'string', + 'Must be a string' + ); + + return value; } }; @@ -865,6 +878,7 @@ export type ConfigType = { [CONFIG_KEYS.OCO_OMIT_SCOPE]: boolean; [CONFIG_KEYS.OCO_TEST_MOCK_TYPE]: string; [CONFIG_KEYS.OCO_HOOK_AUTO_UNCOMMENT]: boolean; + [CONFIG_KEYS.OCO_CUSTOM_PROMPT]?: string; }; export const defaultConfigPath = pathJoin(homedir(), '.opencommit'); @@ -913,11 +927,15 @@ export const DEFAULT_CONFIG = { OCO_WHY: false, OCO_OMIT_SCOPE: false, OCO_GITPUSH: true, // todo: deprecate - OCO_HOOK_AUTO_UNCOMMENT: false + OCO_HOOK_AUTO_UNCOMMENT: false, + OCO_CUSTOM_PROMPT: undefined }; const initGlobalConfig = (configPath: string = defaultConfigPath) => { - writeFileSync(configPath, iniStringify(DEFAULT_CONFIG), 'utf8'); + const configContent = Object.entries(DEFAULT_CONFIG) + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + writeFileSync(configPath, configContent, 'utf8'); return DEFAULT_CONFIG; }; @@ -953,6 +971,7 @@ const getEnvConfig = (envPath: string) => { OCO_ONE_LINE_COMMIT: parseConfigVarValue(process.env.OCO_ONE_LINE_COMMIT), OCO_TEST_MOCK_TYPE: process.env.OCO_TEST_MOCK_TYPE, OCO_OMIT_SCOPE: parseConfigVarValue(process.env.OCO_OMIT_SCOPE), + OCO_CUSTOM_PROMPT: process.env.OCO_CUSTOM_PROMPT, OCO_GITPUSH: parseConfigVarValue(process.env.OCO_GITPUSH) // todo: deprecate }; @@ -962,7 +981,21 @@ export const setGlobalConfig = ( config: ConfigType, configPath: string = defaultConfigPath ) => { - writeFileSync(configPath, iniStringify(config), 'utf8'); + const configContent = Object.entries(config) + .filter(([_, value]) => value !== undefined && value !== null) + .map(([key, value]) => { + // Handle multiline strings by escaping newlines + if (typeof value === 'string' && value.includes('\n')) { + return `${key}="${value.replace(/\n/g, '\\n')}"`; + } + // Handle strings with special characters + if (typeof value === 'string' && (value.includes(' ') || value.includes('"') || value.includes("'"))) { + return `${key}="${value.replace(/"/g, '\\"')}"`; + } + return `${key}=${value}`; + }) + .join('\n'); + writeFileSync(configPath, configContent, 'utf8'); }; export const getIsGlobalConfigFileExist = ( @@ -977,8 +1010,31 @@ export const getGlobalConfig = (configPath: string = defaultConfigPath) => { const isGlobalConfigFileExist = getIsGlobalConfigFileExist(configPath); if (!isGlobalConfigFileExist) globalConfig = initGlobalConfig(configPath); else { - const configFile = readFileSync(configPath, 'utf8'); - globalConfig = iniParse(configFile) as ConfigType; + // Use dotenv to parse the global config file + dotenv.config({ path: configPath }); + + // Extract the config values from process.env + globalConfig = { + OCO_API_KEY: process.env.OCO_API_KEY, + OCO_TOKENS_MAX_INPUT: parseConfigVarValue(process.env.OCO_TOKENS_MAX_INPUT) || DEFAULT_CONFIG.OCO_TOKENS_MAX_INPUT, + OCO_TOKENS_MAX_OUTPUT: parseConfigVarValue(process.env.OCO_TOKENS_MAX_OUTPUT) || DEFAULT_CONFIG.OCO_TOKENS_MAX_OUTPUT, + OCO_API_URL: process.env.OCO_API_URL, + OCO_API_CUSTOM_HEADERS: process.env.OCO_API_CUSTOM_HEADERS, + OCO_DESCRIPTION: parseConfigVarValue(process.env.OCO_DESCRIPTION) || DEFAULT_CONFIG.OCO_DESCRIPTION, + OCO_EMOJI: parseConfigVarValue(process.env.OCO_EMOJI) || DEFAULT_CONFIG.OCO_EMOJI, + OCO_WHY: parseConfigVarValue(process.env.OCO_WHY) || DEFAULT_CONFIG.OCO_WHY, + OCO_MODEL: process.env.OCO_MODEL || DEFAULT_CONFIG.OCO_MODEL, + OCO_LANGUAGE: process.env.OCO_LANGUAGE || DEFAULT_CONFIG.OCO_LANGUAGE, + OCO_MESSAGE_TEMPLATE_PLACEHOLDER: process.env.OCO_MESSAGE_TEMPLATE_PLACEHOLDER || DEFAULT_CONFIG.OCO_MESSAGE_TEMPLATE_PLACEHOLDER, + OCO_PROMPT_MODULE: (process.env.OCO_PROMPT_MODULE as OCO_PROMPT_MODULE_ENUM) || DEFAULT_CONFIG.OCO_PROMPT_MODULE, + OCO_AI_PROVIDER: (process.env.OCO_AI_PROVIDER as OCO_AI_PROVIDER_ENUM) || DEFAULT_CONFIG.OCO_AI_PROVIDER, + OCO_GITPUSH: parseConfigVarValue(process.env.OCO_GITPUSH) || DEFAULT_CONFIG.OCO_GITPUSH, + OCO_ONE_LINE_COMMIT: parseConfigVarValue(process.env.OCO_ONE_LINE_COMMIT) || DEFAULT_CONFIG.OCO_ONE_LINE_COMMIT, + OCO_OMIT_SCOPE: parseConfigVarValue(process.env.OCO_OMIT_SCOPE) || DEFAULT_CONFIG.OCO_OMIT_SCOPE, + OCO_TEST_MOCK_TYPE: process.env.OCO_TEST_MOCK_TYPE || DEFAULT_CONFIG.OCO_TEST_MOCK_TYPE, + OCO_HOOK_AUTO_UNCOMMENT: parseConfigVarValue(process.env.OCO_HOOK_AUTO_UNCOMMENT) || DEFAULT_CONFIG.OCO_HOOK_AUTO_UNCOMMENT, + OCO_CUSTOM_PROMPT: process.env.OCO_CUSTOM_PROMPT || DEFAULT_CONFIG.OCO_CUSTOM_PROMPT + } as ConfigType; } return globalConfig; @@ -1170,6 +1226,11 @@ function getConfigKeyDetails(key) { description: 'Automatically uncomment the commit message in the hook', values: ['true', 'false'] }; + case CONFIG_KEYS.OCO_CUSTOM_PROMPT: + return { + description: 'Custom prompt to use instead of the default prompt template', + values: ['Any string'] + }; default: return { description: 'String value', diff --git a/src/prompts.ts b/src/prompts.ts index 7967a58f..630ebe49 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -137,6 +137,13 @@ const INIT_MAIN_PROMPT = ( ): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({ role: 'system', content: (() => { + // If a custom prompt is configured, use it instead of the default prompt + if (config.OCO_CUSTOM_PROMPT) { + const userInputContext = userInputCodeContext(context); + return userInputContext ? `${config.OCO_CUSTOM_PROMPT}\n${userInputContext}` : config.OCO_CUSTOM_PROMPT; + } + + // Default prompt logic const commitConvention = fullGitMojiSpec ? 'GitMoji specification' : 'Conventional Commit Convention';