git-coco 0.11.1 → 0.12.1

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