git-coco 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/changelog/handler.d.ts +3 -0
- package/dist/commands/changelog/index.d.ts +10 -0
- package/dist/commands/changelog/options.d.ts +16 -0
- 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 +689 -470
- package/dist/index.esm.mjs.map +1 -1
- package/dist/index.js +689 -471
- package/dist/lib/langchain/executeChain.d.ts +6 -0
- package/dist/lib/langchain/prompts/changelog.d.ts +3 -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/getCommitLogRange.d.ts +7 -0
- package/dist/lib/simple-git/getDiff.d.ts +2 -2
- package/dist/lib/simple-git/getStatus.d.ts +1 -1
- package/dist/lib/simple-git/getSummaryText.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 +16 -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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
356
|
+
const template$2 = `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$2 = ['text'];
|
|
363
|
+
const SUMMARIZE_PROMPT = new prompts.PromptTemplate({
|
|
364
|
+
template: template$2,
|
|
365
|
+
inputVariables: inputVariables$2,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
async function parseDefaultFileDiff(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]);
|
|
373
|
+
}
|
|
374
|
+
async function parseRenamedFileDiff(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)
|
|
@@ -568,23 +408,23 @@ const parseRenamedFileDiff = async (nodeFile, git, logger) => {
|
|
|
568
408
|
result = 'Error comparing file contents.';
|
|
569
409
|
}
|
|
570
410
|
return result;
|
|
571
|
-
}
|
|
572
|
-
|
|
411
|
+
}
|
|
412
|
+
async function getDiff(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,36 +444,105 @@ 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
|
-
|
|
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' });
|
|
621
468
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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$1 = `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$1 = ['summary'];
|
|
540
|
+
const COMMIT_PROMPT = new prompts.PromptTemplate({
|
|
541
|
+
template: template$1,
|
|
542
|
+
inputVariables: inputVariables$1,
|
|
543
|
+
});
|
|
635
544
|
|
|
636
|
-
|
|
545
|
+
function getStatus(file, location = 'index') {
|
|
637
546
|
if ('index' in file && 'working_dir' in file) {
|
|
638
547
|
const statusCode = file[location];
|
|
639
548
|
switch (statusCode) {
|
|
@@ -665,11 +574,11 @@ const getStatus = (file, location = 'index') => {
|
|
|
665
574
|
return 'unknown';
|
|
666
575
|
}
|
|
667
576
|
else {
|
|
668
|
-
throw new Error(
|
|
577
|
+
throw new Error('Invalid file type');
|
|
669
578
|
}
|
|
670
|
-
}
|
|
579
|
+
}
|
|
671
580
|
|
|
672
|
-
|
|
581
|
+
function getSummaryText(file, change) {
|
|
673
582
|
const status = change.status || getStatus(file);
|
|
674
583
|
let filePath;
|
|
675
584
|
if ('path' in file) {
|
|
@@ -679,13 +588,186 @@ const getSummaryText = (file, change) => {
|
|
|
679
588
|
filePath = change?.filePath || file.file;
|
|
680
589
|
}
|
|
681
590
|
else {
|
|
682
|
-
throw new Error(
|
|
591
|
+
throw new Error('Invalid file type');
|
|
683
592
|
}
|
|
684
593
|
if (change.oldFilePath) {
|
|
685
594
|
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
686
595
|
}
|
|
687
596
|
return `${status}: ${filePath}`;
|
|
597
|
+
}
|
|
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
|
+
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
738
|
+
temperature: 0.4,
|
|
739
|
+
mode: 'stdout',
|
|
740
|
+
ignoredFiles: ['package-lock.json'],
|
|
741
|
+
ignoredExtensions: ['.map', '.lock'],
|
|
688
742
|
};
|
|
743
|
+
/**
|
|
744
|
+
* Load application config
|
|
745
|
+
*
|
|
746
|
+
* Merge config from multiple sources.
|
|
747
|
+
*
|
|
748
|
+
* \* Order of precedence:
|
|
749
|
+
* \* 1. Command line flags
|
|
750
|
+
* \* 2. Environment variables
|
|
751
|
+
* \* 3. Project config
|
|
752
|
+
* \* 4. Git config
|
|
753
|
+
* \* 5. XDG config
|
|
754
|
+
* \* 6. .gitignore
|
|
755
|
+
* \* 7. .ignore
|
|
756
|
+
* \* 8. Default config
|
|
757
|
+
*
|
|
758
|
+
* @returns {Config} application config
|
|
759
|
+
**/
|
|
760
|
+
function loadConfig(argv = {}) {
|
|
761
|
+
// Default config
|
|
762
|
+
let config = DEFAULT_CONFIG;
|
|
763
|
+
config = loadGitignore(config);
|
|
764
|
+
config = loadIgnore(config);
|
|
765
|
+
config = loadXDGConfig(config);
|
|
766
|
+
config = loadGitConfig(config);
|
|
767
|
+
config = loadProjectConfig(config);
|
|
768
|
+
config = loadEnvConfig(config);
|
|
769
|
+
return { ...config, ...argv };
|
|
770
|
+
}
|
|
689
771
|
|
|
690
772
|
const config = loadConfig();
|
|
691
773
|
const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
|
|
@@ -743,7 +825,7 @@ async function getChanges({ git, options }) {
|
|
|
743
825
|
};
|
|
744
826
|
}
|
|
745
827
|
|
|
746
|
-
|
|
828
|
+
async function noResult({ git, logger }) {
|
|
747
829
|
const { staged, unstaged, untracked } = await getChanges({ git });
|
|
748
830
|
const hasStaged = staged && staged.length > 0;
|
|
749
831
|
const hasUnstaged = unstaged && unstaged.length > 0;
|
|
@@ -770,112 +852,117 @@ const noResult = async ({ git, logger }) => {
|
|
|
770
852
|
else {
|
|
771
853
|
logger.log('No repo changes detected. 👀', { color: 'blue' });
|
|
772
854
|
}
|
|
773
|
-
};
|
|
774
|
-
|
|
775
|
-
async function createCommit(commitMsg, git) {
|
|
776
|
-
return await git.commit(commitMsg);
|
|
777
855
|
}
|
|
778
856
|
|
|
779
|
-
const SEPERATOR = chalk.blue('----------------');
|
|
780
857
|
const isInteractive = (argv) => {
|
|
781
858
|
return argv?.mode === 'interactive' || argv.interactive;
|
|
782
859
|
};
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
860
|
+
const SEPERATOR = chalk.blue('----------------');
|
|
861
|
+
|
|
862
|
+
function logResult(result) {
|
|
863
|
+
console.log(`\n${chalk.bgBlue(chalk.bold('Proposed Commit:'))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function editResult(result, options) {
|
|
867
|
+
if (options.openInEditor) {
|
|
868
|
+
return await prompts$1.editor({
|
|
869
|
+
message: 'Edit the commit message',
|
|
870
|
+
default: result,
|
|
871
|
+
waitForUseInput: false,
|
|
872
|
+
validate: (text) => (text ? true : 'Commit message cannot be empty'),
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
return result;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async function getUserReviewDecision() {
|
|
879
|
+
return await prompts$1.select({
|
|
880
|
+
message: 'Would you like to make any changes to the commit message?',
|
|
881
|
+
choices: [
|
|
882
|
+
{
|
|
883
|
+
name: '✨ Looks good!',
|
|
884
|
+
value: 'approve',
|
|
885
|
+
description: 'Commit staged changes with generated commit message',
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
name: '📝 Edit',
|
|
889
|
+
value: 'edit',
|
|
890
|
+
description: 'Edit the commit message before proceeding',
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
name: '🪶 Modify Prompt',
|
|
894
|
+
value: 'modifyPrompt',
|
|
895
|
+
description: 'Modify the prompt template and regenerate the commit message',
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
name: '🔄 Retry - Message Only',
|
|
899
|
+
value: 'retryMessageOnly',
|
|
900
|
+
description: 'Restart the function execution from generating the commit message',
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
name: '🔄 Retry - Full',
|
|
904
|
+
value: 'retryFull',
|
|
905
|
+
description: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
name: '💣 Cancel',
|
|
909
|
+
value: 'cancel',
|
|
910
|
+
},
|
|
911
|
+
],
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
async function editPrompt(options) {
|
|
916
|
+
return await prompts$1.editor({
|
|
917
|
+
message: 'Edit the prompt',
|
|
918
|
+
default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
|
|
919
|
+
waitForUseInput: false,
|
|
920
|
+
validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async function generateAndReviewLoop({ label, factory, parser, noResult, agent, options, }) {
|
|
925
|
+
const { logger } = options;
|
|
796
926
|
let continueLoop = true;
|
|
927
|
+
let modifyPrompt = false;
|
|
928
|
+
let context = '';
|
|
929
|
+
let result = '';
|
|
930
|
+
const changes = await factory();
|
|
931
|
+
// if we don't have any changes, bail.
|
|
932
|
+
if (!changes || !changes.length) {
|
|
933
|
+
await noResult(options);
|
|
934
|
+
}
|
|
797
935
|
while (continueLoop) {
|
|
798
|
-
if (
|
|
799
|
-
|
|
800
|
-
color: 'blue',
|
|
801
|
-
});
|
|
802
|
-
summary = await fileChangeParser(changes, { tokenizer, git, model, logger });
|
|
936
|
+
if (!context.length) {
|
|
937
|
+
context = await parser(changes, result, options);
|
|
803
938
|
}
|
|
804
|
-
//
|
|
805
|
-
if (!
|
|
806
|
-
await noResult(
|
|
807
|
-
process.exit(0);
|
|
939
|
+
// if we still don't have a context, bail.
|
|
940
|
+
if (!context.length) {
|
|
941
|
+
await noResult(options);
|
|
808
942
|
}
|
|
809
|
-
// Prompt user for commit template prompt, if necessary
|
|
810
943
|
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
|
-
});
|
|
944
|
+
options.prompt = await editPrompt(options);
|
|
817
945
|
}
|
|
818
|
-
logger.startTimer().startSpinner(`Generating
|
|
946
|
+
logger.startTimer().startSpinner(`Generating ${label}\n`, {
|
|
819
947
|
color: 'blue',
|
|
820
948
|
});
|
|
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.', {
|
|
949
|
+
result = await agent(context, options);
|
|
950
|
+
if (!result) {
|
|
951
|
+
logger.stopSpinner('💀 Agent failed to return content.', {
|
|
832
952
|
mode: 'fail',
|
|
833
953
|
color: 'red',
|
|
834
954
|
});
|
|
835
955
|
process.exit(0);
|
|
836
956
|
}
|
|
837
957
|
logger
|
|
838
|
-
.stopSpinner(
|
|
958
|
+
.stopSpinner(`Generated ${label}`, {
|
|
839
959
|
color: 'green',
|
|
840
960
|
mode: 'succeed',
|
|
841
961
|
})
|
|
842
962
|
.stopTimer();
|
|
843
963
|
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
|
-
});
|
|
964
|
+
logResult(result);
|
|
965
|
+
const reviewAnswer = await getUserReviewDecision();
|
|
879
966
|
if (reviewAnswer === 'cancel') {
|
|
880
967
|
process.exit(0);
|
|
881
968
|
}
|
|
@@ -883,133 +970,138 @@ const generateCommitMessageAndReviewLoop = async (changes, options) => {
|
|
|
883
970
|
options.openInEditor = true;
|
|
884
971
|
}
|
|
885
972
|
if (reviewAnswer === 'retryFull') {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
973
|
+
context = '';
|
|
974
|
+
result = '';
|
|
975
|
+
options.prompt = '';
|
|
889
976
|
continue;
|
|
890
977
|
}
|
|
891
978
|
if (reviewAnswer === 'retryMessageOnly') {
|
|
892
979
|
modifyPrompt = false;
|
|
893
|
-
|
|
980
|
+
result = '';
|
|
894
981
|
continue;
|
|
895
982
|
}
|
|
896
983
|
if (reviewAnswer === 'modifyPrompt') {
|
|
897
984
|
modifyPrompt = true;
|
|
898
|
-
|
|
985
|
+
result = '';
|
|
899
986
|
continue;
|
|
900
987
|
}
|
|
901
988
|
}
|
|
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
|
-
}
|
|
989
|
+
// if we're here, we're done.
|
|
990
|
+
result = await editResult(result, options);
|
|
915
991
|
continueLoop = false;
|
|
916
992
|
}
|
|
917
|
-
return
|
|
993
|
+
return result;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const executeChain = async ({ llm, prompt, variables }) => {
|
|
997
|
+
if (!llm || !prompt || !variables) {
|
|
998
|
+
throw new Error('The input parameters "llm", "prompt", and "variables" are all required.');
|
|
999
|
+
}
|
|
1000
|
+
const chain = new chains.LLMChain({ llm, prompt });
|
|
1001
|
+
let res;
|
|
1002
|
+
try {
|
|
1003
|
+
res = await chain.call(variables);
|
|
1004
|
+
}
|
|
1005
|
+
catch (error) {
|
|
1006
|
+
if (error instanceof Error) {
|
|
1007
|
+
throw new Error(`LLMChain call error: ${error.message}`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
if (!res) {
|
|
1011
|
+
throw new Error('Empty response from LLMChain call');
|
|
1012
|
+
}
|
|
1013
|
+
if (res.error) {
|
|
1014
|
+
throw new Error(`LLMChain response error: ${res.error}`);
|
|
1015
|
+
}
|
|
1016
|
+
return res.text.trim();
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
async function createCommit(commitMsg, git) {
|
|
1020
|
+
return await git.commit(commitMsg);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const logSuccess = () => {
|
|
1024
|
+
console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
|
|
918
1025
|
};
|
|
919
|
-
|
|
1026
|
+
|
|
1027
|
+
const handleResult = async (result, { mode, git }) => {
|
|
920
1028
|
// Handle resulting commit message
|
|
921
1029
|
switch (mode) {
|
|
922
1030
|
case 'interactive':
|
|
923
|
-
await createCommit(
|
|
1031
|
+
await createCommit(result, git);
|
|
924
1032
|
logSuccess();
|
|
925
1033
|
break;
|
|
926
1034
|
case 'stdout':
|
|
927
1035
|
default:
|
|
928
|
-
process.stdout.write(
|
|
1036
|
+
process.stdout.write(result, 'utf8');
|
|
929
1037
|
break;
|
|
930
1038
|
}
|
|
931
1039
|
process.exit(0);
|
|
932
1040
|
};
|
|
933
1041
|
|
|
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;
|
|
1042
|
+
const tokenizer = getTokenizer();
|
|
1043
|
+
const git$1 = simpleGit.simpleGit();
|
|
1044
|
+
async function handler$1(argv) {
|
|
1045
|
+
const options = loadConfig(argv);
|
|
1046
|
+
const logger = new Logger(options);
|
|
1047
|
+
const key = getApiKeyForModel(options.model, options);
|
|
1048
|
+
if (!key) {
|
|
1049
|
+
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
1050
|
+
process.exit(1);
|
|
993
1051
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1052
|
+
const model = getModel(options.model, key, {
|
|
1053
|
+
temperature: 0.4,
|
|
1054
|
+
maxConcurrency: 10,
|
|
1055
|
+
});
|
|
1056
|
+
const INTERACTIVE = isInteractive(options);
|
|
1057
|
+
async function factory() {
|
|
1058
|
+
const changes = await getChanges({ git: git$1 });
|
|
1059
|
+
return changes.staged;
|
|
998
1060
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1061
|
+
async function parser(changes) {
|
|
1062
|
+
return await fileChangeParser({
|
|
1063
|
+
changes,
|
|
1064
|
+
commit: '--staged',
|
|
1065
|
+
options: { tokenizer, git: git$1, model, logger },
|
|
1066
|
+
});
|
|
1004
1067
|
}
|
|
1068
|
+
const commitMsg = await generateAndReviewLoop({
|
|
1069
|
+
label: 'Commit Message',
|
|
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: git$1, logger });
|
|
1085
|
+
process.exit(0);
|
|
1086
|
+
},
|
|
1087
|
+
options: {
|
|
1088
|
+
...options,
|
|
1089
|
+
prompt: options.prompt || COMMIT_PROMPT.template,
|
|
1090
|
+
logger,
|
|
1091
|
+
interactive: INTERACTIVE,
|
|
1092
|
+
},
|
|
1093
|
+
});
|
|
1094
|
+
const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
|
|
1095
|
+
handleResult(commitMsg, {
|
|
1096
|
+
mode: MODE,
|
|
1097
|
+
git: git$1,
|
|
1098
|
+
});
|
|
1005
1099
|
}
|
|
1006
1100
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
const
|
|
1011
|
-
const description = 'Generate a commit message based on the diff summary';
|
|
1012
|
-
const builder = {
|
|
1101
|
+
/**
|
|
1102
|
+
* Command line options via yargs
|
|
1103
|
+
*/
|
|
1104
|
+
const options$1 = {
|
|
1013
1105
|
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
1014
1106
|
openAIApiKey: {
|
|
1015
1107
|
type: 'string',
|
|
@@ -1055,10 +1147,56 @@ const builder = {
|
|
|
1055
1147
|
description: 'Ignored extensions',
|
|
1056
1148
|
},
|
|
1057
1149
|
};
|
|
1150
|
+
const builder$1 = (yargs) => {
|
|
1151
|
+
return yargs.options(options$1);
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
var commit = {
|
|
1155
|
+
command: 'commit',
|
|
1156
|
+
desc: 'Generate commit message',
|
|
1157
|
+
builder: builder$1,
|
|
1158
|
+
handler: handler$1,
|
|
1159
|
+
options: options$1,
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
const template = `Write informative git changelog, in the imperative, based on a series of individual messages.
|
|
1163
|
+
|
|
1164
|
+
- Typically a hyphen or asterisk is used for the bullet
|
|
1165
|
+
- Summarize dependency updates
|
|
1166
|
+
|
|
1167
|
+
"""{summary}"""
|
|
1168
|
+
|
|
1169
|
+
Changelog:`;
|
|
1170
|
+
const inputVariables = ['summary'];
|
|
1171
|
+
const CHANGELOG_PROMPT = new prompts.PromptTemplate({
|
|
1172
|
+
template,
|
|
1173
|
+
inputVariables,
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
async function getCommitLogRange(from, to, { noMerges, git }) {
|
|
1177
|
+
try {
|
|
1178
|
+
const output = await git.raw([
|
|
1179
|
+
'log',
|
|
1180
|
+
`${from}..${to}`,
|
|
1181
|
+
'--pretty=format:%s',
|
|
1182
|
+
// Include '--no-merges' here if you want to exclude merge commits.
|
|
1183
|
+
noMerges ? '--no-merges' : null,
|
|
1184
|
+
].filter(Boolean)); // filter(Boolean) removes any null values from the array
|
|
1185
|
+
const messages = output.split('\n').filter(Boolean);
|
|
1186
|
+
return messages;
|
|
1187
|
+
}
|
|
1188
|
+
catch (error) {
|
|
1189
|
+
// If there's an error, handle it appropriately
|
|
1190
|
+
console.error('Error getting commit messages:', error);
|
|
1191
|
+
throw error;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const git = simpleGit.simpleGit();
|
|
1058
1196
|
async function handler(argv) {
|
|
1059
1197
|
const options = loadConfig(argv);
|
|
1060
1198
|
const logger = new Logger(options);
|
|
1061
|
-
const key =
|
|
1199
|
+
const key = getApiKeyForModel(options.model, options);
|
|
1062
1200
|
if (!key) {
|
|
1063
1201
|
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
1064
1202
|
process.exit(1);
|
|
@@ -1068,47 +1206,127 @@ async function handler(argv) {
|
|
|
1068
1206
|
maxConcurrency: 10,
|
|
1069
1207
|
});
|
|
1070
1208
|
const INTERACTIVE = isInteractive(options);
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
logger
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1209
|
+
const [from, to] = options.range?.split(':');
|
|
1210
|
+
if (!from || !to) {
|
|
1211
|
+
logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
|
|
1212
|
+
process.exit(1);
|
|
1213
|
+
}
|
|
1214
|
+
async function factory() {
|
|
1215
|
+
const messages = await getCommitLogRange(from, to, { git, noMerges: true });
|
|
1216
|
+
return messages;
|
|
1217
|
+
}
|
|
1218
|
+
async function parser(messages) {
|
|
1219
|
+
const result = messages.join('\n');
|
|
1220
|
+
return result;
|
|
1221
|
+
}
|
|
1222
|
+
const changelogMsg = await generateAndReviewLoop({
|
|
1223
|
+
label: 'Changelog',
|
|
1224
|
+
factory,
|
|
1225
|
+
parser,
|
|
1226
|
+
agent: async (context, options) => {
|
|
1227
|
+
const prompt = getPrompt({
|
|
1228
|
+
template: options.prompt,
|
|
1229
|
+
variables: CHANGELOG_PROMPT.inputVariables,
|
|
1230
|
+
fallback: CHANGELOG_PROMPT,
|
|
1231
|
+
});
|
|
1232
|
+
return await executeChain({
|
|
1233
|
+
llm: model,
|
|
1234
|
+
prompt,
|
|
1235
|
+
variables: { summary: context },
|
|
1236
|
+
});
|
|
1237
|
+
},
|
|
1238
|
+
noResult: async () => {
|
|
1239
|
+
await noResult({ git, logger });
|
|
1240
|
+
process.exit(0);
|
|
1241
|
+
},
|
|
1242
|
+
options: {
|
|
1243
|
+
...options,
|
|
1244
|
+
prompt: options.prompt || CHANGELOG_PROMPT.template,
|
|
1245
|
+
logger,
|
|
1246
|
+
interactive: INTERACTIVE,
|
|
1247
|
+
},
|
|
1080
1248
|
});
|
|
1081
|
-
const MODE = (options.
|
|
1082
|
-
|
|
1083
|
-
options?.mode ||
|
|
1084
|
-
'stdout';
|
|
1085
|
-
handleResult(commitMsg, {
|
|
1249
|
+
const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
|
|
1250
|
+
handleResult(changelogMsg, {
|
|
1086
1251
|
mode: MODE,
|
|
1087
1252
|
git,
|
|
1088
1253
|
});
|
|
1089
1254
|
}
|
|
1090
1255
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1256
|
+
/**
|
|
1257
|
+
* Command line options via yargs
|
|
1258
|
+
*/
|
|
1259
|
+
const options = {
|
|
1260
|
+
range: {
|
|
1261
|
+
type: 'string',
|
|
1262
|
+
alias: 'r',
|
|
1263
|
+
description: 'Commit range e.g `HEAD~2:HEAD`',
|
|
1264
|
+
demandOption: true,
|
|
1265
|
+
},
|
|
1266
|
+
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
1267
|
+
openAIApiKey: {
|
|
1268
|
+
type: 'string',
|
|
1269
|
+
description: 'OpenAI API Key',
|
|
1270
|
+
conflicts: 'huggingFaceHubApiKey',
|
|
1271
|
+
},
|
|
1272
|
+
huggingFaceHubApiKey: {
|
|
1273
|
+
type: 'string',
|
|
1274
|
+
description: 'HuggingFace Hub API Key',
|
|
1275
|
+
conflicts: 'openAIApiKey',
|
|
1276
|
+
},
|
|
1277
|
+
tokenLimit: { type: 'number', description: 'Token limit' },
|
|
1278
|
+
prompt: {
|
|
1279
|
+
type: 'string',
|
|
1280
|
+
alias: 'p',
|
|
1281
|
+
description: 'Prompt for llm',
|
|
1282
|
+
},
|
|
1283
|
+
i: {
|
|
1284
|
+
type: 'boolean',
|
|
1285
|
+
alias: 'interactive',
|
|
1286
|
+
description: 'Toggle interactive mode',
|
|
1287
|
+
},
|
|
1288
|
+
e: {
|
|
1289
|
+
type: 'boolean',
|
|
1290
|
+
alias: 'edit',
|
|
1291
|
+
description: 'Open generated changelog message in editor before proceeding',
|
|
1292
|
+
},
|
|
1293
|
+
summarizePrompt: {
|
|
1294
|
+
type: 'string',
|
|
1295
|
+
description: 'Prompt for summarizing large files',
|
|
1296
|
+
},
|
|
1297
|
+
ignoredFiles: {
|
|
1298
|
+
type: 'array',
|
|
1299
|
+
description: 'Ignored files',
|
|
1300
|
+
},
|
|
1301
|
+
ignoredExtensions: {
|
|
1302
|
+
type: 'array',
|
|
1303
|
+
description: 'Ignored extensions',
|
|
1304
|
+
},
|
|
1305
|
+
};
|
|
1306
|
+
const builder = (yargs) => {
|
|
1307
|
+
return yargs.options(options);
|
|
1308
|
+
};
|
|
1309
|
+
|
|
1310
|
+
var changelog = {
|
|
1311
|
+
command: 'changelog',
|
|
1312
|
+
desc: 'Generate a changelog from a commit range',
|
|
1313
|
+
builder,
|
|
1314
|
+
handler,
|
|
1315
|
+
options,
|
|
1316
|
+
};
|
|
1098
1317
|
|
|
1099
1318
|
yargs
|
|
1100
1319
|
.scriptName('coco')
|
|
1101
|
-
.
|
|
1102
|
-
|
|
1103
|
-
|
|
1320
|
+
.usage('$0 <cmd> [args]')
|
|
1321
|
+
.command([commit.command, '$0'], commit.desc,
|
|
1322
|
+
// TODO: fix type on builder
|
|
1323
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1324
|
+
// @ts-ignore
|
|
1325
|
+
commit.builder, commit.handler)
|
|
1326
|
+
.command(changelog.command, changelog.desc,
|
|
1327
|
+
// TODO: fix type on builder
|
|
1328
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1329
|
+
// @ts-ignore
|
|
1330
|
+
changelog.builder, changelog.handler)
|
|
1104
1331
|
.demandCommand()
|
|
1105
|
-
.
|
|
1106
|
-
.option('h', { alias: 'help' })
|
|
1107
|
-
.option('v', {
|
|
1108
|
-
alias: 'verbose',
|
|
1109
|
-
type: 'boolean',
|
|
1110
|
-
description: 'Run with verbose logging',
|
|
1111
|
-
}).argv;
|
|
1112
|
-
|
|
1113
|
-
exports.commit = commit;
|
|
1114
|
-
exports.loadConfig = loadConfig;
|
|
1332
|
+
.help().argv;
|