git-coco 0.11.0 → 0.12.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/index.d.ts +55 -75
- package/dist/index.esm.mjs +1413 -1176
- package/dist/index.js +1292 -1054
- package/package.json +9 -9
package/dist/index.esm.mjs
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { PromptTemplate, BasePromptTemplate, ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate } from '@langchain/core/prompts';
|
|
3
3
|
import { ConditionalPromptSelector, isChatModel } from '@langchain/core/example_selectors';
|
|
4
4
|
import yargs from 'yargs';
|
|
5
|
+
import chalk from 'chalk';
|
|
5
6
|
import * as fs from 'fs';
|
|
6
7
|
import fs__default from 'fs';
|
|
7
8
|
import { confirm, editor, select, password, input } from '@inquirer/prompts';
|
|
8
|
-
import chalk from 'chalk';
|
|
9
9
|
import * as ini from 'ini';
|
|
10
10
|
import * as os from 'os';
|
|
11
11
|
import os__default from 'os';
|
|
@@ -15,28 +15,49 @@ import Ajv from 'ajv';
|
|
|
15
15
|
import ora from 'ora';
|
|
16
16
|
import now from 'performance-now';
|
|
17
17
|
import prettyMilliseconds from 'pretty-ms';
|
|
18
|
+
import { ChatOllama } from '@langchain/ollama';
|
|
19
|
+
import { ChatOpenAI } from '@langchain/openai';
|
|
20
|
+
import { JsonOutputParser, BaseOutputParser } from '@langchain/core/output_parsers';
|
|
21
|
+
import { simpleGit } from 'simple-git';
|
|
18
22
|
import pQueue from 'p-queue';
|
|
19
23
|
import { Document, BaseDocumentTransformer } from '@langchain/core/documents';
|
|
20
24
|
import { RUN_KEY } from '@langchain/core/outputs';
|
|
21
25
|
import { CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager';
|
|
22
26
|
import { ensureConfig, Runnable } from '@langchain/core/runnables';
|
|
23
27
|
import { BaseLangChain, BaseLanguageModel } from '@langchain/core/language_models/base';
|
|
24
|
-
import { BaseOutputParser, JsonOutputParser } from '@langchain/core/output_parsers';
|
|
25
28
|
import '@langchain/core/messages';
|
|
26
29
|
import '@langchain/core/memory';
|
|
27
30
|
import '@langchain/core/chat_history';
|
|
28
31
|
import '@langchain/core/utils/tiktoken';
|
|
29
|
-
import { ChatOpenAI } from '@langchain/openai';
|
|
30
32
|
import '@langchain/core/utils/async_caller';
|
|
31
33
|
import '@langchain/core/utils/env';
|
|
32
34
|
import '@langchain/core/utils/json_patch';
|
|
33
35
|
import { createTwoFilesPatch } from 'diff';
|
|
34
|
-
import { ChatOllama } from '@langchain/ollama';
|
|
35
36
|
import { minimatch } from 'minimatch';
|
|
36
|
-
import { simpleGit } from 'simple-git';
|
|
37
37
|
import { encoding_for_model } from 'tiktoken';
|
|
38
38
|
import { exec } from 'child_process';
|
|
39
39
|
|
|
40
|
+
const isInteractive = (config) => {
|
|
41
|
+
return config?.mode === 'interactive' || !!config?.interactive;
|
|
42
|
+
};
|
|
43
|
+
const SEPERATOR = chalk.blue('─────────────');
|
|
44
|
+
const LOGO = chalk.green(`┌──────┐
|
|
45
|
+
│┏┏┓┏┏┓│
|
|
46
|
+
│┗┗┛┗┗┛│
|
|
47
|
+
└──────┘`);
|
|
48
|
+
chalk.green(`┌────┐
|
|
49
|
+
│coco│
|
|
50
|
+
└────┘`);
|
|
51
|
+
const USAGE_BANNER = chalk.green(`${LOGO}
|
|
52
|
+
${chalk.bgGreen(`\xa0v${process.env.npm_package_version}\xa0`)}
|
|
53
|
+
`);
|
|
54
|
+
const getCommandUsageHeader = (command) => {
|
|
55
|
+
return chalk.green(`${USAGE_BANNER}\n${chalk.white('Command:')}\n\xa0\xa0\xa0\xa0\xa0 $0 ${chalk.greenBright(command)} [options]`);
|
|
56
|
+
};
|
|
57
|
+
const CONFIG_ALREADY_EXISTS = (path) => {
|
|
58
|
+
return `coco config found in '${path}', do you want to override it?`;
|
|
59
|
+
};
|
|
60
|
+
|
|
40
61
|
/**
|
|
41
62
|
* Returns a new object with all undefined keys removed
|
|
42
63
|
*
|
|
@@ -47,16 +68,64 @@ function removeUndefined(obj) {
|
|
|
47
68
|
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
48
69
|
}
|
|
49
70
|
|
|
50
|
-
|
|
71
|
+
async function updateFileSection({ filePath, startComment, endComment, getNewContent, confirmUpdate = true, confirmMessage = (path) => `A section already exists in ${path}, do you want to override it?`, }) {
|
|
72
|
+
const lines = fs__default.existsSync(filePath) ? fs__default.readFileSync(filePath, 'utf-8').split(/\r?\n/) : [];
|
|
73
|
+
const newLines = [];
|
|
74
|
+
let foundSection = false;
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
if (lines[i].trim() === startComment) {
|
|
77
|
+
foundSection = true;
|
|
78
|
+
if (confirmUpdate) {
|
|
79
|
+
const confirmOverwrite = await confirm({
|
|
80
|
+
message: typeof confirmMessage === 'function' ? confirmMessage(filePath) : confirmMessage,
|
|
81
|
+
default: false,
|
|
82
|
+
});
|
|
83
|
+
if (!confirmOverwrite) {
|
|
84
|
+
// keep all lines until the end comment
|
|
85
|
+
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
86
|
+
newLines.push(lines[i]);
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
newLines.push(endComment);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
newLines.push(startComment);
|
|
94
|
+
// Insert the new content
|
|
95
|
+
const newContent = await getNewContent();
|
|
96
|
+
newLines.push(newContent);
|
|
97
|
+
// Skip the existing content of the section
|
|
98
|
+
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
99
|
+
i++;
|
|
100
|
+
}
|
|
101
|
+
newLines.push(endComment);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (!foundSection || lines[i].trim() !== endComment) {
|
|
105
|
+
newLines.push(lines[i]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// If section wasn't found, append it at the end
|
|
109
|
+
if (!foundSection) {
|
|
110
|
+
newLines.push('\n' + startComment);
|
|
111
|
+
const newContent = await getNewContent();
|
|
112
|
+
newLines.push(newContent);
|
|
113
|
+
newLines.push(endComment);
|
|
114
|
+
}
|
|
115
|
+
// Write the updated contents back to the file
|
|
116
|
+
fs__default.writeFileSync(filePath, newLines.join('\n'));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const template$4 = `GOAL: Use functional abstractions to summarize the following text
|
|
51
120
|
|
|
52
121
|
RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
|
|
53
122
|
|
|
54
123
|
TEXT:"""{text}"""
|
|
55
124
|
`;
|
|
56
|
-
const inputVariables$
|
|
125
|
+
const inputVariables$3 = ['text'];
|
|
57
126
|
const SUMMARIZE_PROMPT = new PromptTemplate({
|
|
58
|
-
inputVariables: inputVariables$
|
|
59
|
-
template: template$
|
|
127
|
+
inputVariables: inputVariables$3,
|
|
128
|
+
template: template$4,
|
|
60
129
|
});
|
|
61
130
|
|
|
62
131
|
/**
|
|
@@ -107,9 +176,9 @@ function getDefaultServiceApiKey(config) {
|
|
|
107
176
|
}
|
|
108
177
|
const DEFAULT_OPENAI_LLM_SERVICE = {
|
|
109
178
|
provider: 'openai',
|
|
110
|
-
model: 'gpt-4
|
|
179
|
+
model: 'gpt-4',
|
|
111
180
|
tokenLimit: 2024,
|
|
112
|
-
temperature: 0.
|
|
181
|
+
temperature: 0.32,
|
|
113
182
|
authentication: {
|
|
114
183
|
type: 'APIKey',
|
|
115
184
|
credentials: {
|
|
@@ -182,68 +251,6 @@ const CONFIG_KEYS = Object.keys({
|
|
|
182
251
|
prompt: '',
|
|
183
252
|
});
|
|
184
253
|
|
|
185
|
-
async function updateFileSection({ filePath, startComment, endComment, getNewContent, confirmUpdate = true, confirmMessage = (path) => `A section already exists in ${path}, do you want to override it?`, }) {
|
|
186
|
-
const lines = fs__default.existsSync(filePath) ? fs__default.readFileSync(filePath, 'utf-8').split(/\r?\n/) : [];
|
|
187
|
-
const newLines = [];
|
|
188
|
-
let foundSection = false;
|
|
189
|
-
for (let i = 0; i < lines.length; i++) {
|
|
190
|
-
if (lines[i].trim() === startComment) {
|
|
191
|
-
foundSection = true;
|
|
192
|
-
if (confirmUpdate) {
|
|
193
|
-
const confirmOverwrite = await confirm({
|
|
194
|
-
message: typeof confirmMessage === 'function' ? confirmMessage(filePath) : confirmMessage,
|
|
195
|
-
default: false,
|
|
196
|
-
});
|
|
197
|
-
if (!confirmOverwrite) {
|
|
198
|
-
// keep all lines until the end comment
|
|
199
|
-
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
200
|
-
newLines.push(lines[i]);
|
|
201
|
-
i++;
|
|
202
|
-
}
|
|
203
|
-
newLines.push(endComment);
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
newLines.push(startComment);
|
|
208
|
-
// Insert the new content
|
|
209
|
-
const newContent = await getNewContent();
|
|
210
|
-
newLines.push(newContent);
|
|
211
|
-
// Skip the existing content of the section
|
|
212
|
-
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
213
|
-
i++;
|
|
214
|
-
}
|
|
215
|
-
newLines.push(endComment);
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
if (!foundSection || lines[i].trim() !== endComment) {
|
|
219
|
-
newLines.push(lines[i]);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
// If section wasn't found, append it at the end
|
|
223
|
-
if (!foundSection) {
|
|
224
|
-
newLines.push('\n' + startComment);
|
|
225
|
-
const newContent = await getNewContent();
|
|
226
|
-
newLines.push(newContent);
|
|
227
|
-
newLines.push(endComment);
|
|
228
|
-
}
|
|
229
|
-
// Write the updated contents back to the file
|
|
230
|
-
fs__default.writeFileSync(filePath, newLines.join('\n'));
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const isInteractive = (argv) => {
|
|
234
|
-
return argv?.mode === 'interactive' || argv.interactive;
|
|
235
|
-
};
|
|
236
|
-
const SEPERATOR = chalk.blue('─────────────');
|
|
237
|
-
const LOGO = chalk.green(`┌────────────┐
|
|
238
|
-
│┌─┐┌─┐┌─┐┌─┐│
|
|
239
|
-
││ │ ││ │ ││
|
|
240
|
-
│└─┘└─┘└─┘└─┘│
|
|
241
|
-
└────────────┘
|
|
242
|
-
`);
|
|
243
|
-
const CONFIG_ALREADY_EXISTS = (path) => {
|
|
244
|
-
return `coco config found in '${path}', do you want to override it?`;
|
|
245
|
-
};
|
|
246
|
-
|
|
247
254
|
/**
|
|
248
255
|
* Load environment variables
|
|
249
256
|
*
|
|
@@ -267,6 +274,9 @@ function loadEnvConfig(config) {
|
|
|
267
274
|
handleServiceEnvVar(envConfig.service, key, envValue);
|
|
268
275
|
}
|
|
269
276
|
else {
|
|
277
|
+
if (key === 'service' || !envValue) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
270
280
|
envConfig[key] = envValue;
|
|
271
281
|
}
|
|
272
282
|
});
|
|
@@ -375,7 +385,6 @@ function loadGitConfig(config) {
|
|
|
375
385
|
config = {
|
|
376
386
|
...config,
|
|
377
387
|
service: service,
|
|
378
|
-
temperature: gitConfigParsed.coco?.temperature || config.temperature,
|
|
379
388
|
prompt: gitConfigParsed.coco?.prompt || config.prompt,
|
|
380
389
|
mode: gitConfigParsed.coco?.mode || config.mode,
|
|
381
390
|
summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
|
|
@@ -1916,88 +1925,651 @@ function commandExecutor(handler) {
|
|
|
1916
1925
|
}
|
|
1917
1926
|
|
|
1918
1927
|
/**
|
|
1919
|
-
*
|
|
1920
|
-
*
|
|
1921
|
-
* @
|
|
1928
|
+
* Get LLM Model Based on Configuration
|
|
1929
|
+
*
|
|
1930
|
+
* @param fields
|
|
1931
|
+
* @param configuration
|
|
1932
|
+
* @returns LLM Model
|
|
1922
1933
|
*/
|
|
1923
|
-
function
|
|
1924
|
-
|
|
1934
|
+
function getLlm(provider, model, config) {
|
|
1935
|
+
if (!model) {
|
|
1936
|
+
throw new Error(`Invalid LLM Service: ${provider}/${model}`);
|
|
1937
|
+
}
|
|
1938
|
+
switch (provider) {
|
|
1939
|
+
case 'ollama':
|
|
1940
|
+
return new ChatOllama({
|
|
1941
|
+
baseUrl: DEFAULT_OLLAMA_LLM_SERVICE.endpoint,
|
|
1942
|
+
maxConcurrency: config.service.maxConcurrent,
|
|
1943
|
+
model,
|
|
1944
|
+
});
|
|
1945
|
+
case 'openai':
|
|
1946
|
+
default:
|
|
1947
|
+
const openAiModel = new ChatOpenAI({
|
|
1948
|
+
openAIApiKey: getApiKeyForModel(config),
|
|
1949
|
+
model,
|
|
1950
|
+
temperature: config.service.temperature || 0.2,
|
|
1951
|
+
});
|
|
1952
|
+
return openAiModel;
|
|
1953
|
+
}
|
|
1925
1954
|
}
|
|
1926
1955
|
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
return res.text && res.text.trim();
|
|
1956
|
+
function getPrompt({ template, variables, fallback }) {
|
|
1957
|
+
if (!template && !fallback)
|
|
1958
|
+
throw new Error('Must provide either a template or a fallback');
|
|
1959
|
+
return (template
|
|
1960
|
+
? new PromptTemplate({
|
|
1961
|
+
template,
|
|
1962
|
+
inputVariables: variables,
|
|
1963
|
+
})
|
|
1964
|
+
: fallback);
|
|
1937
1965
|
}
|
|
1938
1966
|
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
* @returns {DirectoryDiff[]} The groups created.
|
|
1943
|
-
*/
|
|
1944
|
-
function createDirectoryDiffs(node) {
|
|
1945
|
-
const groupByPath = {};
|
|
1946
|
-
function traverse(node) {
|
|
1947
|
-
node.diffs.forEach((diff) => {
|
|
1948
|
-
const path = getPathFromFilePath(diff.file);
|
|
1949
|
-
if (!groupByPath[path]) {
|
|
1950
|
-
groupByPath[path] = { diffs: [], path, tokenCount: 0 };
|
|
1951
|
-
}
|
|
1952
|
-
groupByPath[path].diffs.push(diff);
|
|
1953
|
-
groupByPath[path].tokenCount += diff.tokenCount;
|
|
1954
|
-
});
|
|
1955
|
-
node.children.forEach(traverse);
|
|
1967
|
+
const executeChain = async ({ llm, prompt, variables, parser }) => {
|
|
1968
|
+
if (!llm || !prompt || !variables) {
|
|
1969
|
+
throw new Error('The input parameters "llm", "prompt", and "variables" are all required.');
|
|
1956
1970
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1971
|
+
const chain = prompt.pipe(llm).pipe(parser);
|
|
1972
|
+
let res;
|
|
1973
|
+
try {
|
|
1974
|
+
res = await chain.invoke(variables);
|
|
1975
|
+
}
|
|
1976
|
+
catch (error) {
|
|
1977
|
+
if (error instanceof Error) {
|
|
1978
|
+
throw new Error(`LLMChain call error: ${error.message}`);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
if (!res) {
|
|
1982
|
+
throw new Error('Empty response from LLMChain call');
|
|
1983
|
+
}
|
|
1984
|
+
return res;
|
|
1985
|
+
};
|
|
1986
|
+
|
|
1987
|
+
function extractTicketIdFromBranchName(branchName) {
|
|
1988
|
+
const regex = /((?<!([A-Z]+)-?)[A-Z]+-\d+)/;
|
|
1989
|
+
const match = branchName.match(regex);
|
|
1990
|
+
return match ? match[0] : null;
|
|
1959
1991
|
}
|
|
1992
|
+
|
|
1960
1993
|
/**
|
|
1961
|
-
*
|
|
1994
|
+
* Retrieves the commit log range between two specified commits.
|
|
1995
|
+
*
|
|
1996
|
+
* @param from - The starting commit.
|
|
1997
|
+
* @param to - The ending commit.
|
|
1998
|
+
* @param options - Additional options for retrieving the commit log range.
|
|
1999
|
+
* @returns A promise that resolves to an array of commit log messages.
|
|
2000
|
+
* @throws If there is an error retrieving the commit log range.
|
|
1962
2001
|
*/
|
|
1963
|
-
async function
|
|
2002
|
+
async function getCommitLogRange(from, to, { noMerges, git }) {
|
|
1964
2003
|
try {
|
|
1965
|
-
const
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
file: diff.file,
|
|
1969
|
-
summary: diff.summary,
|
|
1970
|
-
},
|
|
1971
|
-
})), {
|
|
1972
|
-
chain,
|
|
1973
|
-
textSplitter,
|
|
1974
|
-
options: {
|
|
1975
|
-
returnIntermediateSteps: true,
|
|
1976
|
-
},
|
|
1977
|
-
});
|
|
1978
|
-
const newTokenTotal = tokenizer(directorySummary);
|
|
1979
|
-
return {
|
|
1980
|
-
diffs: directory.diffs,
|
|
1981
|
-
path: directory.path,
|
|
1982
|
-
summary: directorySummary,
|
|
1983
|
-
tokenCount: newTokenTotal,
|
|
1984
|
-
};
|
|
2004
|
+
const logOptions = { from: `${from}^1`, to, '--no-merges': noMerges };
|
|
2005
|
+
const commitLog = await git.log(logOptions);
|
|
2006
|
+
return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
|
|
1985
2007
|
}
|
|
1986
2008
|
catch (error) {
|
|
1987
|
-
|
|
1988
|
-
|
|
2009
|
+
// If there's an error, handle it appropriately
|
|
2010
|
+
throw error;
|
|
1989
2011
|
}
|
|
1990
2012
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* Retrieves the name of the current branch.
|
|
2016
|
+
*
|
|
2017
|
+
* @param {GetCurrentBranchName} options - The options for retrieving the branch name.
|
|
2018
|
+
* @returns {Promise<string>} - A promise that resolves to the name of the current branch.
|
|
2019
|
+
*/
|
|
2020
|
+
async function getCurrentBranchName({ git }) {
|
|
2021
|
+
return await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
/**
|
|
2025
|
+
* Retrieves the commit log between the current branch and a specified target branch.
|
|
2026
|
+
*
|
|
2027
|
+
* @param {Object} options - The options for retrieving the commit log.
|
|
2028
|
+
* @param {SimpleGit} options.git - The SimpleGit instance.
|
|
2029
|
+
* @param {Logger} options.logger - The logger for logging messages.
|
|
2030
|
+
* @param {string} options.targetBranch - The target branch to compare against.
|
|
2031
|
+
* @returns {Promise<string[]>} The array of commit messages in the commit log.
|
|
2032
|
+
*/
|
|
2033
|
+
async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
|
|
2034
|
+
try {
|
|
2035
|
+
// Get the current branch name
|
|
2036
|
+
const currentBranch = await getCurrentBranchName({ git });
|
|
2037
|
+
// Get the list of commits that are unique to the current branch compared to the target branch
|
|
2038
|
+
const uniqueCommits = (await git.raw(['rev-list', `${targetBranch}..${currentBranch}`]))
|
|
2039
|
+
.split('\n')
|
|
2040
|
+
.filter(Boolean)
|
|
2041
|
+
.reverse();
|
|
2042
|
+
logger?.verbose(`Found ${uniqueCommits.length} unique commits between "${currentBranch}" and "${targetBranch}"`, { color: 'blue' });
|
|
2043
|
+
const firstCommit = uniqueCommits[0];
|
|
2044
|
+
const lastCommit = uniqueCommits[uniqueCommits.length - 1];
|
|
2045
|
+
if (!firstCommit || !lastCommit) {
|
|
2046
|
+
logger?.log('Unable to determine first and last commit between branches', { color: 'yellow' });
|
|
2047
|
+
return [];
|
|
2048
|
+
}
|
|
2049
|
+
// Retrieve commit log with messages
|
|
2050
|
+
return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
|
|
2051
|
+
}
|
|
2052
|
+
catch (error) {
|
|
2053
|
+
logger?.log('Encountered an error getting commit log between branches', { color: 'red' });
|
|
2054
|
+
}
|
|
2055
|
+
return [];
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
/**
|
|
2059
|
+
* Retrieves the commit log for the current branch.
|
|
2060
|
+
*
|
|
2061
|
+
* @param {Object} options - The options for retrieving the commit log.
|
|
2062
|
+
* @param {SimpleGit} options.git - The SimpleGit instance.
|
|
2063
|
+
* @param {Logger} options.logger - The logger for logging messages.
|
|
2064
|
+
* @param {string} [options.comparisonBranch='main'] - The branch to compare against.
|
|
2065
|
+
* @param {string} [options.comparisonRemote='origin'] - The remote to compare against.
|
|
2066
|
+
* @returns {Promise<string[]>} The array of commit messages in the commit log.
|
|
2067
|
+
*/
|
|
2068
|
+
async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
|
|
2069
|
+
try {
|
|
2070
|
+
// Get the current branch name
|
|
2071
|
+
const branch = await getCurrentBranchName({ git });
|
|
2072
|
+
// Check if the current branch has any commits
|
|
2073
|
+
const hasCommits = (await git.raw(['rev-list', '--count', branch])) !== '0';
|
|
2074
|
+
if (!hasCommits) {
|
|
2075
|
+
logger?.log('No commits on the current branch.');
|
|
2076
|
+
return [];
|
|
2077
|
+
}
|
|
2078
|
+
// Get the list of commits that are unique to the current branch
|
|
2079
|
+
let uniqueCommits;
|
|
2080
|
+
if (comparisonBranch === branch) {
|
|
2081
|
+
// If the comparison branch is the same as the current branch, we compare against the remote.
|
|
2082
|
+
uniqueCommits = (await git.raw(['rev-list', `${comparisonRemote}/${comparisonBranch}..${branch}`]))
|
|
2083
|
+
.split('\n')
|
|
2084
|
+
.filter(Boolean)
|
|
2085
|
+
.reverse();
|
|
2086
|
+
}
|
|
2087
|
+
else {
|
|
2088
|
+
// Your existing code for different branches
|
|
2089
|
+
uniqueCommits = (await git.raw(['rev-list', `${comparisonBranch}..${branch}`]))
|
|
2090
|
+
.split('\n')
|
|
2091
|
+
.filter(Boolean)
|
|
2092
|
+
.reverse();
|
|
2093
|
+
}
|
|
2094
|
+
logger?.verbose(`Found ${uniqueCommits.length} unique commits on "${branch}"`, { color: 'blue' });
|
|
2095
|
+
const firstCommit = uniqueCommits[0];
|
|
2096
|
+
const lastCommit = uniqueCommits[uniqueCommits.length - 1];
|
|
2097
|
+
if (!firstCommit || !lastCommit) {
|
|
2098
|
+
logger?.log('Unable to determine first and last commit on the current branch', { color: 'yellow' });
|
|
2099
|
+
return [];
|
|
2100
|
+
}
|
|
2101
|
+
// Retrieve commit log with messages
|
|
2102
|
+
return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
|
|
2103
|
+
}
|
|
2104
|
+
catch (error) {
|
|
2105
|
+
logger?.log('Encountered an error getting commit log from current branch', { color: 'red' });
|
|
2106
|
+
}
|
|
2107
|
+
return [];
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
/**
|
|
2111
|
+
* Retrieves the SimpleGit instance for the repository.
|
|
2112
|
+
* @returns {SimpleGit} The SimpleGit instance.
|
|
2113
|
+
*/
|
|
2114
|
+
const getRepo = () => {
|
|
2115
|
+
let git;
|
|
2116
|
+
try {
|
|
2117
|
+
git = simpleGit();
|
|
2118
|
+
}
|
|
2119
|
+
catch (e) {
|
|
2120
|
+
console.log('Error initializing git repo', e);
|
|
2121
|
+
process.exit(1);
|
|
2122
|
+
}
|
|
2123
|
+
return git;
|
|
2124
|
+
};
|
|
2125
|
+
|
|
2126
|
+
const template$3 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
|
|
2127
|
+
Commit Messages must have a short description that is less than 50 characters and a longer detailed summary around 300 characters, the shorter and more concise the better.
|
|
2128
|
+
|
|
2129
|
+
Please follow the guidelines below when writing your commit message:
|
|
2130
|
+
|
|
2131
|
+
- Write concisely using an informal tone
|
|
2132
|
+
- Avoid phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
|
|
2133
|
+
- Avoid referencing specific files names or long paths in the commit message
|
|
2134
|
+
- DO NOT include any diffs or file changes in the commit message
|
|
2135
|
+
- Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
|
|
2136
|
+
|
|
2137
|
+
{format_instructions}
|
|
2138
|
+
|
|
2139
|
+
""""""
|
|
2140
|
+
{summary}
|
|
2141
|
+
""""""
|
|
2142
|
+
|
|
2143
|
+
{additional}
|
|
2144
|
+
`;
|
|
2145
|
+
const inputVariables$2 = ['summary', 'format_instructions', 'additional'];
|
|
2146
|
+
const COMMIT_PROMPT = new PromptTemplate({
|
|
2147
|
+
template: template$3,
|
|
2148
|
+
inputVariables: inputVariables$2,
|
|
2149
|
+
});
|
|
2150
|
+
|
|
2151
|
+
/**
|
|
2152
|
+
* Verify template string contains all required input variables
|
|
2153
|
+
*
|
|
2154
|
+
* @param text template string
|
|
2155
|
+
* @param inputVariables template variables
|
|
2156
|
+
* @returns boolean or error message
|
|
2157
|
+
*/
|
|
2158
|
+
function validatePromptTemplate(text, inputVariables) {
|
|
2159
|
+
if (!text) {
|
|
2160
|
+
return 'Prompt template cannot be empty';
|
|
2161
|
+
}
|
|
2162
|
+
if (!inputVariables.some((entry) => text.includes(entry))) {
|
|
2163
|
+
return ('Prompt template must include at least one of the following input variables: ' +
|
|
2164
|
+
inputVariables.map((value) => `{${value}}`).join(', '));
|
|
2165
|
+
}
|
|
2166
|
+
return true;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
async function editPrompt(options) {
|
|
2170
|
+
return await editor({
|
|
2171
|
+
message: 'Edit the prompt',
|
|
2172
|
+
default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
|
|
2173
|
+
waitForUseInput: false,
|
|
2174
|
+
postfix: 'Press ENTER to continue',
|
|
2175
|
+
validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
async function editResult(result, options) {
|
|
2180
|
+
if (options.openInEditor) {
|
|
2181
|
+
return await editor({
|
|
2182
|
+
message: 'Edit the commit message',
|
|
2183
|
+
default: result,
|
|
2184
|
+
waitForUseInput: false,
|
|
2185
|
+
validate: (text) => (text ? true : 'Commit message cannot be empty'),
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
return result;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
async function getUserReviewDecision({ label, descriptions, enableRetry = true, enableFullRetry = true, enableModifyPrompt = true, }) {
|
|
2192
|
+
const choices = [
|
|
2193
|
+
{
|
|
2194
|
+
name: '✨ Looks good!',
|
|
2195
|
+
value: 'approve',
|
|
2196
|
+
description: descriptions?.approve || `Continue with the generated ${label}`,
|
|
2197
|
+
},
|
|
2198
|
+
{
|
|
2199
|
+
name: '📝 Edit',
|
|
2200
|
+
value: 'edit',
|
|
2201
|
+
description: descriptions?.edit || `Edit the generated ${label} before proceeding`,
|
|
2202
|
+
},
|
|
2203
|
+
];
|
|
2204
|
+
if (enableModifyPrompt) {
|
|
2205
|
+
choices.push({
|
|
2206
|
+
name: '🪶 Modify Prompt',
|
|
2207
|
+
value: 'modifyPrompt',
|
|
2208
|
+
description: descriptions?.modifyPrompt || `Modify the prompt template and regenerate the ${label}`,
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
if (enableRetry) {
|
|
2212
|
+
choices.push({
|
|
2213
|
+
name: '🔄 Retry',
|
|
2214
|
+
value: 'retryMessageOnly',
|
|
2215
|
+
description: descriptions?.retryMessageOnly ||
|
|
2216
|
+
`Restart the function execution from generating the ${label}`,
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
if (enableFullRetry) {
|
|
2220
|
+
choices.push({
|
|
2221
|
+
name: '🔄 Retry Full',
|
|
2222
|
+
value: 'retryFull',
|
|
2223
|
+
description: descriptions?.retryFull ||
|
|
2224
|
+
`Restart the function execution from the beginning, regenerating both the summary and ${label}`,
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
choices.push({
|
|
2228
|
+
name: '💣 Cancel',
|
|
2229
|
+
value: 'cancel',
|
|
2230
|
+
description: descriptions?.cancel || `Cancel the ${label}`,
|
|
2231
|
+
});
|
|
2232
|
+
return (await select({
|
|
2233
|
+
message: `Would you like to make any changes to the ${label}?`,
|
|
2234
|
+
choices,
|
|
2235
|
+
}));
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
function logResult(label, result) {
|
|
2239
|
+
console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
async function generateAndReviewLoop({ label, factory, parser, noResult, agent, options, }) {
|
|
2243
|
+
const { logger } = options;
|
|
2244
|
+
let continueLoop = true;
|
|
2245
|
+
let modifyPrompt = false;
|
|
2246
|
+
let context = '';
|
|
2247
|
+
let result = '';
|
|
2248
|
+
const changes = await factory();
|
|
2249
|
+
// if we don't have any changes, bail.
|
|
2250
|
+
if (!changes || !Object.keys(changes).length) {
|
|
2251
|
+
await noResult(options);
|
|
2252
|
+
}
|
|
2253
|
+
while (continueLoop) {
|
|
2254
|
+
if (!context.length) {
|
|
2255
|
+
context = await parser(changes, result, options);
|
|
2256
|
+
}
|
|
2257
|
+
// if we still don't have a context, bail.
|
|
2258
|
+
if (!context.length) {
|
|
2259
|
+
await noResult(options);
|
|
2260
|
+
}
|
|
2261
|
+
if (modifyPrompt) {
|
|
2262
|
+
options.prompt = await editPrompt(options);
|
|
2263
|
+
}
|
|
2264
|
+
logger.startTimer().startSpinner(`Generating ${label}\n`, {
|
|
2265
|
+
color: 'blue',
|
|
2266
|
+
});
|
|
2267
|
+
result = await agent(context, options);
|
|
2268
|
+
if (!result) {
|
|
2269
|
+
logger.stopSpinner('💀 Agent failed to return content.', {
|
|
2270
|
+
mode: 'fail',
|
|
2271
|
+
color: 'red',
|
|
2272
|
+
});
|
|
2273
|
+
process.exit(0);
|
|
2274
|
+
}
|
|
2275
|
+
logger
|
|
2276
|
+
.stopSpinner(`Generated ${label}`, {
|
|
2277
|
+
color: 'green',
|
|
2278
|
+
mode: 'succeed',
|
|
2279
|
+
})
|
|
2280
|
+
.stopTimer();
|
|
2281
|
+
if (options?.interactive) {
|
|
2282
|
+
logResult(label, result);
|
|
2283
|
+
const reviewAnswer = await getUserReviewDecision({
|
|
2284
|
+
label,
|
|
2285
|
+
...(options?.review || {}),
|
|
2286
|
+
});
|
|
2287
|
+
if (reviewAnswer === 'cancel') {
|
|
2288
|
+
process.exit(0);
|
|
2289
|
+
}
|
|
2290
|
+
if (reviewAnswer === 'edit') {
|
|
2291
|
+
options.openInEditor = true;
|
|
2292
|
+
}
|
|
2293
|
+
if (reviewAnswer === 'retryFull') {
|
|
2294
|
+
context = '';
|
|
2295
|
+
result = '';
|
|
2296
|
+
options.prompt = '';
|
|
2297
|
+
continue;
|
|
2298
|
+
}
|
|
2299
|
+
if (reviewAnswer === 'retryMessageOnly') {
|
|
2300
|
+
modifyPrompt = false;
|
|
2301
|
+
result = '';
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
2304
|
+
if (reviewAnswer === 'modifyPrompt') {
|
|
2305
|
+
modifyPrompt = true;
|
|
2306
|
+
result = '';
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
// if we're here, we're done.
|
|
2311
|
+
result = await editResult(result, options);
|
|
2312
|
+
continueLoop = false;
|
|
2313
|
+
}
|
|
2314
|
+
return result;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
const logSuccess = () => {
|
|
2318
|
+
console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
|
|
2319
|
+
};
|
|
2320
|
+
|
|
2321
|
+
async function handleResult({ result, mode, interactiveHandler }) {
|
|
2322
|
+
switch (mode) {
|
|
2323
|
+
case 'interactive':
|
|
2324
|
+
if (interactiveHandler) {
|
|
2325
|
+
await interactiveHandler(result);
|
|
2326
|
+
}
|
|
2327
|
+
else {
|
|
2328
|
+
console.warn('No result handler provided for interactive mode.');
|
|
2329
|
+
logSuccess();
|
|
2330
|
+
}
|
|
2331
|
+
break;
|
|
2332
|
+
case 'stdout':
|
|
2333
|
+
default:
|
|
2334
|
+
process.stdout.write(result, 'utf8');
|
|
2335
|
+
break;
|
|
2336
|
+
}
|
|
2337
|
+
process.exit(0);
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
const template$2 = `Write informative git changelog, in the imperative, based on a series of individual messages.
|
|
2341
|
+
|
|
2342
|
+
- Include the git commit hash as reference for each change, including just the first 7 characters
|
|
2343
|
+
- Logically group changes, and if necessary, summarize dependency updates
|
|
2344
|
+
|
|
2345
|
+
{format_instructions}
|
|
2346
|
+
|
|
2347
|
+
"""{summary}"""`;
|
|
2348
|
+
const inputVariables$1 = ['format_instructions', 'summary'];
|
|
2349
|
+
const CHANGELOG_PROMPT = new PromptTemplate({
|
|
2350
|
+
template: template$2,
|
|
2351
|
+
inputVariables: inputVariables$1,
|
|
2352
|
+
});
|
|
2353
|
+
|
|
2354
|
+
const handler$3 = async (argv, logger) => {
|
|
2355
|
+
const config = loadConfig(argv);
|
|
2356
|
+
const git = getRepo();
|
|
2357
|
+
const key = getApiKeyForModel(config);
|
|
2358
|
+
const { provider, model } = getModelAndProviderFromConfig(config);
|
|
2359
|
+
if (config.service.authentication.type !== 'None' && !key) {
|
|
2360
|
+
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
2361
|
+
process.exit(1);
|
|
2362
|
+
}
|
|
2363
|
+
const llm = getLlm(provider, model, config);
|
|
2364
|
+
const INTERACTIVE = isInteractive(config);
|
|
2365
|
+
if (INTERACTIVE) {
|
|
2366
|
+
logger.log(LOGO);
|
|
2367
|
+
}
|
|
2368
|
+
async function factory() {
|
|
2369
|
+
const branchName = await getCurrentBranchName({ git });
|
|
2370
|
+
if (config.range && config.range.includes(':')) {
|
|
2371
|
+
const [from, to] = config.range.split(':');
|
|
2372
|
+
if (!from || !to) {
|
|
2373
|
+
logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
|
|
2374
|
+
process.exit(1);
|
|
2375
|
+
}
|
|
2376
|
+
return {
|
|
2377
|
+
branch: branchName,
|
|
2378
|
+
commits: await getCommitLogRange(from, to, { git, noMerges: true }),
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
if (argv.branch) {
|
|
2382
|
+
logger.verbose(`Generating commit log against branch: ${argv.branch}`, { color: 'yellow' });
|
|
2383
|
+
return {
|
|
2384
|
+
branch: branchName,
|
|
2385
|
+
commits: await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch }),
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
logger.verbose(`No range or branch provided. Defaulting to current branch`, { color: 'yellow' });
|
|
2389
|
+
return {
|
|
2390
|
+
branch: branchName,
|
|
2391
|
+
commits: await getCommitLogCurrentBranch({ git, logger }),
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
async function parser({ branch, commits }) {
|
|
2395
|
+
let result;
|
|
2396
|
+
if (!commits || commits.length === 0) {
|
|
2397
|
+
result = `## ${branch}\n\nNo commits found.`;
|
|
2398
|
+
}
|
|
2399
|
+
else {
|
|
2400
|
+
result = `## ${branch}\n\n${commits.map((commit) => commit.trim()).join('\n\n')}`;
|
|
2401
|
+
}
|
|
2402
|
+
return result;
|
|
2403
|
+
}
|
|
2404
|
+
const changelogMsg = await generateAndReviewLoop({
|
|
2405
|
+
label: 'changelog',
|
|
2406
|
+
factory,
|
|
2407
|
+
parser,
|
|
2408
|
+
agent: async (context, options) => {
|
|
2409
|
+
const parser = new JsonOutputParser();
|
|
2410
|
+
const prompt = getPrompt({
|
|
2411
|
+
template: options.prompt,
|
|
2412
|
+
variables: CHANGELOG_PROMPT.inputVariables,
|
|
2413
|
+
fallback: CHANGELOG_PROMPT,
|
|
2414
|
+
});
|
|
2415
|
+
const formatInstructions = "Respond with a valid JSON object, containing two fields: 'header' and 'content', both strings.";
|
|
2416
|
+
const changelog = await executeChain({
|
|
2417
|
+
llm,
|
|
2418
|
+
prompt,
|
|
2419
|
+
variables: {
|
|
2420
|
+
summary: context,
|
|
2421
|
+
format_instructions: formatInstructions,
|
|
2422
|
+
},
|
|
2423
|
+
parser,
|
|
2424
|
+
});
|
|
2425
|
+
const branchName = await getCurrentBranchName({ git });
|
|
2426
|
+
const ticketId = extractTicketIdFromBranchName(branchName);
|
|
2427
|
+
const footer = ticketId ? `\n\nPart of **${ticketId}**` : '';
|
|
2428
|
+
return `${changelog.header}\n\n${changelog.content}${footer}`;
|
|
2429
|
+
},
|
|
2430
|
+
noResult: async () => {
|
|
2431
|
+
if (config.range) {
|
|
2432
|
+
logger.log(`No commits found in the provided range.`, { color: 'red' });
|
|
2433
|
+
process.exit(0);
|
|
2434
|
+
}
|
|
2435
|
+
logger.log(`No commits found in the current branch.`, { color: 'red' });
|
|
2436
|
+
process.exit(0);
|
|
2437
|
+
},
|
|
2438
|
+
options: {
|
|
2439
|
+
...config,
|
|
2440
|
+
prompt: config.prompt || CHANGELOG_PROMPT.template,
|
|
2441
|
+
logger,
|
|
2442
|
+
interactive: INTERACTIVE,
|
|
2443
|
+
review: {
|
|
2444
|
+
enableFullRetry: false,
|
|
2445
|
+
},
|
|
2446
|
+
},
|
|
2447
|
+
});
|
|
2448
|
+
const MODE = (INTERACTIVE && 'interactive') || (config.commit && 'interactive') || config?.mode || 'stdout';
|
|
2449
|
+
handleResult({
|
|
2450
|
+
result: changelogMsg,
|
|
2451
|
+
interactiveHandler: async () => {
|
|
2452
|
+
logSuccess();
|
|
2453
|
+
},
|
|
2454
|
+
mode: MODE,
|
|
2455
|
+
});
|
|
2456
|
+
};
|
|
2457
|
+
|
|
2458
|
+
/**
|
|
2459
|
+
* Command line options via yargs
|
|
2460
|
+
*/
|
|
2461
|
+
const options$3 = {
|
|
2462
|
+
range: {
|
|
2463
|
+
type: 'string',
|
|
2464
|
+
alias: 'r',
|
|
2465
|
+
description: 'Commit range e.g `HEAD~2:HEAD`',
|
|
2466
|
+
},
|
|
2467
|
+
branch: {
|
|
2468
|
+
type: 'string',
|
|
2469
|
+
alias: 'b',
|
|
2470
|
+
description: 'Target branch to compare against',
|
|
2471
|
+
},
|
|
2472
|
+
i: {
|
|
2473
|
+
type: 'boolean',
|
|
2474
|
+
alias: 'interactive',
|
|
2475
|
+
description: 'Toggle interactive mode',
|
|
2476
|
+
},
|
|
2477
|
+
};
|
|
2478
|
+
const builder$3 = (yargsInstance) => {
|
|
2479
|
+
return yargsInstance.options(options$3).usage(getCommandUsageHeader(changelog.command));
|
|
2480
|
+
};
|
|
2481
|
+
|
|
2482
|
+
var changelog = {
|
|
2483
|
+
command: 'changelog',
|
|
2484
|
+
desc: 'Generate a changelog from current or target branch or provided commit range.',
|
|
2485
|
+
builder: builder$3,
|
|
2486
|
+
handler: commandExecutor(handler$3),
|
|
2487
|
+
options: options$3,
|
|
2488
|
+
};
|
|
2489
|
+
|
|
2490
|
+
/**
|
|
2491
|
+
* Extract the path from a file path string.
|
|
2492
|
+
* @param {string} filePath - The full file path.
|
|
2493
|
+
* @returns {string} The path portion of the file path.
|
|
2494
|
+
*/
|
|
2495
|
+
function getPathFromFilePath(filePath) {
|
|
2496
|
+
return filePath.split('/').slice(0, -1).join('/');
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
async function summarize(documents, { chain, textSplitter, options }) {
|
|
2500
|
+
const { returnIntermediateSteps = false } = options || {};
|
|
2501
|
+
const docs = await textSplitter.splitDocuments(documents.map((doc) => new Document(doc)));
|
|
2502
|
+
const res = await chain.invoke({
|
|
2503
|
+
input_documents: docs,
|
|
2504
|
+
returnIntermediateSteps,
|
|
2505
|
+
});
|
|
2506
|
+
if (res.error)
|
|
2507
|
+
throw new Error(res.error);
|
|
2508
|
+
return res.text && res.text.trim();
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
/**
|
|
2512
|
+
* Create groups from a given node info.
|
|
2513
|
+
* @param {DiffNode} node - The node info to start grouping.
|
|
2514
|
+
* @returns {DirectoryDiff[]} The groups created.
|
|
2515
|
+
*/
|
|
2516
|
+
function createDirectoryDiffs(node) {
|
|
2517
|
+
const groupByPath = {};
|
|
2518
|
+
function traverse(node) {
|
|
2519
|
+
node.diffs.forEach((diff) => {
|
|
2520
|
+
const path = getPathFromFilePath(diff.file);
|
|
2521
|
+
if (!groupByPath[path]) {
|
|
2522
|
+
groupByPath[path] = { diffs: [], path, tokenCount: 0 };
|
|
2523
|
+
}
|
|
2524
|
+
groupByPath[path].diffs.push(diff);
|
|
2525
|
+
groupByPath[path].tokenCount += diff.tokenCount;
|
|
2526
|
+
});
|
|
2527
|
+
node.children.forEach(traverse);
|
|
2528
|
+
}
|
|
2529
|
+
traverse(node);
|
|
2530
|
+
return Object.values(groupByPath);
|
|
2531
|
+
}
|
|
2532
|
+
/**
|
|
2533
|
+
* Summarize a directory diff asynchronously.
|
|
2534
|
+
*/
|
|
2535
|
+
async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenizer }) {
|
|
2536
|
+
try {
|
|
2537
|
+
const directorySummary = await summarize(directory.diffs.map((diff) => ({
|
|
2538
|
+
pageContent: diff.diff,
|
|
2539
|
+
metadata: {
|
|
2540
|
+
file: diff.file,
|
|
2541
|
+
summary: diff.summary,
|
|
2542
|
+
},
|
|
2543
|
+
})), {
|
|
2544
|
+
chain,
|
|
2545
|
+
textSplitter,
|
|
2546
|
+
options: {
|
|
2547
|
+
returnIntermediateSteps: true,
|
|
2548
|
+
},
|
|
2549
|
+
});
|
|
2550
|
+
const newTokenTotal = tokenizer(directorySummary);
|
|
2551
|
+
return {
|
|
2552
|
+
diffs: directory.diffs,
|
|
2553
|
+
path: directory.path,
|
|
2554
|
+
summary: directorySummary,
|
|
2555
|
+
tokenCount: newTokenTotal,
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
catch (error) {
|
|
2559
|
+
console.error(error);
|
|
2560
|
+
return directory;
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
const defaultOutputCallback = (group) => {
|
|
2564
|
+
let output = `
|
|
2565
|
+
-------\n* changes in "/${group.path}"\n\n`;
|
|
2566
|
+
if (group.summary) {
|
|
2567
|
+
output += `${group.diffs.map((diff) => ` • ${diff.summary}`).join('\n')}\n\nSummary:\n\n${group.summary}\n\n`;
|
|
2568
|
+
}
|
|
2569
|
+
else {
|
|
2570
|
+
output += `${group.diffs.map((diff) => ` • ${diff.summary}\n\n${diff.diff}`).join('\n\n')}\n\n`;
|
|
2571
|
+
}
|
|
2572
|
+
return output;
|
|
2001
2573
|
};
|
|
2002
2574
|
async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 2048, textSplitter, chain, handleOutput = defaultOutputCallback, }) {
|
|
2003
2575
|
const queue = new pQueue({ concurrency: 8 });
|
|
@@ -4252,7 +4824,7 @@ var vector_db_qa = /*#__PURE__*/Object.freeze({
|
|
|
4252
4824
|
});
|
|
4253
4825
|
|
|
4254
4826
|
/* eslint-disable spaced-comment */
|
|
4255
|
-
const template$
|
|
4827
|
+
const template$1 = `Write a concise summary of the following:
|
|
4256
4828
|
|
|
4257
4829
|
|
|
4258
4830
|
"{text}"
|
|
@@ -4260,7 +4832,7 @@ const template$2 = `Write a concise summary of the following:
|
|
|
4260
4832
|
|
|
4261
4833
|
CONCISE SUMMARY:`;
|
|
4262
4834
|
const DEFAULT_PROMPT = /*#__PURE__*/ new PromptTemplate({
|
|
4263
|
-
template: template$
|
|
4835
|
+
template: template$1,
|
|
4264
4836
|
inputVariables: ["text"],
|
|
4265
4837
|
});
|
|
4266
4838
|
|
|
@@ -5187,757 +5759,456 @@ function representYamlBinary(object /*, style*/) {
|
|
|
5187
5759
|
return result;
|
|
5188
5760
|
}
|
|
5189
5761
|
|
|
5190
|
-
function isBinary(obj) {
|
|
5191
|
-
return Object.prototype.toString.call(obj) === '[object Uint8Array]';
|
|
5192
|
-
}
|
|
5193
|
-
|
|
5194
|
-
var binary = new type('tag:yaml.org,2002:binary', {
|
|
5195
|
-
kind: 'scalar',
|
|
5196
|
-
resolve: resolveYamlBinary,
|
|
5197
|
-
construct: constructYamlBinary,
|
|
5198
|
-
predicate: isBinary,
|
|
5199
|
-
represent: representYamlBinary
|
|
5200
|
-
});
|
|
5201
|
-
|
|
5202
|
-
var _hasOwnProperty$3 = Object.prototype.hasOwnProperty;
|
|
5203
|
-
var _toString$2 = Object.prototype.toString;
|
|
5204
|
-
|
|
5205
|
-
function resolveYamlOmap(data) {
|
|
5206
|
-
if (data === null) return true;
|
|
5207
|
-
|
|
5208
|
-
var objectKeys = [], index, length, pair, pairKey, pairHasKey,
|
|
5209
|
-
object = data;
|
|
5210
|
-
|
|
5211
|
-
for (index = 0, length = object.length; index < length; index += 1) {
|
|
5212
|
-
pair = object[index];
|
|
5213
|
-
pairHasKey = false;
|
|
5214
|
-
|
|
5215
|
-
if (_toString$2.call(pair) !== '[object Object]') return false;
|
|
5216
|
-
|
|
5217
|
-
for (pairKey in pair) {
|
|
5218
|
-
if (_hasOwnProperty$3.call(pair, pairKey)) {
|
|
5219
|
-
if (!pairHasKey) pairHasKey = true;
|
|
5220
|
-
else return false;
|
|
5221
|
-
}
|
|
5222
|
-
}
|
|
5223
|
-
|
|
5224
|
-
if (!pairHasKey) return false;
|
|
5225
|
-
|
|
5226
|
-
if (objectKeys.indexOf(pairKey) === -1) objectKeys.push(pairKey);
|
|
5227
|
-
else return false;
|
|
5228
|
-
}
|
|
5229
|
-
|
|
5230
|
-
return true;
|
|
5231
|
-
}
|
|
5232
|
-
|
|
5233
|
-
function constructYamlOmap(data) {
|
|
5234
|
-
return data !== null ? data : [];
|
|
5235
|
-
}
|
|
5236
|
-
|
|
5237
|
-
var omap = new type('tag:yaml.org,2002:omap', {
|
|
5238
|
-
kind: 'sequence',
|
|
5239
|
-
resolve: resolveYamlOmap,
|
|
5240
|
-
construct: constructYamlOmap
|
|
5241
|
-
});
|
|
5242
|
-
|
|
5243
|
-
var _toString$1 = Object.prototype.toString;
|
|
5244
|
-
|
|
5245
|
-
function resolveYamlPairs(data) {
|
|
5246
|
-
if (data === null) return true;
|
|
5247
|
-
|
|
5248
|
-
var index, length, pair, keys, result,
|
|
5249
|
-
object = data;
|
|
5250
|
-
|
|
5251
|
-
result = new Array(object.length);
|
|
5252
|
-
|
|
5253
|
-
for (index = 0, length = object.length; index < length; index += 1) {
|
|
5254
|
-
pair = object[index];
|
|
5255
|
-
|
|
5256
|
-
if (_toString$1.call(pair) !== '[object Object]') return false;
|
|
5257
|
-
|
|
5258
|
-
keys = Object.keys(pair);
|
|
5259
|
-
|
|
5260
|
-
if (keys.length !== 1) return false;
|
|
5261
|
-
|
|
5262
|
-
result[index] = [ keys[0], pair[keys[0]] ];
|
|
5263
|
-
}
|
|
5264
|
-
|
|
5265
|
-
return true;
|
|
5266
|
-
}
|
|
5267
|
-
|
|
5268
|
-
function constructYamlPairs(data) {
|
|
5269
|
-
if (data === null) return [];
|
|
5270
|
-
|
|
5271
|
-
var index, length, pair, keys, result,
|
|
5272
|
-
object = data;
|
|
5273
|
-
|
|
5274
|
-
result = new Array(object.length);
|
|
5275
|
-
|
|
5276
|
-
for (index = 0, length = object.length; index < length; index += 1) {
|
|
5277
|
-
pair = object[index];
|
|
5278
|
-
|
|
5279
|
-
keys = Object.keys(pair);
|
|
5280
|
-
|
|
5281
|
-
result[index] = [ keys[0], pair[keys[0]] ];
|
|
5282
|
-
}
|
|
5283
|
-
|
|
5284
|
-
return result;
|
|
5285
|
-
}
|
|
5286
|
-
|
|
5287
|
-
var pairs = new type('tag:yaml.org,2002:pairs', {
|
|
5288
|
-
kind: 'sequence',
|
|
5289
|
-
resolve: resolveYamlPairs,
|
|
5290
|
-
construct: constructYamlPairs
|
|
5291
|
-
});
|
|
5292
|
-
|
|
5293
|
-
var _hasOwnProperty$2 = Object.prototype.hasOwnProperty;
|
|
5294
|
-
|
|
5295
|
-
function resolveYamlSet(data) {
|
|
5296
|
-
if (data === null) return true;
|
|
5297
|
-
|
|
5298
|
-
var key, object = data;
|
|
5299
|
-
|
|
5300
|
-
for (key in object) {
|
|
5301
|
-
if (_hasOwnProperty$2.call(object, key)) {
|
|
5302
|
-
if (object[key] !== null) return false;
|
|
5303
|
-
}
|
|
5304
|
-
}
|
|
5305
|
-
|
|
5306
|
-
return true;
|
|
5307
|
-
}
|
|
5308
|
-
|
|
5309
|
-
function constructYamlSet(data) {
|
|
5310
|
-
return data !== null ? data : {};
|
|
5311
|
-
}
|
|
5312
|
-
|
|
5313
|
-
var set = new type('tag:yaml.org,2002:set', {
|
|
5314
|
-
kind: 'mapping',
|
|
5315
|
-
resolve: resolveYamlSet,
|
|
5316
|
-
construct: constructYamlSet
|
|
5317
|
-
});
|
|
5318
|
-
|
|
5319
|
-
core.extend({
|
|
5320
|
-
implicit: [
|
|
5321
|
-
timestamp,
|
|
5322
|
-
merge
|
|
5323
|
-
],
|
|
5324
|
-
explicit: [
|
|
5325
|
-
binary,
|
|
5326
|
-
omap,
|
|
5327
|
-
pairs,
|
|
5328
|
-
set
|
|
5329
|
-
]
|
|
5330
|
-
});
|
|
5331
|
-
|
|
5332
|
-
function simpleEscapeSequence(c) {
|
|
5333
|
-
/* eslint-disable indent */
|
|
5334
|
-
return (c === 0x30/* 0 */) ? '\x00' :
|
|
5335
|
-
(c === 0x61/* a */) ? '\x07' :
|
|
5336
|
-
(c === 0x62/* b */) ? '\x08' :
|
|
5337
|
-
(c === 0x74/* t */) ? '\x09' :
|
|
5338
|
-
(c === 0x09/* Tab */) ? '\x09' :
|
|
5339
|
-
(c === 0x6E/* n */) ? '\x0A' :
|
|
5340
|
-
(c === 0x76/* v */) ? '\x0B' :
|
|
5341
|
-
(c === 0x66/* f */) ? '\x0C' :
|
|
5342
|
-
(c === 0x72/* r */) ? '\x0D' :
|
|
5343
|
-
(c === 0x65/* e */) ? '\x1B' :
|
|
5344
|
-
(c === 0x20/* Space */) ? ' ' :
|
|
5345
|
-
(c === 0x22/* " */) ? '\x22' :
|
|
5346
|
-
(c === 0x2F/* / */) ? '/' :
|
|
5347
|
-
(c === 0x5C/* \ */) ? '\x5C' :
|
|
5348
|
-
(c === 0x4E/* N */) ? '\x85' :
|
|
5349
|
-
(c === 0x5F/* _ */) ? '\xA0' :
|
|
5350
|
-
(c === 0x4C/* L */) ? '\u2028' :
|
|
5351
|
-
(c === 0x50/* P */) ? '\u2029' : '';
|
|
5352
|
-
}
|
|
5353
|
-
|
|
5354
|
-
var simpleEscapeCheck = new Array(256); // integer, for fast access
|
|
5355
|
-
var simpleEscapeMap = new Array(256);
|
|
5356
|
-
for (var i = 0; i < 256; i++) {
|
|
5357
|
-
simpleEscapeCheck[i] = simpleEscapeSequence(i) ? 1 : 0;
|
|
5358
|
-
simpleEscapeMap[i] = simpleEscapeSequence(i);
|
|
5359
|
-
}
|
|
5360
|
-
|
|
5361
|
-
/**
|
|
5362
|
-
* Get Summarization Chain
|
|
5363
|
-
* @param model
|
|
5364
|
-
* @param options
|
|
5365
|
-
* @returns
|
|
5366
|
-
*/
|
|
5367
|
-
function getSummarizationChain(model, options = { type: 'map_reduce' }) {
|
|
5368
|
-
return loadSummarizationChain(model, options);
|
|
5369
|
-
}
|
|
5370
|
-
|
|
5371
|
-
/**
|
|
5372
|
-
* Get Recursive Character Text Splitter
|
|
5373
|
-
*
|
|
5374
|
-
* @param options
|
|
5375
|
-
* @returns
|
|
5376
|
-
*/
|
|
5377
|
-
function getTextSplitter(options = {}) {
|
|
5378
|
-
return new RecursiveCharacterTextSplitter(options);
|
|
5379
|
-
}
|
|
5380
|
-
|
|
5381
|
-
/**
|
|
5382
|
-
* Parses the default file diff for a given nodeFile.
|
|
5383
|
-
*
|
|
5384
|
-
* @param nodeFile - The file change object.
|
|
5385
|
-
* @param commit - The commit to diff against. Defaults to '--staged'.
|
|
5386
|
-
* @param git - The SimpleGit instance.
|
|
5387
|
-
* @returns A Promise that resolves to the file diff as a string.
|
|
5388
|
-
*/
|
|
5389
|
-
async function parseDefaultFileDiff(nodeFile, commit = '--staged', git) {
|
|
5390
|
-
if (commit !== '--staged') {
|
|
5391
|
-
return await git.diff([`${commit}~1..${commit}`, '--', nodeFile.filePath]);
|
|
5392
|
-
}
|
|
5393
|
-
return await git.diff([commit, nodeFile.filePath]);
|
|
5394
|
-
}
|
|
5395
|
-
/**
|
|
5396
|
-
* Parses the diff for a renamed file.
|
|
5397
|
-
*
|
|
5398
|
-
* @param nodeFile - The file change object.
|
|
5399
|
-
* @param commit - The commit hash or '--staged'.
|
|
5400
|
-
* @param git - The SimpleGit instance.
|
|
5401
|
-
* @param logger - The logger instance.
|
|
5402
|
-
* @returns A Promise that resolves to the diff string.
|
|
5403
|
-
*/
|
|
5404
|
-
async function parseRenamedFileDiff(nodeFile, commit, git, logger) {
|
|
5405
|
-
let result = '';
|
|
5406
|
-
const oldFilePath = nodeFile?.oldFilePath || nodeFile.filePath;
|
|
5407
|
-
let previousCommitHash = 'HEAD';
|
|
5408
|
-
let newCommitHash = '';
|
|
5409
|
-
if (commit !== '--staged') {
|
|
5410
|
-
try {
|
|
5411
|
-
previousCommitHash = await git.revparse([`${commit}~1`]);
|
|
5412
|
-
}
|
|
5413
|
-
catch (err) {
|
|
5414
|
-
logger.verbose(`Error getting previous commit hash for ${nodeFile.filePath}`, {
|
|
5415
|
-
color: 'red',
|
|
5416
|
-
});
|
|
5417
|
-
}
|
|
5418
|
-
newCommitHash = commit;
|
|
5419
|
-
}
|
|
5420
|
-
try {
|
|
5421
|
-
const [previousContent, newContent] = await Promise.all([
|
|
5422
|
-
git.show([`${previousCommitHash}:${oldFilePath}`]),
|
|
5423
|
-
git.show([`${newCommitHash}:${nodeFile.filePath}`]),
|
|
5424
|
-
]);
|
|
5425
|
-
if (previousContent !== newContent) {
|
|
5426
|
-
result = createTwoFilesPatch(oldFilePath, nodeFile.filePath, previousContent, newContent, '', '', {
|
|
5427
|
-
context: 3,
|
|
5428
|
-
});
|
|
5429
|
-
// remove the first 4 lines of the patch (they contain the old and new file names)
|
|
5430
|
-
result = result.split('\n').slice(4).join('\n');
|
|
5431
|
-
}
|
|
5432
|
-
else {
|
|
5433
|
-
result = 'File contents are unchanged.';
|
|
5434
|
-
}
|
|
5435
|
-
}
|
|
5436
|
-
catch (err) {
|
|
5437
|
-
logger.verbose(`Error comparing file contents for ${nodeFile.filePath}`, { color: 'red' });
|
|
5438
|
-
result = 'Error comparing file contents.';
|
|
5439
|
-
}
|
|
5440
|
-
return result;
|
|
5762
|
+
function isBinary(obj) {
|
|
5763
|
+
return Object.prototype.toString.call(obj) === '[object Uint8Array]';
|
|
5441
5764
|
}
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5765
|
+
|
|
5766
|
+
var binary = new type('tag:yaml.org,2002:binary', {
|
|
5767
|
+
kind: 'scalar',
|
|
5768
|
+
resolve: resolveYamlBinary,
|
|
5769
|
+
construct: constructYamlBinary,
|
|
5770
|
+
predicate: isBinary,
|
|
5771
|
+
represent: representYamlBinary
|
|
5772
|
+
});
|
|
5773
|
+
|
|
5774
|
+
var _hasOwnProperty$3 = Object.prototype.hasOwnProperty;
|
|
5775
|
+
var _toString$2 = Object.prototype.toString;
|
|
5776
|
+
|
|
5777
|
+
function resolveYamlOmap(data) {
|
|
5778
|
+
if (data === null) return true;
|
|
5779
|
+
|
|
5780
|
+
var objectKeys = [], index, length, pair, pairKey, pairHasKey,
|
|
5781
|
+
object = data;
|
|
5782
|
+
|
|
5783
|
+
for (index = 0, length = object.length; index < length; index += 1) {
|
|
5784
|
+
pair = object[index];
|
|
5785
|
+
pairHasKey = false;
|
|
5786
|
+
|
|
5787
|
+
if (_toString$2.call(pair) !== '[object Object]') return false;
|
|
5788
|
+
|
|
5789
|
+
for (pairKey in pair) {
|
|
5790
|
+
if (_hasOwnProperty$3.call(pair, pairKey)) {
|
|
5791
|
+
if (!pairHasKey) pairHasKey = true;
|
|
5792
|
+
else return false;
|
|
5793
|
+
}
|
|
5461
5794
|
}
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5795
|
+
|
|
5796
|
+
if (!pairHasKey) return false;
|
|
5797
|
+
|
|
5798
|
+
if (objectKeys.indexOf(pairKey) === -1) objectKeys.push(pairKey);
|
|
5799
|
+
else return false;
|
|
5800
|
+
}
|
|
5801
|
+
|
|
5802
|
+
return true;
|
|
5465
5803
|
}
|
|
5466
5804
|
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
const MAX_TOKENS_PER_SUMMARY = 12288;
|
|
5470
|
-
async function fileChangeParser({ changes, commit, options: { tokenizer, git, llm: model, logger }, }) {
|
|
5471
|
-
const textSplitter = getTextSplitter({ chunkSize: 10000, chunkOverlap: 250 });
|
|
5472
|
-
// const textSplitter = new TokenTextSplitter({
|
|
5473
|
-
// chunkSize: 10000,
|
|
5474
|
-
// chunkOverlap: 250,
|
|
5475
|
-
// });
|
|
5476
|
-
const summarizationChain = getSummarizationChain(model, {
|
|
5477
|
-
type: 'map_reduce',
|
|
5478
|
-
combineMapPrompt: SUMMARIZE_PROMPT,
|
|
5479
|
-
combinePrompt: SUMMARIZE_PROMPT,
|
|
5480
|
-
});
|
|
5481
|
-
logger.startTimer();
|
|
5482
|
-
const rootTreeNode = createDiffTree(changes);
|
|
5483
|
-
logger.stopTimer('Created file hierarchy');
|
|
5484
|
-
// Collect diffs
|
|
5485
|
-
logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
|
|
5486
|
-
const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, commit, { git, logger }), tokenizer, logger);
|
|
5487
|
-
logger.stopSpinner('Diffs Collected').stopTimer();
|
|
5488
|
-
// Summarize diffs
|
|
5489
|
-
logger.startTimer();
|
|
5490
|
-
const summary = await summarizeDiffs(diffs, {
|
|
5491
|
-
tokenizer,
|
|
5492
|
-
maxTokens: MAX_TOKENS_PER_SUMMARY,
|
|
5493
|
-
textSplitter,
|
|
5494
|
-
chain: summarizationChain,
|
|
5495
|
-
logger,
|
|
5496
|
-
});
|
|
5497
|
-
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
5498
|
-
return summary;
|
|
5805
|
+
function constructYamlOmap(data) {
|
|
5806
|
+
return data !== null ? data : [];
|
|
5499
5807
|
}
|
|
5500
5808
|
|
|
5501
|
-
|
|
5502
|
-
|
|
5809
|
+
var omap = new type('tag:yaml.org,2002:omap', {
|
|
5810
|
+
kind: 'sequence',
|
|
5811
|
+
resolve: resolveYamlOmap,
|
|
5812
|
+
construct: constructYamlOmap
|
|
5813
|
+
});
|
|
5814
|
+
|
|
5815
|
+
var _toString$1 = Object.prototype.toString;
|
|
5503
5816
|
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
- DO NOT use specific names or files from the code
|
|
5507
|
-
- DO NOT include any diffs or file changes in the commit message
|
|
5508
|
-
- Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
|
|
5817
|
+
function resolveYamlPairs(data) {
|
|
5818
|
+
if (data === null) return true;
|
|
5509
5819
|
|
|
5510
|
-
|
|
5820
|
+
var index, length, pair, keys, result,
|
|
5821
|
+
object = data;
|
|
5511
5822
|
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5823
|
+
result = new Array(object.length);
|
|
5824
|
+
|
|
5825
|
+
for (index = 0, length = object.length; index < length; index += 1) {
|
|
5826
|
+
pair = object[index];
|
|
5827
|
+
|
|
5828
|
+
if (_toString$1.call(pair) !== '[object Object]') return false;
|
|
5829
|
+
|
|
5830
|
+
keys = Object.keys(pair);
|
|
5831
|
+
|
|
5832
|
+
if (keys.length !== 1) return false;
|
|
5833
|
+
|
|
5834
|
+
result[index] = [ keys[0], pair[keys[0]] ];
|
|
5835
|
+
}
|
|
5836
|
+
|
|
5837
|
+
return true;
|
|
5838
|
+
}
|
|
5839
|
+
|
|
5840
|
+
function constructYamlPairs(data) {
|
|
5841
|
+
if (data === null) return [];
|
|
5842
|
+
|
|
5843
|
+
var index, length, pair, keys, result,
|
|
5844
|
+
object = data;
|
|
5845
|
+
|
|
5846
|
+
result = new Array(object.length);
|
|
5847
|
+
|
|
5848
|
+
for (index = 0, length = object.length; index < length; index += 1) {
|
|
5849
|
+
pair = object[index];
|
|
5850
|
+
|
|
5851
|
+
keys = Object.keys(pair);
|
|
5852
|
+
|
|
5853
|
+
result[index] = [ keys[0], pair[keys[0]] ];
|
|
5854
|
+
}
|
|
5855
|
+
|
|
5856
|
+
return result;
|
|
5857
|
+
}
|
|
5858
|
+
|
|
5859
|
+
var pairs = new type('tag:yaml.org,2002:pairs', {
|
|
5860
|
+
kind: 'sequence',
|
|
5861
|
+
resolve: resolveYamlPairs,
|
|
5862
|
+
construct: constructYamlPairs
|
|
5517
5863
|
});
|
|
5518
5864
|
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
if (
|
|
5528
|
-
|
|
5529
|
-
}
|
|
5530
|
-
switch (provider) {
|
|
5531
|
-
case 'ollama':
|
|
5532
|
-
return new ChatOllama({
|
|
5533
|
-
baseUrl: DEFAULT_OLLAMA_LLM_SERVICE.endpoint,
|
|
5534
|
-
maxConcurrency: config.service.maxConcurrent,
|
|
5535
|
-
model,
|
|
5536
|
-
});
|
|
5537
|
-
case 'openai':
|
|
5538
|
-
default:
|
|
5539
|
-
const openAiModel = new ChatOpenAI({
|
|
5540
|
-
openAIApiKey: getApiKeyForModel(config),
|
|
5541
|
-
model,
|
|
5542
|
-
temperature: config.service.temperature || 0.2,
|
|
5543
|
-
});
|
|
5544
|
-
return openAiModel;
|
|
5865
|
+
var _hasOwnProperty$2 = Object.prototype.hasOwnProperty;
|
|
5866
|
+
|
|
5867
|
+
function resolveYamlSet(data) {
|
|
5868
|
+
if (data === null) return true;
|
|
5869
|
+
|
|
5870
|
+
var key, object = data;
|
|
5871
|
+
|
|
5872
|
+
for (key in object) {
|
|
5873
|
+
if (_hasOwnProperty$2.call(object, key)) {
|
|
5874
|
+
if (object[key] !== null) return false;
|
|
5545
5875
|
}
|
|
5876
|
+
}
|
|
5877
|
+
|
|
5878
|
+
return true;
|
|
5546
5879
|
}
|
|
5547
5880
|
|
|
5548
|
-
function
|
|
5549
|
-
|
|
5550
|
-
throw new Error('Must provide either a template or a fallback');
|
|
5551
|
-
return (template
|
|
5552
|
-
? new PromptTemplate({
|
|
5553
|
-
template,
|
|
5554
|
-
inputVariables: variables,
|
|
5555
|
-
})
|
|
5556
|
-
: fallback);
|
|
5881
|
+
function constructYamlSet(data) {
|
|
5882
|
+
return data !== null ? data : {};
|
|
5557
5883
|
}
|
|
5558
5884
|
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5885
|
+
var set = new type('tag:yaml.org,2002:set', {
|
|
5886
|
+
kind: 'mapping',
|
|
5887
|
+
resolve: resolveYamlSet,
|
|
5888
|
+
construct: constructYamlSet
|
|
5889
|
+
});
|
|
5890
|
+
|
|
5891
|
+
core.extend({
|
|
5892
|
+
implicit: [
|
|
5893
|
+
timestamp,
|
|
5894
|
+
merge
|
|
5895
|
+
],
|
|
5896
|
+
explicit: [
|
|
5897
|
+
binary,
|
|
5898
|
+
omap,
|
|
5899
|
+
pairs,
|
|
5900
|
+
set
|
|
5901
|
+
]
|
|
5902
|
+
});
|
|
5903
|
+
|
|
5904
|
+
function simpleEscapeSequence(c) {
|
|
5905
|
+
/* eslint-disable indent */
|
|
5906
|
+
return (c === 0x30/* 0 */) ? '\x00' :
|
|
5907
|
+
(c === 0x61/* a */) ? '\x07' :
|
|
5908
|
+
(c === 0x62/* b */) ? '\x08' :
|
|
5909
|
+
(c === 0x74/* t */) ? '\x09' :
|
|
5910
|
+
(c === 0x09/* Tab */) ? '\x09' :
|
|
5911
|
+
(c === 0x6E/* n */) ? '\x0A' :
|
|
5912
|
+
(c === 0x76/* v */) ? '\x0B' :
|
|
5913
|
+
(c === 0x66/* f */) ? '\x0C' :
|
|
5914
|
+
(c === 0x72/* r */) ? '\x0D' :
|
|
5915
|
+
(c === 0x65/* e */) ? '\x1B' :
|
|
5916
|
+
(c === 0x20/* Space */) ? ' ' :
|
|
5917
|
+
(c === 0x22/* " */) ? '\x22' :
|
|
5918
|
+
(c === 0x2F/* / */) ? '/' :
|
|
5919
|
+
(c === 0x5C/* \ */) ? '\x5C' :
|
|
5920
|
+
(c === 0x4E/* N */) ? '\x85' :
|
|
5921
|
+
(c === 0x5F/* _ */) ? '\xA0' :
|
|
5922
|
+
(c === 0x4C/* L */) ? '\u2028' :
|
|
5923
|
+
(c === 0x50/* P */) ? '\u2029' : '';
|
|
5924
|
+
}
|
|
5925
|
+
|
|
5926
|
+
var simpleEscapeCheck = new Array(256); // integer, for fast access
|
|
5927
|
+
var simpleEscapeMap = new Array(256);
|
|
5928
|
+
for (var i = 0; i < 256; i++) {
|
|
5929
|
+
simpleEscapeCheck[i] = simpleEscapeSequence(i) ? 1 : 0;
|
|
5930
|
+
simpleEscapeMap[i] = simpleEscapeSequence(i);
|
|
5601
5931
|
}
|
|
5602
5932
|
|
|
5603
5933
|
/**
|
|
5604
|
-
*
|
|
5605
|
-
*
|
|
5606
|
-
* @param
|
|
5607
|
-
* @
|
|
5608
|
-
* @returns The summary text for the file change.
|
|
5609
|
-
* @throws Error if the file type is invalid.
|
|
5934
|
+
* Get Summarization Chain
|
|
5935
|
+
* @param model
|
|
5936
|
+
* @param options
|
|
5937
|
+
* @returns
|
|
5610
5938
|
*/
|
|
5611
|
-
function
|
|
5612
|
-
|
|
5613
|
-
let filePath;
|
|
5614
|
-
if ('path' in file) {
|
|
5615
|
-
filePath = file.path;
|
|
5616
|
-
}
|
|
5617
|
-
else if ('file' in file) {
|
|
5618
|
-
filePath = change?.filePath || file.file;
|
|
5619
|
-
}
|
|
5620
|
-
else {
|
|
5621
|
-
throw new Error('Invalid file type');
|
|
5622
|
-
}
|
|
5623
|
-
if (change.oldFilePath) {
|
|
5624
|
-
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
5625
|
-
}
|
|
5626
|
-
return `${status}: ${filePath}`;
|
|
5939
|
+
function getSummarizationChain(model, options = { type: 'map_reduce' }) {
|
|
5940
|
+
return loadSummarizationChain(model, options);
|
|
5627
5941
|
}
|
|
5628
5942
|
|
|
5629
5943
|
/**
|
|
5630
|
-
*
|
|
5944
|
+
* Get Recursive Character Text Splitter
|
|
5631
5945
|
*
|
|
5632
|
-
* @param
|
|
5633
|
-
* @returns
|
|
5946
|
+
* @param options
|
|
5947
|
+
* @returns
|
|
5634
5948
|
*/
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
const staged = [];
|
|
5638
|
-
const unstaged = [];
|
|
5639
|
-
const untracked = [];
|
|
5640
|
-
const status = await git.status();
|
|
5641
|
-
status.files.forEach((file) => {
|
|
5642
|
-
const fileChange = {
|
|
5643
|
-
filePath: file.path,
|
|
5644
|
-
oldFilePath: status.renamed.filter((renamed) => renamed.to === file.path)[0]?.from,
|
|
5645
|
-
};
|
|
5646
|
-
// Unstaged files
|
|
5647
|
-
if (file.working_dir !== '?' && file.working_dir !== ' ') {
|
|
5648
|
-
fileChange.status = getStatus(file, 'working_dir');
|
|
5649
|
-
fileChange.summary = getSummaryText(file, fileChange);
|
|
5650
|
-
unstaged.push(fileChange);
|
|
5651
|
-
}
|
|
5652
|
-
// Staged files
|
|
5653
|
-
if (file.index !== ' ' && file.index !== '?') {
|
|
5654
|
-
fileChange.status = getStatus(file);
|
|
5655
|
-
fileChange.summary = getSummaryText(file, fileChange);
|
|
5656
|
-
staged.push(fileChange);
|
|
5657
|
-
}
|
|
5658
|
-
// Untracked files
|
|
5659
|
-
if (file.working_dir === '?' && file.index === '?') {
|
|
5660
|
-
fileChange.status = 'added';
|
|
5661
|
-
fileChange.summary = getSummaryText(file, fileChange);
|
|
5662
|
-
untracked.push(fileChange);
|
|
5663
|
-
}
|
|
5664
|
-
});
|
|
5665
|
-
const ignoredExtensionsSet = new Set(ignoredExtensions.map((extension) => extension.toLowerCase()));
|
|
5666
|
-
const filteredStaged = staged.filter((file) => {
|
|
5667
|
-
const extension = path__default.extname(file.filePath).toLowerCase();
|
|
5668
|
-
return (!ignoredExtensionsSet.has(extension) &&
|
|
5669
|
-
!ignoredFiles.some((ignoredPattern) => minimatch(file.filePath, ignoredPattern)));
|
|
5670
|
-
});
|
|
5671
|
-
const filteredUnstaged = unstaged.filter((file) => {
|
|
5672
|
-
const extension = path__default.extname(file.filePath).toLowerCase();
|
|
5673
|
-
return (!ignoredExtensionsSet.has(extension) &&
|
|
5674
|
-
!ignoredFiles.some((ignoredPattern) => minimatch(file.filePath, ignoredPattern)));
|
|
5675
|
-
});
|
|
5676
|
-
const filteredUntracked = untracked.filter((file) => {
|
|
5677
|
-
const extension = path__default.extname(file.filePath).toLowerCase();
|
|
5678
|
-
return (!ignoredExtensionsSet.has(extension) &&
|
|
5679
|
-
!ignoredFiles.some((ignoredPattern) => minimatch(file.filePath, ignoredPattern)));
|
|
5680
|
-
});
|
|
5681
|
-
return {
|
|
5682
|
-
staged: filteredStaged,
|
|
5683
|
-
unstaged: filteredUnstaged,
|
|
5684
|
-
untracked: filteredUntracked,
|
|
5685
|
-
};
|
|
5686
|
-
}
|
|
5687
|
-
|
|
5688
|
-
async function noResult({ git, logger }) {
|
|
5689
|
-
const { staged, unstaged, untracked } = await getChanges({ git });
|
|
5690
|
-
const hasStaged = staged && staged.length > 0;
|
|
5691
|
-
const hasUnstaged = unstaged && unstaged.length > 0;
|
|
5692
|
-
const hasUntracked = untracked && untracked.length > 0;
|
|
5693
|
-
if (hasStaged) {
|
|
5694
|
-
logger.log(`Staged files detected, but no summary generated...`, { color: 'red' });
|
|
5695
|
-
logger.log(`Files are likely either:\n • changed files are ignored\n • file diff is too large.`, { color: 'yellow' });
|
|
5696
|
-
}
|
|
5697
|
-
else if (hasUnstaged || hasUntracked) {
|
|
5698
|
-
logger.log('Forget something? No staged changes found... 👻', { color: 'red' });
|
|
5699
|
-
if (hasUnstaged) {
|
|
5700
|
-
logger.log('\nChanges not staged for commit:', { color: 'yellow' });
|
|
5701
|
-
logger.verbose(`\t${unstaged.map(({ summary }) => summary).join('\n\t')}`, {
|
|
5702
|
-
color: 'red',
|
|
5703
|
-
});
|
|
5704
|
-
}
|
|
5705
|
-
if (hasUntracked) {
|
|
5706
|
-
logger.log('\nUntracked changes:', { color: 'yellow' });
|
|
5707
|
-
logger.verbose(`\t${untracked.map(({ summary }) => summary).join('\n\t')}`, {
|
|
5708
|
-
color: 'red',
|
|
5709
|
-
});
|
|
5710
|
-
}
|
|
5711
|
-
}
|
|
5712
|
-
else {
|
|
5713
|
-
logger.log('No repo changes detected. 👀', { color: 'blue' });
|
|
5714
|
-
}
|
|
5949
|
+
function getTextSplitter(options = {}) {
|
|
5950
|
+
return new RecursiveCharacterTextSplitter(options);
|
|
5715
5951
|
}
|
|
5716
5952
|
|
|
5717
5953
|
/**
|
|
5718
|
-
*
|
|
5954
|
+
* Parses the default file diff for a given nodeFile.
|
|
5719
5955
|
*
|
|
5720
|
-
* @param
|
|
5721
|
-
* @param
|
|
5722
|
-
* @
|
|
5956
|
+
* @param nodeFile - The file change object.
|
|
5957
|
+
* @param commit - The commit to diff against. Defaults to '--staged'.
|
|
5958
|
+
* @param git - The SimpleGit instance.
|
|
5959
|
+
* @returns A Promise that resolves to the file diff as a string.
|
|
5723
5960
|
*/
|
|
5724
|
-
function
|
|
5725
|
-
if (
|
|
5726
|
-
return
|
|
5727
|
-
}
|
|
5728
|
-
if (!inputVariables.some((entry) => text.includes(entry))) {
|
|
5729
|
-
return ('Prompt template must include at least one of the following input variables: ' +
|
|
5730
|
-
inputVariables.map((value) => `{${value}}`).join(', '));
|
|
5731
|
-
}
|
|
5732
|
-
return true;
|
|
5733
|
-
}
|
|
5734
|
-
|
|
5735
|
-
async function editPrompt(options) {
|
|
5736
|
-
return await editor({
|
|
5737
|
-
message: 'Edit the prompt',
|
|
5738
|
-
default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
|
|
5739
|
-
waitForUseInput: false,
|
|
5740
|
-
postfix: 'Press ENTER to continue',
|
|
5741
|
-
validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
|
|
5742
|
-
});
|
|
5743
|
-
}
|
|
5744
|
-
|
|
5745
|
-
async function editResult(result, options) {
|
|
5746
|
-
if (options.openInEditor) {
|
|
5747
|
-
return await editor({
|
|
5748
|
-
message: 'Edit the commit message',
|
|
5749
|
-
default: result,
|
|
5750
|
-
waitForUseInput: false,
|
|
5751
|
-
validate: (text) => (text ? true : 'Commit message cannot be empty'),
|
|
5752
|
-
});
|
|
5753
|
-
}
|
|
5754
|
-
return result;
|
|
5755
|
-
}
|
|
5756
|
-
|
|
5757
|
-
async function getUserReviewDecision({ label, descriptions, enableRetry = true, enableFullRetry = true, enableModifyPrompt = true, }) {
|
|
5758
|
-
const choices = [
|
|
5759
|
-
{
|
|
5760
|
-
name: '✨ Looks good!',
|
|
5761
|
-
value: 'approve',
|
|
5762
|
-
description: descriptions?.approve || `Continue with the generated ${label}`,
|
|
5763
|
-
},
|
|
5764
|
-
{
|
|
5765
|
-
name: '📝 Edit',
|
|
5766
|
-
value: 'edit',
|
|
5767
|
-
description: descriptions?.edit || `Edit the generated ${label} before proceeding`,
|
|
5768
|
-
},
|
|
5769
|
-
];
|
|
5770
|
-
if (enableModifyPrompt) {
|
|
5771
|
-
choices.push({
|
|
5772
|
-
name: '🪶 Modify Prompt',
|
|
5773
|
-
value: 'modifyPrompt',
|
|
5774
|
-
description: descriptions?.modifyPrompt || `Modify the prompt template and regenerate the ${label}`,
|
|
5775
|
-
});
|
|
5776
|
-
}
|
|
5777
|
-
if (enableRetry) {
|
|
5778
|
-
choices.push({
|
|
5779
|
-
name: '🔄 Retry',
|
|
5780
|
-
value: 'retryMessageOnly',
|
|
5781
|
-
description: descriptions?.retryMessageOnly ||
|
|
5782
|
-
`Restart the function execution from generating the ${label}`,
|
|
5783
|
-
});
|
|
5784
|
-
}
|
|
5785
|
-
if (enableFullRetry) {
|
|
5786
|
-
choices.push({
|
|
5787
|
-
name: '🔄 Retry Full',
|
|
5788
|
-
value: 'retryFull',
|
|
5789
|
-
description: descriptions?.retryFull ||
|
|
5790
|
-
`Restart the function execution from the beginning, regenerating both the summary and ${label}`,
|
|
5791
|
-
});
|
|
5961
|
+
async function parseDefaultFileDiff(nodeFile, commit = '--staged', git) {
|
|
5962
|
+
if (commit !== '--staged') {
|
|
5963
|
+
return await git.diff([`${commit}~1..${commit}`, '--', nodeFile.filePath]);
|
|
5792
5964
|
}
|
|
5793
|
-
|
|
5794
|
-
name: '💣 Cancel',
|
|
5795
|
-
value: 'cancel',
|
|
5796
|
-
description: descriptions?.cancel || `Cancel the ${label}`,
|
|
5797
|
-
});
|
|
5798
|
-
return (await select({
|
|
5799
|
-
message: `Would you like to make any changes to the ${label}?`,
|
|
5800
|
-
choices,
|
|
5801
|
-
}));
|
|
5802
|
-
}
|
|
5803
|
-
|
|
5804
|
-
function logResult(label, result) {
|
|
5805
|
-
console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
|
|
5965
|
+
return await git.diff([commit, nodeFile.filePath]);
|
|
5806
5966
|
}
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5967
|
+
/**
|
|
5968
|
+
* Parses the diff for a renamed file.
|
|
5969
|
+
*
|
|
5970
|
+
* @param nodeFile - The file change object.
|
|
5971
|
+
* @param commit - The commit hash or '--staged'.
|
|
5972
|
+
* @param git - The SimpleGit instance.
|
|
5973
|
+
* @param logger - The logger instance.
|
|
5974
|
+
* @returns A Promise that resolves to the diff string.
|
|
5975
|
+
*/
|
|
5976
|
+
async function parseRenamedFileDiff(nodeFile, commit, git, logger) {
|
|
5813
5977
|
let result = '';
|
|
5814
|
-
const
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
if (!context.length) {
|
|
5821
|
-
context = await parser(changes, result, options);
|
|
5822
|
-
}
|
|
5823
|
-
// if we still don't have a context, bail.
|
|
5824
|
-
if (!context.length) {
|
|
5825
|
-
await noResult(options);
|
|
5826
|
-
}
|
|
5827
|
-
if (modifyPrompt) {
|
|
5828
|
-
options.prompt = await editPrompt(options);
|
|
5978
|
+
const oldFilePath = nodeFile?.oldFilePath || nodeFile.filePath;
|
|
5979
|
+
let previousCommitHash = 'HEAD';
|
|
5980
|
+
let newCommitHash = '';
|
|
5981
|
+
if (commit !== '--staged') {
|
|
5982
|
+
try {
|
|
5983
|
+
previousCommitHash = await git.revparse([`${commit}~1`]);
|
|
5829
5984
|
}
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
});
|
|
5833
|
-
result = await agent(context, options);
|
|
5834
|
-
if (!result) {
|
|
5835
|
-
logger.stopSpinner('💀 Agent failed to return content.', {
|
|
5836
|
-
mode: 'fail',
|
|
5985
|
+
catch (err) {
|
|
5986
|
+
logger.verbose(`Error getting previous commit hash for ${nodeFile.filePath}`, {
|
|
5837
5987
|
color: 'red',
|
|
5838
5988
|
});
|
|
5839
|
-
process.exit(0);
|
|
5840
5989
|
}
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
.
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
...(options?.review || {}),
|
|
5990
|
+
newCommitHash = commit;
|
|
5991
|
+
}
|
|
5992
|
+
try {
|
|
5993
|
+
const [previousContent, newContent] = await Promise.all([
|
|
5994
|
+
git.show([`${previousCommitHash}:${oldFilePath}`]),
|
|
5995
|
+
git.show([`${newCommitHash}:${nodeFile.filePath}`]),
|
|
5996
|
+
]);
|
|
5997
|
+
if (previousContent !== newContent) {
|
|
5998
|
+
result = createTwoFilesPatch(oldFilePath, nodeFile.filePath, previousContent, newContent, '', '', {
|
|
5999
|
+
context: 3,
|
|
5852
6000
|
});
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
}
|
|
5856
|
-
if (reviewAnswer === 'edit') {
|
|
5857
|
-
options.openInEditor = true;
|
|
5858
|
-
}
|
|
5859
|
-
if (reviewAnswer === 'retryFull') {
|
|
5860
|
-
context = '';
|
|
5861
|
-
result = '';
|
|
5862
|
-
options.prompt = '';
|
|
5863
|
-
continue;
|
|
5864
|
-
}
|
|
5865
|
-
if (reviewAnswer === 'retryMessageOnly') {
|
|
5866
|
-
modifyPrompt = false;
|
|
5867
|
-
result = '';
|
|
5868
|
-
continue;
|
|
5869
|
-
}
|
|
5870
|
-
if (reviewAnswer === 'modifyPrompt') {
|
|
5871
|
-
modifyPrompt = true;
|
|
5872
|
-
result = '';
|
|
5873
|
-
continue;
|
|
5874
|
-
}
|
|
6001
|
+
// remove the first 4 lines of the patch (they contain the old and new file names)
|
|
6002
|
+
result = result.split('\n').slice(4).join('\n');
|
|
5875
6003
|
}
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
6004
|
+
else {
|
|
6005
|
+
result = 'File contents are unchanged.';
|
|
6006
|
+
}
|
|
6007
|
+
}
|
|
6008
|
+
catch (err) {
|
|
6009
|
+
logger.verbose(`Error comparing file contents for ${nodeFile.filePath}`, { color: 'red' });
|
|
6010
|
+
result = 'Error comparing file contents.';
|
|
5879
6011
|
}
|
|
5880
6012
|
return result;
|
|
5881
6013
|
}
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
6014
|
+
/**
|
|
6015
|
+
* Retrieves the diff for a given file change in a specific commit.
|
|
6016
|
+
* If the file is deleted, it returns a message indicating that the file has been deleted.
|
|
6017
|
+
* If the file is renamed, it parses the renamed file diff and returns it.
|
|
6018
|
+
* Otherwise, it retrieves the default diff from the index and returns it.
|
|
6019
|
+
*
|
|
6020
|
+
* @param nodeFile - The file change object.
|
|
6021
|
+
* @param commit - The commit hash.
|
|
6022
|
+
* @param git - The SimpleGit instance.
|
|
6023
|
+
* @param logger - The logger instance.
|
|
6024
|
+
* @returns A promise that resolves to the diff as a string.
|
|
6025
|
+
*/
|
|
6026
|
+
async function getDiff(nodeFile, commit, { git, logger, }) {
|
|
6027
|
+
if (nodeFile.status === 'deleted') {
|
|
6028
|
+
return 'This file has been deleted.';
|
|
5886
6029
|
}
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
res = await chain.invoke(variables);
|
|
6030
|
+
if (nodeFile.status === 'renamed' && nodeFile.oldFilePath) {
|
|
6031
|
+
const renamedDiff = await parseRenamedFileDiff(nodeFile, commit, git, logger);
|
|
6032
|
+
return renamedDiff;
|
|
5891
6033
|
}
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
6034
|
+
// If not deleted or renamed, get the diff from the index
|
|
6035
|
+
const defaultDiff = await parseDefaultFileDiff(nodeFile, commit, git);
|
|
6036
|
+
return defaultDiff;
|
|
6037
|
+
}
|
|
6038
|
+
|
|
6039
|
+
// Max tokens for GPT-3 is 4096
|
|
6040
|
+
// const MAX_TOKENS_PER_SUMMARY = 4096
|
|
6041
|
+
const MAX_TOKENS_PER_SUMMARY = 12288;
|
|
6042
|
+
async function fileChangeParser({ changes, commit, options: { tokenizer, git, llm: model, logger }, }) {
|
|
6043
|
+
const textSplitter = getTextSplitter({ chunkSize: 10000, chunkOverlap: 250 });
|
|
6044
|
+
// const textSplitter = new TokenTextSplitter({
|
|
6045
|
+
// chunkSize: 10000,
|
|
6046
|
+
// chunkOverlap: 250,
|
|
6047
|
+
// });
|
|
6048
|
+
const summarizationChain = getSummarizationChain(model, {
|
|
6049
|
+
type: 'map_reduce',
|
|
6050
|
+
combineMapPrompt: SUMMARIZE_PROMPT,
|
|
6051
|
+
combinePrompt: SUMMARIZE_PROMPT,
|
|
6052
|
+
});
|
|
6053
|
+
logger.startTimer();
|
|
6054
|
+
const rootTreeNode = createDiffTree(changes);
|
|
6055
|
+
logger.stopTimer('Created file hierarchy');
|
|
6056
|
+
// Collect diffs
|
|
6057
|
+
logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
|
|
6058
|
+
const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, commit, { git, logger }), tokenizer, logger);
|
|
6059
|
+
logger.stopSpinner('Diffs Collected').stopTimer();
|
|
6060
|
+
// Summarize diffs
|
|
6061
|
+
logger.startTimer();
|
|
6062
|
+
const summary = await summarizeDiffs(diffs, {
|
|
6063
|
+
tokenizer,
|
|
6064
|
+
maxTokens: MAX_TOKENS_PER_SUMMARY,
|
|
6065
|
+
textSplitter,
|
|
6066
|
+
chain: summarizationChain,
|
|
6067
|
+
logger,
|
|
6068
|
+
});
|
|
6069
|
+
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
6070
|
+
return summary;
|
|
6071
|
+
}
|
|
6072
|
+
|
|
6073
|
+
/**
|
|
6074
|
+
* Creates a commit with the specified commit message.
|
|
6075
|
+
*
|
|
6076
|
+
* @param message The commit message.
|
|
6077
|
+
* @param git The SimpleGit instance.
|
|
6078
|
+
* @returns A Promise that resolves to the CommitResult.
|
|
6079
|
+
*/
|
|
6080
|
+
async function createCommit(message, git) {
|
|
6081
|
+
return await git.commit(message);
|
|
6082
|
+
}
|
|
6083
|
+
|
|
6084
|
+
/**
|
|
6085
|
+
* Determines the status of a file based on its changes in the Git repository.
|
|
6086
|
+
*
|
|
6087
|
+
* @param file - The file to check the status of.
|
|
6088
|
+
* @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
|
|
6089
|
+
* @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
|
|
6090
|
+
* @throws Error if the file type is invalid.
|
|
6091
|
+
*/
|
|
6092
|
+
function getStatus(file, location = 'index') {
|
|
6093
|
+
if ('index' in file && 'working_dir' in file) {
|
|
6094
|
+
const statusCode = file[location];
|
|
6095
|
+
switch (statusCode) {
|
|
6096
|
+
case 'A':
|
|
6097
|
+
return 'added';
|
|
6098
|
+
case 'D':
|
|
6099
|
+
return 'deleted';
|
|
6100
|
+
case 'M':
|
|
6101
|
+
return 'modified';
|
|
6102
|
+
case 'R':
|
|
6103
|
+
return 'renamed';
|
|
6104
|
+
case '?':
|
|
6105
|
+
return 'untracked';
|
|
6106
|
+
default:
|
|
6107
|
+
return 'unknown';
|
|
5895
6108
|
}
|
|
5896
6109
|
}
|
|
5897
|
-
if (
|
|
5898
|
-
|
|
6110
|
+
else if ('changes' in file && 'binary' in file) {
|
|
6111
|
+
if (file.changes === 0)
|
|
6112
|
+
return 'untracked';
|
|
6113
|
+
if (file.file.includes('=>'))
|
|
6114
|
+
return 'renamed';
|
|
6115
|
+
if (file.deletions === 0 && file.insertions > 0)
|
|
6116
|
+
return 'added';
|
|
6117
|
+
if (file.insertions === 0 && file.deletions > 0)
|
|
6118
|
+
return 'deleted';
|
|
6119
|
+
if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
|
|
6120
|
+
return 'modified';
|
|
6121
|
+
return 'unknown';
|
|
5899
6122
|
}
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
|
|
5905
|
-
};
|
|
6123
|
+
else {
|
|
6124
|
+
throw new Error('Invalid file type');
|
|
6125
|
+
}
|
|
6126
|
+
}
|
|
5906
6127
|
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
process.stdout.write(result, 'utf8');
|
|
5921
|
-
break;
|
|
6128
|
+
/**
|
|
6129
|
+
* Returns the summary text for a file change.
|
|
6130
|
+
*
|
|
6131
|
+
* @param file - The file status or diff result.
|
|
6132
|
+
* @param change - The partial file change object.
|
|
6133
|
+
* @returns The summary text for the file change.
|
|
6134
|
+
* @throws Error if the file type is invalid.
|
|
6135
|
+
*/
|
|
6136
|
+
function getSummaryText(file, change) {
|
|
6137
|
+
const status = change.status || getStatus(file);
|
|
6138
|
+
let filePath;
|
|
6139
|
+
if ('path' in file) {
|
|
6140
|
+
filePath = file.path;
|
|
5922
6141
|
}
|
|
5923
|
-
|
|
6142
|
+
else if ('file' in file) {
|
|
6143
|
+
filePath = change?.filePath || file.file;
|
|
6144
|
+
}
|
|
6145
|
+
else {
|
|
6146
|
+
throw new Error('Invalid file type');
|
|
6147
|
+
}
|
|
6148
|
+
if (change.oldFilePath) {
|
|
6149
|
+
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
6150
|
+
}
|
|
6151
|
+
return `${status}: ${filePath}`;
|
|
5924
6152
|
}
|
|
5925
6153
|
|
|
5926
6154
|
/**
|
|
5927
|
-
* Retrieves the
|
|
5928
|
-
*
|
|
6155
|
+
* Retrieves the changes in the Git repository.
|
|
6156
|
+
*
|
|
6157
|
+
* @param {GetChangesInput} options - The options for retrieving the changes.
|
|
6158
|
+
* @returns {Promise<GetChangesResult>} A promise that resolves to the changes in the Git repository.
|
|
5929
6159
|
*/
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
};
|
|
6160
|
+
async function getChanges({ git, options }) {
|
|
6161
|
+
const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options || {};
|
|
6162
|
+
const staged = [];
|
|
6163
|
+
const unstaged = [];
|
|
6164
|
+
const untracked = [];
|
|
6165
|
+
const status = await git.status();
|
|
6166
|
+
status.files.forEach((file) => {
|
|
6167
|
+
const fileChange = {
|
|
6168
|
+
filePath: file.path,
|
|
6169
|
+
oldFilePath: status.renamed.filter((renamed) => renamed.to === file.path)[0]?.from,
|
|
6170
|
+
};
|
|
6171
|
+
// Unstaged files
|
|
6172
|
+
if (file.working_dir !== '?' && file.working_dir !== ' ') {
|
|
6173
|
+
fileChange.status = getStatus(file, 'working_dir');
|
|
6174
|
+
fileChange.summary = getSummaryText(file, fileChange);
|
|
6175
|
+
unstaged.push(fileChange);
|
|
6176
|
+
}
|
|
6177
|
+
// Staged files
|
|
6178
|
+
if (file.index !== ' ' && file.index !== '?') {
|
|
6179
|
+
fileChange.status = getStatus(file);
|
|
6180
|
+
fileChange.summary = getSummaryText(file, fileChange);
|
|
6181
|
+
staged.push(fileChange);
|
|
6182
|
+
}
|
|
6183
|
+
// Untracked files
|
|
6184
|
+
if (file.working_dir === '?' && file.index === '?') {
|
|
6185
|
+
fileChange.status = 'added';
|
|
6186
|
+
fileChange.summary = getSummaryText(file, fileChange);
|
|
6187
|
+
untracked.push(fileChange);
|
|
6188
|
+
}
|
|
6189
|
+
});
|
|
6190
|
+
const ignoredExtensionsSet = new Set(ignoredExtensions.map((extension) => extension.toLowerCase()));
|
|
6191
|
+
const filteredStaged = staged.filter((file) => {
|
|
6192
|
+
const extension = path__default.extname(file.filePath).toLowerCase();
|
|
6193
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
6194
|
+
!ignoredFiles.some((ignoredPattern) => minimatch(file.filePath, ignoredPattern)));
|
|
6195
|
+
});
|
|
6196
|
+
const filteredUnstaged = unstaged.filter((file) => {
|
|
6197
|
+
const extension = path__default.extname(file.filePath).toLowerCase();
|
|
6198
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
6199
|
+
!ignoredFiles.some((ignoredPattern) => minimatch(file.filePath, ignoredPattern)));
|
|
6200
|
+
});
|
|
6201
|
+
const filteredUntracked = untracked.filter((file) => {
|
|
6202
|
+
const extension = path__default.extname(file.filePath).toLowerCase();
|
|
6203
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
6204
|
+
!ignoredFiles.some((ignoredPattern) => minimatch(file.filePath, ignoredPattern)));
|
|
6205
|
+
});
|
|
6206
|
+
return {
|
|
6207
|
+
staged: filteredStaged,
|
|
6208
|
+
unstaged: filteredUnstaged,
|
|
6209
|
+
untracked: filteredUntracked,
|
|
6210
|
+
};
|
|
6211
|
+
}
|
|
5941
6212
|
|
|
5942
6213
|
/**
|
|
5943
6214
|
* Retrieves a TikToken for the specified model.
|
|
@@ -5961,331 +6232,112 @@ const getTokenCounter = async (modelName) => {
|
|
|
5961
6232
|
});
|
|
5962
6233
|
};
|
|
5963
6234
|
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
if (options.service.authentication.type !== 'None' && !key) {
|
|
5981
|
-
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
5982
|
-
process.exit(1);
|
|
5983
|
-
}
|
|
5984
|
-
const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4');
|
|
5985
|
-
const llm = getLlm(provider, model, options);
|
|
5986
|
-
const INTERACTIVE = isInteractive(options);
|
|
5987
|
-
if (INTERACTIVE) {
|
|
5988
|
-
logger.log(LOGO);
|
|
5989
|
-
}
|
|
5990
|
-
async function factory() {
|
|
5991
|
-
const changes = await getChanges({ git });
|
|
5992
|
-
return changes.staged;
|
|
5993
|
-
}
|
|
5994
|
-
async function parser(changes) {
|
|
5995
|
-
return await fileChangeParser({
|
|
5996
|
-
changes,
|
|
5997
|
-
commit: '--staged',
|
|
5998
|
-
options: { tokenizer, git, llm, logger },
|
|
5999
|
-
});
|
|
6000
|
-
}
|
|
6001
|
-
const commitMsg = await generateAndReviewLoop({
|
|
6002
|
-
label: 'commit message',
|
|
6003
|
-
options: {
|
|
6004
|
-
...options,
|
|
6005
|
-
prompt: options.prompt || COMMIT_PROMPT.template,
|
|
6006
|
-
logger,
|
|
6007
|
-
interactive: INTERACTIVE,
|
|
6008
|
-
review: {
|
|
6009
|
-
descriptions: {
|
|
6010
|
-
approve: `Commit staged changes with generated commit message`,
|
|
6011
|
-
edit: 'Edit the commit message before proceeding',
|
|
6012
|
-
modifyPrompt: 'Modify the prompt template and regenerate the commit message',
|
|
6013
|
-
retryMessageOnly: 'Restart the function execution from generating the commit message',
|
|
6014
|
-
retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
|
|
6015
|
-
},
|
|
6016
|
-
},
|
|
6017
|
-
},
|
|
6018
|
-
factory,
|
|
6019
|
-
parser,
|
|
6020
|
-
agent: async (context, options) => {
|
|
6021
|
-
const parser = new JsonOutputParser();
|
|
6022
|
-
const prompt = getPrompt({
|
|
6023
|
-
template: options.prompt,
|
|
6024
|
-
variables: COMMIT_PROMPT.inputVariables,
|
|
6025
|
-
fallback: COMMIT_PROMPT,
|
|
6026
|
-
});
|
|
6027
|
-
const formatInstructions = "Respond with a valid JSON object, containing two fields: 'title' and 'body'.";
|
|
6028
|
-
const commitMsg = await executeChain({
|
|
6029
|
-
llm,
|
|
6030
|
-
prompt,
|
|
6031
|
-
variables: { summary: context, format_instructions: formatInstructions },
|
|
6032
|
-
parser,
|
|
6033
|
-
});
|
|
6034
|
-
return `${commitMsg.title}\n\n${commitMsg.body}`;
|
|
6035
|
-
},
|
|
6036
|
-
noResult: async () => {
|
|
6037
|
-
await noResult({ git, logger });
|
|
6038
|
-
process.exit(0);
|
|
6039
|
-
},
|
|
6040
|
-
});
|
|
6041
|
-
const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
|
|
6042
|
-
handleResult({
|
|
6043
|
-
result: commitMsg,
|
|
6044
|
-
interactiveHandler: async (result) => {
|
|
6045
|
-
await createCommit(result, git);
|
|
6046
|
-
logSuccess();
|
|
6047
|
-
},
|
|
6048
|
-
mode: MODE,
|
|
6049
|
-
});
|
|
6050
|
-
};
|
|
6051
|
-
|
|
6052
|
-
/**
|
|
6053
|
-
* Command line options via yargs
|
|
6054
|
-
*/
|
|
6055
|
-
const options$2 = {
|
|
6056
|
-
service: { type: 'string', description: 'LLM/Model-Name', choices: ['openai', 'ollama'] },
|
|
6057
|
-
openAIApiKey: {
|
|
6058
|
-
type: 'string',
|
|
6059
|
-
description: 'OpenAI API Key',
|
|
6060
|
-
},
|
|
6061
|
-
tokenLimit: { type: 'number', description: 'Token limit' },
|
|
6062
|
-
prompt: {
|
|
6063
|
-
type: 'string',
|
|
6064
|
-
alias: 'p',
|
|
6065
|
-
description: 'Commit message prompt',
|
|
6066
|
-
},
|
|
6067
|
-
i: {
|
|
6068
|
-
type: 'boolean',
|
|
6069
|
-
alias: 'interactive',
|
|
6070
|
-
description: 'Toggle interactive mode',
|
|
6071
|
-
},
|
|
6072
|
-
s: {
|
|
6073
|
-
type: 'boolean',
|
|
6074
|
-
description: 'Automatically commit staged changes with generated commit message',
|
|
6075
|
-
default: false,
|
|
6076
|
-
},
|
|
6077
|
-
e: {
|
|
6078
|
-
type: 'boolean',
|
|
6079
|
-
alias: 'edit',
|
|
6080
|
-
description: 'Open commit message in editor before proceeding',
|
|
6081
|
-
},
|
|
6082
|
-
summarizePrompt: {
|
|
6083
|
-
type: 'string',
|
|
6084
|
-
description: 'Large file summary prompt',
|
|
6085
|
-
},
|
|
6086
|
-
ignoredFiles: {
|
|
6087
|
-
type: 'array',
|
|
6088
|
-
description: 'Ignored files',
|
|
6089
|
-
},
|
|
6090
|
-
ignoredExtensions: {
|
|
6091
|
-
type: 'array',
|
|
6092
|
-
description: 'Ignored extensions',
|
|
6093
|
-
},
|
|
6094
|
-
};
|
|
6095
|
-
const builder$2 = (yargs) => {
|
|
6096
|
-
return yargs.options(options$2);
|
|
6097
|
-
};
|
|
6098
|
-
|
|
6099
|
-
var commit = {
|
|
6100
|
-
command: 'commit',
|
|
6101
|
-
desc: 'Write a commit message summarizing the staged changes.',
|
|
6102
|
-
builder: builder$2,
|
|
6103
|
-
handler: commandExecutor(handler$2),
|
|
6104
|
-
options: options$2,
|
|
6105
|
-
};
|
|
6106
|
-
|
|
6107
|
-
/**
|
|
6108
|
-
* Retrieves the commit log range between two specified commits.
|
|
6109
|
-
*
|
|
6110
|
-
* @param from - The starting commit.
|
|
6111
|
-
* @param to - The ending commit.
|
|
6112
|
-
* @param options - Additional options for retrieving the commit log range.
|
|
6113
|
-
* @returns A promise that resolves to an array of commit log messages.
|
|
6114
|
-
* @throws If there is an error retrieving the commit log range.
|
|
6115
|
-
*/
|
|
6116
|
-
async function getCommitLogRange(from, to, { noMerges, git }) {
|
|
6117
|
-
try {
|
|
6118
|
-
const logOptions = { from: `${from}^1`, to, '--no-merges': noMerges };
|
|
6119
|
-
const commitLog = await git.log(logOptions);
|
|
6120
|
-
return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n - ${author_name}<${author_email}> (${hash})`);
|
|
6121
|
-
}
|
|
6122
|
-
catch (error) {
|
|
6123
|
-
// If there's an error, handle it appropriately
|
|
6124
|
-
console.error('Error getting commit messages:', error);
|
|
6125
|
-
throw error;
|
|
6126
|
-
}
|
|
6127
|
-
}
|
|
6128
|
-
|
|
6129
|
-
/**
|
|
6130
|
-
* Retrieves the name of the current branch.
|
|
6131
|
-
*
|
|
6132
|
-
* @param {GetCurrentBranchName} options - The options for retrieving the branch name.
|
|
6133
|
-
* @returns {Promise<string>} - A promise that resolves to the name of the current branch.
|
|
6134
|
-
*/
|
|
6135
|
-
async function getCurrentBranchName({ git }) {
|
|
6136
|
-
return await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
6137
|
-
}
|
|
6138
|
-
|
|
6139
|
-
/**
|
|
6140
|
-
* Retrieves the commit log for the current branch.
|
|
6141
|
-
*
|
|
6142
|
-
* @param {Object} options - The options for retrieving the commit log.
|
|
6143
|
-
* @param {SimpleGit} options.git - The SimpleGit instance.
|
|
6144
|
-
* @param {Logger} options.logger - The logger for logging messages.
|
|
6145
|
-
* @param {string} [options.comparisonBranch='main'] - The branch to compare against.
|
|
6146
|
-
* @param {string} [options.comparisonRemote='origin'] - The remote to compare against.
|
|
6147
|
-
* @returns {Promise<string[]>} The array of commit messages in the commit log.
|
|
6148
|
-
*/
|
|
6149
|
-
async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
|
|
6150
|
-
try {
|
|
6151
|
-
// Get the current branch name
|
|
6152
|
-
const branch = await getCurrentBranchName({ git });
|
|
6153
|
-
// Check if the current branch has any commits
|
|
6154
|
-
const hasCommits = (await git.raw(['rev-list', '--count', branch])) !== '0';
|
|
6155
|
-
if (!hasCommits) {
|
|
6156
|
-
logger?.log('No commits on the current branch.');
|
|
6157
|
-
return [];
|
|
6158
|
-
}
|
|
6159
|
-
// Get the list of commits that are unique to the current branch
|
|
6160
|
-
let uniqueCommits;
|
|
6161
|
-
if (comparisonBranch === branch) {
|
|
6162
|
-
// If the comparison branch is the same as the current branch, we compare against the remote.
|
|
6163
|
-
uniqueCommits = (await git.raw(['rev-list', `${comparisonRemote}/${comparisonBranch}..${branch}`]))
|
|
6164
|
-
.split('\n')
|
|
6165
|
-
.filter(Boolean)
|
|
6166
|
-
.reverse();
|
|
6167
|
-
}
|
|
6168
|
-
else {
|
|
6169
|
-
// Your existing code for different branches
|
|
6170
|
-
uniqueCommits = (await git.raw(['rev-list', `${comparisonBranch}..${branch}`]))
|
|
6171
|
-
.split('\n')
|
|
6172
|
-
.filter(Boolean)
|
|
6173
|
-
.reverse();
|
|
6235
|
+
async function noResult$1({ git, logger }) {
|
|
6236
|
+
const { staged, unstaged, untracked } = await getChanges({ git });
|
|
6237
|
+
const hasStaged = staged && staged.length > 0;
|
|
6238
|
+
const hasUnstaged = unstaged && unstaged.length > 0;
|
|
6239
|
+
const hasUntracked = untracked && untracked.length > 0;
|
|
6240
|
+
if (hasStaged) {
|
|
6241
|
+
logger.log(`Staged files detected, but no summary generated...`, { color: 'red' });
|
|
6242
|
+
logger.log(`Files are likely either:\n • changed files are ignored\n • file diff is too large.`, { color: 'yellow' });
|
|
6243
|
+
}
|
|
6244
|
+
else if (hasUnstaged || hasUntracked) {
|
|
6245
|
+
logger.log('Forget something? No staged changes found... 👻', { color: 'red' });
|
|
6246
|
+
if (hasUnstaged) {
|
|
6247
|
+
logger.log('\nChanges not staged for commit:', { color: 'yellow' });
|
|
6248
|
+
logger.verbose(`\t${unstaged.map(({ summary }) => summary).join('\n\t')}`, {
|
|
6249
|
+
color: 'red',
|
|
6250
|
+
});
|
|
6174
6251
|
}
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
return [];
|
|
6252
|
+
if (hasUntracked) {
|
|
6253
|
+
logger.log('\nUntracked changes:', { color: 'yellow' });
|
|
6254
|
+
logger.verbose(`\t${untracked.map(({ summary }) => summary).join('\n\t')}`, {
|
|
6255
|
+
color: 'red',
|
|
6256
|
+
});
|
|
6181
6257
|
}
|
|
6182
|
-
// Retrieve commit log with messages
|
|
6183
|
-
return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
|
|
6184
6258
|
}
|
|
6185
|
-
|
|
6186
|
-
logger
|
|
6259
|
+
else {
|
|
6260
|
+
logger.log('No repo changes detected. 👀', { color: 'blue' });
|
|
6187
6261
|
}
|
|
6188
|
-
return [];
|
|
6189
6262
|
}
|
|
6190
6263
|
|
|
6191
|
-
const
|
|
6192
|
-
|
|
6193
|
-
- Typically a hyphen or asterisk is used for the bullet
|
|
6194
|
-
- Summarize dependency updates
|
|
6195
|
-
|
|
6196
|
-
{format_instructions}
|
|
6197
|
-
|
|
6198
|
-
"""{summary}"""`;
|
|
6199
|
-
const inputVariables = ['format_instructions', 'summary'];
|
|
6200
|
-
const CHANGELOG_PROMPT = new PromptTemplate({
|
|
6201
|
-
template,
|
|
6202
|
-
inputVariables,
|
|
6203
|
-
});
|
|
6204
|
-
|
|
6205
|
-
const handler$1 = async (argv, logger) => {
|
|
6206
|
-
const config = loadConfig(argv);
|
|
6264
|
+
const handler$2 = async (argv, logger) => {
|
|
6207
6265
|
const git = getRepo();
|
|
6266
|
+
const config = loadConfig(argv);
|
|
6208
6267
|
const key = getApiKeyForModel(config);
|
|
6209
6268
|
const { provider, model } = getModelAndProviderFromConfig(config);
|
|
6210
6269
|
if (config.service.authentication.type !== 'None' && !key) {
|
|
6211
6270
|
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
6212
6271
|
process.exit(1);
|
|
6213
6272
|
}
|
|
6273
|
+
const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4');
|
|
6214
6274
|
const llm = getLlm(provider, model, config);
|
|
6215
6275
|
const INTERACTIVE = isInteractive(config);
|
|
6216
6276
|
if (INTERACTIVE) {
|
|
6217
6277
|
logger.log(LOGO);
|
|
6218
6278
|
}
|
|
6219
6279
|
async function factory() {
|
|
6220
|
-
const
|
|
6221
|
-
|
|
6222
|
-
const [from, to] = config.range.split(':');
|
|
6223
|
-
if (!from || !to) {
|
|
6224
|
-
logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
|
|
6225
|
-
process.exit(1);
|
|
6226
|
-
}
|
|
6227
|
-
return {
|
|
6228
|
-
branch: branchName,
|
|
6229
|
-
commits: await getCommitLogRange(from, to, { git, noMerges: true }),
|
|
6230
|
-
};
|
|
6231
|
-
}
|
|
6232
|
-
logger.verbose(`No range provided. Defaulting to current branch`, { color: 'yellow' });
|
|
6233
|
-
return {
|
|
6234
|
-
branch: branchName,
|
|
6235
|
-
commits: await getCommitLogCurrentBranch({ git, logger }),
|
|
6236
|
-
};
|
|
6280
|
+
const changes = await getChanges({ git });
|
|
6281
|
+
return changes.staged;
|
|
6237
6282
|
}
|
|
6238
|
-
async function parser(
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6283
|
+
async function parser(changes) {
|
|
6284
|
+
return await fileChangeParser({
|
|
6285
|
+
changes,
|
|
6286
|
+
commit: '--staged',
|
|
6287
|
+
options: { tokenizer, git, llm, logger },
|
|
6288
|
+
});
|
|
6242
6289
|
}
|
|
6243
|
-
const
|
|
6244
|
-
label: '
|
|
6290
|
+
const commitMsg = await generateAndReviewLoop({
|
|
6291
|
+
label: 'commit message',
|
|
6292
|
+
options: {
|
|
6293
|
+
...config,
|
|
6294
|
+
prompt: config.prompt || COMMIT_PROMPT.template,
|
|
6295
|
+
logger,
|
|
6296
|
+
interactive: INTERACTIVE,
|
|
6297
|
+
review: {
|
|
6298
|
+
descriptions: {
|
|
6299
|
+
approve: `Commit staged changes with generated commit message`,
|
|
6300
|
+
edit: 'Edit the commit message before proceeding',
|
|
6301
|
+
modifyPrompt: 'Modify the prompt template and regenerate the commit message',
|
|
6302
|
+
retryMessageOnly: 'Restart the function execution from generating the commit message',
|
|
6303
|
+
retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
|
|
6304
|
+
},
|
|
6305
|
+
},
|
|
6306
|
+
},
|
|
6245
6307
|
factory,
|
|
6246
6308
|
parser,
|
|
6247
6309
|
agent: async (context, options) => {
|
|
6248
6310
|
const parser = new JsonOutputParser();
|
|
6249
6311
|
const prompt = getPrompt({
|
|
6250
6312
|
template: options.prompt,
|
|
6251
|
-
variables:
|
|
6252
|
-
fallback:
|
|
6313
|
+
variables: COMMIT_PROMPT.inputVariables,
|
|
6314
|
+
fallback: COMMIT_PROMPT,
|
|
6253
6315
|
});
|
|
6254
|
-
const formatInstructions = "Respond with a valid JSON object, containing two fields: '
|
|
6255
|
-
const
|
|
6316
|
+
const formatInstructions = "Respond with a valid JSON object, containing two fields: 'title' and 'body', both strings.";
|
|
6317
|
+
const additionalContext = argv.additional ? `${argv.additional}` : '';
|
|
6318
|
+
const commitMsg = await executeChain({
|
|
6256
6319
|
llm,
|
|
6257
6320
|
prompt,
|
|
6258
6321
|
variables: {
|
|
6259
6322
|
summary: context,
|
|
6260
6323
|
format_instructions: formatInstructions,
|
|
6324
|
+
additional: additionalContext,
|
|
6261
6325
|
},
|
|
6262
6326
|
parser,
|
|
6263
6327
|
});
|
|
6264
|
-
|
|
6265
|
-
return `${
|
|
6328
|
+
const appendedText = argv.append ? `\n\n${argv.append}` : '';
|
|
6329
|
+
return `${commitMsg.title}\n\n${commitMsg.body}${appendedText}`;
|
|
6266
6330
|
},
|
|
6267
6331
|
noResult: async () => {
|
|
6268
|
-
|
|
6269
|
-
logger.log(`No commits found in the provided range.`, { color: 'red' });
|
|
6270
|
-
process.exit(0);
|
|
6271
|
-
}
|
|
6272
|
-
logger.log(`No commits found in the current branch.`, { color: 'red' });
|
|
6332
|
+
await noResult$1({ git, logger });
|
|
6273
6333
|
process.exit(0);
|
|
6274
6334
|
},
|
|
6275
|
-
options: {
|
|
6276
|
-
...config,
|
|
6277
|
-
prompt: config.prompt || CHANGELOG_PROMPT.template,
|
|
6278
|
-
logger,
|
|
6279
|
-
interactive: INTERACTIVE,
|
|
6280
|
-
review: {
|
|
6281
|
-
enableFullRetry: false,
|
|
6282
|
-
},
|
|
6283
|
-
},
|
|
6284
6335
|
});
|
|
6285
6336
|
const MODE = (INTERACTIVE && 'interactive') || (config.commit && 'interactive') || config?.mode || 'stdout';
|
|
6286
6337
|
handleResult({
|
|
6287
|
-
result:
|
|
6288
|
-
interactiveHandler: async () => {
|
|
6338
|
+
result: commitMsg,
|
|
6339
|
+
interactiveHandler: async (result) => {
|
|
6340
|
+
await createCommit(result, git);
|
|
6289
6341
|
logSuccess();
|
|
6290
6342
|
},
|
|
6291
6343
|
mode: MODE,
|
|
@@ -6295,43 +6347,39 @@ const handler$1 = async (argv, logger) => {
|
|
|
6295
6347
|
/**
|
|
6296
6348
|
* Command line options via yargs
|
|
6297
6349
|
*/
|
|
6298
|
-
const options$
|
|
6299
|
-
range: {
|
|
6300
|
-
type: 'string',
|
|
6301
|
-
alias: 'r',
|
|
6302
|
-
description: 'Commit range e.g `HEAD~2:HEAD`',
|
|
6303
|
-
},
|
|
6304
|
-
tokenLimit: { type: 'number', description: 'Token limit' },
|
|
6305
|
-
prompt: {
|
|
6306
|
-
type: 'string',
|
|
6307
|
-
alias: 'p',
|
|
6308
|
-
description: 'Prompt for llm',
|
|
6309
|
-
},
|
|
6350
|
+
const options$2 = {
|
|
6310
6351
|
i: {
|
|
6311
|
-
type: 'boolean',
|
|
6312
6352
|
alias: 'interactive',
|
|
6313
6353
|
description: 'Toggle interactive mode',
|
|
6314
|
-
},
|
|
6315
|
-
e: {
|
|
6316
6354
|
type: 'boolean',
|
|
6317
|
-
alias: 'edit',
|
|
6318
|
-
description: 'Open generated changelog message in editor before proceeding',
|
|
6319
6355
|
},
|
|
6320
|
-
|
|
6356
|
+
ignoredFiles: {
|
|
6357
|
+
description: 'Ignored files',
|
|
6358
|
+
type: 'array',
|
|
6359
|
+
},
|
|
6360
|
+
ignoredExtensions: {
|
|
6361
|
+
description: 'Ignored extensions',
|
|
6362
|
+
type: 'array',
|
|
6363
|
+
},
|
|
6364
|
+
append: {
|
|
6365
|
+
description: 'Add content to the end of the generated commit message',
|
|
6366
|
+
type: 'string',
|
|
6367
|
+
},
|
|
6368
|
+
additional: {
|
|
6369
|
+
description: 'Add extra contextual information to the prompt',
|
|
6321
6370
|
type: 'string',
|
|
6322
|
-
description: 'Prompt for summarizing large files',
|
|
6323
6371
|
},
|
|
6324
6372
|
};
|
|
6325
|
-
const builder$
|
|
6326
|
-
return
|
|
6373
|
+
const builder$2 = (yargsInstance) => {
|
|
6374
|
+
return yargsInstance.options(options$2).usage(getCommandUsageHeader(commit.command));
|
|
6327
6375
|
};
|
|
6328
6376
|
|
|
6329
|
-
var
|
|
6330
|
-
command: '
|
|
6331
|
-
desc: '
|
|
6332
|
-
builder: builder$
|
|
6333
|
-
handler: commandExecutor(handler$
|
|
6334
|
-
options: options$
|
|
6377
|
+
var commit = {
|
|
6378
|
+
command: 'commit',
|
|
6379
|
+
desc: 'Summarize the staged changes in a commit message.',
|
|
6380
|
+
builder: builder$2,
|
|
6381
|
+
handler: commandExecutor(handler$2),
|
|
6382
|
+
options: options$2,
|
|
6335
6383
|
};
|
|
6336
6384
|
|
|
6337
6385
|
/**
|
|
@@ -6637,7 +6685,7 @@ const questions = {
|
|
|
6637
6685
|
}),
|
|
6638
6686
|
};
|
|
6639
6687
|
|
|
6640
|
-
const handler = async (argv, logger) => {
|
|
6688
|
+
const handler$1 = async (argv, logger) => {
|
|
6641
6689
|
const options = loadConfig(argv);
|
|
6642
6690
|
logger.log(LOGO);
|
|
6643
6691
|
let scope = options?.scope;
|
|
@@ -6675,8 +6723,11 @@ const handler = async (argv, logger) => {
|
|
|
6675
6723
|
if (advOptions) {
|
|
6676
6724
|
config.mode = await questions.selectMode();
|
|
6677
6725
|
config.defaultBranch = await questions.selectDefaultGitBranch();
|
|
6678
|
-
config.
|
|
6679
|
-
|
|
6726
|
+
config.service = {
|
|
6727
|
+
...config.service,
|
|
6728
|
+
temperature: await questions.inputModelTemperature(),
|
|
6729
|
+
tokenLimit: await questions.inputTokenLimit(),
|
|
6730
|
+
};
|
|
6680
6731
|
config.verbose = await questions.enableVerboseMode();
|
|
6681
6732
|
const promptForIgnores = await confirm({
|
|
6682
6733
|
message: 'would you like to configure ignored files and extensions?',
|
|
@@ -6740,20 +6791,217 @@ const handler = async (argv, logger) => {
|
|
|
6740
6791
|
/**
|
|
6741
6792
|
* Command line options via yargs
|
|
6742
6793
|
*/
|
|
6743
|
-
const options = {
|
|
6794
|
+
const options$1 = {
|
|
6744
6795
|
scope: {
|
|
6745
6796
|
type: 'string',
|
|
6746
6797
|
description: 'configure coco for the current user or project?',
|
|
6747
6798
|
choices: ['global', 'project'],
|
|
6748
6799
|
},
|
|
6749
6800
|
};
|
|
6750
|
-
const builder = (yargs) => {
|
|
6751
|
-
return yargs.options(options);
|
|
6801
|
+
const builder$1 = (yargs) => {
|
|
6802
|
+
return yargs.options(options$1).usage(getCommandUsageHeader(init.command));
|
|
6752
6803
|
};
|
|
6753
6804
|
|
|
6754
6805
|
var init = {
|
|
6755
6806
|
command: 'init',
|
|
6756
6807
|
desc: 'install & configure coco globally or for the current project',
|
|
6808
|
+
builder: builder$1,
|
|
6809
|
+
handler: commandExecutor(handler$1),
|
|
6810
|
+
options: options$1,
|
|
6811
|
+
};
|
|
6812
|
+
|
|
6813
|
+
/**
|
|
6814
|
+
* Formats a commit log into a readable string format.
|
|
6815
|
+
*
|
|
6816
|
+
* @param commitLog - The commit log result containing an array of commit details.
|
|
6817
|
+
* @returns An array of formatted commit log strings.
|
|
6818
|
+
*
|
|
6819
|
+
* Each formatted string includes:
|
|
6820
|
+
* - The date of the commit in square brackets.
|
|
6821
|
+
* - The commit message.
|
|
6822
|
+
* - The commit body.
|
|
6823
|
+
* - The commit hash in parentheses.
|
|
6824
|
+
* - The author's name and email in angle brackets.
|
|
6825
|
+
*/
|
|
6826
|
+
const formatCommitLog = (commitLog) => {
|
|
6827
|
+
return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
|
|
6828
|
+
};
|
|
6829
|
+
|
|
6830
|
+
const getChangesByTimestamp = async ({ since, git }) => {
|
|
6831
|
+
const commitLog = await git.log({ '--since': since });
|
|
6832
|
+
return formatCommitLog(commitLog);
|
|
6833
|
+
};
|
|
6834
|
+
|
|
6835
|
+
const getChangesSinceLastTag = async ({ git }) => {
|
|
6836
|
+
const tags = await git.tags();
|
|
6837
|
+
if (tags.all.length > 0) {
|
|
6838
|
+
const lastTag = tags.latest;
|
|
6839
|
+
const commitLog = await git.log({ from: lastTag });
|
|
6840
|
+
return formatCommitLog(commitLog);
|
|
6841
|
+
}
|
|
6842
|
+
else {
|
|
6843
|
+
return ['No tags found in the repository.'];
|
|
6844
|
+
}
|
|
6845
|
+
};
|
|
6846
|
+
|
|
6847
|
+
async function noResult({ logger }) {
|
|
6848
|
+
logger.log('No repo changes detected. 👀', { color: 'blue' });
|
|
6849
|
+
}
|
|
6850
|
+
|
|
6851
|
+
const template = `Following the formatting instructions, summarize the following changes in the underlying git repository/branch.
|
|
6852
|
+
The summarization should descibe in a general sense what has changed in the repository over the specified timeframe. Specific files can be mentioned, but the summary should be general enough to be useful to someone who has not seen the changes.
|
|
6853
|
+
|
|
6854
|
+
Breaking down the changes into categories (e.g. bug fixes, new features, etc.) with markdown headings is encouraged.
|
|
6855
|
+
|
|
6856
|
+
{timeframe}
|
|
6857
|
+
|
|
6858
|
+
{format_instructions}
|
|
6859
|
+
|
|
6860
|
+
"""{changes}"""`;
|
|
6861
|
+
const inputVariables = ['format_instructions', 'changes', 'timeframe'];
|
|
6862
|
+
const RECAP_PROMPT = new PromptTemplate({
|
|
6863
|
+
template,
|
|
6864
|
+
inputVariables,
|
|
6865
|
+
});
|
|
6866
|
+
|
|
6867
|
+
const handler = async (argv, logger) => {
|
|
6868
|
+
const git = getRepo();
|
|
6869
|
+
const config = loadConfig(argv);
|
|
6870
|
+
const key = getApiKeyForModel(config);
|
|
6871
|
+
const { provider, model } = getModelAndProviderFromConfig(config);
|
|
6872
|
+
if (config.service.authentication.type !== 'None' && !key) {
|
|
6873
|
+
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
6874
|
+
process.exit(1);
|
|
6875
|
+
}
|
|
6876
|
+
const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4');
|
|
6877
|
+
const llm = getLlm(provider, model, config);
|
|
6878
|
+
const INTERACTIVE = isInteractive(config);
|
|
6879
|
+
if (INTERACTIVE) {
|
|
6880
|
+
logger.log(LOGO);
|
|
6881
|
+
}
|
|
6882
|
+
const { 'last-month': lastMonth, 'last-tag': lastTag, yesterday, 'last-week': lastWeek } = argv;
|
|
6883
|
+
const timeframe = lastMonth ? 'last-month' : lastTag ? 'last-tag' : yesterday ? 'yesterday' : lastWeek ? 'last-week' : 'current';
|
|
6884
|
+
logger.log(`Generating recap for timeframe: ${timeframe}`);
|
|
6885
|
+
async function factory() {
|
|
6886
|
+
switch (timeframe) {
|
|
6887
|
+
case 'current':
|
|
6888
|
+
const { staged, unstaged, untracked } = await getChanges({ git });
|
|
6889
|
+
logger.log(`Staged: ${staged.length}, Unstaged: ${unstaged?.length || 0}, Untracked: ${untracked?.length || 0}`);
|
|
6890
|
+
const result = await fileChangeParser({
|
|
6891
|
+
changes: [...staged, ...(unstaged ? unstaged : []), ...(untracked ? untracked : [])],
|
|
6892
|
+
commit: 'HEAD',
|
|
6893
|
+
options: { tokenizer, git, llm, logger },
|
|
6894
|
+
});
|
|
6895
|
+
logger.log(`Parsed Result: ${result}`);
|
|
6896
|
+
return [result];
|
|
6897
|
+
case 'yesterday':
|
|
6898
|
+
const yesterday = new Date();
|
|
6899
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
6900
|
+
return await getChangesByTimestamp({ git, since: yesterday.toISOString().split('T')[0] });
|
|
6901
|
+
case 'last-week':
|
|
6902
|
+
const lastWeek = new Date();
|
|
6903
|
+
lastWeek.setDate(lastWeek.getDate() - 7);
|
|
6904
|
+
return await getChangesByTimestamp({ git, since: lastWeek.toISOString().split('T')[0] });
|
|
6905
|
+
case 'last-month':
|
|
6906
|
+
const lastMonth = new Date();
|
|
6907
|
+
lastMonth.setMonth(lastMonth.getMonth() - 1);
|
|
6908
|
+
return await getChangesByTimestamp({ git, since: lastMonth.toISOString().split('T')[0] });
|
|
6909
|
+
case 'last-tag':
|
|
6910
|
+
const tags = await getChangesSinceLastTag({ git });
|
|
6911
|
+
return tags;
|
|
6912
|
+
default:
|
|
6913
|
+
logger.log(`Invalid timeframe: ${timeframe}`, { color: 'red' });
|
|
6914
|
+
return [];
|
|
6915
|
+
}
|
|
6916
|
+
}
|
|
6917
|
+
async function parser(changes) {
|
|
6918
|
+
console.log({ changes });
|
|
6919
|
+
return changes.join('\n');
|
|
6920
|
+
}
|
|
6921
|
+
const recap = await generateAndReviewLoop({
|
|
6922
|
+
label: 'recap',
|
|
6923
|
+
options: {
|
|
6924
|
+
...config,
|
|
6925
|
+
prompt: config.prompt || RECAP_PROMPT.template,
|
|
6926
|
+
logger,
|
|
6927
|
+
interactive: INTERACTIVE,
|
|
6928
|
+
review: {
|
|
6929
|
+
descriptions: {
|
|
6930
|
+
approve: `Commit staged changes with generated commit message`,
|
|
6931
|
+
edit: 'Edit the commit message before proceeding',
|
|
6932
|
+
modifyPrompt: 'Modify the prompt template and regenerate the commit message',
|
|
6933
|
+
retryMessageOnly: 'Restart the function execution from generating the commit message',
|
|
6934
|
+
retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
|
|
6935
|
+
},
|
|
6936
|
+
},
|
|
6937
|
+
},
|
|
6938
|
+
factory,
|
|
6939
|
+
parser,
|
|
6940
|
+
agent: async (context, options) => {
|
|
6941
|
+
const parser = new JsonOutputParser();
|
|
6942
|
+
const formatInstructions = "Respond with a valid JSON object, containing one field: 'summary', a string.";
|
|
6943
|
+
const prompt = getPrompt({
|
|
6944
|
+
template: options.prompt,
|
|
6945
|
+
variables: RECAP_PROMPT.inputVariables,
|
|
6946
|
+
fallback: RECAP_PROMPT,
|
|
6947
|
+
});
|
|
6948
|
+
const response = await executeChain({
|
|
6949
|
+
llm,
|
|
6950
|
+
prompt,
|
|
6951
|
+
variables: {
|
|
6952
|
+
changes: context,
|
|
6953
|
+
format_instructions: formatInstructions,
|
|
6954
|
+
timeframe,
|
|
6955
|
+
},
|
|
6956
|
+
parser,
|
|
6957
|
+
});
|
|
6958
|
+
logger.log(response.summary || 'no response');
|
|
6959
|
+
return `${response.summary}`;
|
|
6960
|
+
},
|
|
6961
|
+
noResult: async () => {
|
|
6962
|
+
await noResult({ git, logger });
|
|
6963
|
+
process.exit(0);
|
|
6964
|
+
},
|
|
6965
|
+
});
|
|
6966
|
+
logger.log(`Recap generated: ${recap}`, { color: 'green' });
|
|
6967
|
+
};
|
|
6968
|
+
|
|
6969
|
+
/**
|
|
6970
|
+
* Command line options via yargs
|
|
6971
|
+
*/
|
|
6972
|
+
const options = {
|
|
6973
|
+
yesterday: {
|
|
6974
|
+
type: 'boolean',
|
|
6975
|
+
description: 'Recap for yesterday',
|
|
6976
|
+
},
|
|
6977
|
+
"last-week": {
|
|
6978
|
+
alias: 'week',
|
|
6979
|
+
type: 'boolean',
|
|
6980
|
+
description: 'Recap for last week',
|
|
6981
|
+
},
|
|
6982
|
+
"last-month": {
|
|
6983
|
+
alias: 'month',
|
|
6984
|
+
type: 'boolean',
|
|
6985
|
+
description: 'Recap for last month',
|
|
6986
|
+
},
|
|
6987
|
+
"last-tag": {
|
|
6988
|
+
alias: 'tag',
|
|
6989
|
+
type: 'boolean',
|
|
6990
|
+
description: 'Recap for last tag',
|
|
6991
|
+
},
|
|
6992
|
+
i: {
|
|
6993
|
+
type: 'boolean',
|
|
6994
|
+
alias: 'interactive',
|
|
6995
|
+
description: 'Toggle interactive mode',
|
|
6996
|
+
},
|
|
6997
|
+
};
|
|
6998
|
+
const builder = (yargsInstance) => {
|
|
6999
|
+
return yargsInstance.options(options).usage(getCommandUsageHeader(recap.command));
|
|
7000
|
+
};
|
|
7001
|
+
|
|
7002
|
+
var recap = {
|
|
7003
|
+
command: 'recap',
|
|
7004
|
+
desc: 'Summarize the changes in the repository over a specified timeframe.',
|
|
6757
7005
|
builder,
|
|
6758
7006
|
handler: commandExecutor(handler),
|
|
6759
7007
|
options,
|
|
@@ -6764,22 +7012,11 @@ var types = /*#__PURE__*/Object.freeze({
|
|
|
6764
7012
|
});
|
|
6765
7013
|
|
|
6766
7014
|
const y = yargs();
|
|
6767
|
-
y.scriptName('coco')
|
|
6768
|
-
y.command([commit.command, '$0'], commit.desc,
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
6776
|
-
// @ts-ignore
|
|
6777
|
-
changelog.builder, changelog.handler).options(changelog.options);
|
|
6778
|
-
y.command(init.command, init.desc,
|
|
6779
|
-
// TODO: fix type on builder
|
|
6780
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
6781
|
-
// @ts-ignore
|
|
6782
|
-
init.builder, init.handler).options(init.options);
|
|
6783
|
-
y.parse(process.argv.slice(2));
|
|
6784
|
-
|
|
6785
|
-
export { changelog, commit, init, types };
|
|
7015
|
+
y.scriptName('coco');
|
|
7016
|
+
y.command([commit.command, '$0'], commit.desc, commit.builder, commit.handler);
|
|
7017
|
+
y.command(changelog.command, changelog.desc, changelog.builder, changelog.handler);
|
|
7018
|
+
y.command(recap.command, recap.desc, recap.builder, recap.handler);
|
|
7019
|
+
y.command(init.command, init.desc, init.builder, init.handler);
|
|
7020
|
+
y.help().parse(process.argv.slice(2));
|
|
7021
|
+
|
|
7022
|
+
export { changelog, commit, init, recap, types };
|