git-coco 0.3.2 â 0.3.4
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/dist/commands/commit/handler.d.ts +3 -0
- package/dist/commands/commit/index.d.ts +10 -0
- package/dist/commands/commit/options.d.ts +15 -0
- package/dist/commands/types.d.ts +14 -0
- package/dist/index.d.ts +1 -3
- package/dist/index.esm.mjs +588 -486
- package/dist/index.esm.mjs.map +1 -1
- package/dist/index.js +586 -485
- package/dist/lib/config/types.d.ts +1 -1
- package/dist/lib/langchain/executeChain.d.ts +6 -0
- package/dist/lib/langchain/prompts/commitDefault.d.ts +1 -1
- package/dist/lib/langchain/prompts/summarize.d.ts +1 -1
- package/dist/lib/langchain/utils.d.ts +4 -7
- package/dist/lib/parsers/default/index.d.ts +2 -2
- package/dist/lib/parsers/default/utils/createDiffTree.d.ts +1 -0
- package/dist/lib/parsers/noResult.d.ts +4 -2
- package/dist/lib/simple-git/getChanges.d.ts +7 -4
- package/dist/lib/simple-git/getChangesByCommit.d.ts +13 -0
- package/dist/lib/simple-git/getDiff.d.ts +1 -1
- package/dist/lib/simple-git/getDiffFromCommmit.d.ts +10 -0
- package/dist/lib/simple-git/getStatus.d.ts +2 -2
- package/dist/lib/simple-git/getSummaryText.d.ts +2 -2
- package/dist/lib/simple-git/helpers.d.ts +6 -0
- package/dist/lib/types.d.ts +21 -10
- package/dist/lib/ui/editPrompt.d.ts +2 -0
- package/dist/lib/ui/editResult.d.ts +2 -0
- package/dist/lib/ui/generateAndReviewLoop.d.ts +15 -0
- package/dist/lib/ui/getUserReviewDecision.d.ts +2 -0
- package/dist/lib/ui/handleResult.d.ts +5 -0
- package/dist/lib/ui/helpers.d.ts +3 -0
- package/dist/lib/ui/logResult.d.ts +1 -0
- package/dist/lib/ui/logSuccess.d.ts +1 -0
- package/dist/stats.html +1 -1
- package/package.json +5 -5
- package/dist/commands/commit.d.ts +0 -16
- package/dist/lib/langchain/chains/llm.d.ts +0 -6
- package/dist/lib/ui.d.ts +0 -2
- package/dist/types.d.ts +0 -10
package/dist/index.esm.mjs
CHANGED
|
@@ -1,229 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import yargs from 'yargs';
|
|
3
|
-
import { select, editor } from '@inquirer/prompts';
|
|
4
|
-
import { simpleGit } from 'simple-git';
|
|
5
|
-
import * as fs from 'fs';
|
|
6
|
-
import * as os from 'os';
|
|
7
|
-
import * as path from 'path';
|
|
8
|
-
import path__default from 'path';
|
|
9
|
-
import * as ini from 'ini';
|
|
10
|
-
import { PromptTemplate } from 'langchain/prompts';
|
|
11
3
|
import pQueue from 'p-queue';
|
|
12
4
|
import { Document } from 'langchain/document';
|
|
13
5
|
import { HuggingFaceInference } from 'langchain/llms/hf';
|
|
6
|
+
import { PromptTemplate } from 'langchain/prompts';
|
|
14
7
|
import { loadSummarizationChain, LLMChain } from 'langchain/chains';
|
|
15
8
|
import { OpenAI } from 'langchain/llms/openai';
|
|
16
9
|
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
|
|
17
10
|
import { createTwoFilesPatch } from 'diff';
|
|
18
|
-
import chalk from 'chalk';
|
|
19
11
|
import GPT3NodeTokenizer from 'gpt3-tokenizer';
|
|
12
|
+
import chalk from 'chalk';
|
|
20
13
|
import ora from 'ora';
|
|
21
14
|
import now from 'performance-now';
|
|
22
15
|
import prettyMilliseconds from 'pretty-ms';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import path__default from 'path';
|
|
23
18
|
import { minimatch } from 'minimatch';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
* @returns
|
|
30
|
-
*/
|
|
31
|
-
function removeUndefined(obj) {
|
|
32
|
-
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Load environment variables
|
|
37
|
-
*
|
|
38
|
-
* @param {Config} config
|
|
39
|
-
* @returns {Config} Updated config
|
|
40
|
-
**/
|
|
41
|
-
function loadEnvConfig(config) {
|
|
42
|
-
const envConfig = {
|
|
43
|
-
model: process.env.COCO_MODEL || undefined,
|
|
44
|
-
openAIApiKey: process.env.OPENAI_API_KEY || undefined,
|
|
45
|
-
huggingFaceHubApiKey: process.env.HUGGINGFACE_HUB_API_KEY || undefined,
|
|
46
|
-
tokenLimit: process.env.COCO_TOKEN_LIMIT
|
|
47
|
-
? parseInt(process.env.COCO_TOKEN_LIMIT)
|
|
48
|
-
: undefined,
|
|
49
|
-
prompt: process.env.COCO_PROMPT,
|
|
50
|
-
mode: process.env.COCO_MODE,
|
|
51
|
-
summarizePrompt: process.env.COCO_SUMMARIZE_PROMPT,
|
|
52
|
-
ignoredFiles: process.env.COCO_IGNORED_FILES
|
|
53
|
-
? process.env.COCO_IGNORED_FILES.split(',')
|
|
54
|
-
: undefined,
|
|
55
|
-
ignoredExtensions: process.env.COCO_IGNORED_EXTENSIONS
|
|
56
|
-
? process.env.COCO_IGNORED_EXTENSIONS.split(',')
|
|
57
|
-
: undefined,
|
|
58
|
-
};
|
|
59
|
-
config = { ...config, ...removeUndefined(envConfig) };
|
|
60
|
-
return config;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Load git profile config (from ~/.gitconfig)
|
|
65
|
-
*
|
|
66
|
-
* @param {Config} config
|
|
67
|
-
* @returns {Config} Updated config
|
|
68
|
-
**/
|
|
69
|
-
function loadGitConfig(config) {
|
|
70
|
-
const gitConfigPath = path.join(os.homedir(), '.gitconfig');
|
|
71
|
-
if (fs.existsSync(gitConfigPath)) {
|
|
72
|
-
const gitConfigRaw = fs.readFileSync(gitConfigPath, 'utf-8');
|
|
73
|
-
const gitConfigParsed = ini.parse(gitConfigRaw);
|
|
74
|
-
config = {
|
|
75
|
-
...config,
|
|
76
|
-
model: gitConfigParsed.coco?.model || config.model,
|
|
77
|
-
openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
|
|
78
|
-
huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
|
|
79
|
-
tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
|
|
80
|
-
prompt: gitConfigParsed.coco?.prompt || config.prompt,
|
|
81
|
-
mode: gitConfigParsed.coco?.mode || config.mode,
|
|
82
|
-
temperature: gitConfigParsed.coco?.temperature || config.temperature,
|
|
83
|
-
summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
|
|
84
|
-
ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
|
|
85
|
-
ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
return config;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Load .gitignore in project root
|
|
93
|
-
*
|
|
94
|
-
* @param {Config} config
|
|
95
|
-
* @returns
|
|
96
|
-
*/
|
|
97
|
-
function loadGitignore(config) {
|
|
98
|
-
if (fs.existsSync('.gitignore')) {
|
|
99
|
-
const gitignoreContent = fs.readFileSync('.gitignore', 'utf-8');
|
|
100
|
-
config.ignoredFiles = [
|
|
101
|
-
...(config?.ignoredFiles || []),
|
|
102
|
-
...gitignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
103
|
-
];
|
|
104
|
-
}
|
|
105
|
-
return config;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Load .ignore in project root
|
|
109
|
-
*
|
|
110
|
-
* @param {Config} config
|
|
111
|
-
* @returns
|
|
112
|
-
*/
|
|
113
|
-
function loadIgnore(config) {
|
|
114
|
-
if (fs.existsSync('.ignore')) {
|
|
115
|
-
const ignoreContent = fs.readFileSync('.ignore', 'utf-8');
|
|
116
|
-
config.ignoredFiles = [
|
|
117
|
-
...(config?.ignoredFiles || []),
|
|
118
|
-
...ignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
119
|
-
];
|
|
120
|
-
}
|
|
121
|
-
return config;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Load project config
|
|
126
|
-
*
|
|
127
|
-
* @param {Config} config
|
|
128
|
-
* @returns {Config} Updated config
|
|
129
|
-
**/
|
|
130
|
-
function loadProjectConfig(config) {
|
|
131
|
-
if (fs.existsSync('.coco.config.json')) {
|
|
132
|
-
const projectConfig = JSON.parse(fs.readFileSync('.coco.config.json', 'utf-8'));
|
|
133
|
-
config = { ...config, ...projectConfig };
|
|
134
|
-
}
|
|
135
|
-
return config;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Load XDG config
|
|
140
|
-
*
|
|
141
|
-
* @param {Config} config
|
|
142
|
-
* @returns {Config} Updated config
|
|
143
|
-
*/
|
|
144
|
-
function loadXDGConfig(config) {
|
|
145
|
-
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
146
|
-
const xdgConfigPath = path.join(xdgConfigHome, 'coco', 'config.json');
|
|
147
|
-
if (fs.existsSync(xdgConfigPath)) {
|
|
148
|
-
const xdgConfig = JSON.parse(fs.readFileSync(xdgConfigPath, 'utf-8'));
|
|
149
|
-
config = { ...config, ...xdgConfig };
|
|
150
|
-
}
|
|
151
|
-
return config;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const template$1 = `Write informative git commit message based on the diffs & file changes provided in the "Diff Summary" section.
|
|
155
|
-
Commit Messages must have a short description that is less than 50 characters followed by a newline character and then a more verbose detailed description.
|
|
156
|
-
- Write concisely using an informal tone
|
|
157
|
-
- List significant changes
|
|
158
|
-
- DO NOT use phrases like "this commit", "this change", etc.
|
|
159
|
-
- DO NOT use specific names or files from the code
|
|
160
|
-
- Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
|
|
161
|
-
|
|
162
|
-
"""{summary}"""
|
|
163
|
-
|
|
164
|
-
Commit:`;
|
|
165
|
-
const inputVariables$1 = ['summary'];
|
|
166
|
-
const COMMIT_PROMPT = new PromptTemplate({
|
|
167
|
-
template: template$1,
|
|
168
|
-
inputVariables: inputVariables$1,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const template = `GOAL: Use functional abstractions to summarize the following text
|
|
172
|
-
|
|
173
|
-
RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
|
|
174
|
-
|
|
175
|
-
TEXT:"""{text}"""
|
|
176
|
-
`;
|
|
177
|
-
const inputVariables = ['text'];
|
|
178
|
-
const SUMMARIZE_PROMPT = new PromptTemplate({
|
|
179
|
-
template,
|
|
180
|
-
inputVariables,
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Default Config
|
|
185
|
-
*
|
|
186
|
-
* @type {Config}
|
|
187
|
-
*/
|
|
188
|
-
const DEFAULT_CONFIG = {
|
|
189
|
-
model: 'openai/gpt-3.5-turbo',
|
|
190
|
-
verbose: false,
|
|
191
|
-
tokenLimit: 1024,
|
|
192
|
-
prompt: COMMIT_PROMPT.template,
|
|
193
|
-
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
194
|
-
temperature: 0.4,
|
|
195
|
-
mode: 'stdout',
|
|
196
|
-
ignoredFiles: ['package-lock.json'],
|
|
197
|
-
ignoredExtensions: ['.map', '.lock'],
|
|
198
|
-
};
|
|
199
|
-
/**
|
|
200
|
-
* Load application config
|
|
201
|
-
*
|
|
202
|
-
* Merge config from multiple sources.
|
|
203
|
-
*
|
|
204
|
-
* \* Order of precedence:
|
|
205
|
-
* \* 1. Command line flags
|
|
206
|
-
* \* 2. Environment variables
|
|
207
|
-
* \* 3. Project config
|
|
208
|
-
* \* 4. Git config
|
|
209
|
-
* \* 5. XDG config
|
|
210
|
-
* \* 6. .gitignore
|
|
211
|
-
* \* 7. .ignore
|
|
212
|
-
* \* 8. Default config
|
|
213
|
-
*
|
|
214
|
-
* @returns {Config} application config
|
|
215
|
-
**/
|
|
216
|
-
function loadConfig(argv = {}) {
|
|
217
|
-
// Default config
|
|
218
|
-
let config = DEFAULT_CONFIG;
|
|
219
|
-
config = loadGitignore(config);
|
|
220
|
-
config = loadIgnore(config);
|
|
221
|
-
config = loadXDGConfig(config);
|
|
222
|
-
config = loadGitConfig(config);
|
|
223
|
-
config = loadProjectConfig(config);
|
|
224
|
-
config = loadEnvConfig(config);
|
|
225
|
-
return { ...config, ...argv };
|
|
226
|
-
}
|
|
19
|
+
import * as fs from 'fs';
|
|
20
|
+
import * as os from 'os';
|
|
21
|
+
import * as ini from 'ini';
|
|
22
|
+
import { simpleGit } from 'simple-git';
|
|
23
|
+
import { editor, select } from '@inquirer/prompts';
|
|
227
24
|
|
|
228
25
|
/**
|
|
229
26
|
* Extract the path from a file path string.
|
|
@@ -366,12 +163,29 @@ class DiffTreeNode {
|
|
|
366
163
|
getPath() {
|
|
367
164
|
return this.path.join('/');
|
|
368
165
|
}
|
|
166
|
+
print(indentation = 0) {
|
|
167
|
+
const indent = ' '.repeat(indentation);
|
|
168
|
+
let output = `${indent}- Path: ${this.getPath()}\n`;
|
|
169
|
+
if (this.files.length > 0) {
|
|
170
|
+
output += `${indent} Files:\n`;
|
|
171
|
+
for (const file of this.files) {
|
|
172
|
+
output += `${indent} - ${file.summary}\n`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (this.children.size > 0) {
|
|
176
|
+
output += `${indent} Children:\n`;
|
|
177
|
+
for (const [, child] of this.children) {
|
|
178
|
+
output += child.print(indentation + 4);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return output;
|
|
182
|
+
}
|
|
369
183
|
}
|
|
370
184
|
const createDiffTree = (changes) => {
|
|
371
185
|
const root = new DiffTreeNode();
|
|
372
186
|
for (const change of changes) {
|
|
373
187
|
let currentParent = root;
|
|
374
|
-
const parts = change.
|
|
188
|
+
const parts = change.filePath.split('/');
|
|
375
189
|
parts.pop();
|
|
376
190
|
for (const part of parts) {
|
|
377
191
|
let childNode = currentParent.getChild(part);
|
|
@@ -383,8 +197,8 @@ const createDiffTree = (changes) => {
|
|
|
383
197
|
}
|
|
384
198
|
// Create a NodeFile object and add it to the parent
|
|
385
199
|
currentParent.addFile({
|
|
386
|
-
|
|
387
|
-
|
|
200
|
+
filePath: change.filePath,
|
|
201
|
+
oldFilePath: change.oldFilePath,
|
|
388
202
|
summary: change.summary,
|
|
389
203
|
status: change.status,
|
|
390
204
|
});
|
|
@@ -402,11 +216,11 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
|
|
|
402
216
|
// TODO: Swap out the GPT3Tokenizer for LangChain tokenizer
|
|
403
217
|
const tokenizedDiff = tokenizer.encode(diff).text;
|
|
404
218
|
const tokenCount = tokenizedDiff.length;
|
|
405
|
-
logger.verbose(`Collected diff for ${nodeFile.
|
|
219
|
+
logger.verbose(`Collected diff for ${nodeFile.filePath} (${tokenCount} tokens)`, {
|
|
406
220
|
color: 'magenta',
|
|
407
221
|
});
|
|
408
222
|
return {
|
|
409
|
-
file: nodeFile.
|
|
223
|
+
file: nodeFile.filePath,
|
|
410
224
|
summary: nodeFile.summary,
|
|
411
225
|
diff,
|
|
412
226
|
tokenCount,
|
|
@@ -431,7 +245,7 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
|
|
|
431
245
|
* @param configuration
|
|
432
246
|
* @returns LLM Model
|
|
433
247
|
*/
|
|
434
|
-
function getModel(name, key, fields
|
|
248
|
+
function getModel(name, key, fields) {
|
|
435
249
|
const [llm, model] = name.split(/\/(.*)/s);
|
|
436
250
|
if (!model) {
|
|
437
251
|
throw new Error(`Invalid model: ${name}`);
|
|
@@ -450,7 +264,7 @@ function getModel(name, key, fields, configuration) {
|
|
|
450
264
|
openAIApiKey: key,
|
|
451
265
|
modelName: model,
|
|
452
266
|
...fields,
|
|
453
|
-
}
|
|
267
|
+
});
|
|
454
268
|
}
|
|
455
269
|
}
|
|
456
270
|
/**
|
|
@@ -459,7 +273,7 @@ function getModel(name, key, fields, configuration) {
|
|
|
459
273
|
* @param options
|
|
460
274
|
* @returns
|
|
461
275
|
*/
|
|
462
|
-
function
|
|
276
|
+
function getApiKeyForModel(name, options) {
|
|
463
277
|
const [llm, model] = name.split(/\/(.*)/s);
|
|
464
278
|
if (!model) {
|
|
465
279
|
throw new Error(`Invalid model: ${name}`);
|
|
@@ -516,19 +330,47 @@ function validatePromptTemplate(text, inputVariables) {
|
|
|
516
330
|
return true;
|
|
517
331
|
}
|
|
518
332
|
|
|
519
|
-
const
|
|
520
|
-
|
|
333
|
+
const template$1 = `GOAL: Use functional abstractions to summarize the following text
|
|
334
|
+
|
|
335
|
+
RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
|
|
336
|
+
|
|
337
|
+
TEXT:"""{text}"""
|
|
338
|
+
`;
|
|
339
|
+
const inputVariables$1 = ['text'];
|
|
340
|
+
const SUMMARIZE_PROMPT = new PromptTemplate({
|
|
341
|
+
template: template$1,
|
|
342
|
+
inputVariables: inputVariables$1,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const parseDefaultFileDiff = async (nodeFile, commit = '--staged', git) => {
|
|
346
|
+
if (commit !== '--staged') {
|
|
347
|
+
return await git.diff([`${commit}~1..${commit}`, '--', nodeFile.filePath]);
|
|
348
|
+
}
|
|
349
|
+
return await git.diff([commit, nodeFile.filePath]);
|
|
521
350
|
};
|
|
522
|
-
const parseRenamedFileDiff = async (nodeFile, git, logger) => {
|
|
351
|
+
const parseRenamedFileDiff = async (nodeFile, commit, git, logger) => {
|
|
523
352
|
let result = '';
|
|
524
|
-
const
|
|
353
|
+
const oldFilePath = nodeFile?.oldFilePath || nodeFile.filePath;
|
|
354
|
+
let previousCommitHash = 'HEAD';
|
|
355
|
+
let newCommitHash = '';
|
|
356
|
+
if (commit !== '--staged') {
|
|
357
|
+
try {
|
|
358
|
+
previousCommitHash = await git.revparse([`${commit}~1`]);
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
logger.verbose(`Error getting previous commit hash for ${nodeFile.filePath}`, {
|
|
362
|
+
color: 'red',
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
newCommitHash = commit;
|
|
366
|
+
}
|
|
525
367
|
try {
|
|
526
|
-
const [
|
|
527
|
-
git.show([
|
|
528
|
-
git.show([
|
|
368
|
+
const [previousContent, newContent] = await Promise.all([
|
|
369
|
+
git.show([`${previousCommitHash}:${oldFilePath}`]),
|
|
370
|
+
git.show([`${newCommitHash}:${nodeFile.filePath}`]),
|
|
529
371
|
]);
|
|
530
|
-
if (
|
|
531
|
-
result = createTwoFilesPatch(
|
|
372
|
+
if (previousContent !== newContent) {
|
|
373
|
+
result = createTwoFilesPatch(oldFilePath, nodeFile.filePath, previousContent, newContent, '', '', {
|
|
532
374
|
context: 3,
|
|
533
375
|
});
|
|
534
376
|
// remove the first 4 lines of the patch (they contain the old and new file names)
|
|
@@ -539,27 +381,27 @@ const parseRenamedFileDiff = async (nodeFile, git, logger) => {
|
|
|
539
381
|
}
|
|
540
382
|
}
|
|
541
383
|
catch (err) {
|
|
542
|
-
logger.verbose(`Error comparing file contents for ${nodeFile.
|
|
384
|
+
logger.verbose(`Error comparing file contents for ${nodeFile.filePath}`, { color: 'red' });
|
|
543
385
|
result = 'Error comparing file contents.';
|
|
544
386
|
}
|
|
545
387
|
return result;
|
|
546
388
|
};
|
|
547
|
-
const getDiff = async (nodeFile, { git, logger, }) => {
|
|
389
|
+
const getDiff = async (nodeFile, commit, { git, logger, }) => {
|
|
548
390
|
if (nodeFile.status === 'deleted') {
|
|
549
391
|
return 'This file has been deleted.';
|
|
550
392
|
}
|
|
551
|
-
if (nodeFile.status === 'renamed' && nodeFile.
|
|
552
|
-
const renamedDiff = await parseRenamedFileDiff(nodeFile, git, logger);
|
|
393
|
+
if (nodeFile.status === 'renamed' && nodeFile.oldFilePath) {
|
|
394
|
+
const renamedDiff = await parseRenamedFileDiff(nodeFile, commit, git, logger);
|
|
553
395
|
return renamedDiff;
|
|
554
396
|
}
|
|
555
397
|
// If not deleted or renamed, get the diff from the index
|
|
556
|
-
const defaultDiff = await parseDefaultFileDiff(nodeFile, git);
|
|
398
|
+
const defaultDiff = await parseDefaultFileDiff(nodeFile, commit, git);
|
|
557
399
|
return defaultDiff;
|
|
558
400
|
};
|
|
559
401
|
|
|
560
402
|
const MAX_TOKENS_PER_SUMMARY = 2048;
|
|
561
|
-
|
|
562
|
-
const textSplitter = getTextSplitter({ chunkSize: 2000, chunkOverlap: 125
|
|
403
|
+
async function fileChangeParser({ changes, commit, options: { tokenizer, git, model, logger }, }) {
|
|
404
|
+
const textSplitter = getTextSplitter({ chunkSize: 2000, chunkOverlap: 125 });
|
|
563
405
|
const summarizationChain = getChain(model, {
|
|
564
406
|
type: 'map_reduce',
|
|
565
407
|
combineMapPrompt: SUMMARIZE_PROMPT,
|
|
@@ -570,7 +412,7 @@ const fileChangeParser = async (changes, { tokenizer, git, model, logger }) => {
|
|
|
570
412
|
logger.stopTimer('Created file hierarchy');
|
|
571
413
|
// Collect diffs
|
|
572
414
|
logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
|
|
573
|
-
const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, { git, logger }), tokenizer, logger);
|
|
415
|
+
const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, commit, { git, logger }), tokenizer, logger);
|
|
574
416
|
logger.stopSpinner('Diffs Collected').stopTimer();
|
|
575
417
|
// Summarize diffs
|
|
576
418
|
logger.startTimer();
|
|
@@ -579,19 +421,11 @@ const fileChangeParser = async (changes, { tokenizer, git, model, logger }) => {
|
|
|
579
421
|
maxTokens: MAX_TOKENS_PER_SUMMARY,
|
|
580
422
|
textSplitter,
|
|
581
423
|
chain: summarizationChain,
|
|
582
|
-
logger
|
|
424
|
+
logger,
|
|
583
425
|
});
|
|
584
426
|
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
585
427
|
return summary;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const SEPERATOR = chalk.blue('----------------');
|
|
589
|
-
const logCommit = (commit) => {
|
|
590
|
-
console.log(`\n${chalk.bgBlue(chalk.bold('Proposed Commit:'))}\n${SEPERATOR}\n${commit}\n${SEPERATOR}\n`);
|
|
591
|
-
};
|
|
592
|
-
const logSuccess = () => {
|
|
593
|
-
console.log(chalk.green(chalk.bold('\nAll set! ðĶūðĪ')));
|
|
594
|
-
};
|
|
428
|
+
}
|
|
595
429
|
|
|
596
430
|
/**
|
|
597
431
|
* Wrapper around GPT3NodeTokenizer to handle default export.
|
|
@@ -666,76 +500,266 @@ class Logger {
|
|
|
666
500
|
}
|
|
667
501
|
}
|
|
668
502
|
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
throw new Error(`LLMChain response error: ${res.error}`);
|
|
688
|
-
}
|
|
689
|
-
return res.text.trim();
|
|
690
|
-
};
|
|
503
|
+
const template = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
|
|
504
|
+
Commit Messages must have a short description that is less than 50 characters followed by a newline character and then a more verbose detailed description.
|
|
505
|
+
|
|
506
|
+
- Typically a hyphen or asterisk is used for the bullet
|
|
507
|
+
- Write concisely using an informal tone
|
|
508
|
+
- DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
|
|
509
|
+
- DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
|
|
510
|
+
- DO NOT use specific names or files from the code
|
|
511
|
+
- Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
|
|
512
|
+
|
|
513
|
+
"""{summary}"""
|
|
514
|
+
|
|
515
|
+
Commit:`;
|
|
516
|
+
const inputVariables = ['summary'];
|
|
517
|
+
const COMMIT_PROMPT = new PromptTemplate({
|
|
518
|
+
template,
|
|
519
|
+
inputVariables,
|
|
520
|
+
});
|
|
691
521
|
|
|
692
522
|
const getStatus = (file, location = 'index') => {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
523
|
+
if ('index' in file && 'working_dir' in file) {
|
|
524
|
+
const statusCode = file[location];
|
|
525
|
+
switch (statusCode) {
|
|
526
|
+
case 'A':
|
|
527
|
+
return 'added';
|
|
528
|
+
case 'D':
|
|
529
|
+
return 'deleted';
|
|
530
|
+
case 'M':
|
|
531
|
+
return 'modified';
|
|
532
|
+
case 'R':
|
|
533
|
+
return 'renamed';
|
|
534
|
+
case '?':
|
|
535
|
+
return 'untracked';
|
|
536
|
+
default:
|
|
537
|
+
return 'unknown';
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
else if ('changes' in file && 'binary' in file) {
|
|
541
|
+
if (file.changes === 0)
|
|
542
|
+
return 'untracked';
|
|
543
|
+
if (file.file.includes('=>'))
|
|
544
|
+
return 'renamed';
|
|
545
|
+
if (file.deletions === 0 && file.insertions > 0)
|
|
546
|
+
return 'added';
|
|
547
|
+
if (file.insertions === 0 && file.deletions > 0)
|
|
548
|
+
return 'deleted';
|
|
549
|
+
if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
|
|
550
|
+
return 'modified';
|
|
551
|
+
return 'unknown';
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
throw new Error("Invalid file type");
|
|
714
555
|
}
|
|
715
|
-
return status;
|
|
716
556
|
};
|
|
717
557
|
|
|
718
558
|
const getSummaryText = (file, change) => {
|
|
719
559
|
const status = change.status || getStatus(file);
|
|
720
|
-
|
|
721
|
-
|
|
560
|
+
let filePath;
|
|
561
|
+
if ('path' in file) {
|
|
562
|
+
filePath = file.path;
|
|
563
|
+
}
|
|
564
|
+
else if ('file' in file) {
|
|
565
|
+
filePath = change?.filePath || file.file;
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
throw new Error("Invalid file type");
|
|
569
|
+
}
|
|
570
|
+
if (change.oldFilePath) {
|
|
571
|
+
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
572
|
+
}
|
|
573
|
+
return `${status}: ${filePath}`;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Returns a new object with all undefined keys removed
|
|
578
|
+
*
|
|
579
|
+
* @param obj Object to remove undefined keys from
|
|
580
|
+
* @returns
|
|
581
|
+
*/
|
|
582
|
+
function removeUndefined(obj) {
|
|
583
|
+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Load environment variables
|
|
588
|
+
*
|
|
589
|
+
* @param {Config} config
|
|
590
|
+
* @returns {Config} Updated config
|
|
591
|
+
**/
|
|
592
|
+
function loadEnvConfig(config) {
|
|
593
|
+
const envConfig = {
|
|
594
|
+
model: process.env.COCO_MODEL || undefined,
|
|
595
|
+
openAIApiKey: process.env.OPENAI_API_KEY || undefined,
|
|
596
|
+
huggingFaceHubApiKey: process.env.HUGGINGFACE_HUB_API_KEY || undefined,
|
|
597
|
+
tokenLimit: process.env.COCO_TOKEN_LIMIT
|
|
598
|
+
? parseInt(process.env.COCO_TOKEN_LIMIT)
|
|
599
|
+
: undefined,
|
|
600
|
+
prompt: process.env.COCO_PROMPT,
|
|
601
|
+
mode: process.env.COCO_MODE,
|
|
602
|
+
summarizePrompt: process.env.COCO_SUMMARIZE_PROMPT,
|
|
603
|
+
ignoredFiles: process.env.COCO_IGNORED_FILES
|
|
604
|
+
? process.env.COCO_IGNORED_FILES.split(',')
|
|
605
|
+
: undefined,
|
|
606
|
+
ignoredExtensions: process.env.COCO_IGNORED_EXTENSIONS
|
|
607
|
+
? process.env.COCO_IGNORED_EXTENSIONS.split(',')
|
|
608
|
+
: undefined,
|
|
609
|
+
};
|
|
610
|
+
config = { ...config, ...removeUndefined(envConfig) };
|
|
611
|
+
return config;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Load git profile config (from ~/.gitconfig)
|
|
616
|
+
*
|
|
617
|
+
* @param {Config} config
|
|
618
|
+
* @returns {Config} Updated config
|
|
619
|
+
**/
|
|
620
|
+
function loadGitConfig(config) {
|
|
621
|
+
const gitConfigPath = path.join(os.homedir(), '.gitconfig');
|
|
622
|
+
if (fs.existsSync(gitConfigPath)) {
|
|
623
|
+
const gitConfigRaw = fs.readFileSync(gitConfigPath, 'utf-8');
|
|
624
|
+
const gitConfigParsed = ini.parse(gitConfigRaw);
|
|
625
|
+
config = {
|
|
626
|
+
...config,
|
|
627
|
+
model: gitConfigParsed.coco?.model || config.model,
|
|
628
|
+
openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
|
|
629
|
+
huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
|
|
630
|
+
tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
|
|
631
|
+
prompt: gitConfigParsed.coco?.prompt || config.prompt,
|
|
632
|
+
mode: gitConfigParsed.coco?.mode || config.mode,
|
|
633
|
+
temperature: gitConfigParsed.coco?.temperature || config.temperature,
|
|
634
|
+
summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
|
|
635
|
+
ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
|
|
636
|
+
ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
return config;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Load .gitignore in project root
|
|
644
|
+
*
|
|
645
|
+
* @param {Config} config
|
|
646
|
+
* @returns
|
|
647
|
+
*/
|
|
648
|
+
function loadGitignore(config) {
|
|
649
|
+
if (fs.existsSync('.gitignore')) {
|
|
650
|
+
const gitignoreContent = fs.readFileSync('.gitignore', 'utf-8');
|
|
651
|
+
config.ignoredFiles = [
|
|
652
|
+
...(config?.ignoredFiles || []),
|
|
653
|
+
...gitignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
654
|
+
];
|
|
655
|
+
}
|
|
656
|
+
return config;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Load .ignore in project root
|
|
660
|
+
*
|
|
661
|
+
* @param {Config} config
|
|
662
|
+
* @returns
|
|
663
|
+
*/
|
|
664
|
+
function loadIgnore(config) {
|
|
665
|
+
if (fs.existsSync('.ignore')) {
|
|
666
|
+
const ignoreContent = fs.readFileSync('.ignore', 'utf-8');
|
|
667
|
+
config.ignoredFiles = [
|
|
668
|
+
...(config?.ignoredFiles || []),
|
|
669
|
+
...ignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
670
|
+
];
|
|
671
|
+
}
|
|
672
|
+
return config;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Load project config
|
|
677
|
+
*
|
|
678
|
+
* @param {Config} config
|
|
679
|
+
* @returns {Config} Updated config
|
|
680
|
+
**/
|
|
681
|
+
function loadProjectConfig(config) {
|
|
682
|
+
if (fs.existsSync('.coco.config.json')) {
|
|
683
|
+
const projectConfig = JSON.parse(fs.readFileSync('.coco.config.json', 'utf-8'));
|
|
684
|
+
config = { ...config, ...projectConfig };
|
|
722
685
|
}
|
|
723
|
-
return
|
|
686
|
+
return config;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Load XDG config
|
|
691
|
+
*
|
|
692
|
+
* @param {Config} config
|
|
693
|
+
* @returns {Config} Updated config
|
|
694
|
+
*/
|
|
695
|
+
function loadXDGConfig(config) {
|
|
696
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
697
|
+
const xdgConfigPath = path.join(xdgConfigHome, 'coco', 'config.json');
|
|
698
|
+
if (fs.existsSync(xdgConfigPath)) {
|
|
699
|
+
const xdgConfig = JSON.parse(fs.readFileSync(xdgConfigPath, 'utf-8'));
|
|
700
|
+
config = { ...config, ...xdgConfig };
|
|
701
|
+
}
|
|
702
|
+
return config;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Default Config
|
|
707
|
+
*
|
|
708
|
+
* @type {Config}
|
|
709
|
+
*/
|
|
710
|
+
const DEFAULT_CONFIG = {
|
|
711
|
+
model: 'openai/gpt-4',
|
|
712
|
+
verbose: false,
|
|
713
|
+
tokenLimit: 1024,
|
|
714
|
+
prompt: COMMIT_PROMPT.template,
|
|
715
|
+
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
716
|
+
temperature: 0.4,
|
|
717
|
+
mode: 'stdout',
|
|
718
|
+
ignoredFiles: ['package-lock.json'],
|
|
719
|
+
ignoredExtensions: ['.map', '.lock'],
|
|
724
720
|
};
|
|
721
|
+
/**
|
|
722
|
+
* Load application config
|
|
723
|
+
*
|
|
724
|
+
* Merge config from multiple sources.
|
|
725
|
+
*
|
|
726
|
+
* \* Order of precedence:
|
|
727
|
+
* \* 1. Command line flags
|
|
728
|
+
* \* 2. Environment variables
|
|
729
|
+
* \* 3. Project config
|
|
730
|
+
* \* 4. Git config
|
|
731
|
+
* \* 5. XDG config
|
|
732
|
+
* \* 6. .gitignore
|
|
733
|
+
* \* 7. .ignore
|
|
734
|
+
* \* 8. Default config
|
|
735
|
+
*
|
|
736
|
+
* @returns {Config} application config
|
|
737
|
+
**/
|
|
738
|
+
function loadConfig(argv = {}) {
|
|
739
|
+
// Default config
|
|
740
|
+
let config = DEFAULT_CONFIG;
|
|
741
|
+
config = loadGitignore(config);
|
|
742
|
+
config = loadIgnore(config);
|
|
743
|
+
config = loadXDGConfig(config);
|
|
744
|
+
config = loadGitConfig(config);
|
|
745
|
+
config = loadProjectConfig(config);
|
|
746
|
+
config = loadEnvConfig(config);
|
|
747
|
+
return { ...config, ...argv };
|
|
748
|
+
}
|
|
725
749
|
|
|
726
750
|
const config = loadConfig();
|
|
727
751
|
const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
|
|
728
752
|
const DEFAULT_IGNORED_EXTENSIONS = config?.ignoredExtensions?.length ? config.ignoredExtensions : [];
|
|
729
|
-
async function getChanges(git, options
|
|
730
|
-
const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options;
|
|
753
|
+
async function getChanges({ git, options }) {
|
|
754
|
+
const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options || {};
|
|
731
755
|
const staged = [];
|
|
732
756
|
const unstaged = [];
|
|
733
757
|
const untracked = [];
|
|
734
758
|
const status = await git.status();
|
|
735
759
|
status.files.forEach((file) => {
|
|
736
760
|
const fileChange = {
|
|
737
|
-
|
|
738
|
-
|
|
761
|
+
filePath: file.path,
|
|
762
|
+
oldFilePath: status.renamed.filter((renamed) => renamed.to === file.path)[0]?.from,
|
|
739
763
|
};
|
|
740
764
|
// Unstaged files
|
|
741
765
|
if (file.working_dir !== '?' && file.working_dir !== ' ') {
|
|
@@ -758,16 +782,19 @@ async function getChanges(git, options = {}) {
|
|
|
758
782
|
});
|
|
759
783
|
const ignoredExtensionsSet = new Set(ignoredExtensions.map((extension) => extension.toLowerCase()));
|
|
760
784
|
const filteredStaged = staged.filter((file) => {
|
|
761
|
-
const extension = path__default.extname(file.
|
|
762
|
-
return !ignoredExtensionsSet.has(extension) &&
|
|
785
|
+
const extension = path__default.extname(file.filePath).toLowerCase();
|
|
786
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
787
|
+
!ignoredFiles.some((ignoredPattern) => minimatch(file.filePath, ignoredPattern)));
|
|
763
788
|
});
|
|
764
789
|
const filteredUnstaged = unstaged.filter((file) => {
|
|
765
|
-
const extension = path__default.extname(file.
|
|
766
|
-
return !ignoredExtensionsSet.has(extension) &&
|
|
790
|
+
const extension = path__default.extname(file.filePath).toLowerCase();
|
|
791
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
792
|
+
!ignoredFiles.some((ignoredPattern) => minimatch(file.filePath, ignoredPattern)));
|
|
767
793
|
});
|
|
768
794
|
const filteredUntracked = untracked.filter((file) => {
|
|
769
|
-
const extension = path__default.extname(file.
|
|
770
|
-
return !ignoredExtensionsSet.has(extension) &&
|
|
795
|
+
const extension = path__default.extname(file.filePath).toLowerCase();
|
|
796
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
797
|
+
!ignoredFiles.some((ignoredPattern) => minimatch(file.filePath, ignoredPattern)));
|
|
771
798
|
});
|
|
772
799
|
return {
|
|
773
800
|
staged: filteredStaged,
|
|
@@ -776,8 +803,8 @@ async function getChanges(git, options = {}) {
|
|
|
776
803
|
};
|
|
777
804
|
}
|
|
778
805
|
|
|
779
|
-
|
|
780
|
-
const { staged, unstaged, untracked } = await getChanges(git);
|
|
806
|
+
async function noResult({ git, logger }) {
|
|
807
|
+
const { staged, unstaged, untracked } = await getChanges({ git });
|
|
781
808
|
const hasStaged = staged && staged.length > 0;
|
|
782
809
|
const hasUnstaged = unstaged && unstaged.length > 0;
|
|
783
810
|
const hasUntracked = untracked && untracked.length > 0;
|
|
@@ -803,18 +830,254 @@ const noResult = async ({ git, logger }) => {
|
|
|
803
830
|
else {
|
|
804
831
|
logger.log('No repo changes detected. ð', { color: 'blue' });
|
|
805
832
|
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const isInteractive = (argv) => {
|
|
836
|
+
return argv?.mode === 'interactive' || argv.interactive;
|
|
837
|
+
};
|
|
838
|
+
const SEPERATOR = chalk.blue('----------------');
|
|
839
|
+
|
|
840
|
+
function logResult(result) {
|
|
841
|
+
console.log(`\n${chalk.bgBlue(chalk.bold('Proposed Commit:'))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async function editResult(result, options) {
|
|
845
|
+
if (options.openInEditor) {
|
|
846
|
+
return await editor({
|
|
847
|
+
message: 'Edit the commit message',
|
|
848
|
+
default: result,
|
|
849
|
+
waitForUseInput: false,
|
|
850
|
+
validate: (text) => (text ? true : 'Commit message cannot be empty'),
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
return result;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async function getUserReviewDecision() {
|
|
857
|
+
return await select({
|
|
858
|
+
message: 'Would you like to make any changes to the commit message?',
|
|
859
|
+
choices: [
|
|
860
|
+
{
|
|
861
|
+
name: 'âĻ Looks good!',
|
|
862
|
+
value: 'approve',
|
|
863
|
+
description: 'Commit staged changes with generated commit message',
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
name: 'ð Edit',
|
|
867
|
+
value: 'edit',
|
|
868
|
+
description: 'Edit the commit message before proceeding',
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
name: 'ðŠķ Modify Prompt',
|
|
872
|
+
value: 'modifyPrompt',
|
|
873
|
+
description: 'Modify the prompt template and regenerate the commit message',
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
name: 'ð Retry - Message Only',
|
|
877
|
+
value: 'retryMessageOnly',
|
|
878
|
+
description: 'Restart the function execution from generating the commit message',
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
name: 'ð Retry - Full',
|
|
882
|
+
value: 'retryFull',
|
|
883
|
+
description: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
name: 'ðĢ Cancel',
|
|
887
|
+
value: 'cancel',
|
|
888
|
+
},
|
|
889
|
+
],
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function editPrompt(options) {
|
|
894
|
+
return await editor({
|
|
895
|
+
message: 'Edit the prompt',
|
|
896
|
+
default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
|
|
897
|
+
waitForUseInput: false,
|
|
898
|
+
validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
async function generateAndReviewLoop({ factory, parser, noResult, agent, options, }) {
|
|
903
|
+
const { logger } = options;
|
|
904
|
+
let continueLoop = true;
|
|
905
|
+
let modifyPrompt = false;
|
|
906
|
+
let context = '';
|
|
907
|
+
let result = '';
|
|
908
|
+
const changes = await factory();
|
|
909
|
+
// if we don't have any changes, bail.
|
|
910
|
+
if (!changes || !changes.length) {
|
|
911
|
+
await noResult(options);
|
|
912
|
+
}
|
|
913
|
+
while (continueLoop) {
|
|
914
|
+
if (!context.length) {
|
|
915
|
+
context = await parser(changes, result, options);
|
|
916
|
+
}
|
|
917
|
+
// if we still don't have a context, bail.
|
|
918
|
+
if (!context.length) {
|
|
919
|
+
await noResult(options);
|
|
920
|
+
}
|
|
921
|
+
if (modifyPrompt) {
|
|
922
|
+
options.prompt = await editPrompt(options);
|
|
923
|
+
}
|
|
924
|
+
logger.startTimer().startSpinner(`Generating Message\n`, {
|
|
925
|
+
color: 'blue',
|
|
926
|
+
});
|
|
927
|
+
result = await agent(context, options);
|
|
928
|
+
if (!result) {
|
|
929
|
+
logger.stopSpinner('ð Agent failed to generate message.', {
|
|
930
|
+
mode: 'fail',
|
|
931
|
+
color: 'red',
|
|
932
|
+
});
|
|
933
|
+
process.exit(0);
|
|
934
|
+
}
|
|
935
|
+
logger
|
|
936
|
+
.stopSpinner('Generated Commit Message', {
|
|
937
|
+
color: 'green',
|
|
938
|
+
mode: 'succeed',
|
|
939
|
+
})
|
|
940
|
+
.stopTimer();
|
|
941
|
+
if (options?.interactive) {
|
|
942
|
+
logResult(result);
|
|
943
|
+
const reviewAnswer = await getUserReviewDecision();
|
|
944
|
+
if (reviewAnswer === 'cancel') {
|
|
945
|
+
process.exit(0);
|
|
946
|
+
}
|
|
947
|
+
if (reviewAnswer === 'edit') {
|
|
948
|
+
options.openInEditor = true;
|
|
949
|
+
}
|
|
950
|
+
if (reviewAnswer === 'retryFull') {
|
|
951
|
+
context = '';
|
|
952
|
+
result = '';
|
|
953
|
+
options.prompt = '';
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
if (reviewAnswer === 'retryMessageOnly') {
|
|
957
|
+
modifyPrompt = false;
|
|
958
|
+
result = '';
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (reviewAnswer === 'modifyPrompt') {
|
|
962
|
+
modifyPrompt = true;
|
|
963
|
+
result = '';
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
// if we're here, we're done.
|
|
968
|
+
result = await editResult(result, options);
|
|
969
|
+
continueLoop = false;
|
|
970
|
+
}
|
|
971
|
+
return result;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const executeChain = async ({ llm, prompt, variables }) => {
|
|
975
|
+
if (!llm || !prompt || !variables) {
|
|
976
|
+
throw new Error('The input parameters "llm", "prompt", and "variables" are all required.');
|
|
977
|
+
}
|
|
978
|
+
const chain = new LLMChain({ llm, prompt });
|
|
979
|
+
let res;
|
|
980
|
+
try {
|
|
981
|
+
res = await chain.call(variables);
|
|
982
|
+
}
|
|
983
|
+
catch (error) {
|
|
984
|
+
if (error instanceof Error) {
|
|
985
|
+
throw new Error(`LLMChain call error: ${error.message}`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (!res) {
|
|
989
|
+
throw new Error('Empty response from LLMChain call');
|
|
990
|
+
}
|
|
991
|
+
if (res.error) {
|
|
992
|
+
throw new Error(`LLMChain response error: ${res.error}`);
|
|
993
|
+
}
|
|
994
|
+
return res.text.trim();
|
|
806
995
|
};
|
|
807
996
|
|
|
808
997
|
async function createCommit(commitMsg, git) {
|
|
809
998
|
return await git.commit(commitMsg);
|
|
810
999
|
}
|
|
811
1000
|
|
|
812
|
-
|
|
1001
|
+
const logSuccess = () => {
|
|
1002
|
+
console.log(chalk.green(chalk.bold('\nAll set! ðĶūðĪ')));
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
const handleResult = async (result, { mode, git }) => {
|
|
1006
|
+
// Handle resulting commit message
|
|
1007
|
+
switch (mode) {
|
|
1008
|
+
case 'interactive':
|
|
1009
|
+
await createCommit(result, git);
|
|
1010
|
+
logSuccess();
|
|
1011
|
+
break;
|
|
1012
|
+
case 'stdout':
|
|
1013
|
+
default:
|
|
1014
|
+
process.stdout.write(result, 'utf8');
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
process.exit(0);
|
|
1018
|
+
};
|
|
1019
|
+
|
|
813
1020
|
const tokenizer = getTokenizer();
|
|
814
1021
|
const git = simpleGit();
|
|
815
|
-
|
|
816
|
-
const
|
|
817
|
-
const
|
|
1022
|
+
async function handler(argv) {
|
|
1023
|
+
const options = loadConfig(argv);
|
|
1024
|
+
const logger = new Logger(options);
|
|
1025
|
+
const key = getApiKeyForModel(options.model, options);
|
|
1026
|
+
if (!key) {
|
|
1027
|
+
logger.log(`No API Key found. ðïļðŠ`, { color: 'red' });
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
}
|
|
1030
|
+
const model = getModel(options.model, key, {
|
|
1031
|
+
temperature: 0.4,
|
|
1032
|
+
maxConcurrency: 10,
|
|
1033
|
+
});
|
|
1034
|
+
const INTERACTIVE = isInteractive(options);
|
|
1035
|
+
async function factory() {
|
|
1036
|
+
const changes = await getChanges({ git });
|
|
1037
|
+
return changes.staged;
|
|
1038
|
+
}
|
|
1039
|
+
async function parser(changes) {
|
|
1040
|
+
return await fileChangeParser({
|
|
1041
|
+
changes,
|
|
1042
|
+
commit: '--staged',
|
|
1043
|
+
options: { tokenizer, git, model, logger },
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
const commitMsg = await generateAndReviewLoop({
|
|
1047
|
+
factory,
|
|
1048
|
+
parser,
|
|
1049
|
+
agent: async (context, options) => {
|
|
1050
|
+
return await executeChain({
|
|
1051
|
+
llm: model,
|
|
1052
|
+
prompt: getPrompt({
|
|
1053
|
+
template: options.prompt,
|
|
1054
|
+
variables: COMMIT_PROMPT.inputVariables,
|
|
1055
|
+
fallback: COMMIT_PROMPT,
|
|
1056
|
+
}),
|
|
1057
|
+
variables: { summary: context },
|
|
1058
|
+
});
|
|
1059
|
+
},
|
|
1060
|
+
noResult: async () => {
|
|
1061
|
+
await noResult({ git, logger });
|
|
1062
|
+
process.exit(0);
|
|
1063
|
+
},
|
|
1064
|
+
options: {
|
|
1065
|
+
...options,
|
|
1066
|
+
logger,
|
|
1067
|
+
interactive: INTERACTIVE,
|
|
1068
|
+
},
|
|
1069
|
+
});
|
|
1070
|
+
const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
|
|
1071
|
+
handleResult(commitMsg, {
|
|
1072
|
+
mode: MODE,
|
|
1073
|
+
git,
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Command line options via yargs
|
|
1079
|
+
*/
|
|
1080
|
+
const options = {
|
|
818
1081
|
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
819
1082
|
openAIApiKey: {
|
|
820
1083
|
type: 'string',
|
|
@@ -860,182 +1123,21 @@ const builder = {
|
|
|
860
1123
|
description: 'Ignored extensions',
|
|
861
1124
|
},
|
|
862
1125
|
};
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
const key = getModelAPIKey(options.model, options);
|
|
867
|
-
if (!key) {
|
|
868
|
-
logger.log(`No API Key found. ðïļðŠ`, { color: 'red' });
|
|
869
|
-
process.exit(1);
|
|
870
|
-
}
|
|
871
|
-
const model = getModel(options.model, key, {
|
|
872
|
-
temperature: 0.4,
|
|
873
|
-
maxConcurrency: 10,
|
|
874
|
-
});
|
|
875
|
-
const INTERACTIVE = options?.mode === 'interactive' || options.interactive;
|
|
876
|
-
const { staged: changes } = await getChanges(git);
|
|
877
|
-
let summary = '';
|
|
878
|
-
let commitMsg = '';
|
|
879
|
-
let promptTemplate = options?.prompt || '';
|
|
880
|
-
let modifyPrompt = false;
|
|
881
|
-
while (true) {
|
|
882
|
-
if (changes.length !== 0 && !summary.length) {
|
|
883
|
-
logger.verbose(`\nChanged Files: \n ${changes.map(({ summary }) => summary).join('\n ')}`, {
|
|
884
|
-
color: 'blue',
|
|
885
|
-
});
|
|
886
|
-
summary = await fileChangeParser(changes, { tokenizer, git, model, logger });
|
|
887
|
-
}
|
|
888
|
-
// Handle empty summary
|
|
889
|
-
if (!summary.length) {
|
|
890
|
-
await noResult({ git, logger });
|
|
891
|
-
process.exit(0);
|
|
892
|
-
}
|
|
893
|
-
// Prompt user for commit template prompt, if necessary
|
|
894
|
-
if (modifyPrompt) {
|
|
895
|
-
promptTemplate = await editor({
|
|
896
|
-
message: 'Edit the prompt',
|
|
897
|
-
default: promptTemplate.length ? promptTemplate : COMMIT_PROMPT.template,
|
|
898
|
-
waitForUseInput: false,
|
|
899
|
-
validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
logger.startTimer().startSpinner(`Generating Commit Message\n`, {
|
|
903
|
-
color: 'blue',
|
|
904
|
-
});
|
|
905
|
-
commitMsg = await llm({
|
|
906
|
-
llm: model,
|
|
907
|
-
prompt: getPrompt({
|
|
908
|
-
template: promptTemplate,
|
|
909
|
-
variables: COMMIT_PROMPT.inputVariables,
|
|
910
|
-
fallback: COMMIT_PROMPT,
|
|
911
|
-
}),
|
|
912
|
-
variables: { summary },
|
|
913
|
-
});
|
|
914
|
-
if (!commitMsg) {
|
|
915
|
-
logger.stopSpinner('ð Failed to generate commit message.', {
|
|
916
|
-
mode: 'fail',
|
|
917
|
-
color: 'red',
|
|
918
|
-
});
|
|
919
|
-
process.exit(0);
|
|
920
|
-
}
|
|
921
|
-
logger
|
|
922
|
-
.stopSpinner('Generated Commit Message', {
|
|
923
|
-
color: 'green',
|
|
924
|
-
mode: 'succeed',
|
|
925
|
-
})
|
|
926
|
-
.stopTimer();
|
|
927
|
-
if (INTERACTIVE) {
|
|
928
|
-
logCommit(commitMsg);
|
|
929
|
-
const reviewAnswer = await select({
|
|
930
|
-
message: 'Would you like to make any changes to the commit message?',
|
|
931
|
-
choices: [
|
|
932
|
-
{
|
|
933
|
-
name: 'âĻ Looks good!',
|
|
934
|
-
value: 'approve',
|
|
935
|
-
description: 'Commit staged changes with generated commit message',
|
|
936
|
-
},
|
|
937
|
-
{
|
|
938
|
-
name: 'ð Edit',
|
|
939
|
-
value: 'edit',
|
|
940
|
-
description: 'Edit the commit message before proceeding',
|
|
941
|
-
},
|
|
942
|
-
{
|
|
943
|
-
name: 'ðŠķ Modify Prompt',
|
|
944
|
-
value: 'modifyPrompt',
|
|
945
|
-
description: 'Modify the prompt template and regenerate the commit message',
|
|
946
|
-
},
|
|
947
|
-
{
|
|
948
|
-
name: 'ð Retry - Message Only',
|
|
949
|
-
value: 'retryMessageOnly',
|
|
950
|
-
description: 'Restart the function execution from generating the commit message',
|
|
951
|
-
},
|
|
952
|
-
{
|
|
953
|
-
name: 'ð Retry - Full',
|
|
954
|
-
value: 'retryFull',
|
|
955
|
-
description: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
|
|
956
|
-
},
|
|
957
|
-
{
|
|
958
|
-
name: 'ðĢ Cancel',
|
|
959
|
-
value: 'cancel',
|
|
960
|
-
},
|
|
961
|
-
],
|
|
962
|
-
});
|
|
963
|
-
if (reviewAnswer === 'cancel') {
|
|
964
|
-
process.exit(0);
|
|
965
|
-
}
|
|
966
|
-
if (reviewAnswer === 'edit') {
|
|
967
|
-
options.openInEditor = true;
|
|
968
|
-
}
|
|
969
|
-
if (reviewAnswer === 'retryFull') {
|
|
970
|
-
summary = '';
|
|
971
|
-
commitMsg = '';
|
|
972
|
-
promptTemplate = '';
|
|
973
|
-
continue;
|
|
974
|
-
}
|
|
975
|
-
if (reviewAnswer === 'retryMessageOnly') {
|
|
976
|
-
modifyPrompt = false;
|
|
977
|
-
commitMsg = '';
|
|
978
|
-
continue;
|
|
979
|
-
}
|
|
980
|
-
if (reviewAnswer === 'modifyPrompt') {
|
|
981
|
-
modifyPrompt = true;
|
|
982
|
-
commitMsg = '';
|
|
983
|
-
continue;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
if (options.openInEditor) {
|
|
987
|
-
commitMsg = await editor({
|
|
988
|
-
message: 'Edit the commit message',
|
|
989
|
-
default: commitMsg,
|
|
990
|
-
waitForUseInput: false,
|
|
991
|
-
validate: (text) => {
|
|
992
|
-
if (!text) {
|
|
993
|
-
return 'Commit message cannot be empty';
|
|
994
|
-
}
|
|
995
|
-
return true;
|
|
996
|
-
},
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
const MODE = (options.interactive && 'interactive') ||
|
|
1000
|
-
(options.commit && 'interactive') ||
|
|
1001
|
-
options?.mode ||
|
|
1002
|
-
'stdout';
|
|
1003
|
-
// Handle resulting commit message
|
|
1004
|
-
switch (MODE) {
|
|
1005
|
-
case 'interactive':
|
|
1006
|
-
await createCommit(commitMsg, git);
|
|
1007
|
-
logSuccess();
|
|
1008
|
-
break;
|
|
1009
|
-
case 'stdout':
|
|
1010
|
-
default:
|
|
1011
|
-
process.stdout.write(commitMsg, 'utf8');
|
|
1012
|
-
break;
|
|
1013
|
-
}
|
|
1014
|
-
process.exit(0);
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
var commit = /*#__PURE__*/Object.freeze({
|
|
1019
|
-
__proto__: null,
|
|
1020
|
-
builder: builder,
|
|
1021
|
-
command: command,
|
|
1022
|
-
description: description,
|
|
1023
|
-
handler: handler
|
|
1024
|
-
});
|
|
1126
|
+
const builder = (yargs) => {
|
|
1127
|
+
return yargs.options(options);
|
|
1128
|
+
};
|
|
1025
1129
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
.option('h', { alias: 'help' })
|
|
1034
|
-
.option('v', {
|
|
1035
|
-
alias: 'verbose',
|
|
1036
|
-
type: 'boolean',
|
|
1037
|
-
description: 'Run with verbose logging',
|
|
1038
|
-
}).argv;
|
|
1130
|
+
var commit = {
|
|
1131
|
+
command: 'commit',
|
|
1132
|
+
desc: 'Generate commit message',
|
|
1133
|
+
builder,
|
|
1134
|
+
handler,
|
|
1135
|
+
options,
|
|
1136
|
+
};
|
|
1039
1137
|
|
|
1040
|
-
|
|
1138
|
+
yargs.scriptName('coco').usage('$0 <cmd> [args]').command([commit.command, '$0'], commit.desc,
|
|
1139
|
+
// TODO: fix type on builder
|
|
1140
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1141
|
+
// @ts-ignore
|
|
1142
|
+
commit.builder, commit.handler).argv;
|
|
1041
1143
|
//# sourceMappingURL=index.esm.mjs.map
|