Compare commits

..

26 Commits

Author SHA1 Message Date
di-sukharev
ca4be719b2 Merge branch 'dev' into oco_find_v1 2024-09-01 18:26:46 +03:00
di-sukharev
5e37fd29b7 Merge remote-tracking branch 'origin/dev' into oco_find_v1 2024-09-01 18:24:48 +03:00
di-sukharev
7286456a04 Merge remote-tracking branch 'origin/dev' into oco_find_v1 2024-08-27 17:09:44 +03:00
di-sukharev
85468823f9 feat(package.json): add uglify-js dependency for JavaScript minification
feat(find.ts): implement functions to find declarations and usages of functions,
generate call hierarchy, and create mermaid diagrams for better visualization of code structure
refactor(find.ts): improve findInFiles function to accept options for grep
and enhance the handling of occurrences for better clarity and usability
2024-08-27 16:46:27 +03:00
di-sukharev
7eb9a1b45c rename azure method 2024-08-25 22:34:22 +03:00
di-sukharev
825c2fe825 feat(commands): remove CommandsEnum.ts and integrate commands into ENUMS.ts for better organization
feat(cli): add findCommand to the CLI for enhanced functionality in searching
fix(commitlint): correct capitalization in intro message for consistency
fix(prepare-commit-msg-hook): correct capitalization in intro message for consistency
refactor(utils): rename getOpenCommitIgnore to getIgnoredFolders for clarity and improve ignored folder retrieval logic
2024-08-24 20:17:16 +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
15 changed files with 744 additions and 431 deletions

4
.gitignore vendored
View File

