git-coco 0.6.3 β 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +116 -74
- package/dist/index.esm.mjs +758 -646
- package/dist/index.js +756 -644
- package/package.json +6 -7
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 chalk = require('chalk');
|
|
14
|
-
var ora = require('ora');
|
|
15
|
-
var now = require('performance-now');
|
|
16
|
-
var prettyMilliseconds = require('pretty-ms');
|
|
17
|
-
var path = require('path');
|
|
18
22
|
var minimatch = require('minimatch');
|
|
19
|
-
var fs = require('fs');
|
|
20
|
-
var prompts$1 = require('@inquirer/prompts');
|
|
21
|
-
var os = require('os');
|
|
22
|
-
var ini = require('ini');
|
|
23
23
|
var simpleGit = require('simple-git');
|
|
24
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,147 +41,568 @@ 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;
|
|
182
|
+
function parseEnvValue(key, value) {
|
|
183
|
+
if (value === undefined) {
|
|
184
|
+
return undefined;
|
|
176
185
|
}
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
else if (key === 'tokenLimit' && typeof value === 'string') {
|
|
187
|
+
return parseInt(value);
|
|
179
188
|
}
|
|
180
|
-
|
|
181
|
-
|
|
189
|
+
else if ((key === 'ignoredFiles' || key === 'ignoredExtensions') &&
|
|
190
|
+
typeof value === 'string' &&
|
|
191
|
+
value.includes(',')) {
|
|
192
|
+
return value.split(',');
|
|
182
193
|
}
|
|
183
|
-
|
|
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();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function formatEnvValue(value) {
|
|
207
|
+
if (typeof value === 'number') {
|
|
208
|
+
return `${value}`;
|
|
209
|
+
}
|
|
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
|
+
});
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
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,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return config;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
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.
|
|
269
|
+
*/
|
|
270
|
+
const appendToGitConfig = async (filePath, config) => {
|
|
271
|
+
if (!fs__namespace.existsSync(filePath)) {
|
|
272
|
+
throw new Error(`File ${filePath} does not exist.`);
|
|
273
|
+
}
|
|
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
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Load .gitignore in project root
|
|
305
|
+
*
|
|
306
|
+
* @param {Config} config
|
|
307
|
+
* @returns
|
|
308
|
+
*/
|
|
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
|
+
];
|
|
316
|
+
}
|
|
317
|
+
return config;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Load .ignore in project root
|
|
321
|
+
*
|
|
322
|
+
* @param {Config} config
|
|
323
|
+
* @returns
|
|
324
|
+
*/
|
|
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;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
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;
|
|
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
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Load XDG config
|
|
360
|
+
*
|
|
361
|
+
* @param {Config} config
|
|
362
|
+
* @returns {Config} Updated config
|
|
363
|
+
*/
|
|
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 };
|
|
370
|
+
}
|
|
371
|
+
return config;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Load application config
|
|
376
|
+
*
|
|
377
|
+
* Merge config from multiple sources.
|
|
378
|
+
*
|
|
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
|
+
}
|
|
402
|
+
|
|
403
|
+
class Logger {
|
|
404
|
+
constructor(config) {
|
|
405
|
+
this.config = config;
|
|
406
|
+
this.spinner = null;
|
|
407
|
+
}
|
|
408
|
+
log(message, options = { color: 'blue' }) {
|
|
409
|
+
let outputMessage = message;
|
|
410
|
+
if (options.color) {
|
|
411
|
+
outputMessage = chalk[options.color](outputMessage);
|
|
412
|
+
}
|
|
413
|
+
console.log(outputMessage);
|
|
414
|
+
return this;
|
|
415
|
+
}
|
|
416
|
+
verbose(message, options = {}) {
|
|
417
|
+
if (!this.config?.verbose) {
|
|
418
|
+
return this;
|
|
419
|
+
}
|
|
420
|
+
this.log(message, options);
|
|
421
|
+
return this;
|
|
422
|
+
}
|
|
423
|
+
startTimer() {
|
|
424
|
+
this.timerStart = now();
|
|
425
|
+
return this;
|
|
426
|
+
}
|
|
427
|
+
stopTimer(message, options = { color: 'yellow' }) {
|
|
428
|
+
if (!this.config?.verbose || !this.timerStart) {
|
|
429
|
+
return this;
|
|
430
|
+
}
|
|
431
|
+
const elapsedTime = prettyMilliseconds(now() - this.timerStart);
|
|
432
|
+
let outputMessage = message
|
|
433
|
+
? `${message} (β² ${elapsedTime})`
|
|
434
|
+
: `β² ${elapsedTime}`;
|
|
435
|
+
if (options.color) {
|
|
436
|
+
outputMessage = chalk[options.color](outputMessage);
|
|
437
|
+
}
|
|
438
|
+
console.log(outputMessage);
|
|
439
|
+
return this;
|
|
440
|
+
}
|
|
441
|
+
startSpinner(message, options = { color: 'green' }) {
|
|
442
|
+
const spinnerMessage = options.color ? chalk[options.color](message) : message;
|
|
443
|
+
this.spinner = ora(spinnerMessage).start();
|
|
444
|
+
return this;
|
|
445
|
+
}
|
|
446
|
+
stopSpinner(message = '', options = { mode: 'succeed', color: 'green' }) {
|
|
447
|
+
const spinnerMessage = options?.color ? chalk[options.color](message) : message;
|
|
448
|
+
this.spinner?.[options.mode || 'succeed'](spinnerMessage);
|
|
449
|
+
this.spinner = null;
|
|
450
|
+
return this;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
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);
|
|
460
|
+
}
|
|
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
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
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.
|
|
474
|
+
*/
|
|
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();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Create groups from a given node info.
|
|
493
|
+
* @param {DiffNode} node - The node info to start grouping.
|
|
494
|
+
* @returns {DirectoryDiff[]} The groups created.
|
|
495
|
+
*/
|
|
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 };
|
|
503
|
+
}
|
|
504
|
+
groupByPath[path].diffs.push(diff);
|
|
505
|
+
groupByPath[path].tokenCount += diff.tokenCount;
|
|
506
|
+
});
|
|
507
|
+
node.children.forEach(traverse);
|
|
508
|
+
}
|
|
509
|
+
traverse(node);
|
|
510
|
+
return Object.values(groupByPath);
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
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
|
+
};
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
console.error(error);
|
|
540
|
+
return directory;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
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`;
|
|
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('');
|
|
589
|
+
}
|
|
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;
|
|
598
|
+
}
|
|
599
|
+
addFile(file) {
|
|
600
|
+
this.files.push(file);
|
|
601
|
+
}
|
|
602
|
+
addChild(part, node) {
|
|
603
|
+
this.children.set(part, node);
|
|
604
|
+
}
|
|
605
|
+
getChild(part) {
|
|
184
606
|
return this.children.get(part);
|
|
185
607
|
}
|
|
186
608
|
getPath() {
|
|
@@ -359,18 +781,6 @@ function validatePromptTemplate(text, inputVariables) {
|
|
|
359
781
|
return true;
|
|
360
782
|
}
|
|
361
783
|
|
|
362
|
-
const template$2 = `GOAL: Use functional abstractions to summarize the following text
|
|
363
|
-
|
|
364
|
-
RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
|
|
365
|
-
|
|
366
|
-
TEXT:"""{text}"""
|
|
367
|
-
`;
|
|
368
|
-
const inputVariables$2 = ['text'];
|
|
369
|
-
const SUMMARIZE_PROMPT = new prompts.PromptTemplate({
|
|
370
|
-
template: template$2,
|
|
371
|
-
inputVariables: inputVariables$2,
|
|
372
|
-
});
|
|
373
|
-
|
|
374
784
|
async function parseDefaultFileDiff(nodeFile, commit = '--staged', git) {
|
|
375
785
|
if (commit !== '--staged') {
|
|
376
786
|
return await git.diff([`${commit}~1..${commit}`, '--', nodeFile.filePath]);
|
|
@@ -452,59 +862,8 @@ async function fileChangeParser({ changes, commit, options: { tokenizer, git, ll
|
|
|
452
862
|
chain: summarizationChain,
|
|
453
863
|
logger,
|
|
454
864
|
});
|
|
455
|
-
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
456
|
-
return summary;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
class Logger {
|
|
460
|
-
constructor(config) {
|
|
461
|
-
this.config = config;
|
|
462
|
-
this.spinner = null;
|
|
463
|
-
}
|
|
464
|
-
log(message, options = { color: 'blue' }) {
|
|
465
|
-
let outputMessage = message;
|
|
466
|
-
if (options.color) {
|
|
467
|
-
outputMessage = chalk[options.color](outputMessage);
|
|
468
|
-
}
|
|
469
|
-
console.log(outputMessage);
|
|
470
|
-
return this;
|
|
471
|
-
}
|
|
472
|
-
verbose(message, options = {}) {
|
|
473
|
-
if (!this.config?.verbose) {
|
|
474
|
-
return this;
|
|
475
|
-
}
|
|
476
|
-
this.log(message, options);
|
|
477
|
-
return this;
|
|
478
|
-
}
|
|
479
|
-
startTimer() {
|
|
480
|
-
this.timerStart = now();
|
|
481
|
-
return this;
|
|
482
|
-
}
|
|
483
|
-
stopTimer(message, options = { color: 'yellow' }) {
|
|
484
|
-
if (!this.config?.verbose || !this.timerStart) {
|
|
485
|
-
return this;
|
|
486
|
-
}
|
|
487
|
-
const elapsedTime = prettyMilliseconds(now() - this.timerStart);
|
|
488
|
-
let outputMessage = message
|
|
489
|
-
? `${message} (β² ${elapsedTime})`
|
|
490
|
-
: `β² ${elapsedTime}`;
|
|
491
|
-
if (options.color) {
|
|
492
|
-
outputMessage = chalk[options.color](outputMessage);
|
|
493
|
-
}
|
|
494
|
-
console.log(outputMessage);
|
|
495
|
-
return this;
|
|
496
|
-
}
|
|
497
|
-
startSpinner(message, options = { color: 'green' }) {
|
|
498
|
-
const spinnerMessage = options.color ? chalk[options.color](message) : message;
|
|
499
|
-
this.spinner = ora(spinnerMessage).start();
|
|
500
|
-
return this;
|
|
501
|
-
}
|
|
502
|
-
stopSpinner(message = '', options = { mode: 'succeed', color: 'green' }) {
|
|
503
|
-
const spinnerMessage = options?.color ? chalk[options.color](message) : message;
|
|
504
|
-
this.spinner?.[options.mode || 'succeed'](spinnerMessage);
|
|
505
|
-
this.spinner = null;
|
|
506
|
-
return this;
|
|
507
|
-
}
|
|
865
|
+
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
866
|
+
return summary;
|
|
508
867
|
}
|
|
509
868
|
|
|
510
869
|
const template$1 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
|
|
@@ -532,368 +891,52 @@ function getStatus(file, location = 'index') {
|
|
|
532
891
|
switch (statusCode) {
|
|
533
892
|
case 'A':
|
|
534
893
|
return 'added';
|
|
535
|
-
case 'D':
|
|
536
|
-
return 'deleted';
|
|
537
|
-
case 'M':
|
|
538
|
-
return 'modified';
|
|
539
|
-
case 'R':
|
|
540
|
-
return 'renamed';
|
|
541
|
-
case '?':
|
|
542
|
-
return 'untracked';
|
|
543
|
-
default:
|
|
544
|
-
return 'unknown';
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
else if ('changes' in file && 'binary' in file) {
|
|
548
|
-
if (file.changes === 0)
|
|
549
|
-
return 'untracked';
|
|
550
|
-
if (file.file.includes('=>'))
|
|
551
|
-
return 'renamed';
|
|
552
|
-
if (file.deletions === 0 && file.insertions > 0)
|
|
553
|
-
return 'added';
|
|
554
|
-
if (file.insertions === 0 && file.deletions > 0)
|
|
555
|
-
return 'deleted';
|
|
556
|
-
if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
|
|
557
|
-
return 'modified';
|
|
558
|
-
return 'unknown';
|
|
559
|
-
}
|
|
560
|
-
else {
|
|
561
|
-
throw new Error('Invalid file type');
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function getSummaryText(file, change) {
|
|
566
|
-
const status = change.status || getStatus(file);
|
|
567
|
-
let filePath;
|
|
568
|
-
if ('path' in file) {
|
|
569
|
-
filePath = file.path;
|
|
570
|
-
}
|
|
571
|
-
else if ('file' in file) {
|
|
572
|
-
filePath = change?.filePath || file.file;
|
|
573
|
-
}
|
|
574
|
-
else {
|
|
575
|
-
throw new Error('Invalid file type');
|
|
576
|
-
}
|
|
577
|
-
if (change.oldFilePath) {
|
|
578
|
-
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
579
|
-
}
|
|
580
|
-
return `${status}: ${filePath}`;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* Returns a new object with all undefined keys removed
|
|
585
|
-
*
|
|
586
|
-
* @param obj Object to remove undefined keys from
|
|
587
|
-
* @returns
|
|
588
|
-
*/
|
|
589
|
-
function removeUndefined(obj) {
|
|
590
|
-
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* Default Config
|
|
595
|
-
*
|
|
596
|
-
* @type {Config}
|
|
597
|
-
*/
|
|
598
|
-
const DEFAULT_CONFIG = {
|
|
599
|
-
service: 'openai/gpt-4',
|
|
600
|
-
verbose: false,
|
|
601
|
-
tokenLimit: 1024,
|
|
602
|
-
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
603
|
-
temperature: 0.4,
|
|
604
|
-
mode: 'stdout',
|
|
605
|
-
ignoredFiles: ['package-lock.json'],
|
|
606
|
-
ignoredExtensions: ['.map', '.lock'],
|
|
607
|
-
defaultBranch: 'main',
|
|
608
|
-
};
|
|
609
|
-
/**
|
|
610
|
-
* Create a named export of all config keys for use in other modules.
|
|
611
|
-
*
|
|
612
|
-
* @see Currently used in `src/lib/config/services/env.ts` to validate all env vars.
|
|
613
|
-
*
|
|
614
|
-
* @type {string[]}
|
|
615
|
-
*/
|
|
616
|
-
const CONFIG_KEYS = Object.keys({
|
|
617
|
-
...DEFAULT_CONFIG,
|
|
618
|
-
huggingFaceHubApiKey: '',
|
|
619
|
-
openAIApiKey: '',
|
|
620
|
-
prompt: '',
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
async function updateFileSection(filePath, startComment, endComment, getNewContent, confirmUpdate = true) {
|
|
624
|
-
const lines = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8').split(/\r?\n/) : [];
|
|
625
|
-
const newLines = [];
|
|
626
|
-
let foundSection = false;
|
|
627
|
-
for (let i = 0; i < lines.length; i++) {
|
|
628
|
-
if (lines[i].trim() === startComment) {
|
|
629
|
-
foundSection = true;
|
|
630
|
-
if (confirmUpdate) {
|
|
631
|
-
const confirmOverwrite = await prompts$1.confirm({
|
|
632
|
-
message: `A section already exists in ${filePath}, do you want to override it?`,
|
|
633
|
-
default: false,
|
|
634
|
-
});
|
|
635
|
-
if (!confirmOverwrite) {
|
|
636
|
-
// keep all lines until the end comment
|
|
637
|
-
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
638
|
-
newLines.push(lines[i]);
|
|
639
|
-
i++;
|
|
640
|
-
}
|
|
641
|
-
newLines.push(endComment);
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
newLines.push(startComment);
|
|
646
|
-
// Insert the new content
|
|
647
|
-
const newContent = await getNewContent();
|
|
648
|
-
newLines.push(newContent);
|
|
649
|
-
// Skip the existing content of the section
|
|
650
|
-
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
651
|
-
i++;
|
|
652
|
-
}
|
|
653
|
-
newLines.push(endComment);
|
|
654
|
-
continue;
|
|
655
|
-
}
|
|
656
|
-
if (!foundSection || lines[i].trim() !== endComment) {
|
|
657
|
-
newLines.push(lines[i]);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
// If section wasn't found, append it at the end
|
|
661
|
-
if (!foundSection) {
|
|
662
|
-
newLines.push('\n' + startComment);
|
|
663
|
-
const newContent = await getNewContent();
|
|
664
|
-
newLines.push(newContent);
|
|
665
|
-
newLines.push(endComment);
|
|
666
|
-
}
|
|
667
|
-
// Write the updated contents back to the file
|
|
668
|
-
fs.writeFileSync(filePath, newLines.join('\n'));
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
/**
|
|
672
|
-
* Load environment variables
|
|
673
|
-
*
|
|
674
|
-
* @param {Config} config
|
|
675
|
-
* @returns {Config} Updated config
|
|
676
|
-
**/
|
|
677
|
-
function loadEnvConfig(config) {
|
|
678
|
-
const envConfig = {};
|
|
679
|
-
CONFIG_KEYS.forEach((key) => {
|
|
680
|
-
const envVarName = toEnvVarName(key);
|
|
681
|
-
const envValue = parseEnvValue(key, process.env[envVarName]);
|
|
682
|
-
if (envValue === undefined)
|
|
683
|
-
return;
|
|
684
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
685
|
-
// @ts-ignore
|
|
686
|
-
envConfig[key] = envValue;
|
|
687
|
-
});
|
|
688
|
-
return { ...config, ...removeUndefined(envConfig) };
|
|
689
|
-
}
|
|
690
|
-
function parseEnvValue(key, value) {
|
|
691
|
-
if (value === undefined) {
|
|
692
|
-
return undefined;
|
|
693
|
-
}
|
|
694
|
-
else if (key === 'tokenLimit' && typeof value === 'string') {
|
|
695
|
-
return parseInt(value);
|
|
696
|
-
}
|
|
697
|
-
else if ((key === 'ignoredFiles' || key === 'ignoredExtensions') &&
|
|
698
|
-
typeof value === 'string' &&
|
|
699
|
-
value.includes(',')) {
|
|
700
|
-
return value.split(',');
|
|
701
|
-
}
|
|
702
|
-
return value;
|
|
703
|
-
}
|
|
704
|
-
function toEnvVarName(key) {
|
|
705
|
-
switch (key) {
|
|
706
|
-
case 'openAIApiKey':
|
|
707
|
-
return 'OPENAI_API_KEY';
|
|
708
|
-
case 'huggingFaceHubApiKey':
|
|
709
|
-
return 'HUGGINGFACE_HUB_API_KEY';
|
|
710
|
-
default:
|
|
711
|
-
return 'COCO_' + key.replace(/([A-Z])/g, '_$1').toUpperCase();
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
function formatEnvValue(value) {
|
|
715
|
-
if (typeof value === 'number') {
|
|
716
|
-
return `${value}`;
|
|
717
|
-
}
|
|
718
|
-
else if (Array.isArray(value)) {
|
|
719
|
-
return `${value.join(',')}`;
|
|
720
|
-
}
|
|
721
|
-
else if (typeof value === 'string') {
|
|
722
|
-
// Escape newlines and tabs in strings
|
|
723
|
-
return `${value.replace(/\n/g, '\\n').replace(/\t/g, '\\t')}`;
|
|
724
|
-
}
|
|
725
|
-
return `${value}`;
|
|
726
|
-
}
|
|
727
|
-
const appendToEnvFile = async (filePath, config) => {
|
|
728
|
-
const startComment = '# -- Start coco config --';
|
|
729
|
-
const endComment = '# -- End coco config --';
|
|
730
|
-
const getNewContent = async () => {
|
|
731
|
-
return Object.entries(config)
|
|
732
|
-
.map(([key, value]) => `${toEnvVarName(key)}=${formatEnvValue(value)}`)
|
|
733
|
-
.join('\n');
|
|
734
|
-
};
|
|
735
|
-
await updateFileSection(filePath, startComment, endComment, getNewContent);
|
|
736
|
-
};
|
|
737
|
-
|
|
738
|
-
/**
|
|
739
|
-
* Load git profile config (from ~/.gitconfig)
|
|
740
|
-
*
|
|
741
|
-
* @param {Config} config
|
|
742
|
-
* @returns {Config} Updated config
|
|
743
|
-
**/
|
|
744
|
-
function loadGitConfig(config) {
|
|
745
|
-
const gitConfigPath = path__namespace.join(os__namespace.homedir(), '.gitconfig');
|
|
746
|
-
if (fs__namespace.existsSync(gitConfigPath)) {
|
|
747
|
-
const gitConfigRaw = fs__namespace.readFileSync(gitConfigPath, 'utf-8');
|
|
748
|
-
const gitConfigParsed = ini__namespace.parse(gitConfigRaw);
|
|
749
|
-
config = {
|
|
750
|
-
...config,
|
|
751
|
-
service: gitConfigParsed.coco?.model || config.service,
|
|
752
|
-
openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
|
|
753
|
-
huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
|
|
754
|
-
tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
|
|
755
|
-
prompt: gitConfigParsed.coco?.prompt || config.prompt,
|
|
756
|
-
mode: gitConfigParsed.coco?.mode || config.mode,
|
|
757
|
-
temperature: gitConfigParsed.coco?.temperature || config.temperature,
|
|
758
|
-
summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
|
|
759
|
-
ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
|
|
760
|
-
ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
|
|
761
|
-
defaultBranch: gitConfigParsed.coco?.defaultBranch || config.defaultBranch,
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
return config;
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* Appends the provided configuration to a git config file.
|
|
768
|
-
*
|
|
769
|
-
* @param filePath - The path to the .gitconfig
|
|
770
|
-
* @param config - The configuration object to append.
|
|
771
|
-
*/
|
|
772
|
-
const appendToGitConfig = async (filePath, config) => {
|
|
773
|
-
if (!fs__namespace.existsSync(filePath)) {
|
|
774
|
-
throw new Error(`File ${filePath} does not exist.`);
|
|
775
|
-
}
|
|
776
|
-
const startComment = '# -- Start coco config --';
|
|
777
|
-
const endComment = '# -- End coco config --';
|
|
778
|
-
const header = '[coco]';
|
|
779
|
-
// Function to generate new content for the coco section
|
|
780
|
-
const getNewContent = async () => {
|
|
781
|
-
const contentLines = [header];
|
|
782
|
-
for (const key in config) {
|
|
783
|
-
// check if string has new lines, if so, wrap in quotes
|
|
784
|
-
if (typeof config[key] === 'string') {
|
|
785
|
-
const value = config[key];
|
|
786
|
-
if (value.includes('\n')) {
|
|
787
|
-
contentLines.push(`\t${key} = ${JSON.stringify(value)}`);
|
|
788
|
-
continue;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
contentLines.push(`\t${key} = ${config[key]}`);
|
|
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';
|
|
792
904
|
}
|
|
793
|
-
return contentLines.join('\n');
|
|
794
|
-
};
|
|
795
|
-
// Use the updateFileSection utility
|
|
796
|
-
await updateFileSection(filePath, startComment, endComment, getNewContent);
|
|
797
|
-
};
|
|
798
|
-
|
|
799
|
-
/**
|
|
800
|
-
* Load .gitignore in project root
|
|
801
|
-
*
|
|
802
|
-
* @param {Config} config
|
|
803
|
-
* @returns
|
|
804
|
-
*/
|
|
805
|
-
function loadGitignore(config) {
|
|
806
|
-
if (fs__namespace.existsSync('.gitignore')) {
|
|
807
|
-
const gitignoreContent = fs__namespace.readFileSync('.gitignore', 'utf-8');
|
|
808
|
-
config.ignoredFiles = [
|
|
809
|
-
...(config?.ignoredFiles || []),
|
|
810
|
-
...gitignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
811
|
-
];
|
|
812
905
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
...(config?.ignoredFiles || []),
|
|
826
|
-
...ignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
827
|
-
];
|
|
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';
|
|
828
918
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
/**
|
|
833
|
-
* Load project config
|
|
834
|
-
*
|
|
835
|
-
* @param {Config} config
|
|
836
|
-
* @returns {Config} Updated config
|
|
837
|
-
**/
|
|
838
|
-
function loadProjectConfig(config) {
|
|
839
|
-
// TODO: Add validation based of JSON schema?
|
|
840
|
-
// @see https://github.com/acornejo/jjv
|
|
841
|
-
if (fs__namespace.existsSync('.coco.config.json')) {
|
|
842
|
-
const projectConfig = JSON.parse(fs__namespace.readFileSync('.coco.config.json', 'utf-8'));
|
|
843
|
-
config = { ...config, ...projectConfig };
|
|
919
|
+
else {
|
|
920
|
+
throw new Error('Invalid file type');
|
|
844
921
|
}
|
|
845
|
-
return config;
|
|
846
922
|
}
|
|
847
|
-
const appendToProjectConfig = (filePath, config) => {
|
|
848
|
-
fs__namespace.writeFileSync(filePath, JSON.stringify({
|
|
849
|
-
$schema: 'https://git-co.co/schema.json',
|
|
850
|
-
...config,
|
|
851
|
-
}, null, 2));
|
|
852
|
-
};
|
|
853
923
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
*/
|
|
860
|
-
function loadXDGConfig(config) {
|
|
861
|
-
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path__namespace.join(os__namespace.homedir(), '.config');
|
|
862
|
-
const xdgConfigPath = path__namespace.join(xdgConfigHome, 'coco', 'config.json');
|
|
863
|
-
if (fs__namespace.existsSync(xdgConfigPath)) {
|
|
864
|
-
const xdgConfig = JSON.parse(fs__namespace.readFileSync(xdgConfigPath, 'utf-8'));
|
|
865
|
-
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;
|
|
866
929
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
* \* 2. Environment variables
|
|
878
|
-
* \* 3. Project config
|
|
879
|
-
* \* 4. Git config
|
|
880
|
-
* \* 5. XDG config
|
|
881
|
-
* \* 6. .gitignore
|
|
882
|
-
* \* 7. .ignore
|
|
883
|
-
* \* 8. Default config
|
|
884
|
-
*
|
|
885
|
-
* @returns {Config} application config
|
|
886
|
-
**/
|
|
887
|
-
function loadConfig(argv = {}) {
|
|
888
|
-
// Default config
|
|
889
|
-
let config = DEFAULT_CONFIG;
|
|
890
|
-
config = loadGitignore(config);
|
|
891
|
-
config = loadIgnore(config);
|
|
892
|
-
config = loadXDGConfig(config);
|
|
893
|
-
config = loadGitConfig(config);
|
|
894
|
-
config = loadProjectConfig(config);
|
|
895
|
-
config = loadEnvConfig(config);
|
|
896
|
-
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}`;
|
|
897
940
|
}
|
|
898
941
|
|
|
899
942
|
const config = loadConfig();
|
|
@@ -981,11 +1024,6 @@ async function noResult({ git, logger }) {
|
|
|
981
1024
|
}
|
|
982
1025
|
}
|
|
983
1026
|
|
|
984
|
-
const isInteractive = (argv) => {
|
|
985
|
-
return argv?.mode === 'interactive' || argv.interactive;
|
|
986
|
-
};
|
|
987
|
-
const SEPERATOR = chalk.blue('----------------');
|
|
988
|
-
|
|
989
1027
|
function logResult(label, result) {
|
|
990
1028
|
console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
|
|
991
1029
|
}
|
|
@@ -1167,7 +1205,7 @@ async function handleResult({ result, mode, interactiveHandler }) {
|
|
|
1167
1205
|
await interactiveHandler(result);
|
|
1168
1206
|
}
|
|
1169
1207
|
else {
|
|
1170
|
-
console.
|
|
1208
|
+
console.warn('No result handler provided for interactive mode.');
|
|
1171
1209
|
logSuccess();
|
|
1172
1210
|
}
|
|
1173
1211
|
break;
|
|
@@ -1205,11 +1243,10 @@ async function createCommit(commitMsg, git) {
|
|
|
1205
1243
|
return await git.commit(commitMsg);
|
|
1206
1244
|
}
|
|
1207
1245
|
|
|
1208
|
-
|
|
1246
|
+
const handler$2 = async (argv, logger) => {
|
|
1209
1247
|
const git = getRepo();
|
|
1210
1248
|
const options = loadConfig(argv);
|
|
1211
1249
|
const { service } = options;
|
|
1212
|
-
const logger = new Logger(options);
|
|
1213
1250
|
const key = getApiKeyForModel(service, options);
|
|
1214
1251
|
const tokenizer = await getTokenCounter(getModelFromService(service));
|
|
1215
1252
|
if (!key) {
|
|
@@ -1221,6 +1258,9 @@ async function handler$2(argv) {
|
|
|
1221
1258
|
maxConcurrency: 10,
|
|
1222
1259
|
});
|
|
1223
1260
|
const INTERACTIVE = isInteractive(options);
|
|
1261
|
+
if (INTERACTIVE) {
|
|
1262
|
+
logger.log(LOGO);
|
|
1263
|
+
}
|
|
1224
1264
|
async function factory() {
|
|
1225
1265
|
const changes = await getChanges({ git });
|
|
1226
1266
|
return changes.staged;
|
|
@@ -1276,7 +1316,7 @@ async function handler$2(argv) {
|
|
|
1276
1316
|
},
|
|
1277
1317
|
mode: MODE,
|
|
1278
1318
|
});
|
|
1279
|
-
}
|
|
1319
|
+
};
|
|
1280
1320
|
|
|
1281
1321
|
/**
|
|
1282
1322
|
* Command line options via yargs
|
|
@@ -1335,7 +1375,7 @@ var commit = {
|
|
|
1335
1375
|
command: 'commit',
|
|
1336
1376
|
desc: 'Generate commit message',
|
|
1337
1377
|
builder: builder$2,
|
|
1338
|
-
handler: handler$2,
|
|
1378
|
+
handler: commandExecutor(handler$2),
|
|
1339
1379
|
options: options$2,
|
|
1340
1380
|
};
|
|
1341
1381
|
|
|
@@ -1412,9 +1452,8 @@ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main
|
|
|
1412
1452
|
return [];
|
|
1413
1453
|
}
|
|
1414
1454
|
|
|
1415
|
-
|
|
1455
|
+
const handler$1 = async (argv, logger) => {
|
|
1416
1456
|
const options = loadConfig(argv);
|
|
1417
|
-
const logger = new Logger(options);
|
|
1418
1457
|
const git = getRepo();
|
|
1419
1458
|
const key = getApiKeyForModel(options.service, options);
|
|
1420
1459
|
if (!key) {
|
|
@@ -1426,6 +1465,9 @@ async function handler$1(argv) {
|
|
|
1426
1465
|
maxConcurrency: 10,
|
|
1427
1466
|
});
|
|
1428
1467
|
const INTERACTIVE = isInteractive(options);
|
|
1468
|
+
if (INTERACTIVE) {
|
|
1469
|
+
logger.log(LOGO);
|
|
1470
|
+
}
|
|
1429
1471
|
async function factory() {
|
|
1430
1472
|
if (options.range) {
|
|
1431
1473
|
const [from, to] = options.range?.split(':');
|
|
@@ -1473,7 +1515,7 @@ async function handler$1(argv) {
|
|
|
1473
1515
|
interactive: INTERACTIVE,
|
|
1474
1516
|
review: {
|
|
1475
1517
|
enableFullRetry: false,
|
|
1476
|
-
}
|
|
1518
|
+
},
|
|
1477
1519
|
},
|
|
1478
1520
|
});
|
|
1479
1521
|
const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
|
|
@@ -1484,7 +1526,7 @@ async function handler$1(argv) {
|
|
|
1484
1526
|
},
|
|
1485
1527
|
mode: MODE,
|
|
1486
1528
|
});
|
|
1487
|
-
}
|
|
1529
|
+
};
|
|
1488
1530
|
|
|
1489
1531
|
/**
|
|
1490
1532
|
* Command line options via yargs
|
|
@@ -1543,39 +1585,138 @@ var changelog = {
|
|
|
1543
1585
|
command: 'changelog',
|
|
1544
1586
|
desc: 'Generate a changelog from a commit range',
|
|
1545
1587
|
builder: builder$1,
|
|
1546
|
-
handler: handler$1,
|
|
1588
|
+
handler: commandExecutor(handler$1),
|
|
1547
1589
|
options: options$1,
|
|
1548
1590
|
};
|
|
1549
1591
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
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
|
+
});
|
|
1563
1625
|
});
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
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;
|
|
1569
1695
|
}
|
|
1696
|
+
logger.startSpinner(`Installing '${packageName}' in project...`, { color: 'blue' });
|
|
1697
|
+
await installNpmPackage({ name: packageName, cwd: projectRoot });
|
|
1698
|
+
logger.stopSpinner(`Installed '${packageName}' in project`);
|
|
1570
1699
|
}
|
|
1571
|
-
|
|
1572
|
-
};
|
|
1573
|
-
|
|
1700
|
+
catch (error) {
|
|
1701
|
+
console.error(`Error: ${error.message}`);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function getPathToUsersGitConfig() {
|
|
1574
1706
|
return path.join(os.homedir(), '.gitconfig');
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
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);
|
|
1579
1720
|
const level = await prompts$1.select({
|
|
1580
1721
|
message: 'configure coco at the system or project level:',
|
|
1581
1722
|
choices: [
|
|
@@ -1593,11 +1734,25 @@ async function handler(argv) {
|
|
|
1593
1734
|
});
|
|
1594
1735
|
let configFilePath = '';
|
|
1595
1736
|
switch (level) {
|
|
1596
|
-
case 'system':
|
|
1597
|
-
configFilePath = await handleSystemLevelConfig();
|
|
1598
|
-
break;
|
|
1599
1737
|
case 'project':
|
|
1600
|
-
|
|
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();
|
|
1601
1756
|
break;
|
|
1602
1757
|
}
|
|
1603
1758
|
// interactive v.s stdout mode
|
|
@@ -1694,7 +1849,7 @@ async function handler(argv) {
|
|
|
1694
1849
|
// add to config after logging, so that the API key is not logged
|
|
1695
1850
|
config.openAIApiKey = apiKey;
|
|
1696
1851
|
const isApproved = await prompts$1.confirm({
|
|
1697
|
-
message: '
|
|
1852
|
+
message: 'looking good? (API key hidden for security)',
|
|
1698
1853
|
});
|
|
1699
1854
|
if (isApproved) {
|
|
1700
1855
|
if (configFilePath.endsWith('.gitconfig')) {
|
|
@@ -1706,62 +1861,19 @@ async function handler(argv) {
|
|
|
1706
1861
|
else if (configFilePath === '.coco.config.json') {
|
|
1707
1862
|
await appendToProjectConfig(configFilePath, config);
|
|
1708
1863
|
}
|
|
1709
|
-
|
|
1864
|
+
// After config is written, check for package installation
|
|
1865
|
+
await checkAndHandlePackageInstallation({ global: level === 'system', logger });
|
|
1866
|
+
logger.log(`\ninit successful! π¦Ύπ€π`, { color: 'green' });
|
|
1710
1867
|
}
|
|
1711
1868
|
else {
|
|
1712
|
-
logger.log('
|
|
1869
|
+
logger.log('\ninit cancelled.', { color: 'yellow' });
|
|
1713
1870
|
}
|
|
1714
|
-
}
|
|
1871
|
+
};
|
|
1715
1872
|
|
|
1716
1873
|
/**
|
|
1717
1874
|
* Command line options via yargs
|
|
1718
1875
|
*/
|
|
1719
|
-
const options = {
|
|
1720
|
-
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
1721
|
-
openAIApiKey: {
|
|
1722
|
-
type: 'string',
|
|
1723
|
-
description: 'OpenAI API Key',
|
|
1724
|
-
conflicts: 'huggingFaceHubApiKey',
|
|
1725
|
-
},
|
|
1726
|
-
huggingFaceHubApiKey: {
|
|
1727
|
-
type: 'string',
|
|
1728
|
-
description: 'HuggingFace Hub API Key',
|
|
1729
|
-
conflicts: 'openAIApiKey',
|
|
1730
|
-
},
|
|
1731
|
-
tokenLimit: { type: 'number', description: 'Token limit' },
|
|
1732
|
-
prompt: {
|
|
1733
|
-
type: 'string',
|
|
1734
|
-
alias: 'p',
|
|
1735
|
-
description: 'Commit message prompt',
|
|
1736
|
-
},
|
|
1737
|
-
i: {
|
|
1738
|
-
type: 'boolean',
|
|
1739
|
-
alias: 'interactive',
|
|
1740
|
-
description: 'Toggle interactive mode',
|
|
1741
|
-
},
|
|
1742
|
-
s: {
|
|
1743
|
-
type: 'boolean',
|
|
1744
|
-
description: 'Automatically commit staged changes with generated commit message',
|
|
1745
|
-
default: false,
|
|
1746
|
-
},
|
|
1747
|
-
e: {
|
|
1748
|
-
type: 'boolean',
|
|
1749
|
-
alias: 'edit',
|
|
1750
|
-
description: 'Open commit message in editor before proceeding',
|
|
1751
|
-
},
|
|
1752
|
-
summarizePrompt: {
|
|
1753
|
-
type: 'string',
|
|
1754
|
-
description: 'Large file summary prompt',
|
|
1755
|
-
},
|
|
1756
|
-
ignoredFiles: {
|
|
1757
|
-
type: 'array',
|
|
1758
|
-
description: 'Ignored files',
|
|
1759
|
-
},
|
|
1760
|
-
ignoredExtensions: {
|
|
1761
|
-
type: 'array',
|
|
1762
|
-
description: 'Ignored extensions',
|
|
1763
|
-
},
|
|
1764
|
-
};
|
|
1876
|
+
const options = {};
|
|
1765
1877
|
const builder = (yargs) => {
|
|
1766
1878
|
return yargs.options(options);
|
|
1767
1879
|
};
|
|
@@ -1770,7 +1882,7 @@ var init = {
|
|
|
1770
1882
|
command: 'init',
|
|
1771
1883
|
desc: 'Setup coco for a new project or system',
|
|
1772
1884
|
builder,
|
|
1773
|
-
handler,
|
|
1885
|
+
handler: commandExecutor(handler),
|
|
1774
1886
|
options,
|
|
1775
1887
|
};
|
|
1776
1888
|
|