git-coco 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/changelog/handler.d.ts +3 -0
- package/dist/commands/changelog/index.d.ts +10 -0
- package/dist/commands/changelog/options.d.ts +16 -0
- package/dist/commands/commit/handler.d.ts +3 -0
- package/dist/commands/commit/index.d.ts +10 -0
- package/dist/commands/commit/options.d.ts +15 -0
- package/dist/commands/types.d.ts +14 -0
- package/dist/index.d.ts +1 -3
- package/dist/index.esm.mjs +689 -470
- package/dist/index.esm.mjs.map +1 -1
- package/dist/index.js +689 -471
- package/dist/lib/langchain/executeChain.d.ts +6 -0
- package/dist/lib/langchain/prompts/changelog.d.ts +3 -0
- package/dist/lib/langchain/utils.d.ts +2 -2
- package/dist/lib/parsers/default/index.d.ts +2 -2
- package/dist/lib/parsers/default/utils/createDiffTree.d.ts +1 -0
- package/dist/lib/parsers/noResult.d.ts +4 -2
- package/dist/lib/simple-git/getChanges.d.ts +2 -2
- package/dist/lib/simple-git/getChangesByCommit.d.ts +2 -2
- package/dist/lib/simple-git/getCommitLogRange.d.ts +7 -0
- package/dist/lib/simple-git/getDiff.d.ts +2 -2
- package/dist/lib/simple-git/getStatus.d.ts +1 -1
- package/dist/lib/simple-git/getSummaryText.d.ts +1 -1
- package/dist/lib/types.d.ts +19 -8
- package/dist/lib/ui/editPrompt.d.ts +2 -0
- package/dist/lib/ui/editResult.d.ts +2 -0
- package/dist/lib/ui/generateAndReviewLoop.d.ts +16 -0
- package/dist/lib/ui/getUserReviewDecision.d.ts +2 -0
- package/dist/lib/ui/handleResult.d.ts +5 -0
- package/dist/lib/ui/helpers.d.ts +3 -0
- package/dist/lib/ui/logResult.d.ts +1 -0
- package/dist/lib/ui/logSuccess.d.ts +1 -0
- package/dist/stats.html +1 -1
- package/package.json +3 -3
- package/dist/commands/commit.d.ts +0 -16
- package/dist/lib/langchain/chains/llm.d.ts +0 -6
- package/dist/lib/ui.d.ts +0 -24
- package/dist/types.d.ts +0 -10
package/dist/index.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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
333
|
+
const template$2 = `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$2 = ['text'];
|
|
340
|
+
const SUMMARIZE_PROMPT = new PromptTemplate({
|
|
341
|
+
template: template$2,
|
|
342
|
+
inputVariables: inputVariables$2,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
async function parseDefaultFileDiff(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]);
|
|
350
|
+
}
|
|
351
|
+
async function parseRenamedFileDiff(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)
|
|
@@ -545,23 +385,23 @@ const parseRenamedFileDiff = async (nodeFile, git, logger) => {
|
|
|
545
385
|
result = 'Error comparing file contents.';
|
|
546
386
|
}
|
|
547
387
|
return result;
|
|
548
|
-
}
|
|
549
|
-
|
|
388
|
+
}
|
|
389
|
+
async function getDiff(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,36 +421,105 @@ 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
|
-
|
|
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' });
|
|
598
445
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
446
|
+
else {
|
|
447
|
+
tokenizer = new GPT3NodeTokenizer({ type: 'gpt3' });
|
|
448
|
+
}
|
|
449
|
+
return tokenizer;
|
|
450
|
+
};
|
|
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);
|
|
602
461
|
}
|
|
462
|
+
console.log(outputMessage);
|
|
463
|
+
return this;
|
|
603
464
|
}
|
|
604
|
-
|
|
605
|
-
|
|
465
|
+
verbose(message, options = {}) {
|
|
466
|
+
if (!this.config?.verbose) {
|
|
467
|
+
return this;
|
|
468
|
+
}
|
|
469
|
+
this.log(message, options);
|
|
470
|
+
return this;
|
|
606
471
|
}
|
|
607
|
-
|
|
608
|
-
|
|
472
|
+
startTimer() {
|
|
473
|
+
this.timerStart = now();
|
|
474
|
+
return this;
|
|
609
475
|
}
|
|
610
|
-
|
|
611
|
-
|
|
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$1 = `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}"""
|
|
612
514
|
|
|
613
|
-
|
|
515
|
+
Commit:`;
|
|
516
|
+
const inputVariables$1 = ['summary'];
|
|
517
|
+
const COMMIT_PROMPT = new PromptTemplate({
|
|
518
|
+
template: template$1,
|
|
519
|
+
inputVariables: inputVariables$1,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
function getStatus(file, location = 'index') {
|
|
614
523
|
if ('index' in file && 'working_dir' in file) {
|
|
615
524
|
const statusCode = file[location];
|
|
616
525
|
switch (statusCode) {
|
|
@@ -642,11 +551,11 @@ const getStatus = (file, location = 'index') => {
|
|
|
642
551
|
return 'unknown';
|
|
643
552
|
}
|
|
644
553
|
else {
|
|
645
|
-
throw new Error(
|
|
554
|
+
throw new Error('Invalid file type');
|
|
646
555
|
}
|
|
647
|
-
}
|
|
556
|
+
}
|
|
648
557
|
|
|
649
|
-
|
|
558
|
+
function getSummaryText(file, change) {
|
|
650
559
|
const status = change.status || getStatus(file);
|
|
651
560
|
let filePath;
|
|
652
561
|
if ('path' in file) {
|
|
@@ -656,13 +565,186 @@ const getSummaryText = (file, change) => {
|
|
|
656
565
|
filePath = change?.filePath || file.file;
|
|
657
566
|
}
|
|
658
567
|
else {
|
|
659
|
-
throw new Error(
|
|
568
|
+
throw new Error('Invalid file type');
|
|
660
569
|
}
|
|
661
570
|
if (change.oldFilePath) {
|
|
662
571
|
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
663
572
|
}
|
|
664
573
|
return `${status}: ${filePath}`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Returns a new object with all undefined keys removed
|
|
578
|
+
*
|
|
579
|
+
* @param obj Object to remove undefined keys from
|
|
580
|
+
* @returns
|
|
581
|
+
*/
|
|
582
|
+
function removeUndefined(obj) {
|
|
583
|
+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Load environment variables
|
|
588
|
+
*
|
|
589
|
+
* @param {Config} config
|
|
590
|
+
* @returns {Config} Updated config
|
|
591
|
+
**/
|
|
592
|
+
function loadEnvConfig(config) {
|
|
593
|
+
const envConfig = {
|
|
594
|
+
model: process.env.COCO_MODEL || undefined,
|
|
595
|
+
openAIApiKey: process.env.OPENAI_API_KEY || undefined,
|
|
596
|
+
huggingFaceHubApiKey: process.env.HUGGINGFACE_HUB_API_KEY || undefined,
|
|
597
|
+
tokenLimit: process.env.COCO_TOKEN_LIMIT
|
|
598
|
+
? parseInt(process.env.COCO_TOKEN_LIMIT)
|
|
599
|
+
: undefined,
|
|
600
|
+
prompt: process.env.COCO_PROMPT,
|
|
601
|
+
mode: process.env.COCO_MODE,
|
|
602
|
+
summarizePrompt: process.env.COCO_SUMMARIZE_PROMPT,
|
|
603
|
+
ignoredFiles: process.env.COCO_IGNORED_FILES
|
|
604
|
+
? process.env.COCO_IGNORED_FILES.split(',')
|
|
605
|
+
: undefined,
|
|
606
|
+
ignoredExtensions: process.env.COCO_IGNORED_EXTENSIONS
|
|
607
|
+
? process.env.COCO_IGNORED_EXTENSIONS.split(',')
|
|
608
|
+
: undefined,
|
|
609
|
+
};
|
|
610
|
+
config = { ...config, ...removeUndefined(envConfig) };
|
|
611
|
+
return config;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Load git profile config (from ~/.gitconfig)
|
|
616
|
+
*
|
|
617
|
+
* @param {Config} config
|
|
618
|
+
* @returns {Config} Updated config
|
|
619
|
+
**/
|
|
620
|
+
function loadGitConfig(config) {
|
|
621
|
+
const gitConfigPath = path.join(os.homedir(), '.gitconfig');
|
|
622
|
+
if (fs.existsSync(gitConfigPath)) {
|
|
623
|
+
const gitConfigRaw = fs.readFileSync(gitConfigPath, 'utf-8');
|
|
624
|
+
const gitConfigParsed = ini.parse(gitConfigRaw);
|
|
625
|
+
config = {
|
|
626
|
+
...config,
|
|
627
|
+
model: gitConfigParsed.coco?.model || config.model,
|
|
628
|
+
openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
|
|
629
|
+
huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
|
|
630
|
+
tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
|
|
631
|
+
prompt: gitConfigParsed.coco?.prompt || config.prompt,
|
|
632
|
+
mode: gitConfigParsed.coco?.mode || config.mode,
|
|
633
|
+
temperature: gitConfigParsed.coco?.temperature || config.temperature,
|
|
634
|
+
summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
|
|
635
|
+
ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
|
|
636
|
+
ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
return config;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Load .gitignore in project root
|
|
644
|
+
*
|
|
645
|
+
* @param {Config} config
|
|
646
|
+
* @returns
|
|
647
|
+
*/
|
|
648
|
+
function loadGitignore(config) {
|
|
649
|
+
if (fs.existsSync('.gitignore')) {
|
|
650
|
+
const gitignoreContent = fs.readFileSync('.gitignore', 'utf-8');
|
|
651
|
+
config.ignoredFiles = [
|
|
652
|
+
...(config?.ignoredFiles || []),
|
|
653
|
+
...gitignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
654
|
+
];
|
|
655
|
+
}
|
|
656
|
+
return config;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Load .ignore in project root
|
|
660
|
+
*
|
|
661
|
+
* @param {Config} config
|
|
662
|
+
* @returns
|
|
663
|
+
*/
|
|
664
|
+
function loadIgnore(config) {
|
|
665
|
+
if (fs.existsSync('.ignore')) {
|
|
666
|
+
const ignoreContent = fs.readFileSync('.ignore', 'utf-8');
|
|
667
|
+
config.ignoredFiles = [
|
|
668
|
+
...(config?.ignoredFiles || []),
|
|
669
|
+
...ignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
670
|
+
];
|
|
671
|
+
}
|
|
672
|
+
return config;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Load project config
|
|
677
|
+
*
|
|
678
|
+
* @param {Config} config
|
|
679
|
+
* @returns {Config} Updated config
|
|
680
|
+
**/
|
|
681
|
+
function loadProjectConfig(config) {
|
|
682
|
+
if (fs.existsSync('.coco.config.json')) {
|
|
683
|
+
const projectConfig = JSON.parse(fs.readFileSync('.coco.config.json', 'utf-8'));
|
|
684
|
+
config = { ...config, ...projectConfig };
|
|
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
|
+
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
715
|
+
temperature: 0.4,
|
|
716
|
+
mode: 'stdout',
|
|
717
|
+
ignoredFiles: ['package-lock.json'],
|
|
718
|
+
ignoredExtensions: ['.map', '.lock'],
|
|
665
719
|
};
|
|
720
|
+
/**
|
|
721
|
+
* Load application config
|
|
722
|
+
*
|
|
723
|
+
* Merge config from multiple sources.
|
|
724
|
+
*
|
|
725
|
+
* \* Order of precedence:
|
|
726
|
+
* \* 1. Command line flags
|
|
727
|
+
* \* 2. Environment variables
|
|
728
|
+
* \* 3. Project config
|
|
729
|
+
* \* 4. Git config
|
|
730
|
+
* \* 5. XDG config
|
|
731
|
+
* \* 6. .gitignore
|
|
732
|
+
* \* 7. .ignore
|
|
733
|
+
* \* 8. Default config
|
|
734
|
+
*
|
|
735
|
+
* @returns {Config} application config
|
|
736
|
+
**/
|
|
737
|
+
function loadConfig(argv = {}) {
|
|
738
|
+
// Default config
|
|
739
|
+
let config = DEFAULT_CONFIG;
|
|
740
|
+
config = loadGitignore(config);
|
|
741
|
+
config = loadIgnore(config);
|
|
742
|
+
config = loadXDGConfig(config);
|
|
743
|
+
config = loadGitConfig(config);
|
|
744
|
+
config = loadProjectConfig(config);
|
|
745
|
+
config = loadEnvConfig(config);
|
|
746
|
+
return { ...config, ...argv };
|
|
747
|
+
}
|
|
666
748
|
|
|
667
749
|
const config = loadConfig();
|
|
668
750
|
const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
|
|
@@ -720,7 +802,7 @@ async function getChanges({ git, options }) {
|
|
|
720
802
|
};
|
|
721
803
|
}
|
|
722
804
|
|
|
723
|
-
|
|
805
|
+
async function noResult({ git, logger }) {
|
|
724
806
|
const { staged, unstaged, untracked } = await getChanges({ git });
|
|
725
807
|
const hasStaged = staged && staged.length > 0;
|
|
726
808
|
const hasUnstaged = unstaged && unstaged.length > 0;
|
|
@@ -747,112 +829,117 @@ const noResult = async ({ git, logger }) => {
|
|
|
747
829
|
else {
|
|
748
830
|
logger.log('No repo changes detected. 👀', { color: 'blue' });
|
|
749
831
|
}
|
|
750
|
-
};
|
|
751
|
-
|
|
752
|
-
async function createCommit(commitMsg, git) {
|
|
753
|
-
return await git.commit(commitMsg);
|
|
754
832
|
}
|
|
755
833
|
|
|
756
|
-
const SEPERATOR = chalk.blue('----------------');
|
|
757
834
|
const isInteractive = (argv) => {
|
|
758
835
|
return argv?.mode === 'interactive' || argv.interactive;
|
|
759
836
|
};
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
837
|
+
const SEPERATOR = chalk.blue('----------------');
|
|
838
|
+
|
|
839
|
+
function logResult(result) {
|
|
840
|
+
console.log(`\n${chalk.bgBlue(chalk.bold('Proposed Commit:'))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function editResult(result, options) {
|
|
844
|
+
if (options.openInEditor) {
|
|
845
|
+
return await editor({
|
|
846
|
+
message: 'Edit the commit message',
|
|
847
|
+
default: result,
|
|
848
|
+
waitForUseInput: false,
|
|
849
|
+
validate: (text) => (text ? true : 'Commit message cannot be empty'),
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
return result;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
async function getUserReviewDecision() {
|
|
856
|
+
return await select({
|
|
857
|
+
message: 'Would you like to make any changes to the commit message?',
|
|
858
|
+
choices: [
|
|
859
|
+
{
|
|
860
|
+
name: '✨ Looks good!',
|
|
861
|
+
value: 'approve',
|
|
862
|
+
description: 'Commit staged changes with generated commit message',
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
name: '📝 Edit',
|
|
866
|
+
value: 'edit',
|
|
867
|
+
description: 'Edit the commit message before proceeding',
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
name: '🪶 Modify Prompt',
|
|
871
|
+
value: 'modifyPrompt',
|
|
872
|
+
description: 'Modify the prompt template and regenerate the commit message',
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
name: '🔄 Retry - Message Only',
|
|
876
|
+
value: 'retryMessageOnly',
|
|
877
|
+
description: 'Restart the function execution from generating the commit message',
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
name: '🔄 Retry - Full',
|
|
881
|
+
value: 'retryFull',
|
|
882
|
+
description: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
name: '💣 Cancel',
|
|
886
|
+
value: 'cancel',
|
|
887
|
+
},
|
|
888
|
+
],
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
async function editPrompt(options) {
|
|
893
|
+
return await editor({
|
|
894
|
+
message: 'Edit the prompt',
|
|
895
|
+
default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
|
|
896
|
+
waitForUseInput: false,
|
|
897
|
+
validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async function generateAndReviewLoop({ label, factory, parser, noResult, agent, options, }) {
|
|
902
|
+
const { logger } = options;
|
|
773
903
|
let continueLoop = true;
|
|
904
|
+
let modifyPrompt = false;
|
|
905
|
+
let context = '';
|
|
906
|
+
let result = '';
|
|
907
|
+
const changes = await factory();
|
|
908
|
+
// if we don't have any changes, bail.
|
|
909
|
+
if (!changes || !changes.length) {
|
|
910
|
+
await noResult(options);
|
|
911
|
+
}
|
|
774
912
|
while (continueLoop) {
|
|
775
|
-
if (
|
|
776
|
-
|
|
777
|
-
color: 'blue',
|
|
778
|
-
});
|
|
779
|
-
summary = await fileChangeParser(changes, { tokenizer, git, model, logger });
|
|
913
|
+
if (!context.length) {
|
|
914
|
+
context = await parser(changes, result, options);
|
|
780
915
|
}
|
|
781
|
-
//
|
|
782
|
-
if (!
|
|
783
|
-
await noResult(
|
|
784
|
-
process.exit(0);
|
|
916
|
+
// if we still don't have a context, bail.
|
|
917
|
+
if (!context.length) {
|
|
918
|
+
await noResult(options);
|
|
785
919
|
}
|
|
786
|
-
// Prompt user for commit template prompt, if necessary
|
|
787
920
|
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
|
-
});
|
|
921
|
+
options.prompt = await editPrompt(options);
|
|
794
922
|
}
|
|
795
|
-
logger.startTimer().startSpinner(`Generating
|
|
923
|
+
logger.startTimer().startSpinner(`Generating ${label}\n`, {
|
|
796
924
|
color: 'blue',
|
|
797
925
|
});
|
|
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.', {
|
|
926
|
+
result = await agent(context, options);
|
|
927
|
+
if (!result) {
|
|
928
|
+
logger.stopSpinner('💀 Agent failed to return content.', {
|
|
809
929
|
mode: 'fail',
|
|
810
930
|
color: 'red',
|
|
811
931
|
});
|
|
812
932
|
process.exit(0);
|
|
813
933
|
}
|
|
814
934
|
logger
|
|
815
|
-
.stopSpinner(
|
|
935
|
+
.stopSpinner(`Generated ${label}`, {
|
|
816
936
|
color: 'green',
|
|
817
937
|
mode: 'succeed',
|
|
818
938
|
})
|
|
819
939
|
.stopTimer();
|
|
820
940
|
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
|
-
});
|
|
941
|
+
logResult(result);
|
|
942
|
+
const reviewAnswer = await getUserReviewDecision();
|
|
856
943
|
if (reviewAnswer === 'cancel') {
|
|
857
944
|
process.exit(0);
|
|
858
945
|
}
|
|
@@ -860,133 +947,138 @@ const generateCommitMessageAndReviewLoop = async (changes, options) => {
|
|
|
860
947
|
options.openInEditor = true;
|
|
861
948
|
}
|
|
862
949
|
if (reviewAnswer === 'retryFull') {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
950
|
+
context = '';
|
|
951
|
+
result = '';
|
|
952
|
+
options.prompt = '';
|
|
866
953
|
continue;
|
|
867
954
|
}
|
|
868
955
|
if (reviewAnswer === 'retryMessageOnly') {
|
|
869
956
|
modifyPrompt = false;
|
|
870
|
-
|
|
957
|
+
result = '';
|
|
871
958
|
continue;
|
|
872
959
|
}
|
|
873
960
|
if (reviewAnswer === 'modifyPrompt') {
|
|
874
961
|
modifyPrompt = true;
|
|
875
|
-
|
|
962
|
+
result = '';
|
|
876
963
|
continue;
|
|
877
964
|
}
|
|
878
965
|
}
|
|
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
|
-
}
|
|
966
|
+
// if we're here, we're done.
|
|
967
|
+
result = await editResult(result, options);
|
|
892
968
|
continueLoop = false;
|
|
893
969
|
}
|
|
894
|
-
return
|
|
970
|
+
return result;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const executeChain = async ({ llm, prompt, variables }) => {
|
|
974
|
+
if (!llm || !prompt || !variables) {
|
|
975
|
+
throw new Error('The input parameters "llm", "prompt", and "variables" are all required.');
|
|
976
|
+
}
|
|
977
|
+
const chain = new LLMChain({ llm, prompt });
|
|
978
|
+
let res;
|
|
979
|
+
try {
|
|
980
|
+
res = await chain.call(variables);
|
|
981
|
+
}
|
|
982
|
+
catch (error) {
|
|
983
|
+
if (error instanceof Error) {
|
|
984
|
+
throw new Error(`LLMChain call error: ${error.message}`);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (!res) {
|
|
988
|
+
throw new Error('Empty response from LLMChain call');
|
|
989
|
+
}
|
|
990
|
+
if (res.error) {
|
|
991
|
+
throw new Error(`LLMChain response error: ${res.error}`);
|
|
992
|
+
}
|
|
993
|
+
return res.text.trim();
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
async function createCommit(commitMsg, git) {
|
|
997
|
+
return await git.commit(commitMsg);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const logSuccess = () => {
|
|
1001
|
+
console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
|
|
895
1002
|
};
|
|
896
|
-
|
|
1003
|
+
|
|
1004
|
+
const handleResult = async (result, { mode, git }) => {
|
|
897
1005
|
// Handle resulting commit message
|
|
898
1006
|
switch (mode) {
|
|
899
1007
|
case 'interactive':
|
|
900
|
-
await createCommit(
|
|
1008
|
+
await createCommit(result, git);
|
|
901
1009
|
logSuccess();
|
|
902
1010
|
break;
|
|
903
1011
|
case 'stdout':
|
|
904
1012
|
default:
|
|
905
|
-
process.stdout.write(
|
|
1013
|
+
process.stdout.write(result, 'utf8');
|
|
906
1014
|
break;
|
|
907
1015
|
}
|
|
908
1016
|
process.exit(0);
|
|
909
1017
|
};
|
|
910
1018
|
|
|
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;
|
|
1019
|
+
const tokenizer = getTokenizer();
|
|
1020
|
+
const git$1 = simpleGit();
|
|
1021
|
+
async function handler$1(argv) {
|
|
1022
|
+
const options = loadConfig(argv);
|
|
1023
|
+
const logger = new Logger(options);
|
|
1024
|
+
const key = getApiKeyForModel(options.model, options);
|
|
1025
|
+
if (!key) {
|
|
1026
|
+
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
1027
|
+
process.exit(1);
|
|
970
1028
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1029
|
+
const model = getModel(options.model, key, {
|
|
1030
|
+
temperature: 0.4,
|
|
1031
|
+
maxConcurrency: 10,
|
|
1032
|
+
});
|
|
1033
|
+
const INTERACTIVE = isInteractive(options);
|
|
1034
|
+
async function factory() {
|
|
1035
|
+
const changes = await getChanges({ git: git$1 });
|
|
1036
|
+
return changes.staged;
|
|
975
1037
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1038
|
+
async function parser(changes) {
|
|
1039
|
+
return await fileChangeParser({
|
|
1040
|
+
changes,
|
|
1041
|
+
commit: '--staged',
|
|
1042
|
+
options: { tokenizer, git: git$1, model, logger },
|
|
1043
|
+
});
|
|
981
1044
|
}
|
|
1045
|
+
const commitMsg = await generateAndReviewLoop({
|
|
1046
|
+
label: 'Commit Message',
|
|
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: git$1, logger });
|
|
1062
|
+
process.exit(0);
|
|
1063
|
+
},
|
|
1064
|
+
options: {
|
|
1065
|
+
...options,
|
|
1066
|
+
prompt: options.prompt || COMMIT_PROMPT.template,
|
|
1067
|
+
logger,
|
|
1068
|
+
interactive: INTERACTIVE,
|
|
1069
|
+
},
|
|
1070
|
+
});
|
|
1071
|
+
const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
|
|
1072
|
+
handleResult(commitMsg, {
|
|
1073
|
+
mode: MODE,
|
|
1074
|
+
git: git$1,
|
|
1075
|
+
});
|
|
982
1076
|
}
|
|
983
1077
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const
|
|
988
|
-
const description = 'Generate a commit message based on the diff summary';
|
|
989
|
-
const builder = {
|
|
1078
|
+
/**
|
|
1079
|
+
* Command line options via yargs
|
|
1080
|
+
*/
|
|
1081
|
+
const options$1 = {
|
|
990
1082
|
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
991
1083
|
openAIApiKey: {
|
|
992
1084
|
type: 'string',
|
|
@@ -1032,10 +1124,56 @@ const builder = {
|
|
|
1032
1124
|
description: 'Ignored extensions',
|
|
1033
1125
|
},
|
|
1034
1126
|
};
|
|
1127
|
+
const builder$1 = (yargs) => {
|
|
1128
|
+
return yargs.options(options$1);
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
var commit = {
|
|
1132
|
+
command: 'commit',
|
|
1133
|
+
desc: 'Generate commit message',
|
|
1134
|
+
builder: builder$1,
|
|
1135
|
+
handler: handler$1,
|
|
1136
|
+
options: options$1,
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
const template = `Write informative git changelog, in the imperative, based on a series of individual messages.
|
|
1140
|
+
|
|
1141
|
+
- Typically a hyphen or asterisk is used for the bullet
|
|
1142
|
+
- Summarize dependency updates
|
|
1143
|
+
|
|
1144
|
+
"""{summary}"""
|
|
1145
|
+
|
|
1146
|
+
Changelog:`;
|
|
1147
|
+
const inputVariables = ['summary'];
|
|
1148
|
+
const CHANGELOG_PROMPT = new PromptTemplate({
|
|
1149
|
+
template,
|
|
1150
|
+
inputVariables,
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
async function getCommitLogRange(from, to, { noMerges, git }) {
|
|
1154
|
+
try {
|
|
1155
|
+
const output = await git.raw([
|
|
1156
|
+
'log',
|
|
1157
|
+
`${from}..${to}`,
|
|
1158
|
+
'--pretty=format:%s',
|
|
1159
|
+
// Include '--no-merges' here if you want to exclude merge commits.
|
|
1160
|
+
noMerges ? '--no-merges' : null,
|
|
1161
|
+
].filter(Boolean)); // filter(Boolean) removes any null values from the array
|
|
1162
|
+
const messages = output.split('\n').filter(Boolean);
|
|
1163
|
+
return messages;
|
|
1164
|
+
}
|
|
1165
|
+
catch (error) {
|
|
1166
|
+
// If there's an error, handle it appropriately
|
|
1167
|
+
console.error('Error getting commit messages:', error);
|
|
1168
|
+
throw error;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const git = simpleGit();
|
|
1035
1173
|
async function handler(argv) {
|
|
1036
1174
|
const options = loadConfig(argv);
|
|
1037
1175
|
const logger = new Logger(options);
|
|
1038
|
-
const key =
|
|
1176
|
+
const key = getApiKeyForModel(options.model, options);
|
|
1039
1177
|
if (!key) {
|
|
1040
1178
|
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
1041
1179
|
process.exit(1);
|
|
@@ -1045,47 +1183,128 @@ async function handler(argv) {
|
|
|
1045
1183
|
maxConcurrency: 10,
|
|
1046
1184
|
});
|
|
1047
1185
|
const INTERACTIVE = isInteractive(options);
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
logger
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1186
|
+
const [from, to] = options.range?.split(':');
|
|
1187
|
+
if (!from || !to) {
|
|
1188
|
+
logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
|
|
1189
|
+
process.exit(1);
|
|
1190
|
+
}
|
|
1191
|
+
async function factory() {
|
|
1192
|
+
const messages = await getCommitLogRange(from, to, { git, noMerges: true });
|
|
1193
|
+
return messages;
|
|
1194
|
+
}
|
|
1195
|
+
async function parser(messages) {
|
|
1196
|
+
const result = messages.join('\n');
|
|
1197
|
+
return result;
|
|
1198
|
+
}
|
|
1199
|
+
const changelogMsg = await generateAndReviewLoop({
|
|
1200
|
+
label: 'Changelog',
|
|
1201
|
+
factory,
|
|
1202
|
+
parser,
|
|
1203
|
+
agent: async (context, options) => {
|
|
1204
|
+
const prompt = getPrompt({
|
|
1205
|
+
template: options.prompt,
|
|
1206
|
+
variables: CHANGELOG_PROMPT.inputVariables,
|
|
1207
|
+
fallback: CHANGELOG_PROMPT,
|
|
1208
|
+
});
|
|
1209
|
+
return await executeChain({
|
|
1210
|
+
llm: model,
|
|
1211
|
+
prompt,
|
|
1212
|
+
variables: { summary: context },
|
|
1213
|
+
});
|
|
1214
|
+
},
|
|
1215
|
+
noResult: async () => {
|
|
1216
|
+
await noResult({ git, logger });
|
|
1217
|
+
process.exit(0);
|
|
1218
|
+
},
|
|
1219
|
+
options: {
|
|
1220
|
+
...options,
|
|
1221
|
+
prompt: options.prompt || CHANGELOG_PROMPT.template,
|
|
1222
|
+
logger,
|
|
1223
|
+
interactive: INTERACTIVE,
|
|
1224
|
+
},
|
|
1057
1225
|
});
|
|
1058
|
-
const MODE = (options.
|
|
1059
|
-
|
|
1060
|
-
options?.mode ||
|
|
1061
|
-
'stdout';
|
|
1062
|
-
handleResult(commitMsg, {
|
|
1226
|
+
const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
|
|
1227
|
+
handleResult(changelogMsg, {
|
|
1063
1228
|
mode: MODE,
|
|
1064
1229
|
git,
|
|
1065
1230
|
});
|
|
1066
1231
|
}
|
|
1067
1232
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1233
|
+
/**
|
|
1234
|
+
* Command line options via yargs
|
|
1235
|
+
*/
|
|
1236
|
+
const options = {
|
|
1237
|
+
range: {
|
|
1238
|
+
type: 'string',
|
|
1239
|
+
alias: 'r',
|
|
1240
|
+
description: 'Commit range e.g `HEAD~2:HEAD`',
|
|
1241
|
+
demandOption: true,
|
|
1242
|
+
},
|
|
1243
|
+
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
1244
|
+
openAIApiKey: {
|
|
1245
|
+
type: 'string',
|
|
1246
|
+
description: 'OpenAI API Key',
|
|
1247
|
+
conflicts: 'huggingFaceHubApiKey',
|
|
1248
|
+
},
|
|
1249
|
+
huggingFaceHubApiKey: {
|
|
1250
|
+
type: 'string',
|
|
1251
|
+
description: 'HuggingFace Hub API Key',
|
|
1252
|
+
conflicts: 'openAIApiKey',
|
|
1253
|
+
},
|
|
1254
|
+
tokenLimit: { type: 'number', description: 'Token limit' },
|
|
1255
|
+
prompt: {
|
|
1256
|
+
type: 'string',
|
|
1257
|
+
alias: 'p',
|
|
1258
|
+
description: 'Prompt for llm',
|
|
1259
|
+
},
|
|
1260
|
+
i: {
|
|
1261
|
+
type: 'boolean',
|
|
1262
|
+
alias: 'interactive',
|
|
1263
|
+
description: 'Toggle interactive mode',
|
|
1264
|
+
},
|
|
1265
|
+
e: {
|
|
1266
|
+
type: 'boolean',
|
|
1267
|
+
alias: 'edit',
|
|
1268
|
+
description: 'Open generated changelog message in editor before proceeding',
|
|
1269
|
+
},
|
|
1270
|
+
summarizePrompt: {
|
|
1271
|
+
type: 'string',
|
|
1272
|
+
description: 'Prompt for summarizing large files',
|
|
1273
|
+
},
|
|
1274
|
+
ignoredFiles: {
|
|
1275
|
+
type: 'array',
|
|
1276
|
+
description: 'Ignored files',
|
|
1277
|
+
},
|
|
1278
|
+
ignoredExtensions: {
|
|
1279
|
+
type: 'array',
|
|
1280
|
+
description: 'Ignored extensions',
|
|
1281
|
+
},
|
|
1282
|
+
};
|
|
1283
|
+
const builder = (yargs) => {
|
|
1284
|
+
return yargs.options(options);
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
var changelog = {
|
|
1288
|
+
command: 'changelog',
|
|
1289
|
+
desc: 'Generate a changelog from a commit range',
|
|
1290
|
+
builder,
|
|
1291
|
+
handler,
|
|
1292
|
+
options,
|
|
1293
|
+
};
|
|
1075
1294
|
|
|
1076
1295
|
yargs
|
|
1077
1296
|
.scriptName('coco')
|
|
1078
|
-
.
|
|
1079
|
-
|
|
1080
|
-
|
|
1297
|
+
.usage('$0 <cmd> [args]')
|
|
1298
|
+
.command([commit.command, '$0'], commit.desc,
|
|
1299
|
+
// TODO: fix type on builder
|
|
1300
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1301
|
+
// @ts-ignore
|
|
1302
|
+
commit.builder, commit.handler)
|
|
1303
|
+
.command(changelog.command, changelog.desc,
|
|
1304
|
+
// TODO: fix type on builder
|
|
1305
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1306
|
+
// @ts-ignore
|
|
1307
|
+
changelog.builder, changelog.handler)
|
|
1081
1308
|
.demandCommand()
|
|
1082
|
-
.
|
|
1083
|
-
.option('h', { alias: 'help' })
|
|
1084
|
-
.option('v', {
|
|
1085
|
-
alias: 'verbose',
|
|
1086
|
-
type: 'boolean',
|
|
1087
|
-
description: 'Run with verbose logging',
|
|
1088
|
-
}).argv;
|
|
1089
|
-
|
|
1090
|
-
export { commit, loadConfig };
|
|
1309
|
+
.help().argv;
|
|
1091
1310
|
//# sourceMappingURL=index.esm.mjs.map
|