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