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