gennady 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/gennady.js CHANGED
@@ -4,6 +4,7 @@ import { dirname, join } from 'path';
4
4
  import { CommitGen } from '../src/commit-gen/commit-gen.js';
5
5
  import { parseArgs } from '../src/utils/parse-args.js';
6
6
  import { style } from '../src/utils/style.js';
7
+ import { getSysLang } from '../src/utils/language.js';
7
8
 
8
9
  const PROMPTS_DIR = join(
9
10
  typeof __dirname !== 'string' ? dirname(fileURLToPath(import.meta.url)) : __dirname,
@@ -12,22 +13,31 @@ const PROMPTS_DIR = join(
12
13
 
13
14
  const params = parseArgs(process.argv, {
14
15
  mode: ['mode', 'm'],
16
+ oneline: ['short', 'one', 'o'],
15
17
  reviewerModel: ['model'],
16
18
  targetBranch: ['branch', 'b'],
17
19
  });
18
20
 
19
21
  const commit = new CommitGen({
20
22
  ...params,
21
- onelinePromptTemplate: readFileSync(join(PROMPTS_DIR, 'oneline-prompt.md')).toString(),
22
- detailedPromptTemplate: readFileSync(join(PROMPTS_DIR, 'detailed-prompt.md')).toString(),
23
- composerPromptTemplate: readFileSync(join(PROMPTS_DIR, 'composer-prompt.md')).toString(),
23
+ basePromptTemplate: readFileSync(join(PROMPTS_DIR, 'base-prompt.md')).toString(),
24
+ formatOnelinePromptTemplate: readFileSync(join(PROMPTS_DIR, 'format-oneline-prompt.md')).toString(),
25
+ formatDetailedPromptTemplate: readFileSync(join(PROMPTS_DIR, 'format-detailed-prompt.md')).toString(),
26
+ translatePromptTemplate: readFileSync(join(PROMPTS_DIR, 'translate-prompt.md')).toString(),
24
27
  });
25
28
 
26
29
  console.info(`🤖`, style.whiteBright.bold(`GENNADY`), `(${style.cyan(commit.reviewerModel)} → ${style.yellow(commit.mode)})`, `🗯️`);
27
30
  console.info(style.gray(`-`.repeat(30)));
28
31
 
29
32
  const msg = await commit.generate();
33
+ if (msg) {
34
+ console.info(`-`.repeat(40), '\n');
35
+ console.info(style.whiteBright(msg), '\n');
36
+ console.info(`^`.repeat(40), '\n');
30
37
 
31
- console.info(`-`.repeat(40), '\n');
32
- console.info(style.whiteBright(msg), '\n');
33
- console.info(`^`.repeat(40), '\n');
38
+ const lang = getSysLang();
39
+ if (lang !== 'en') {
40
+ console.info(await commit.translate(msg, lang), '\n');
41
+ console.info(`^`.repeat(40), '\n');
42
+ }
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gennady",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "author": "Konstantin Lebedev <ibnrubaxa@gmail.com>",
5
5
  "description": "Gennady — Generate Next-level Automated Description Intelligence",
6
6
  "keywords": [
@@ -13,10 +13,7 @@
13
13
  "license": "MIT",
14
14
  "repository": "https://github.com/rubaxa/commit-gen",
15
15
  "type": "module",
16
- "bin": {
17
- "gennady": "./cli/gennady.js",
18
- "gennadi": "./cli/gennady.js"
19
- },
16
+ "bin": "./cli/gennady.js",
20
17
  "main": "index.js",
21
18
  "scripts": {
22
19
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -1,20 +1,24 @@
1
- import { execSync as nodeExecSync } from 'node:child_process';
1
+ import { getGitCommitCount, getGitDiff } from '../git/git-cmd.js';
2
+ import { parseGitDiff } from '../git/git-diff.js';
2
3
  import { style } from '../utils/style.js';
3
4
 
4
5
  export class CommitGen {
5
6
  constructor(init) {
6
7
  this.init = {
7
8
  mode: 'auto',
9
+ oneline: false,
8
10
  reviewerModel: 'llama3:8b',
9
11
  targetBranch: undefined,
10
- onelinePromptTemplate: 'Oneline commit: {diff}',
11
- detailedPromptTemplate: 'Detailed commit: {diff}',
12
- composerPromptTemplate: 'Compose final commit message from parts: {messages}',
13
12
 
14
- maxInputTokens: init.maxInputTokens || 5000,
13
+ maxInputTokens: init.maxInputTokens || 4000,
15
14
  ollamaUrl: init.ollamaUrl || 'http://127.0.0.1:11434/api/generate',
16
15
  logger: console,
17
16
 
17
+ basePromptTemplate: undefined,
18
+ formatOnelinePromptTemplate: undefined,
19
+ formatDetailedPromptTemplate: undefined,
20
+ translatePromptTemplate: undefined,
21
+
18
22
  ...init,
19
23
  };
20
24
  }
@@ -35,112 +39,22 @@ export class CommitGen {
35
39
  return this.init.targetBranch;
36
40
  }
37
41
 
38
- execSync(cmd) {
39
- try {
40
- return nodeExecSync(cmd, { encoding: 'utf-8'});
41
- } catch (e) {
42
- return '';
43
- }
44
- }
45
-
46
- getCommitCount() {
47
- const output = this.execSync('git rev-list --count HEAD 2>/dev/null');
48
- return parseInt(output, 10) || 0;
49
- }
50
-
51
- getDiff() {
52
- if (this.init.targetBranch) {
53
- return this.execSync(`git diff ${this.init.targetBranch}`);
54
- }
55
-
56
- return this.execSync('git diff --cached');
57
- }
58
-
59
- parseDiff(diffStr) {
60
- const lines = diffStr.split('\n');
61
- const files = [];
62
- let currentFile = null;
63
-
64
- lines.forEach((line) => {
65
- if (line.startsWith('diff --git')) {
66
- if (currentFile) files.push(currentFile);
67
- const parts = line.split(' ');
68
- const bFile = parts[3] ? parts[3].replace('b/', '') : parts[2].replace('a/', '');
69
- currentFile = { filename: bFile, diff: [line] };
70
- } else if (currentFile) {
71
- currentFile.diff.push(line);
72
- }
73
- });
74
-
75
- if (currentFile) files.push(currentFile);
76
-
77
- return files;
78
- }
79
-
80
- categorizeFiles(files) {
81
- const lockFiles = [];
82
- const dotFiles = [];
83
- const docFiles = [];
84
- const renamedFiles = [];
85
- const deletedFiles = [];
86
- const changedFiles = [];
87
-
88
- files.forEach((file) => {
89
- const isRenamed = file.diff.some(
90
- (line) => line.includes('rename from') || line.includes('rename to')
91
- );
92
- const isDeleted = file.diff.some((line) => line.includes('deleted file mode'));
93
-
94
- if (/\.md$/.test(file.filename)) {
95
- docFiles.push(file);
96
- } else if (/^\./.test(file.filename)) {
97
- dotFiles.push(file);
98
- } else if (/(\.lock|-lock\.json)$/.test(file.filename)) {
99
- lockFiles.push(file);
100
- } else if (isRenamed) {
101
- renamedFiles.push(file);
102
- } else if (isDeleted) {
103
- deletedFiles.push(file);
104
- } else {
105
- changedFiles.push(file);
106
- }
107
- });
108
-
109
- return { dotFiles, docFiles, lockFiles, renamedFiles, deletedFiles, changedFiles };
110
- }
111
-
112
42
  generateDiffText(files) {
113
43
  return files
114
44
  .map((file) => `File: ${file.filename}\n${file.diff.join('\n')}`)
115
45
  .join('\n\n');
116
46
  }
117
47
 
118
- tokenCount(text) {
119
- return text.split(/\s+/).length;
120
- }
121
-
122
- splitDiffByTokens(diffText, template, maxTokens) {
123
- const templateTokens = this.tokenCount(template.replace('{diff}', ''));
124
- const availableTokens = maxTokens - templateTokens;
125
- const words = diffText.split(/\s+/);
126
- const chunks = [];
127
-
128
- for (let i = 0; i < words.length; i += availableTokens) {
129
- chunks.push(words.slice(i, i + availableTokens).join(' '));
130
- }
131
-
132
- return chunks;
133
- }
134
-
135
- async generateFromOllama(prompt) {
48
+ async fetchPrompt(prompt, context) {
136
49
  try {
137
50
  const req = await fetch(this.init.ollamaUrl, {
138
51
  method: 'POST',
139
52
  headers: {'Content-Type': 'application/json'},
140
53
  body: JSON.stringify({
141
- prompt,
142
54
  model: this.init.reviewerModel,
143
55
  stream: false,
56
+ prompt,
57
+ context,
144
58
  }),
145
59
  });
146
60
  const data = await req.json();
@@ -150,71 +64,99 @@ export class CommitGen {
150
64
  return '';
151
65
  }
152
66
  }
153
-
154
- async generateMessage(prompt) {
155
- const msg = await this.generateFromOllama(prompt);
156
- return msg.replace(/(^[\s\S]*<message>|<\/message>[\s\S]*)/g, '').replace(/\n\s*- /, '\n\n- ')
67
+ async generateCommitMessage(prompt) {
68
+ const text = await this.fetchPrompt(prompt);
69
+ return text;
157
70
  }
158
71
 
159
72
  async generate() {
160
- const commitCount = this.getCommitCount();
161
- const diffStr = this.getDiff();
162
- const parsedFiles = this.parseDiff(diffStr);
163
- const categories = this.categorizeFiles(parsedFiles);
73
+ const {maxInputTokens} = this.init
74
+ const commitCount = getGitCommitCount();
75
+ const diffStr = getGitDiff(this.init.targetBranch);
76
+ const parsedDiff = parseGitDiff(diffStr).sort((a, b) => a.tokens - b.tokens);
77
+ const codeChanged = parsedDiff.filter(f => !f.isDeleted && !f.isRenamed && f.programmingLanguage);
78
+
79
+ if (codeChanged.length === 0) {
80
+ this.logger.warn(`No changes detected, skipping commit message generation.`);
81
+ return;
82
+ }
83
+
84
+ const tokens = codeChanged.reduce((sum, file) => sum + file.tokens, 0);
85
+ const maxTokens = codeChanged[codeChanged.length - 1].tokens;
86
+ const languages = [...new Set(codeChanged.map(f => f.programmingLanguage).filter(Boolean))].join(', ');
164
87
  const mode =
165
88
  this.init.mode === 'auto'
166
89
  ? this.init.targetBranch
167
90
  ? 'detailed'
168
- : commitCount > 1 && categories.changedFiles.length < 5
91
+ : commitCount > 1 && codeChanged.length < 5
169
92
  ? 'oneline'
170
93
  : 'detailed'
171
94
  : this.init.mode;
172
95
 
173
- this.logger.info(`Generating commit message (mode: ${mode}, commits: ${commitCount}):`);
96
+ this.logger.info(`- Mode: ${style.bold.magentaBright(mode)}`);
97
+ this.logger.info(`- Languages: ${style.yellow(languages)}`);
98
+ this.logger.info(`- Tokens: ${style.bold.cyanBright(tokens)} ${style.gray(`(max per file: ${maxTokens})`)}`);
174
99
 
175
- Object.entries(categories).forEach(([key, value]) => {
176
- this.logger.info(`- ${style.bold(key)}:`, style.cyan(value.length + ''));
177
- });
100
+ if (maxTokens > maxInputTokens) {
101
+ this.logger.error(`TODO: Diff is too large, skipping commit message generation.`);
102
+ process.exit(1);
103
+ }
178
104
 
179
- const diffText = this.generateDiffText(categories.changedFiles);
105
+ const batches = codeChanged.reduce((acc, file) => {
106
+ if (!acc[0] || acc[0].tokens + file.tokens > maxInputTokens) {
107
+ if (acc[0]) {
108
+ acc[0].languages = [...new Set(acc[0].languages)];
109
+ }
180
110
 
181
- // this.logger.info(`Diff:`, diffText);
111
+ acc.unshift({tokens: 0, diff: '', languages: []});
112
+ }
182
113
 
183
- const promptTemplate = (mode === 'oneline'
184
- ? this.init.onelinePromptTemplate
185
- : this.init.detailedPromptTemplate).trim();
114
+ acc[0].tokens += file.tokens;
115
+ acc[0].diff += `File: ${file.filename}\n${file.diff.hunks.flatMap(h => h.changes).join('\n')}\n\n`;
116
+ acc[0].languages.push(file.programmingLanguage);
186
117
 
187
- const fullPrompt = promptTemplate.replace('{diff}', diffText);
188
- const totalTokens = this.tokenCount(fullPrompt);
189
- let finalMessage = '';
118
+ return acc;
119
+ }, []);
190
120
 
191
- this.logger.info(`Total tokens: ${totalTokens}`);
192
-
193
- if (totalTokens < this.init.maxInputTokens) {
194
- finalMessage = await this.generateMessage(fullPrompt);
195
- } else {
196
- const diffChunks = this.splitDiffByTokens(
197
- diffText,
198
- promptTemplate,
199
- this.init.maxInputTokens,
200
- );
201
- const partialMessages = [];
202
-
203
- for (const chunk of diffChunks) {
204
- const partPrompt = promptTemplate.replace('{diff}', chunk);
205
- const msg = await this.generateMessage(partPrompt);
206
-
207
- partialMessages.push(msg);
208
- }
209
-
210
- const composerPrompt = this.init.composerPromptTemplate.replace(
211
- '{messages}',
212
- partialMessages.join('\n'),
213
- );
214
-
215
- finalMessage = await this.generateMessage(composerPrompt);
216
- }
121
+ this.logger.info(`- Queue: ${style.bold.cyan(batches.length)}`);
122
+ this.logger.info(`-`.repeat(40));
123
+
124
+ const startGenTime = performance.now();
125
+ const results = await Promise.all(batches.map(async (batch) => {
126
+ console.info(`- Task:`, batch.tokens, batch.languages);
127
+
128
+ const prompt = this.init.basePromptTemplate
129
+ .replaceAll('{languages}', batch.languages.join('/'))
130
+ .replaceAll('{input}', batch.diff);
131
+
132
+ const msg = await this.generateCommitMessage(prompt);
133
+
134
+ return msg
135
+ }));
136
+
137
+ this.logger.info(`-`.repeat(30));
138
+ this.logger.info(`- Generation time: ${style.blueBright(((performance.now() - startGenTime) / 1000).toFixed(2))}s`);
139
+
140
+ const startFormatTime = performance.now();
141
+ const formatted = await this.toFormat(
142
+ mode === 'detailed' && !this.init.oneline ? this.init.formatDetailedPromptTemplate : this.init.formatOnelinePromptTemplate,
143
+ results.join('\n\n'),
144
+ );
145
+ this.logger.info(`- Formatting time: ${style.blueBright(((performance.now() - startFormatTime) / 1000).toFixed(2))}s`);
146
+
147
+ return formatted.trim();
148
+ }
149
+
150
+ async toFormat(format, text) {
151
+ const result = await this.fetchPrompt(format.replaceAll('{input}', text));
152
+ return result.replace(/(^[\s\S]*<message>|<\/message>[\s\S]*$)/g, '').replace(`\n-`, `\n\n-`);
153
+ }
154
+
155
+ async translate(input, lang) {
156
+ const result = await this.fetchPrompt(this.init.translatePromptTemplate
157
+ .replaceAll('{input}', input)
158
+ .replaceAll('{lang}', lang));
217
159
 
218
- return finalMessage;
160
+ return result.replace(/(^[\s\S]*<message>|<\/message>[\s\S]*)/g, '');
219
161
  }
220
162
  }
@@ -0,0 +1,32 @@
1
+ import { execSync as nodeExecSync } from 'node:child_process';
2
+
3
+ const execSync = (cmd) => {
4
+ try {
5
+ return nodeExecSync(cmd, { encoding: 'utf-8'});
6
+ } catch (e) {
7
+ return '';
8
+ }
9
+ }
10
+
11
+ const detectBaseBranch = () => {
12
+ const branchesOutput = execSync('git branch --list 2>/dev/null');
13
+ const match = branchesOutput?.match(/\s*\*?\s*(master|main)$/m);
14
+ return match?.[1] || 'master';
15
+ }
16
+
17
+ export const getGitCommitCount = () => {
18
+ try {
19
+ const output = execSync(`git rev-list --count HEAD ^${detectBaseBranch()} 2>/dev/null`);
20
+ return parseInt(output, 10) || 0;
21
+ } catch {
22
+ return 0;
23
+ }
24
+ }
25
+
26
+ export const getGitDiff = (targetBranch) => {
27
+ if (targetBranch) {
28
+ return execSync(`git diff ${targetBranch}`);
29
+ }
30
+
31
+ return execSync('git diff --cached');
32
+ }
@@ -0,0 +1,146 @@
1
+ import { getProgrammingLanguage } from "../utils/language.js";
2
+ import { countTokens } from "../utils/tokens.js";
3
+
4
+ const getCategory = (filename, metadata) => {
5
+ if (metadata && metadata.extra && Array.isArray(metadata.extra)) {
6
+ for (let line of metadata.extra) {
7
+ if (/^Binary files? /i.test(line) || /GIT binary patch/i.test(line)) {
8
+ return 'bin';
9
+ }
10
+ }
11
+ }
12
+
13
+ const categories = {
14
+ bin: /^(exe|dll|so|bin)\b$/i,
15
+ lock: /^(package-lock\.json|yarn\.lock|npm-shrinkwrap\.json|composer\.lock|podfile\.lock|go\.sum|gemfile\.lock)$/i,
16
+ json: /\.json$/i,
17
+ doc: /\.(md|markdown|txt|rst)$/i,
18
+ cfg: /^(\..+|.*\.(yml|yaml|rc|ini|conf))$/i,
19
+ img: /\.(png|jpe?g|gif|svg|bmp|tiff|ico)$/i,
20
+ css: /\.(css|less|scss|sass|styl)$/i,
21
+ html: /\.(html?)$/i,
22
+ code: /\.(js|jsx|ts|tsx|java|py|c|cpp|cs|rb|php|go|swift|m|mm|kt)$/i,
23
+ };
24
+
25
+ const category = Object.entries(categories).find(
26
+ ([, regexp]) => regexp.test(filename)
27
+ )?.[0] || 'other';
28
+
29
+ return category;
30
+ };
31
+
32
+ export const parseGitDiff = (diffText) => {
33
+ const lines = diffText.split('\n');
34
+ const result = [];
35
+ let currentFile = null;
36
+ let currentHunk = null;
37
+
38
+ lines.forEach((line) => {
39
+ if (line.startsWith('diff --git')) {
40
+ if (currentFile) {
41
+ currentFile.diff.tokens = currentFile.diff.hunks.reduce((sum, hunk) => sum + hunk.tokens, 0);
42
+ currentFile.tokens = currentFile.diff.tokens;
43
+ const extMatch = currentFile.filename.match(/\.([^.]+)$/);
44
+ currentFile.ext = extMatch ? extMatch[1].toLowerCase() : '';
45
+ currentFile.category = getCategory(currentFile.filename, currentFile.metadata);
46
+ currentFile.programmingLanguage = getProgrammingLanguage(currentFile.ext);
47
+ result.push(currentFile);
48
+ }
49
+
50
+ const fileMatch = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
51
+ const oldFilename = fileMatch ? fileMatch[1] : null;
52
+ const filename = fileMatch ? fileMatch[2] : null;
53
+
54
+ currentFile = {
55
+ isNew: false,
56
+ isDeleted: false,
57
+ isRenamed: false,
58
+ filename,
59
+ oldFilename,
60
+ newFileMode: null,
61
+ deletedFileMode: null,
62
+ diff: {
63
+ tokens: 0,
64
+ hunks: []
65
+ },
66
+ tokens: 0,
67
+ metadata: {},
68
+ ext: '',
69
+ category: 'other',
70
+ programmingLanguage: undefined
71
+ };
72
+
73
+ currentHunk = null;
74
+ } else if (currentFile && line.startsWith('new file mode')) {
75
+ const parts = line.split(' ');
76
+
77
+ currentFile.isNew = true;
78
+ currentFile.newFileMode = parts[3] || null;
79
+ currentFile.metadata.newFileMode = currentFile.newFileMode;
80
+ } else if (currentFile && line.startsWith('deleted file mode')) {
81
+ const parts = line.split(' ');
82
+
83
+ currentFile.isDeleted = true;
84
+ currentFile.deletedFileMode = parts[3] || null;
85
+ currentFile.metadata.deletedFileMode = currentFile.deletedFileMode;
86
+ } else if (currentFile && line.startsWith('old mode')) {
87
+ const parts = line.split(' ');
88
+ currentFile.metadata.oldMode = parts[2] || null;
89
+ } else if (currentFile && line.startsWith('new mode')) {
90
+ const parts = line.split(' ');
91
+ currentFile.metadata.newMode = parts[2] || null;
92
+ } else if (currentFile && line.startsWith('similarity index')) {
93
+ const parts = line.split(' ');
94
+ currentFile.metadata.similarityIndex = parts[2] || null;
95
+ } else if (currentFile && line.startsWith('rename from')) {
96
+ const from = line.substring('rename from'.length).trim();
97
+
98
+ currentFile.isRenamed = true;
99
+ currentFile.oldFilename = from;
100
+ currentFile.metadata.renameFrom = from;
101
+ } else if (currentFile && line.startsWith('rename to')) {
102
+ const to = line.substring('rename to'.length).trim();
103
+
104
+ currentFile.isRenamed = true;
105
+ currentFile.filename = to;
106
+ currentFile.metadata.renameTo = to;
107
+ } else if (currentFile && line.startsWith('index')) {
108
+ currentFile.metadata.index = line.substring('index'.length).trim();
109
+ } else if (currentFile && (line.startsWith('--- ') || line.startsWith('+++ '))) {
110
+ if (line.startsWith('--- ')) {
111
+ currentFile.metadata.oldFileMarker = line;
112
+ } else {
113
+ currentFile.metadata.newFileMarker = line;
114
+ }
115
+ } else if (currentFile && line.startsWith('@@')) {
116
+ currentHunk = {
117
+ header: line,
118
+ changes: [line],
119
+ tokens: countTokens(line)
120
+ };
121
+ currentFile.diff.hunks.push(currentHunk);
122
+ } else if (currentHunk && (line.startsWith('+') || line.startsWith('-') || line.startsWith(' '))) {
123
+ currentHunk.changes.push(line);
124
+ currentHunk.tokens += countTokens(line);
125
+ } else if (currentFile) {
126
+ if (!currentFile.metadata.extra) {
127
+ currentFile.metadata.extra = [];
128
+ }
129
+ currentFile.metadata.extra.push(line);
130
+ }
131
+ });
132
+
133
+ if (currentFile) {
134
+ const extMatch = currentFile.filename.match(/\.([^.]+)$/);
135
+
136
+ currentFile.diff.tokens = currentFile.diff.hunks.reduce((sum, hunk) => sum + hunk.tokens, 0);
137
+ currentFile.tokens = currentFile.diff.tokens;
138
+ currentFile.ext = extMatch ? extMatch[1].toLowerCase() : '';
139
+ currentFile.category = getCategory(currentFile.filename, currentFile.metadata);
140
+ currentFile.programmingLanguage = getProgrammingLanguage(currentFile.ext);
141
+
142
+ result.push(currentFile);
143
+ }
144
+
145
+ return result;
146
+ }
@@ -0,0 +1,27 @@
1
+ You are {languages} expert tasked with writing a git commit subject and description based on code changes.
2
+
3
+ **Subject:**
4
+ - Clearly summarize the key changes in one concise yet informative sentence.
5
+ - Do not describe changes for each file.
6
+ - Do not mention stylistic changes or fixed typos.
7
+ - The subject must provide enough context to understand the commit at a glance.
8
+ - End with an emoji instead of a period.
9
+
10
+ **Description:**
11
+ - Expand on the changes by listing key modifications as an unordered list.
12
+ - Do not describe changes for each file.
13
+ - Do not mention stylistic changes or fixed typos.
14
+ - Group related changes into single points.
15
+ - Each list item **must** be less than 140 characters and should not end with a period.
16
+
17
+ **Output Format (any deviation from this format is incorrect):**
18
+ <message>subject emoji
19
+ - description item 1
20
+ - description item 2</message>
21
+
22
+ **Extremely important:**
23
+ - The output must contain only commit message inside `<message>` and `</message>`.
24
+ - Do not add unnecessary words and markup, strictly follow the output format.
25
+
26
+ **Input:**
27
+ {input}
@@ -0,0 +1,16 @@
1
+ You are a {languages} expert tasked with writing a git commit subject and description based on code changes.
2
+
3
+ ### **Correct Output Format (no deviations allowed):**
4
+ <message>
5
+ [Commit subject ending with an emoji]
6
+ - [Description item 1]
7
+ - [Description item 2]
8
+ - [Description item N]
9
+ </message>
10
+
11
+ ### **Fix this incorrect commit message:**
12
+ {input}
13
+
14
+ **Extremely important:**
15
+ - The output must contain only commit message inside `<message>` and `</message>`.
16
+ - Do not add unnecessary words and markup, strictly follow the output format.
@@ -0,0 +1,11 @@
1
+ You are a {languages} expert tasked with writing a git commit subject and description based on code changes.
2
+
3
+ ### **Correct Output Format (no deviations allowed):**
4
+ <message>Oneline commit message without loss of meaning and important changes and ending with an emoji</message>
5
+
6
+ ### **Fix this incorrect commit message:**
7
+ {input}
8
+
9
+ **Extremely important:**
10
+ - The output must contain only commit message inside `<message>` and `</message>`.
11
+ - Do not add unnecessary words and markup, strictly follow the output format.
@@ -0,0 +1,11 @@
1
+ ### Translate text inside <message> and </message> tags from English to **{lang}**:
2
+
3
+ ### **Output:**
4
+ <message>Translation result</message>
5
+
6
+ ### **Extremely important:**
7
+ - The translation must be wrapped inside <message> and </message> tags.
8
+ - Do not add unnecessary words and markup, strictly follow the output format.
9
+
10
+ ### **Input:**
11
+ <message>{input}</message>
@@ -0,0 +1,39 @@
1
+ import { execSync as nodeExecSync } from 'node:child_process';
2
+
3
+ const languages = {
4
+ js: 'JavaScript',
5
+ jsx: 'JavaScript',
6
+ ts: 'TypeScript',
7
+ tsx: 'TypeScript',
8
+ java: 'Java',
9
+ py: 'Python',
10
+ c: 'C',
11
+ cpp: 'C++',
12
+ cs: 'C#',
13
+ rb: 'Ruby',
14
+ php: 'PHP',
15
+ go: 'Go',
16
+ swift: 'Swift',
17
+ m: 'Objective-C',
18
+ mm: 'Objective-C++',
19
+ kt: 'Kotlin',
20
+ html: 'HTML',
21
+ css: 'CSS',
22
+ less: 'Less',
23
+ scss: 'SCSS',
24
+ sass: 'Sass'
25
+ };
26
+
27
+ export const getProgrammingLanguage = (ext) => {
28
+ return languages[ext] || undefined;
29
+ };
30
+
31
+ export const getSysLang = () => {
32
+ try {
33
+ const values = nodeExecSync("osascript -e 'user locale of (get system info)'").toString().trim().toLowerCase().split('_');
34
+ const lang = values.filter(v => v !== 'en' && v !== 'us');
35
+ return lang[0] || 'en';
36
+ } catch {
37
+ return 'en';
38
+ }
39
+ };
@@ -0,0 +1,4 @@
1
+ export const countTokens = (text) => {
2
+ const tokens = text.match(/[\p{L}\p{N}_]+|[^\s\p{L}\p{N}_]/gu);
3
+ return tokens ? tokens.length : 0;
4
+ }
@@ -1,7 +0,0 @@
1
- You are a IT expert tasked with writing a commit message for git based on the provided code changes. Follow these rules:
2
-
3
- - Combine and summarize all messages into one preserving the structure
4
- - Messages are separated by "---"
5
-
6
- **Input:**
7
- {messages}
@@ -1,32 +0,0 @@
1
- You are a JavaScript/TypeScript expert tasked with writing a commit message for git based on the provided code changes. Follow these rules carefully:
2
-
3
- **Subject:**
4
- - Clearly summarize the key changes in one concise yet informative sentence.
5
- - Use **present tense** and **imperative mood** (e.g., "Fix bug", "Add feature").
6
- - Do **not** describe changes for each file.
7
- - Do **not** mention stylistic changes or fixed typos.
8
- - The subject **must** provide enough context to understand the commit at a glance.
9
- - End with an **emoji** instead of a period.
10
-
11
- **Description:**
12
- - Expand on the changes by listing key modifications as an unordered list.
13
- - Do **not** describe changes for each file.
14
- - Do **not** mention stylistic changes or fixed typos.
15
- - Group related changes into single points.
16
- - Each list item **must** be less than 140 characters and should not end with a period.
17
-
18
- **Output Format:**
19
- <message>subject emoji
20
- - description item 1
21
- - description item 2</message>
22
-
23
-
24
- **Important:**
25
- - The description **must** be an unordered list.
26
- - Do **not** include introductory phrases like "Here's the commit message".
27
- - The output must contain **only** the `<message>` tags and the commit message inside.
28
- - The output **must** be wrapped inside `<message>` and `</message>` tags.
29
- - Any deviation from this format is incorrect.
30
-
31
- **Input:**
32
- {diff}
@@ -1,22 +0,0 @@
1
- You are a JavaScript/TypeScript expert tasked with writing a commit message for git based on the provided code changes. Follow these rules:
2
-
3
- **Online message:**
4
- - Summarize changes as one sentence of key changes (e.g., broken code, new features).
5
- - Use present tense, imperative mood.
6
- - Do not describe changes for each file.
7
- - Do not mention stylistic changes or fixed typos.
8
- - Group related changes into a single point.
9
-
10
- **Output Format:**
11
- <message>your commit message here</message>
12
-
13
- **Input:**
14
- {diff}
15
-
16
- **Important:**
17
- - The commit message must be wrapped inside `<message>` and `</message>` tags.
18
- - The message must be a **single line** with no line breaks, lists, bullet points, or additional formatting.
19
- - Do not include any additional text, explanations, or commentary.
20
- - Do not include introductory phrases like "Here's the commit message".
21
- - The output must contain **only** the `<message>` tags and the commit message inside.
22
- - **Lists, multiple lines, or extra formatting are strictly forbidden.** Only a single sentence inside `<message>`.