git-coco 0.6.2 β 0.7.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 +125 -86
- package/dist/index.esm.mjs +1036 -893
- package/dist/index.js +1035 -892
- package/package.json +2 -3
package/dist/index.esm.mjs
CHANGED
|
@@ -1,455 +1,383 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import yargs from 'yargs';
|
|
3
|
+
import { PromptTemplate } from 'langchain/prompts';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import fs__default from 'fs';
|
|
6
|
+
import { confirm, editor, select, password, input } from '@inquirer/prompts';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import os__default from 'os';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import path__default from 'path';
|
|
12
|
+
import * as ini from 'ini';
|
|
13
|
+
import ora from 'ora';
|
|
14
|
+
import now from 'performance-now';
|
|
15
|
+
import prettyMilliseconds from 'pretty-ms';
|
|
3
16
|
import pQueue from 'p-queue';
|
|
4
17
|
import { Document } from 'langchain/document';
|
|
5
18
|
import { HuggingFaceInference } from 'langchain/llms/hf';
|
|
6
|
-
import { PromptTemplate } from 'langchain/prompts';
|
|
7
19
|
import { loadSummarizationChain, LLMChain } from 'langchain/chains';
|
|
8
20
|
import { OpenAI } from 'langchain/llms/openai';
|
|
9
21
|
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
|
|
10
22
|
import { createTwoFilesPatch } from 'diff';
|
|
11
|
-
import GPT3NodeTokenizer from 'gpt3-tokenizer';
|
|
12
|
-
import chalk from 'chalk';
|
|
13
|
-
import ora from 'ora';
|
|
14
|
-
import now from 'performance-now';
|
|
15
|
-
import prettyMilliseconds from 'pretty-ms';
|
|
16
|
-
import * as path from 'path';
|
|
17
|
-
import path__default from 'path';
|
|
18
23
|
import { minimatch } from 'minimatch';
|
|
19
|
-
import * as fs from 'fs';
|
|
20
|
-
import fs__default from 'fs';
|
|
21
|
-
import { confirm, editor, select, password, input } from '@inquirer/prompts';
|
|
22
|
-
import * as os from 'os';
|
|
23
|
-
import os__default from 'os';
|
|
24
|
-
import * as ini from 'ini';
|
|
25
24
|
import { simpleGit } from 'simple-git';
|
|
25
|
+
import { encoding_for_model } from 'tiktoken';
|
|
26
|
+
import { exec } from 'child_process';
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* @
|
|
29
|
+
* Returns a new object with all undefined keys removed
|
|
30
|
+
*
|
|
31
|
+
* @param obj Object to remove undefined keys from
|
|
32
|
+
* @returns
|
|
31
33
|
*/
|
|
32
|
-
function
|
|
33
|
-
return
|
|
34
|
+
function removeUndefined(obj) {
|
|
35
|
+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
38
|
+
const template$2 = `GOAL: Use functional abstractions to summarize the following text
|
|
39
|
+
|
|
40
|
+
RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
|
|
41
|
+
|
|
42
|
+
TEXT:"""{text}"""
|
|
43
|
+
`;
|
|
44
|
+
const inputVariables$2 = ['text'];
|
|
45
|
+
const SUMMARIZE_PROMPT = new PromptTemplate({
|
|
46
|
+
template: template$2,
|
|
47
|
+
inputVariables: inputVariables$2,
|
|
48
|
+
});
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* @
|
|
51
|
+
* Default Config
|
|
52
|
+
*
|
|
53
|
+
* @type {Config}
|
|
52
54
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
node.children.forEach(traverse);
|
|
65
|
-
}
|
|
66
|
-
traverse(node);
|
|
67
|
-
return Object.values(groupByPath);
|
|
68
|
-
}
|
|
55
|
+
const DEFAULT_CONFIG = {
|
|
56
|
+
service: 'openai/gpt-4',
|
|
57
|
+
verbose: false,
|
|
58
|
+
tokenLimit: 1024,
|
|
59
|
+
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
60
|
+
temperature: 0.4,
|
|
61
|
+
mode: 'stdout',
|
|
62
|
+
ignoredFiles: ['package-lock.json'],
|
|
63
|
+
ignoredExtensions: ['.map', '.lock'],
|
|
64
|
+
defaultBranch: 'main',
|
|
65
|
+
};
|
|
69
66
|
/**
|
|
70
|
-
*
|
|
67
|
+
* Create a named export of all config keys for use in other modules.
|
|
68
|
+
*
|
|
69
|
+
* @see Currently used in `src/lib/config/services/env.ts` to validate all env vars.
|
|
70
|
+
*
|
|
71
|
+
* @type {string[]}
|
|
71
72
|
*/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
73
|
+
const CONFIG_KEYS = Object.keys({
|
|
74
|
+
...DEFAULT_CONFIG,
|
|
75
|
+
huggingFaceHubApiKey: '',
|
|
76
|
+
openAIApiKey: '',
|
|
77
|
+
prompt: '',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
async function updateFileSection({ filePath, startComment, endComment, getNewContent, confirmUpdate = true, confirmMessage = (path) => `A section already exists in ${path}, do you want to override it?`, }) {
|
|
81
|
+
const lines = fs__default.existsSync(filePath) ? fs__default.readFileSync(filePath, 'utf-8').split(/\r?\n/) : [];
|
|
82
|
+
const newLines = [];
|
|
83
|
+
let foundSection = false;
|
|
84
|
+
for (let i = 0; i < lines.length; i++) {
|
|
85
|
+
if (lines[i].trim() === startComment) {
|
|
86
|
+
foundSection = true;
|
|
87
|
+
if (confirmUpdate) {
|
|
88
|
+
const confirmOverwrite = await confirm({
|
|
89
|
+
message: typeof confirmMessage === 'function' ? confirmMessage(filePath) : confirmMessage,
|
|
90
|
+
default: false,
|
|
91
|
+
});
|
|
92
|
+
if (!confirmOverwrite) {
|
|
93
|
+
// keep all lines until the end comment
|
|
94
|
+
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
95
|
+
newLines.push(lines[i]);
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
newLines.push(endComment);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
newLines.push(startComment);
|
|
103
|
+
// Insert the new content
|
|
104
|
+
const newContent = await getNewContent();
|
|
105
|
+
newLines.push(newContent);
|
|
106
|
+
// Skip the existing content of the section
|
|
107
|
+
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
108
|
+
i++;
|
|
109
|
+
}
|
|
110
|
+
newLines.push(endComment);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!foundSection || lines[i].trim() !== endComment) {
|
|
114
|
+
newLines.push(lines[i]);
|
|
115
|
+
}
|
|
94
116
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
117
|
+
// If section wasn't found, append it at the end
|
|
118
|
+
if (!foundSection) {
|
|
119
|
+
newLines.push('\n' + startComment);
|
|
120
|
+
const newContent = await getNewContent();
|
|
121
|
+
newLines.push(newContent);
|
|
122
|
+
newLines.push(endComment);
|
|
98
123
|
}
|
|
124
|
+
// Write the updated contents back to the file
|
|
125
|
+
fs__default.writeFileSync(filePath, newLines.join('\n'));
|
|
99
126
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (group.summary) {
|
|
104
|
-
output += `${group.diffs.map((diff) => ` β’ ${diff.summary}`).join('\n')}\n\nSummary:\n\n${group.summary}\n\n`;
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
output += `${group.diffs.map((diff) => ` β’ ${diff.summary}\n\n${diff.diff}`).join('\n\n')}\n\n`;
|
|
108
|
-
}
|
|
109
|
-
return output;
|
|
127
|
+
|
|
128
|
+
const isInteractive = (argv) => {
|
|
129
|
+
return argv?.mode === 'interactive' || argv.interactive;
|
|
110
130
|
};
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
color: totalTokenCount > maxTokens ? 'yellow' : 'green',
|
|
139
|
-
});
|
|
140
|
-
return group;
|
|
141
|
-
}, { priority: group.tokenCount });
|
|
131
|
+
const SEPERATOR = chalk.blue('βββββββββββββ');
|
|
132
|
+
const LOGO = chalk.green(`ββββββββββββββ
|
|
133
|
+
ββββββββββββββ
|
|
134
|
+
ββ β ββ β ββ
|
|
135
|
+
ββββββββββββββ
|
|
136
|
+
ββββββββββββββ
|
|
137
|
+
`);
|
|
138
|
+
const CONFIG_ALREADY_EXISTS = (path) => {
|
|
139
|
+
return `coco config found in '${path}', do you want to override it?`;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Load environment variables
|
|
144
|
+
*
|
|
145
|
+
* @param {Config} config
|
|
146
|
+
* @returns {Config} Updated config
|
|
147
|
+
**/
|
|
148
|
+
function loadEnvConfig(config) {
|
|
149
|
+
const envConfig = {};
|
|
150
|
+
CONFIG_KEYS.forEach((key) => {
|
|
151
|
+
const envVarName = toEnvVarName(key);
|
|
152
|
+
const envValue = parseEnvValue(key, process.env[envVarName]);
|
|
153
|
+
if (envValue === undefined)
|
|
154
|
+
return;
|
|
155
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
156
|
+
// @ts-ignore
|
|
157
|
+
envConfig[key] = envValue;
|
|
142
158
|
});
|
|
143
|
-
|
|
144
|
-
logger.stopSpinner(`Summarized Diffs`);
|
|
145
|
-
return directoryDiffs.map(handleOutput).join('');
|
|
159
|
+
return { ...config, ...removeUndefined(envConfig) };
|
|
146
160
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
this.path = [];
|
|
151
|
-
this.files = [];
|
|
152
|
-
this.children = new Map();
|
|
153
|
-
if (path)
|
|
154
|
-
this.path = path;
|
|
161
|
+
function parseEnvValue(key, value) {
|
|
162
|
+
if (value === undefined) {
|
|
163
|
+
return undefined;
|
|
155
164
|
}
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
else if (key === 'tokenLimit' && typeof value === 'string') {
|
|
166
|
+
return parseInt(value);
|
|
158
167
|
}
|
|
159
|
-
|
|
160
|
-
|
|
168
|
+
else if ((key === 'ignoredFiles' || key === 'ignoredExtensions') &&
|
|
169
|
+
typeof value === 'string' &&
|
|
170
|
+
value.includes(',')) {
|
|
171
|
+
return value.split(',');
|
|
161
172
|
}
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
function toEnvVarName(key) {
|
|
176
|
+
switch (key) {
|
|
177
|
+
case 'openAIApiKey':
|
|
178
|
+
return 'OPENAI_API_KEY';
|
|
179
|
+
case 'huggingFaceHubApiKey':
|
|
180
|
+
return 'HUGGINGFACE_HUB_API_KEY';
|
|
181
|
+
default:
|
|
182
|
+
return 'COCO_' + key.replace(/([A-Z])/g, '_$1').toUpperCase();
|
|
164
183
|
}
|
|
165
|
-
|
|
166
|
-
|
|
184
|
+
}
|
|
185
|
+
function formatEnvValue(value) {
|
|
186
|
+
if (typeof value === 'number') {
|
|
187
|
+
return `${value}`;
|
|
167
188
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
let output = `${indent}- Path: ${this.getPath()}\n`;
|
|
171
|
-
if (this.files.length > 0) {
|
|
172
|
-
output += `${indent} Files:\n`;
|
|
173
|
-
for (const file of this.files) {
|
|
174
|
-
output += `${indent} - ${file.summary}\n`;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
if (this.children.size > 0) {
|
|
178
|
-
output += `${indent} Children:\n`;
|
|
179
|
-
for (const [, child] of this.children) {
|
|
180
|
-
output += child.print(indentation + 4);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return output;
|
|
189
|
+
else if (Array.isArray(value)) {
|
|
190
|
+
return `${value.join(',')}`;
|
|
184
191
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
for (const change of changes) {
|
|
189
|
-
let currentParent = root;
|
|
190
|
-
const parts = change.filePath.split('/');
|
|
191
|
-
parts.pop();
|
|
192
|
-
for (const part of parts) {
|
|
193
|
-
let childNode = currentParent.getChild(part);
|
|
194
|
-
if (!childNode) {
|
|
195
|
-
childNode = new DiffTreeNode([...currentParent.path, part]);
|
|
196
|
-
currentParent.addChild(part, childNode);
|
|
197
|
-
}
|
|
198
|
-
currentParent = childNode;
|
|
199
|
-
}
|
|
200
|
-
// Create a NodeFile object and add it to the parent
|
|
201
|
-
currentParent.addFile({
|
|
202
|
-
filePath: change.filePath,
|
|
203
|
-
oldFilePath: change.oldFilePath,
|
|
204
|
-
summary: change.summary,
|
|
205
|
-
status: change.status,
|
|
206
|
-
});
|
|
192
|
+
else if (typeof value === 'string') {
|
|
193
|
+
// Escape newlines and tabs in strings
|
|
194
|
+
return `${value.replace(/\n/g, '\\n').replace(/\t/g, '\\t')}`;
|
|
207
195
|
}
|
|
208
|
-
return
|
|
196
|
+
return `${value}`;
|
|
197
|
+
}
|
|
198
|
+
const appendToEnvFile = async (filePath, config) => {
|
|
199
|
+
const startComment = '# -- Start coco config --';
|
|
200
|
+
const endComment = '# -- End coco config --';
|
|
201
|
+
const getNewContent = async () => {
|
|
202
|
+
return Object.entries(config)
|
|
203
|
+
.map(([key, value]) => `${toEnvVarName(key)}=${formatEnvValue(value)}`)
|
|
204
|
+
.join('\n');
|
|
205
|
+
};
|
|
206
|
+
await updateFileSection({
|
|
207
|
+
filePath,
|
|
208
|
+
startComment,
|
|
209
|
+
endComment,
|
|
210
|
+
getNewContent,
|
|
211
|
+
confirmMessage: CONFIG_ALREADY_EXISTS,
|
|
212
|
+
});
|
|
209
213
|
};
|
|
210
214
|
|
|
211
215
|
/**
|
|
212
|
-
*
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
216
|
+
* Load git profile config (from ~/.gitconfig)
|
|
217
|
+
*
|
|
218
|
+
* @param {Config} config
|
|
219
|
+
* @returns {Config} Updated config
|
|
220
|
+
**/
|
|
221
|
+
function loadGitConfig(config) {
|
|
222
|
+
const gitConfigPath = path.join(os.homedir(), '.gitconfig');
|
|
223
|
+
if (fs.existsSync(gitConfigPath)) {
|
|
224
|
+
const gitConfigRaw = fs.readFileSync(gitConfigPath, 'utf-8');
|
|
225
|
+
const gitConfigParsed = ini.parse(gitConfigRaw);
|
|
226
|
+
config = {
|
|
227
|
+
...config,
|
|
228
|
+
service: gitConfigParsed.coco?.model || config.service,
|
|
229
|
+
openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
|
|
230
|
+
huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
|
|
231
|
+
tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
|
|
232
|
+
prompt: gitConfigParsed.coco?.prompt || config.prompt,
|
|
233
|
+
mode: gitConfigParsed.coco?.mode || config.mode,
|
|
234
|
+
temperature: gitConfigParsed.coco?.temperature || config.temperature,
|
|
235
|
+
summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
|
|
236
|
+
ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
|
|
237
|
+
ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
|
|
238
|
+
defaultBranch: gitConfigParsed.coco?.defaultBranch || config.defaultBranch,
|
|
229
239
|
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const childrenPromises = Array.from(node.children.values()).map(async (child) => collectDiffs(child, getFileDiff, tokenizer, logger));
|
|
233
|
-
const [diffs, children] = await Promise.all([
|
|
234
|
-
Promise.all(diffPromises),
|
|
235
|
-
Promise.all(childrenPromises),
|
|
236
|
-
]);
|
|
237
|
-
return {
|
|
238
|
-
path: node.getPath(),
|
|
239
|
-
diffs,
|
|
240
|
-
children,
|
|
241
|
-
};
|
|
240
|
+
}
|
|
241
|
+
return config;
|
|
242
242
|
}
|
|
243
|
-
|
|
244
243
|
/**
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
* @param
|
|
248
|
-
* @
|
|
244
|
+
* Appends the provided configuration to a git config file.
|
|
245
|
+
*
|
|
246
|
+
* @param filePath - The path to the .gitconfig
|
|
247
|
+
* @param config - The configuration object to append.
|
|
249
248
|
*/
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
throw new Error(`Invalid model: ${name}`);
|
|
254
|
-
}
|
|
255
|
-
switch (llm) {
|
|
256
|
-
case 'huggingface':
|
|
257
|
-
return new HuggingFaceInference({
|
|
258
|
-
model: model,
|
|
259
|
-
apiKey: key,
|
|
260
|
-
maxConcurrency: 4,
|
|
261
|
-
...fields,
|
|
262
|
-
});
|
|
263
|
-
case 'openai':
|
|
264
|
-
default:
|
|
265
|
-
return new OpenAI({
|
|
266
|
-
openAIApiKey: key,
|
|
267
|
-
modelName: model,
|
|
268
|
-
...fields,
|
|
269
|
-
});
|
|
249
|
+
const appendToGitConfig = async (filePath, config) => {
|
|
250
|
+
if (!fs.existsSync(filePath)) {
|
|
251
|
+
throw new Error(`File ${filePath} does not exist.`);
|
|
270
252
|
}
|
|
271
|
-
|
|
253
|
+
const startComment = '# -- Start coco config --';
|
|
254
|
+
const endComment = '# -- End coco config --';
|
|
255
|
+
const header = '[coco]';
|
|
256
|
+
// Function to generate new content for the coco section
|
|
257
|
+
const getNewContent = async () => {
|
|
258
|
+
const contentLines = [header];
|
|
259
|
+
for (const key in config) {
|
|
260
|
+
// check if string has new lines, if so, wrap in quotes
|
|
261
|
+
if (typeof config[key] === 'string') {
|
|
262
|
+
const value = config[key];
|
|
263
|
+
if (value.includes('\n')) {
|
|
264
|
+
contentLines.push(`\t${key} = ${JSON.stringify(value)}`);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
contentLines.push(`\t${key} = ${config[key]}`);
|
|
269
|
+
}
|
|
270
|
+
return contentLines.join('\n');
|
|
271
|
+
};
|
|
272
|
+
await updateFileSection({
|
|
273
|
+
filePath,
|
|
274
|
+
startComment,
|
|
275
|
+
endComment,
|
|
276
|
+
getNewContent,
|
|
277
|
+
confirmUpdate: true,
|
|
278
|
+
confirmMessage: CONFIG_ALREADY_EXISTS,
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
|
|
272
282
|
/**
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
* @param
|
|
283
|
+
* Load .gitignore in project root
|
|
284
|
+
*
|
|
285
|
+
* @param {Config} config
|
|
276
286
|
* @returns
|
|
277
287
|
*/
|
|
278
|
-
function
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
return options.huggingFaceHubApiKey;
|
|
286
|
-
case 'openai':
|
|
287
|
-
default:
|
|
288
|
-
return options.openAIApiKey;
|
|
288
|
+
function loadGitignore(config) {
|
|
289
|
+
if (fs.existsSync('.gitignore')) {
|
|
290
|
+
const gitignoreContent = fs.readFileSync('.gitignore', 'utf-8');
|
|
291
|
+
config.ignoredFiles = [
|
|
292
|
+
...(config?.ignoredFiles || []),
|
|
293
|
+
...gitignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
294
|
+
];
|
|
289
295
|
}
|
|
296
|
+
return config;
|
|
290
297
|
}
|
|
291
298
|
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
299
|
+
* Load .ignore in project root
|
|
300
|
+
*
|
|
301
|
+
* @param {Config} config
|
|
294
302
|
* @returns
|
|
295
303
|
*/
|
|
296
|
-
function
|
|
297
|
-
|
|
304
|
+
function loadIgnore(config) {
|
|
305
|
+
if (fs.existsSync('.ignore')) {
|
|
306
|
+
const ignoreContent = fs.readFileSync('.ignore', 'utf-8');
|
|
307
|
+
config.ignoredFiles = [
|
|
308
|
+
...(config?.ignoredFiles || []),
|
|
309
|
+
...ignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
310
|
+
];
|
|
311
|
+
}
|
|
312
|
+
return config;
|
|
298
313
|
}
|
|
314
|
+
|
|
299
315
|
/**
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
* @param
|
|
303
|
-
* @returns
|
|
304
|
-
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
template,
|
|
314
|
-
inputVariables: variables,
|
|
315
|
-
})
|
|
316
|
-
: fallback);
|
|
316
|
+
* Load project config
|
|
317
|
+
*
|
|
318
|
+
* @param {Config} config
|
|
319
|
+
* @returns {Config} Updated config
|
|
320
|
+
**/
|
|
321
|
+
function loadProjectConfig(config) {
|
|
322
|
+
// TODO: Add validation based of JSON schema?
|
|
323
|
+
// @see https://github.com/acornejo/jjv
|
|
324
|
+
if (fs.existsSync('.coco.config.json')) {
|
|
325
|
+
const projectConfig = JSON.parse(fs.readFileSync('.coco.config.json', 'utf-8'));
|
|
326
|
+
config = { ...config, ...projectConfig };
|
|
327
|
+
}
|
|
328
|
+
return config;
|
|
317
329
|
}
|
|
330
|
+
const appendToProjectConfig = (filePath, config) => {
|
|
331
|
+
fs.writeFileSync(filePath, JSON.stringify({
|
|
332
|
+
$schema: 'https://git-co.co/schema.json',
|
|
333
|
+
...config,
|
|
334
|
+
}, null, 2));
|
|
335
|
+
};
|
|
336
|
+
|
|
318
337
|
/**
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
* @param
|
|
322
|
-
* @returns
|
|
338
|
+
* Load XDG config
|
|
339
|
+
*
|
|
340
|
+
* @param {Config} config
|
|
341
|
+
* @returns {Config} Updated config
|
|
323
342
|
*/
|
|
324
|
-
function
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
inputVariables.map((value) => `{${value}}`).join(', '));
|
|
331
|
-
}
|
|
332
|
-
return true;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const template$2 = `GOAL: Use functional abstractions to summarize the following text
|
|
336
|
-
|
|
337
|
-
RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
|
|
338
|
-
|
|
339
|
-
TEXT:"""{text}"""
|
|
340
|
-
`;
|
|
341
|
-
const inputVariables$2 = ['text'];
|
|
342
|
-
const SUMMARIZE_PROMPT = new PromptTemplate({
|
|
343
|
-
template: template$2,
|
|
344
|
-
inputVariables: inputVariables$2,
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
async function parseDefaultFileDiff(nodeFile, commit = '--staged', git) {
|
|
348
|
-
if (commit !== '--staged') {
|
|
349
|
-
return await git.diff([`${commit}~1..${commit}`, '--', nodeFile.filePath]);
|
|
350
|
-
}
|
|
351
|
-
return await git.diff([commit, nodeFile.filePath]);
|
|
352
|
-
}
|
|
353
|
-
async function parseRenamedFileDiff(nodeFile, commit, git, logger) {
|
|
354
|
-
let result = '';
|
|
355
|
-
const oldFilePath = nodeFile?.oldFilePath || nodeFile.filePath;
|
|
356
|
-
let previousCommitHash = 'HEAD';
|
|
357
|
-
let newCommitHash = '';
|
|
358
|
-
if (commit !== '--staged') {
|
|
359
|
-
try {
|
|
360
|
-
previousCommitHash = await git.revparse([`${commit}~1`]);
|
|
361
|
-
}
|
|
362
|
-
catch (err) {
|
|
363
|
-
logger.verbose(`Error getting previous commit hash for ${nodeFile.filePath}`, {
|
|
364
|
-
color: 'red',
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
newCommitHash = commit;
|
|
368
|
-
}
|
|
369
|
-
try {
|
|
370
|
-
const [previousContent, newContent] = await Promise.all([
|
|
371
|
-
git.show([`${previousCommitHash}:${oldFilePath}`]),
|
|
372
|
-
git.show([`${newCommitHash}:${nodeFile.filePath}`]),
|
|
373
|
-
]);
|
|
374
|
-
if (previousContent !== newContent) {
|
|
375
|
-
result = createTwoFilesPatch(oldFilePath, nodeFile.filePath, previousContent, newContent, '', '', {
|
|
376
|
-
context: 3,
|
|
377
|
-
});
|
|
378
|
-
// remove the first 4 lines of the patch (they contain the old and new file names)
|
|
379
|
-
result = result.split('\n').slice(4).join('\n');
|
|
380
|
-
}
|
|
381
|
-
else {
|
|
382
|
-
result = 'File contents are unchanged.';
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
catch (err) {
|
|
386
|
-
logger.verbose(`Error comparing file contents for ${nodeFile.filePath}`, { color: 'red' });
|
|
387
|
-
result = 'Error comparing file contents.';
|
|
388
|
-
}
|
|
389
|
-
return result;
|
|
390
|
-
}
|
|
391
|
-
async function getDiff(nodeFile, commit, { git, logger, }) {
|
|
392
|
-
if (nodeFile.status === 'deleted') {
|
|
393
|
-
return 'This file has been deleted.';
|
|
394
|
-
}
|
|
395
|
-
if (nodeFile.status === 'renamed' && nodeFile.oldFilePath) {
|
|
396
|
-
const renamedDiff = await parseRenamedFileDiff(nodeFile, commit, git, logger);
|
|
397
|
-
return renamedDiff;
|
|
343
|
+
function loadXDGConfig(config) {
|
|
344
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
345
|
+
const xdgConfigPath = path.join(xdgConfigHome, 'coco', 'config.json');
|
|
346
|
+
if (fs.existsSync(xdgConfigPath)) {
|
|
347
|
+
const xdgConfig = JSON.parse(fs.readFileSync(xdgConfigPath, 'utf-8'));
|
|
348
|
+
config = { ...config, ...xdgConfig };
|
|
398
349
|
}
|
|
399
|
-
|
|
400
|
-
const defaultDiff = await parseDefaultFileDiff(nodeFile, commit, git);
|
|
401
|
-
return defaultDiff;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const MAX_TOKENS_PER_SUMMARY = 2048;
|
|
405
|
-
async function fileChangeParser({ changes, commit, options: { tokenizer, git, model, logger }, }) {
|
|
406
|
-
const textSplitter = getTextSplitter({ chunkSize: 2000, chunkOverlap: 125 });
|
|
407
|
-
const summarizationChain = getChain(model, {
|
|
408
|
-
type: 'map_reduce',
|
|
409
|
-
combineMapPrompt: SUMMARIZE_PROMPT,
|
|
410
|
-
combinePrompt: SUMMARIZE_PROMPT,
|
|
411
|
-
});
|
|
412
|
-
logger.startTimer();
|
|
413
|
-
const rootTreeNode = createDiffTree(changes);
|
|
414
|
-
logger.stopTimer('Created file hierarchy');
|
|
415
|
-
// Collect diffs
|
|
416
|
-
logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
|
|
417
|
-
const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, commit, { git, logger }), tokenizer, logger);
|
|
418
|
-
logger.stopSpinner('Diffs Collected').stopTimer();
|
|
419
|
-
// Summarize diffs
|
|
420
|
-
logger.startTimer();
|
|
421
|
-
const summary = await summarizeDiffs(diffs, {
|
|
422
|
-
tokenizer,
|
|
423
|
-
maxTokens: MAX_TOKENS_PER_SUMMARY,
|
|
424
|
-
textSplitter,
|
|
425
|
-
chain: summarizationChain,
|
|
426
|
-
logger,
|
|
427
|
-
});
|
|
428
|
-
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
429
|
-
return summary;
|
|
350
|
+
return config;
|
|
430
351
|
}
|
|
431
352
|
|
|
432
353
|
/**
|
|
433
|
-
*
|
|
354
|
+
* Load application config
|
|
434
355
|
*
|
|
435
|
-
*
|
|
356
|
+
* Merge config from multiple sources.
|
|
436
357
|
*
|
|
437
|
-
*
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
358
|
+
* \* Order of precedence:
|
|
359
|
+
* \* 1. Command line flags
|
|
360
|
+
* \* 2. Environment variables
|
|
361
|
+
* \* 3. Project config
|
|
362
|
+
* \* 4. Git config
|
|
363
|
+
* \* 5. XDG config
|
|
364
|
+
* \* 6. .gitignore
|
|
365
|
+
* \* 7. .ignore
|
|
366
|
+
* \* 8. Default config
|
|
367
|
+
*
|
|
368
|
+
* @returns {Config} application config
|
|
369
|
+
**/
|
|
370
|
+
function loadConfig(argv = {}) {
|
|
371
|
+
// Default config
|
|
372
|
+
let config = DEFAULT_CONFIG;
|
|
373
|
+
config = loadGitignore(config);
|
|
374
|
+
config = loadIgnore(config);
|
|
375
|
+
config = loadXDGConfig(config);
|
|
376
|
+
config = loadGitConfig(config);
|
|
377
|
+
config = loadProjectConfig(config);
|
|
378
|
+
config = loadEnvConfig(config);
|
|
379
|
+
return { ...config, ...argv };
|
|
380
|
+
}
|
|
453
381
|
|
|
454
382
|
class Logger {
|
|
455
383
|
constructor(config) {
|
|
@@ -502,391 +430,492 @@ class Logger {
|
|
|
502
430
|
}
|
|
503
431
|
}
|
|
504
432
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
- DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
|
|
512
|
-
- DO NOT use specific names or files from the code
|
|
513
|
-
- Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
|
|
514
|
-
|
|
515
|
-
"""{summary}"""
|
|
516
|
-
|
|
517
|
-
Commit:`;
|
|
518
|
-
const inputVariables$1 = ['summary'];
|
|
519
|
-
const COMMIT_PROMPT = new PromptTemplate({
|
|
520
|
-
template: template$1,
|
|
521
|
-
inputVariables: inputVariables$1,
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
function getStatus(file, location = 'index') {
|
|
525
|
-
if ('index' in file && 'working_dir' in file) {
|
|
526
|
-
const statusCode = file[location];
|
|
527
|
-
switch (statusCode) {
|
|
528
|
-
case 'A':
|
|
529
|
-
return 'added';
|
|
530
|
-
case 'D':
|
|
531
|
-
return 'deleted';
|
|
532
|
-
case 'M':
|
|
533
|
-
return 'modified';
|
|
534
|
-
case 'R':
|
|
535
|
-
return 'renamed';
|
|
536
|
-
case '?':
|
|
537
|
-
return 'untracked';
|
|
538
|
-
default:
|
|
539
|
-
return 'unknown';
|
|
433
|
+
function commandExecutor(handler) {
|
|
434
|
+
return async (argv) => {
|
|
435
|
+
const options = loadConfig(argv);
|
|
436
|
+
const logger = new Logger(options);
|
|
437
|
+
try {
|
|
438
|
+
await handler(argv, logger);
|
|
540
439
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
return 'added';
|
|
549
|
-
if (file.insertions === 0 && file.deletions > 0)
|
|
550
|
-
return 'deleted';
|
|
551
|
-
if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
|
|
552
|
-
return 'modified';
|
|
553
|
-
return 'unknown';
|
|
554
|
-
}
|
|
555
|
-
else {
|
|
556
|
-
throw new Error('Invalid file type');
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function getSummaryText(file, change) {
|
|
561
|
-
const status = change.status || getStatus(file);
|
|
562
|
-
let filePath;
|
|
563
|
-
if ('path' in file) {
|
|
564
|
-
filePath = file.path;
|
|
565
|
-
}
|
|
566
|
-
else if ('file' in file) {
|
|
567
|
-
filePath = change?.filePath || file.file;
|
|
568
|
-
}
|
|
569
|
-
else {
|
|
570
|
-
throw new Error('Invalid file type');
|
|
571
|
-
}
|
|
572
|
-
if (change.oldFilePath) {
|
|
573
|
-
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
574
|
-
}
|
|
575
|
-
return `${status}: ${filePath}`;
|
|
440
|
+
catch (error) {
|
|
441
|
+
logger.log('\nFailed to execute command', { color: 'yellow' });
|
|
442
|
+
logger.verbose(`\nError: "${error.message}"`, { color: 'red' });
|
|
443
|
+
logger.log('\nThanks for using coco, make it a great day! ππ€', { color: 'blue' });
|
|
444
|
+
process.exit(0);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
576
447
|
}
|
|
577
448
|
|
|
578
449
|
/**
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
* @
|
|
582
|
-
* @returns
|
|
450
|
+
* Extract the path from a file path string.
|
|
451
|
+
* @param {string} filePath - The full file path.
|
|
452
|
+
* @returns {string} The path portion of the file path.
|
|
583
453
|
*/
|
|
584
|
-
function
|
|
585
|
-
return
|
|
454
|
+
function getPathFromFilePath(filePath) {
|
|
455
|
+
return filePath.split('/').slice(0, -1).join('/');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function summarize(documents, { chain, textSplitter, options }) {
|
|
459
|
+
const { returnIntermediateSteps = false } = options || {};
|
|
460
|
+
const docs = await textSplitter.splitDocuments(documents.map((doc) => new Document(doc)));
|
|
461
|
+
const res = await chain.call({
|
|
462
|
+
input_documents: docs,
|
|
463
|
+
returnIntermediateSteps,
|
|
464
|
+
});
|
|
465
|
+
if (res.error)
|
|
466
|
+
throw new Error(res.error);
|
|
467
|
+
return res.text && res.text.trim();
|
|
586
468
|
}
|
|
587
469
|
|
|
588
470
|
/**
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
* @
|
|
592
|
-
*/
|
|
593
|
-
const DEFAULT_CONFIG = {
|
|
594
|
-
model: 'openai/gpt-4',
|
|
595
|
-
verbose: false,
|
|
596
|
-
tokenLimit: 1024,
|
|
597
|
-
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
598
|
-
temperature: 0.4,
|
|
599
|
-
mode: 'stdout',
|
|
600
|
-
ignoredFiles: ['package-lock.json'],
|
|
601
|
-
ignoredExtensions: ['.map', '.lock'],
|
|
602
|
-
defaultBranch: 'main',
|
|
603
|
-
};
|
|
604
|
-
/**
|
|
605
|
-
* Config keys
|
|
606
|
-
*
|
|
607
|
-
* @type {string[]}
|
|
471
|
+
* Create groups from a given node info.
|
|
472
|
+
* @param {DiffNode} node - The node info to start grouping.
|
|
473
|
+
* @returns {DirectoryDiff[]} The groups created.
|
|
608
474
|
*/
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
async function updateFileSection(filePath, startComment, endComment, getNewContent, confirmUpdate = true) {
|
|
617
|
-
const lines = fs__default.existsSync(filePath) ? fs__default.readFileSync(filePath, 'utf-8').split(/\r?\n/) : [];
|
|
618
|
-
const newLines = [];
|
|
619
|
-
let foundSection = false;
|
|
620
|
-
for (let i = 0; i < lines.length; i++) {
|
|
621
|
-
if (lines[i].trim() === startComment) {
|
|
622
|
-
foundSection = true;
|
|
623
|
-
if (confirmUpdate) {
|
|
624
|
-
const confirmOverwrite = await confirm({
|
|
625
|
-
message: `A section already exists in ${filePath}, do you want to override it?`,
|
|
626
|
-
default: false,
|
|
627
|
-
});
|
|
628
|
-
if (!confirmOverwrite) {
|
|
629
|
-
// keep all lines until the end comment
|
|
630
|
-
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
631
|
-
newLines.push(lines[i]);
|
|
632
|
-
i++;
|
|
633
|
-
}
|
|
634
|
-
newLines.push(endComment);
|
|
635
|
-
continue;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
newLines.push(startComment);
|
|
639
|
-
// Insert the new content
|
|
640
|
-
const newContent = await getNewContent();
|
|
641
|
-
newLines.push(newContent);
|
|
642
|
-
// Skip the existing content of the section
|
|
643
|
-
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
644
|
-
i++;
|
|
475
|
+
function createDirectoryDiffs(node) {
|
|
476
|
+
const groupByPath = {};
|
|
477
|
+
function traverse(node) {
|
|
478
|
+
node.diffs.forEach((diff) => {
|
|
479
|
+
const path = getPathFromFilePath(diff.file);
|
|
480
|
+
if (!groupByPath[path]) {
|
|
481
|
+
groupByPath[path] = { diffs: [], path, tokenCount: 0 };
|
|
645
482
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
newLines.push(lines[i]);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
// If section wasn't found, append it at the end
|
|
654
|
-
if (!foundSection) {
|
|
655
|
-
newLines.push('\n' + startComment);
|
|
656
|
-
const newContent = await getNewContent();
|
|
657
|
-
newLines.push(newContent);
|
|
658
|
-
newLines.push(endComment);
|
|
483
|
+
groupByPath[path].diffs.push(diff);
|
|
484
|
+
groupByPath[path].tokenCount += diff.tokenCount;
|
|
485
|
+
});
|
|
486
|
+
node.children.forEach(traverse);
|
|
659
487
|
}
|
|
660
|
-
|
|
661
|
-
|
|
488
|
+
traverse(node);
|
|
489
|
+
return Object.values(groupByPath);
|
|
662
490
|
}
|
|
663
|
-
|
|
664
491
|
/**
|
|
665
|
-
*
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
492
|
+
* Summarize a directory diff asynchronously.
|
|
493
|
+
*/
|
|
494
|
+
async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenizer }) {
|
|
495
|
+
try {
|
|
496
|
+
const directorySummary = await summarize(directory.diffs.map((diff) => ({
|
|
497
|
+
pageContent: diff.diff,
|
|
498
|
+
metadata: {
|
|
499
|
+
file: diff.file,
|
|
500
|
+
summary: diff.summary,
|
|
501
|
+
},
|
|
502
|
+
})), {
|
|
503
|
+
chain,
|
|
504
|
+
textSplitter,
|
|
505
|
+
options: {
|
|
506
|
+
returnIntermediateSteps: true,
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
const newTokenTotal = tokenizer(directorySummary);
|
|
510
|
+
return {
|
|
511
|
+
diffs: directory.diffs,
|
|
512
|
+
path: directory.path,
|
|
513
|
+
summary: directorySummary,
|
|
514
|
+
tokenCount: newTokenTotal,
|
|
515
|
+
};
|
|
689
516
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
return value.split(',');
|
|
517
|
+
catch (error) {
|
|
518
|
+
console.error(error);
|
|
519
|
+
return directory;
|
|
694
520
|
}
|
|
695
|
-
return value;
|
|
696
521
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
522
|
+
const defaultOutputCallback = (group) => {
|
|
523
|
+
let output = `
|
|
524
|
+
-------\n* changes in "/${group.path}"\n\n`;
|
|
525
|
+
if (group.summary) {
|
|
526
|
+
output += `${group.diffs.map((diff) => ` β’ ${diff.summary}`).join('\n')}\n\nSummary:\n\n${group.summary}\n\n`;
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
output += `${group.diffs.map((diff) => ` β’ ${diff.summary}\n\n${diff.diff}`).join('\n\n')}\n\n`;
|
|
705
530
|
}
|
|
531
|
+
return output;
|
|
532
|
+
};
|
|
533
|
+
async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 2048, textSplitter, chain, handleOutput = defaultOutputCallback, }) {
|
|
534
|
+
const queue = new pQueue({ concurrency: 8 });
|
|
535
|
+
logger.startTimer().startSpinner(`Organizing Diffs...`, { color: 'blue' });
|
|
536
|
+
const directoryDiffs = createDirectoryDiffs(rootDiffNode);
|
|
537
|
+
// Sort by token count descending
|
|
538
|
+
directoryDiffs.sort((a, b) => b.tokenCount - a.tokenCount);
|
|
539
|
+
let totalTokenCount = directoryDiffs.reduce((sum, group) => sum + group.tokenCount, 0);
|
|
540
|
+
logger.stopSpinner('Diffs Organized').stopTimer();
|
|
541
|
+
logger.startSpinner(`Consolidating Diffs`, { color: 'blue' });
|
|
542
|
+
const processingTasks = directoryDiffs.map((group, i) => {
|
|
543
|
+
return queue.add(async () => {
|
|
544
|
+
// If the diff token count is already less than the average req, we can skip summarizing.
|
|
545
|
+
const isLessThanAvgTokenReq = group.tokenCount <= maxTokens / directoryDiffs.length;
|
|
546
|
+
if (totalTokenCount <= maxTokens || isLessThanAvgTokenReq) {
|
|
547
|
+
return group;
|
|
548
|
+
}
|
|
549
|
+
group = await summarizeDirectoryDiff(group, {
|
|
550
|
+
chain,
|
|
551
|
+
textSplitter,
|
|
552
|
+
tokenizer,
|
|
553
|
+
});
|
|
554
|
+
// We need to subtract the old token count and add the new one
|
|
555
|
+
totalTokenCount = totalTokenCount - directoryDiffs[i].tokenCount + group.tokenCount;
|
|
556
|
+
directoryDiffs[i] = group;
|
|
557
|
+
logger
|
|
558
|
+
.verbose(`\n β’ Summarized diffs in "/${group.path}" `, { color: 'blue' })
|
|
559
|
+
.verbose(`\nTotal token count: ${totalTokenCount}`, {
|
|
560
|
+
color: totalTokenCount > maxTokens ? 'yellow' : 'green',
|
|
561
|
+
});
|
|
562
|
+
return group;
|
|
563
|
+
}, { priority: group.tokenCount });
|
|
564
|
+
});
|
|
565
|
+
await Promise.all(processingTasks);
|
|
566
|
+
logger.stopSpinner(`Summarized Diffs`);
|
|
567
|
+
return directoryDiffs.map(handleOutput).join('');
|
|
706
568
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
569
|
+
|
|
570
|
+
class DiffTreeNode {
|
|
571
|
+
constructor(path) {
|
|
572
|
+
this.path = [];
|
|
573
|
+
this.files = [];
|
|
574
|
+
this.children = new Map();
|
|
575
|
+
if (path)
|
|
576
|
+
this.path = path;
|
|
710
577
|
}
|
|
711
|
-
|
|
712
|
-
|
|
578
|
+
addFile(file) {
|
|
579
|
+
this.files.push(file);
|
|
713
580
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
581
|
+
addChild(part, node) {
|
|
582
|
+
this.children.set(part, node);
|
|
583
|
+
}
|
|
584
|
+
getChild(part) {
|
|
585
|
+
return this.children.get(part);
|
|
586
|
+
}
|
|
587
|
+
getPath() {
|
|
588
|
+
return this.path.join('/');
|
|
589
|
+
}
|
|
590
|
+
print(indentation = 0) {
|
|
591
|
+
const indent = ' '.repeat(indentation);
|
|
592
|
+
let output = `${indent}- Path: ${this.getPath()}\n`;
|
|
593
|
+
if (this.files.length > 0) {
|
|
594
|
+
output += `${indent} Files:\n`;
|
|
595
|
+
for (const file of this.files) {
|
|
596
|
+
output += `${indent} - ${file.summary}\n`;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (this.children.size > 0) {
|
|
600
|
+
output += `${indent} Children:\n`;
|
|
601
|
+
for (const [, child] of this.children) {
|
|
602
|
+
output += child.print(indentation + 4);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return output;
|
|
717
606
|
}
|
|
718
|
-
return `${value}`;
|
|
719
607
|
}
|
|
720
|
-
const
|
|
721
|
-
const
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
608
|
+
const createDiffTree = (changes) => {
|
|
609
|
+
const root = new DiffTreeNode();
|
|
610
|
+
for (const change of changes) {
|
|
611
|
+
let currentParent = root;
|
|
612
|
+
const parts = change.filePath.split('/');
|
|
613
|
+
parts.pop();
|
|
614
|
+
for (const part of parts) {
|
|
615
|
+
let childNode = currentParent.getChild(part);
|
|
616
|
+
if (!childNode) {
|
|
617
|
+
childNode = new DiffTreeNode([...currentParent.path, part]);
|
|
618
|
+
currentParent.addChild(part, childNode);
|
|
619
|
+
}
|
|
620
|
+
currentParent = childNode;
|
|
621
|
+
}
|
|
622
|
+
// Create a NodeFile object and add it to the parent
|
|
623
|
+
currentParent.addFile({
|
|
624
|
+
filePath: change.filePath,
|
|
625
|
+
oldFilePath: change.oldFilePath,
|
|
626
|
+
summary: change.summary,
|
|
627
|
+
status: change.status,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
return root;
|
|
729
631
|
};
|
|
730
632
|
|
|
731
633
|
/**
|
|
732
|
-
*
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
|
|
748
|
-
prompt: gitConfigParsed.coco?.prompt || config.prompt,
|
|
749
|
-
mode: gitConfigParsed.coco?.mode || config.mode,
|
|
750
|
-
temperature: gitConfigParsed.coco?.temperature || config.temperature,
|
|
751
|
-
summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
|
|
752
|
-
ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
|
|
753
|
-
ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
|
|
754
|
-
defaultBranch: gitConfigParsed.coco?.defaultBranch || config.defaultBranch,
|
|
634
|
+
* Asynchronously collect diffs for a given node and its children.
|
|
635
|
+
*/
|
|
636
|
+
async function collectDiffs(node, getFileDiff, tokenizer, logger) {
|
|
637
|
+
// Collect diffs for the files of the current node
|
|
638
|
+
const diffPromises = node.files.map(async (nodeFile) => {
|
|
639
|
+
const diff = await getFileDiff(nodeFile);
|
|
640
|
+
const tokenCount = tokenizer(diff);
|
|
641
|
+
logger.verbose(`Collected diff for ${nodeFile.filePath} (${tokenCount} tokens)`, {
|
|
642
|
+
color: 'magenta',
|
|
643
|
+
});
|
|
644
|
+
return {
|
|
645
|
+
file: nodeFile.filePath,
|
|
646
|
+
summary: nodeFile.summary,
|
|
647
|
+
diff,
|
|
648
|
+
tokenCount,
|
|
755
649
|
};
|
|
650
|
+
});
|
|
651
|
+
// Collect diffs for the children of the current node
|
|
652
|
+
const childrenPromises = Array.from(node.children.values()).map(async (child) => collectDiffs(child, getFileDiff, tokenizer, logger));
|
|
653
|
+
const [diffs, children] = await Promise.all([
|
|
654
|
+
Promise.all(diffPromises),
|
|
655
|
+
Promise.all(childrenPromises),
|
|
656
|
+
]);
|
|
657
|
+
return {
|
|
658
|
+
path: node.getPath(),
|
|
659
|
+
diffs,
|
|
660
|
+
children,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function getModelAndProviderFromService(service) {
|
|
665
|
+
const [provider, model] = service.split(/\/(.*)/s);
|
|
666
|
+
if (!model || !provider) {
|
|
667
|
+
throw new Error(`Invalid service: ${service}`);
|
|
756
668
|
}
|
|
757
|
-
return
|
|
669
|
+
return { provider, model };
|
|
670
|
+
}
|
|
671
|
+
function getModelFromService(service) {
|
|
672
|
+
const { model } = getModelAndProviderFromService(service);
|
|
673
|
+
return model;
|
|
758
674
|
}
|
|
759
675
|
/**
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
* @param
|
|
763
|
-
* @
|
|
676
|
+
* Get LLM Model Based on Configuration
|
|
677
|
+
* @param fields
|
|
678
|
+
* @param configuration
|
|
679
|
+
* @returns LLM Model
|
|
764
680
|
*/
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
681
|
+
function getLlm(service, key, fields) {
|
|
682
|
+
const { provider, model } = getModelAndProviderFromService(service);
|
|
683
|
+
if (!model) {
|
|
684
|
+
throw new Error(`Invalid LLM Service: ${service}`);
|
|
768
685
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
return contentLines.join('\n');
|
|
787
|
-
};
|
|
788
|
-
// Use the updateFileSection utility
|
|
789
|
-
await updateFileSection(filePath, startComment, endComment, getNewContent);
|
|
790
|
-
};
|
|
791
|
-
|
|
686
|
+
switch (provider) {
|
|
687
|
+
case 'huggingface':
|
|
688
|
+
return new HuggingFaceInference({
|
|
689
|
+
model: model,
|
|
690
|
+
apiKey: key,
|
|
691
|
+
maxConcurrency: 4,
|
|
692
|
+
...fields,
|
|
693
|
+
});
|
|
694
|
+
case 'openai':
|
|
695
|
+
default:
|
|
696
|
+
return new OpenAI({
|
|
697
|
+
openAIApiKey: key,
|
|
698
|
+
modelName: model,
|
|
699
|
+
...fields,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
}
|
|
792
703
|
/**
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
* @param
|
|
704
|
+
* Retrieve appropriate API key based on selected model
|
|
705
|
+
* @param service
|
|
706
|
+
* @param options
|
|
796
707
|
* @returns
|
|
797
708
|
*/
|
|
798
|
-
function
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
709
|
+
function getApiKeyForModel(service, options) {
|
|
710
|
+
const { provider } = getModelAndProviderFromService(service);
|
|
711
|
+
switch (provider) {
|
|
712
|
+
case 'huggingface':
|
|
713
|
+
return options.huggingFaceHubApiKey;
|
|
714
|
+
case 'openai':
|
|
715
|
+
default:
|
|
716
|
+
return options.openAIApiKey;
|
|
805
717
|
}
|
|
806
|
-
return config;
|
|
807
718
|
}
|
|
808
719
|
/**
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
* @param {Config} config
|
|
720
|
+
* Get Recursive Character Text Splitter
|
|
721
|
+
* @param options
|
|
812
722
|
* @returns
|
|
813
723
|
*/
|
|
814
|
-
function
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
724
|
+
function getTextSplitter(options = {}) {
|
|
725
|
+
return new RecursiveCharacterTextSplitter(options);
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Get Summarization Chain
|
|
729
|
+
* @param model
|
|
730
|
+
* @param options
|
|
731
|
+
* @returns
|
|
732
|
+
*/
|
|
733
|
+
function getSummarizationChain(model, options = { type: 'map_reduce' }) {
|
|
734
|
+
return loadSummarizationChain(model, options);
|
|
735
|
+
}
|
|
736
|
+
function getPrompt({ template, variables, fallback }) {
|
|
737
|
+
if (!template && !fallback)
|
|
738
|
+
throw new Error('Must provide either a template or a fallback');
|
|
739
|
+
return (template
|
|
740
|
+
? new PromptTemplate({
|
|
741
|
+
template,
|
|
742
|
+
inputVariables: variables,
|
|
743
|
+
})
|
|
744
|
+
: fallback);
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Verify template string contains all required input variables
|
|
748
|
+
* @param text template string
|
|
749
|
+
* @param inputVariables template variables
|
|
750
|
+
* @returns boolean or error message
|
|
751
|
+
*/
|
|
752
|
+
function validatePromptTemplate(text, inputVariables) {
|
|
753
|
+
if (!text) {
|
|
754
|
+
return 'Prompt template cannot be empty';
|
|
755
|
+
}
|
|
756
|
+
if (!inputVariables.some((entry) => text.includes(entry))) {
|
|
757
|
+
return ('Prompt template must include at least one of the following input variables: ' +
|
|
758
|
+
inputVariables.map((value) => `{${value}}`).join(', '));
|
|
759
|
+
}
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async function parseDefaultFileDiff(nodeFile, commit = '--staged', git) {
|
|
764
|
+
if (commit !== '--staged') {
|
|
765
|
+
return await git.diff([`${commit}~1..${commit}`, '--', nodeFile.filePath]);
|
|
766
|
+
}
|
|
767
|
+
return await git.diff([commit, nodeFile.filePath]);
|
|
768
|
+
}
|
|
769
|
+
async function parseRenamedFileDiff(nodeFile, commit, git, logger) {
|
|
770
|
+
let result = '';
|
|
771
|
+
const oldFilePath = nodeFile?.oldFilePath || nodeFile.filePath;
|
|
772
|
+
let previousCommitHash = 'HEAD';
|
|
773
|
+
let newCommitHash = '';
|
|
774
|
+
if (commit !== '--staged') {
|
|
775
|
+
try {
|
|
776
|
+
previousCommitHash = await git.revparse([`${commit}~1`]);
|
|
777
|
+
}
|
|
778
|
+
catch (err) {
|
|
779
|
+
logger.verbose(`Error getting previous commit hash for ${nodeFile.filePath}`, {
|
|
780
|
+
color: 'red',
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
newCommitHash = commit;
|
|
784
|
+
}
|
|
785
|
+
try {
|
|
786
|
+
const [previousContent, newContent] = await Promise.all([
|
|
787
|
+
git.show([`${previousCommitHash}:${oldFilePath}`]),
|
|
788
|
+
git.show([`${newCommitHash}:${nodeFile.filePath}`]),
|
|
789
|
+
]);
|
|
790
|
+
if (previousContent !== newContent) {
|
|
791
|
+
result = createTwoFilesPatch(oldFilePath, nodeFile.filePath, previousContent, newContent, '', '', {
|
|
792
|
+
context: 3,
|
|
793
|
+
});
|
|
794
|
+
// remove the first 4 lines of the patch (they contain the old and new file names)
|
|
795
|
+
result = result.split('\n').slice(4).join('\n');
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
result = 'File contents are unchanged.';
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
catch (err) {
|
|
802
|
+
logger.verbose(`Error comparing file contents for ${nodeFile.filePath}`, { color: 'red' });
|
|
803
|
+
result = 'Error comparing file contents.';
|
|
804
|
+
}
|
|
805
|
+
return result;
|
|
806
|
+
}
|
|
807
|
+
async function getDiff(nodeFile, commit, { git, logger, }) {
|
|
808
|
+
if (nodeFile.status === 'deleted') {
|
|
809
|
+
return 'This file has been deleted.';
|
|
810
|
+
}
|
|
811
|
+
if (nodeFile.status === 'renamed' && nodeFile.oldFilePath) {
|
|
812
|
+
const renamedDiff = await parseRenamedFileDiff(nodeFile, commit, git, logger);
|
|
813
|
+
return renamedDiff;
|
|
814
|
+
}
|
|
815
|
+
// If not deleted or renamed, get the diff from the index
|
|
816
|
+
const defaultDiff = await parseDefaultFileDiff(nodeFile, commit, git);
|
|
817
|
+
return defaultDiff;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const MAX_TOKENS_PER_SUMMARY = 2048;
|
|
821
|
+
async function fileChangeParser({ changes, commit, options: { tokenizer, git, llm: model, logger }, }) {
|
|
822
|
+
const textSplitter = getTextSplitter({ chunkSize: 2000, chunkOverlap: 125 });
|
|
823
|
+
const summarizationChain = getSummarizationChain(model, {
|
|
824
|
+
type: 'map_reduce',
|
|
825
|
+
combineMapPrompt: SUMMARIZE_PROMPT,
|
|
826
|
+
combinePrompt: SUMMARIZE_PROMPT,
|
|
827
|
+
});
|
|
828
|
+
logger.startTimer();
|
|
829
|
+
const rootTreeNode = createDiffTree(changes);
|
|
830
|
+
logger.stopTimer('Created file hierarchy');
|
|
831
|
+
// Collect diffs
|
|
832
|
+
logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
|
|
833
|
+
const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, commit, { git, logger }), tokenizer, logger);
|
|
834
|
+
logger.stopSpinner('Diffs Collected').stopTimer();
|
|
835
|
+
// Summarize diffs
|
|
836
|
+
logger.startTimer();
|
|
837
|
+
const summary = await summarizeDiffs(diffs, {
|
|
838
|
+
tokenizer,
|
|
839
|
+
maxTokens: MAX_TOKENS_PER_SUMMARY,
|
|
840
|
+
textSplitter,
|
|
841
|
+
chain: summarizationChain,
|
|
842
|
+
logger,
|
|
843
|
+
});
|
|
844
|
+
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
845
|
+
return summary;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const template$1 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
|
|
849
|
+
Commit Messages must have a short description that is less than 50 characters followed by a newline character and then a more verbose detailed description.
|
|
850
|
+
|
|
851
|
+
- Typically a hyphen or asterisk is used for the bullet
|
|
852
|
+
- Write concisely using an informal tone
|
|
853
|
+
- DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
|
|
854
|
+
- DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
|
|
855
|
+
- DO NOT use specific names or files from the code
|
|
856
|
+
- Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
|
|
857
|
+
|
|
858
|
+
"""{summary}"""
|
|
859
|
+
|
|
860
|
+
Commit:`;
|
|
861
|
+
const inputVariables$1 = ['summary'];
|
|
862
|
+
const COMMIT_PROMPT = new PromptTemplate({
|
|
863
|
+
template: template$1,
|
|
864
|
+
inputVariables: inputVariables$1,
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
function getStatus(file, location = 'index') {
|
|
868
|
+
if ('index' in file && 'working_dir' in file) {
|
|
869
|
+
const statusCode = file[location];
|
|
870
|
+
switch (statusCode) {
|
|
871
|
+
case 'A':
|
|
872
|
+
return 'added';
|
|
873
|
+
case 'D':
|
|
874
|
+
return 'deleted';
|
|
875
|
+
case 'M':
|
|
876
|
+
return 'modified';
|
|
877
|
+
case 'R':
|
|
878
|
+
return 'renamed';
|
|
879
|
+
case '?':
|
|
880
|
+
return 'untracked';
|
|
881
|
+
default:
|
|
882
|
+
return 'unknown';
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
else if ('changes' in file && 'binary' in file) {
|
|
886
|
+
if (file.changes === 0)
|
|
887
|
+
return 'untracked';
|
|
888
|
+
if (file.file.includes('=>'))
|
|
889
|
+
return 'renamed';
|
|
890
|
+
if (file.deletions === 0 && file.insertions > 0)
|
|
891
|
+
return 'added';
|
|
892
|
+
if (file.insertions === 0 && file.deletions > 0)
|
|
893
|
+
return 'deleted';
|
|
894
|
+
if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
|
|
895
|
+
return 'modified';
|
|
896
|
+
return 'unknown';
|
|
821
897
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
/**
|
|
826
|
-
* Load project config
|
|
827
|
-
*
|
|
828
|
-
* @param {Config} config
|
|
829
|
-
* @returns {Config} Updated config
|
|
830
|
-
**/
|
|
831
|
-
function loadProjectConfig(config) {
|
|
832
|
-
// TODO: Add validation based of JSON schema?
|
|
833
|
-
// @see https://github.com/acornejo/jjv
|
|
834
|
-
if (fs.existsSync('.coco.config.json')) {
|
|
835
|
-
const projectConfig = JSON.parse(fs.readFileSync('.coco.config.json', 'utf-8'));
|
|
836
|
-
config = { ...config, ...projectConfig };
|
|
898
|
+
else {
|
|
899
|
+
throw new Error('Invalid file type');
|
|
837
900
|
}
|
|
838
|
-
return config;
|
|
839
901
|
}
|
|
840
|
-
const appendToProjectConfig = (filePath, config) => {
|
|
841
|
-
fs.writeFileSync(filePath, JSON.stringify({
|
|
842
|
-
$schema: 'https://git-co.co/schema.json',
|
|
843
|
-
...config,
|
|
844
|
-
}, null, 2));
|
|
845
|
-
};
|
|
846
902
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
*/
|
|
853
|
-
function loadXDGConfig(config) {
|
|
854
|
-
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
855
|
-
const xdgConfigPath = path.join(xdgConfigHome, 'coco', 'config.json');
|
|
856
|
-
if (fs.existsSync(xdgConfigPath)) {
|
|
857
|
-
const xdgConfig = JSON.parse(fs.readFileSync(xdgConfigPath, 'utf-8'));
|
|
858
|
-
config = { ...config, ...xdgConfig };
|
|
903
|
+
function getSummaryText(file, change) {
|
|
904
|
+
const status = change.status || getStatus(file);
|
|
905
|
+
let filePath;
|
|
906
|
+
if ('path' in file) {
|
|
907
|
+
filePath = file.path;
|
|
859
908
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
* \* 2. Environment variables
|
|
871
|
-
* \* 3. Project config
|
|
872
|
-
* \* 4. Git config
|
|
873
|
-
* \* 5. XDG config
|
|
874
|
-
* \* 6. .gitignore
|
|
875
|
-
* \* 7. .ignore
|
|
876
|
-
* \* 8. Default config
|
|
877
|
-
*
|
|
878
|
-
* @returns {Config} application config
|
|
879
|
-
**/
|
|
880
|
-
function loadConfig(argv = {}) {
|
|
881
|
-
// Default config
|
|
882
|
-
let config = DEFAULT_CONFIG;
|
|
883
|
-
config = loadGitignore(config);
|
|
884
|
-
config = loadIgnore(config);
|
|
885
|
-
config = loadXDGConfig(config);
|
|
886
|
-
config = loadGitConfig(config);
|
|
887
|
-
config = loadProjectConfig(config);
|
|
888
|
-
config = loadEnvConfig(config);
|
|
889
|
-
return { ...config, ...argv };
|
|
909
|
+
else if ('file' in file) {
|
|
910
|
+
filePath = change?.filePath || file.file;
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
throw new Error('Invalid file type');
|
|
914
|
+
}
|
|
915
|
+
if (change.oldFilePath) {
|
|
916
|
+
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
917
|
+
}
|
|
918
|
+
return `${status}: ${filePath}`;
|
|
890
919
|
}
|
|
891
920
|
|
|
892
921
|
const config = loadConfig();
|
|
@@ -974,11 +1003,6 @@ async function noResult({ git, logger }) {
|
|
|
974
1003
|
}
|
|
975
1004
|
}
|
|
976
1005
|
|
|
977
|
-
const isInteractive = (argv) => {
|
|
978
|
-
return argv?.mode === 'interactive' || argv.interactive;
|
|
979
|
-
};
|
|
980
|
-
const SEPERATOR = chalk.blue('----------------');
|
|
981
|
-
|
|
982
1006
|
function logResult(label, result) {
|
|
983
1007
|
console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
|
|
984
1008
|
}
|
|
@@ -995,41 +1019,51 @@ async function editResult(result, options) {
|
|
|
995
1019
|
return result;
|
|
996
1020
|
}
|
|
997
1021
|
|
|
998
|
-
async function getUserReviewDecision() {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1022
|
+
async function getUserReviewDecision({ label, descriptions, enableRetry = true, enableFullRetry = true, enableModifyPrompt = true, }) {
|
|
1023
|
+
const choices = [
|
|
1024
|
+
{
|
|
1025
|
+
name: 'β¨ Looks good!',
|
|
1026
|
+
value: 'approve',
|
|
1027
|
+
description: descriptions?.approve || `Continue with the generated ${label}`,
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
name: 'π Edit',
|
|
1031
|
+
value: 'edit',
|
|
1032
|
+
description: descriptions?.edit || `Edit the generated ${label} before proceeding`,
|
|
1033
|
+
},
|
|
1034
|
+
];
|
|
1035
|
+
if (enableModifyPrompt) {
|
|
1036
|
+
choices.push({
|
|
1037
|
+
name: 'πͺΆ Modify Prompt',
|
|
1038
|
+
value: 'modifyPrompt',
|
|
1039
|
+
description: descriptions?.modifyPrompt || `Modify the prompt template and regenerate the ${label}`,
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
if (enableRetry) {
|
|
1043
|
+
choices.push({
|
|
1044
|
+
name: 'π Retry',
|
|
1045
|
+
value: 'retryMessageOnly',
|
|
1046
|
+
description: descriptions?.retryMessageOnly ||
|
|
1047
|
+
`Restart the function execution from generating the ${label}`,
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
if (enableFullRetry) {
|
|
1051
|
+
choices.push({
|
|
1052
|
+
name: 'π Retry Full',
|
|
1053
|
+
value: 'retryFull',
|
|
1054
|
+
description: descriptions?.retryFull ||
|
|
1055
|
+
`Restart the function execution from the beginning, regenerating both the summary and ${label}`,
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
choices.push({
|
|
1059
|
+
name: 'π£ Cancel',
|
|
1060
|
+
value: 'cancel',
|
|
1061
|
+
description: descriptions?.cancel || `Cancel the ${label}`,
|
|
1032
1062
|
});
|
|
1063
|
+
return (await select({
|
|
1064
|
+
message: `Would you like to make any changes to the ${label}?`,
|
|
1065
|
+
choices,
|
|
1066
|
+
}));
|
|
1033
1067
|
}
|
|
1034
1068
|
|
|
1035
1069
|
async function editPrompt(options) {
|
|
@@ -1082,7 +1116,10 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
|
|
|
1082
1116
|
.stopTimer();
|
|
1083
1117
|
if (options?.interactive) {
|
|
1084
1118
|
logResult(label, result);
|
|
1085
|
-
const reviewAnswer = await getUserReviewDecision(
|
|
1119
|
+
const reviewAnswer = await getUserReviewDecision({
|
|
1120
|
+
label,
|
|
1121
|
+
...options?.review || {},
|
|
1122
|
+
});
|
|
1086
1123
|
if (reviewAnswer === 'cancel') {
|
|
1087
1124
|
process.exit(0);
|
|
1088
1125
|
}
|
|
@@ -1136,20 +1173,20 @@ const executeChain = async ({ llm, prompt, variables }) => {
|
|
|
1136
1173
|
return res.text.trim();
|
|
1137
1174
|
};
|
|
1138
1175
|
|
|
1139
|
-
async function createCommit(commitMsg, git) {
|
|
1140
|
-
return await git.commit(commitMsg);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
1176
|
const logSuccess = () => {
|
|
1144
1177
|
console.log(chalk.green(chalk.bold('\nAll set! π¦Ύπ€')));
|
|
1145
1178
|
};
|
|
1146
1179
|
|
|
1147
|
-
|
|
1148
|
-
// Handle resulting commit message
|
|
1180
|
+
async function handleResult({ result, mode, interactiveHandler }) {
|
|
1149
1181
|
switch (mode) {
|
|
1150
1182
|
case 'interactive':
|
|
1151
|
-
|
|
1152
|
-
|
|
1183
|
+
if (interactiveHandler) {
|
|
1184
|
+
await interactiveHandler(result);
|
|
1185
|
+
}
|
|
1186
|
+
else {
|
|
1187
|
+
console.warn('No result handler provided for interactive mode.');
|
|
1188
|
+
logSuccess();
|
|
1189
|
+
}
|
|
1153
1190
|
break;
|
|
1154
1191
|
case 'stdout':
|
|
1155
1192
|
default:
|
|
@@ -1157,7 +1194,7 @@ const handleResult = async (result, { mode, git }) => {
|
|
|
1157
1194
|
break;
|
|
1158
1195
|
}
|
|
1159
1196
|
process.exit(0);
|
|
1160
|
-
}
|
|
1197
|
+
}
|
|
1161
1198
|
|
|
1162
1199
|
const getRepo = () => {
|
|
1163
1200
|
let git;
|
|
@@ -1171,21 +1208,38 @@ const getRepo = () => {
|
|
|
1171
1208
|
return git;
|
|
1172
1209
|
};
|
|
1173
1210
|
|
|
1174
|
-
async
|
|
1175
|
-
|
|
1211
|
+
const getTikToken = async (modelName) => {
|
|
1212
|
+
return await encoding_for_model(modelName);
|
|
1213
|
+
};
|
|
1214
|
+
const getTokenCounter = async (modelName) => getTikToken(modelName).then((tokenizer) => (text) => {
|
|
1215
|
+
// console.log('Running GetTokenCount', { tokenizer, length: text.length })
|
|
1216
|
+
const tokens = tokenizer.encode(text);
|
|
1217
|
+
// console.log('Tokens', { tokenCount: tokens.length })
|
|
1218
|
+
return tokens.length;
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
async function createCommit(commitMsg, git) {
|
|
1222
|
+
return await git.commit(commitMsg);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const handler$2 = async (argv, logger) => {
|
|
1176
1226
|
const git = getRepo();
|
|
1177
1227
|
const options = loadConfig(argv);
|
|
1178
|
-
const
|
|
1179
|
-
const key = getApiKeyForModel(
|
|
1228
|
+
const { service } = options;
|
|
1229
|
+
const key = getApiKeyForModel(service, options);
|
|
1230
|
+
const tokenizer = await getTokenCounter(getModelFromService(service));
|
|
1180
1231
|
if (!key) {
|
|
1181
1232
|
logger.log(`No API Key found. ποΈπͺ`, { color: 'red' });
|
|
1182
1233
|
process.exit(1);
|
|
1183
1234
|
}
|
|
1184
|
-
const
|
|
1235
|
+
const llm = getLlm(service, key, {
|
|
1185
1236
|
temperature: 0.4,
|
|
1186
1237
|
maxConcurrency: 10,
|
|
1187
1238
|
});
|
|
1188
1239
|
const INTERACTIVE = isInteractive(options);
|
|
1240
|
+
if (INTERACTIVE) {
|
|
1241
|
+
logger.log(LOGO);
|
|
1242
|
+
}
|
|
1189
1243
|
async function factory() {
|
|
1190
1244
|
const changes = await getChanges({ git });
|
|
1191
1245
|
return changes.staged;
|
|
@@ -1194,16 +1248,16 @@ async function handler$2(argv) {
|
|
|
1194
1248
|
return await fileChangeParser({
|
|
1195
1249
|
changes,
|
|
1196
1250
|
commit: '--staged',
|
|
1197
|
-
options: { tokenizer, git,
|
|
1251
|
+
options: { tokenizer, git, llm, logger },
|
|
1198
1252
|
});
|
|
1199
1253
|
}
|
|
1200
1254
|
const commitMsg = await generateAndReviewLoop({
|
|
1201
|
-
label: '
|
|
1255
|
+
label: 'commit message',
|
|
1202
1256
|
factory,
|
|
1203
1257
|
parser,
|
|
1204
1258
|
agent: async (context, options) => {
|
|
1205
1259
|
return await executeChain({
|
|
1206
|
-
llm
|
|
1260
|
+
llm,
|
|
1207
1261
|
prompt: getPrompt({
|
|
1208
1262
|
template: options.prompt,
|
|
1209
1263
|
variables: COMMIT_PROMPT.inputVariables,
|
|
@@ -1221,14 +1275,27 @@ async function handler$2(argv) {
|
|
|
1221
1275
|
prompt: options.prompt || COMMIT_PROMPT.template,
|
|
1222
1276
|
logger,
|
|
1223
1277
|
interactive: INTERACTIVE,
|
|
1278
|
+
review: {
|
|
1279
|
+
descriptions: {
|
|
1280
|
+
approve: `Commit staged changes with generated commit message`,
|
|
1281
|
+
edit: 'Edit the commit message before proceeding',
|
|
1282
|
+
modifyPrompt: 'Modify the prompt template and regenerate the commit message',
|
|
1283
|
+
retryMessageOnly: 'Restart the function execution from generating the commit message',
|
|
1284
|
+
retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
|
|
1285
|
+
},
|
|
1286
|
+
},
|
|
1224
1287
|
},
|
|
1225
1288
|
});
|
|
1226
1289
|
const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
|
|
1227
|
-
handleResult(
|
|
1290
|
+
handleResult({
|
|
1291
|
+
result: commitMsg,
|
|
1292
|
+
interactiveHandler: async (result) => {
|
|
1293
|
+
await createCommit(result, git);
|
|
1294
|
+
logSuccess();
|
|
1295
|
+
},
|
|
1228
1296
|
mode: MODE,
|
|
1229
|
-
git,
|
|
1230
1297
|
});
|
|
1231
|
-
}
|
|
1298
|
+
};
|
|
1232
1299
|
|
|
1233
1300
|
/**
|
|
1234
1301
|
* Command line options via yargs
|
|
@@ -1287,7 +1354,7 @@ var commit = {
|
|
|
1287
1354
|
command: 'commit',
|
|
1288
1355
|
desc: 'Generate commit message',
|
|
1289
1356
|
builder: builder$2,
|
|
1290
|
-
handler: handler$2,
|
|
1357
|
+
handler: commandExecutor(handler$2),
|
|
1291
1358
|
options: options$2,
|
|
1292
1359
|
};
|
|
1293
1360
|
|
|
@@ -1364,20 +1431,22 @@ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main
|
|
|
1364
1431
|
return [];
|
|
1365
1432
|
}
|
|
1366
1433
|
|
|
1367
|
-
|
|
1434
|
+
const handler$1 = async (argv, logger) => {
|
|
1368
1435
|
const options = loadConfig(argv);
|
|
1369
|
-
const logger = new Logger(options);
|
|
1370
1436
|
const git = getRepo();
|
|
1371
|
-
const key = getApiKeyForModel(options.
|
|
1437
|
+
const key = getApiKeyForModel(options.service, options);
|
|
1372
1438
|
if (!key) {
|
|
1373
1439
|
logger.log(`No API Key found. ποΈπͺ`, { color: 'red' });
|
|
1374
1440
|
process.exit(1);
|
|
1375
1441
|
}
|
|
1376
|
-
const model =
|
|
1442
|
+
const model = getLlm(options.service, key, {
|
|
1377
1443
|
temperature: 0.4,
|
|
1378
1444
|
maxConcurrency: 10,
|
|
1379
1445
|
});
|
|
1380
1446
|
const INTERACTIVE = isInteractive(options);
|
|
1447
|
+
if (INTERACTIVE) {
|
|
1448
|
+
logger.log(LOGO);
|
|
1449
|
+
}
|
|
1381
1450
|
async function factory() {
|
|
1382
1451
|
if (options.range) {
|
|
1383
1452
|
const [from, to] = options.range?.split(':');
|
|
@@ -1395,7 +1464,7 @@ async function handler$1(argv) {
|
|
|
1395
1464
|
return result;
|
|
1396
1465
|
}
|
|
1397
1466
|
const changelogMsg = await generateAndReviewLoop({
|
|
1398
|
-
label: '
|
|
1467
|
+
label: 'changelog',
|
|
1399
1468
|
factory,
|
|
1400
1469
|
parser,
|
|
1401
1470
|
agent: async (context, options) => {
|
|
@@ -1423,14 +1492,20 @@ async function handler$1(argv) {
|
|
|
1423
1492
|
prompt: options.prompt || CHANGELOG_PROMPT.template,
|
|
1424
1493
|
logger,
|
|
1425
1494
|
interactive: INTERACTIVE,
|
|
1495
|
+
review: {
|
|
1496
|
+
enableFullRetry: false,
|
|
1497
|
+
},
|
|
1426
1498
|
},
|
|
1427
1499
|
});
|
|
1428
1500
|
const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
|
|
1429
|
-
handleResult(
|
|
1501
|
+
handleResult({
|
|
1502
|
+
result: changelogMsg,
|
|
1503
|
+
interactiveHandler: async () => {
|
|
1504
|
+
logSuccess();
|
|
1505
|
+
},
|
|
1430
1506
|
mode: MODE,
|
|
1431
|
-
git,
|
|
1432
1507
|
});
|
|
1433
|
-
}
|
|
1508
|
+
};
|
|
1434
1509
|
|
|
1435
1510
|
/**
|
|
1436
1511
|
* Command line options via yargs
|
|
@@ -1489,39 +1564,138 @@ var changelog = {
|
|
|
1489
1564
|
command: 'changelog',
|
|
1490
1565
|
desc: 'Generate a changelog from a commit range',
|
|
1491
1566
|
builder: builder$1,
|
|
1492
|
-
handler: handler$1,
|
|
1567
|
+
handler: commandExecutor(handler$1),
|
|
1493
1568
|
options: options$1,
|
|
1494
1569
|
};
|
|
1495
1570
|
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1571
|
+
/**
|
|
1572
|
+
* Finds the project root directory starting from the given current directory.
|
|
1573
|
+
* It checks if the `.git` directory or `package.json` file exists in the current directory or any of its parent directories.
|
|
1574
|
+
* If found, it returns the path to the project root directory.
|
|
1575
|
+
* If not found, it throws an error.
|
|
1576
|
+
*
|
|
1577
|
+
* @param currentDir - The current directory to start searching from.
|
|
1578
|
+
* @returns The path to the project root directory.
|
|
1579
|
+
* @throws Error if the project root directory cannot be found.
|
|
1580
|
+
*/
|
|
1581
|
+
function findProjectRoot(currentDir) {
|
|
1582
|
+
const root = path__default.parse(currentDir).root;
|
|
1583
|
+
while (currentDir !== root) {
|
|
1584
|
+
if (fs__default.existsSync(path__default.join(currentDir, '.git')) ||
|
|
1585
|
+
fs__default.existsSync(path__default.join(currentDir, 'package.json'))) {
|
|
1586
|
+
return currentDir;
|
|
1587
|
+
}
|
|
1588
|
+
currentDir = path__default.dirname(currentDir);
|
|
1589
|
+
}
|
|
1590
|
+
throw new Error('Unable to find project root. Are you in the right directory?');
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// Function to execute a command and return a promise
|
|
1594
|
+
function execPromise(command, options = {}) {
|
|
1595
|
+
return new Promise((resolve, reject) => {
|
|
1596
|
+
exec(command, options, (error, stdout, stderr) => {
|
|
1597
|
+
if (error) {
|
|
1598
|
+
reject(`Execution error: ${error}`);
|
|
1599
|
+
}
|
|
1600
|
+
else {
|
|
1601
|
+
resolve({ stdout, stderr });
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1509
1604
|
});
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* Installs a package using npm.
|
|
1609
|
+
*
|
|
1610
|
+
* @param {InstallPackageInput} options - The options for installing the package.
|
|
1611
|
+
* @returns {Promise<boolean>} - A promise that resolves to true if the package is installed successfully, false otherwise.
|
|
1612
|
+
*/
|
|
1613
|
+
async function installNpmPackage({ name, flags = [], cwd = process.cwd(), }) {
|
|
1614
|
+
const { stdout, stderr } = await execPromise(`npm i ${name} ${flags.join(' ')} --yes`, { cwd });
|
|
1615
|
+
if (stderr) {
|
|
1616
|
+
console.error(`Execution error: ${stderr}`);
|
|
1617
|
+
return false;
|
|
1618
|
+
}
|
|
1619
|
+
console.log(stdout);
|
|
1620
|
+
console.error(stderr);
|
|
1621
|
+
return true;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* Checks if a package is installed in a project.
|
|
1626
|
+
*
|
|
1627
|
+
* @param packageName - The name of the package to check.
|
|
1628
|
+
* @param projectPath - The path to the project.
|
|
1629
|
+
* @returns True if the package is installed, false otherwise.
|
|
1630
|
+
*/
|
|
1631
|
+
function isPackageInstalled(packageName, projectPath) {
|
|
1632
|
+
try {
|
|
1633
|
+
// Construct the path to the package.json file
|
|
1634
|
+
const packageJsonPath = path__default.join(projectPath, 'package.json');
|
|
1635
|
+
// Read the package.json file
|
|
1636
|
+
const packageJson = JSON.parse(fs__default.readFileSync(packageJsonPath, 'utf8'));
|
|
1637
|
+
// Check both dependencies and devDependencies
|
|
1638
|
+
const dependencies = packageJson.dependencies || {};
|
|
1639
|
+
const devDependencies = packageJson.devDependencies || {};
|
|
1640
|
+
// Return true if the package is found in either
|
|
1641
|
+
return dependencies.hasOwnProperty(packageName) || devDependencies.hasOwnProperty(packageName);
|
|
1642
|
+
}
|
|
1643
|
+
catch (error) {
|
|
1644
|
+
console.error(`Error checking package installation: ${error.message}`);
|
|
1645
|
+
return false;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// TODO: QoL improvement to import this from `package.json`
|
|
1650
|
+
const packageName = 'git-coco';
|
|
1651
|
+
async function checkAndHandlePackageInstallation({ global = false, logger, }) {
|
|
1652
|
+
try {
|
|
1653
|
+
// Global installation
|
|
1654
|
+
if (global) {
|
|
1655
|
+
logger.startSpinner(`Installing '${packageName}' globally...`, { color: 'blue' });
|
|
1656
|
+
await installNpmPackage({ name: packageName, flags: ['-g'] });
|
|
1657
|
+
logger.stopSpinner(`Installed '${packageName}' globally`);
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
// Project level installation
|
|
1661
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
1662
|
+
let shouldInstall = false;
|
|
1663
|
+
if (isPackageInstalled(packageName, projectRoot)) {
|
|
1664
|
+
shouldInstall = await confirm({
|
|
1665
|
+
message: `'${packageName}' is already installed in '${projectRoot}/package.json', would you like to update?`,
|
|
1666
|
+
default: shouldInstall,
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
else {
|
|
1670
|
+
shouldInstall = true;
|
|
1671
|
+
}
|
|
1672
|
+
if (!shouldInstall) {
|
|
1673
|
+
return;
|
|
1515
1674
|
}
|
|
1675
|
+
logger.startSpinner(`Installing '${packageName}' in project...`, { color: 'blue' });
|
|
1676
|
+
await installNpmPackage({ name: packageName, cwd: projectRoot });
|
|
1677
|
+
logger.stopSpinner(`Installed '${packageName}' in project`);
|
|
1516
1678
|
}
|
|
1517
|
-
|
|
1518
|
-
};
|
|
1519
|
-
|
|
1679
|
+
catch (error) {
|
|
1680
|
+
console.error(`Error: ${error.message}`);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function getPathToUsersGitConfig() {
|
|
1520
1685
|
return path__default.join(os__default.homedir(), '.gitconfig');
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
const
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
async function createProjectFileAndReturnPath(fileName, contents) {
|
|
1689
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
1690
|
+
const configFile = `${projectRoot}/${fileName}`;
|
|
1691
|
+
if (!fs__default.existsSync(configFile)) {
|
|
1692
|
+
fs__default.writeFileSync(configFile, contents || '');
|
|
1693
|
+
}
|
|
1694
|
+
return configFile;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
const handler = async (argv, logger) => {
|
|
1698
|
+
logger.log(LOGO);
|
|
1525
1699
|
const level = await select({
|
|
1526
1700
|
message: 'configure coco at the system or project level:',
|
|
1527
1701
|
choices: [
|
|
@@ -1539,11 +1713,25 @@ async function handler(argv) {
|
|
|
1539
1713
|
});
|
|
1540
1714
|
let configFilePath = '';
|
|
1541
1715
|
switch (level) {
|
|
1542
|
-
case 'system':
|
|
1543
|
-
configFilePath = await handleSystemLevelConfig();
|
|
1544
|
-
break;
|
|
1545
1716
|
case 'project':
|
|
1546
|
-
|
|
1717
|
+
const projectConfiguration = await select({
|
|
1718
|
+
message: 'select type project level configuration:',
|
|
1719
|
+
choices: [
|
|
1720
|
+
{
|
|
1721
|
+
name: '.coco.config.json',
|
|
1722
|
+
value: '.coco.config.json',
|
|
1723
|
+
},
|
|
1724
|
+
{
|
|
1725
|
+
name: '.env',
|
|
1726
|
+
value: '.env',
|
|
1727
|
+
},
|
|
1728
|
+
],
|
|
1729
|
+
});
|
|
1730
|
+
configFilePath = await createProjectFileAndReturnPath(projectConfiguration);
|
|
1731
|
+
break;
|
|
1732
|
+
case 'system':
|
|
1733
|
+
default:
|
|
1734
|
+
configFilePath = getPathToUsersGitConfig();
|
|
1547
1735
|
break;
|
|
1548
1736
|
}
|
|
1549
1737
|
// interactive v.s stdout mode
|
|
@@ -1640,7 +1828,7 @@ async function handler(argv) {
|
|
|
1640
1828
|
// add to config after logging, so that the API key is not logged
|
|
1641
1829
|
config.openAIApiKey = apiKey;
|
|
1642
1830
|
const isApproved = await confirm({
|
|
1643
|
-
message: '
|
|
1831
|
+
message: 'looking good? (API key hidden for security)',
|
|
1644
1832
|
});
|
|
1645
1833
|
if (isApproved) {
|
|
1646
1834
|
if (configFilePath.endsWith('.gitconfig')) {
|
|
@@ -1652,62 +1840,19 @@ async function handler(argv) {
|
|
|
1652
1840
|
else if (configFilePath === '.coco.config.json') {
|
|
1653
1841
|
await appendToProjectConfig(configFilePath, config);
|
|
1654
1842
|
}
|
|
1655
|
-
|
|
1843
|
+
// After config is written, check for package installation
|
|
1844
|
+
await checkAndHandlePackageInstallation({ global: level === 'system', logger });
|
|
1845
|
+
logger.log(`\ninit successful! π¦Ύπ€π`, { color: 'green' });
|
|
1656
1846
|
}
|
|
1657
1847
|
else {
|
|
1658
|
-
logger.log('
|
|
1848
|
+
logger.log('\ninit cancelled.', { color: 'yellow' });
|
|
1659
1849
|
}
|
|
1660
|
-
}
|
|
1850
|
+
};
|
|
1661
1851
|
|
|
1662
1852
|
/**
|
|
1663
1853
|
* Command line options via yargs
|
|
1664
1854
|
*/
|
|
1665
|
-
const options = {
|
|
1666
|
-
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
1667
|
-
openAIApiKey: {
|
|
1668
|
-
type: 'string',
|
|
1669
|
-
description: 'OpenAI API Key',
|
|
1670
|
-
conflicts: 'huggingFaceHubApiKey',
|
|
1671
|
-
},
|
|
1672
|
-
huggingFaceHubApiKey: {
|
|
1673
|
-
type: 'string',
|
|
1674
|
-
description: 'HuggingFace Hub API Key',
|
|
1675
|
-
conflicts: 'openAIApiKey',
|
|
1676
|
-
},
|
|
1677
|
-
tokenLimit: { type: 'number', description: 'Token limit' },
|
|
1678
|
-
prompt: {
|
|
1679
|
-
type: 'string',
|
|
1680
|
-
alias: 'p',
|
|
1681
|
-
description: 'Commit message prompt',
|
|
1682
|
-
},
|
|
1683
|
-
i: {
|
|
1684
|
-
type: 'boolean',
|
|
1685
|
-
alias: 'interactive',
|
|
1686
|
-
description: 'Toggle interactive mode',
|
|
1687
|
-
},
|
|
1688
|
-
s: {
|
|
1689
|
-
type: 'boolean',
|
|
1690
|
-
description: 'Automatically commit staged changes with generated commit message',
|
|
1691
|
-
default: false,
|
|
1692
|
-
},
|
|
1693
|
-
e: {
|
|
1694
|
-
type: 'boolean',
|
|
1695
|
-
alias: 'edit',
|
|
1696
|
-
description: 'Open commit message in editor before proceeding',
|
|
1697
|
-
},
|
|
1698
|
-
summarizePrompt: {
|
|
1699
|
-
type: 'string',
|
|
1700
|
-
description: 'Large file summary prompt',
|
|
1701
|
-
},
|
|
1702
|
-
ignoredFiles: {
|
|
1703
|
-
type: 'array',
|
|
1704
|
-
description: 'Ignored files',
|
|
1705
|
-
},
|
|
1706
|
-
ignoredExtensions: {
|
|
1707
|
-
type: 'array',
|
|
1708
|
-
description: 'Ignored extensions',
|
|
1709
|
-
},
|
|
1710
|
-
};
|
|
1855
|
+
const options = {};
|
|
1711
1856
|
const builder = (yargs) => {
|
|
1712
1857
|
return yargs.options(options);
|
|
1713
1858
|
};
|
|
@@ -1716,7 +1861,7 @@ var init = {
|
|
|
1716
1861
|
command: 'init',
|
|
1717
1862
|
desc: 'Setup coco for a new project or system',
|
|
1718
1863
|
builder,
|
|
1719
|
-
handler,
|
|
1864
|
+
handler: commandExecutor(handler),
|
|
1720
1865
|
options,
|
|
1721
1866
|
};
|
|
1722
1867
|
|
|
@@ -1724,25 +1869,23 @@ var types = /*#__PURE__*/Object.freeze({
|
|
|
1724
1869
|
__proto__: null
|
|
1725
1870
|
});
|
|
1726
1871
|
|
|
1727
|
-
yargs
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
.command([commit.command, '$0'], commit.desc,
|
|
1872
|
+
const y = yargs();
|
|
1873
|
+
y.scriptName('coco').usage('$0 <cmd> [args]');
|
|
1874
|
+
y.command([commit.command, '$0'], commit.desc,
|
|
1731
1875
|
// TODO: fix type on builder
|
|
1732
1876
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1733
1877
|
// @ts-ignore
|
|
1734
|
-
commit.builder, commit.handler)
|
|
1735
|
-
|
|
1878
|
+
commit.builder, commit.handler);
|
|
1879
|
+
y.command(changelog.command, changelog.desc,
|
|
1736
1880
|
// TODO: fix type on builder
|
|
1737
1881
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1738
1882
|
// @ts-ignore
|
|
1739
|
-
changelog.builder, changelog.handler)
|
|
1740
|
-
|
|
1883
|
+
changelog.builder, changelog.handler);
|
|
1884
|
+
y.command(init.command, init.desc,
|
|
1741
1885
|
// TODO: fix type on builder
|
|
1742
1886
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1743
1887
|
// @ts-ignore
|
|
1744
|
-
init.builder, init.handler)
|
|
1745
|
-
|
|
1746
|
-
.help().argv;
|
|
1888
|
+
init.builder, init.handler);
|
|
1889
|
+
y.parse(process.argv.slice(2));
|
|
1747
1890
|
|
|
1748
1891
|
export { changelog, commit, init, types };
|