git-coco 0.3.2 â 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/commit/handler.d.ts +3 -0
- package/dist/commands/commit/index.d.ts +10 -0
- package/dist/commands/commit/options.d.ts +15 -0
- package/dist/commands/types.d.ts +14 -0
- package/dist/index.d.ts +1 -3
- package/dist/index.esm.mjs +588 -486
- package/dist/index.esm.mjs.map +1 -1
- package/dist/index.js +586 -485
- package/dist/lib/config/types.d.ts +1 -1
- package/dist/lib/langchain/executeChain.d.ts +6 -0
- package/dist/lib/langchain/prompts/commitDefault.d.ts +1 -1
- package/dist/lib/langchain/prompts/summarize.d.ts +1 -1
- package/dist/lib/langchain/utils.d.ts +4 -7
- package/dist/lib/parsers/default/index.d.ts +2 -2
- package/dist/lib/parsers/default/utils/createDiffTree.d.ts +1 -0
- package/dist/lib/parsers/noResult.d.ts +4 -2
- package/dist/lib/simple-git/getChanges.d.ts +7 -4
- package/dist/lib/simple-git/getChangesByCommit.d.ts +13 -0
- package/dist/lib/simple-git/getDiff.d.ts +1 -1
- package/dist/lib/simple-git/getDiffFromCommmit.d.ts +10 -0
- package/dist/lib/simple-git/getStatus.d.ts +2 -2
- package/dist/lib/simple-git/getSummaryText.d.ts +2 -2
- package/dist/lib/simple-git/helpers.d.ts +6 -0
- package/dist/lib/types.d.ts +21 -10
- package/dist/lib/ui/editPrompt.d.ts +2 -0
- package/dist/lib/ui/editResult.d.ts +2 -0
- package/dist/lib/ui/generateAndReviewLoop.d.ts +15 -0
- package/dist/lib/ui/getUserReviewDecision.d.ts +2 -0
- package/dist/lib/ui/handleResult.d.ts +5 -0
- package/dist/lib/ui/helpers.d.ts +3 -0
- package/dist/lib/ui/logResult.d.ts +1 -0
- package/dist/lib/ui/logSuccess.d.ts +1 -0
- package/dist/stats.html +1 -1
- package/package.json +5 -5
- package/dist/commands/commit.d.ts +0 -16
- package/dist/lib/langchain/chains/llm.d.ts +0 -6
- package/dist/lib/ui.d.ts +0 -2
- package/dist/types.d.ts +0 -10
package/dist/index.js
CHANGED
|
@@ -2,26 +2,26 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
var yargs = require('yargs');
|
|
5
|
-
var prompts$1 = require('@inquirer/prompts');
|
|
6
|
-
var simpleGit = require('simple-git');
|
|
7
|
-
var fs = require('fs');
|
|
8
|
-
var os = require('os');
|
|
9
|
-
var path = require('path');
|
|
10
|
-
var ini = require('ini');
|
|
11
|
-
var prompts = require('langchain/prompts');
|
|
12
5
|
var pQueue = require('p-queue');
|
|
13
6
|
var document = require('langchain/document');
|
|
14
7
|
var hf = require('langchain/llms/hf');
|
|
8
|
+
var prompts = require('langchain/prompts');
|
|
15
9
|
var chains = require('langchain/chains');
|
|
16
10
|
var openai = require('langchain/llms/openai');
|
|
17
11
|
var text_splitter = require('langchain/text_splitter');
|
|
18
12
|
var diff = require('diff');
|
|
19
|
-
var chalk = require('chalk');
|
|
20
13
|
var GPT3NodeTokenizer = require('gpt3-tokenizer');
|
|
14
|
+
var chalk = require('chalk');
|
|
21
15
|
var ora = require('ora');
|
|
22
16
|
var now = require('performance-now');
|
|
23
17
|
var prettyMilliseconds = require('pretty-ms');
|
|
18
|
+
var path = require('path');
|
|
24
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,214 +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 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
|
-
- Write concisely using an informal tone
|
|
180
|
-
- List significant changes
|
|
181
|
-
- DO NOT use phrases like "this commit", "this change", etc.
|
|
182
|
-
- DO NOT use specific names or files from the code
|
|
183
|
-
- Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
|
|
184
|
-
|
|
185
|
-
"""{summary}"""
|
|
186
|
-
|
|
187
|
-
Commit:`;
|
|
188
|
-
const inputVariables$1 = ['summary'];
|
|
189
|
-
const COMMIT_PROMPT = new prompts.PromptTemplate({
|
|
190
|
-
template: template$1,
|
|
191
|
-
inputVariables: inputVariables$1,
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
const template = `GOAL: Use functional abstractions to summarize the following text
|
|
195
|
-
|
|
196
|
-
RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
|
|
197
|
-
|
|
198
|
-
TEXT:"""{text}"""
|
|
199
|
-
`;
|
|
200
|
-
const inputVariables = ['text'];
|
|
201
|
-
const SUMMARIZE_PROMPT = new prompts.PromptTemplate({
|
|
202
|
-
template,
|
|
203
|
-
inputVariables,
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Default Config
|
|
208
|
-
*
|
|
209
|
-
* @type {Config}
|
|
210
|
-
*/
|
|
211
|
-
const DEFAULT_CONFIG = {
|
|
212
|
-
model: 'openai/gpt-3.5-turbo',
|
|
213
|
-
verbose: false,
|
|
214
|
-
tokenLimit: 1024,
|
|
215
|
-
prompt: COMMIT_PROMPT.template,
|
|
216
|
-
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
217
|
-
temperature: 0.4,
|
|
218
|
-
mode: 'stdout',
|
|
219
|
-
ignoredFiles: ['package-lock.json'],
|
|
220
|
-
ignoredExtensions: ['.map', '.lock'],
|
|
221
|
-
};
|
|
222
|
-
/**
|
|
223
|
-
* Load application config
|
|
224
|
-
*
|
|
225
|
-
* Merge config from multiple sources.
|
|
226
|
-
*
|
|
227
|
-
* \* Order of precedence:
|
|
228
|
-
* \* 1. Command line flags
|
|
229
|
-
* \* 2. Environment variables
|
|
230
|
-
* \* 3. Project config
|
|
231
|
-
* \* 4. Git config
|
|
232
|
-
* \* 5. XDG config
|
|
233
|
-
* \* 6. .gitignore
|
|
234
|
-
* \* 7. .ignore
|
|
235
|
-
* \* 8. Default config
|
|
236
|
-
*
|
|
237
|
-
* @returns {Config} application config
|
|
238
|
-
**/
|
|
239
|
-
function loadConfig(argv = {}) {
|
|
240
|
-
// Default config
|
|
241
|
-
let config = DEFAULT_CONFIG;
|
|
242
|
-
config = loadGitignore(config);
|
|
243
|
-
config = loadIgnore(config);
|
|
244
|
-
config = loadXDGConfig(config);
|
|
245
|
-
config = loadGitConfig(config);
|
|
246
|
-
config = loadProjectConfig(config);
|
|
247
|
-
config = loadEnvConfig(config);
|
|
248
|
-
return { ...config, ...argv };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
48
|
/**
|
|
252
49
|
* Extract the path from a file path string.
|
|
253
50
|
* @param {string} filePath - The full file path.
|
|
@@ -389,12 +186,29 @@ class DiffTreeNode {
|
|
|
389
186
|
getPath() {
|
|
390
187
|
return this.path.join('/');
|
|
391
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
|
+
}
|
|
392
206
|
}
|
|
393
207
|
const createDiffTree = (changes) => {
|
|
394
208
|
const root = new DiffTreeNode();
|
|
395
209
|
for (const change of changes) {
|
|
396
210
|
let currentParent = root;
|
|
397
|
-
const parts = change.
|
|
211
|
+
const parts = change.filePath.split('/');
|
|
398
212
|
parts.pop();
|
|
399
213
|
for (const part of parts) {
|
|
400
214
|
let childNode = currentParent.getChild(part);
|
|
@@ -406,8 +220,8 @@ const createDiffTree = (changes) => {
|
|
|
406
220
|
}
|
|
407
221
|
// Create a NodeFile object and add it to the parent
|
|
408
222
|
currentParent.addFile({
|
|
409
|
-
|
|
410
|
-
|
|
223
|
+
filePath: change.filePath,
|
|
224
|
+
oldFilePath: change.oldFilePath,
|
|
411
225
|
summary: change.summary,
|
|
412
226
|
status: change.status,
|
|
413
227
|
});
|
|
@@ -425,11 +239,11 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
|
|
|
425
239
|
// TODO: Swap out the GPT3Tokenizer for LangChain tokenizer
|
|
426
240
|
const tokenizedDiff = tokenizer.encode(diff).text;
|
|
427
241
|
const tokenCount = tokenizedDiff.length;
|
|
428
|
-
logger.verbose(`Collected diff for ${nodeFile.
|
|
242
|
+
logger.verbose(`Collected diff for ${nodeFile.filePath} (${tokenCount} tokens)`, {
|
|
429
243
|
color: 'magenta',
|
|
430
244
|
});
|
|
431
245
|
return {
|
|
432
|
-
file: nodeFile.
|
|
246
|
+
file: nodeFile.filePath,
|
|
433
247
|
summary: nodeFile.summary,
|
|
434
248
|
diff,
|
|
435
249
|
tokenCount,
|
|
@@ -454,7 +268,7 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
|
|
|
454
268
|
* @param configuration
|
|
455
269
|
* @returns LLM Model
|
|
456
270
|
*/
|
|
457
|
-
function getModel(name, key, fields
|
|
271
|
+
function getModel(name, key, fields) {
|
|
458
272
|
const [llm, model] = name.split(/\/(.*)/s);
|
|
459
273
|
if (!model) {
|
|
460
274
|
throw new Error(`Invalid model: ${name}`);
|
|
@@ -473,7 +287,7 @@ function getModel(name, key, fields, configuration) {
|
|
|
473
287
|
openAIApiKey: key,
|
|
474
288
|
modelName: model,
|
|
475
289
|
...fields,
|
|
476
|
-
}
|
|
290
|
+
});
|
|
477
291
|
}
|
|
478
292
|
}
|
|
479
293
|
/**
|
|
@@ -482,7 +296,7 @@ function getModel(name, key, fields, configuration) {
|
|
|
482
296
|
* @param options
|
|
483
297
|
* @returns
|
|
484
298
|
*/
|
|
485
|
-
function
|
|
299
|
+
function getApiKeyForModel(name, options) {
|
|
486
300
|
const [llm, model] = name.split(/\/(.*)/s);
|
|
487
301
|
if (!model) {
|
|
488
302
|
throw new Error(`Invalid model: ${name}`);
|
|
@@ -539,19 +353,47 @@ function validatePromptTemplate(text, inputVariables) {
|
|
|
539
353
|
return true;
|
|
540
354
|
}
|
|
541
355
|
|
|
542
|
-
const
|
|
543
|
-
|
|
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]);
|
|
544
373
|
};
|
|
545
|
-
const parseRenamedFileDiff = async (nodeFile, git, logger) => {
|
|
374
|
+
const parseRenamedFileDiff = async (nodeFile, commit, git, logger) => {
|
|
546
375
|
let result = '';
|
|
547
|
-
const
|
|
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
|
+
}
|
|
548
390
|
try {
|
|
549
|
-
const [
|
|
550
|
-
git.show([
|
|
551
|
-
git.show([
|
|
391
|
+
const [previousContent, newContent] = await Promise.all([
|
|
392
|
+
git.show([`${previousCommitHash}:${oldFilePath}`]),
|
|
393
|
+
git.show([`${newCommitHash}:${nodeFile.filePath}`]),
|
|
552
394
|
]);
|
|
553
|
-
if (
|
|
554
|
-
result = diff.createTwoFilesPatch(
|
|
395
|
+
if (previousContent !== newContent) {
|
|
396
|
+
result = diff.createTwoFilesPatch(oldFilePath, nodeFile.filePath, previousContent, newContent, '', '', {
|
|
555
397
|
context: 3,
|
|
556
398
|
});
|
|
557
399
|
// remove the first 4 lines of the patch (they contain the old and new file names)
|
|
@@ -562,27 +404,27 @@ const parseRenamedFileDiff = async (nodeFile, git, logger) => {
|
|
|
562
404
|
}
|
|
563
405
|
}
|
|
564
406
|
catch (err) {
|
|
565
|
-
logger.verbose(`Error comparing file contents for ${nodeFile.
|
|
407
|
+
logger.verbose(`Error comparing file contents for ${nodeFile.filePath}`, { color: 'red' });
|
|
566
408
|
result = 'Error comparing file contents.';
|
|
567
409
|
}
|
|
568
410
|
return result;
|
|
569
411
|
};
|
|
570
|
-
const getDiff = async (nodeFile, { git, logger, }) => {
|
|
412
|
+
const getDiff = async (nodeFile, commit, { git, logger, }) => {
|
|
571
413
|
if (nodeFile.status === 'deleted') {
|
|
572
414
|
return 'This file has been deleted.';
|
|
573
415
|
}
|
|
574
|
-
if (nodeFile.status === 'renamed' && nodeFile.
|
|
575
|
-
const renamedDiff = await parseRenamedFileDiff(nodeFile, git, logger);
|
|
416
|
+
if (nodeFile.status === 'renamed' && nodeFile.oldFilePath) {
|
|
417
|
+
const renamedDiff = await parseRenamedFileDiff(nodeFile, commit, git, logger);
|
|
576
418
|
return renamedDiff;
|
|
577
419
|
}
|
|
578
420
|
// If not deleted or renamed, get the diff from the index
|
|
579
|
-
const defaultDiff = await parseDefaultFileDiff(nodeFile, git);
|
|
421
|
+
const defaultDiff = await parseDefaultFileDiff(nodeFile, commit, git);
|
|
580
422
|
return defaultDiff;
|
|
581
423
|
};
|
|
582
424
|
|
|
583
425
|
const MAX_TOKENS_PER_SUMMARY = 2048;
|
|
584
|
-
|
|
585
|
-
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 });
|
|
586
428
|
const summarizationChain = getChain(model, {
|
|
587
429
|
type: 'map_reduce',
|
|
588
430
|
combineMapPrompt: SUMMARIZE_PROMPT,
|
|
@@ -593,7 +435,7 @@ const fileChangeParser = async (changes, { tokenizer, git, model, logger }) => {
|
|
|
593
435
|
logger.stopTimer('Created file hierarchy');
|
|
594
436
|
// Collect diffs
|
|
595
437
|
logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
|
|
596
|
-
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);
|
|
597
439
|
logger.stopSpinner('Diffs Collected').stopTimer();
|
|
598
440
|
// Summarize diffs
|
|
599
441
|
logger.startTimer();
|
|
@@ -602,19 +444,11 @@ const fileChangeParser = async (changes, { tokenizer, git, model, logger }) => {
|
|
|
602
444
|
maxTokens: MAX_TOKENS_PER_SUMMARY,
|
|
603
445
|
textSplitter,
|
|
604
446
|
chain: summarizationChain,
|
|
605
|
-
logger
|
|
447
|
+
logger,
|
|
606
448
|
});
|
|
607
449
|
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
608
450
|
return summary;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
const SEPERATOR = chalk.blue('----------------');
|
|
612
|
-
const logCommit = (commit) => {
|
|
613
|
-
console.log(`\n${chalk.bgBlue(chalk.bold('Proposed Commit:'))}\n${SEPERATOR}\n${commit}\n${SEPERATOR}\n`);
|
|
614
|
-
};
|
|
615
|
-
const logSuccess = () => {
|
|
616
|
-
console.log(chalk.green(chalk.bold('\nAll set! ðĶūðĪ')));
|
|
617
|
-
};
|
|
451
|
+
}
|
|
618
452
|
|
|
619
453
|
/**
|
|
620
454
|
* Wrapper around GPT3NodeTokenizer to handle default export.
|
|
@@ -689,76 +523,266 @@ class Logger {
|
|
|
689
523
|
}
|
|
690
524
|
}
|
|
691
525
|
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
+
});
|
|
544
|
+
|
|
545
|
+
const getStatus = (file, location = 'index') => {
|
|
546
|
+
if ('index' in file && 'working_dir' in file) {
|
|
547
|
+
const statusCode = file[location];
|
|
548
|
+
switch (statusCode) {
|
|
549
|
+
case 'A':
|
|
550
|
+
return 'added';
|
|
551
|
+
case 'D':
|
|
552
|
+
return 'deleted';
|
|
553
|
+
case 'M':
|
|
554
|
+
return 'modified';
|
|
555
|
+
case 'R':
|
|
556
|
+
return 'renamed';
|
|
557
|
+
case '?':
|
|
558
|
+
return 'untracked';
|
|
559
|
+
default:
|
|
560
|
+
return 'unknown';
|
|
704
561
|
}
|
|
705
562
|
}
|
|
706
|
-
if (
|
|
707
|
-
|
|
563
|
+
else if ('changes' in file && 'binary' in file) {
|
|
564
|
+
if (file.changes === 0)
|
|
565
|
+
return 'untracked';
|
|
566
|
+
if (file.file.includes('=>'))
|
|
567
|
+
return 'renamed';
|
|
568
|
+
if (file.deletions === 0 && file.insertions > 0)
|
|
569
|
+
return 'added';
|
|
570
|
+
if (file.insertions === 0 && file.deletions > 0)
|
|
571
|
+
return 'deleted';
|
|
572
|
+
if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
|
|
573
|
+
return 'modified';
|
|
574
|
+
return 'unknown';
|
|
708
575
|
}
|
|
709
|
-
|
|
710
|
-
throw new Error(
|
|
576
|
+
else {
|
|
577
|
+
throw new Error("Invalid file type");
|
|
711
578
|
}
|
|
712
|
-
return res.text.trim();
|
|
713
579
|
};
|
|
714
580
|
|
|
715
|
-
const
|
|
716
|
-
const
|
|
717
|
-
let
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
status = 'renamed';
|
|
730
|
-
break;
|
|
731
|
-
case '?':
|
|
732
|
-
status = 'untracked';
|
|
733
|
-
break;
|
|
734
|
-
default:
|
|
735
|
-
status = 'unknown';
|
|
736
|
-
break;
|
|
581
|
+
const getSummaryText = (file, change) => {
|
|
582
|
+
const status = change.status || getStatus(file);
|
|
583
|
+
let filePath;
|
|
584
|
+
if ('path' in file) {
|
|
585
|
+
filePath = file.path;
|
|
586
|
+
}
|
|
587
|
+
else if ('file' in file) {
|
|
588
|
+
filePath = change?.filePath || file.file;
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
throw new Error("Invalid file type");
|
|
592
|
+
}
|
|
593
|
+
if (change.oldFilePath) {
|
|
594
|
+
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
737
595
|
}
|
|
738
|
-
return status
|
|
596
|
+
return `${status}: ${filePath}`;
|
|
739
597
|
};
|
|
740
598
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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 };
|
|
745
708
|
}
|
|
746
|
-
return
|
|
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'],
|
|
747
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
|
+
}
|
|
748
772
|
|
|
749
773
|
const config = loadConfig();
|
|
750
774
|
const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
|
|
751
775
|
const DEFAULT_IGNORED_EXTENSIONS = config?.ignoredExtensions?.length ? config.ignoredExtensions : [];
|
|
752
|
-
async function getChanges(git, options
|
|
753
|
-
const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options;
|
|
776
|
+
async function getChanges({ git, options }) {
|
|
777
|
+
const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options || {};
|
|
754
778
|
const staged = [];
|
|
755
779
|
const unstaged = [];
|
|
756
780
|
const untracked = [];
|
|
757
781
|
const status = await git.status();
|
|
758
782
|
status.files.forEach((file) => {
|
|
759
783
|
const fileChange = {
|
|
760
|
-
|
|
761
|
-
|
|
784
|
+
filePath: file.path,
|
|
785
|
+
oldFilePath: status.renamed.filter((renamed) => renamed.to === file.path)[0]?.from,
|
|
762
786
|
};
|
|
763
787
|
// Unstaged files
|
|
764
788
|
if (file.working_dir !== '?' && file.working_dir !== ' ') {
|
|
@@ -781,16 +805,19 @@ async function getChanges(git, options = {}) {
|
|
|
781
805
|
});
|
|
782
806
|
const ignoredExtensionsSet = new Set(ignoredExtensions.map((extension) => extension.toLowerCase()));
|
|
783
807
|
const filteredStaged = staged.filter((file) => {
|
|
784
|
-
const extension = path.extname(file.
|
|
785
|
-
return !ignoredExtensionsSet.has(extension) &&
|
|
808
|
+
const extension = path.extname(file.filePath).toLowerCase();
|
|
809
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
810
|
+
!ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
|
|
786
811
|
});
|
|
787
812
|
const filteredUnstaged = unstaged.filter((file) => {
|
|
788
|
-
const extension = path.extname(file.
|
|
789
|
-
return !ignoredExtensionsSet.has(extension) &&
|
|
813
|
+
const extension = path.extname(file.filePath).toLowerCase();
|
|
814
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
815
|
+
!ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
|
|
790
816
|
});
|
|
791
817
|
const filteredUntracked = untracked.filter((file) => {
|
|
792
|
-
const extension = path.extname(file.
|
|
793
|
-
return !ignoredExtensionsSet.has(extension) &&
|
|
818
|
+
const extension = path.extname(file.filePath).toLowerCase();
|
|
819
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
820
|
+
!ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
|
|
794
821
|
});
|
|
795
822
|
return {
|
|
796
823
|
staged: filteredStaged,
|
|
@@ -799,8 +826,8 @@ async function getChanges(git, options = {}) {
|
|
|
799
826
|
};
|
|
800
827
|
}
|
|
801
828
|
|
|
802
|
-
|
|
803
|
-
const { staged, unstaged, untracked } = await getChanges(git);
|
|
829
|
+
async function noResult({ git, logger }) {
|
|
830
|
+
const { staged, unstaged, untracked } = await getChanges({ git });
|
|
804
831
|
const hasStaged = staged && staged.length > 0;
|
|
805
832
|
const hasUnstaged = unstaged && unstaged.length > 0;
|
|
806
833
|
const hasUntracked = untracked && untracked.length > 0;
|
|
@@ -826,18 +853,254 @@ const noResult = async ({ git, logger }) => {
|
|
|
826
853
|
else {
|
|
827
854
|
logger.log('No repo changes detected. ð', { color: 'blue' });
|
|
828
855
|
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const isInteractive = (argv) => {
|
|
859
|
+
return argv?.mode === 'interactive' || argv.interactive;
|
|
860
|
+
};
|
|
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;
|
|
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
|
+
}
|
|
936
|
+
while (continueLoop) {
|
|
937
|
+
if (!context.length) {
|
|
938
|
+
context = await parser(changes, result, options);
|
|
939
|
+
}
|
|
940
|
+
// if we still don't have a context, bail.
|
|
941
|
+
if (!context.length) {
|
|
942
|
+
await noResult(options);
|
|
943
|
+
}
|
|
944
|
+
if (modifyPrompt) {
|
|
945
|
+
options.prompt = await editPrompt(options);
|
|
946
|
+
}
|
|
947
|
+
logger.startTimer().startSpinner(`Generating Message\n`, {
|
|
948
|
+
color: 'blue',
|
|
949
|
+
});
|
|
950
|
+
result = await agent(context, options);
|
|
951
|
+
if (!result) {
|
|
952
|
+
logger.stopSpinner('ð Agent failed to generate message.', {
|
|
953
|
+
mode: 'fail',
|
|
954
|
+
color: 'red',
|
|
955
|
+
});
|
|
956
|
+
process.exit(0);
|
|
957
|
+
}
|
|
958
|
+
logger
|
|
959
|
+
.stopSpinner('Generated Commit Message', {
|
|
960
|
+
color: 'green',
|
|
961
|
+
mode: 'succeed',
|
|
962
|
+
})
|
|
963
|
+
.stopTimer();
|
|
964
|
+
if (options?.interactive) {
|
|
965
|
+
logResult(result);
|
|
966
|
+
const reviewAnswer = await getUserReviewDecision();
|
|
967
|
+
if (reviewAnswer === 'cancel') {
|
|
968
|
+
process.exit(0);
|
|
969
|
+
}
|
|
970
|
+
if (reviewAnswer === 'edit') {
|
|
971
|
+
options.openInEditor = true;
|
|
972
|
+
}
|
|
973
|
+
if (reviewAnswer === 'retryFull') {
|
|
974
|
+
context = '';
|
|
975
|
+
result = '';
|
|
976
|
+
options.prompt = '';
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
if (reviewAnswer === 'retryMessageOnly') {
|
|
980
|
+
modifyPrompt = false;
|
|
981
|
+
result = '';
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
if (reviewAnswer === 'modifyPrompt') {
|
|
985
|
+
modifyPrompt = true;
|
|
986
|
+
result = '';
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
// if we're here, we're done.
|
|
991
|
+
result = await editResult(result, options);
|
|
992
|
+
continueLoop = false;
|
|
993
|
+
}
|
|
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();
|
|
829
1018
|
};
|
|
830
1019
|
|
|
831
1020
|
async function createCommit(commitMsg, git) {
|
|
832
1021
|
return await git.commit(commitMsg);
|
|
833
1022
|
}
|
|
834
1023
|
|
|
835
|
-
|
|
1024
|
+
const logSuccess = () => {
|
|
1025
|
+
console.log(chalk.green(chalk.bold('\nAll set! ðĶūðĪ')));
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
const handleResult = async (result, { mode, git }) => {
|
|
1029
|
+
// Handle resulting commit message
|
|
1030
|
+
switch (mode) {
|
|
1031
|
+
case 'interactive':
|
|
1032
|
+
await createCommit(result, git);
|
|
1033
|
+
logSuccess();
|
|
1034
|
+
break;
|
|
1035
|
+
case 'stdout':
|
|
1036
|
+
default:
|
|
1037
|
+
process.stdout.write(result, 'utf8');
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
process.exit(0);
|
|
1041
|
+
};
|
|
1042
|
+
|
|
836
1043
|
const tokenizer = getTokenizer();
|
|
837
1044
|
const git = simpleGit.simpleGit();
|
|
838
|
-
|
|
839
|
-
const
|
|
840
|
-
const
|
|
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);
|
|
1052
|
+
}
|
|
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;
|
|
1061
|
+
}
|
|
1062
|
+
async function parser(changes) {
|
|
1063
|
+
return await fileChangeParser({
|
|
1064
|
+
changes,
|
|
1065
|
+
commit: '--staged',
|
|
1066
|
+
options: { tokenizer, git, model, logger },
|
|
1067
|
+
});
|
|
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
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Command line options via yargs
|
|
1102
|
+
*/
|
|
1103
|
+
const options = {
|
|
841
1104
|
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
842
1105
|
openAIApiKey: {
|
|
843
1106
|
type: 'string',
|
|
@@ -883,182 +1146,20 @@ const builder = {
|
|
|
883
1146
|
description: 'Ignored extensions',
|
|
884
1147
|
},
|
|
885
1148
|
};
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
const key = getModelAPIKey(options.model, options);
|
|
890
|
-
if (!key) {
|
|
891
|
-
logger.log(`No API Key found. ðïļðŠ`, { color: 'red' });
|
|
892
|
-
process.exit(1);
|
|
893
|
-
}
|
|
894
|
-
const model = getModel(options.model, key, {
|
|
895
|
-
temperature: 0.4,
|
|
896
|
-
maxConcurrency: 10,
|
|
897
|
-
});
|
|
898
|
-
const INTERACTIVE = options?.mode === 'interactive' || options.interactive;
|
|
899
|
-
const { staged: changes } = await getChanges(git);
|
|
900
|
-
let summary = '';
|
|
901
|
-
let commitMsg = '';
|
|
902
|
-
let promptTemplate = options?.prompt || '';
|
|
903
|
-
let modifyPrompt = false;
|
|
904
|
-
while (true) {
|
|
905
|
-
if (changes.length !== 0 && !summary.length) {
|
|
906
|
-
logger.verbose(`\nChanged Files: \n ${changes.map(({ summary }) => summary).join('\n ')}`, {
|
|
907
|
-
color: 'blue',
|
|
908
|
-
});
|
|
909
|
-
summary = await fileChangeParser(changes, { tokenizer, git, model, logger });
|
|
910
|
-
}
|
|
911
|
-
// Handle empty summary
|
|
912
|
-
if (!summary.length) {
|
|
913
|
-
await noResult({ git, logger });
|
|
914
|
-
process.exit(0);
|
|
915
|
-
}
|
|
916
|
-
// Prompt user for commit template prompt, if necessary
|
|
917
|
-
if (modifyPrompt) {
|
|
918
|
-
promptTemplate = await prompts$1.editor({
|
|
919
|
-
message: 'Edit the prompt',
|
|
920
|
-
default: promptTemplate.length ? promptTemplate : COMMIT_PROMPT.template,
|
|
921
|
-
waitForUseInput: false,
|
|
922
|
-
validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
|
|
923
|
-
});
|
|
924
|
-
}
|
|
925
|
-
logger.startTimer().startSpinner(`Generating Commit Message\n`, {
|
|
926
|
-
color: 'blue',
|
|
927
|
-
});
|
|
928
|
-
commitMsg = await llm({
|
|
929
|
-
llm: model,
|
|
930
|
-
prompt: getPrompt({
|
|
931
|
-
template: promptTemplate,
|
|
932
|
-
variables: COMMIT_PROMPT.inputVariables,
|
|
933
|
-
fallback: COMMIT_PROMPT,
|
|
934
|
-
}),
|
|
935
|
-
variables: { summary },
|
|
936
|
-
});
|
|
937
|
-
if (!commitMsg) {
|
|
938
|
-
logger.stopSpinner('ð Failed to generate commit message.', {
|
|
939
|
-
mode: 'fail',
|
|
940
|
-
color: 'red',
|
|
941
|
-
});
|
|
942
|
-
process.exit(0);
|
|
943
|
-
}
|
|
944
|
-
logger
|
|
945
|
-
.stopSpinner('Generated Commit Message', {
|
|
946
|
-
color: 'green',
|
|
947
|
-
mode: 'succeed',
|
|
948
|
-
})
|
|
949
|
-
.stopTimer();
|
|
950
|
-
if (INTERACTIVE) {
|
|
951
|
-
logCommit(commitMsg);
|
|
952
|
-
const reviewAnswer = await prompts$1.select({
|
|
953
|
-
message: 'Would you like to make any changes to the commit message?',
|
|
954
|
-
choices: [
|
|
955
|
-
{
|
|
956
|
-
name: 'âĻ Looks good!',
|
|
957
|
-
value: 'approve',
|
|
958
|
-
description: 'Commit staged changes with generated commit message',
|
|
959
|
-
},
|
|
960
|
-
{
|
|
961
|
-
name: 'ð Edit',
|
|
962
|
-
value: 'edit',
|
|
963
|
-
description: 'Edit the commit message before proceeding',
|
|
964
|
-
},
|
|
965
|
-
{
|
|
966
|
-
name: 'ðŠķ Modify Prompt',
|
|
967
|
-
value: 'modifyPrompt',
|
|
968
|
-
description: 'Modify the prompt template and regenerate the commit message',
|
|
969
|
-
},
|
|
970
|
-
{
|
|
971
|
-
name: 'ð Retry - Message Only',
|
|
972
|
-
value: 'retryMessageOnly',
|
|
973
|
-
description: 'Restart the function execution from generating the commit message',
|
|
974
|
-
},
|
|
975
|
-
{
|
|
976
|
-
name: 'ð Retry - Full',
|
|
977
|
-
value: 'retryFull',
|
|
978
|
-
description: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
|
|
979
|
-
},
|
|
980
|
-
{
|
|
981
|
-
name: 'ðĢ Cancel',
|
|
982
|
-
value: 'cancel',
|
|
983
|
-
},
|
|
984
|
-
],
|
|
985
|
-
});
|
|
986
|
-
if (reviewAnswer === 'cancel') {
|
|
987
|
-
process.exit(0);
|
|
988
|
-
}
|
|
989
|
-
if (reviewAnswer === 'edit') {
|
|
990
|
-
options.openInEditor = true;
|
|
991
|
-
}
|
|
992
|
-
if (reviewAnswer === 'retryFull') {
|
|
993
|
-
summary = '';
|
|
994
|
-
commitMsg = '';
|
|
995
|
-
promptTemplate = '';
|
|
996
|
-
continue;
|
|
997
|
-
}
|
|
998
|
-
if (reviewAnswer === 'retryMessageOnly') {
|
|
999
|
-
modifyPrompt = false;
|
|
1000
|
-
commitMsg = '';
|
|
1001
|
-
continue;
|
|
1002
|
-
}
|
|
1003
|
-
if (reviewAnswer === 'modifyPrompt') {
|
|
1004
|
-
modifyPrompt = true;
|
|
1005
|
-
commitMsg = '';
|
|
1006
|
-
continue;
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
if (options.openInEditor) {
|
|
1010
|
-
commitMsg = await prompts$1.editor({
|
|
1011
|
-
message: 'Edit the commit message',
|
|
1012
|
-
default: commitMsg,
|
|
1013
|
-
waitForUseInput: false,
|
|
1014
|
-
validate: (text) => {
|
|
1015
|
-
if (!text) {
|
|
1016
|
-
return 'Commit message cannot be empty';
|
|
1017
|
-
}
|
|
1018
|
-
return true;
|
|
1019
|
-
},
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
const MODE = (options.interactive && 'interactive') ||
|
|
1023
|
-
(options.commit && 'interactive') ||
|
|
1024
|
-
options?.mode ||
|
|
1025
|
-
'stdout';
|
|
1026
|
-
// Handle resulting commit message
|
|
1027
|
-
switch (MODE) {
|
|
1028
|
-
case 'interactive':
|
|
1029
|
-
await createCommit(commitMsg, git);
|
|
1030
|
-
logSuccess();
|
|
1031
|
-
break;
|
|
1032
|
-
case 'stdout':
|
|
1033
|
-
default:
|
|
1034
|
-
process.stdout.write(commitMsg, 'utf8');
|
|
1035
|
-
break;
|
|
1036
|
-
}
|
|
1037
|
-
process.exit(0);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
var commit = /*#__PURE__*/Object.freeze({
|
|
1042
|
-
__proto__: null,
|
|
1043
|
-
builder: builder,
|
|
1044
|
-
command: command,
|
|
1045
|
-
description: description,
|
|
1046
|
-
handler: handler
|
|
1047
|
-
});
|
|
1149
|
+
const builder = (yargs) => {
|
|
1150
|
+
return yargs.options(options);
|
|
1151
|
+
};
|
|
1048
1152
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
.option('h', { alias: 'help' })
|
|
1057
|
-
.option('v', {
|
|
1058
|
-
alias: 'verbose',
|
|
1059
|
-
type: 'boolean',
|
|
1060
|
-
description: 'Run with verbose logging',
|
|
1061
|
-
}).argv;
|
|
1153
|
+
var commit = {
|
|
1154
|
+
command: 'commit',
|
|
1155
|
+
desc: 'Generate commit message',
|
|
1156
|
+
builder,
|
|
1157
|
+
handler,
|
|
1158
|
+
options,
|
|
1159
|
+
};
|
|
1062
1160
|
|
|
1063
|
-
|
|
1064
|
-
|
|
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;
|