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.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var prompts = require('@langchain/core/prompts');
|
|
4
|
+
var prompts$1 = require('@langchain/core/prompts');
|
|
5
5
|
var example_selectors = require('@langchain/core/example_selectors');
|
|
6
6
|
var yargs = require('yargs');
|
|
7
|
-
var fs = require('fs');
|
|
8
|
-
var prompts$1 = require('@inquirer/prompts');
|
|
9
7
|
var chalk = require('chalk');
|
|
8
|
+
var fs = require('fs');
|
|
9
|
+
var prompts = require('@inquirer/prompts');
|
|
10
10
|
var ini = require('ini');
|
|
11
11
|
var os = require('os');
|
|
12
12
|
var path = require('path');
|
|
@@ -14,25 +14,25 @@ var Ajv = require('ajv');
|
|
|
14
14
|
var ora = require('ora');
|
|
15
15
|
var now = require('performance-now');
|
|
16
16
|
var prettyMilliseconds = require('pretty-ms');
|
|
17
|
+
var ollama = require('@langchain/ollama');
|
|
18
|
+
var openai = require('@langchain/openai');
|
|
19
|
+
var output_parsers = require('@langchain/core/output_parsers');
|
|
20
|
+
var simpleGit = require('simple-git');
|
|
17
21
|
var pQueue = require('p-queue');
|
|
18
22
|
var documents = require('@langchain/core/documents');
|
|
19
23
|
var outputs = require('@langchain/core/outputs');
|
|
20
24
|
var manager = require('@langchain/core/callbacks/manager');
|
|
21
25
|
var runnables = require('@langchain/core/runnables');
|
|
22
26
|
var base = require('@langchain/core/language_models/base');
|
|
23
|
-
var output_parsers = require('@langchain/core/output_parsers');
|
|
24
27
|
require('@langchain/core/messages');
|
|
25
28
|
require('@langchain/core/memory');
|
|
26
29
|
require('@langchain/core/chat_history');
|
|
27
30
|
require('@langchain/core/utils/tiktoken');
|
|
28
|
-
var openai = require('@langchain/openai');
|
|
29
31
|
require('@langchain/core/utils/async_caller');
|
|
30
32
|
require('@langchain/core/utils/env');
|
|
31
33
|
require('@langchain/core/utils/json_patch');
|
|
32
34
|
var diff = require('diff');
|
|
33
|
-
var ollama = require('@langchain/ollama');
|
|
34
35
|
var minimatch = require('minimatch');
|
|
35
|
-
var simpleGit = require('simple-git');
|
|
36
36
|
var tiktoken = require('tiktoken');
|
|
37
37
|
var child_process = require('child_process');
|
|
38
38
|
|
|
@@ -58,6 +58,27 @@ var ini__namespace = /*#__PURE__*/_interopNamespaceDefault(ini);
|
|
|
58
58
|
var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
|
|
59
59
|
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
60
60
|
|
|
61
|
+
const isInteractive = (config) => {
|
|
62
|
+
return config?.mode === 'interactive' || !!config?.interactive;
|
|
63
|
+
};
|
|
64
|
+
const SEPERATOR = chalk.blue('─────────────');
|
|
65
|
+
const LOGO = chalk.green(`┌──────┐
|
|
66
|
+
│┏┏┓┏┏┓│
|
|
67
|
+
│┗┗┛┗┗┛│
|
|
68
|
+
└──────┘`);
|
|
69
|
+
chalk.green(`┌────┐
|
|
70
|
+
│coco│
|
|
71
|
+
└────┘`);
|
|
72
|
+
const USAGE_BANNER = chalk.green(`${LOGO}
|
|
73
|
+
${chalk.bgGreen(`\xa0v${process.env.npm_package_version}\xa0`)}
|
|
74
|
+
`);
|
|
75
|
+
const getCommandUsageHeader = (command) => {
|
|
76
|
+
return chalk.green(`${USAGE_BANNER}\n${chalk.white('Command:')}\n\xa0\xa0\xa0\xa0\xa0 $0 ${chalk.greenBright(command)} [options]`);
|
|
77
|
+
};
|
|
78
|
+
const CONFIG_ALREADY_EXISTS = (path) => {
|
|
79
|
+
return `coco config found in '${path}', do you want to override it?`;
|
|
80
|
+
};
|
|
81
|
+
|
|
61
82
|
/**
|
|
62
83
|
* Returns a new object with all undefined keys removed
|
|
63
84
|
*
|
|
@@ -68,16 +89,64 @@ function removeUndefined(obj) {
|
|
|
68
89
|
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
69
90
|
}
|
|
70
91
|
|
|
71
|
-
|
|
92
|
+
async function updateFileSection({ filePath, startComment, endComment, getNewContent, confirmUpdate = true, confirmMessage = (path) => `A section already exists in ${path}, do you want to override it?`, }) {
|
|
93
|
+
const lines = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8').split(/\r?\n/) : [];
|
|
94
|
+
const newLines = [];
|
|
95
|
+
let foundSection = false;
|
|
96
|
+
for (let i = 0; i < lines.length; i++) {
|
|
97
|
+
if (lines[i].trim() === startComment) {
|
|
98
|
+
foundSection = true;
|
|
99
|
+
if (confirmUpdate) {
|
|
100
|
+
const confirmOverwrite = await prompts.confirm({
|
|
101
|
+
message: typeof confirmMessage === 'function' ? confirmMessage(filePath) : confirmMessage,
|
|
102
|
+
default: false,
|
|
103
|
+
});
|
|
104
|
+
if (!confirmOverwrite) {
|
|
105
|
+
// keep all lines until the end comment
|
|
106
|
+
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
107
|
+
newLines.push(lines[i]);
|
|
108
|
+
i++;
|
|
109
|
+
}
|
|
110
|
+
newLines.push(endComment);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
newLines.push(startComment);
|
|
115
|
+
// Insert the new content
|
|
116
|
+
const newContent = await getNewContent();
|
|
117
|
+
newLines.push(newContent);
|
|
118
|
+
// Skip the existing content of the section
|
|
119
|
+
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
120
|
+
i++;
|
|
121
|
+
}
|
|
122
|
+
newLines.push(endComment);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!foundSection || lines[i].trim() !== endComment) {
|
|
126
|
+
newLines.push(lines[i]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// If section wasn't found, append it at the end
|
|
130
|
+
if (!foundSection) {
|
|
131
|
+
newLines.push('\n' + startComment);
|
|
132
|
+
const newContent = await getNewContent();
|
|
133
|
+
newLines.push(newContent);
|
|
134
|
+
newLines.push(endComment);
|
|
135
|
+
}
|
|
136
|
+
// Write the updated contents back to the file
|
|
137
|
+
fs.writeFileSync(filePath, newLines.join('\n'));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const template$4 = `GOAL: Use functional abstractions to summarize the following text
|
|
72
141
|
|
|
73
142
|
RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
|
|
74
143
|
|
|
75
144
|
TEXT:"""{text}"""
|
|
76
145
|
`;
|
|
77
|
-
const inputVariables$
|
|
78
|
-
const SUMMARIZE_PROMPT = new prompts.PromptTemplate({
|
|
79
|
-
inputVariables: inputVariables$
|
|
80
|
-
template: template$
|
|
146
|
+
const inputVariables$3 = ['text'];
|
|
147
|
+
const SUMMARIZE_PROMPT = new prompts$1.PromptTemplate({
|
|
148
|
+
inputVariables: inputVariables$3,
|
|
149
|
+
template: template$4,
|
|
81
150
|
});
|
|
82
151
|
|
|
83
152
|
/**
|
|
@@ -128,9 +197,9 @@ function getDefaultServiceApiKey(config) {
|
|
|
128
197
|
}
|
|
129
198
|
const DEFAULT_OPENAI_LLM_SERVICE = {
|
|
130
199
|
provider: 'openai',
|
|
131
|
-
model: 'gpt-4
|
|
200
|
+
model: 'gpt-4',
|
|
132
201
|
tokenLimit: 2024,
|
|
133
|
-
temperature: 0.
|
|
202
|
+
temperature: 0.32,
|
|
134
203
|
authentication: {
|
|
135
204
|
type: 'APIKey',
|
|
136
205
|
credentials: {
|
|
@@ -203,68 +272,6 @@ const CONFIG_KEYS = Object.keys({
|
|
|
203
272
|
prompt: '',
|
|
204
273
|
});
|
|
205
274
|
|
|
206
|
-
async function updateFileSection({ filePath, startComment, endComment, getNewContent, confirmUpdate = true, confirmMessage = (path) => `A section already exists in ${path}, do you want to override it?`, }) {
|
|
207
|
-
const lines = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8').split(/\r?\n/) : [];
|
|
208
|
-
const newLines = [];
|
|
209
|
-
let foundSection = false;
|
|
210
|
-
for (let i = 0; i < lines.length; i++) {
|
|
211
|
-
if (lines[i].trim() === startComment) {
|
|
212
|
-
foundSection = true;
|
|
213
|
-
if (confirmUpdate) {
|
|
214
|
-
const confirmOverwrite = await prompts$1.confirm({
|
|
215
|
-
message: typeof confirmMessage === 'function' ? confirmMessage(filePath) : confirmMessage,
|
|
216
|
-
default: false,
|
|
217
|
-
});
|
|
218
|
-
if (!confirmOverwrite) {
|
|
219
|
-
// keep all lines until the end comment
|
|
220
|
-
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
221
|
-
newLines.push(lines[i]);
|
|
222
|
-
i++;
|
|
223
|
-
}
|
|
224
|
-
newLines.push(endComment);
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
newLines.push(startComment);
|
|
229
|
-
// Insert the new content
|
|
230
|
-
const newContent = await getNewContent();
|
|
231
|
-
newLines.push(newContent);
|
|
232
|
-
// Skip the existing content of the section
|
|
233
|
-
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
234
|
-
i++;
|
|
235
|
-
}
|
|
236
|
-
newLines.push(endComment);
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
if (!foundSection || lines[i].trim() !== endComment) {
|
|
240
|
-
newLines.push(lines[i]);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
// If section wasn't found, append it at the end
|
|
244
|
-
if (!foundSection) {
|
|
245
|
-
newLines.push('\n' + startComment);
|
|
246
|
-
const newContent = await getNewContent();
|
|
247
|
-
newLines.push(newContent);
|
|
248
|
-
newLines.push(endComment);
|
|
249
|
-
}
|
|
250
|
-
// Write the updated contents back to the file
|
|
251
|
-
fs.writeFileSync(filePath, newLines.join('\n'));
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const isInteractive = (argv) => {
|
|
255
|
-
return argv?.mode === 'interactive' || argv.interactive;
|
|
256
|
-
};
|
|
257
|
-
const SEPERATOR = chalk.blue('─────────────');
|
|
258
|
-
const LOGO = chalk.green(`┌────────────┐
|
|
259
|
-
│┌─┐┌─┐┌─┐┌─┐│
|
|
260
|
-
││ │ ││ │ ││
|
|
261
|
-
│└─┘└─┘└─┘└─┘│
|
|
262
|
-
└────────────┘
|
|
263
|
-
`);
|
|
264
|
-
const CONFIG_ALREADY_EXISTS = (path) => {
|
|
265
|
-
return `coco config found in '${path}', do you want to override it?`;
|
|
266
|
-
};
|
|
267
|
-
|
|
268
275
|
/**
|
|
269
276
|
* Load environment variables
|
|
270
277
|
*
|
|
@@ -288,6 +295,9 @@ function loadEnvConfig(config) {
|
|
|
288
295
|
handleServiceEnvVar(envConfig.service, key, envValue);
|
|
289
296
|
}
|
|
290
297
|
else {
|
|
298
|
+
if (key === 'service' || !envValue) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
291
301
|
envConfig[key] = envValue;
|
|
292
302
|
}
|
|
293
303
|
});
|
|
@@ -396,7 +406,6 @@ function loadGitConfig(config) {
|
|
|
396
406
|
config = {
|
|
397
407
|
...config,
|
|
398
408
|
service: service,
|
|
399
|
-
temperature: gitConfigParsed.coco?.temperature || config.temperature,
|
|
400
409
|
prompt: gitConfigParsed.coco?.prompt || config.prompt,
|
|
401
410
|
mode: gitConfigParsed.coco?.mode || config.mode,
|
|
402
411
|
summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
|
|
@@ -1937,89 +1946,652 @@ function commandExecutor(handler) {
|
|
|
1937
1946
|
}
|
|
1938
1947
|
|
|
1939
1948
|
/**
|
|
1940
|
-
*
|
|
1941
|
-
*
|
|
1942
|
-
* @
|
|
1949
|
+
* Get LLM Model Based on Configuration
|
|
1950
|
+
*
|
|
1951
|
+
* @param fields
|
|
1952
|
+
* @param configuration
|
|
1953
|
+
* @returns LLM Model
|
|
1943
1954
|
*/
|
|
1944
|
-
function
|
|
1945
|
-
|
|
1955
|
+
function getLlm(provider, model, config) {
|
|
1956
|
+
if (!model) {
|
|
1957
|
+
throw new Error(`Invalid LLM Service: ${provider}/${model}`);
|
|
1958
|
+
}
|
|
1959
|
+
switch (provider) {
|
|
1960
|
+
case 'ollama':
|
|
1961
|
+
return new ollama.ChatOllama({
|
|
1962
|
+
baseUrl: DEFAULT_OLLAMA_LLM_SERVICE.endpoint,
|
|
1963
|
+
maxConcurrency: config.service.maxConcurrent,
|
|
1964
|
+
model,
|
|
1965
|
+
});
|
|
1966
|
+
case 'openai':
|
|
1967
|
+
default:
|
|
1968
|
+
const openAiModel = new openai.ChatOpenAI({
|
|
1969
|
+
openAIApiKey: getApiKeyForModel(config),
|
|
1970
|
+
model,
|
|
1971
|
+
temperature: config.service.temperature || 0.2,
|
|
1972
|
+
});
|
|
1973
|
+
return openAiModel;
|
|
1974
|
+
}
|
|
1946
1975
|
}
|
|
1947
1976
|
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
return res.text && res.text.trim();
|
|
1977
|
+
function getPrompt({ template, variables, fallback }) {
|
|
1978
|
+
if (!template && !fallback)
|
|
1979
|
+
throw new Error('Must provide either a template or a fallback');
|
|
1980
|
+
return (template
|
|
1981
|
+
? new prompts$1.PromptTemplate({
|
|
1982
|
+
template,
|
|
1983
|
+
inputVariables: variables,
|
|
1984
|
+
})
|
|
1985
|
+
: fallback);
|
|
1958
1986
|
}
|
|
1959
1987
|
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
* @returns {DirectoryDiff[]} The groups created.
|
|
1964
|
-
*/
|
|
1965
|
-
function createDirectoryDiffs(node) {
|
|
1966
|
-
const groupByPath = {};
|
|
1967
|
-
function traverse(node) {
|
|
1968
|
-
node.diffs.forEach((diff) => {
|
|
1969
|
-
const path = getPathFromFilePath(diff.file);
|
|
1970
|
-
if (!groupByPath[path]) {
|
|
1971
|
-
groupByPath[path] = { diffs: [], path, tokenCount: 0 };
|
|
1972
|
-
}
|
|
1973
|
-
groupByPath[path].diffs.push(diff);
|
|
1974
|
-
groupByPath[path].tokenCount += diff.tokenCount;
|
|
1975
|
-
});
|
|
1976
|
-
node.children.forEach(traverse);
|
|
1988
|
+
const executeChain = async ({ llm, prompt, variables, parser }) => {
|
|
1989
|
+
if (!llm || !prompt || !variables) {
|
|
1990
|
+
throw new Error('The input parameters "llm", "prompt", and "variables" are all required.');
|
|
1977
1991
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1992
|
+
const chain = prompt.pipe(llm).pipe(parser);
|
|
1993
|
+
let res;
|
|
1994
|
+
try {
|
|
1995
|
+
res = await chain.invoke(variables);
|
|
1996
|
+
}
|
|
1997
|
+
catch (error) {
|
|
1998
|
+
if (error instanceof Error) {
|
|
1999
|
+
throw new Error(`LLMChain call error: ${error.message}`);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
if (!res) {
|
|
2003
|
+
throw new Error('Empty response from LLMChain call');
|
|
2004
|
+
}
|
|
2005
|
+
return res;
|
|
2006
|
+
};
|
|
2007
|
+
|
|
2008
|
+
function extractTicketIdFromBranchName(branchName) {
|
|
2009
|
+
const regex = /((?<!([A-Z]+)-?)[A-Z]+-\d+)/;
|
|
2010
|
+
const match = branchName.match(regex);
|
|
2011
|
+
return match ? match[0] : null;
|
|
1980
2012
|
}
|
|
2013
|
+
|
|
1981
2014
|
/**
|
|
1982
|
-
*
|
|
2015
|
+
* Retrieves the commit log range between two specified commits.
|
|
2016
|
+
*
|
|
2017
|
+
* @param from - The starting commit.
|
|
2018
|
+
* @param to - The ending commit.
|
|
2019
|
+
* @param options - Additional options for retrieving the commit log range.
|
|
2020
|
+
* @returns A promise that resolves to an array of commit log messages.
|
|
2021
|
+
* @throws If there is an error retrieving the commit log range.
|
|
1983
2022
|
*/
|
|
1984
|
-
async function
|
|
2023
|
+
async function getCommitLogRange(from, to, { noMerges, git }) {
|
|
1985
2024
|
try {
|
|
1986
|
-
const
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
file: diff.file,
|
|
1990
|
-
summary: diff.summary,
|
|
1991
|
-
},
|
|
1992
|
-
})), {
|
|
1993
|
-
chain,
|
|
1994
|
-
textSplitter,
|
|
1995
|
-
options: {
|
|
1996
|
-
returnIntermediateSteps: true,
|
|
1997
|
-
},
|
|
1998
|
-
});
|
|
1999
|
-
const newTokenTotal = tokenizer(directorySummary);
|
|
2000
|
-
return {
|
|
2001
|
-
diffs: directory.diffs,
|
|
2002
|
-
path: directory.path,
|
|
2003
|
-
summary: directorySummary,
|
|
2004
|
-
tokenCount: newTokenTotal,
|
|
2005
|
-
};
|
|
2025
|
+
const logOptions = { from: `${from}^1`, to, '--no-merges': noMerges };
|
|
2026
|
+
const commitLog = await git.log(logOptions);
|
|
2027
|
+
return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
|
|
2006
2028
|
}
|
|
2007
2029
|
catch (error) {
|
|
2008
|
-
|
|
2009
|
-
|
|
2030
|
+
// If there's an error, handle it appropriately
|
|
2031
|
+
throw error;
|
|
2010
2032
|
}
|
|
2011
2033
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2034
|
+
|
|
2035
|
+
/**
|
|
2036
|
+
* Retrieves the name of the current branch.
|
|
2037
|
+
*
|
|
2038
|
+
* @param {GetCurrentBranchName} options - The options for retrieving the branch name.
|
|
2039
|
+
* @returns {Promise<string>} - A promise that resolves to the name of the current branch.
|
|
2040
|
+
*/
|
|
2041
|
+
async function getCurrentBranchName({ git }) {
|
|
2042
|
+
return await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
/**
|
|
2046
|
+
* Retrieves the commit log between the current branch and a specified target branch.
|
|
2047
|
+
*
|
|
2048
|
+
* @param {Object} options - The options for retrieving the commit log.
|
|
2049
|
+
* @param {SimpleGit} options.git - The SimpleGit instance.
|
|
2050
|
+
* @param {Logger} options.logger - The logger for logging messages.
|
|
2051
|
+
* @param {string} options.targetBranch - The target branch to compare against.
|
|
2052
|
+
* @returns {Promise<string[]>} The array of commit messages in the commit log.
|
|
2053
|
+
*/
|
|
2054
|
+
async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
|
|
2055
|
+
try {
|
|
2056
|
+
// Get the current branch name
|
|
2057
|
+
const currentBranch = await getCurrentBranchName({ git });
|
|
2058
|
+
// Get the list of commits that are unique to the current branch compared to the target branch
|
|
2059
|
+
const uniqueCommits = (await git.raw(['rev-list', `${targetBranch}..${currentBranch}`]))
|
|
2060
|
+
.split('\n')
|
|
2061
|
+
.filter(Boolean)
|
|
2062
|
+
.reverse();
|
|
2063
|
+
logger?.verbose(`Found ${uniqueCommits.length} unique commits between "${currentBranch}" and "${targetBranch}"`, { color: 'blue' });
|
|
2064
|
+
const firstCommit = uniqueCommits[0];
|
|
2065
|
+
const lastCommit = uniqueCommits[uniqueCommits.length - 1];
|
|
2066
|
+
if (!firstCommit || !lastCommit) {
|
|
2067
|
+
logger?.log('Unable to determine first and last commit between branches', { color: 'yellow' });
|
|
2068
|
+
return [];
|
|
2069
|
+
}
|
|
2070
|
+
// Retrieve commit log with messages
|
|
2071
|
+
return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
|
|
2072
|
+
}
|
|
2073
|
+
catch (error) {
|
|
2074
|
+
logger?.log('Encountered an error getting commit log between branches', { color: 'red' });
|
|
2075
|
+
}
|
|
2076
|
+
return [];
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
/**
|
|
2080
|
+
* Retrieves the commit log for the current branch.
|
|
2081
|
+
*
|
|
2082
|
+
* @param {Object} options - The options for retrieving the commit log.
|
|
2083
|
+
* @param {SimpleGit} options.git - The SimpleGit instance.
|
|
2084
|
+
* @param {Logger} options.logger - The logger for logging messages.
|
|
2085
|
+
* @param {string} [options.comparisonBranch='main'] - The branch to compare against.
|
|
2086
|
+
* @param {string} [options.comparisonRemote='origin'] - The remote to compare against.
|
|
2087
|
+
* @returns {Promise<string[]>} The array of commit messages in the commit log.
|
|
2088
|
+
*/
|
|
2089
|
+
async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
|
|
2090
|
+
try {
|
|
2091
|
+
// Get the current branch name
|
|
2092
|
+
const branch = await getCurrentBranchName({ git });
|
|
2093
|
+
// Check if the current branch has any commits
|
|
2094
|
+
const hasCommits = (await git.raw(['rev-list', '--count', branch])) !== '0';
|
|
2095
|
+
if (!hasCommits) {
|
|
2096
|
+
logger?.log('No commits on the current branch.');
|
|
2097
|
+
return [];
|
|
2098
|
+
}
|
|
2099
|
+
// Get the list of commits that are unique to the current branch
|
|
2100
|
+
let uniqueCommits;
|
|
2101
|
+
if (comparisonBranch === branch) {
|
|
2102
|
+
// If the comparison branch is the same as the current branch, we compare against the remote.
|
|
2103
|
+
uniqueCommits = (await git.raw(['rev-list', `${comparisonRemote}/${comparisonBranch}..${branch}`]))
|
|
2104
|
+
.split('\n')
|
|
2105
|
+
.filter(Boolean)
|
|
2106
|
+
.reverse();
|
|
2107
|
+
}
|
|
2108
|
+
else {
|
|
2109
|
+
// Your existing code for different branches
|
|
2110
|
+
uniqueCommits = (await git.raw(['rev-list', `${comparisonBranch}..${branch}`]))
|
|
2111
|
+
.split('\n')
|
|
2112
|
+
.filter(Boolean)
|
|
2113
|
+
.reverse();
|
|
2114
|
+
}
|
|
2115
|
+
logger?.verbose(`Found ${uniqueCommits.length} unique commits on "${branch}"`, { color: 'blue' });
|
|
2116
|
+
const firstCommit = uniqueCommits[0];
|
|
2117
|
+
const lastCommit = uniqueCommits[uniqueCommits.length - 1];
|
|
2118
|
+
if (!firstCommit || !lastCommit) {
|
|
2119
|
+
logger?.log('Unable to determine first and last commit on the current branch', { color: 'yellow' });
|
|
2120
|
+
return [];
|
|
2121
|
+
}
|
|
2122
|
+
// Retrieve commit log with messages
|
|
2123
|
+
return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
|
|
2124
|
+
}
|
|
2125
|
+
catch (error) {
|
|
2126
|
+
logger?.log('Encountered an error getting commit log from current branch', { color: 'red' });
|
|
2127
|
+
}
|
|
2128
|
+
return [];
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
/**
|
|
2132
|
+
* Retrieves the SimpleGit instance for the repository.
|
|
2133
|
+
* @returns {SimpleGit} The SimpleGit instance.
|
|
2134
|
+
*/
|
|
2135
|
+
const getRepo = () => {
|
|
2136
|
+
let git;
|
|
2137
|
+
try {
|
|
2138
|
+
git = simpleGit.simpleGit();
|
|
2139
|
+
}
|
|
2140
|
+
catch (e) {
|
|
2141
|
+
console.log('Error initializing git repo', e);
|
|
2142
|
+
process.exit(1);
|
|
2143
|
+
}
|
|
2144
|
+
return git;
|
|
2145
|
+
};
|
|
2146
|
+
|
|
2147
|
+
const template$3 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
|
|
2148
|
+
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.
|
|
2149
|
+
|
|
2150
|
+
Please follow the guidelines below when writing your commit message:
|
|
2151
|
+
|
|
2152
|
+
- Write concisely using an informal tone
|
|
2153
|
+
- Avoid phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
|
|
2154
|
+
- Avoid referencing specific files names or long paths in the commit message
|
|
2155
|
+
- DO NOT include any diffs or file changes in the commit message
|
|
2156
|
+
- Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
|
|
2157
|
+
|
|
2158
|
+
{format_instructions}
|
|
2159
|
+
|
|
2160
|
+
""""""
|
|
2161
|
+
{summary}
|
|
2162
|
+
""""""
|
|
2163
|
+
|
|
2164
|
+
{additional}
|
|
2165
|
+
`;
|
|
2166
|
+
const inputVariables$2 = ['summary', 'format_instructions', 'additional'];
|
|
2167
|
+
const COMMIT_PROMPT = new prompts$1.PromptTemplate({
|
|
2168
|
+
template: template$3,
|
|
2169
|
+
inputVariables: inputVariables$2,
|
|
2170
|
+
});
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* Verify template string contains all required input variables
|
|
2174
|
+
*
|
|
2175
|
+
* @param text template string
|
|
2176
|
+
* @param inputVariables template variables
|
|
2177
|
+
* @returns boolean or error message
|
|
2178
|
+
*/
|
|
2179
|
+
function validatePromptTemplate(text, inputVariables) {
|
|
2180
|
+
if (!text) {
|
|
2181
|
+
return 'Prompt template cannot be empty';
|
|
2182
|
+
}
|
|
2183
|
+
if (!inputVariables.some((entry) => text.includes(entry))) {
|
|
2184
|
+
return ('Prompt template must include at least one of the following input variables: ' +
|
|
2185
|
+
inputVariables.map((value) => `{${value}}`).join(', '));
|
|
2186
|
+
}
|
|
2187
|
+
return true;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
async function editPrompt(options) {
|
|
2191
|
+
return await prompts.editor({
|
|
2192
|
+
message: 'Edit the prompt',
|
|
2193
|
+
default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
|
|
2194
|
+
waitForUseInput: false,
|
|
2195
|
+
postfix: 'Press ENTER to continue',
|
|
2196
|
+
validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
async function editResult(result, options) {
|
|
2201
|
+
if (options.openInEditor) {
|
|
2202
|
+
return await prompts.editor({
|
|
2203
|
+
message: 'Edit the commit message',
|
|
2204
|
+
default: result,
|
|
2205
|
+
waitForUseInput: false,
|
|
2206
|
+
validate: (text) => (text ? true : 'Commit message cannot be empty'),
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
return result;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
async function getUserReviewDecision({ label, descriptions, enableRetry = true, enableFullRetry = true, enableModifyPrompt = true, }) {
|
|
2213
|
+
const choices = [
|
|
2214
|
+
{
|
|
2215
|
+
name: '✨ Looks good!',
|
|
2216
|
+
value: 'approve',
|
|
2217
|
+
description: descriptions?.approve || `Continue with the generated ${label}`,
|
|
2218
|
+
},
|
|
2219
|
+
{
|
|
2220
|
+
name: '📝 Edit',
|
|
2221
|
+
value: 'edit',
|
|
2222
|
+
description: descriptions?.edit || `Edit the generated ${label} before proceeding`,
|
|
2223
|
+
},
|
|
2224
|
+
];
|
|
2225
|
+
if (enableModifyPrompt) {
|
|
2226
|
+
choices.push({
|
|
2227
|
+
name: '🪶 Modify Prompt',
|
|
2228
|
+
value: 'modifyPrompt',
|
|
2229
|
+
description: descriptions?.modifyPrompt || `Modify the prompt template and regenerate the ${label}`,
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
if (enableRetry) {
|
|
2233
|
+
choices.push({
|
|
2234
|
+
name: '🔄 Retry',
|
|
2235
|
+
value: 'retryMessageOnly',
|
|
2236
|
+
description: descriptions?.retryMessageOnly ||
|
|
2237
|
+
`Restart the function execution from generating the ${label}`,
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
if (enableFullRetry) {
|
|
2241
|
+
choices.push({
|
|
2242
|
+
name: '🔄 Retry Full',
|
|
2243
|
+
value: 'retryFull',
|
|
2244
|
+
description: descriptions?.retryFull ||
|
|
2245
|
+
`Restart the function execution from the beginning, regenerating both the summary and ${label}`,
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
choices.push({
|
|
2249
|
+
name: '💣 Cancel',
|
|
2250
|
+
value: 'cancel',
|
|
2251
|
+
description: descriptions?.cancel || `Cancel the ${label}`,
|
|
2252
|
+
});
|
|
2253
|
+
return (await prompts.select({
|
|
2254
|
+
message: `Would you like to make any changes to the ${label}?`,
|
|
2255
|
+
choices,
|
|
2256
|
+
}));
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
function logResult(label, result) {
|
|
2260
|
+
console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
async function generateAndReviewLoop({ label, factory, parser, noResult, agent, options, }) {
|
|
2264
|
+
const { logger } = options;
|
|
2265
|
+
let continueLoop = true;
|
|
2266
|
+
let modifyPrompt = false;
|
|
2267
|
+
let context = '';
|
|
2268
|
+
let result = '';
|
|
2269
|
+
const changes = await factory();
|
|
2270
|
+
// if we don't have any changes, bail.
|
|
2271
|
+
if (!changes || !Object.keys(changes).length) {
|
|
2272
|
+
await noResult(options);
|
|
2273
|
+
}
|
|
2274
|
+
while (continueLoop) {
|
|
2275
|
+
if (!context.length) {
|
|
2276
|
+
context = await parser(changes, result, options);
|
|
2277
|
+
}
|
|
2278
|
+
// if we still don't have a context, bail.
|
|
2279
|
+
if (!context.length) {
|
|
2280
|
+
await noResult(options);
|
|
2281
|
+
}
|
|
2282
|
+
if (modifyPrompt) {
|
|
2283
|
+
options.prompt = await editPrompt(options);
|
|
2284
|
+
}
|
|
2285
|
+
logger.startTimer().startSpinner(`Generating ${label}\n`, {
|
|
2286
|
+
color: 'blue',
|
|
2287
|
+
});
|
|
2288
|
+
result = await agent(context, options);
|
|
2289
|
+
if (!result) {
|
|
2290
|
+
logger.stopSpinner('💀 Agent failed to return content.', {
|
|
2291
|
+
mode: 'fail',
|
|
2292
|
+
color: 'red',
|
|
2293
|
+
});
|
|
2294
|
+
process.exit(0);
|
|
2295
|
+
}
|
|
2296
|
+
logger
|
|
2297
|
+
.stopSpinner(`Generated ${label}`, {
|
|
2298
|
+
color: 'green',
|
|
2299
|
+
mode: 'succeed',
|
|
2300
|
+
})
|
|
2301
|
+
.stopTimer();
|
|
2302
|
+
if (options?.interactive) {
|
|
2303
|
+
logResult(label, result);
|
|
2304
|
+
const reviewAnswer = await getUserReviewDecision({
|
|
2305
|
+
label,
|
|
2306
|
+
...(options?.review || {}),
|
|
2307
|
+
});
|
|
2308
|
+
if (reviewAnswer === 'cancel') {
|
|
2309
|
+
process.exit(0);
|
|
2310
|
+
}
|
|
2311
|
+
if (reviewAnswer === 'edit') {
|
|
2312
|
+
options.openInEditor = true;
|
|
2313
|
+
}
|
|
2314
|
+
if (reviewAnswer === 'retryFull') {
|
|
2315
|
+
context = '';
|
|
2316
|
+
result = '';
|
|
2317
|
+
options.prompt = '';
|
|
2318
|
+
continue;
|
|
2319
|
+
}
|
|
2320
|
+
if (reviewAnswer === 'retryMessageOnly') {
|
|
2321
|
+
modifyPrompt = false;
|
|
2322
|
+
result = '';
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
if (reviewAnswer === 'modifyPrompt') {
|
|
2326
|
+
modifyPrompt = true;
|
|
2327
|
+
result = '';
|
|
2328
|
+
continue;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
// if we're here, we're done.
|
|
2332
|
+
result = await editResult(result, options);
|
|
2333
|
+
continueLoop = false;
|
|
2334
|
+
}
|
|
2335
|
+
return result;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
const logSuccess = () => {
|
|
2339
|
+
console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
|
|
2340
|
+
};
|
|
2341
|
+
|
|
2342
|
+
async function handleResult({ result, mode, interactiveHandler }) {
|
|
2343
|
+
switch (mode) {
|
|
2344
|
+
case 'interactive':
|
|
2345
|
+
if (interactiveHandler) {
|
|
2346
|
+
await interactiveHandler(result);
|
|
2347
|
+
}
|
|
2348
|
+
else {
|
|
2349
|
+
console.warn('No result handler provided for interactive mode.');
|
|
2350
|
+
logSuccess();
|
|
2351
|
+
}
|
|
2352
|
+
break;
|
|
2353
|
+
case 'stdout':
|
|
2354
|
+
default:
|
|
2355
|
+
process.stdout.write(result, 'utf8');
|
|
2356
|
+
break;
|
|
2357
|
+
}
|
|
2358
|
+
process.exit(0);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
const template$2 = `Write informative git changelog, in the imperative, based on a series of individual messages.
|
|
2362
|
+
|
|
2363
|
+
- Include the git commit hash as reference for each change, including just the first 7 characters
|
|
2364
|
+
- Logically group changes, and if necessary, summarize dependency updates
|
|
2365
|
+
|
|
2366
|
+
{format_instructions}
|
|
2367
|
+
|
|
2368
|
+
"""{summary}"""`;
|
|
2369
|
+
const inputVariables$1 = ['format_instructions', 'summary'];
|
|
2370
|
+
const CHANGELOG_PROMPT = new prompts$1.PromptTemplate({
|
|
2371
|
+
template: template$2,
|
|
2372
|
+
inputVariables: inputVariables$1,
|
|
2373
|
+
});
|
|
2374
|
+
|
|
2375
|
+
const handler$3 = async (argv, logger) => {
|
|
2376
|
+
const config = loadConfig(argv);
|
|
2377
|
+
const git = getRepo();
|
|
2378
|
+
const key = getApiKeyForModel(config);
|
|
2379
|
+
const { provider, model } = getModelAndProviderFromConfig(config);
|
|
2380
|
+
if (config.service.authentication.type !== 'None' && !key) {
|
|
2381
|
+
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
2382
|
+
process.exit(1);
|
|
2383
|
+
}
|
|
2384
|
+
const llm = getLlm(provider, model, config);
|
|
2385
|
+
const INTERACTIVE = isInteractive(config);
|
|
2386
|
+
if (INTERACTIVE) {
|
|
2387
|
+
logger.log(LOGO);
|
|
2388
|
+
}
|
|
2389
|
+
async function factory() {
|
|
2390
|
+
const branchName = await getCurrentBranchName({ git });
|
|
2391
|
+
if (config.range && config.range.includes(':')) {
|
|
2392
|
+
const [from, to] = config.range.split(':');
|
|
2393
|
+
if (!from || !to) {
|
|
2394
|
+
logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
|
|
2395
|
+
process.exit(1);
|
|
2396
|
+
}
|
|
2397
|
+
return {
|
|
2398
|
+
branch: branchName,
|
|
2399
|
+
commits: await getCommitLogRange(from, to, { git, noMerges: true }),
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
if (argv.branch) {
|
|
2403
|
+
logger.verbose(`Generating commit log against branch: ${argv.branch}`, { color: 'yellow' });
|
|
2404
|
+
return {
|
|
2405
|
+
branch: branchName,
|
|
2406
|
+
commits: await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch }),
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
logger.verbose(`No range or branch provided. Defaulting to current branch`, { color: 'yellow' });
|
|
2410
|
+
return {
|
|
2411
|
+
branch: branchName,
|
|
2412
|
+
commits: await getCommitLogCurrentBranch({ git, logger }),
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
async function parser({ branch, commits }) {
|
|
2416
|
+
let result;
|
|
2417
|
+
if (!commits || commits.length === 0) {
|
|
2418
|
+
result = `## ${branch}\n\nNo commits found.`;
|
|
2419
|
+
}
|
|
2420
|
+
else {
|
|
2421
|
+
result = `## ${branch}\n\n${commits.map((commit) => commit.trim()).join('\n\n')}`;
|
|
2422
|
+
}
|
|
2423
|
+
return result;
|
|
2424
|
+
}
|
|
2425
|
+
const changelogMsg = await generateAndReviewLoop({
|
|
2426
|
+
label: 'changelog',
|
|
2427
|
+
factory,
|
|
2428
|
+
parser,
|
|
2429
|
+
agent: async (context, options) => {
|
|
2430
|
+
const parser = new output_parsers.JsonOutputParser();
|
|
2431
|
+
const prompt = getPrompt({
|
|
2432
|
+
template: options.prompt,
|
|
2433
|
+
variables: CHANGELOG_PROMPT.inputVariables,
|
|
2434
|
+
fallback: CHANGELOG_PROMPT,
|
|
2435
|
+
});
|
|
2436
|
+
const formatInstructions = "Respond with a valid JSON object, containing two fields: 'header' and 'content', both strings.";
|
|
2437
|
+
const changelog = await executeChain({
|
|
2438
|
+
llm,
|
|
2439
|
+
prompt,
|
|
2440
|
+
variables: {
|
|
2441
|
+
summary: context,
|
|
2442
|
+
format_instructions: formatInstructions,
|
|
2443
|
+
},
|
|
2444
|
+
parser,
|
|
2445
|
+
});
|
|
2446
|
+
const branchName = await getCurrentBranchName({ git });
|
|
2447
|
+
const ticketId = extractTicketIdFromBranchName(branchName);
|
|
2448
|
+
const footer = ticketId ? `\n\nPart of **${ticketId}**` : '';
|
|
2449
|
+
return `${changelog.header}\n\n${changelog.content}${footer}`;
|
|
2450
|
+
},
|
|
2451
|
+
noResult: async () => {
|
|
2452
|
+
if (config.range) {
|
|
2453
|
+
logger.log(`No commits found in the provided range.`, { color: 'red' });
|
|
2454
|
+
process.exit(0);
|
|
2455
|
+
}
|
|
2456
|
+
logger.log(`No commits found in the current branch.`, { color: 'red' });
|
|
2457
|
+
process.exit(0);
|
|
2458
|
+
},
|
|
2459
|
+
options: {
|
|
2460
|
+
...config,
|
|
2461
|
+
prompt: config.prompt || CHANGELOG_PROMPT.template,
|
|
2462
|
+
logger,
|
|
2463
|
+
interactive: INTERACTIVE,
|
|
2464
|
+
review: {
|
|
2465
|
+
enableFullRetry: false,
|
|
2466
|
+
},
|
|
2467
|
+
},
|
|
2468
|
+
});
|
|
2469
|
+
const MODE = (INTERACTIVE && 'interactive') || (config.commit && 'interactive') || config?.mode || 'stdout';
|
|
2470
|
+
handleResult({
|
|
2471
|
+
result: changelogMsg,
|
|
2472
|
+
interactiveHandler: async () => {
|
|
2473
|
+
logSuccess();
|
|
2474
|
+
},
|
|
2475
|
+
mode: MODE,
|
|
2476
|
+
});
|
|
2477
|
+
};
|
|
2478
|
+
|
|
2479
|
+
/**
|
|
2480
|
+
* Command line options via yargs
|
|
2481
|
+
*/
|
|
2482
|
+
const options$3 = {
|
|
2483
|
+
range: {
|
|
2484
|
+
type: 'string',
|
|
2485
|
+
alias: 'r',
|
|
2486
|
+
description: 'Commit range e.g `HEAD~2:HEAD`',
|
|
2487
|
+
},
|
|
2488
|
+
branch: {
|
|
2489
|
+
type: 'string',
|
|
2490
|
+
alias: 'b',
|
|
2491
|
+
description: 'Target branch to compare against',
|
|
2492
|
+
},
|
|
2493
|
+
i: {
|
|
2494
|
+
type: 'boolean',
|
|
2495
|
+
alias: 'interactive',
|
|
2496
|
+
description: 'Toggle interactive mode',
|
|
2497
|
+
},
|
|
2498
|
+
};
|
|
2499
|
+
const builder$3 = (yargsInstance) => {
|
|
2500
|
+
return yargsInstance.options(options$3).usage(getCommandUsageHeader(changelog.command));
|
|
2501
|
+
};
|
|
2502
|
+
|
|
2503
|
+
var changelog = {
|
|
2504
|
+
command: 'changelog',
|
|
2505
|
+
desc: 'Generate a changelog from current or target branch or provided commit range.',
|
|
2506
|
+
builder: builder$3,
|
|
2507
|
+
handler: commandExecutor(handler$3),
|
|
2508
|
+
options: options$3,
|
|
2509
|
+
};
|
|
2510
|
+
|
|
2511
|
+
/**
|
|
2512
|
+
* Extract the path from a file path string.
|
|
2513
|
+
* @param {string} filePath - The full file path.
|
|
2514
|
+
* @returns {string} The path portion of the file path.
|
|
2515
|
+
*/
|
|
2516
|
+
function getPathFromFilePath(filePath) {
|
|
2517
|
+
return filePath.split('/').slice(0, -1).join('/');
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
async function summarize(documents$1, { chain, textSplitter, options }) {
|
|
2521
|
+
const { returnIntermediateSteps = false } = options || {};
|
|
2522
|
+
const docs = await textSplitter.splitDocuments(documents$1.map((doc) => new documents.Document(doc)));
|
|
2523
|
+
const res = await chain.invoke({
|
|
2524
|
+
input_documents: docs,
|
|
2525
|
+
returnIntermediateSteps,
|
|
2526
|
+
});
|
|
2527
|
+
if (res.error)
|
|
2528
|
+
throw new Error(res.error);
|
|
2529
|
+
return res.text && res.text.trim();
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
/**
|
|
2533
|
+
* Create groups from a given node info.
|
|
2534
|
+
* @param {DiffNode} node - The node info to start grouping.
|
|
2535
|
+
* @returns {DirectoryDiff[]} The groups created.
|
|
2536
|
+
*/
|
|
2537
|
+
function createDirectoryDiffs(node) {
|
|
2538
|
+
const groupByPath = {};
|
|
2539
|
+
function traverse(node) {
|
|
2540
|
+
node.diffs.forEach((diff) => {
|
|
2541
|
+
const path = getPathFromFilePath(diff.file);
|
|
2542
|
+
if (!groupByPath[path]) {
|
|
2543
|
+
groupByPath[path] = { diffs: [], path, tokenCount: 0 };
|
|
2544
|
+
}
|
|
2545
|
+
groupByPath[path].diffs.push(diff);
|
|
2546
|
+
groupByPath[path].tokenCount += diff.tokenCount;
|
|
2547
|
+
});
|
|
2548
|
+
node.children.forEach(traverse);
|
|
2549
|
+
}
|
|
2550
|
+
traverse(node);
|
|
2551
|
+
return Object.values(groupByPath);
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Summarize a directory diff asynchronously.
|
|
2555
|
+
*/
|
|
2556
|
+
async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenizer }) {
|
|
2557
|
+
try {
|
|
2558
|
+
const directorySummary = await summarize(directory.diffs.map((diff) => ({
|
|
2559
|
+
pageContent: diff.diff,
|
|
2560
|
+
metadata: {
|
|
2561
|
+
file: diff.file,
|
|
2562
|
+
summary: diff.summary,
|
|
2563
|
+
},
|
|
2564
|
+
})), {
|
|
2565
|
+
chain,
|
|
2566
|
+
textSplitter,
|
|
2567
|
+
options: {
|
|
2568
|
+
returnIntermediateSteps: true,
|
|
2569
|
+
},
|
|
2570
|
+
});
|
|
2571
|
+
const newTokenTotal = tokenizer(directorySummary);
|
|
2572
|
+
return {
|
|
2573
|
+
diffs: directory.diffs,
|
|
2574
|
+
path: directory.path,
|
|
2575
|
+
summary: directorySummary,
|
|
2576
|
+
tokenCount: newTokenTotal,
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
catch (error) {
|
|
2580
|
+
console.error(error);
|
|
2581
|
+
return directory;
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
const defaultOutputCallback = (group) => {
|
|
2585
|
+
let output = `
|
|
2586
|
+
-------\n* changes in "/${group.path}"\n\n`;
|
|
2587
|
+
if (group.summary) {
|
|
2588
|
+
output += `${group.diffs.map((diff) => ` • ${diff.summary}`).join('\n')}\n\nSummary:\n\n${group.summary}\n\n`;
|
|
2589
|
+
}
|
|
2590
|
+
else {
|
|
2591
|
+
output += `${group.diffs.map((diff) => ` • ${diff.summary}\n\n${diff.diff}`).join('\n\n')}\n\n`;
|
|
2592
|
+
}
|
|
2593
|
+
return output;
|
|
2594
|
+
};
|
|
2023
2595
|
async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 2048, textSplitter, chain, handleOutput = defaultOutputCallback, }) {
|
|
2024
2596
|
const queue = new pQueue({ concurrency: 8 });
|
|
2025
2597
|
logger.startTimer().startSpinner(`Organizing Diffs...`, { color: 'blue' });
|
|
@@ -2585,7 +3157,7 @@ class LLMChain extends BaseChain {
|
|
|
2585
3157
|
}
|
|
2586
3158
|
return new LLMChain({
|
|
2587
3159
|
llm: await base.BaseLanguageModel.deserialize(llm),
|
|
2588
|
-
prompt: await prompts.BasePromptTemplate.deserialize(prompt),
|
|
3160
|
+
prompt: await prompts$1.BasePromptTemplate.deserialize(prompt),
|
|
2589
3161
|
});
|
|
2590
3162
|
}
|
|
2591
3163
|
/** @deprecated */
|
|
@@ -2615,7 +3187,7 @@ You should build the API url in order to get a response that is as short as poss
|
|
|
2615
3187
|
|
|
2616
3188
|
Question:{question}
|
|
2617
3189
|
API url:`;
|
|
2618
|
-
const API_URL_PROMPT_TEMPLATE = /* #__PURE__ */ new prompts.PromptTemplate({
|
|
3190
|
+
const API_URL_PROMPT_TEMPLATE = /* #__PURE__ */ new prompts$1.PromptTemplate({
|
|
2619
3191
|
inputVariables: ["api_docs", "question"],
|
|
2620
3192
|
template: API_URL_RAW_PROMPT_TEMPLATE,
|
|
2621
3193
|
});
|
|
@@ -2628,7 +3200,7 @@ Here is the response from the API:
|
|
|
2628
3200
|
Summarize this response to answer the original question.
|
|
2629
3201
|
|
|
2630
3202
|
Summary:`;
|
|
2631
|
-
const API_RESPONSE_PROMPT_TEMPLATE = /* #__PURE__ */ new prompts.PromptTemplate({
|
|
3203
|
+
const API_RESPONSE_PROMPT_TEMPLATE = /* #__PURE__ */ new prompts$1.PromptTemplate({
|
|
2632
3204
|
inputVariables: ["api_docs", "question", "api_url", "api_response"],
|
|
2633
3205
|
template: API_RESPONSE_RAW_PROMPT_TEMPLATE,
|
|
2634
3206
|
});
|
|
@@ -3353,7 +3925,7 @@ class RefineDocumentsChain extends BaseChain {
|
|
|
3353
3925
|
return "RefineDocumentsChain";
|
|
3354
3926
|
}
|
|
3355
3927
|
get defaultDocumentPrompt() {
|
|
3356
|
-
return new prompts.PromptTemplate({
|
|
3928
|
+
return new prompts$1.PromptTemplate({
|
|
3357
3929
|
inputVariables: ["page_content"],
|
|
3358
3930
|
template: "{page_content}",
|
|
3359
3931
|
});
|
|
@@ -3510,7 +4082,7 @@ var combine_docs_chain = /*#__PURE__*/Object.freeze({
|
|
|
3510
4082
|
});
|
|
3511
4083
|
|
|
3512
4084
|
/* eslint-disable spaced-comment */
|
|
3513
|
-
const DEFAULT_QA_PROMPT = /*#__PURE__*/ new prompts.PromptTemplate({
|
|
4085
|
+
const DEFAULT_QA_PROMPT = /*#__PURE__*/ new prompts$1.PromptTemplate({
|
|
3514
4086
|
template: "Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.\n\n{context}\n\nQuestion: {question}\nHelpful Answer:",
|
|
3515
4087
|
inputVariables: ["context", "question"],
|
|
3516
4088
|
});
|
|
@@ -3519,10 +4091,10 @@ If you don't know the answer, just say that you don't know, don't try to make up
|
|
|
3519
4091
|
----------------
|
|
3520
4092
|
{context}`;
|
|
3521
4093
|
const messages = [
|
|
3522
|
-
/*#__PURE__*/ prompts.SystemMessagePromptTemplate.fromTemplate(system_template),
|
|
3523
|
-
/*#__PURE__*/ prompts.HumanMessagePromptTemplate.fromTemplate("{question}"),
|
|
4094
|
+
/*#__PURE__*/ prompts$1.SystemMessagePromptTemplate.fromTemplate(system_template),
|
|
4095
|
+
/*#__PURE__*/ prompts$1.HumanMessagePromptTemplate.fromTemplate("{question}"),
|
|
3524
4096
|
];
|
|
3525
|
-
const CHAT_PROMPT = /*#__PURE__*/ prompts.ChatPromptTemplate.fromMessages(messages);
|
|
4097
|
+
const CHAT_PROMPT = /*#__PURE__*/ prompts$1.ChatPromptTemplate.fromMessages(messages);
|
|
3526
4098
|
const QA_PROMPT_SELECTOR = /*#__PURE__*/ new example_selectors.ConditionalPromptSelector(DEFAULT_QA_PROMPT, [[example_selectors.isChatModel, CHAT_PROMPT]]);
|
|
3527
4099
|
|
|
3528
4100
|
/**
|
|
@@ -4273,15 +4845,15 @@ var vector_db_qa = /*#__PURE__*/Object.freeze({
|
|
|
4273
4845
|
});
|
|
4274
4846
|
|
|
4275
4847
|
/* eslint-disable spaced-comment */
|
|
4276
|
-
const template$
|
|
4848
|
+
const template$1 = `Write a concise summary of the following:
|
|
4277
4849
|
|
|
4278
4850
|
|
|
4279
4851
|
"{text}"
|
|
4280
4852
|
|
|
4281
4853
|
|
|
4282
4854
|
CONCISE SUMMARY:`;
|
|
4283
|
-
const DEFAULT_PROMPT = /*#__PURE__*/ new prompts.PromptTemplate({
|
|
4284
|
-
template: template$
|
|
4855
|
+
const DEFAULT_PROMPT = /*#__PURE__*/ new prompts$1.PromptTemplate({
|
|
4856
|
+
template: template$1,
|
|
4285
4857
|
inputVariables: ["text"],
|
|
4286
4858
|
});
|
|
4287
4859
|
|
|
@@ -4297,7 +4869,7 @@ Given the new context, refine the original summary
|
|
|
4297
4869
|
If the context isn't useful, return the original summary.
|
|
4298
4870
|
|
|
4299
4871
|
REFINED SUMMARY:`;
|
|
4300
|
-
const REFINE_PROMPT = /* #__PURE__ */ new prompts.PromptTemplate({
|
|
4872
|
+
const REFINE_PROMPT = /* #__PURE__ */ new prompts$1.PromptTemplate({
|
|
4301
4873
|
template: refinePromptTemplate,
|
|
4302
4874
|
inputVariables: ["existing_answer", "text"],
|
|
4303
4875
|
});
|
|
@@ -5380,585 +5952,284 @@ for (var i = 0; i < 256; i++) {
|
|
|
5380
5952
|
}
|
|
5381
5953
|
|
|
5382
5954
|
/**
|
|
5383
|
-
* Get Summarization Chain
|
|
5384
|
-
* @param model
|
|
5385
|
-
* @param options
|
|
5386
|
-
* @returns
|
|
5387
|
-
*/
|
|
5388
|
-
function getSummarizationChain(model, options = { type: 'map_reduce' }) {
|
|
5389
|
-
return loadSummarizationChain(model, options);
|
|
5390
|
-
}
|
|
5391
|
-
|
|
5392
|
-
/**
|
|
5393
|
-
* Get Recursive Character Text Splitter
|
|
5394
|
-
*
|
|
5395
|
-
* @param options
|
|
5396
|
-
* @returns
|
|
5397
|
-
*/
|
|
5398
|
-
function getTextSplitter(options = {}) {
|
|
5399
|
-
return new RecursiveCharacterTextSplitter(options);
|
|
5400
|
-
}
|
|
5401
|
-
|
|
5402
|
-
/**
|
|
5403
|
-
* Parses the default file diff for a given nodeFile.
|
|
5404
|
-
*
|
|
5405
|
-
* @param nodeFile - The file change object.
|
|
5406
|
-
* @param commit - The commit to diff against. Defaults to '--staged'.
|
|
5407
|
-
* @param git - The SimpleGit instance.
|
|
5408
|
-
* @returns A Promise that resolves to the file diff as a string.
|
|
5409
|
-
*/
|
|
5410
|
-
async function parseDefaultFileDiff(nodeFile, commit = '--staged', git) {
|
|
5411
|
-
if (commit !== '--staged') {
|
|
5412
|
-
return await git.diff([`${commit}~1..${commit}`, '--', nodeFile.filePath]);
|
|
5413
|
-
}
|
|
5414
|
-
return await git.diff([commit, nodeFile.filePath]);
|
|
5415
|
-
}
|
|
5416
|
-
/**
|
|
5417
|
-
* Parses the diff for a renamed file.
|
|
5418
|
-
*
|
|
5419
|
-
* @param nodeFile - The file change object.
|
|
5420
|
-
* @param commit - The commit hash or '--staged'.
|
|
5421
|
-
* @param git - The SimpleGit instance.
|
|
5422
|
-
* @param logger - The logger instance.
|
|
5423
|
-
* @returns A Promise that resolves to the diff string.
|
|
5424
|
-
*/
|
|
5425
|
-
async function parseRenamedFileDiff(nodeFile, commit, git, logger) {
|
|
5426
|
-
let result = '';
|
|
5427
|
-
const oldFilePath = nodeFile?.oldFilePath || nodeFile.filePath;
|
|
5428
|
-
let previousCommitHash = 'HEAD';
|
|
5429
|
-
let newCommitHash = '';
|
|
5430
|
-
if (commit !== '--staged') {
|
|
5431
|
-
try {
|
|
5432
|
-
previousCommitHash = await git.revparse([`${commit}~1`]);
|
|
5433
|
-
}
|
|
5434
|
-
catch (err) {
|
|
5435
|
-
logger.verbose(`Error getting previous commit hash for ${nodeFile.filePath}`, {
|
|
5436
|
-
color: 'red',
|
|
5437
|
-
});
|
|
5438
|
-
}
|
|
5439
|
-
newCommitHash = commit;
|
|
5440
|
-
}
|
|
5441
|
-
try {
|
|
5442
|
-
const [previousContent, newContent] = await Promise.all([
|
|
5443
|
-
git.show([`${previousCommitHash}:${oldFilePath}`]),
|
|
5444
|
-
git.show([`${newCommitHash}:${nodeFile.filePath}`]),
|
|
5445
|
-
]);
|
|
5446
|
-
if (previousContent !== newContent) {
|
|
5447
|
-
result = diff.createTwoFilesPatch(oldFilePath, nodeFile.filePath, previousContent, newContent, '', '', {
|
|
5448
|
-
context: 3,
|
|
5449
|
-
});
|
|
5450
|
-
// remove the first 4 lines of the patch (they contain the old and new file names)
|
|
5451
|
-
result = result.split('\n').slice(4).join('\n');
|
|
5452
|
-
}
|
|
5453
|
-
else {
|
|
5454
|
-
result = 'File contents are unchanged.';
|
|
5455
|
-
}
|
|
5456
|
-
}
|
|
5457
|
-
catch (err) {
|
|
5458
|
-
logger.verbose(`Error comparing file contents for ${nodeFile.filePath}`, { color: 'red' });
|
|
5459
|
-
result = 'Error comparing file contents.';
|
|
5460
|
-
}
|
|
5461
|
-
return result;
|
|
5462
|
-
}
|
|
5463
|
-
/**
|
|
5464
|
-
* Retrieves the diff for a given file change in a specific commit.
|
|
5465
|
-
* If the file is deleted, it returns a message indicating that the file has been deleted.
|
|
5466
|
-
* If the file is renamed, it parses the renamed file diff and returns it.
|
|
5467
|
-
* Otherwise, it retrieves the default diff from the index and returns it.
|
|
5468
|
-
*
|
|
5469
|
-
* @param nodeFile - The file change object.
|
|
5470
|
-
* @param commit - The commit hash.
|
|
5471
|
-
* @param git - The SimpleGit instance.
|
|
5472
|
-
* @param logger - The logger instance.
|
|
5473
|
-
* @returns A promise that resolves to the diff as a string.
|
|
5474
|
-
*/
|
|
5475
|
-
async function getDiff(nodeFile, commit, { git, logger, }) {
|
|
5476
|
-
if (nodeFile.status === 'deleted') {
|
|
5477
|
-
return 'This file has been deleted.';
|
|
5478
|
-
}
|
|
5479
|
-
if (nodeFile.status === 'renamed' && nodeFile.oldFilePath) {
|
|
5480
|
-
const renamedDiff = await parseRenamedFileDiff(nodeFile, commit, git, logger);
|
|
5481
|
-
return renamedDiff;
|
|
5482
|
-
}
|
|
5483
|
-
// If not deleted or renamed, get the diff from the index
|
|
5484
|
-
const defaultDiff = await parseDefaultFileDiff(nodeFile, commit, git);
|
|
5485
|
-
return defaultDiff;
|
|
5486
|
-
}
|
|
5487
|
-
|
|
5488
|
-
// Max tokens for GPT-3 is 4096
|
|
5489
|
-
// const MAX_TOKENS_PER_SUMMARY = 4096
|
|
5490
|
-
const MAX_TOKENS_PER_SUMMARY = 12288;
|
|
5491
|
-
async function fileChangeParser({ changes, commit, options: { tokenizer, git, llm: model, logger }, }) {
|
|
5492
|
-
const textSplitter = getTextSplitter({ chunkSize: 10000, chunkOverlap: 250 });
|
|
5493
|
-
// const textSplitter = new TokenTextSplitter({
|
|
5494
|
-
// chunkSize: 10000,
|
|
5495
|
-
// chunkOverlap: 250,
|
|
5496
|
-
// });
|
|
5497
|
-
const summarizationChain = getSummarizationChain(model, {
|
|
5498
|
-
type: 'map_reduce',
|
|
5499
|
-
combineMapPrompt: SUMMARIZE_PROMPT,
|
|
5500
|
-
combinePrompt: SUMMARIZE_PROMPT,
|
|
5501
|
-
});
|
|
5502
|
-
logger.startTimer();
|
|
5503
|
-
const rootTreeNode = createDiffTree(changes);
|
|
5504
|
-
logger.stopTimer('Created file hierarchy');
|
|
5505
|
-
// Collect diffs
|
|
5506
|
-
logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
|
|
5507
|
-
const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, commit, { git, logger }), tokenizer, logger);
|
|
5508
|
-
logger.stopSpinner('Diffs Collected').stopTimer();
|
|
5509
|
-
// Summarize diffs
|
|
5510
|
-
logger.startTimer();
|
|
5511
|
-
const summary = await summarizeDiffs(diffs, {
|
|
5512
|
-
tokenizer,
|
|
5513
|
-
maxTokens: MAX_TOKENS_PER_SUMMARY,
|
|
5514
|
-
textSplitter,
|
|
5515
|
-
chain: summarizationChain,
|
|
5516
|
-
logger,
|
|
5517
|
-
});
|
|
5518
|
-
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
5519
|
-
return summary;
|
|
5520
|
-
}
|
|
5521
|
-
|
|
5522
|
-
const template$1 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
|
|
5523
|
-
Commit Messages must have a short description that is less than 50 characters and a longer detailed summary no more than 300 characters, the shorter and more concise the better. Please follow the guidelines below when writing your commit message:
|
|
5524
|
-
|
|
5525
|
-
- Write concisely using an informal tone
|
|
5526
|
-
- DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
|
|
5527
|
-
- DO NOT use specific names or files from the code
|
|
5528
|
-
- DO NOT include any diffs or file changes in the commit message
|
|
5529
|
-
- Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
|
|
5530
|
-
|
|
5531
|
-
{format_instructions}
|
|
5532
|
-
|
|
5533
|
-
"""{summary}"""`;
|
|
5534
|
-
const inputVariables$1 = ['summary', 'format_instructions'];
|
|
5535
|
-
const COMMIT_PROMPT = new prompts.PromptTemplate({
|
|
5536
|
-
template: template$1,
|
|
5537
|
-
inputVariables: inputVariables$1,
|
|
5538
|
-
});
|
|
5539
|
-
|
|
5540
|
-
/**
|
|
5541
|
-
* Get LLM Model Based on Configuration
|
|
5542
|
-
*
|
|
5543
|
-
* @param fields
|
|
5544
|
-
* @param configuration
|
|
5545
|
-
* @returns LLM Model
|
|
5546
|
-
*/
|
|
5547
|
-
function getLlm(provider, model, config) {
|
|
5548
|
-
if (!model) {
|
|
5549
|
-
throw new Error(`Invalid LLM Service: ${provider}/${model}`);
|
|
5550
|
-
}
|
|
5551
|
-
switch (provider) {
|
|
5552
|
-
case 'ollama':
|
|
5553
|
-
return new ollama.ChatOllama({
|
|
5554
|
-
baseUrl: DEFAULT_OLLAMA_LLM_SERVICE.endpoint,
|
|
5555
|
-
maxConcurrency: config.service.maxConcurrent,
|
|
5556
|
-
model,
|
|
5557
|
-
});
|
|
5558
|
-
case 'openai':
|
|
5559
|
-
default:
|
|
5560
|
-
const openAiModel = new openai.ChatOpenAI({
|
|
5561
|
-
openAIApiKey: getApiKeyForModel(config),
|
|
5562
|
-
model,
|
|
5563
|
-
temperature: config.service.temperature || 0.2,
|
|
5564
|
-
});
|
|
5565
|
-
return openAiModel;
|
|
5566
|
-
}
|
|
5567
|
-
}
|
|
5568
|
-
|
|
5569
|
-
function getPrompt({ template, variables, fallback }) {
|
|
5570
|
-
if (!template && !fallback)
|
|
5571
|
-
throw new Error('Must provide either a template or a fallback');
|
|
5572
|
-
return (template
|
|
5573
|
-
? new prompts.PromptTemplate({
|
|
5574
|
-
template,
|
|
5575
|
-
inputVariables: variables,
|
|
5576
|
-
})
|
|
5577
|
-
: fallback);
|
|
5578
|
-
}
|
|
5579
|
-
|
|
5580
|
-
/**
|
|
5581
|
-
* Determines the status of a file based on its changes in the Git repository.
|
|
5582
|
-
*
|
|
5583
|
-
* @param file - The file to check the status of.
|
|
5584
|
-
* @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
|
|
5585
|
-
* @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
|
|
5586
|
-
* @throws Error if the file type is invalid.
|
|
5955
|
+
* Get Summarization Chain
|
|
5956
|
+
* @param model
|
|
5957
|
+
* @param options
|
|
5958
|
+
* @returns
|
|
5587
5959
|
*/
|
|
5588
|
-
function
|
|
5589
|
-
|
|
5590
|
-
const statusCode = file[location];
|
|
5591
|
-
switch (statusCode) {
|
|
5592
|
-
case 'A':
|
|
5593
|
-
return 'added';
|
|
5594
|
-
case 'D':
|
|
5595
|
-
return 'deleted';
|
|
5596
|
-
case 'M':
|
|
5597
|
-
return 'modified';
|
|
5598
|
-
case 'R':
|
|
5599
|
-
return 'renamed';
|
|
5600
|
-
case '?':
|
|
5601
|
-
return 'untracked';
|
|
5602
|
-
default:
|
|
5603
|
-
return 'unknown';
|
|
5604
|
-
}
|
|
5605
|
-
}
|
|
5606
|
-
else if ('changes' in file && 'binary' in file) {
|
|
5607
|
-
if (file.changes === 0)
|
|
5608
|
-
return 'untracked';
|
|
5609
|
-
if (file.file.includes('=>'))
|
|
5610
|
-
return 'renamed';
|
|
5611
|
-
if (file.deletions === 0 && file.insertions > 0)
|
|
5612
|
-
return 'added';
|
|
5613
|
-
if (file.insertions === 0 && file.deletions > 0)
|
|
5614
|
-
return 'deleted';
|
|
5615
|
-
if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
|
|
5616
|
-
return 'modified';
|
|
5617
|
-
return 'unknown';
|
|
5618
|
-
}
|
|
5619
|
-
else {
|
|
5620
|
-
throw new Error('Invalid file type');
|
|
5621
|
-
}
|
|
5960
|
+
function getSummarizationChain(model, options = { type: 'map_reduce' }) {
|
|
5961
|
+
return loadSummarizationChain(model, options);
|
|
5622
5962
|
}
|
|
5623
5963
|
|
|
5624
5964
|
/**
|
|
5625
|
-
*
|
|
5965
|
+
* Get Recursive Character Text Splitter
|
|
5626
5966
|
*
|
|
5627
|
-
* @param
|
|
5628
|
-
* @
|
|
5629
|
-
* @returns The summary text for the file change.
|
|
5630
|
-
* @throws Error if the file type is invalid.
|
|
5967
|
+
* @param options
|
|
5968
|
+
* @returns
|
|
5631
5969
|
*/
|
|
5632
|
-
function
|
|
5633
|
-
|
|
5634
|
-
let filePath;
|
|
5635
|
-
if ('path' in file) {
|
|
5636
|
-
filePath = file.path;
|
|
5637
|
-
}
|
|
5638
|
-
else if ('file' in file) {
|
|
5639
|
-
filePath = change?.filePath || file.file;
|
|
5640
|
-
}
|
|
5641
|
-
else {
|
|
5642
|
-
throw new Error('Invalid file type');
|
|
5643
|
-
}
|
|
5644
|
-
if (change.oldFilePath) {
|
|
5645
|
-
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
5646
|
-
}
|
|
5647
|
-
return `${status}: ${filePath}`;
|
|
5970
|
+
function getTextSplitter(options = {}) {
|
|
5971
|
+
return new RecursiveCharacterTextSplitter(options);
|
|
5648
5972
|
}
|
|
5649
5973
|
|
|
5650
5974
|
/**
|
|
5651
|
-
*
|
|
5975
|
+
* Parses the default file diff for a given nodeFile.
|
|
5652
5976
|
*
|
|
5653
|
-
* @param
|
|
5654
|
-
* @
|
|
5977
|
+
* @param nodeFile - The file change object.
|
|
5978
|
+
* @param commit - The commit to diff against. Defaults to '--staged'.
|
|
5979
|
+
* @param git - The SimpleGit instance.
|
|
5980
|
+
* @returns A Promise that resolves to the file diff as a string.
|
|
5655
5981
|
*/
|
|
5656
|
-
async function
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
const unstaged = [];
|
|
5660
|
-
const untracked = [];
|
|
5661
|
-
const status = await git.status();
|
|
5662
|
-
status.files.forEach((file) => {
|
|
5663
|
-
const fileChange = {
|
|
5664
|
-
filePath: file.path,
|
|
5665
|
-
oldFilePath: status.renamed.filter((renamed) => renamed.to === file.path)[0]?.from,
|
|
5666
|
-
};
|
|
5667
|
-
// Unstaged files
|
|
5668
|
-
if (file.working_dir !== '?' && file.working_dir !== ' ') {
|
|
5669
|
-
fileChange.status = getStatus(file, 'working_dir');
|
|
5670
|
-
fileChange.summary = getSummaryText(file, fileChange);
|
|
5671
|
-
unstaged.push(fileChange);
|
|
5672
|
-
}
|
|
5673
|
-
// Staged files
|
|
5674
|
-
if (file.index !== ' ' && file.index !== '?') {
|
|
5675
|
-
fileChange.status = getStatus(file);
|
|
5676
|
-
fileChange.summary = getSummaryText(file, fileChange);
|
|
5677
|
-
staged.push(fileChange);
|
|
5678
|
-
}
|
|
5679
|
-
// Untracked files
|
|
5680
|
-
if (file.working_dir === '?' && file.index === '?') {
|
|
5681
|
-
fileChange.status = 'added';
|
|
5682
|
-
fileChange.summary = getSummaryText(file, fileChange);
|
|
5683
|
-
untracked.push(fileChange);
|
|
5684
|
-
}
|
|
5685
|
-
});
|
|
5686
|
-
const ignoredExtensionsSet = new Set(ignoredExtensions.map((extension) => extension.toLowerCase()));
|
|
5687
|
-
const filteredStaged = staged.filter((file) => {
|
|
5688
|
-
const extension = path.extname(file.filePath).toLowerCase();
|
|
5689
|
-
return (!ignoredExtensionsSet.has(extension) &&
|
|
5690
|
-
!ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
|
|
5691
|
-
});
|
|
5692
|
-
const filteredUnstaged = unstaged.filter((file) => {
|
|
5693
|
-
const extension = path.extname(file.filePath).toLowerCase();
|
|
5694
|
-
return (!ignoredExtensionsSet.has(extension) &&
|
|
5695
|
-
!ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
|
|
5696
|
-
});
|
|
5697
|
-
const filteredUntracked = untracked.filter((file) => {
|
|
5698
|
-
const extension = path.extname(file.filePath).toLowerCase();
|
|
5699
|
-
return (!ignoredExtensionsSet.has(extension) &&
|
|
5700
|
-
!ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
|
|
5701
|
-
});
|
|
5702
|
-
return {
|
|
5703
|
-
staged: filteredStaged,
|
|
5704
|
-
unstaged: filteredUnstaged,
|
|
5705
|
-
untracked: filteredUntracked,
|
|
5706
|
-
};
|
|
5707
|
-
}
|
|
5708
|
-
|
|
5709
|
-
async function noResult({ git, logger }) {
|
|
5710
|
-
const { staged, unstaged, untracked } = await getChanges({ git });
|
|
5711
|
-
const hasStaged = staged && staged.length > 0;
|
|
5712
|
-
const hasUnstaged = unstaged && unstaged.length > 0;
|
|
5713
|
-
const hasUntracked = untracked && untracked.length > 0;
|
|
5714
|
-
if (hasStaged) {
|
|
5715
|
-
logger.log(`Staged files detected, but no summary generated...`, { color: 'red' });
|
|
5716
|
-
logger.log(`Files are likely either:\n • changed files are ignored\n • file diff is too large.`, { color: 'yellow' });
|
|
5717
|
-
}
|
|
5718
|
-
else if (hasUnstaged || hasUntracked) {
|
|
5719
|
-
logger.log('Forget something? No staged changes found... 👻', { color: 'red' });
|
|
5720
|
-
if (hasUnstaged) {
|
|
5721
|
-
logger.log('\nChanges not staged for commit:', { color: 'yellow' });
|
|
5722
|
-
logger.verbose(`\t${unstaged.map(({ summary }) => summary).join('\n\t')}`, {
|
|
5723
|
-
color: 'red',
|
|
5724
|
-
});
|
|
5725
|
-
}
|
|
5726
|
-
if (hasUntracked) {
|
|
5727
|
-
logger.log('\nUntracked changes:', { color: 'yellow' });
|
|
5728
|
-
logger.verbose(`\t${untracked.map(({ summary }) => summary).join('\n\t')}`, {
|
|
5729
|
-
color: 'red',
|
|
5730
|
-
});
|
|
5731
|
-
}
|
|
5732
|
-
}
|
|
5733
|
-
else {
|
|
5734
|
-
logger.log('No repo changes detected. 👀', { color: 'blue' });
|
|
5982
|
+
async function parseDefaultFileDiff(nodeFile, commit = '--staged', git) {
|
|
5983
|
+
if (commit !== '--staged') {
|
|
5984
|
+
return await git.diff([`${commit}~1..${commit}`, '--', nodeFile.filePath]);
|
|
5735
5985
|
}
|
|
5986
|
+
return await git.diff([commit, nodeFile.filePath]);
|
|
5736
5987
|
}
|
|
5737
|
-
|
|
5738
5988
|
/**
|
|
5739
|
-
*
|
|
5989
|
+
* Parses the diff for a renamed file.
|
|
5740
5990
|
*
|
|
5741
|
-
* @param
|
|
5742
|
-
* @param
|
|
5743
|
-
* @
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
}
|
|
5749
|
-
if (!inputVariables.some((entry) => text.includes(entry))) {
|
|
5750
|
-
return ('Prompt template must include at least one of the following input variables: ' +
|
|
5751
|
-
inputVariables.map((value) => `{${value}}`).join(', '));
|
|
5752
|
-
}
|
|
5753
|
-
return true;
|
|
5754
|
-
}
|
|
5755
|
-
|
|
5756
|
-
async function editPrompt(options) {
|
|
5757
|
-
return await prompts$1.editor({
|
|
5758
|
-
message: 'Edit the prompt',
|
|
5759
|
-
default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
|
|
5760
|
-
waitForUseInput: false,
|
|
5761
|
-
postfix: 'Press ENTER to continue',
|
|
5762
|
-
validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
|
|
5763
|
-
});
|
|
5764
|
-
}
|
|
5765
|
-
|
|
5766
|
-
async function editResult(result, options) {
|
|
5767
|
-
if (options.openInEditor) {
|
|
5768
|
-
return await prompts$1.editor({
|
|
5769
|
-
message: 'Edit the commit message',
|
|
5770
|
-
default: result,
|
|
5771
|
-
waitForUseInput: false,
|
|
5772
|
-
validate: (text) => (text ? true : 'Commit message cannot be empty'),
|
|
5773
|
-
});
|
|
5774
|
-
}
|
|
5775
|
-
return result;
|
|
5776
|
-
}
|
|
5777
|
-
|
|
5778
|
-
async function getUserReviewDecision({ label, descriptions, enableRetry = true, enableFullRetry = true, enableModifyPrompt = true, }) {
|
|
5779
|
-
const choices = [
|
|
5780
|
-
{
|
|
5781
|
-
name: '✨ Looks good!',
|
|
5782
|
-
value: 'approve',
|
|
5783
|
-
description: descriptions?.approve || `Continue with the generated ${label}`,
|
|
5784
|
-
},
|
|
5785
|
-
{
|
|
5786
|
-
name: '📝 Edit',
|
|
5787
|
-
value: 'edit',
|
|
5788
|
-
description: descriptions?.edit || `Edit the generated ${label} before proceeding`,
|
|
5789
|
-
},
|
|
5790
|
-
];
|
|
5791
|
-
if (enableModifyPrompt) {
|
|
5792
|
-
choices.push({
|
|
5793
|
-
name: '🪶 Modify Prompt',
|
|
5794
|
-
value: 'modifyPrompt',
|
|
5795
|
-
description: descriptions?.modifyPrompt || `Modify the prompt template and regenerate the ${label}`,
|
|
5796
|
-
});
|
|
5797
|
-
}
|
|
5798
|
-
if (enableRetry) {
|
|
5799
|
-
choices.push({
|
|
5800
|
-
name: '🔄 Retry',
|
|
5801
|
-
value: 'retryMessageOnly',
|
|
5802
|
-
description: descriptions?.retryMessageOnly ||
|
|
5803
|
-
`Restart the function execution from generating the ${label}`,
|
|
5804
|
-
});
|
|
5805
|
-
}
|
|
5806
|
-
if (enableFullRetry) {
|
|
5807
|
-
choices.push({
|
|
5808
|
-
name: '🔄 Retry Full',
|
|
5809
|
-
value: 'retryFull',
|
|
5810
|
-
description: descriptions?.retryFull ||
|
|
5811
|
-
`Restart the function execution from the beginning, regenerating both the summary and ${label}`,
|
|
5812
|
-
});
|
|
5813
|
-
}
|
|
5814
|
-
choices.push({
|
|
5815
|
-
name: '💣 Cancel',
|
|
5816
|
-
value: 'cancel',
|
|
5817
|
-
description: descriptions?.cancel || `Cancel the ${label}`,
|
|
5818
|
-
});
|
|
5819
|
-
return (await prompts$1.select({
|
|
5820
|
-
message: `Would you like to make any changes to the ${label}?`,
|
|
5821
|
-
choices,
|
|
5822
|
-
}));
|
|
5823
|
-
}
|
|
5824
|
-
|
|
5825
|
-
function logResult(label, result) {
|
|
5826
|
-
console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
|
|
5827
|
-
}
|
|
5828
|
-
|
|
5829
|
-
async function generateAndReviewLoop({ label, factory, parser, noResult, agent, options, }) {
|
|
5830
|
-
const { logger } = options;
|
|
5831
|
-
let continueLoop = true;
|
|
5832
|
-
let modifyPrompt = false;
|
|
5833
|
-
let context = '';
|
|
5991
|
+
* @param nodeFile - The file change object.
|
|
5992
|
+
* @param commit - The commit hash or '--staged'.
|
|
5993
|
+
* @param git - The SimpleGit instance.
|
|
5994
|
+
* @param logger - The logger instance.
|
|
5995
|
+
* @returns A Promise that resolves to the diff string.
|
|
5996
|
+
*/
|
|
5997
|
+
async function parseRenamedFileDiff(nodeFile, commit, git, logger) {
|
|
5834
5998
|
let result = '';
|
|
5835
|
-
const
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
if (!context.length) {
|
|
5842
|
-
context = await parser(changes, result, options);
|
|
5843
|
-
}
|
|
5844
|
-
// if we still don't have a context, bail.
|
|
5845
|
-
if (!context.length) {
|
|
5846
|
-
await noResult(options);
|
|
5847
|
-
}
|
|
5848
|
-
if (modifyPrompt) {
|
|
5849
|
-
options.prompt = await editPrompt(options);
|
|
5999
|
+
const oldFilePath = nodeFile?.oldFilePath || nodeFile.filePath;
|
|
6000
|
+
let previousCommitHash = 'HEAD';
|
|
6001
|
+
let newCommitHash = '';
|
|
6002
|
+
if (commit !== '--staged') {
|
|
6003
|
+
try {
|
|
6004
|
+
previousCommitHash = await git.revparse([`${commit}~1`]);
|
|
5850
6005
|
}
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
});
|
|
5854
|
-
result = await agent(context, options);
|
|
5855
|
-
if (!result) {
|
|
5856
|
-
logger.stopSpinner('💀 Agent failed to return content.', {
|
|
5857
|
-
mode: 'fail',
|
|
6006
|
+
catch (err) {
|
|
6007
|
+
logger.verbose(`Error getting previous commit hash for ${nodeFile.filePath}`, {
|
|
5858
6008
|
color: 'red',
|
|
5859
6009
|
});
|
|
5860
|
-
process.exit(0);
|
|
5861
6010
|
}
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
.
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
...(options?.review || {}),
|
|
6011
|
+
newCommitHash = commit;
|
|
6012
|
+
}
|
|
6013
|
+
try {
|
|
6014
|
+
const [previousContent, newContent] = await Promise.all([
|
|
6015
|
+
git.show([`${previousCommitHash}:${oldFilePath}`]),
|
|
6016
|
+
git.show([`${newCommitHash}:${nodeFile.filePath}`]),
|
|
6017
|
+
]);
|
|
6018
|
+
if (previousContent !== newContent) {
|
|
6019
|
+
result = diff.createTwoFilesPatch(oldFilePath, nodeFile.filePath, previousContent, newContent, '', '', {
|
|
6020
|
+
context: 3,
|
|
5873
6021
|
});
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
}
|
|
5880
|
-
if (reviewAnswer === 'retryFull') {
|
|
5881
|
-
context = '';
|
|
5882
|
-
result = '';
|
|
5883
|
-
options.prompt = '';
|
|
5884
|
-
continue;
|
|
5885
|
-
}
|
|
5886
|
-
if (reviewAnswer === 'retryMessageOnly') {
|
|
5887
|
-
modifyPrompt = false;
|
|
5888
|
-
result = '';
|
|
5889
|
-
continue;
|
|
5890
|
-
}
|
|
5891
|
-
if (reviewAnswer === 'modifyPrompt') {
|
|
5892
|
-
modifyPrompt = true;
|
|
5893
|
-
result = '';
|
|
5894
|
-
continue;
|
|
5895
|
-
}
|
|
6022
|
+
// remove the first 4 lines of the patch (they contain the old and new file names)
|
|
6023
|
+
result = result.split('\n').slice(4).join('\n');
|
|
6024
|
+
}
|
|
6025
|
+
else {
|
|
6026
|
+
result = 'File contents are unchanged.';
|
|
5896
6027
|
}
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
6028
|
+
}
|
|
6029
|
+
catch (err) {
|
|
6030
|
+
logger.verbose(`Error comparing file contents for ${nodeFile.filePath}`, { color: 'red' });
|
|
6031
|
+
result = 'Error comparing file contents.';
|
|
5900
6032
|
}
|
|
5901
6033
|
return result;
|
|
5902
6034
|
}
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
6035
|
+
/**
|
|
6036
|
+
* Retrieves the diff for a given file change in a specific commit.
|
|
6037
|
+
* If the file is deleted, it returns a message indicating that the file has been deleted.
|
|
6038
|
+
* If the file is renamed, it parses the renamed file diff and returns it.
|
|
6039
|
+
* Otherwise, it retrieves the default diff from the index and returns it.
|
|
6040
|
+
*
|
|
6041
|
+
* @param nodeFile - The file change object.
|
|
6042
|
+
* @param commit - The commit hash.
|
|
6043
|
+
* @param git - The SimpleGit instance.
|
|
6044
|
+
* @param logger - The logger instance.
|
|
6045
|
+
* @returns A promise that resolves to the diff as a string.
|
|
6046
|
+
*/
|
|
6047
|
+
async function getDiff(nodeFile, commit, { git, logger, }) {
|
|
6048
|
+
if (nodeFile.status === 'deleted') {
|
|
6049
|
+
return 'This file has been deleted.';
|
|
5907
6050
|
}
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
res = await chain.invoke(variables);
|
|
6051
|
+
if (nodeFile.status === 'renamed' && nodeFile.oldFilePath) {
|
|
6052
|
+
const renamedDiff = await parseRenamedFileDiff(nodeFile, commit, git, logger);
|
|
6053
|
+
return renamedDiff;
|
|
5912
6054
|
}
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
6055
|
+
// If not deleted or renamed, get the diff from the index
|
|
6056
|
+
const defaultDiff = await parseDefaultFileDiff(nodeFile, commit, git);
|
|
6057
|
+
return defaultDiff;
|
|
6058
|
+
}
|
|
6059
|
+
|
|
6060
|
+
// Max tokens for GPT-3 is 4096
|
|
6061
|
+
// const MAX_TOKENS_PER_SUMMARY = 4096
|
|
6062
|
+
const MAX_TOKENS_PER_SUMMARY = 12288;
|
|
6063
|
+
async function fileChangeParser({ changes, commit, options: { tokenizer, git, llm: model, logger }, }) {
|
|
6064
|
+
const textSplitter = getTextSplitter({ chunkSize: 10000, chunkOverlap: 250 });
|
|
6065
|
+
// const textSplitter = new TokenTextSplitter({
|
|
6066
|
+
// chunkSize: 10000,
|
|
6067
|
+
// chunkOverlap: 250,
|
|
6068
|
+
// });
|
|
6069
|
+
const summarizationChain = getSummarizationChain(model, {
|
|
6070
|
+
type: 'map_reduce',
|
|
6071
|
+
combineMapPrompt: SUMMARIZE_PROMPT,
|
|
6072
|
+
combinePrompt: SUMMARIZE_PROMPT,
|
|
6073
|
+
});
|
|
6074
|
+
logger.startTimer();
|
|
6075
|
+
const rootTreeNode = createDiffTree(changes);
|
|
6076
|
+
logger.stopTimer('Created file hierarchy');
|
|
6077
|
+
// Collect diffs
|
|
6078
|
+
logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
|
|
6079
|
+
const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, commit, { git, logger }), tokenizer, logger);
|
|
6080
|
+
logger.stopSpinner('Diffs Collected').stopTimer();
|
|
6081
|
+
// Summarize diffs
|
|
6082
|
+
logger.startTimer();
|
|
6083
|
+
const summary = await summarizeDiffs(diffs, {
|
|
6084
|
+
tokenizer,
|
|
6085
|
+
maxTokens: MAX_TOKENS_PER_SUMMARY,
|
|
6086
|
+
textSplitter,
|
|
6087
|
+
chain: summarizationChain,
|
|
6088
|
+
logger,
|
|
6089
|
+
});
|
|
6090
|
+
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
6091
|
+
return summary;
|
|
6092
|
+
}
|
|
6093
|
+
|
|
6094
|
+
/**
|
|
6095
|
+
* Creates a commit with the specified commit message.
|
|
6096
|
+
*
|
|
6097
|
+
* @param message The commit message.
|
|
6098
|
+
* @param git The SimpleGit instance.
|
|
6099
|
+
* @returns A Promise that resolves to the CommitResult.
|
|
6100
|
+
*/
|
|
6101
|
+
async function createCommit(message, git) {
|
|
6102
|
+
return await git.commit(message);
|
|
6103
|
+
}
|
|
6104
|
+
|
|
6105
|
+
/**
|
|
6106
|
+
* Determines the status of a file based on its changes in the Git repository.
|
|
6107
|
+
*
|
|
6108
|
+
* @param file - The file to check the status of.
|
|
6109
|
+
* @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
|
|
6110
|
+
* @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
|
|
6111
|
+
* @throws Error if the file type is invalid.
|
|
6112
|
+
*/
|
|
6113
|
+
function getStatus(file, location = 'index') {
|
|
6114
|
+
if ('index' in file && 'working_dir' in file) {
|
|
6115
|
+
const statusCode = file[location];
|
|
6116
|
+
switch (statusCode) {
|
|
6117
|
+
case 'A':
|
|
6118
|
+
return 'added';
|
|
6119
|
+
case 'D':
|
|
6120
|
+
return 'deleted';
|
|
6121
|
+
case 'M':
|
|
6122
|
+
return 'modified';
|
|
6123
|
+
case 'R':
|
|
6124
|
+
return 'renamed';
|
|
6125
|
+
case '?':
|
|
6126
|
+
return 'untracked';
|
|
6127
|
+
default:
|
|
6128
|
+
return 'unknown';
|
|
5916
6129
|
}
|
|
5917
6130
|
}
|
|
5918
|
-
if (
|
|
5919
|
-
|
|
6131
|
+
else if ('changes' in file && 'binary' in file) {
|
|
6132
|
+
if (file.changes === 0)
|
|
6133
|
+
return 'untracked';
|
|
6134
|
+
if (file.file.includes('=>'))
|
|
6135
|
+
return 'renamed';
|
|
6136
|
+
if (file.deletions === 0 && file.insertions > 0)
|
|
6137
|
+
return 'added';
|
|
6138
|
+
if (file.insertions === 0 && file.deletions > 0)
|
|
6139
|
+
return 'deleted';
|
|
6140
|
+
if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
|
|
6141
|
+
return 'modified';
|
|
6142
|
+
return 'unknown';
|
|
5920
6143
|
}
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
|
|
5926
|
-
};
|
|
6144
|
+
else {
|
|
6145
|
+
throw new Error('Invalid file type');
|
|
6146
|
+
}
|
|
6147
|
+
}
|
|
5927
6148
|
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
process.stdout.write(result, 'utf8');
|
|
5942
|
-
break;
|
|
6149
|
+
/**
|
|
6150
|
+
* Returns the summary text for a file change.
|
|
6151
|
+
*
|
|
6152
|
+
* @param file - The file status or diff result.
|
|
6153
|
+
* @param change - The partial file change object.
|
|
6154
|
+
* @returns The summary text for the file change.
|
|
6155
|
+
* @throws Error if the file type is invalid.
|
|
6156
|
+
*/
|
|
6157
|
+
function getSummaryText(file, change) {
|
|
6158
|
+
const status = change.status || getStatus(file);
|
|
6159
|
+
let filePath;
|
|
6160
|
+
if ('path' in file) {
|
|
6161
|
+
filePath = file.path;
|
|
5943
6162
|
}
|
|
5944
|
-
|
|
6163
|
+
else if ('file' in file) {
|
|
6164
|
+
filePath = change?.filePath || file.file;
|
|
6165
|
+
}
|
|
6166
|
+
else {
|
|
6167
|
+
throw new Error('Invalid file type');
|
|
6168
|
+
}
|
|
6169
|
+
if (change.oldFilePath) {
|
|
6170
|
+
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
6171
|
+
}
|
|
6172
|
+
return `${status}: ${filePath}`;
|
|
5945
6173
|
}
|
|
5946
6174
|
|
|
5947
6175
|
/**
|
|
5948
|
-
* Retrieves the
|
|
5949
|
-
*
|
|
6176
|
+
* Retrieves the changes in the Git repository.
|
|
6177
|
+
*
|
|
6178
|
+
* @param {GetChangesInput} options - The options for retrieving the changes.
|
|
6179
|
+
* @returns {Promise<GetChangesResult>} A promise that resolves to the changes in the Git repository.
|
|
5950
6180
|
*/
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
};
|
|
6181
|
+
async function getChanges({ git, options }) {
|
|
6182
|
+
const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options || {};
|
|
6183
|
+
const staged = [];
|
|
6184
|
+
const unstaged = [];
|
|
6185
|
+
const untracked = [];
|
|
6186
|
+
const status = await git.status();
|
|
6187
|
+
status.files.forEach((file) => {
|
|
6188
|
+
const fileChange = {
|
|
6189
|
+
filePath: file.path,
|
|
6190
|
+
oldFilePath: status.renamed.filter((renamed) => renamed.to === file.path)[0]?.from,
|
|
6191
|
+
};
|
|
6192
|
+
// Unstaged files
|
|
6193
|
+
if (file.working_dir !== '?' && file.working_dir !== ' ') {
|
|
6194
|
+
fileChange.status = getStatus(file, 'working_dir');
|
|
6195
|
+
fileChange.summary = getSummaryText(file, fileChange);
|
|
6196
|
+
unstaged.push(fileChange);
|
|
6197
|
+
}
|
|
6198
|
+
// Staged files
|
|
6199
|
+
if (file.index !== ' ' && file.index !== '?') {
|
|
6200
|
+
fileChange.status = getStatus(file);
|
|
6201
|
+
fileChange.summary = getSummaryText(file, fileChange);
|
|
6202
|
+
staged.push(fileChange);
|
|
6203
|
+
}
|
|
6204
|
+
// Untracked files
|
|
6205
|
+
if (file.working_dir === '?' && file.index === '?') {
|
|
6206
|
+
fileChange.status = 'added';
|
|
6207
|
+
fileChange.summary = getSummaryText(file, fileChange);
|
|
6208
|
+
untracked.push(fileChange);
|
|
6209
|
+
}
|
|
6210
|
+
});
|
|
6211
|
+
const ignoredExtensionsSet = new Set(ignoredExtensions.map((extension) => extension.toLowerCase()));
|
|
6212
|
+
const filteredStaged = staged.filter((file) => {
|
|
6213
|
+
const extension = path.extname(file.filePath).toLowerCase();
|
|
6214
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
6215
|
+
!ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
|
|
6216
|
+
});
|
|
6217
|
+
const filteredUnstaged = unstaged.filter((file) => {
|
|
6218
|
+
const extension = path.extname(file.filePath).toLowerCase();
|
|
6219
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
6220
|
+
!ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
|
|
6221
|
+
});
|
|
6222
|
+
const filteredUntracked = untracked.filter((file) => {
|
|
6223
|
+
const extension = path.extname(file.filePath).toLowerCase();
|
|
6224
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
6225
|
+
!ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
|
|
6226
|
+
});
|
|
6227
|
+
return {
|
|
6228
|
+
staged: filteredStaged,
|
|
6229
|
+
unstaged: filteredUnstaged,
|
|
6230
|
+
untracked: filteredUntracked,
|
|
6231
|
+
};
|
|
6232
|
+
}
|
|
5962
6233
|
|
|
5963
6234
|
/**
|
|
5964
6235
|
* Retrieves a TikToken for the specified model.
|
|
@@ -5982,331 +6253,112 @@ const getTokenCounter = async (modelName) => {
|
|
|
5982
6253
|
});
|
|
5983
6254
|
};
|
|
5984
6255
|
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
if (options.service.authentication.type !== 'None' && !key) {
|
|
6002
|
-
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
6003
|
-
process.exit(1);
|
|
6004
|
-
}
|
|
6005
|
-
const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4');
|
|
6006
|
-
const llm = getLlm(provider, model, options);
|
|
6007
|
-
const INTERACTIVE = isInteractive(options);
|
|
6008
|
-
if (INTERACTIVE) {
|
|
6009
|
-
logger.log(LOGO);
|
|
6010
|
-
}
|
|
6011
|
-
async function factory() {
|
|
6012
|
-
const changes = await getChanges({ git });
|
|
6013
|
-
return changes.staged;
|
|
6014
|
-
}
|
|
6015
|
-
async function parser(changes) {
|
|
6016
|
-
return await fileChangeParser({
|
|
6017
|
-
changes,
|
|
6018
|
-
commit: '--staged',
|
|
6019
|
-
options: { tokenizer, git, llm, logger },
|
|
6020
|
-
});
|
|
6021
|
-
}
|
|
6022
|
-
const commitMsg = await generateAndReviewLoop({
|
|
6023
|
-
label: 'commit message',
|
|
6024
|
-
options: {
|
|
6025
|
-
...options,
|
|
6026
|
-
prompt: options.prompt || COMMIT_PROMPT.template,
|
|
6027
|
-
logger,
|
|
6028
|
-
interactive: INTERACTIVE,
|
|
6029
|
-
review: {
|
|
6030
|
-
descriptions: {
|
|
6031
|
-
approve: `Commit staged changes with generated commit message`,
|
|
6032
|
-
edit: 'Edit the commit message before proceeding',
|
|
6033
|
-
modifyPrompt: 'Modify the prompt template and regenerate the commit message',
|
|
6034
|
-
retryMessageOnly: 'Restart the function execution from generating the commit message',
|
|
6035
|
-
retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
|
|
6036
|
-
},
|
|
6037
|
-
},
|
|
6038
|
-
},
|
|
6039
|
-
factory,
|
|
6040
|
-
parser,
|
|
6041
|
-
agent: async (context, options) => {
|
|
6042
|
-
const parser = new output_parsers.JsonOutputParser();
|
|
6043
|
-
const prompt = getPrompt({
|
|
6044
|
-
template: options.prompt,
|
|
6045
|
-
variables: COMMIT_PROMPT.inputVariables,
|
|
6046
|
-
fallback: COMMIT_PROMPT,
|
|
6047
|
-
});
|
|
6048
|
-
const formatInstructions = "Respond with a valid JSON object, containing two fields: 'title' and 'body'.";
|
|
6049
|
-
const commitMsg = await executeChain({
|
|
6050
|
-
llm,
|
|
6051
|
-
prompt,
|
|
6052
|
-
variables: { summary: context, format_instructions: formatInstructions },
|
|
6053
|
-
parser,
|
|
6054
|
-
});
|
|
6055
|
-
return `${commitMsg.title}\n\n${commitMsg.body}`;
|
|
6056
|
-
},
|
|
6057
|
-
noResult: async () => {
|
|
6058
|
-
await noResult({ git, logger });
|
|
6059
|
-
process.exit(0);
|
|
6060
|
-
},
|
|
6061
|
-
});
|
|
6062
|
-
const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
|
|
6063
|
-
handleResult({
|
|
6064
|
-
result: commitMsg,
|
|
6065
|
-
interactiveHandler: async (result) => {
|
|
6066
|
-
await createCommit(result, git);
|
|
6067
|
-
logSuccess();
|
|
6068
|
-
},
|
|
6069
|
-
mode: MODE,
|
|
6070
|
-
});
|
|
6071
|
-
};
|
|
6072
|
-
|
|
6073
|
-
/**
|
|
6074
|
-
* Command line options via yargs
|
|
6075
|
-
*/
|
|
6076
|
-
const options$2 = {
|
|
6077
|
-
service: { type: 'string', description: 'LLM/Model-Name', choices: ['openai', 'ollama'] },
|
|
6078
|
-
openAIApiKey: {
|
|
6079
|
-
type: 'string',
|
|
6080
|
-
description: 'OpenAI API Key',
|
|
6081
|
-
},
|
|
6082
|
-
tokenLimit: { type: 'number', description: 'Token limit' },
|
|
6083
|
-
prompt: {
|
|
6084
|
-
type: 'string',
|
|
6085
|
-
alias: 'p',
|
|
6086
|
-
description: 'Commit message prompt',
|
|
6087
|
-
},
|
|
6088
|
-
i: {
|
|
6089
|
-
type: 'boolean',
|
|
6090
|
-
alias: 'interactive',
|
|
6091
|
-
description: 'Toggle interactive mode',
|
|
6092
|
-
},
|
|
6093
|
-
s: {
|
|
6094
|
-
type: 'boolean',
|
|
6095
|
-
description: 'Automatically commit staged changes with generated commit message',
|
|
6096
|
-
default: false,
|
|
6097
|
-
},
|
|
6098
|
-
e: {
|
|
6099
|
-
type: 'boolean',
|
|
6100
|
-
alias: 'edit',
|
|
6101
|
-
description: 'Open commit message in editor before proceeding',
|
|
6102
|
-
},
|
|
6103
|
-
summarizePrompt: {
|
|
6104
|
-
type: 'string',
|
|
6105
|
-
description: 'Large file summary prompt',
|
|
6106
|
-
},
|
|
6107
|
-
ignoredFiles: {
|
|
6108
|
-
type: 'array',
|
|
6109
|
-
description: 'Ignored files',
|
|
6110
|
-
},
|
|
6111
|
-
ignoredExtensions: {
|
|
6112
|
-
type: 'array',
|
|
6113
|
-
description: 'Ignored extensions',
|
|
6114
|
-
},
|
|
6115
|
-
};
|
|
6116
|
-
const builder$2 = (yargs) => {
|
|
6117
|
-
return yargs.options(options$2);
|
|
6118
|
-
};
|
|
6119
|
-
|
|
6120
|
-
var commit = {
|
|
6121
|
-
command: 'commit',
|
|
6122
|
-
desc: 'Write a commit message summarizing the staged changes.',
|
|
6123
|
-
builder: builder$2,
|
|
6124
|
-
handler: commandExecutor(handler$2),
|
|
6125
|
-
options: options$2,
|
|
6126
|
-
};
|
|
6127
|
-
|
|
6128
|
-
/**
|
|
6129
|
-
* Retrieves the commit log range between two specified commits.
|
|
6130
|
-
*
|
|
6131
|
-
* @param from - The starting commit.
|
|
6132
|
-
* @param to - The ending commit.
|
|
6133
|
-
* @param options - Additional options for retrieving the commit log range.
|
|
6134
|
-
* @returns A promise that resolves to an array of commit log messages.
|
|
6135
|
-
* @throws If there is an error retrieving the commit log range.
|
|
6136
|
-
*/
|
|
6137
|
-
async function getCommitLogRange(from, to, { noMerges, git }) {
|
|
6138
|
-
try {
|
|
6139
|
-
const logOptions = { from: `${from}^1`, to, '--no-merges': noMerges };
|
|
6140
|
-
const commitLog = await git.log(logOptions);
|
|
6141
|
-
return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n - ${author_name}<${author_email}> (${hash})`);
|
|
6142
|
-
}
|
|
6143
|
-
catch (error) {
|
|
6144
|
-
// If there's an error, handle it appropriately
|
|
6145
|
-
console.error('Error getting commit messages:', error);
|
|
6146
|
-
throw error;
|
|
6147
|
-
}
|
|
6148
|
-
}
|
|
6149
|
-
|
|
6150
|
-
/**
|
|
6151
|
-
* Retrieves the name of the current branch.
|
|
6152
|
-
*
|
|
6153
|
-
* @param {GetCurrentBranchName} options - The options for retrieving the branch name.
|
|
6154
|
-
* @returns {Promise<string>} - A promise that resolves to the name of the current branch.
|
|
6155
|
-
*/
|
|
6156
|
-
async function getCurrentBranchName({ git }) {
|
|
6157
|
-
return await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
6158
|
-
}
|
|
6159
|
-
|
|
6160
|
-
/**
|
|
6161
|
-
* Retrieves the commit log for the current branch.
|
|
6162
|
-
*
|
|
6163
|
-
* @param {Object} options - The options for retrieving the commit log.
|
|
6164
|
-
* @param {SimpleGit} options.git - The SimpleGit instance.
|
|
6165
|
-
* @param {Logger} options.logger - The logger for logging messages.
|
|
6166
|
-
* @param {string} [options.comparisonBranch='main'] - The branch to compare against.
|
|
6167
|
-
* @param {string} [options.comparisonRemote='origin'] - The remote to compare against.
|
|
6168
|
-
* @returns {Promise<string[]>} The array of commit messages in the commit log.
|
|
6169
|
-
*/
|
|
6170
|
-
async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
|
|
6171
|
-
try {
|
|
6172
|
-
// Get the current branch name
|
|
6173
|
-
const branch = await getCurrentBranchName({ git });
|
|
6174
|
-
// Check if the current branch has any commits
|
|
6175
|
-
const hasCommits = (await git.raw(['rev-list', '--count', branch])) !== '0';
|
|
6176
|
-
if (!hasCommits) {
|
|
6177
|
-
logger?.log('No commits on the current branch.');
|
|
6178
|
-
return [];
|
|
6179
|
-
}
|
|
6180
|
-
// Get the list of commits that are unique to the current branch
|
|
6181
|
-
let uniqueCommits;
|
|
6182
|
-
if (comparisonBranch === branch) {
|
|
6183
|
-
// If the comparison branch is the same as the current branch, we compare against the remote.
|
|
6184
|
-
uniqueCommits = (await git.raw(['rev-list', `${comparisonRemote}/${comparisonBranch}..${branch}`]))
|
|
6185
|
-
.split('\n')
|
|
6186
|
-
.filter(Boolean)
|
|
6187
|
-
.reverse();
|
|
6188
|
-
}
|
|
6189
|
-
else {
|
|
6190
|
-
// Your existing code for different branches
|
|
6191
|
-
uniqueCommits = (await git.raw(['rev-list', `${comparisonBranch}..${branch}`]))
|
|
6192
|
-
.split('\n')
|
|
6193
|
-
.filter(Boolean)
|
|
6194
|
-
.reverse();
|
|
6256
|
+
async function noResult$1({ git, logger }) {
|
|
6257
|
+
const { staged, unstaged, untracked } = await getChanges({ git });
|
|
6258
|
+
const hasStaged = staged && staged.length > 0;
|
|
6259
|
+
const hasUnstaged = unstaged && unstaged.length > 0;
|
|
6260
|
+
const hasUntracked = untracked && untracked.length > 0;
|
|
6261
|
+
if (hasStaged) {
|
|
6262
|
+
logger.log(`Staged files detected, but no summary generated...`, { color: 'red' });
|
|
6263
|
+
logger.log(`Files are likely either:\n • changed files are ignored\n • file diff is too large.`, { color: 'yellow' });
|
|
6264
|
+
}
|
|
6265
|
+
else if (hasUnstaged || hasUntracked) {
|
|
6266
|
+
logger.log('Forget something? No staged changes found... 👻', { color: 'red' });
|
|
6267
|
+
if (hasUnstaged) {
|
|
6268
|
+
logger.log('\nChanges not staged for commit:', { color: 'yellow' });
|
|
6269
|
+
logger.verbose(`\t${unstaged.map(({ summary }) => summary).join('\n\t')}`, {
|
|
6270
|
+
color: 'red',
|
|
6271
|
+
});
|
|
6195
6272
|
}
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
return [];
|
|
6273
|
+
if (hasUntracked) {
|
|
6274
|
+
logger.log('\nUntracked changes:', { color: 'yellow' });
|
|
6275
|
+
logger.verbose(`\t${untracked.map(({ summary }) => summary).join('\n\t')}`, {
|
|
6276
|
+
color: 'red',
|
|
6277
|
+
});
|
|
6202
6278
|
}
|
|
6203
|
-
// Retrieve commit log with messages
|
|
6204
|
-
return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
|
|
6205
6279
|
}
|
|
6206
|
-
|
|
6207
|
-
logger
|
|
6280
|
+
else {
|
|
6281
|
+
logger.log('No repo changes detected. 👀', { color: 'blue' });
|
|
6208
6282
|
}
|
|
6209
|
-
return [];
|
|
6210
6283
|
}
|
|
6211
6284
|
|
|
6212
|
-
const
|
|
6213
|
-
|
|
6214
|
-
- Typically a hyphen or asterisk is used for the bullet
|
|
6215
|
-
- Summarize dependency updates
|
|
6216
|
-
|
|
6217
|
-
{format_instructions}
|
|
6218
|
-
|
|
6219
|
-
"""{summary}"""`;
|
|
6220
|
-
const inputVariables = ['format_instructions', 'summary'];
|
|
6221
|
-
const CHANGELOG_PROMPT = new prompts.PromptTemplate({
|
|
6222
|
-
template,
|
|
6223
|
-
inputVariables,
|
|
6224
|
-
});
|
|
6225
|
-
|
|
6226
|
-
const handler$1 = async (argv, logger) => {
|
|
6227
|
-
const config = loadConfig(argv);
|
|
6285
|
+
const handler$2 = async (argv, logger) => {
|
|
6228
6286
|
const git = getRepo();
|
|
6287
|
+
const config = loadConfig(argv);
|
|
6229
6288
|
const key = getApiKeyForModel(config);
|
|
6230
6289
|
const { provider, model } = getModelAndProviderFromConfig(config);
|
|
6231
6290
|
if (config.service.authentication.type !== 'None' && !key) {
|
|
6232
6291
|
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
6233
6292
|
process.exit(1);
|
|
6234
6293
|
}
|
|
6294
|
+
const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4');
|
|
6235
6295
|
const llm = getLlm(provider, model, config);
|
|
6236
6296
|
const INTERACTIVE = isInteractive(config);
|
|
6237
6297
|
if (INTERACTIVE) {
|
|
6238
6298
|
logger.log(LOGO);
|
|
6239
6299
|
}
|
|
6240
6300
|
async function factory() {
|
|
6241
|
-
const
|
|
6242
|
-
|
|
6243
|
-
const [from, to] = config.range.split(':');
|
|
6244
|
-
if (!from || !to) {
|
|
6245
|
-
logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
|
|
6246
|
-
process.exit(1);
|
|
6247
|
-
}
|
|
6248
|
-
return {
|
|
6249
|
-
branch: branchName,
|
|
6250
|
-
commits: await getCommitLogRange(from, to, { git, noMerges: true }),
|
|
6251
|
-
};
|
|
6252
|
-
}
|
|
6253
|
-
logger.verbose(`No range provided. Defaulting to current branch`, { color: 'yellow' });
|
|
6254
|
-
return {
|
|
6255
|
-
branch: branchName,
|
|
6256
|
-
commits: await getCommitLogCurrentBranch({ git, logger }),
|
|
6257
|
-
};
|
|
6301
|
+
const changes = await getChanges({ git });
|
|
6302
|
+
return changes.staged;
|
|
6258
6303
|
}
|
|
6259
|
-
async function parser(
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6304
|
+
async function parser(changes) {
|
|
6305
|
+
return await fileChangeParser({
|
|
6306
|
+
changes,
|
|
6307
|
+
commit: '--staged',
|
|
6308
|
+
options: { tokenizer, git, llm, logger },
|
|
6309
|
+
});
|
|
6263
6310
|
}
|
|
6264
|
-
const
|
|
6265
|
-
label: '
|
|
6311
|
+
const commitMsg = await generateAndReviewLoop({
|
|
6312
|
+
label: 'commit message',
|
|
6313
|
+
options: {
|
|
6314
|
+
...config,
|
|
6315
|
+
prompt: config.prompt || COMMIT_PROMPT.template,
|
|
6316
|
+
logger,
|
|
6317
|
+
interactive: INTERACTIVE,
|
|
6318
|
+
review: {
|
|
6319
|
+
descriptions: {
|
|
6320
|
+
approve: `Commit staged changes with generated commit message`,
|
|
6321
|
+
edit: 'Edit the commit message before proceeding',
|
|
6322
|
+
modifyPrompt: 'Modify the prompt template and regenerate the commit message',
|
|
6323
|
+
retryMessageOnly: 'Restart the function execution from generating the commit message',
|
|
6324
|
+
retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
|
|
6325
|
+
},
|
|
6326
|
+
},
|
|
6327
|
+
},
|
|
6266
6328
|
factory,
|
|
6267
6329
|
parser,
|
|
6268
6330
|
agent: async (context, options) => {
|
|
6269
6331
|
const parser = new output_parsers.JsonOutputParser();
|
|
6270
6332
|
const prompt = getPrompt({
|
|
6271
6333
|
template: options.prompt,
|
|
6272
|
-
variables:
|
|
6273
|
-
fallback:
|
|
6334
|
+
variables: COMMIT_PROMPT.inputVariables,
|
|
6335
|
+
fallback: COMMIT_PROMPT,
|
|
6274
6336
|
});
|
|
6275
|
-
const formatInstructions = "Respond with a valid JSON object, containing two fields: '
|
|
6276
|
-
const
|
|
6337
|
+
const formatInstructions = "Respond with a valid JSON object, containing two fields: 'title' and 'body', both strings.";
|
|
6338
|
+
const additionalContext = argv.additional ? `${argv.additional}` : '';
|
|
6339
|
+
const commitMsg = await executeChain({
|
|
6277
6340
|
llm,
|
|
6278
6341
|
prompt,
|
|
6279
6342
|
variables: {
|
|
6280
6343
|
summary: context,
|
|
6281
6344
|
format_instructions: formatInstructions,
|
|
6345
|
+
additional: additionalContext,
|
|
6282
6346
|
},
|
|
6283
6347
|
parser,
|
|
6284
6348
|
});
|
|
6285
|
-
|
|
6286
|
-
return `${
|
|
6349
|
+
const appendedText = argv.append ? `\n\n${argv.append}` : '';
|
|
6350
|
+
return `${commitMsg.title}\n\n${commitMsg.body}${appendedText}`;
|
|
6287
6351
|
},
|
|
6288
6352
|
noResult: async () => {
|
|
6289
|
-
|
|
6290
|
-
logger.log(`No commits found in the provided range.`, { color: 'red' });
|
|
6291
|
-
process.exit(0);
|
|
6292
|
-
}
|
|
6293
|
-
logger.log(`No commits found in the current branch.`, { color: 'red' });
|
|
6353
|
+
await noResult$1({ git, logger });
|
|
6294
6354
|
process.exit(0);
|
|
6295
6355
|
},
|
|
6296
|
-
options: {
|
|
6297
|
-
...config,
|
|
6298
|
-
prompt: config.prompt || CHANGELOG_PROMPT.template,
|
|
6299
|
-
logger,
|
|
6300
|
-
interactive: INTERACTIVE,
|
|
6301
|
-
review: {
|
|
6302
|
-
enableFullRetry: false,
|
|
6303
|
-
},
|
|
6304
|
-
},
|
|
6305
6356
|
});
|
|
6306
6357
|
const MODE = (INTERACTIVE && 'interactive') || (config.commit && 'interactive') || config?.mode || 'stdout';
|
|
6307
6358
|
handleResult({
|
|
6308
|
-
result:
|
|
6309
|
-
interactiveHandler: async () => {
|
|
6359
|
+
result: commitMsg,
|
|
6360
|
+
interactiveHandler: async (result) => {
|
|
6361
|
+
await createCommit(result, git);
|
|
6310
6362
|
logSuccess();
|
|
6311
6363
|
},
|
|
6312
6364
|
mode: MODE,
|
|
@@ -6316,43 +6368,39 @@ const handler$1 = async (argv, logger) => {
|
|
|
6316
6368
|
/**
|
|
6317
6369
|
* Command line options via yargs
|
|
6318
6370
|
*/
|
|
6319
|
-
const options$
|
|
6320
|
-
range: {
|
|
6321
|
-
type: 'string',
|
|
6322
|
-
alias: 'r',
|
|
6323
|
-
description: 'Commit range e.g `HEAD~2:HEAD`',
|
|
6324
|
-
},
|
|
6325
|
-
tokenLimit: { type: 'number', description: 'Token limit' },
|
|
6326
|
-
prompt: {
|
|
6327
|
-
type: 'string',
|
|
6328
|
-
alias: 'p',
|
|
6329
|
-
description: 'Prompt for llm',
|
|
6330
|
-
},
|
|
6371
|
+
const options$2 = {
|
|
6331
6372
|
i: {
|
|
6332
|
-
type: 'boolean',
|
|
6333
6373
|
alias: 'interactive',
|
|
6334
6374
|
description: 'Toggle interactive mode',
|
|
6335
|
-
},
|
|
6336
|
-
e: {
|
|
6337
6375
|
type: 'boolean',
|
|
6338
|
-
alias: 'edit',
|
|
6339
|
-
description: 'Open generated changelog message in editor before proceeding',
|
|
6340
6376
|
},
|
|
6341
|
-
|
|
6377
|
+
ignoredFiles: {
|
|
6378
|
+
description: 'Ignored files',
|
|
6379
|
+
type: 'array',
|
|
6380
|
+
},
|
|
6381
|
+
ignoredExtensions: {
|
|
6382
|
+
description: 'Ignored extensions',
|
|
6383
|
+
type: 'array',
|
|
6384
|
+
},
|
|
6385
|
+
append: {
|
|
6386
|
+
description: 'Add content to the end of the generated commit message',
|
|
6387
|
+
type: 'string',
|
|
6388
|
+
},
|
|
6389
|
+
additional: {
|
|
6390
|
+
description: 'Add extra contextual information to the prompt',
|
|
6342
6391
|
type: 'string',
|
|
6343
|
-
description: 'Prompt for summarizing large files',
|
|
6344
6392
|
},
|
|
6345
6393
|
};
|
|
6346
|
-
const builder$
|
|
6347
|
-
return
|
|
6394
|
+
const builder$2 = (yargsInstance) => {
|
|
6395
|
+
return yargsInstance.options(options$2).usage(getCommandUsageHeader(commit.command));
|
|
6348
6396
|
};
|
|
6349
6397
|
|
|
6350
|
-
var
|
|
6351
|
-
command: '
|
|
6352
|
-
desc: '
|
|
6353
|
-
builder: builder$
|
|
6354
|
-
handler: commandExecutor(handler$
|
|
6355
|
-
options: options$
|
|
6398
|
+
var commit = {
|
|
6399
|
+
command: 'commit',
|
|
6400
|
+
desc: 'Summarize the staged changes in a commit message.',
|
|
6401
|
+
builder: builder$2,
|
|
6402
|
+
handler: commandExecutor(handler$2),
|
|
6403
|
+
options: options$2,
|
|
6356
6404
|
};
|
|
6357
6405
|
|
|
6358
6406
|
/**
|
|
@@ -6452,13 +6500,13 @@ async function checkAndHandlePackageInstallation({ global = false, logger, }) {
|
|
|
6452
6500
|
const projectRoot = findProjectRoot(process.cwd());
|
|
6453
6501
|
let shouldInstall = false;
|
|
6454
6502
|
if (isPackageInstalled(packageName, projectRoot)) {
|
|
6455
|
-
shouldInstall = await prompts
|
|
6503
|
+
shouldInstall = await prompts.confirm({
|
|
6456
6504
|
message: `'${packageName}' is already installed in '${projectRoot}/package.json', would you like to update?`,
|
|
6457
6505
|
default: shouldInstall,
|
|
6458
6506
|
});
|
|
6459
6507
|
}
|
|
6460
6508
|
else {
|
|
6461
|
-
shouldInstall = await prompts
|
|
6509
|
+
shouldInstall = await prompts.confirm({
|
|
6462
6510
|
message: `'${packageName}' is not installed in '${projectRoot}/package.json', would you like to install?`,
|
|
6463
6511
|
default: shouldInstall,
|
|
6464
6512
|
});
|
|
@@ -6496,7 +6544,7 @@ const questions = {
|
|
|
6496
6544
|
/**
|
|
6497
6545
|
* @description configure coco globally for the current user or project?
|
|
6498
6546
|
*/
|
|
6499
|
-
whatScope: async () => await prompts
|
|
6547
|
+
whatScope: async () => await prompts.select({
|
|
6500
6548
|
message: 'configure coco globally for the current user or for the current directory?',
|
|
6501
6549
|
choices: [
|
|
6502
6550
|
{
|
|
@@ -6511,7 +6559,7 @@ const questions = {
|
|
|
6511
6559
|
},
|
|
6512
6560
|
],
|
|
6513
6561
|
}),
|
|
6514
|
-
selectLLMProvider: async () => await prompts
|
|
6562
|
+
selectLLMProvider: async () => await prompts.select({
|
|
6515
6563
|
message: 'select language model provider:',
|
|
6516
6564
|
choices: [
|
|
6517
6565
|
{
|
|
@@ -6553,7 +6601,7 @@ const questions = {
|
|
|
6553
6601
|
})),
|
|
6554
6602
|
];
|
|
6555
6603
|
}
|
|
6556
|
-
return await prompts
|
|
6604
|
+
return await prompts.select({
|
|
6557
6605
|
message: 'select language model:',
|
|
6558
6606
|
choices: availableModels,
|
|
6559
6607
|
});
|
|
@@ -6564,7 +6612,7 @@ const questions = {
|
|
|
6564
6612
|
* print results to stdout
|
|
6565
6613
|
* @returns 'interactive' | 'stdout'
|
|
6566
6614
|
*/
|
|
6567
|
-
selectMode: async () => await prompts
|
|
6615
|
+
selectMode: async () => await prompts.select({
|
|
6568
6616
|
message: 'select mode:',
|
|
6569
6617
|
choices: [
|
|
6570
6618
|
{
|
|
@@ -6582,19 +6630,19 @@ const questions = {
|
|
|
6582
6630
|
inputOpenAIApiKey: async () => {
|
|
6583
6631
|
// check for existing env var
|
|
6584
6632
|
if (process.env.OPENAI_API_KEY) {
|
|
6585
|
-
return (await prompts
|
|
6633
|
+
return (await prompts.confirm({
|
|
6586
6634
|
message: `use existing OPENAI_API_KEY env var?`,
|
|
6587
6635
|
default: true,
|
|
6588
6636
|
}))
|
|
6589
6637
|
? process.env.OPENAI_API_KEY
|
|
6590
|
-
: await prompts
|
|
6638
|
+
: await prompts.password({
|
|
6591
6639
|
message: `enter your OpenAI API key:`,
|
|
6592
6640
|
validate(input) {
|
|
6593
6641
|
return input.length > 0 ? true : 'API key cannot be empty';
|
|
6594
6642
|
},
|
|
6595
6643
|
});
|
|
6596
6644
|
}
|
|
6597
|
-
return await prompts
|
|
6645
|
+
return await prompts.password({
|
|
6598
6646
|
message: `enter your OpenAI API key:`,
|
|
6599
6647
|
validate(input) {
|
|
6600
6648
|
return input.length > 0 ? true : 'API key cannot be empty';
|
|
@@ -6602,48 +6650,48 @@ const questions = {
|
|
|
6602
6650
|
});
|
|
6603
6651
|
},
|
|
6604
6652
|
inputTokenLimit: async () => {
|
|
6605
|
-
const tokenLimit = await prompts
|
|
6653
|
+
const tokenLimit = await prompts.input({
|
|
6606
6654
|
message: 'maximum number of tokens for generating commit messages:',
|
|
6607
6655
|
default: '300',
|
|
6608
6656
|
});
|
|
6609
6657
|
return parseInt(tokenLimit);
|
|
6610
6658
|
},
|
|
6611
6659
|
inputModelTemperature: async () => {
|
|
6612
|
-
const temperature = await prompts
|
|
6660
|
+
const temperature = await prompts.input({
|
|
6613
6661
|
message: 'model temperature for generating commit messages:',
|
|
6614
6662
|
default: '0.36',
|
|
6615
6663
|
});
|
|
6616
6664
|
return parseFloat(temperature);
|
|
6617
6665
|
},
|
|
6618
|
-
selectDefaultGitBranch: async () => (await prompts
|
|
6666
|
+
selectDefaultGitBranch: async () => (await prompts.input({
|
|
6619
6667
|
message: 'default branch for the repository:',
|
|
6620
6668
|
default: 'main',
|
|
6621
6669
|
})) || 'main',
|
|
6622
|
-
configureAdvancedOptions: async () => await prompts
|
|
6670
|
+
configureAdvancedOptions: async () => await prompts.confirm({
|
|
6623
6671
|
message: 'would you like to configure advanced options?',
|
|
6624
6672
|
default: false,
|
|
6625
6673
|
}),
|
|
6626
|
-
enableVerboseMode: async () => await prompts
|
|
6674
|
+
enableVerboseMode: async () => await prompts.confirm({
|
|
6627
6675
|
message: 'enable verbose logging:',
|
|
6628
6676
|
default: false,
|
|
6629
6677
|
}),
|
|
6630
|
-
whatFilesToIgnore: async () => (await prompts
|
|
6678
|
+
whatFilesToIgnore: async () => (await prompts.input({
|
|
6631
6679
|
message: 'paths of files to be excluded when generating commit messages (comma-separated):',
|
|
6632
6680
|
default: 'package-lock.json',
|
|
6633
6681
|
}))
|
|
6634
6682
|
?.split(',')
|
|
6635
6683
|
?.map((file) => file.trim()) || [],
|
|
6636
|
-
whatExtensionsToIgnore: async () => (await prompts
|
|
6684
|
+
whatExtensionsToIgnore: async () => (await prompts.input({
|
|
6637
6685
|
message: 'file extensions to be excluded when generating commit messages (comma-separated):',
|
|
6638
6686
|
default: '.map, .lock',
|
|
6639
6687
|
}))
|
|
6640
6688
|
?.split(',')
|
|
6641
6689
|
?.map((ext) => ext.trim()) || [],
|
|
6642
|
-
modifyCommitPrompt: async () => await prompts
|
|
6690
|
+
modifyCommitPrompt: async () => await prompts.editor({
|
|
6643
6691
|
message: 'modify default commit message prompt:',
|
|
6644
6692
|
default: COMMIT_PROMPT.template,
|
|
6645
6693
|
}),
|
|
6646
|
-
selectProjectConfigFileType: async () => await prompts
|
|
6694
|
+
selectProjectConfigFileType: async () => await prompts.select({
|
|
6647
6695
|
message: 'where would you like to store the project config?',
|
|
6648
6696
|
choices: [
|
|
6649
6697
|
{
|
|
@@ -6658,7 +6706,7 @@ const questions = {
|
|
|
6658
6706
|
}),
|
|
6659
6707
|
};
|
|
6660
6708
|
|
|
6661
|
-
const handler = async (argv, logger) => {
|
|
6709
|
+
const handler$1 = async (argv, logger) => {
|
|
6662
6710
|
const options = loadConfig(argv);
|
|
6663
6711
|
logger.log(LOGO);
|
|
6664
6712
|
let scope = options?.scope;
|
|
@@ -6696,10 +6744,13 @@ const handler = async (argv, logger) => {
|
|
|
6696
6744
|
if (advOptions) {
|
|
6697
6745
|
config.mode = await questions.selectMode();
|
|
6698
6746
|
config.defaultBranch = await questions.selectDefaultGitBranch();
|
|
6699
|
-
config.
|
|
6700
|
-
|
|
6747
|
+
config.service = {
|
|
6748
|
+
...config.service,
|
|
6749
|
+
temperature: await questions.inputModelTemperature(),
|
|
6750
|
+
tokenLimit: await questions.inputTokenLimit(),
|
|
6751
|
+
};
|
|
6701
6752
|
config.verbose = await questions.enableVerboseMode();
|
|
6702
|
-
const promptForIgnores = await prompts
|
|
6753
|
+
const promptForIgnores = await prompts.confirm({
|
|
6703
6754
|
message: 'would you like to configure ignored files and extensions?',
|
|
6704
6755
|
default: false,
|
|
6705
6756
|
});
|
|
@@ -6707,7 +6758,7 @@ const handler = async (argv, logger) => {
|
|
|
6707
6758
|
config.ignoredFiles = await questions.whatFilesToIgnore();
|
|
6708
6759
|
config.ignoredExtensions = await questions.whatExtensionsToIgnore();
|
|
6709
6760
|
}
|
|
6710
|
-
const promptForCommitPrompt = await prompts
|
|
6761
|
+
const promptForCommitPrompt = await prompts.confirm({
|
|
6711
6762
|
message: 'would you like to configure the commit message prompt?',
|
|
6712
6763
|
default: false,
|
|
6713
6764
|
});
|
|
@@ -6724,7 +6775,7 @@ const handler = async (argv, logger) => {
|
|
|
6724
6775
|
approvalMessage = 'looking good? (API key hidden for security)';
|
|
6725
6776
|
}
|
|
6726
6777
|
}
|
|
6727
|
-
const isApproved = await prompts
|
|
6778
|
+
const isApproved = await prompts.confirm({
|
|
6728
6779
|
message: approvalMessage,
|
|
6729
6780
|
});
|
|
6730
6781
|
let configFilePath = '';
|
|
@@ -6761,20 +6812,217 @@ const handler = async (argv, logger) => {
|
|
|
6761
6812
|
/**
|
|
6762
6813
|
* Command line options via yargs
|
|
6763
6814
|
*/
|
|
6764
|
-
const options = {
|
|
6815
|
+
const options$1 = {
|
|
6765
6816
|
scope: {
|
|
6766
6817
|
type: 'string',
|
|
6767
6818
|
description: 'configure coco for the current user or project?',
|
|
6768
6819
|
choices: ['global', 'project'],
|
|
6769
6820
|
},
|
|
6770
6821
|
};
|
|
6771
|
-
const builder = (yargs) => {
|
|
6772
|
-
return yargs.options(options);
|
|
6822
|
+
const builder$1 = (yargs) => {
|
|
6823
|
+
return yargs.options(options$1).usage(getCommandUsageHeader(init.command));
|
|
6773
6824
|
};
|
|
6774
6825
|
|
|
6775
6826
|
var init = {
|
|
6776
6827
|
command: 'init',
|
|
6777
6828
|
desc: 'install & configure coco globally or for the current project',
|
|
6829
|
+
builder: builder$1,
|
|
6830
|
+
handler: commandExecutor(handler$1),
|
|
6831
|
+
options: options$1,
|
|
6832
|
+
};
|
|
6833
|
+
|
|
6834
|
+
/**
|
|
6835
|
+
* Formats a commit log into a readable string format.
|
|
6836
|
+
*
|
|
6837
|
+
* @param commitLog - The commit log result containing an array of commit details.
|
|
6838
|
+
* @returns An array of formatted commit log strings.
|
|
6839
|
+
*
|
|
6840
|
+
* Each formatted string includes:
|
|
6841
|
+
* - The date of the commit in square brackets.
|
|
6842
|
+
* - The commit message.
|
|
6843
|
+
* - The commit body.
|
|
6844
|
+
* - The commit hash in parentheses.
|
|
6845
|
+
* - The author's name and email in angle brackets.
|
|
6846
|
+
*/
|
|
6847
|
+
const formatCommitLog = (commitLog) => {
|
|
6848
|
+
return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
|
|
6849
|
+
};
|
|
6850
|
+
|
|
6851
|
+
const getChangesByTimestamp = async ({ since, git }) => {
|
|
6852
|
+
const commitLog = await git.log({ '--since': since });
|
|
6853
|
+
return formatCommitLog(commitLog);
|
|
6854
|
+
};
|
|
6855
|
+
|
|
6856
|
+
const getChangesSinceLastTag = async ({ git }) => {
|
|
6857
|
+
const tags = await git.tags();
|
|
6858
|
+
if (tags.all.length > 0) {
|
|
6859
|
+
const lastTag = tags.latest;
|
|
6860
|
+
const commitLog = await git.log({ from: lastTag });
|
|
6861
|
+
return formatCommitLog(commitLog);
|
|
6862
|
+
}
|
|
6863
|
+
else {
|
|
6864
|
+
return ['No tags found in the repository.'];
|
|
6865
|
+
}
|
|
6866
|
+
};
|
|
6867
|
+
|
|
6868
|
+
async function noResult({ logger }) {
|
|
6869
|
+
logger.log('No repo changes detected. 👀', { color: 'blue' });
|
|
6870
|
+
}
|
|
6871
|
+
|
|
6872
|
+
const template = `Following the formatting instructions, summarize the following changes in the underlying git repository/branch.
|
|
6873
|
+
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.
|
|
6874
|
+
|
|
6875
|
+
Breaking down the changes into categories (e.g. bug fixes, new features, etc.) with markdown headings is encouraged.
|
|
6876
|
+
|
|
6877
|
+
{timeframe}
|
|
6878
|
+
|
|
6879
|
+
{format_instructions}
|
|
6880
|
+
|
|
6881
|
+
"""{changes}"""`;
|
|
6882
|
+
const inputVariables = ['format_instructions', 'changes', 'timeframe'];
|
|
6883
|
+
const RECAP_PROMPT = new prompts$1.PromptTemplate({
|
|
6884
|
+
template,
|
|
6885
|
+
inputVariables,
|
|
6886
|
+
});
|
|
6887
|
+
|
|
6888
|
+
const handler = async (argv, logger) => {
|
|
6889
|
+
const git = getRepo();
|
|
6890
|
+
const config = loadConfig(argv);
|
|
6891
|
+
const key = getApiKeyForModel(config);
|
|
6892
|
+
const { provider, model } = getModelAndProviderFromConfig(config);
|
|
6893
|
+
if (config.service.authentication.type !== 'None' && !key) {
|
|
6894
|
+
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
6895
|
+
process.exit(1);
|
|
6896
|
+
}
|
|
6897
|
+
const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4');
|
|
6898
|
+
const llm = getLlm(provider, model, config);
|
|
6899
|
+
const INTERACTIVE = isInteractive(config);
|
|
6900
|
+
if (INTERACTIVE) {
|
|
6901
|
+
logger.log(LOGO);
|
|
6902
|
+
}
|
|
6903
|
+
const { 'last-month': lastMonth, 'last-tag': lastTag, yesterday, 'last-week': lastWeek } = argv;
|
|
6904
|
+
const timeframe = lastMonth ? 'last-month' : lastTag ? 'last-tag' : yesterday ? 'yesterday' : lastWeek ? 'last-week' : 'current';
|
|
6905
|
+
logger.log(`Generating recap for timeframe: ${timeframe}`);
|
|
6906
|
+
async function factory() {
|
|
6907
|
+
switch (timeframe) {
|
|
6908
|
+
case 'current':
|
|
6909
|
+
const { staged, unstaged, untracked } = await getChanges({ git });
|
|
6910
|
+
logger.log(`Staged: ${staged.length}, Unstaged: ${unstaged?.length || 0}, Untracked: ${untracked?.length || 0}`);
|
|
6911
|
+
const result = await fileChangeParser({
|
|
6912
|
+
changes: [...staged, ...(unstaged ? unstaged : []), ...(untracked ? untracked : [])],
|
|
6913
|
+
commit: 'HEAD',
|
|
6914
|
+
options: { tokenizer, git, llm, logger },
|
|
6915
|
+
});
|
|
6916
|
+
logger.log(`Parsed Result: ${result}`);
|
|
6917
|
+
return [result];
|
|
6918
|
+
case 'yesterday':
|
|
6919
|
+
const yesterday = new Date();
|
|
6920
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
6921
|
+
return await getChangesByTimestamp({ git, since: yesterday.toISOString().split('T')[0] });
|
|
6922
|
+
case 'last-week':
|
|
6923
|
+
const lastWeek = new Date();
|
|
6924
|
+
lastWeek.setDate(lastWeek.getDate() - 7);
|
|
6925
|
+
return await getChangesByTimestamp({ git, since: lastWeek.toISOString().split('T')[0] });
|
|
6926
|
+
case 'last-month':
|
|
6927
|
+
const lastMonth = new Date();
|
|
6928
|
+
lastMonth.setMonth(lastMonth.getMonth() - 1);
|
|
6929
|
+
return await getChangesByTimestamp({ git, since: lastMonth.toISOString().split('T')[0] });
|
|
6930
|
+
case 'last-tag':
|
|
6931
|
+
const tags = await getChangesSinceLastTag({ git });
|
|
6932
|
+
return tags;
|
|
6933
|
+
default:
|
|
6934
|
+
logger.log(`Invalid timeframe: ${timeframe}`, { color: 'red' });
|
|
6935
|
+
return [];
|
|
6936
|
+
}
|
|
6937
|
+
}
|
|
6938
|
+
async function parser(changes) {
|
|
6939
|
+
console.log({ changes });
|
|
6940
|
+
return changes.join('\n');
|
|
6941
|
+
}
|
|
6942
|
+
const recap = await generateAndReviewLoop({
|
|
6943
|
+
label: 'recap',
|
|
6944
|
+
options: {
|
|
6945
|
+
...config,
|
|
6946
|
+
prompt: config.prompt || RECAP_PROMPT.template,
|
|
6947
|
+
logger,
|
|
6948
|
+
interactive: INTERACTIVE,
|
|
6949
|
+
review: {
|
|
6950
|
+
descriptions: {
|
|
6951
|
+
approve: `Commit staged changes with generated commit message`,
|
|
6952
|
+
edit: 'Edit the commit message before proceeding',
|
|
6953
|
+
modifyPrompt: 'Modify the prompt template and regenerate the commit message',
|
|
6954
|
+
retryMessageOnly: 'Restart the function execution from generating the commit message',
|
|
6955
|
+
retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
|
|
6956
|
+
},
|
|
6957
|
+
},
|
|
6958
|
+
},
|
|
6959
|
+
factory,
|
|
6960
|
+
parser,
|
|
6961
|
+
agent: async (context, options) => {
|
|
6962
|
+
const parser = new output_parsers.JsonOutputParser();
|
|
6963
|
+
const formatInstructions = "Respond with a valid JSON object, containing one field: 'summary', a string.";
|
|
6964
|
+
const prompt = getPrompt({
|
|
6965
|
+
template: options.prompt,
|
|
6966
|
+
variables: RECAP_PROMPT.inputVariables,
|
|
6967
|
+
fallback: RECAP_PROMPT,
|
|
6968
|
+
});
|
|
6969
|
+
const response = await executeChain({
|
|
6970
|
+
llm,
|
|
6971
|
+
prompt,
|
|
6972
|
+
variables: {
|
|
6973
|
+
changes: context,
|
|
6974
|
+
format_instructions: formatInstructions,
|
|
6975
|
+
timeframe,
|
|
6976
|
+
},
|
|
6977
|
+
parser,
|
|
6978
|
+
});
|
|
6979
|
+
logger.log(response.summary || 'no response');
|
|
6980
|
+
return `${response.summary}`;
|
|
6981
|
+
},
|
|
6982
|
+
noResult: async () => {
|
|
6983
|
+
await noResult({ git, logger });
|
|
6984
|
+
process.exit(0);
|
|
6985
|
+
},
|
|
6986
|
+
});
|
|
6987
|
+
logger.log(`Recap generated: ${recap}`, { color: 'green' });
|
|
6988
|
+
};
|
|
6989
|
+
|
|
6990
|
+
/**
|
|
6991
|
+
* Command line options via yargs
|
|
6992
|
+
*/
|
|
6993
|
+
const options = {
|
|
6994
|
+
yesterday: {
|
|
6995
|
+
type: 'boolean',
|
|
6996
|
+
description: 'Recap for yesterday',
|
|
6997
|
+
},
|
|
6998
|
+
"last-week": {
|
|
6999
|
+
alias: 'week',
|
|
7000
|
+
type: 'boolean',
|
|
7001
|
+
description: 'Recap for last week',
|
|
7002
|
+
},
|
|
7003
|
+
"last-month": {
|
|
7004
|
+
alias: 'month',
|
|
7005
|
+
type: 'boolean',
|
|
7006
|
+
description: 'Recap for last month',
|
|
7007
|
+
},
|
|
7008
|
+
"last-tag": {
|
|
7009
|
+
alias: 'tag',
|
|
7010
|
+
type: 'boolean',
|
|
7011
|
+
description: 'Recap for last tag',
|
|
7012
|
+
},
|
|
7013
|
+
i: {
|
|
7014
|
+
type: 'boolean',
|
|
7015
|
+
alias: 'interactive',
|
|
7016
|
+
description: 'Toggle interactive mode',
|
|
7017
|
+
},
|
|
7018
|
+
};
|
|
7019
|
+
const builder = (yargsInstance) => {
|
|
7020
|
+
return yargsInstance.options(options).usage(getCommandUsageHeader(recap.command));
|
|
7021
|
+
};
|
|
7022
|
+
|
|
7023
|
+
var recap = {
|
|
7024
|
+
command: 'recap',
|
|
7025
|
+
desc: 'Summarize the changes in the repository over a specified timeframe.',
|
|
6778
7026
|
builder,
|
|
6779
7027
|
handler: commandExecutor(handler),
|
|
6780
7028
|
options,
|
|
@@ -6785,25 +7033,15 @@ var types = /*#__PURE__*/Object.freeze({
|
|
|
6785
7033
|
});
|
|
6786
7034
|
|
|
6787
7035
|
const y = yargs();
|
|
6788
|
-
y.scriptName('coco')
|
|
6789
|
-
y.command([commit.command, '$0'], commit.desc,
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
y.command(changelog.command, changelog.desc,
|
|
6795
|
-
// TODO: fix type on builder
|
|
6796
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
6797
|
-
// @ts-ignore
|
|
6798
|
-
changelog.builder, changelog.handler).options(changelog.options);
|
|
6799
|
-
y.command(init.command, init.desc,
|
|
6800
|
-
// TODO: fix type on builder
|
|
6801
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
6802
|
-
// @ts-ignore
|
|
6803
|
-
init.builder, init.handler).options(init.options);
|
|
6804
|
-
y.parse(process.argv.slice(2));
|
|
7036
|
+
y.scriptName('coco');
|
|
7037
|
+
y.command([commit.command, '$0'], commit.desc, commit.builder, commit.handler);
|
|
7038
|
+
y.command(changelog.command, changelog.desc, changelog.builder, changelog.handler);
|
|
7039
|
+
y.command(recap.command, recap.desc, recap.builder, recap.handler);
|
|
7040
|
+
y.command(init.command, init.desc, init.builder, init.handler);
|
|
7041
|
+
y.help().parse(process.argv.slice(2));
|
|
6805
7042
|
|
|
6806
7043
|
exports.changelog = changelog;
|
|
6807
7044
|
exports.commit = commit;
|
|
6808
7045
|
exports.init = init;
|
|
7046
|
+
exports.recap = recap;
|
|
6809
7047
|
exports.types = types;
|