@@ -11,4 +11,6 @@ uncaughtExceptions.log
src/*.json
.idea
test.ts
notes.md
notes.md
*.excalidraw
*.tldr

View File

@@ -28,9 +28,7 @@ You can use OpenCommit by simply running it via the CLI like this `oco`. 2 secon
npm install -g opencommit
```
Alternatively run it via `npx opencommit` or `bunx opencommit`
MacOS may ask to run the command with `sudo` when installing a package globally.
Alternatively run it via `npx opencommit` or `bunx opencommit`, but you need to create ~/.opencommit config file in place.
2. Get your API key from [OpenAI](https://platform.openai.com/account/api-keys). Make sure that you add your payment details, so the API works.

View File

@@ -37182,42 +37182,42 @@ var OpenAIClient = class {
// src/engine/azure.ts
var AzureEngine = class {
constructor(config7) {
this.generateCommitMessage = async (messages) => {
try {
const REQUEST_TOKENS = messages.map((msg) => tokenCount(msg.content) + 4).reduce((a4, b7) => a4 + b7, 0);
if (REQUEST_TOKENS > this.config.maxTokensInput - this.config.maxTokensOutput) {
throw new Error("TOO_MUCH_TOKENS" /* tooMuchTokens */);
}
const data = await this.client.getChatCompletions(
this.config.model,
messages
);
const message = data.choices[0].message;
if (message?.content === null) {
return void 0;
}
return message?.content;
} catch (error) {
ce(`${source_default.red("\u2716")} ${this.config.model}`);
const err = error;
ce(`${source_default.red("\u2716")} ${JSON.stringify(error)}`);
if (axios_default.isAxiosError(error) && error.response?.status === 401) {
const openAiError = error.response.data.error;
if (openAiError?.message)
ce(openAiError.message);
ce(
"For help look into README https://github.com/di-sukharev/opencommit#setup"
);
}
throw err;
}
};
this.config = config7;
this.client = new OpenAIClient(
this.config.baseURL,
new AzureKeyCredential(this.config.apiKey)
);
}
async generateCommitMessage(messages) {
try {
const REQUEST_TOKENS = messages.map((msg) => tokenCount(msg.content) + 4).reduce((a4, b7) => a4 + b7, 0);
if (REQUEST_TOKENS > this.config.maxTokensInput - this.config.maxTokensOutput) {
throw new Error("TOO_MUCH_TOKENS" /* tooMuchTokens */);
}
const data = await this.client.getChatCompletions(
this.config.model,
messages
);
const message = data.choices[0].message;
if (message?.content === null) {
return void 0;
}
return message?.content;
} catch (error) {
ce(`${source_default.red("\u2716")} ${this.config.model}`);
const err = error;
ce(`${source_default.red("\u2716")} ${JSON.stringify(error)}`);
if (axios_default.isAxiosError(error) && error.response?.status === 401) {
const openAiError = error.response.data.error;
if (openAiError?.message)
ce(openAiError.message);
ce(
"For help look into README https://github.com/di-sukharev/opencommit#setup"
);
}
throw err;
}
}
};
// src/engine/flowise.ts
@@ -43060,12 +43060,17 @@ var assertGitRepo = async () => {
throw new Error(error);
}
};
var getIgnoredFolders = () => {
try {
return (0, import_fs3.readFileSync)(".opencommitignore").toString().split("\n");
} catch (e3) {
return [];
}
};
var getOpenCommitIgnore = () => {
const ig = (0, import_ignore.default)();
try {
ig.add((0, import_fs3.readFileSync)(".opencommitignore").toString().split("\n"));
} catch (e3) {
}
const ignorePatterns = getIgnoredFolders();
ig.add(ignorePatterns);
return ig;
};
var getCoreHooksPath = async () => {
@@ -43352,7 +43357,7 @@ var commitlintConfigCommand = G3(
parameters: ["<mode>"]
},
async (argv) => {
ae("opencommit \u2014 configure @commitlint");
ae("OpenCommit \u2014 configure @commitlint");
try {
const { mode } = argv._;
if (mode === "get" /* get */) {
@@ -43480,7 +43485,7 @@ var prepareCommitMessageHook = async (isStageAllFlag = false) => {
const staged = await getStagedFiles();
if (!staged)
return;
ae("opencommit");
ae("OpenCommit");
const config7 = getConfig();
if (!config7.OCO_OPENAI_API_KEY && !config7.OCO_ANTHROPIC_API_KEY && !config7.OCO_AZURE_API_KEY) {
throw new Error(
@@ -43534,13 +43539,281 @@ Current version: ${currentVersion}. Latest version: ${latestVersion}.
}
};
// src/commands/find.ts
var generateMermaid = async (stdout) => {
const config7 = getConfig();
const DEFAULT_CONFIG = {
model: config7.OCO_MODEL,
maxTokensOutput: config7.OCO_TOKENS_MAX_OUTPUT,
maxTokensInput: config7.OCO_TOKENS_MAX_INPUT,
baseURL: config7.OCO_OPENAI_BASE_PATH
};
const engine = new OpenAiEngine({
...DEFAULT_CONFIG,
apiKey: config7.OCO_OPENAI_API_KEY
});
const diagram = await engine.generateCommitMessage([
{
role: "system",
content: `You are to generate a mermaid diagram from the given function. Strictly answer in this json format: { "mermaid": "<mermaid diagram>" }. Where <mermaid diagram> is a valid mermaid diagram, e.g:
graph TD
A[Start] --> B[Generate Commit Message]
B --> C{Token count >= Max?}
C -->|Yes| D[Process file diffs]
C -->|No| E[Generate single message]
D --> F[Join messages]
E --> G[Generate message]
F --> H[End]
G --> H
B --> I{Error occurred?}
I -->|Yes| J[Handle error]
J --> H
I -->|No| H
`
},
{
role: "user",
content: stdout
}
]);
return JSON.parse(diagram);
};
function extractFuncName(line) {
const regex = /(?:function|export\s+const|const|let|var)?\s*(?:async\s+)?(\w+)\s*(?:=\s*(?:async\s*)?\(|\()/;
const match = line.match(regex);
return match ? match[1] : null;
}
function extractSingle(lineContent) {
const match = lineContent.match(/\s*(?:public\s+)?(?:async\s+)?(\w+)\s*=/);
return match ? match[1] : null;
}
function mapLinesToOccurrences(input, step = 3) {
const occurrences = [];
let single;
for (let i3 = 0; i3 < input.length; i3 += step) {
if (i3 + 1 >= input.length)
break;
const [fileName, callerLineNumber, ...callerLineContent] = input[i3].split(/[=:]/);
const [, definitionLineNumber, ...definitionLineContent] = input[i3 + 1].split(/[:]/);
if (!single)
single = extractSingle(definitionLineContent.join(":"));
occurrences.push({
fileName,
context: {
number: parseInt(callerLineNumber, 10),
content: callerLineContent.join("=").trim()
},
matches: [
{
number: parseInt(definitionLineNumber, 10),
content: definitionLineContent.join(":").trim()
}
]
});
}
return { occurrences, single };
}
var findDeclarations = async (query, ignoredFolders) => {
const searchQuery = `(async|function|public).*${query.join("[^ \\n]*")}`;
ce(`Searching: ${searchQuery}`);
const occurrences = await findInFiles({ query: searchQuery, ignoredFolders });
if (!occurrences)
return null;
const declarations = mapLinesToOccurrences(occurrences.split("\n"));
return declarations;
};
var findUsagesByDeclaration = async (declaration, ignoredFolders) => {
const searchQuery = `${declaration}\\(.*\\)`;
const occurrences = await findInFiles({
query: searchQuery,
ignoredFolders
});
if (!occurrences)
return null;
const usages = mapLinesToOccurrences(
occurrences.split("\n").filter(Boolean),
2
);
return usages;
};
var findInFiles = async ({
query,
ignoredFolders,
grepOptions = []
}) => {
const withIgnoredFolders = ignoredFolders.length > 0 ? [
"--",
" ",
".",
" ",
ignoredFolders.map((folder) => `:^${folder}`).join(" ")
] : [];
const params = [
"--no-pager",
"grep",
"--show-function",
"-n",
"-i",
...grepOptions,
"--break",
"--color=never",
"--threads",
"10",
"-E",
query,
...withIgnoredFolders
];
try {
const { stdout } = await execa("git", params);
return stdout;
} catch (error) {
return null;
}
};
var generatePermutations = (arr) => {
const n2 = arr.length;
const result = [];
const indices = new Int32Array(n2);
const current = new Array(n2);
for (let i4 = 0; i4 < n2; i4++) {
indices[i4] = i4;
current[i4] = arr[i4];
}
result.push([...current]);
let i3 = 1;
while (i3 < n2) {
if (indices[i3] > 0) {
const j4 = indices[i3] % 2 === 1 ? 0 : indices[i3];
[current[i3], current[j4]] = [current[j4], current[i3]];
result.push([...current]);
indices[i3]--;
i3 = 1;
} else {
indices[i3] = i3;
i3++;
}
}
return result;
};
var shuffleQuery = (query) => {
return generatePermutations(query);
};
var findCommand = G3(
{
name: "find" /* find */,
parameters: ["<query...>"]
},
async (argv) => {
const query = argv._;
ae(`OpenCommit \u2014 \u{1F526} find`);
const ignoredFolders = getIgnoredFolders();
const searchSpinner = le();
let declarations = await findDeclarations(query, ignoredFolders);
ce(`No matches found. Searching semantically similar queries.`);
searchSpinner.start(`Searching for matches...`);
if (!declarations?.occurrences.length) {
const allPossibleQueries = shuffleQuery(query).reverse();
for (const possibleQuery of allPossibleQueries) {
declarations = await findDeclarations(possibleQuery, ignoredFolders);
if (declarations?.occurrences.length)
break;
}
}
if (!declarations?.occurrences.length) {
searchSpinner.stop(`${source_default.red("\u2718")} No function declarations found.`);
return process.exit(1);
}
const usages = await findUsagesByDeclaration(
declarations.single,
ignoredFolders
);
searchSpinner.stop(
`${source_default.green("\u2714")} Found ${source_default.green(
declarations.single
)} definition and ${usages?.occurrences.length} usages.`
);
ie(
declarations.occurrences.map(
(o3) => o3.matches.map(
(m5) => `${o3.fileName}:${m5.number} ${source_default.cyan(
"==>"
)} ${m5.content.replace(
declarations.single,
source_default.green(declarations.single)
)}`
).join("\n")
).join("\n"),
"\u235C DECLARATIONS \u235C"
);
ie(
usages?.occurrences.map(
(o3) => o3.matches.map(
(m5) => `${o3.fileName}:${m5.number} ${source_default.cyan(
"==>"
)} ${m5.content.replace(
declarations.single,
source_default.green(declarations.single)
)}`
)
).join("\n"),
"\u233E USAGES \u233E"
);
const usage = await ee({
message: source_default.cyan("Expand usage:"),
options: usages.occurrences.map(
(o3) => o3.matches.map((m5) => ({
value: { o: o3, m: m5 },
label: `${source_default.yellow(`${o3.fileName}:${m5.number}`)} ${source_default.cyan(
"==>"
)} ${m5.content.replace(
declarations.single,
source_default.green(declarations.single)
)}`,
hint: `parent: ${extractFuncName(o3.context.content) ?? "404"}`
}))
).flat()
});
if (hD2(usage))
process.exit(1);
const { stdout } = await execa("git", [
"--no-pager",
"grep",
"--function-context",
"--heading",
"-E",
usage.m.content.replace("(", "\\(").replace(")", "\\)"),
usage.o.fileName
]);
const mermaidSpinner = le();
mermaidSpinner.start("Generating mermaid diagram...");
const mermaid = await generateMermaid(stdout);
mermaidSpinner.stop();
if (mermaid)
console.log(mermaid.mermaid);
else
ie("No mermaid diagram found.");
const isCommitConfirmedByUser = await Q3({
message: "Create Excalidraw file?"
});
if (isCommitConfirmedByUser)
ce("created diagram.excalidraw");
else
ce("Excalidraw file not created.");
}
);
// src/cli.ts
var extraArgs = process.argv.slice(2);
Z2(
{
version: package_default.version,
name: "opencommit",
commands: [configCommand, hookCommand, commitlintConfigCommand],
commands: [
configCommand,
hookCommand,
commitlintConfigCommand,
findCommand
],
flags: {
fgm: Boolean,
yes: {

View File

@@ -55994,42 +55994,42 @@ var OpenAIClient = class {
// src/engine/azure.ts
var AzureEngine = class {
constructor(config6) {
this.generateCommitMessage = async (messages) => {
try {
const REQUEST_TOKENS = messages.map((msg) => tokenCount(msg.content) + 4).reduce((a3, b3) => a3 + b3, 0);
if (REQUEST_TOKENS > this.config.maxTokensInput - this.config.maxTokensOutput) {
throw new Error("TOO_MUCH_TOKENS" /* tooMuchTokens */);
}
const data = await this.client.getChatCompletions(
this.config.model,
messages
);
const message = data.choices[0].message;
if (message?.content === null) {
return void 0;
}
return message?.content;
} catch (error) {
ce(`${source_default.red("\u2716")} ${this.config.model}`);
const err = error;
ce(`${source_default.red("\u2716")} ${JSON.stringify(error)}`);
if (axios_default.isAxiosError(error) && error.response?.status === 401) {
const openAiError = error.response.data.error;
if (openAiError?.message)
ce(openAiError.message);
ce(
"For help look into README https://github.com/di-sukharev/opencommit#setup"
);
}
throw err;
}
};
this.config = config6;
this.client = new OpenAIClient(
this.config.baseURL,
new AzureKeyCredential(this.config.apiKey)
);
}
async generateCommitMessage(messages) {
try {
const REQUEST_TOKENS = messages.map((msg) => tokenCount(msg.content) + 4).reduce((a3, b3) => a3 + b3, 0);
if (REQUEST_TOKENS > this.config.maxTokensInput - this.config.maxTokensOutput) {
throw new Error("TOO_MUCH_TOKENS" /* tooMuchTokens */);
}
const data = await this.client.getChatCompletions(
this.config.model,
messages
);
const message = data.choices[0].message;
if (message?.content === null) {
return void 0;
}
return message?.content;
} catch (error) {
ce(`${source_default.red("\u2716")} ${this.config.model}`);
const err = error;
ce(`${source_default.red("\u2716")} ${JSON.stringify(error)}`);
if (axios_default.isAxiosError(error) && error.response?.status === 401) {
const openAiError = error.response.data.error;
if (openAiError?.message)
ce(openAiError.message);
ce(
"For help look into README https://github.com/di-sukharev/opencommit#setup"
);
}
throw err;
}
}
};
// src/engine/flowise.ts

340
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "opencommit",
"version": "3.1.2",
"version": "3.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "opencommit",
"version": "3.1.2",
"version": "3.1.1",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.10.0",
@@ -956,38 +956,6 @@
"resolved": "https://registry.npmjs.org/@dqbd/tiktoken/-/tiktoken-1.0.13.tgz",
"integrity": "sha512-941kjlHjfI97l6NuH/AwuXV4mHuVnRooDcHNSlzi98hz+4ug3wT4gJcWjSwSZHqeGAEn90lC9sFD+8a9d5Jvxg=="
},
"node_modules/@esbuild/android-arm": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
"integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz",
"integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -3678,54 +3646,6 @@
"esbuild-windows-arm64": "0.15.18"
}
},
"node_modules/esbuild-android-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz",
"integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-android-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz",
"integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-darwin-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz",
"integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-darwin-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz",
@@ -3742,262 +3662,6 @@
"node": ">=12"
}
},
"node_modules/esbuild-freebsd-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz",
"integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-freebsd-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz",
"integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-32": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz",
"integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz",
"integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-arm": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz",
"integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz",
"integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-mips64le": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz",
"integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==",
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-ppc64le": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz",
"integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-riscv64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz",
"integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-s390x": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz",
"integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-netbsd-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz",
"integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-openbsd-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz",
"integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-sunos-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz",
"integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-32": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz",
"integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz",
"integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz",
"integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import { configCommand } from './commands/config';
import { hookCommand, isHookCalled } from './commands/githook.js';
import { prepareCommitMessageHook } from './commands/prepare-commit-msg-hook';
import { checkIsLatestVersion } from './utils/checkIsLatestVersion';
import { findCommand } from './commands/find';
const extraArgs = process.argv.slice(2);
@@ -16,7 +17,12 @@ cli(
{
version: packageJSON.version,
name: 'opencommit',
commands: [configCommand, hookCommand, commitlintConfigCommand],
commands: [
configCommand,
hookCommand,
commitlintConfigCommand,
findCommand
],
flags: {
fgm: Boolean,
yes: {

View File

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

View File

@@ -16,7 +16,7 @@ export const commitlintConfigCommand = command(
parameters: ['<mode>']
},
async (argv) => {
intro('opencommit — configure @commitlint');
intro('OpenCommit — configure @commitlint');
try {
const { mode } = argv._;

372
src/commands/find.ts Normal file
View File

@@ -0,0 +1,372 @@
import {
confirm,
intro,
isCancel,
note,
outro,
select,
spinner
} from '@clack/prompts';
import chalk from 'chalk';
import { command } from 'cleye';
import { execa } from 'execa';
import { getIgnoredFolders } from '../utils/git';
import { COMMANDS } from './ENUMS';
import { OpenAiEngine } from '../engine/openAi';
import { getConfig } from './config';
type Occurrence = {
fileName: string;
context: {
number: number;
content: string;
};
matches: {
number: number;
content: string;
}[];
};
/*
TODO:
- [ ] format declarations as file:line => context -> declaration
- [ ] format usages as file:line => context -> usage
- [ ] expand on usage to see it's call hierarchy
- [ ] generate Mermaid diagram
*/
const generateMermaid = async (stdout: string) => {
const config = getConfig();
const DEFAULT_CONFIG = {
model: config.OCO_MODEL!,
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
baseURL: config.OCO_OPENAI_BASE_PATH!
};
const engine = new OpenAiEngine({
...DEFAULT_CONFIG,
apiKey: config.OCO_OPENAI_API_KEY!
});
const diagram = await engine.generateCommitMessage([
{
role: 'system',
content: `You are to generate a mermaid diagram from the given function. Strictly answer in this json format: { "mermaid": "<mermaid diagram>" }. Where <mermaid diagram> is a valid mermaid diagram, e.g:
graph TD
A[Start] --> B[Generate Commit Message]
B --> C{Token count >= Max?}
C -->|Yes| D[Process file diffs]
C -->|No| E[Generate single message]
D --> F[Join messages]
E --> G[Generate message]
F --> H[End]
G --> H
B --> I{Error occurred?}
I -->|Yes| J[Handle error]
J --> H
I -->|No| H
`
},
{
role: 'user',
content: stdout
}
]);
return JSON.parse(diagram as string);
};
export function extractFuncName(line: string) {
const regex =
/(?:function|export\s+const|const|let|var)?\s*(?:async\s+)?(\w+)\s*(?:=\s*(?:async\s*)?\(|\()/;
const match = line.match(regex);
return match ? match[1] : null;
}
function extractSingle(lineContent: string): string | null {
const match = lineContent.match(/\s*(?:public\s+)?(?:async\s+)?(\w+)\s*=/);
return match ? match[1] : null;
}
function mapLinesToOccurrences(input: string[], step: number = 3) {
const occurrences: Occurrence[] = [];
let single;
for (let i = 0; i < input.length; i += step) {
if (i + 1 >= input.length) break;
const [fileName, callerLineNumber, ...callerLineContent] =
input[i].split(/[=:]/);
const [, definitionLineNumber, ...definitionLineContent] =
input[i + 1].split(/[:]/);
if (!single) single = extractSingle(definitionLineContent.join(':'));
occurrences.push({
fileName,
context: {
number: parseInt(callerLineNumber, 10),
content: callerLineContent.join('=').trim()
},
matches: [
{
number: parseInt(definitionLineNumber, 10),
content: definitionLineContent.join(':').trim()
}
]
});
}
return { occurrences, single };
}
const findDeclarations = async (query: string[], ignoredFolders: string[]) => {
const searchQuery = `(async|function|public).*${query.join('[^ \\n]*')}`;
outro(`Searching: ${searchQuery}`);
const occurrences = await findInFiles({ query: searchQuery, ignoredFolders });
if (!occurrences) return null;
const declarations = mapLinesToOccurrences(occurrences.split('\n'));
return declarations;
};
const findUsagesByDeclaration = async (
declaration: string,
ignoredFolders: string[]
) => {
const searchQuery = `${declaration}\\(.*\\)`;
const occurrences = await findInFiles({
query: searchQuery,
ignoredFolders
// grepOptions: ['--function-context']
});
if (!occurrences) return null;
const usages = mapLinesToOccurrences(
occurrences.split('\n').filter(Boolean),
2
);
return usages;
};
const buildCallHierarchy = async (
query: string[],
ignoredFolders: string[]
) => {};
const findInFiles = async ({
query,
ignoredFolders,
grepOptions = []
}: {
query: string;
ignoredFolders: string[];
grepOptions?: string[];
}): Promise<string | null> => {
const withIgnoredFolders =
ignoredFolders.length > 0
? [
'--',
' ',
'.',
' ',
ignoredFolders.map((folder) => `:^${folder}`).join(' ')
]
: [];
const params = [
'--no-pager',
'grep',
'--show-function', // show function caller
'-n',
'-i',
...grepOptions,
'--break',
'--color=never',
// '-C',
// '1',
// '--full-name',
// '--heading',
'--threads',
'10',
'-E',
query,
...withIgnoredFolders
];
try {
const { stdout } = await execa('git', params);
return stdout;
} catch (error) {
return null;
}
};
const generatePermutations = (arr: string[]): string[][] => {
const n = arr.length;
const result: string[][] = [];
const indices = new Int32Array(n);
const current = new Array(n);
for (let i = 0; i < n; i++) {
indices[i] = i;
current[i] = arr[i];
}
result.push([...current]);
let i = 1;
while (i < n) {
if (indices[i] > 0) {
const j = indices[i] % 2 === 1 ? 0 : indices[i];
[current[i], current[j]] = [current[j], current[i]];
result.push([...current]);
indices[i]--;
i = 1;
} else {
indices[i] = i;
i++;
}
}
return result;
};
const shuffleQuery = (query: string[]): string[][] => {
return generatePermutations(query);
};
export const findCommand = command(
{
name: COMMANDS.find,
parameters: ['<query...>']
},
async (argv) => {
const query = argv._;
intro(`OpenCommit — 🔦 find`);
const ignoredFolders = getIgnoredFolders();
const searchSpinner = spinner();
let declarations = await findDeclarations(query, ignoredFolders);
outro(`No matches found. Searching semantically similar queries.`);
searchSpinner.start(`Searching for matches...`);
if (!declarations?.occurrences.length) {
const allPossibleQueries = shuffleQuery(query).reverse();
for (const possibleQuery of allPossibleQueries) {
declarations = await findDeclarations(possibleQuery, ignoredFolders);
if (declarations?.occurrences.length) break;
}
}
if (!declarations?.occurrences.length) {
searchSpinner.stop(`${chalk.red('✘')} No function declarations found.`);
return process.exit(1);
}
const usages = await findUsagesByDeclaration(
declarations.single,
ignoredFolders
);
searchSpinner.stop(
`${chalk.green('✔')} Found ${chalk.green(
declarations.single
)} definition and ${usages?.occurrences.length} usages.`
);
note(
declarations.occurrences
.map((o) =>
o.matches
.map(
(m) =>
`${o.fileName}:${m.number} ${chalk.cyan(
'==>'
)} ${m.content.replace(
declarations.single,
chalk.green(declarations.single)
)}`
)
.join('\n')
)
.join('\n'),
'⍜ DECLARATIONS ⍜'
);
note(
usages?.occurrences
.map((o) =>
o.matches.map(
(m) =>
`${o.fileName}:${m.number} ${chalk.cyan(
'==>'
)} ${m.content.replace(
declarations.single,
chalk.green(declarations.single)
)}`
)
)
.join('\n'),
'⌾ USAGES ⌾'
);
const usage = (await select({
message: chalk.cyan('Expand usage:'),
options: usages!.occurrences
.map((o) =>
o.matches.map((m) => ({
value: { o, m },
label: `${chalk.yellow(`${o.fileName}:${m.number}`)} ${chalk.cyan(
'==>'
)} ${m.content.replace(
declarations.single,
chalk.green(declarations.single)
)}`,
hint: `parent: ${extractFuncName(o.context.content) ?? '404'}`
}))
)
.flat()
})) as { o: Occurrence; m: any };
if (isCancel(usage)) process.exit(1);
const { stdout } = await execa('git', [
'--no-pager',
'grep',
'--function-context',
'--heading',
'-E',
usage.m.content.replace('(', '\\(').replace(')', '\\)'),
usage.o.fileName
]);
const mermaidSpinner = spinner();
mermaidSpinner.start('Generating mermaid diagram...');
const mermaid: any = await generateMermaid(stdout);
mermaidSpinner.stop();
if (mermaid) console.log(mermaid.mermaid);
else note('No mermaid diagram found.');
const isCommitConfirmedByUser = await confirm({
message: 'Create Excalidraw file?'
});
if (isCommitConfirmedByUser) outro('created diagram.excalidraw');
else outro('Excalidraw file not created.');
}
);

View File

@@ -35,7 +35,7 @@ export const prepareCommitMessageHook = async (
if (!staged) return;
intro('opencommit');
intro('OpenCommit');
const config = getConfig();

View File

@@ -27,9 +27,9 @@ export class AzureEngine implements AiEngine {
);
}
generateCommitMessage = async (
async generateCommitMessage(
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | undefined> => {
): Promise<string | undefined> {
try {
const REQUEST_TOKENS = messages
.map((msg) => tokenCount(msg.content as string) + 4)
@@ -73,5 +73,5 @@ export class AzureEngine implements AiEngine {
throw err;
}
};
}
}

View File

@@ -28,10 +28,7 @@ export class OllamaAi implements AiEngine {
stream: false
};
try {
const response = await this.client.post(
this.client.getUri(this.config),
params
);
const response = await this.client.post('', params);
const message = response.data.message;

View File

@@ -16,13 +16,18 @@ export const assertGitRepo = async () => {
// (file) => `:(exclude)${file}`
// );
export const getIgnoredFolders = (): string[] => {
try {
return readFileSync('.opencommitignore').toString().split('\n');
} catch (e) {
return [];
}
};
export const getOpenCommitIgnore = (): Ignore => {
const ig = ignore();
try {
ig.add(readFileSync('.opencommitignore').toString().split('\n'));
} catch (e) {}
const ignorePatterns = getIgnoredFolders();
ig.add(ignorePatterns);
return ig;
};