git-coco 0.3.3 → 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 +535 -483
- package/dist/index.esm.mjs.map +1 -1
- package/dist/index.js +534 -483
- package/dist/lib/langchain/executeChain.d.ts +6 -0
- package/dist/lib/langchain/utils.d.ts +2 -2
- 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 +2 -2
- package/dist/lib/simple-git/getChangesByCommit.d.ts +2 -2
- package/dist/lib/simple-git/getDiff.d.ts +1 -1
- package/dist/lib/types.d.ts +19 -8
- 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 +3 -3
- 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 -24
- package/dist/types.d.ts +0 -10
package/dist/index.js
CHANGED
|
@@ -2,26 +2,26 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
var yargs = require('yargs');
|
|
5
|
-
var simpleGit = require('simple-git');
|
|
6
|
-
var fs = require('fs');
|
|
7
|
-
var os = require('os');
|
|
8
|
-
var path = require('path');
|
|
9
|
-
var ini = require('ini');
|
|
10
|
-
var prompts = require('langchain/prompts');
|
|
11
|
-
var chalk = require('chalk');
|
|
12
|
-
var prompts$1 = require('@inquirer/prompts');
|
|
13
5
|
var pQueue = require('p-queue');
|
|
14
6
|
var document = require('langchain/document');
|
|
15
7
|
var hf = require('langchain/llms/hf');
|
|
8
|
+
var prompts = require('langchain/prompts');
|
|
16
9
|
var chains = require('langchain/chains');
|
|
17
10
|
var openai = require('langchain/llms/openai');
|
|
18
11
|
var text_splitter = require('langchain/text_splitter');
|
|
19
12
|
var diff = require('diff');
|
|
20
|
-
var minimatch = require('minimatch');
|
|
21
13
|
var GPT3NodeTokenizer = require('gpt3-tokenizer');
|
|
14
|
+
var chalk = require('chalk');
|
|
22
15
|
var ora = require('ora');
|
|
23
16
|
var now = require('performance-now');
|
|
24
17
|
var prettyMilliseconds = require('pretty-ms');
|
|
18
|
+
var path = require('path');
|
|
19
|
+
var minimatch = require('minimatch');
|
|
20
|
+
var fs = require('fs');
|
|
21
|
+
var os = require('os');
|
|
22
|
+
var ini = require('ini');
|
|
23
|
+
var simpleGit = require('simple-git');
|
|
24
|
+
var prompts$1 = require('@inquirer/prompts');
|
|
25
25
|
|
|
26
26
|
function _interopNamespaceDefault(e) {
|
|
27
27
|
var n = Object.create(null);
|
|
@@ -40,216 +40,11 @@ function _interopNamespaceDefault(e) {
|
|
|
40
40
|
return Object.freeze(n);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
43
44
|
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
|
|
44
45
|
var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
|
|
45
|
-
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
46
46
|
var ini__namespace = /*#__PURE__*/_interopNamespaceDefault(ini);
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
* Returns a new object with all undefined keys removed
|
|
50
|
-
*
|
|
51
|
-
* @param obj Object to remove undefined keys from
|
|
52
|
-
* @returns
|
|
53
|
-
*/
|
|
54
|
-
function removeUndefined(obj) {
|
|
55
|
-
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Load environment variables
|
|
60
|
-
*
|
|
61
|
-
* @param {Config} config
|
|
62
|
-
* @returns {Config} Updated config
|
|
63
|
-
**/
|
|
64
|
-
function loadEnvConfig(config) {
|
|
65
|
-
const envConfig = {
|
|
66
|
-
model: process.env.COCO_MODEL || undefined,
|
|
67
|
-
openAIApiKey: process.env.OPENAI_API_KEY || undefined,
|
|
68
|
-
huggingFaceHubApiKey: process.env.HUGGINGFACE_HUB_API_KEY || undefined,
|
|
69
|
-
tokenLimit: process.env.COCO_TOKEN_LIMIT
|
|
70
|
-
? parseInt(process.env.COCO_TOKEN_LIMIT)
|
|
71
|
-
: undefined,
|
|
72
|
-
prompt: process.env.COCO_PROMPT,
|
|
73
|
-
mode: process.env.COCO_MODE,
|
|
74
|
-
summarizePrompt: process.env.COCO_SUMMARIZE_PROMPT,
|
|
75
|
-
ignoredFiles: process.env.COCO_IGNORED_FILES
|
|
76
|
-
? process.env.COCO_IGNORED_FILES.split(',')
|
|
77
|
-
: undefined,
|
|
78
|
-
ignoredExtensions: process.env.COCO_IGNORED_EXTENSIONS
|
|
79
|
-
? process.env.COCO_IGNORED_EXTENSIONS.split(',')
|
|
80
|
-
: undefined,
|
|
81
|
-
};
|
|
82
|
-
config = { ...config, ...removeUndefined(envConfig) };
|
|
83
|
-
return config;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Load git profile config (from ~/.gitconfig)
|
|
88
|
-
*
|
|
89
|
-
* @param {Config} config
|
|
90
|
-
* @returns {Config} Updated config
|
|
91
|
-
**/
|
|
92
|
-
function loadGitConfig(config) {
|
|
93
|
-
const gitConfigPath = path__namespace.join(os__namespace.homedir(), '.gitconfig');
|
|
94
|
-
if (fs__namespace.existsSync(gitConfigPath)) {
|
|
95
|
-
const gitConfigRaw = fs__namespace.readFileSync(gitConfigPath, 'utf-8');
|
|
96
|
-
const gitConfigParsed = ini__namespace.parse(gitConfigRaw);
|
|
97
|
-
config = {
|
|
98
|
-
...config,
|
|
99
|
-
model: gitConfigParsed.coco?.model || config.model,
|
|
100
|
-
openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
|
|
101
|
-
huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
|
|
102
|
-
tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
|
|
103
|
-
prompt: gitConfigParsed.coco?.prompt || config.prompt,
|
|
104
|
-
mode: gitConfigParsed.coco?.mode || config.mode,
|
|
105
|
-
temperature: gitConfigParsed.coco?.temperature || config.temperature,
|
|
106
|
-
summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
|
|
107
|
-
ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
|
|
108
|
-
ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
return config;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Load .gitignore in project root
|
|
116
|
-
*
|
|
117
|
-
* @param {Config} config
|
|
118
|
-
* @returns
|
|
119
|
-
*/
|
|
120
|
-
function loadGitignore(config) {
|
|
121
|
-
if (fs__namespace.existsSync('.gitignore')) {
|
|
122
|
-
const gitignoreContent = fs__namespace.readFileSync('.gitignore', 'utf-8');
|
|
123
|
-
config.ignoredFiles = [
|
|
124
|
-
...(config?.ignoredFiles || []),
|
|
125
|
-
...gitignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
126
|
-
];
|
|
127
|
-
}
|
|
128
|
-
return config;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Load .ignore in project root
|
|
132
|
-
*
|
|
133
|
-
* @param {Config} config
|
|
134
|
-
* @returns
|
|
135
|
-
*/
|
|
136
|
-
function loadIgnore(config) {
|
|
137
|
-
if (fs__namespace.existsSync('.ignore')) {
|
|
138
|
-
const ignoreContent = fs__namespace.readFileSync('.ignore', 'utf-8');
|
|
139
|
-
config.ignoredFiles = [
|
|
140
|
-
...(config?.ignoredFiles || []),
|
|
141
|
-
...ignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
142
|
-
];
|
|
143
|
-
}
|
|
144
|
-
return config;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Load project config
|
|
149
|
-
*
|
|
150
|
-
* @param {Config} config
|
|
151
|
-
* @returns {Config} Updated config
|
|
152
|
-
**/
|
|
153
|
-
function loadProjectConfig(config) {
|
|
154
|
-
if (fs__namespace.existsSync('.coco.config.json')) {
|
|
155
|
-
const projectConfig = JSON.parse(fs__namespace.readFileSync('.coco.config.json', 'utf-8'));
|
|
156
|
-
config = { ...config, ...projectConfig };
|
|
157
|
-
}
|
|
158
|
-
return config;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Load XDG config
|
|
163
|
-
*
|
|
164
|
-
* @param {Config} config
|
|
165
|
-
* @returns {Config} Updated config
|
|
166
|
-
*/
|
|
167
|
-
function loadXDGConfig(config) {
|
|
168
|
-
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path__namespace.join(os__namespace.homedir(), '.config');
|
|
169
|
-
const xdgConfigPath = path__namespace.join(xdgConfigHome, 'coco', 'config.json');
|
|
170
|
-
if (fs__namespace.existsSync(xdgConfigPath)) {
|
|
171
|
-
const xdgConfig = JSON.parse(fs__namespace.readFileSync(xdgConfigPath, 'utf-8'));
|
|
172
|
-
config = { ...config, ...xdgConfig };
|
|
173
|
-
}
|
|
174
|
-
return config;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const template$1 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
|
|
178
|
-
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.
|
|
179
|
-
|
|
180
|
-
- Typically a hyphen or asterisk is used for the bullet
|
|
181
|
-
- Write concisely using an informal tone
|
|
182
|
-
- DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
|
|
183
|
-
- DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
|
|
184
|
-
- DO NOT use specific names or files from the code
|
|
185
|
-
- Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
|
|
186
|
-
|
|
187
|
-
"""{summary}"""
|
|
188
|
-
|
|
189
|
-
Commit:`;
|
|
190
|
-
const inputVariables$1 = ['summary'];
|
|
191
|
-
const COMMIT_PROMPT = new prompts.PromptTemplate({
|
|
192
|
-
template: template$1,
|
|
193
|
-
inputVariables: inputVariables$1,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
const template = `GOAL: Use functional abstractions to summarize the following text
|
|
197
|
-
|
|
198
|
-
RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
|
|
199
|
-
|
|
200
|
-
TEXT:"""{text}"""
|
|
201
|
-
`;
|
|
202
|
-
const inputVariables = ['text'];
|
|
203
|
-
const SUMMARIZE_PROMPT = new prompts.PromptTemplate({
|
|
204
|
-
template,
|
|
205
|
-
inputVariables,
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Default Config
|
|
210
|
-
*
|
|
211
|
-
* @type {Config}
|
|
212
|
-
*/
|
|
213
|
-
const DEFAULT_CONFIG = {
|
|
214
|
-
model: 'openai/gpt-4',
|
|
215
|
-
verbose: false,
|
|
216
|
-
tokenLimit: 1024,
|
|
217
|
-
prompt: COMMIT_PROMPT.template,
|
|
218
|
-
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
219
|
-
temperature: 0.4,
|
|
220
|
-
mode: 'stdout',
|
|
221
|
-
ignoredFiles: ['package-lock.json'],
|
|
222
|
-
ignoredExtensions: ['.map', '.lock'],
|
|
223
|
-
};
|
|
224
|
-
/**
|
|
225
|
-
* Load application config
|
|
226
|
-
*
|
|
227
|
-
* Merge config from multiple sources.
|
|
228
|
-
*
|
|
229
|
-
* \* Order of precedence:
|
|
230
|
-
* \* 1. Command line flags
|
|
231
|
-
* \* 2. Environment variables
|
|
232
|
-
* \* 3. Project config
|
|
233
|
-
* \* 4. Git config
|
|
234
|
-
* \* 5. XDG config
|
|
235
|
-
* \* 6. .gitignore
|
|
236
|
-
* \* 7. .ignore
|
|
237
|
-
* \* 8. Default config
|
|
238
|
-
*
|
|
239
|
-
* @returns {Config} application config
|
|
240
|
-
**/
|
|
241
|
-
function loadConfig(argv = {}) {
|
|
242
|
-
// Default config
|
|
243
|
-
let config = DEFAULT_CONFIG;
|
|
244
|
-
config = loadGitignore(config);
|
|
245
|
-
config = loadIgnore(config);
|
|
246
|
-
config = loadXDGConfig(config);
|
|
247
|
-
config = loadGitConfig(config);
|
|
248
|
-
config = loadProjectConfig(config);
|
|
249
|
-
config = loadEnvConfig(config);
|
|
250
|
-
return { ...config, ...argv };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
48
|
/**
|
|
254
49
|
* Extract the path from a file path string.
|
|
255
50
|
* @param {string} filePath - The full file path.
|
|
@@ -391,6 +186,23 @@ class DiffTreeNode {
|
|
|
391
186
|
getPath() {
|
|
392
187
|
return this.path.join('/');
|
|
393
188
|
}
|
|
189
|
+
print(indentation = 0) {
|
|
190
|
+
const indent = ' '.repeat(indentation);
|
|
191
|
+
let output = `${indent}- Path: ${this.getPath()}\n`;
|
|
192
|
+
if (this.files.length > 0) {
|
|
193
|
+
output += `${indent} Files:\n`;
|
|
194
|
+
for (const file of this.files) {
|
|
195
|
+
output += `${indent} - ${file.summary}\n`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (this.children.size > 0) {
|
|
199
|
+
output += `${indent} Children:\n`;
|
|
200
|
+
for (const [, child] of this.children) {
|
|
201
|
+
output += child.print(indentation + 4);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return output;
|
|
205
|
+
}
|
|
394
206
|
}
|
|
395
207
|
const createDiffTree = (changes) => {
|
|
396
208
|
const root = new DiffTreeNode();
|
|
@@ -484,7 +296,7 @@ function getModel(name, key, fields) {
|
|
|
484
296
|
* @param options
|
|
485
297
|
* @returns
|
|
486
298
|
*/
|
|
487
|
-
function
|
|
299
|
+
function getApiKeyForModel(name, options) {
|
|
488
300
|
const [llm, model] = name.split(/\/(.*)/s);
|
|
489
301
|
if (!model) {
|
|
490
302
|
throw new Error(`Invalid model: ${name}`);
|
|
@@ -541,19 +353,47 @@ function validatePromptTemplate(text, inputVariables) {
|
|
|
541
353
|
return true;
|
|
542
354
|
}
|
|
543
355
|
|
|
544
|
-
const
|
|
545
|
-
|
|
356
|
+
const template$1 = `GOAL: Use functional abstractions to summarize the following text
|
|
357
|
+
|
|
358
|
+
RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
|
|
359
|
+
|
|
360
|
+
TEXT:"""{text}"""
|
|
361
|
+
`;
|
|
362
|
+
const inputVariables$1 = ['text'];
|
|
363
|
+
const SUMMARIZE_PROMPT = new prompts.PromptTemplate({
|
|
364
|
+
template: template$1,
|
|
365
|
+
inputVariables: inputVariables$1,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const parseDefaultFileDiff = async (nodeFile, commit = '--staged', git) => {
|
|
369
|
+
if (commit !== '--staged') {
|
|
370
|
+
return await git.diff([`${commit}~1..${commit}`, '--', nodeFile.filePath]);
|
|
371
|
+
}
|
|
372
|
+
return await git.diff([commit, nodeFile.filePath]);
|
|
546
373
|
};
|
|
547
|
-
const parseRenamedFileDiff = async (nodeFile, git, logger) => {
|
|
374
|
+
const parseRenamedFileDiff = async (nodeFile, commit, git, logger) => {
|
|
548
375
|
let result = '';
|
|
549
376
|
const oldFilePath = nodeFile?.oldFilePath || nodeFile.filePath;
|
|
377
|
+
let previousCommitHash = 'HEAD';
|
|
378
|
+
let newCommitHash = '';
|
|
379
|
+
if (commit !== '--staged') {
|
|
380
|
+
try {
|
|
381
|
+
previousCommitHash = await git.revparse([`${commit}~1`]);
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
logger.verbose(`Error getting previous commit hash for ${nodeFile.filePath}`, {
|
|
385
|
+
color: 'red',
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
newCommitHash = commit;
|
|
389
|
+
}
|
|
550
390
|
try {
|
|
551
|
-
const [
|
|
552
|
-
git.show([
|
|
553
|
-
git.show([
|
|
391
|
+
const [previousContent, newContent] = await Promise.all([
|
|
392
|
+
git.show([`${previousCommitHash}:${oldFilePath}`]),
|
|
393
|
+
git.show([`${newCommitHash}:${nodeFile.filePath}`]),
|
|
554
394
|
]);
|
|
555
|
-
if (
|
|
556
|
-
result = diff.createTwoFilesPatch(oldFilePath, nodeFile.filePath,
|
|
395
|
+
if (previousContent !== newContent) {
|
|
396
|
+
result = diff.createTwoFilesPatch(oldFilePath, nodeFile.filePath, previousContent, newContent, '', '', {
|
|
557
397
|
context: 3,
|
|
558
398
|
});
|
|
559
399
|
// remove the first 4 lines of the patch (they contain the old and new file names)
|
|
@@ -569,22 +409,22 @@ const parseRenamedFileDiff = async (nodeFile, git, logger) => {
|
|
|
569
409
|
}
|
|
570
410
|
return result;
|
|
571
411
|
};
|
|
572
|
-
const getDiff = async (nodeFile, { git, logger, }) => {
|
|
412
|
+
const getDiff = async (nodeFile, commit, { git, logger, }) => {
|
|
573
413
|
if (nodeFile.status === 'deleted') {
|
|
574
414
|
return 'This file has been deleted.';
|
|
575
415
|
}
|
|
576
416
|
if (nodeFile.status === 'renamed' && nodeFile.oldFilePath) {
|
|
577
|
-
const renamedDiff = await parseRenamedFileDiff(nodeFile, git, logger);
|
|
417
|
+
const renamedDiff = await parseRenamedFileDiff(nodeFile, commit, git, logger);
|
|
578
418
|
return renamedDiff;
|
|
579
419
|
}
|
|
580
420
|
// If not deleted or renamed, get the diff from the index
|
|
581
|
-
const defaultDiff = await parseDefaultFileDiff(nodeFile, git);
|
|
421
|
+
const defaultDiff = await parseDefaultFileDiff(nodeFile, commit, git);
|
|
582
422
|
return defaultDiff;
|
|
583
423
|
};
|
|
584
424
|
|
|
585
425
|
const MAX_TOKENS_PER_SUMMARY = 2048;
|
|
586
|
-
|
|
587
|
-
const textSplitter = getTextSplitter({ chunkSize: 2000, chunkOverlap: 125
|
|
426
|
+
async function fileChangeParser({ changes, commit, options: { tokenizer, git, model, logger }, }) {
|
|
427
|
+
const textSplitter = getTextSplitter({ chunkSize: 2000, chunkOverlap: 125 });
|
|
588
428
|
const summarizationChain = getChain(model, {
|
|
589
429
|
type: 'map_reduce',
|
|
590
430
|
combineMapPrompt: SUMMARIZE_PROMPT,
|
|
@@ -595,7 +435,7 @@ const fileChangeParser = async (changes, { tokenizer, git, model, logger }) => {
|
|
|
595
435
|
logger.stopTimer('Created file hierarchy');
|
|
596
436
|
// Collect diffs
|
|
597
437
|
logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
|
|
598
|
-
const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, { git, logger }), tokenizer, logger);
|
|
438
|
+
const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, commit, { git, logger }), tokenizer, logger);
|
|
599
439
|
logger.stopSpinner('Diffs Collected').stopTimer();
|
|
600
440
|
// Summarize diffs
|
|
601
441
|
logger.startTimer();
|
|
@@ -604,34 +444,103 @@ const fileChangeParser = async (changes, { tokenizer, git, model, logger }) => {
|
|
|
604
444
|
maxTokens: MAX_TOKENS_PER_SUMMARY,
|
|
605
445
|
textSplitter,
|
|
606
446
|
chain: summarizationChain,
|
|
607
|
-
logger
|
|
447
|
+
logger,
|
|
608
448
|
});
|
|
609
449
|
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
610
450
|
return summary;
|
|
611
|
-
}
|
|
451
|
+
}
|
|
612
452
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Wrapper around GPT3NodeTokenizer to handle default export.
|
|
455
|
+
*
|
|
456
|
+
* @see https://github.com/botisan-ai/gpt3-tokenizer/issues/18
|
|
457
|
+
*
|
|
458
|
+
* @returns {GPT3NodeTokenizer} The GPT3NodeTokenizer instance.
|
|
459
|
+
*/
|
|
460
|
+
const getTokenizer = () => {
|
|
461
|
+
let tokenizer;
|
|
462
|
+
// eslint-disable-next-line
|
|
463
|
+
// @ts-ignore
|
|
464
|
+
if (GPT3NodeTokenizer.default) {
|
|
465
|
+
// eslint-disable-next-line
|
|
466
|
+
// @ts-ignore
|
|
467
|
+
tokenizer = new GPT3NodeTokenizer.default({ type: 'gpt3' });
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
tokenizer = new GPT3NodeTokenizer({ type: 'gpt3' });
|
|
471
|
+
}
|
|
472
|
+
return tokenizer;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
class Logger {
|
|
476
|
+
constructor(config) {
|
|
477
|
+
this.config = config;
|
|
478
|
+
this.spinner = null;
|
|
479
|
+
}
|
|
480
|
+
log(message, options = { color: 'blue' }) {
|
|
481
|
+
let outputMessage = message;
|
|
482
|
+
if (options.color) {
|
|
483
|
+
outputMessage = chalk[options.color](outputMessage);
|
|
625
484
|
}
|
|
485
|
+
console.log(outputMessage);
|
|
486
|
+
return this;
|
|
626
487
|
}
|
|
627
|
-
|
|
628
|
-
|
|
488
|
+
verbose(message, options = {}) {
|
|
489
|
+
if (!this.config?.verbose) {
|
|
490
|
+
return this;
|
|
491
|
+
}
|
|
492
|
+
this.log(message, options);
|
|
493
|
+
return this;
|
|
629
494
|
}
|
|
630
|
-
|
|
631
|
-
|
|
495
|
+
startTimer() {
|
|
496
|
+
this.timerStart = now();
|
|
497
|
+
return this;
|
|
632
498
|
}
|
|
633
|
-
|
|
634
|
-
|
|
499
|
+
stopTimer(message, options = { color: 'yellow' }) {
|
|
500
|
+
if (!this.config?.verbose || !this.timerStart) {
|
|
501
|
+
return this;
|
|
502
|
+
}
|
|
503
|
+
const elapsedTime = prettyMilliseconds(now() - this.timerStart);
|
|
504
|
+
let outputMessage = message
|
|
505
|
+
? `${message} (⏲ ${elapsedTime})`
|
|
506
|
+
: `⏲ ${elapsedTime}`;
|
|
507
|
+
if (options.color) {
|
|
508
|
+
outputMessage = chalk[options.color](outputMessage);
|
|
509
|
+
}
|
|
510
|
+
console.log(outputMessage);
|
|
511
|
+
return this;
|
|
512
|
+
}
|
|
513
|
+
startSpinner(message, options = { color: 'green' }) {
|
|
514
|
+
const spinnerMessage = options.color ? chalk[options.color](message) : message;
|
|
515
|
+
this.spinner = ora(spinnerMessage).start();
|
|
516
|
+
return this;
|
|
517
|
+
}
|
|
518
|
+
stopSpinner(message = '', options = { mode: 'succeed', color: 'green' }) {
|
|
519
|
+
const spinnerMessage = options?.color ? chalk[options.color](message) : message;
|
|
520
|
+
this.spinner?.[options.mode || 'succeed'](spinnerMessage);
|
|
521
|
+
this.spinner = null;
|
|
522
|
+
return this;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const template = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
|
|
527
|
+
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.
|
|
528
|
+
|
|
529
|
+
- Typically a hyphen or asterisk is used for the bullet
|
|
530
|
+
- Write concisely using an informal tone
|
|
531
|
+
- DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
|
|
532
|
+
- DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
|
|
533
|
+
- DO NOT use specific names or files from the code
|
|
534
|
+
- Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
|
|
535
|
+
|
|
536
|
+
"""{summary}"""
|
|
537
|
+
|
|
538
|
+
Commit:`;
|
|
539
|
+
const inputVariables = ['summary'];
|
|
540
|
+
const COMMIT_PROMPT = new prompts.PromptTemplate({
|
|
541
|
+
template,
|
|
542
|
+
inputVariables,
|
|
543
|
+
});
|
|
635
544
|
|
|
636
545
|
const getStatus = (file, location = 'index') => {
|
|
637
546
|
if ('index' in file && 'working_dir' in file) {
|
|
@@ -687,6 +596,180 @@ const getSummaryText = (file, change) => {
|
|
|
687
596
|
return `${status}: ${filePath}`;
|
|
688
597
|
};
|
|
689
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Returns a new object with all undefined keys removed
|
|
601
|
+
*
|
|
602
|
+
* @param obj Object to remove undefined keys from
|
|
603
|
+
* @returns
|
|
604
|
+
*/
|
|
605
|
+
function removeUndefined(obj) {
|
|
606
|
+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Load environment variables
|
|
611
|
+
*
|
|
612
|
+
* @param {Config} config
|
|
613
|
+
* @returns {Config} Updated config
|
|
614
|
+
**/
|
|
615
|
+
function loadEnvConfig(config) {
|
|
616
|
+
const envConfig = {
|
|
617
|
+
model: process.env.COCO_MODEL || undefined,
|
|
618
|
+
openAIApiKey: process.env.OPENAI_API_KEY || undefined,
|
|
619
|
+
huggingFaceHubApiKey: process.env.HUGGINGFACE_HUB_API_KEY || undefined,
|
|
620
|
+
tokenLimit: process.env.COCO_TOKEN_LIMIT
|
|
621
|
+
? parseInt(process.env.COCO_TOKEN_LIMIT)
|
|
622
|
+
: undefined,
|
|
623
|
+
prompt: process.env.COCO_PROMPT,
|
|
624
|
+
mode: process.env.COCO_MODE,
|
|
625
|
+
summarizePrompt: process.env.COCO_SUMMARIZE_PROMPT,
|
|
626
|
+
ignoredFiles: process.env.COCO_IGNORED_FILES
|
|
627
|
+
? process.env.COCO_IGNORED_FILES.split(',')
|
|
628
|
+
: undefined,
|
|
629
|
+
ignoredExtensions: process.env.COCO_IGNORED_EXTENSIONS
|
|
630
|
+
? process.env.COCO_IGNORED_EXTENSIONS.split(',')
|
|
631
|
+
: undefined,
|
|
632
|
+
};
|
|
633
|
+
config = { ...config, ...removeUndefined(envConfig) };
|
|
634
|
+
return config;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Load git profile config (from ~/.gitconfig)
|
|
639
|
+
*
|
|
640
|
+
* @param {Config} config
|
|
641
|
+
* @returns {Config} Updated config
|
|
642
|
+
**/
|
|
643
|
+
function loadGitConfig(config) {
|
|
644
|
+
const gitConfigPath = path__namespace.join(os__namespace.homedir(), '.gitconfig');
|
|
645
|
+
if (fs__namespace.existsSync(gitConfigPath)) {
|
|
646
|
+
const gitConfigRaw = fs__namespace.readFileSync(gitConfigPath, 'utf-8');
|
|
647
|
+
const gitConfigParsed = ini__namespace.parse(gitConfigRaw);
|
|
648
|
+
config = {
|
|
649
|
+
...config,
|
|
650
|
+
model: gitConfigParsed.coco?.model || config.model,
|
|
651
|
+
openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
|
|
652
|
+
huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
|
|
653
|
+
tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
|
|
654
|
+
prompt: gitConfigParsed.coco?.prompt || config.prompt,
|
|
655
|
+
mode: gitConfigParsed.coco?.mode || config.mode,
|
|
656
|
+
temperature: gitConfigParsed.coco?.temperature || config.temperature,
|
|
657
|
+
summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
|
|
658
|
+
ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
|
|
659
|
+
ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
return config;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Load .gitignore in project root
|
|
667
|
+
*
|
|
668
|
+
* @param {Config} config
|
|
669
|
+
* @returns
|
|
670
|
+
*/
|
|
671
|
+
function loadGitignore(config) {
|
|
672
|
+
if (fs__namespace.existsSync('.gitignore')) {
|
|
673
|
+
const gitignoreContent = fs__namespace.readFileSync('.gitignore', 'utf-8');
|
|
674
|
+
config.ignoredFiles = [
|
|
675
|
+
...(config?.ignoredFiles || []),
|
|
676
|
+
...gitignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
677
|
+
];
|
|
678
|
+
}
|
|
679
|
+
return config;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Load .ignore in project root
|
|
683
|
+
*
|
|
684
|
+
* @param {Config} config
|
|
685
|
+
* @returns
|
|
686
|
+
*/
|
|
687
|
+
function loadIgnore(config) {
|
|
688
|
+
if (fs__namespace.existsSync('.ignore')) {
|
|
689
|
+
const ignoreContent = fs__namespace.readFileSync('.ignore', 'utf-8');
|
|
690
|
+
config.ignoredFiles = [
|
|
691
|
+
...(config?.ignoredFiles || []),
|
|
692
|
+
...ignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
693
|
+
];
|
|
694
|
+
}
|
|
695
|
+
return config;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Load project config
|
|
700
|
+
*
|
|
701
|
+
* @param {Config} config
|
|
702
|
+
* @returns {Config} Updated config
|
|
703
|
+
**/
|
|
704
|
+
function loadProjectConfig(config) {
|
|
705
|
+
if (fs__namespace.existsSync('.coco.config.json')) {
|
|
706
|
+
const projectConfig = JSON.parse(fs__namespace.readFileSync('.coco.config.json', 'utf-8'));
|
|
707
|
+
config = { ...config, ...projectConfig };
|
|
708
|
+
}
|
|
709
|
+
return config;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Load XDG config
|
|
714
|
+
*
|
|
715
|
+
* @param {Config} config
|
|
716
|
+
* @returns {Config} Updated config
|
|
717
|
+
*/
|
|
718
|
+
function loadXDGConfig(config) {
|
|
719
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path__namespace.join(os__namespace.homedir(), '.config');
|
|
720
|
+
const xdgConfigPath = path__namespace.join(xdgConfigHome, 'coco', 'config.json');
|
|
721
|
+
if (fs__namespace.existsSync(xdgConfigPath)) {
|
|
722
|
+
const xdgConfig = JSON.parse(fs__namespace.readFileSync(xdgConfigPath, 'utf-8'));
|
|
723
|
+
config = { ...config, ...xdgConfig };
|
|
724
|
+
}
|
|
725
|
+
return config;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Default Config
|
|
730
|
+
*
|
|
731
|
+
* @type {Config}
|
|
732
|
+
*/
|
|
733
|
+
const DEFAULT_CONFIG = {
|
|
734
|
+
model: 'openai/gpt-4',
|
|
735
|
+
verbose: false,
|
|
736
|
+
tokenLimit: 1024,
|
|
737
|
+
prompt: COMMIT_PROMPT.template,
|
|
738
|
+
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
739
|
+
temperature: 0.4,
|
|
740
|
+
mode: 'stdout',
|
|
741
|
+
ignoredFiles: ['package-lock.json'],
|
|
742
|
+
ignoredExtensions: ['.map', '.lock'],
|
|
743
|
+
};
|
|
744
|
+
/**
|
|
745
|
+
* Load application config
|
|
746
|
+
*
|
|
747
|
+
* Merge config from multiple sources.
|
|
748
|
+
*
|
|
749
|
+
* \* Order of precedence:
|
|
750
|
+
* \* 1. Command line flags
|
|
751
|
+
* \* 2. Environment variables
|
|
752
|
+
* \* 3. Project config
|
|
753
|
+
* \* 4. Git config
|
|
754
|
+
* \* 5. XDG config
|
|
755
|
+
* \* 6. .gitignore
|
|
756
|
+
* \* 7. .ignore
|
|
757
|
+
* \* 8. Default config
|
|
758
|
+
*
|
|
759
|
+
* @returns {Config} application config
|
|
760
|
+
**/
|
|
761
|
+
function loadConfig(argv = {}) {
|
|
762
|
+
// Default config
|
|
763
|
+
let config = DEFAULT_CONFIG;
|
|
764
|
+
config = loadGitignore(config);
|
|
765
|
+
config = loadIgnore(config);
|
|
766
|
+
config = loadXDGConfig(config);
|
|
767
|
+
config = loadGitConfig(config);
|
|
768
|
+
config = loadProjectConfig(config);
|
|
769
|
+
config = loadEnvConfig(config);
|
|
770
|
+
return { ...config, ...argv };
|
|
771
|
+
}
|
|
772
|
+
|
|
690
773
|
const config = loadConfig();
|
|
691
774
|
const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
|
|
692
775
|
const DEFAULT_IGNORED_EXTENSIONS = config?.ignoredExtensions?.length ? config.ignoredExtensions : [];
|
|
@@ -743,7 +826,7 @@ async function getChanges({ git, options }) {
|
|
|
743
826
|
};
|
|
744
827
|
}
|
|
745
828
|
|
|
746
|
-
|
|
829
|
+
async function noResult({ git, logger }) {
|
|
747
830
|
const { staged, unstaged, untracked } = await getChanges({ git });
|
|
748
831
|
const hasStaged = staged && staged.length > 0;
|
|
749
832
|
const hasUnstaged = unstaged && unstaged.length > 0;
|
|
@@ -770,65 +853,103 @@ const noResult = async ({ git, logger }) => {
|
|
|
770
853
|
else {
|
|
771
854
|
logger.log('No repo changes detected. 👀', { color: 'blue' });
|
|
772
855
|
}
|
|
773
|
-
};
|
|
774
|
-
|
|
775
|
-
async function createCommit(commitMsg, git) {
|
|
776
|
-
return await git.commit(commitMsg);
|
|
777
856
|
}
|
|
778
857
|
|
|
779
|
-
const SEPERATOR = chalk.blue('----------------');
|
|
780
858
|
const isInteractive = (argv) => {
|
|
781
859
|
return argv?.mode === 'interactive' || argv.interactive;
|
|
782
860
|
};
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
861
|
+
const SEPERATOR = chalk.blue('----------------');
|
|
862
|
+
|
|
863
|
+
function logResult(result) {
|
|
864
|
+
console.log(`\n${chalk.bgBlue(chalk.bold('Proposed Commit:'))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async function editResult(result, options) {
|
|
868
|
+
if (options.openInEditor) {
|
|
869
|
+
return await prompts$1.editor({
|
|
870
|
+
message: 'Edit the commit message',
|
|
871
|
+
default: result,
|
|
872
|
+
waitForUseInput: false,
|
|
873
|
+
validate: (text) => (text ? true : 'Commit message cannot be empty'),
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
return result;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async function getUserReviewDecision() {
|
|
880
|
+
return await prompts$1.select({
|
|
881
|
+
message: 'Would you like to make any changes to the commit message?',
|
|
882
|
+
choices: [
|
|
883
|
+
{
|
|
884
|
+
name: '✨ Looks good!',
|
|
885
|
+
value: 'approve',
|
|
886
|
+
description: 'Commit staged changes with generated commit message',
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
name: '📝 Edit',
|
|
890
|
+
value: 'edit',
|
|
891
|
+
description: 'Edit the commit message before proceeding',
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
name: '🪶 Modify Prompt',
|
|
895
|
+
value: 'modifyPrompt',
|
|
896
|
+
description: 'Modify the prompt template and regenerate the commit message',
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
name: '🔄 Retry - Message Only',
|
|
900
|
+
value: 'retryMessageOnly',
|
|
901
|
+
description: 'Restart the function execution from generating the commit message',
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
name: '🔄 Retry - Full',
|
|
905
|
+
value: 'retryFull',
|
|
906
|
+
description: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
name: '💣 Cancel',
|
|
910
|
+
value: 'cancel',
|
|
911
|
+
},
|
|
912
|
+
],
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
async function editPrompt(options) {
|
|
917
|
+
return await prompts$1.editor({
|
|
918
|
+
message: 'Edit the prompt',
|
|
919
|
+
default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
|
|
920
|
+
waitForUseInput: false,
|
|
921
|
+
validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async function generateAndReviewLoop({ factory, parser, noResult, agent, options, }) {
|
|
926
|
+
const { logger } = options;
|
|
796
927
|
let continueLoop = true;
|
|
928
|
+
let modifyPrompt = false;
|
|
929
|
+
let context = '';
|
|
930
|
+
let result = '';
|
|
931
|
+
const changes = await factory();
|
|
932
|
+
// if we don't have any changes, bail.
|
|
933
|
+
if (!changes || !changes.length) {
|
|
934
|
+
await noResult(options);
|
|
935
|
+
}
|
|
797
936
|
while (continueLoop) {
|
|
798
|
-
if (
|
|
799
|
-
|
|
800
|
-
color: 'blue',
|
|
801
|
-
});
|
|
802
|
-
summary = await fileChangeParser(changes, { tokenizer, git, model, logger });
|
|
937
|
+
if (!context.length) {
|
|
938
|
+
context = await parser(changes, result, options);
|
|
803
939
|
}
|
|
804
|
-
//
|
|
805
|
-
if (!
|
|
806
|
-
await noResult(
|
|
807
|
-
process.exit(0);
|
|
940
|
+
// if we still don't have a context, bail.
|
|
941
|
+
if (!context.length) {
|
|
942
|
+
await noResult(options);
|
|
808
943
|
}
|
|
809
|
-
// Prompt user for commit template prompt, if necessary
|
|
810
944
|
if (modifyPrompt) {
|
|
811
|
-
|
|
812
|
-
message: 'Edit the prompt',
|
|
813
|
-
default: promptTemplate.length ? promptTemplate : COMMIT_PROMPT.template,
|
|
814
|
-
waitForUseInput: false,
|
|
815
|
-
validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
|
|
816
|
-
});
|
|
945
|
+
options.prompt = await editPrompt(options);
|
|
817
946
|
}
|
|
818
|
-
logger.startTimer().startSpinner(`Generating
|
|
947
|
+
logger.startTimer().startSpinner(`Generating Message\n`, {
|
|
819
948
|
color: 'blue',
|
|
820
949
|
});
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
template: promptTemplate,
|
|
825
|
-
variables: COMMIT_PROMPT.inputVariables,
|
|
826
|
-
fallback: COMMIT_PROMPT,
|
|
827
|
-
}),
|
|
828
|
-
variables: { summary },
|
|
829
|
-
});
|
|
830
|
-
if (!commitMsg) {
|
|
831
|
-
logger.stopSpinner('💀 Failed to generate commit message.', {
|
|
950
|
+
result = await agent(context, options);
|
|
951
|
+
if (!result) {
|
|
952
|
+
logger.stopSpinner('💀 Agent failed to generate message.', {
|
|
832
953
|
mode: 'fail',
|
|
833
954
|
color: 'red',
|
|
834
955
|
});
|
|
@@ -841,41 +962,8 @@ const generateCommitMessageAndReviewLoop = async (changes, options) => {
|
|
|
841
962
|
})
|
|
842
963
|
.stopTimer();
|
|
843
964
|
if (options?.interactive) {
|
|
844
|
-
|
|
845
|
-
const reviewAnswer = await
|
|
846
|
-
message: 'Would you like to make any changes to the commit message?',
|
|
847
|
-
choices: [
|
|
848
|
-
{
|
|
849
|
-
name: '✨ Looks good!',
|
|
850
|
-
value: 'approve',
|
|
851
|
-
description: 'Commit staged changes with generated commit message',
|
|
852
|
-
},
|
|
853
|
-
{
|
|
854
|
-
name: '📝 Edit',
|
|
855
|
-
value: 'edit',
|
|
856
|
-
description: 'Edit the commit message before proceeding',
|
|
857
|
-
},
|
|
858
|
-
{
|
|
859
|
-
name: '🪶 Modify Prompt',
|
|
860
|
-
value: 'modifyPrompt',
|
|
861
|
-
description: 'Modify the prompt template and regenerate the commit message',
|
|
862
|
-
},
|
|
863
|
-
{
|
|
864
|
-
name: '🔄 Retry - Message Only',
|
|
865
|
-
value: 'retryMessageOnly',
|
|
866
|
-
description: 'Restart the function execution from generating the commit message',
|
|
867
|
-
},
|
|
868
|
-
{
|
|
869
|
-
name: '🔄 Retry - Full',
|
|
870
|
-
value: 'retryFull',
|
|
871
|
-
description: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
|
|
872
|
-
},
|
|
873
|
-
{
|
|
874
|
-
name: '💣 Cancel',
|
|
875
|
-
value: 'cancel',
|
|
876
|
-
},
|
|
877
|
-
],
|
|
878
|
-
});
|
|
965
|
+
logResult(result);
|
|
966
|
+
const reviewAnswer = await getUserReviewDecision();
|
|
879
967
|
if (reviewAnswer === 'cancel') {
|
|
880
968
|
process.exit(0);
|
|
881
969
|
}
|
|
@@ -883,133 +971,136 @@ const generateCommitMessageAndReviewLoop = async (changes, options) => {
|
|
|
883
971
|
options.openInEditor = true;
|
|
884
972
|
}
|
|
885
973
|
if (reviewAnswer === 'retryFull') {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
974
|
+
context = '';
|
|
975
|
+
result = '';
|
|
976
|
+
options.prompt = '';
|
|
889
977
|
continue;
|
|
890
978
|
}
|
|
891
979
|
if (reviewAnswer === 'retryMessageOnly') {
|
|
892
980
|
modifyPrompt = false;
|
|
893
|
-
|
|
981
|
+
result = '';
|
|
894
982
|
continue;
|
|
895
983
|
}
|
|
896
984
|
if (reviewAnswer === 'modifyPrompt') {
|
|
897
985
|
modifyPrompt = true;
|
|
898
|
-
|
|
986
|
+
result = '';
|
|
899
987
|
continue;
|
|
900
988
|
}
|
|
901
989
|
}
|
|
902
|
-
if
|
|
903
|
-
|
|
904
|
-
message: 'Edit the commit message',
|
|
905
|
-
default: commitMsg,
|
|
906
|
-
waitForUseInput: false,
|
|
907
|
-
validate: (text) => {
|
|
908
|
-
if (!text) {
|
|
909
|
-
return 'Commit message cannot be empty';
|
|
910
|
-
}
|
|
911
|
-
return true;
|
|
912
|
-
},
|
|
913
|
-
});
|
|
914
|
-
}
|
|
990
|
+
// if we're here, we're done.
|
|
991
|
+
result = await editResult(result, options);
|
|
915
992
|
continueLoop = false;
|
|
916
993
|
}
|
|
917
|
-
return
|
|
994
|
+
return result;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const executeChain = async ({ llm, prompt, variables }) => {
|
|
998
|
+
if (!llm || !prompt || !variables) {
|
|
999
|
+
throw new Error('The input parameters "llm", "prompt", and "variables" are all required.');
|
|
1000
|
+
}
|
|
1001
|
+
const chain = new chains.LLMChain({ llm, prompt });
|
|
1002
|
+
let res;
|
|
1003
|
+
try {
|
|
1004
|
+
res = await chain.call(variables);
|
|
1005
|
+
}
|
|
1006
|
+
catch (error) {
|
|
1007
|
+
if (error instanceof Error) {
|
|
1008
|
+
throw new Error(`LLMChain call error: ${error.message}`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
if (!res) {
|
|
1012
|
+
throw new Error('Empty response from LLMChain call');
|
|
1013
|
+
}
|
|
1014
|
+
if (res.error) {
|
|
1015
|
+
throw new Error(`LLMChain response error: ${res.error}`);
|
|
1016
|
+
}
|
|
1017
|
+
return res.text.trim();
|
|
918
1018
|
};
|
|
919
|
-
|
|
1019
|
+
|
|
1020
|
+
async function createCommit(commitMsg, git) {
|
|
1021
|
+
return await git.commit(commitMsg);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const logSuccess = () => {
|
|
1025
|
+
console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
const handleResult = async (result, { mode, git }) => {
|
|
920
1029
|
// Handle resulting commit message
|
|
921
1030
|
switch (mode) {
|
|
922
1031
|
case 'interactive':
|
|
923
|
-
await createCommit(
|
|
1032
|
+
await createCommit(result, git);
|
|
924
1033
|
logSuccess();
|
|
925
1034
|
break;
|
|
926
1035
|
case 'stdout':
|
|
927
1036
|
default:
|
|
928
|
-
process.stdout.write(
|
|
1037
|
+
process.stdout.write(result, 'utf8');
|
|
929
1038
|
break;
|
|
930
1039
|
}
|
|
931
1040
|
process.exit(0);
|
|
932
1041
|
};
|
|
933
1042
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
// eslint-disable-next-line
|
|
944
|
-
// @ts-ignore
|
|
945
|
-
if (GPT3NodeTokenizer.default) {
|
|
946
|
-
// eslint-disable-next-line
|
|
947
|
-
// @ts-ignore
|
|
948
|
-
tokenizer = new GPT3NodeTokenizer.default({ type: 'gpt3' });
|
|
949
|
-
}
|
|
950
|
-
else {
|
|
951
|
-
tokenizer = new GPT3NodeTokenizer({ type: 'gpt3' });
|
|
952
|
-
}
|
|
953
|
-
return tokenizer;
|
|
954
|
-
};
|
|
955
|
-
|
|
956
|
-
class Logger {
|
|
957
|
-
constructor(config) {
|
|
958
|
-
this.config = config;
|
|
959
|
-
this.spinner = null;
|
|
960
|
-
}
|
|
961
|
-
log(message, options = { color: 'blue' }) {
|
|
962
|
-
let outputMessage = message;
|
|
963
|
-
if (options.color) {
|
|
964
|
-
outputMessage = chalk[options.color](outputMessage);
|
|
965
|
-
}
|
|
966
|
-
console.log(outputMessage);
|
|
967
|
-
return this;
|
|
968
|
-
}
|
|
969
|
-
verbose(message, options = {}) {
|
|
970
|
-
if (!this.config?.verbose) {
|
|
971
|
-
return this;
|
|
972
|
-
}
|
|
973
|
-
this.log(message, options);
|
|
974
|
-
return this;
|
|
975
|
-
}
|
|
976
|
-
startTimer() {
|
|
977
|
-
this.timerStart = now();
|
|
978
|
-
return this;
|
|
979
|
-
}
|
|
980
|
-
stopTimer(message, options = { color: 'yellow' }) {
|
|
981
|
-
if (!this.config?.verbose || !this.timerStart) {
|
|
982
|
-
return this;
|
|
983
|
-
}
|
|
984
|
-
const elapsedTime = prettyMilliseconds(now() - this.timerStart);
|
|
985
|
-
let outputMessage = message
|
|
986
|
-
? `${message} (⏲ ${elapsedTime})`
|
|
987
|
-
: `⏲ ${elapsedTime}`;
|
|
988
|
-
if (options.color) {
|
|
989
|
-
outputMessage = chalk[options.color](outputMessage);
|
|
990
|
-
}
|
|
991
|
-
console.log(outputMessage);
|
|
992
|
-
return this;
|
|
1043
|
+
const tokenizer = getTokenizer();
|
|
1044
|
+
const git = simpleGit.simpleGit();
|
|
1045
|
+
async function handler(argv) {
|
|
1046
|
+
const options = loadConfig(argv);
|
|
1047
|
+
const logger = new Logger(options);
|
|
1048
|
+
const key = getApiKeyForModel(options.model, options);
|
|
1049
|
+
if (!key) {
|
|
1050
|
+
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
1051
|
+
process.exit(1);
|
|
993
1052
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1053
|
+
const model = getModel(options.model, key, {
|
|
1054
|
+
temperature: 0.4,
|
|
1055
|
+
maxConcurrency: 10,
|
|
1056
|
+
});
|
|
1057
|
+
const INTERACTIVE = isInteractive(options);
|
|
1058
|
+
async function factory() {
|
|
1059
|
+
const changes = await getChanges({ git });
|
|
1060
|
+
return changes.staged;
|
|
998
1061
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1062
|
+
async function parser(changes) {
|
|
1063
|
+
return await fileChangeParser({
|
|
1064
|
+
changes,
|
|
1065
|
+
commit: '--staged',
|
|
1066
|
+
options: { tokenizer, git, model, logger },
|
|
1067
|
+
});
|
|
1004
1068
|
}
|
|
1069
|
+
const commitMsg = await generateAndReviewLoop({
|
|
1070
|
+
factory,
|
|
1071
|
+
parser,
|
|
1072
|
+
agent: async (context, options) => {
|
|
1073
|
+
return await executeChain({
|
|
1074
|
+
llm: model,
|
|
1075
|
+
prompt: getPrompt({
|
|
1076
|
+
template: options.prompt,
|
|
1077
|
+
variables: COMMIT_PROMPT.inputVariables,
|
|
1078
|
+
fallback: COMMIT_PROMPT,
|
|
1079
|
+
}),
|
|
1080
|
+
variables: { summary: context },
|
|
1081
|
+
});
|
|
1082
|
+
},
|
|
1083
|
+
noResult: async () => {
|
|
1084
|
+
await noResult({ git, logger });
|
|
1085
|
+
process.exit(0);
|
|
1086
|
+
},
|
|
1087
|
+
options: {
|
|
1088
|
+
...options,
|
|
1089
|
+
logger,
|
|
1090
|
+
interactive: INTERACTIVE,
|
|
1091
|
+
},
|
|
1092
|
+
});
|
|
1093
|
+
const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
|
|
1094
|
+
handleResult(commitMsg, {
|
|
1095
|
+
mode: MODE,
|
|
1096
|
+
git,
|
|
1097
|
+
});
|
|
1005
1098
|
}
|
|
1006
1099
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
const
|
|
1011
|
-
const description = 'Generate a commit message based on the diff summary';
|
|
1012
|
-
const builder = {
|
|
1100
|
+
/**
|
|
1101
|
+
* Command line options via yargs
|
|
1102
|
+
*/
|
|
1103
|
+
const options = {
|
|
1013
1104
|
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
1014
1105
|
openAIApiKey: {
|
|
1015
1106
|
type: 'string',
|
|
@@ -1055,60 +1146,20 @@ const builder = {
|
|
|
1055
1146
|
description: 'Ignored extensions',
|
|
1056
1147
|
},
|
|
1057
1148
|
};
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
const key = getModelAPIKey(options.model, options);
|
|
1062
|
-
if (!key) {
|
|
1063
|
-
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
1064
|
-
process.exit(1);
|
|
1065
|
-
}
|
|
1066
|
-
const model = getModel(options.model, key, {
|
|
1067
|
-
temperature: 0.4,
|
|
1068
|
-
maxConcurrency: 10,
|
|
1069
|
-
});
|
|
1070
|
-
const INTERACTIVE = isInteractive(options);
|
|
1071
|
-
const { staged: changes } = await getChanges({ git });
|
|
1072
|
-
const commitMsg = await generateCommitMessageAndReviewLoop(changes, {
|
|
1073
|
-
logger,
|
|
1074
|
-
model,
|
|
1075
|
-
git,
|
|
1076
|
-
tokenizer,
|
|
1077
|
-
prompt: options.prompt,
|
|
1078
|
-
interactive: INTERACTIVE,
|
|
1079
|
-
openInEditor: options.openInEditor,
|
|
1080
|
-
});
|
|
1081
|
-
const MODE = (options.interactive && 'interactive') ||
|
|
1082
|
-
(options.commit && 'interactive') ||
|
|
1083
|
-
options?.mode ||
|
|
1084
|
-
'stdout';
|
|
1085
|
-
handleResult(commitMsg, {
|
|
1086
|
-
mode: MODE,
|
|
1087
|
-
git,
|
|
1088
|
-
});
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
var commit = /*#__PURE__*/Object.freeze({
|
|
1092
|
-
__proto__: null,
|
|
1093
|
-
builder: builder,
|
|
1094
|
-
command: command,
|
|
1095
|
-
description: description,
|
|
1096
|
-
handler: handler
|
|
1097
|
-
});
|
|
1149
|
+
const builder = (yargs) => {
|
|
1150
|
+
return yargs.options(options);
|
|
1151
|
+
};
|
|
1098
1152
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
.option('h', { alias: 'help' })
|
|
1107
|
-
.option('v', {
|
|
1108
|
-
alias: 'verbose',
|
|
1109
|
-
type: 'boolean',
|
|
1110
|
-
description: 'Run with verbose logging',
|
|
1111
|
-
}).argv;
|
|
1153
|
+
var commit = {
|
|
1154
|
+
command: 'commit',
|
|
1155
|
+
desc: 'Generate commit message',
|
|
1156
|
+
builder,
|
|
1157
|
+
handler,
|
|
1158
|
+
options,
|
|
1159
|
+
};
|
|
1112
1160
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1161
|
+
yargs.scriptName('coco').usage('$0 <cmd> [args]').command([commit.command, '$0'], commit.desc,
|
|
1162
|
+
// TODO: fix type on builder
|
|
1163
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1164
|
+
// @ts-ignore
|
|
1165
|
+
commit.builder, commit.handler).argv;
|