Compare commits

..

26 Commits

Author SHA1 Message Date
di-sukharev
676c2b20c1 update package-lock.json 2024-08-27 16:58:22 +03:00
di-sukharev
21da102f6d Merge remote-tracking branch 'origin/dev' into refactoring_v1 2024-08-27 16:57:54 +03:00
di-sukharev
6ae7ce2720 chore(config.ts): remove debug console log to clean up the code and improve readability 2024-08-27 16:49:17 +03:00
di-sukharev
e44610fea3 chore(.gitignore): add notes.md to .gitignore to prevent tracking of notes files 2024-08-27 16:49:03 +03:00
Takanori Matsumoto
68c9ed359c fix(dependencies): update tr46, webidl-conversions, and whatwg-url to latest versions for compatibility and security improvements (#395) 2024-08-26 10:28:34 +03:00
di-sukharev
9dcb264420 test(config.test.ts): refactor generateConfig function to accept an object for content to improve readability and maintainability 2024-08-20 21:36:00 +03:00
di-sukharev
dd7fdba94e fix(config.ts): revert OCO_GITPUSH to its original position in the config object for clarity
refactor(config.ts): rename configFromEnv to envConfig for better readability
refactor(gemini.ts): simplify client initialization in the Gemini constructor
test(config.test.ts): add test case to check overriding global config with null values in local .env
test(gemini.test.ts): update AI provider assignment to use OCO_AI_PROVIDER_ENUM for consistency
2024-08-20 21:32:16 +03:00
di-sukharev
5fa12e2d4a feat(config): export OCO_AI_PROVIDER_ENUM to allow external access to AI provider constants
refactor(config): simplify mergeObjects function to improve readability and maintainability
refactor(setConfig): remove unnecessary keysToSet variable to streamline logging
refactor(engine): update switch cases to use OCO_AI_PROVIDER_ENUM for better consistency and clarity
2024-08-20 15:37:41 +03:00
di-sukharev
42a36492ad build 2024-08-20 15:37:33 +03:00
di-sukharev
443d27fc8d chore(docs): mark "Push to git" section in README as deprecated to inform users of upcoming changes
refactor(commit.ts): remove early return for non-pushing users to streamline commit process
refactor(config.ts): add deprecation comments for OCO_GITPUSH to indicate future removal
test(config.test.ts): enhance tests to ensure correct handling of local and global config priorities
test(gemini.test.ts): improve tests for Gemini class to ensure proper functionality and error handling
2024-08-20 15:34:09 +03:00
di-sukharev
04991dd00f fix(engine.ts): include DEFAULT_CONFIG in Gemini and Azure engine instantiation to ensure consistent configuration across engines 2024-08-20 12:58:00 +03:00
di-sukharev
3ded6062c1 fix: remove optional chaining from config access to ensure compatibility and prevent potential runtime errors
refactor(flowise.ts, ollama.ts): update axios client configuration to use a consistent URL format for API requests
fix: update README example to reflect the removal of optional chaining in config access
2024-08-20 12:32:40 +03:00
di-sukharev
f8584e7b78 refactor(engine): rename basePath to baseURL for consistency across interfaces and implementations
fix(engine): update Azure and Flowise engines to use baseURL instead of basePath for API configuration
fix(engine): adjust Ollama engine to handle baseURL and fallback to default URL
style(engine): clean up constructor formatting in OpenAiEngine for better readability
chore(engine): update getEngine function to use baseURL in configuration for all engines
2024-08-20 12:21:13 +03:00
di-sukharev
94faceefd3 remove mb confusing line 2024-08-20 12:06:01 +03:00
di-sukharev
720cd6f9c1 clear readme 2024-08-20 12:05:15 +03:00
di-sukharev
b6a92d557f docs(README.md): update author section and clarify API key storage details
docs(README.md): improve instructions for using OpenCommit CLI and configuration
fix(README.md): correct default model name to gpt-4o-mini in usage examples
fix(package.json): update openai package version to 4.56.0 for compatibility
2024-08-20 12:04:07 +03:00
di-sukharev
71354e4687 feat: add CommandsEnum to define command constants for better maintainability
refactor(generateCommitMessageFromGitDiff): update types for OpenAI messages to improve type safety
fix(commitlint/config): remove optional chaining for OCO_LANGUAGE to ensure proper access
refactor(commitlint/prompts): update types for OpenAI messages to improve type safety
refactor(prompts): update types for OpenAI messages to improve type safety
2024-08-20 12:03:40 +03:00
di-sukharev
8f85ee8f8e refactor(testAi.ts): update import statements to use OpenAI type for better clarity and maintainability
fix(testAi.ts): change parameter type in generateCommitMessage method to align with OpenAI's updated type definitions
2024-08-20 12:01:51 +03:00
di-sukharev
f9103a3c6a build 2024-08-20 12:01:38 +03:00
di-sukharev
4afd7de7a8 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
2024-08-20 12:01:14 +03:00
di-sukharev
5cfa3cded2 feat(engine): refactor AI engine interfaces and implementations to support multiple AI providers and improve configurability
- Introduce `AiEngineConfig` interface for consistent configuration across AI engines.
- Update `generateCommitMessage` method signatures to use `OpenAIClient.Chat.Completions.ChatCompletionMessageParam`.
- Implement specific configurations for each AI provider (Anthropic, Azure, Gemini, Ollama, OpenAI) to enhance flexibility.
- Replace hardcoded values with configurable parameters for model, API key, and token limits.
- Refactor client initialization to use Axios instances for better HTTP request handling.
- Remove deprecated code and improve error handling for better user feedback.
2024-08-20 11:58:19 +03:00
di-sukharev
bb0b0e804e build 2024-08-20 11:56:44 +03:00
di-sukharev
5d87cc514b feat(ENUMS.ts): add ENUMS file to centralize command constants
refactor(commitlint.ts): update import path to use ENUMS for command constants
refactor(config.ts): update import path to use ENUMS for command constants
refactor(githook.ts): update import path to use ENUMS for command constants
fix(prompts.ts): correct conventional commit keywords instruction text
2024-08-19 14:09:27 +03:00
di-sukharev
6f4e8fde93 docs(README.md): update usage examples to remove redundant 'opencommit' command
chore(example.txt): remove unused example.txt file
fix(config.ts): correct import order and improve validation messages
fix(githook.ts): improve error message for unsupported mode
fix(azure.ts): add non-null assertion for message content
fix(gemini.ts): use strict equality for role comparison
refactor(generateCommitMessageFromGitDiff.ts): reorder imports for consistency
refactor(github-action.ts): reorder imports for consistency
refactor(prompts.ts): simplify prompt content generation and improve readability
style(engine.ts): fix inconsistent spacing and import order
2024-08-19 14:00:08 +03:00
di-sukharev
745bb5218f update imports 2024-08-19 13:09:46 +03:00
di-sukharev
eb3be62a4f 3.0.20 2024-08-19 12:34:35 +03:00
14 changed files with 385 additions and 580 deletions

View File

@@ -162,16 +162,6 @@ oco config set OCO_EMOJI=false
Other config options are behaving the same.
### Output WHY the changes were done (WIP)
You can set the `OCO_WHY` config to `true` to have OpenCommit output a short description of WHY the changes were done after the commit message. Default is `false`.
To make this perform accurate we must store 'what files do' in some kind of an index or embedding and perform a lookup (kinda RAG) for the accurate git commit message. If you feel like building this comment on this ticket https://github.com/di-sukharev/opencommit/issues/398 and let's go from there together.
```sh
oco config set OCO_WHY=true
```
### Switch to GPT-4 or other models
By default, OpenCommit uses `gpt-4o-mini` model.

View File

@@ -25193,7 +25193,7 @@ function G3(t2, e3) {
// package.json
var package_default = {
name: "opencommit",
version: "3.1.0",
version: "3.0.20",
description: "Auto-generate impressive commits in 1 second. Killing lame commits with AI \u{1F92F}\u{1F52B}",
keywords: [
"git",
@@ -27752,7 +27752,6 @@ var CONFIG_KEYS = /* @__PURE__ */ ((CONFIG_KEYS2) => {
CONFIG_KEYS2["OCO_EMOJI"] = "OCO_EMOJI";
CONFIG_KEYS2["OCO_MODEL"] = "OCO_MODEL";
CONFIG_KEYS2["OCO_LANGUAGE"] = "OCO_LANGUAGE";
CONFIG_KEYS2["OCO_WHY"] = "OCO_WHY";
CONFIG_KEYS2["OCO_MESSAGE_TEMPLATE_PLACEHOLDER"] = "OCO_MESSAGE_TEMPLATE_PLACEHOLDER";
CONFIG_KEYS2["OCO_PROMPT_MODULE"] = "OCO_PROMPT_MODULE";
CONFIG_KEYS2["OCO_AI_PROVIDER"] = "OCO_AI_PROVIDER";
@@ -28030,25 +28029,44 @@ var configValidators = {
};
var defaultConfigPath = (0, import_path.join)((0, import_os.homedir)(), ".opencommit");
var defaultEnvPath = (0, import_path.resolve)(process.cwd(), ".env");
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_WHY: false,
OCO_GITPUSH: true
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 = (configPath = defaultConfigPath) => {
(0, import_fs.writeFileSync)(configPath, (0, import_ini.stringify)(DEFAULT_CONFIG), "utf8");
return DEFAULT_CONFIG;
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 parseEnvVarValue = (value) => {
try {
@@ -28057,9 +28075,12 @@ var parseEnvVarValue = (value) => {
return value;
}
};
var getEnvConfig = (envPath) => {
var getConfig = ({
configPath = defaultConfigPath,
envPath = defaultEnvPath
} = {}) => {
dotenv.config({ path: envPath });
return {
const envConfig = {
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,
@@ -28083,35 +28104,23 @@ var getEnvConfig = (envPath) => {
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(configPath);
globalConfig = initGlobalConfig();
else {
const configFile = (0, import_fs.readFileSync)(configPath, "utf8");
globalConfig = (0, import_ini.parse)(configFile);
}
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);
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 config7;
};
var setConfig = (keyValues, globalConfigPath = defaultConfigPath) => {
const config7 = getConfig({
globalPath: globalConfigPath
});
var setConfig = (keyValues, configPath = defaultConfigPath) => {
const config7 = getConfig();
for (let [key, value] of keyValues) {
if (!configValidators.hasOwnProperty(key)) {
const supportedKeys = Object.keys(configValidators).join("\n");
@@ -28135,7 +28144,8 @@ For more help refer to our docs: https://github.com/di-sukharev/opencommit`
);
config7[key] = validValue;
}
(0, import_fs.writeFileSync)(globalConfigPath, (0, import_ini.stringify)(config7), "utf8");
(0, import_fs.writeFileSync)(configPath, (0, import_ini.stringify)(config7), "utf8");
assertConfigsAreValid(config7);
ce(`${source_default.green("\u2714")} config successfully set`);
};
var configCommand = G3(
@@ -42390,7 +42400,7 @@ var OpenAiEngine = class {
function getEngine() {
const config7 = getConfig();
const provider = config7.OCO_AI_PROVIDER;
const DEFAULT_CONFIG2 = {
const DEFAULT_CONFIG = {
model: config7.OCO_MODEL,
maxTokensOutput: config7.OCO_TOKENS_MAX_OUTPUT,
maxTokensInput: config7.OCO_TOKENS_MAX_INPUT,
@@ -42399,37 +42409,37 @@ function getEngine() {
switch (provider) {
case "ollama" /* OLLAMA */:
return new OllamaAi({
...DEFAULT_CONFIG2,
...DEFAULT_CONFIG,
apiKey: "",
baseURL: config7.OCO_OLLAMA_API_URL
});
case "anthropic" /* ANTHROPIC */:
return new AnthropicEngine({
...DEFAULT_CONFIG2,
...DEFAULT_CONFIG,
apiKey: config7.OCO_ANTHROPIC_API_KEY
});
case "test" /* TEST */:
return new TestAi(config7.OCO_TEST_MOCK_TYPE);
case "gemini" /* GEMINI */:
return new Gemini({
...DEFAULT_CONFIG2,
...DEFAULT_CONFIG,
apiKey: config7.OCO_GEMINI_API_KEY,
baseURL: config7.OCO_GEMINI_BASE_PATH
});
case "azure" /* AZURE */:
return new AzureEngine({
...DEFAULT_CONFIG2,
...DEFAULT_CONFIG,
apiKey: config7.OCO_AZURE_API_KEY
});
case "flowise" /* FLOWISE */:
return new FlowiseAi({
...DEFAULT_CONFIG2,
baseURL: config7.OCO_FLOWISE_ENDPOINT || DEFAULT_CONFIG2.baseURL,
...DEFAULT_CONFIG,
baseURL: config7.OCO_FLOWISE_ENDPOINT || DEFAULT_CONFIG.baseURL,
apiKey: config7.OCO_FLOWISE_API_KEY
});
default:
return new OpenAiEngine({
...DEFAULT_CONFIG2,
...DEFAULT_CONFIG,
apiKey: config7.OCO_OPENAI_API_KEY
});
}
@@ -42570,7 +42580,7 @@ Example Git Diff is to follow:`
];
var INIT_MAIN_PROMPT = (language, prompts) => ({
role: "system",
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes ${config2.OCO_WHY ? "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.
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.
${config2.OCO_EMOJI ? "Use GitMoji convention to preface the commit." : "Do not preface the commit with anything."}
${config2.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.
@@ -42590,23 +42600,12 @@ var commitlintPrompts = {
// src/modules/commitlint/pwd-commitlint.ts
var import_promises = __toESM(require("fs/promises"), 1);
var import_path3 = __toESM(require("path"), 1);
var findModulePath = (moduleName) => {
const searchPaths = [
import_path3.default.join("node_modules", moduleName),
import_path3.default.join("node_modules", ".pnpm")
];
for (const basePath of searchPaths) {
try {
const resolvedPath = require.resolve(moduleName, { paths: [basePath] });
return resolvedPath;
} catch {
}
}
throw new Error(`Cannot find module ${moduleName}`);
};
var getCommitLintModuleType = async () => {
const packageFile = "@commitlint/load/package.json";
const packageJsonPath = findModulePath(packageFile);
const packageFile = "node_modules/@commitlint/load/package.json";
const packageJsonPath = import_path3.default.join(
process.env.PWD || process.cwd(),
packageFile
);
const packageJson = JSON.parse(await import_promises.default.readFile(packageJsonPath, "utf8"));
if (!packageJson) {
throw new Error(`Failed to parse ${packageFile}`);
@@ -42614,15 +42613,21 @@ var getCommitLintModuleType = async () => {
return packageJson.type === "module" ? "esm" : "cjs";
};
var getCommitLintPWDConfig = async () => {
let load, modulePath;
let load, nodeModulesPath;
switch (await getCommitLintModuleType()) {
case "cjs":
modulePath = findModulePath("@commitlint/load");
load = require(modulePath).default;
nodeModulesPath = import_path3.default.join(
process.env.PWD || process.cwd(),
"node_modules/@commitlint/load"
);
load = require(nodeModulesPath).default;
break;
case "esm":
modulePath = await findModulePath("@commitlint/load/lib/load.js");
load = (await import(modulePath)).default;
nodeModulesPath = import_path3.default.join(
process.env.PWD || process.cwd(),
"node_modules/@commitlint/load/lib/load.js"
);
load = (await import(nodeModulesPath)).default;
break;
}
if (load && typeof load === "function") {
@@ -43167,8 +43172,8 @@ var generateCommitMessageFromGitDiff = async ({
skipCommitConfirmation = false
}) => {
await assertGitRepo();
const commitGenerationSpinner = le();
commitGenerationSpinner.start("Generating the commit message");
const commitSpinner = le();
commitSpinner.start("Generating the commit message");
try {
let commitMessage = await generateCommitMessageByDiff(
diff,
@@ -43183,7 +43188,7 @@ var generateCommitMessageFromGitDiff = async ({
commitMessage
);
}
commitGenerationSpinner.stop("\u{1F4DD} Commit message generated");
commitSpinner.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")}
@@ -43193,20 +43198,14 @@ ${source_default.grey("\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2
const isCommitConfirmedByUser = skipCommitConfirmation || await Q3({
message: "Confirm the commit message?"
});
if (hD2(isCommitConfirmedByUser))
process.exit(1);
if (isCommitConfirmedByUser) {
const committingChangesSpinner = le();
committingChangesSpinner.start("Committing the changes");
if (isCommitConfirmedByUser && !hD2(isCommitConfirmedByUser)) {
const { stdout } = await execa("git", [
"commit",
"-m",
commitMessage,
...extraArgs2
]);
committingChangesSpinner.stop(
`${source_default.green("\u2714")} Successfully committed`
);
ce(`${source_default.green("\u2714")} Successfully committed`);
ce(stdout);
const remotes = await getGitRemotes();
if (!remotes.length) {
@@ -43219,9 +43218,7 @@ ${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 (hD2(isPushConfirmedByUser))
process.exit(1);
if (isPushConfirmedByUser) {
if (isPushConfirmedByUser && !hD2(isPushConfirmedByUser)) {
const pushSpinner = le();
pushSpinner.start(`Running 'git push ${remotes[0]}'`);
const { stdout: stdout2 } = await execa("git", [
@@ -43243,26 +43240,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))
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 (!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`);
}
} else {
}
if (!isCommitConfirmedByUser && !hD2(isCommitConfirmedByUser)) {
const regenerateMessage = await Q3({
message: "Do you want to regenerate the message?"
});
if (hD2(regenerateMessage))
process.exit(1);
if (regenerateMessage) {
if (regenerateMessage && !hD2(isCommitConfirmedByUser)) {
await generateCommitMessageFromGitDiff({
diff,
extraArgs: extraArgs2,
@@ -43271,7 +43268,7 @@ ${source_default.grey("\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2
}
}
} catch (error) {
commitGenerationSpinner.stop("\u{1F4DD} Commit message generated");
commitSpinner.stop("\u{1F4DD} Commit message generated");
const err = error;
ce(`${source_default.red("\u2716")} ${err?.message || err}`);
process.exit(1);
@@ -43305,9 +43302,7 @@ async function commit(extraArgs2 = [], isStageAllFlag = false, fullGitMojiSpec =
const isStageAllAndCommitConfirmedByUser = await Q3({
message: "Do you want to stage all files and generate commit message?"
});
if (hD2(isStageAllAndCommitConfirmedByUser))
process.exit(1);
if (isStageAllAndCommitConfirmedByUser) {
if (isStageAllAndCommitConfirmedByUser && !hD2(isStageAllAndCommitConfirmedByUser)) {
await commit(extraArgs2, true, fullGitMojiSpec);
process.exit(1);
}
@@ -43357,7 +43352,7 @@ var commitlintConfigCommand = G3(
const { mode } = argv._;
if (mode === "get" /* get */) {
const commitLintConfig = await getCommitlintLLMConfig();
ce(JSON.stringify(commitLintConfig, null, 2));
ce(commitLintConfig.toString());
return;
}
if (mode === "force" /* force */) {

View File

@@ -46564,7 +46564,6 @@ var CONFIG_KEYS = /* @__PURE__ */ ((CONFIG_KEYS2) => {
CONFIG_KEYS2["OCO_EMOJI"] = "OCO_EMOJI";
CONFIG_KEYS2["OCO_MODEL"] = "OCO_MODEL";
CONFIG_KEYS2["OCO_LANGUAGE"] = "OCO_LANGUAGE";
CONFIG_KEYS2["OCO_WHY"] = "OCO_WHY";
CONFIG_KEYS2["OCO_MESSAGE_TEMPLATE_PLACEHOLDER"] = "OCO_MESSAGE_TEMPLATE_PLACEHOLDER";
CONFIG_KEYS2["OCO_PROMPT_MODULE"] = "OCO_PROMPT_MODULE";
CONFIG_KEYS2["OCO_AI_PROVIDER"] = "OCO_AI_PROVIDER";
@@ -46842,25 +46841,44 @@ var configValidators = {
};
var defaultConfigPath = (0, import_path.join)((0, import_os.homedir)(), ".opencommit");
var defaultEnvPath = (0, import_path.resolve)(process.cwd(), ".env");
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_WHY: false,
OCO_GITPUSH: true
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 = (configPath = defaultConfigPath) => {
(0, import_fs.writeFileSync)(configPath, (0, import_ini.stringify)(DEFAULT_CONFIG), "utf8");
return DEFAULT_CONFIG;
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 parseEnvVarValue = (value) => {
try {
@@ -46869,9 +46887,12 @@ var parseEnvVarValue = (value) => {
return value;
}
};
var getEnvConfig = (envPath) => {
var getConfig = ({
configPath = defaultConfigPath,
envPath = defaultEnvPath
} = {}) => {
dotenv.config({ path: envPath });
return {
const envConfig = {
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,
@@ -46895,35 +46916,23 @@ var getEnvConfig = (envPath) => {
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(configPath);
globalConfig = initGlobalConfig();
else {
const configFile = (0, import_fs.readFileSync)(configPath, "utf8");
globalConfig = (0, import_ini.parse)(configFile);
}
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);
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 config6;
};
var setConfig = (keyValues, globalConfigPath = defaultConfigPath) => {
const config6 = getConfig({
globalPath: globalConfigPath
});
var setConfig = (keyValues, configPath = defaultConfigPath) => {
const config6 = getConfig();
for (let [key, value] of keyValues) {
if (!configValidators.hasOwnProperty(key)) {
const supportedKeys = Object.keys(configValidators).join("\n");
@@ -46947,7 +46956,8 @@ For more help refer to our docs: https://github.com/di-sukharev/opencommit`
);
config6[key] = validValue;
}
(0, import_fs.writeFileSync)(globalConfigPath, (0, import_ini.stringify)(config6), "utf8");
(0, import_fs.writeFileSync)(configPath, (0, import_ini.stringify)(config6), "utf8");
assertConfigsAreValid(config6);
ce(`${source_default.green("\u2714")} config successfully set`);
};
var configCommand = G2(
@@ -61202,7 +61212,7 @@ var OpenAiEngine = class {
function getEngine() {
const config6 = getConfig();
const provider = config6.OCO_AI_PROVIDER;
const DEFAULT_CONFIG2 = {
const DEFAULT_CONFIG = {
model: config6.OCO_MODEL,
maxTokensOutput: config6.OCO_TOKENS_MAX_OUTPUT,
maxTokensInput: config6.OCO_TOKENS_MAX_INPUT,
@@ -61211,37 +61221,37 @@ function getEngine() {
switch (provider) {
case "ollama" /* OLLAMA */:
return new OllamaAi({
...DEFAULT_CONFIG2,
...DEFAULT_CONFIG,
apiKey: "",
baseURL: config6.OCO_OLLAMA_API_URL
});
case "anthropic" /* ANTHROPIC */:
return new AnthropicEngine({
...DEFAULT_CONFIG2,
...DEFAULT_CONFIG,
apiKey: config6.OCO_ANTHROPIC_API_KEY
});
case "test" /* TEST */:
return new TestAi(config6.OCO_TEST_MOCK_TYPE);
case "gemini" /* GEMINI */:
return new Gemini({
...DEFAULT_CONFIG2,
...DEFAULT_CONFIG,
apiKey: config6.OCO_GEMINI_API_KEY,
baseURL: config6.OCO_GEMINI_BASE_PATH
});
case "azure" /* AZURE */:
return new AzureEngine({
...DEFAULT_CONFIG2,
...DEFAULT_CONFIG,
apiKey: config6.OCO_AZURE_API_KEY
});
case "flowise" /* FLOWISE */:
return new FlowiseAi({
...DEFAULT_CONFIG2,
baseURL: config6.OCO_FLOWISE_ENDPOINT || DEFAULT_CONFIG2.baseURL,
...DEFAULT_CONFIG,
baseURL: config6.OCO_FLOWISE_ENDPOINT || DEFAULT_CONFIG.baseURL,
apiKey: config6.OCO_FLOWISE_API_KEY
});
default:
return new OpenAiEngine({
...DEFAULT_CONFIG2,
...DEFAULT_CONFIG,
apiKey: config6.OCO_OPENAI_API_KEY
});
}
@@ -61382,7 +61392,7 @@ Example Git Diff is to follow:`
];
var INIT_MAIN_PROMPT = (language, prompts) => ({
role: "system",
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes ${config2.OCO_WHY ? "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.
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.
${config2.OCO_EMOJI ? "Use GitMoji convention to preface the commit." : "Do not preface the commit with anything."}
${config2.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.
@@ -61402,23 +61412,12 @@ var commitlintPrompts = {
// src/modules/commitlint/pwd-commitlint.ts
var import_promises = __toESM(require("fs/promises"), 1);
var import_path3 = __toESM(require("path"), 1);
var findModulePath = (moduleName) => {
const searchPaths = [
import_path3.default.join("node_modules", moduleName),
import_path3.default.join("node_modules", ".pnpm")
];
for (const basePath of searchPaths) {
try {
const resolvedPath = require.resolve(moduleName, { paths: [basePath] });
return resolvedPath;
} catch {
}
}
throw new Error(`Cannot find module ${moduleName}`);
};
var getCommitLintModuleType = async () => {
const packageFile = "@commitlint/load/package.json";
const packageJsonPath = findModulePath(packageFile);
const packageFile = "node_modules/@commitlint/load/package.json";
const packageJsonPath = import_path3.default.join(
process.env.PWD || process.cwd(),
packageFile
);
const packageJson = JSON.parse(await import_promises.default.readFile(packageJsonPath, "utf8"));
if (!packageJson) {
throw new Error(`Failed to parse ${packageFile}`);
@@ -61426,15 +61425,21 @@ var getCommitLintModuleType = async () => {
return packageJson.type === "module" ? "esm" : "cjs";
};
var getCommitLintPWDConfig = async () => {
let load, modulePath;
let load, nodeModulesPath;
switch (await getCommitLintModuleType()) {
case "cjs":
modulePath = findModulePath("@commitlint/load");
load = require(modulePath).default;
nodeModulesPath = import_path3.default.join(
process.env.PWD || process.cwd(),
"node_modules/@commitlint/load"
);
load = require(nodeModulesPath).default;
break;
case "esm":
modulePath = await findModulePath("@commitlint/load/lib/load.js");
load = (await import(modulePath)).default;
nodeModulesPath = import_path3.default.join(
process.env.PWD || process.cwd(),
"node_modules/@commitlint/load/lib/load.js"
);
load = (await import(nodeModulesPath)).default;
break;
}
if (load && typeof load === "function") {

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "opencommit",
"version": "3.1.1",
"version": "3.0.20",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "opencommit",
"version": "3.1.1",
"version": "3.0.20",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.10.0",

View File

@@ -1,6 +1,6 @@
{
"name": "opencommit",
"version": "3.1.1",
"version": "3.0.20",
"description": "Auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
"keywords": [
"git",

View File

@@ -50,8 +50,8 @@ const generateCommitMessageFromGitDiff = async ({
skipCommitConfirmation = false
}: GenerateCommitMessageFromGitDiffParams): Promise<void> => {
await assertGitRepo();
const commitGenerationSpinner = spinner();
commitGenerationSpinner.start('Generating the commit message');
const commitSpinner = spinner();
commitSpinner.start('Generating the commit message');
try {
let commitMessage = await generateCommitMessageByDiff(
@@ -73,7 +73,7 @@ const generateCommitMessageFromGitDiff = async ({
);
}
commitGenerationSpinner.stop('📝 Commit message generated');
commitSpinner.stop('📝 Commit message generated');
outro(
`Generated commit message:
@@ -88,20 +88,15 @@ ${chalk.grey('——————————————————')}`
message: 'Confirm the commit message?'
}));
if (isCancel(isCommitConfirmedByUser)) process.exit(1);
if (isCommitConfirmedByUser) {
const committingChangesSpinner = spinner();
committingChangesSpinner.start('Committing the changes');
if (isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
const { stdout } = await execa('git', [
'commit',
'-m',
commitMessage,
...extraArgs
]);
committingChangesSpinner.stop(
`${chalk.green('✔')} Successfully committed`
);
outro(`${chalk.green('✔')} Successfully committed`);
outro(stdout);
@@ -118,9 +113,7 @@ ${chalk.grey('——————————————————')}`
message: 'Do you want to run `git push`?'
});
if (isCancel(isPushConfirmedByUser)) process.exit(1);
if (isPushConfirmedByUser) {
if (isPushConfirmedByUser && !isCancel(isPushConfirmedByUser)) {
const pushSpinner = spinner();
pushSpinner.start(`Running 'git push ${remotes[0]}'`);
@@ -148,30 +141,28 @@ ${chalk.grey('——————————————————')}`
options: remotes.map((remote) => ({ value: remote, label: remote }))
})) as string;
if (isCancel(selectedRemote)) process.exit(1);
if (!isCancel(selectedRemote)) {
const pushSpinner = spinner();
const pushSpinner = spinner();
pushSpinner.start(`Running 'git push ${selectedRemote}'`);
pushSpinner.start(`Running 'git push ${selectedRemote}'`);
const { stdout } = await execa('git', ['push', selectedRemote]);
const { stdout } = await execa('git', ['push', selectedRemote]);
pushSpinner.stop(
`${chalk.green(
'✔'
)} Successfully pushed all commits to ${selectedRemote}`
);
pushSpinner.stop(
`${chalk.green(
'✔'
)} Successfully pushed all commits to ${selectedRemote}`
);
if (stdout) outro(stdout);
if (stdout) outro(stdout);
} else outro(`${chalk.gray('✖')} process cancelled`);
}
} else {
}
if (!isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
const regenerateMessage = await confirm({
message: 'Do you want to regenerate the message?'
});
if (isCancel(regenerateMessage)) process.exit(1);
if (regenerateMessage) {
if (regenerateMessage && !isCancel(isCommitConfirmedByUser)) {
await generateCommitMessageFromGitDiff({
diff,
extraArgs,
@@ -180,7 +171,7 @@ ${chalk.grey('——————————————————')}`
}
}
} catch (error) {
commitGenerationSpinner.stop('📝 Commit message generated');
commitSpinner.stop('📝 Commit message generated');
const err = error as Error;
outro(`${chalk.red('✖')} ${err?.message || err}`);
@@ -228,9 +219,10 @@ export async function commit(
message: 'Do you want to stage all files and generate commit message?'
});
if (isCancel(isStageAllAndCommitConfirmedByUser)) process.exit(1);
if (isStageAllAndCommitConfirmedByUser) {
if (
isStageAllAndCommitConfirmedByUser &&
!isCancel(isStageAllAndCommitConfirmedByUser)
) {
await commit(extraArgs, true, fullGitMojiSpec);
process.exit(1);
}

View File

@@ -23,7 +23,7 @@ export const commitlintConfigCommand = command(
if (mode === CONFIG_MODES.get) {
const commitLintConfig = await getCommitlintLLMConfig();
outro(JSON.stringify(commitLintConfig, null, 2));
outro(commitLintConfig.toString());
return;
}

View File

@@ -23,7 +23,6 @@ export enum CONFIG_KEYS {
OCO_EMOJI = 'OCO_EMOJI',
OCO_MODEL = 'OCO_MODEL',
OCO_LANGUAGE = 'OCO_LANGUAGE',
OCO_WHY = 'OCO_WHY',
OCO_MESSAGE_TEMPLATE_PLACEHOLDER = 'OCO_MESSAGE_TEMPLATE_PLACEHOLDER',
OCO_PROMPT_MODULE = 'OCO_PROMPT_MODULE',
OCO_AI_PROVIDER = 'OCO_AI_PROVIDER',
@@ -377,7 +376,6 @@ export type ConfigType = {
[CONFIG_KEYS.OCO_OPENAI_BASE_PATH]?: string;
[CONFIG_KEYS.OCO_DESCRIPTION]: boolean;
[CONFIG_KEYS.OCO_EMOJI]: boolean;
[CONFIG_KEYS.OCO_WHY]: boolean;
[CONFIG_KEYS.OCO_MODEL]: string;
[CONFIG_KEYS.OCO_LANGUAGE]: string;
[CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER]: string;
@@ -424,26 +422,25 @@ enum OCO_PROMPT_MODULE_ENUM {
COMMITLINT = '@commitlint'
}
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_WHY: false,
OCO_GITPUSH: true // todo: deprecate
};
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
};
const initGlobalConfig = (configPath: string = defaultConfigPath) => {
writeFileSync(configPath, iniStringify(DEFAULT_CONFIG), 'utf8');
return DEFAULT_CONFIG;
writeFileSync(defaultConfigPath, iniStringify(defaultConfig), 'utf8');
return defaultConfig;
};
const parseEnvVarValue = (value?: any) => {
@@ -454,10 +451,16 @@ const parseEnvVarValue = (value?: any) => {
}
};
const getEnvConfig = (envPath: string) => {
export const getConfig = ({
configPath = defaultConfigPath,
envPath = defaultEnvPath
}: {
configPath?: string;
envPath?: string;
} = {}): ConfigType => {
dotenv.config({ path: envPath });
return {
const envConfig = {
OCO_MODEL: process.env.OCO_MODEL,
OCO_OPENAI_API_KEY: process.env.OCO_OPENAI_API_KEY,
@@ -488,59 +491,33 @@ const getEnvConfig = (envPath: string) => {
OCO_GITPUSH: parseEnvVarValue(process.env.OCO_GITPUSH) // todo: deprecate
};
};
const getGlobalConfig = (configPath: string) => {
let globalConfig: ConfigType;
const isGlobalConfigFileExist = existsSync(configPath);
if (!isGlobalConfigFileExist) globalConfig = initGlobalConfig(configPath);
if (!isGlobalConfigFileExist) globalConfig = initGlobalConfig();
else {
const configFile = readFileSync(configPath, 'utf8');
globalConfig = iniParse(configFile) as ConfigType;
}
return globalConfig;
};
const mergeObjects = (main: Partial<ConfigType>, fallback: ConfigType) =>
Object.keys(CONFIG_KEYS).reduce((acc, key) => {
acc[key] = parseEnvVarValue(main[key] ?? fallback[key]);
/**
* 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<ConfigType>, fallback: ConfigType) =>
Object.keys(CONFIG_KEYS).reduce((acc, key) => {
acc[key] = parseEnvVarValue(main[key] ?? fallback[key]);
return acc;
}, {} as ConfigType);
return acc;
}, {} as ConfigType);
interface GetConfigOptions {
globalPath?: string;
envPath?: string;
}
export const getConfig = ({
envPath = defaultEnvPath,
globalPath = defaultConfigPath
}: GetConfigOptions = {}): ConfigType => {
const envConfig = getEnvConfig(envPath);
const globalConfig = getGlobalConfig(globalPath);
const config = mergeConfigs(envConfig, globalConfig);
// env config takes precedence over global ~/.opencommit config file
const config = mergeObjects(envConfig, globalConfig);
return config;
};
export const setConfig = (
keyValues: [key: string, value: string][],
globalConfigPath: string = defaultConfigPath
configPath: string = defaultConfigPath
) => {
const config = getConfig({
globalPath: globalConfigPath
});
const config = getConfig();
for (let [key, value] of keyValues) {
if (!configValidators.hasOwnProperty(key)) {
@@ -566,7 +543,9 @@ export const setConfig = (
config[key] = validValue;
}
writeFileSync(globalConfigPath, iniStringify(config), 'utf8');
writeFileSync(configPath, iniStringify(config), 'utf8');
assertConfigsAreValid(config);
outro(`${chalk.green('✔')} config successfully set`);
};

View File

@@ -258,9 +258,7 @@ const INIT_MAIN_PROMPT = (
prompts: string[]
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
role: 'system',
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes ${
config.OCO_WHY ? '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.
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.'

View File

@@ -1,29 +1,13 @@
import fs from 'fs/promises';
import path from 'path';
const findModulePath = (moduleName: string) => {
const searchPaths = [
path.join('node_modules', moduleName),
path.join('node_modules', '.pnpm')
];
for (const basePath of searchPaths) {
try {
const resolvedPath = require.resolve(moduleName, { paths: [basePath] });
return resolvedPath;
} catch {
// Continue to the next search path if the module is not found
}
}
throw new Error(`Cannot find module ${moduleName}`);
};
const getCommitLintModuleType = async (): Promise<'cjs' | 'esm'> => {
const packageFile = '@commitlint/load/package.json';
const packageJsonPath = findModulePath(packageFile);
const packageFile = 'node_modules/@commitlint/load/package.json';
const packageJsonPath = path.join(
process.env.PWD || process.cwd(),
packageFile,
);
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
if (!packageJson) {
throw new Error(`Failed to parse ${packageFile}`);
}
@@ -35,7 +19,7 @@ const getCommitLintModuleType = async (): Promise<'cjs' | 'esm'> => {
* QualifiedConfig from any version of @commitlint/types
* @see https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/types/src/load.ts
*/
type QualifiedConfigOnAnyVersion = { [key: string]: unknown };
type QualifiedConfigOnAnyVersion = { [key:string]: unknown };
/**
* This code is loading the configuration for the `@commitlint` package from the current working
@@ -43,31 +27,36 @@ type QualifiedConfigOnAnyVersion = { [key: string]: unknown };
*
* @returns
*/
export const getCommitLintPWDConfig =
async (): Promise<QualifiedConfigOnAnyVersion | null> => {
let load: Function, modulePath: string;
switch (await getCommitLintModuleType()) {
case 'cjs':
/**
* CommonJS (<= commitlint@v18.x.x.)
*/
modulePath = findModulePath('@commitlint/load');
load = require(modulePath).default;
break;
case 'esm':
/**
* ES Module (commitlint@v19.x.x. <= )
* Directory import is not supported in ES Module resolution, so import the file directly
*/
modulePath = await findModulePath('@commitlint/load/lib/load.js');
load = (await import(modulePath)).default;
break;
}
export const getCommitLintPWDConfig = async (): Promise<QualifiedConfigOnAnyVersion | null> => {
let load, nodeModulesPath;
switch (await getCommitLintModuleType()) {
case 'cjs':
/**
* CommonJS (<= commitlint@v18.x.x.)
*/
nodeModulesPath = path.join(
process.env.PWD || process.cwd(),
'node_modules/@commitlint/load',
);
load = require(nodeModulesPath).default;
break;
case 'esm':
/**
* ES Module (commitlint@v19.x.x. <= )
* Directory import is not supported in ES Module resolution, so import the file directly
*/
nodeModulesPath = path.join(
process.env.PWD || process.cwd(),
'node_modules/@commitlint/load/lib/load.js',
);
load = (await import(nodeModulesPath)).default;
break;
}
if (load && typeof load === 'function') {
return await load();
}
if (load && typeof load === 'function') {
return await load();
}
// @commitlint/load is not a function
return null;
};
// @commitlint/load is not a function
return null;
};

View File

@@ -181,7 +181,9 @@ describe('cli flow to generate commit message using @commitlint prompt-module',
[],
{ cwd: gitDir }
);
expect(await commitlintGet.findByText('consistency')).toBeInTheConsole();
expect(
await commitlintGet.findByText('[object Object]')
).toBeInTheConsole();
// Run 'oco' using .opencommit-commitlint
await render('echo', [`'console.log("Hello World");' > index.ts`], {

View File

@@ -1,16 +1,10 @@
import { existsSync, readFileSync, rmSync } from 'fs';
import {
DEFAULT_CONFIG,
getConfig,
setConfig
} from '../../src/commands/config';
import { getConfig } from '../../src/commands/config';
import { prepareFile } from './utils';
import { dirname } from 'path';
describe('config', () => {
describe('getConfig', () => {
const originalEnv = { ...process.env };
let globalConfigFile: { filePath: string; cleanup: () => Promise<void> };
let envConfigFile: { filePath: string; cleanup: () => Promise<void> };
let localEnvFile: { filePath: string; cleanup: () => Promise<void> };
function resetEnv(env: NodeJS.ProcessEnv) {
Object.keys(process.env).forEach((key) => {
@@ -25,12 +19,7 @@ describe('config', () => {
beforeEach(async () => {
resetEnv(originalEnv);
if (globalConfigFile) await globalConfigFile.cleanup();
if (envConfigFile) await envConfigFile.cleanup();
});
afterEach(async () => {
if (globalConfigFile) await globalConfigFile.cleanup();
if (envConfigFile) await envConfigFile.cleanup();
if (localEnvFile) await localEnvFile.cleanup();
});
afterAll(() => {
@@ -47,249 +36,115 @@ describe('config', () => {
return await prepareFile(fileName, fileContent);
};
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'
});
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');
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'
});
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);
localEnvFile = await generateConfig('.env', {
OCO_OPENAI_API_KEY: 'local-key',
OCO_ANTHROPIC_API_KEY: 'local-anthropic-key',
OCO_LANGUAGE: 'fr'
});
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'
});
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);
const config = getConfig({
configPath: globalConfigFile.filePath,
envPath: localEnvFile.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'
});
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');
});
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);
});
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);
});
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');
});
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);
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'
});
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}`);
});
localEnvFile = await generateConfig('.env', {
OCO_ANTHROPIC_API_KEY: 'local-anthropic-key'
});
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');
const config = getConfig({
configPath: globalConfigFile.filePath,
envPath: localEnvFile.filePath
});
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
);
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);
});
const config = getConfig({ globalPath: globalConfigFile.filePath });
expect(config.OCO_OPENAI_API_KEY).toEqual('updated-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'
});
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({ 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);
localEnvFile = await generateConfig('.env', {
OCO_TOKENS_MAX_INPUT: '8192',
OCO_ONE_LINE_COMMIT: 'false'
});
it('should throw an error for unsupported config keys', async () => {
globalConfigFile = await generateConfig('.opencommit', {});
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');
}
const config = getConfig({
configPath: globalConfigFile.filePath,
envPath: localEnvFile.filePath
});
it('should persist changes to the config file', async () => {
const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath);
expect(isGlobalConfigFileExist).toBe(false);
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);
});
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');
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'
});
it('should set multiple configs in a row and keep the changes', async () => {
const isGlobalConfigFileExist = existsSync(globalConfigFile.filePath);
expect(isGlobalConfigFileExist).toBe(false);
localEnvFile = await generateConfig('.env', {});
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');
const config = getConfig({
configPath: globalConfigFile.filePath,
envPath: localEnvFile.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');
});
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'
});
localEnvFile = await generateConfig('.env', { OCO_OPENAI_API_KEY: 'null' });
const config = getConfig({
configPath: globalConfigFile.filePath,
envPath: localEnvFile.filePath
});
expect(config).not.toEqual(null);
expect(config.OCO_OPENAI_API_KEY).toEqual(null);
});
});

View File

@@ -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,9 +20,7 @@ export async function prepareFile(
const filePath = path.resolve(tempDir, fileName);
await fsWriteFile(filePath, content);
const cleanup = async () => {
if (existsSync(tempDir)) {
await fsRemove(tempDir, { recursive: true });
}
return fsRemove(tempDir, { recursive: true });
};
return {

View File

@@ -4,7 +4,7 @@
"lib": ["ES6", "ES2020"],
"module": "CommonJS",
"resolveJsonModule": true,
"moduleResolution": "Node",
@@ -21,7 +21,9 @@
"skipLibCheck": true
},
"include": ["test/jest-setup.ts"],
"include": [
"test/jest-setup.ts"
],
"exclude": ["node_modules"],
"ts-node": {
"esm": true,