git-coco 0.6.3 β 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 +116 -74
- package/dist/index.esm.mjs +758 -646
- package/dist/index.js +756 -644
- package/package.json +1 -2
package/dist/index.esm.mjs
CHANGED
|
@@ -1,163 +1,585 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import yargs from 'yargs';
|
|
3
|
+
import { PromptTemplate } from 'langchain/prompts';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import fs__default from 'fs';
|
|
6
|
+
import { confirm, editor, select, password, input } from '@inquirer/prompts';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import os__default from 'os';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import path__default from 'path';
|
|
12
|
+
import * as ini from 'ini';
|
|
13
|
+
import ora from 'ora';
|
|
14
|
+
import now from 'performance-now';
|
|
15
|
+
import prettyMilliseconds from 'pretty-ms';
|
|
3
16
|
import pQueue from 'p-queue';
|
|
4
17
|
import { Document } from 'langchain/document';
|
|
5
18
|
import { HuggingFaceInference } from 'langchain/llms/hf';
|
|
6
|
-
import { PromptTemplate } from 'langchain/prompts';
|
|
7
19
|
import { loadSummarizationChain, LLMChain } from 'langchain/chains';
|
|
8
20
|
import { OpenAI } from 'langchain/llms/openai';
|
|
9
21
|
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
|
|
10
22
|
import { createTwoFilesPatch } from 'diff';
|
|
11
|
-
import chalk from 'chalk';
|
|
12
|
-
import ora from 'ora';
|
|
13
|
-
import now from 'performance-now';
|
|
14
|
-
import prettyMilliseconds from 'pretty-ms';
|
|
15
|
-
import * as path from 'path';
|
|
16
|
-
import path__default from 'path';
|
|
17
23
|
import { minimatch } from 'minimatch';
|
|
18
|
-
import * as fs from 'fs';
|
|
19
|
-
import fs__default from 'fs';
|
|
20
|
-
import { confirm, editor, select, password, input } from '@inquirer/prompts';
|
|
21
|
-
import * as os from 'os';
|
|
22
|
-
import os__default from 'os';
|
|
23
|
-
import * as ini from 'ini';
|
|
24
24
|
import { simpleGit } from 'simple-git';
|
|
25
25
|
import { encoding_for_model } from 'tiktoken';
|
|
26
|
+
import { exec } from 'child_process';
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* @
|
|
29
|
+
* Returns a new object with all undefined keys removed
|
|
30
|
+
*
|
|
31
|
+
* @param obj Object to remove undefined keys from
|
|
32
|
+
* @returns
|
|
31
33
|
*/
|
|
32
|
-
function
|
|
33
|
-
return
|
|
34
|
+
function removeUndefined(obj) {
|
|
35
|
+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
38
|
+
const template$2 = `GOAL: Use functional abstractions to summarize the following text
|
|
39
|
+
|
|
40
|
+
RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
|
|
41
|
+
|
|
42
|
+
TEXT:"""{text}"""
|
|
43
|
+
`;
|
|
44
|
+
const inputVariables$2 = ['text'];
|
|
45
|
+
const SUMMARIZE_PROMPT = new PromptTemplate({
|
|
46
|
+
template: template$2,
|
|
47
|
+
inputVariables: inputVariables$2,
|
|
48
|
+
});
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* @
|
|
51
|
+
* Default Config
|
|
52
|
+
*
|
|
53
|
+
* @type {Config}
|
|
52
54
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
node.children.forEach(traverse);
|
|
65
|
-
}
|
|
66
|
-
traverse(node);
|
|
67
|
-
return Object.values(groupByPath);
|
|
68
|
-
}
|
|
55
|
+
const DEFAULT_CONFIG = {
|
|
56
|
+
service: 'openai/gpt-4',
|
|
57
|
+
verbose: false,
|
|
58
|
+
tokenLimit: 1024,
|
|
59
|
+
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
60
|
+
temperature: 0.4,
|
|
61
|
+
mode: 'stdout',
|
|
62
|
+
ignoredFiles: ['package-lock.json'],
|
|
63
|
+
ignoredExtensions: ['.map', '.lock'],
|
|
64
|
+
defaultBranch: 'main',
|
|
65
|
+
};
|
|
69
66
|
/**
|
|
70
|
-
*
|
|
67
|
+
* Create a named export of all config keys for use in other modules.
|
|
68
|
+
*
|
|
69
|
+
* @see Currently used in `src/lib/config/services/env.ts` to validate all env vars.
|
|
70
|
+
*
|
|
71
|
+
* @type {string[]}
|
|
71
72
|
*/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
73
|
+
const CONFIG_KEYS = Object.keys({
|
|
74
|
+
...DEFAULT_CONFIG,
|
|
75
|
+
huggingFaceHubApiKey: '',
|
|
76
|
+
openAIApiKey: '',
|
|
77
|
+
prompt: '',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
async function updateFileSection({ filePath, startComment, endComment, getNewContent, confirmUpdate = true, confirmMessage = (path) => `A section already exists in ${path}, do you want to override it?`, }) {
|
|
81
|
+
const lines = fs__default.existsSync(filePath) ? fs__default.readFileSync(filePath, 'utf-8').split(/\r?\n/) : [];
|
|
82
|
+
const newLines = [];
|
|
83
|
+
let foundSection = false;
|
|
84
|
+
for (let i = 0; i < lines.length; i++) {
|
|
85
|
+
if (lines[i].trim() === startComment) {
|
|
86
|
+
foundSection = true;
|
|
87
|
+
if (confirmUpdate) {
|
|
88
|
+
const confirmOverwrite = await confirm({
|
|
89
|
+
message: typeof confirmMessage === 'function' ? confirmMessage(filePath) : confirmMessage,
|
|
90
|
+
default: false,
|
|
91
|
+
});
|
|
92
|
+
if (!confirmOverwrite) {
|
|
93
|
+
// keep all lines until the end comment
|
|
94
|
+
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
95
|
+
newLines.push(lines[i]);
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
newLines.push(endComment);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
newLines.push(startComment);
|
|
103
|
+
// Insert the new content
|
|
104
|
+
const newContent = await getNewContent();
|
|
105
|
+
newLines.push(newContent);
|
|
106
|
+
// Skip the existing content of the section
|
|
107
|
+
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
108
|
+
i++;
|
|
109
|
+
}
|
|
110
|
+
newLines.push(endComment);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!foundSection || lines[i].trim() !== endComment) {
|
|
114
|
+
newLines.push(lines[i]);
|
|
115
|
+
}
|
|
94
116
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
117
|
+
// If section wasn't found, append it at the end
|
|
118
|
+
if (!foundSection) {
|
|
119
|
+
newLines.push('\n' + startComment);
|
|
120
|
+
const newContent = await getNewContent();
|
|
121
|
+
newLines.push(newContent);
|
|
122
|
+
newLines.push(endComment);
|
|
98
123
|
}
|
|
124
|
+
// Write the updated contents back to the file
|
|
125
|
+
fs__default.writeFileSync(filePath, newLines.join('\n'));
|
|
99
126
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (group.summary) {
|
|
104
|
-
output += `${group.diffs.map((diff) => ` β’ ${diff.summary}`).join('\n')}\n\nSummary:\n\n${group.summary}\n\n`;
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
output += `${group.diffs.map((diff) => ` β’ ${diff.summary}\n\n${diff.diff}`).join('\n\n')}\n\n`;
|
|
108
|
-
}
|
|
109
|
-
return output;
|
|
127
|
+
|
|
128
|
+
const isInteractive = (argv) => {
|
|
129
|
+
return argv?.mode === 'interactive' || argv.interactive;
|
|
110
130
|
};
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
color: totalTokenCount > maxTokens ? 'yellow' : 'green',
|
|
139
|
-
});
|
|
140
|
-
return group;
|
|
141
|
-
}, { priority: group.tokenCount });
|
|
131
|
+
const SEPERATOR = chalk.blue('βββββββββββββ');
|
|
132
|
+
const LOGO = chalk.green(`ββββββββββββββ
|
|
133
|
+
ββββββββββββββ
|
|
134
|
+
ββ β ββ β ββ
|
|
135
|
+
ββββββββββββββ
|
|
136
|
+
ββββββββββββββ
|
|
137
|
+
`);
|
|
138
|
+
const CONFIG_ALREADY_EXISTS = (path) => {
|
|
139
|
+
return `coco config found in '${path}', do you want to override it?`;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Load environment variables
|
|
144
|
+
*
|
|
145
|
+
* @param {Config} config
|
|
146
|
+
* @returns {Config} Updated config
|
|
147
|
+
**/
|
|
148
|
+
function loadEnvConfig(config) {
|
|
149
|
+
const envConfig = {};
|
|
150
|
+
CONFIG_KEYS.forEach((key) => {
|
|
151
|
+
const envVarName = toEnvVarName(key);
|
|
152
|
+
const envValue = parseEnvValue(key, process.env[envVarName]);
|
|
153
|
+
if (envValue === undefined)
|
|
154
|
+
return;
|
|
155
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
156
|
+
// @ts-ignore
|
|
157
|
+
envConfig[key] = envValue;
|
|
142
158
|
});
|
|
143
|
-
|
|
144
|
-
logger.stopSpinner(`Summarized Diffs`);
|
|
145
|
-
return directoryDiffs.map(handleOutput).join('');
|
|
159
|
+
return { ...config, ...removeUndefined(envConfig) };
|
|
146
160
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
this.path = [];
|
|
151
|
-
this.files = [];
|
|
152
|
-
this.children = new Map();
|
|
153
|
-
if (path)
|
|
154
|
-
this.path = path;
|
|
155
|
-
}
|
|
156
|
-
addFile(file) {
|
|
157
|
-
this.files.push(file);
|
|
161
|
+
function parseEnvValue(key, value) {
|
|
162
|
+
if (value === undefined) {
|
|
163
|
+
return undefined;
|
|
158
164
|
}
|
|
159
|
-
|
|
160
|
-
|
|
165
|
+
else if (key === 'tokenLimit' && typeof value === 'string') {
|
|
166
|
+
return parseInt(value);
|
|
167
|
+
}
|
|
168
|
+
else if ((key === 'ignoredFiles' || key === 'ignoredExtensions') &&
|
|
169
|
+
typeof value === 'string' &&
|
|
170
|
+
value.includes(',')) {
|
|
171
|
+
return value.split(',');
|
|
172
|
+
}
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
function toEnvVarName(key) {
|
|
176
|
+
switch (key) {
|
|
177
|
+
case 'openAIApiKey':
|
|
178
|
+
return 'OPENAI_API_KEY';
|
|
179
|
+
case 'huggingFaceHubApiKey':
|
|
180
|
+
return 'HUGGINGFACE_HUB_API_KEY';
|
|
181
|
+
default:
|
|
182
|
+
return 'COCO_' + key.replace(/([A-Z])/g, '_$1').toUpperCase();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function formatEnvValue(value) {
|
|
186
|
+
if (typeof value === 'number') {
|
|
187
|
+
return `${value}`;
|
|
188
|
+
}
|
|
189
|
+
else if (Array.isArray(value)) {
|
|
190
|
+
return `${value.join(',')}`;
|
|
191
|
+
}
|
|
192
|
+
else if (typeof value === 'string') {
|
|
193
|
+
// Escape newlines and tabs in strings
|
|
194
|
+
return `${value.replace(/\n/g, '\\n').replace(/\t/g, '\\t')}`;
|
|
195
|
+
}
|
|
196
|
+
return `${value}`;
|
|
197
|
+
}
|
|
198
|
+
const appendToEnvFile = async (filePath, config) => {
|
|
199
|
+
const startComment = '# -- Start coco config --';
|
|
200
|
+
const endComment = '# -- End coco config --';
|
|
201
|
+
const getNewContent = async () => {
|
|
202
|
+
return Object.entries(config)
|
|
203
|
+
.map(([key, value]) => `${toEnvVarName(key)}=${formatEnvValue(value)}`)
|
|
204
|
+
.join('\n');
|
|
205
|
+
};
|
|
206
|
+
await updateFileSection({
|
|
207
|
+
filePath,
|
|
208
|
+
startComment,
|
|
209
|
+
endComment,
|
|
210
|
+
getNewContent,
|
|
211
|
+
confirmMessage: CONFIG_ALREADY_EXISTS,
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Load git profile config (from ~/.gitconfig)
|
|
217
|
+
*
|
|
218
|
+
* @param {Config} config
|
|
219
|
+
* @returns {Config} Updated config
|
|
220
|
+
**/
|
|
221
|
+
function loadGitConfig(config) {
|
|
222
|
+
const gitConfigPath = path.join(os.homedir(), '.gitconfig');
|
|
223
|
+
if (fs.existsSync(gitConfigPath)) {
|
|
224
|
+
const gitConfigRaw = fs.readFileSync(gitConfigPath, 'utf-8');
|
|
225
|
+
const gitConfigParsed = ini.parse(gitConfigRaw);
|
|
226
|
+
config = {
|
|
227
|
+
...config,
|
|
228
|
+
service: gitConfigParsed.coco?.model || config.service,
|
|
229
|
+
openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
|
|
230
|
+
huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
|
|
231
|
+
tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
|
|
232
|
+
prompt: gitConfigParsed.coco?.prompt || config.prompt,
|
|
233
|
+
mode: gitConfigParsed.coco?.mode || config.mode,
|
|
234
|
+
temperature: gitConfigParsed.coco?.temperature || config.temperature,
|
|
235
|
+
summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
|
|
236
|
+
ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
|
|
237
|
+
ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
|
|
238
|
+
defaultBranch: gitConfigParsed.coco?.defaultBranch || config.defaultBranch,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return config;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Appends the provided configuration to a git config file.
|
|
245
|
+
*
|
|
246
|
+
* @param filePath - The path to the .gitconfig
|
|
247
|
+
* @param config - The configuration object to append.
|
|
248
|
+
*/
|
|
249
|
+
const appendToGitConfig = async (filePath, config) => {
|
|
250
|
+
if (!fs.existsSync(filePath)) {
|
|
251
|
+
throw new Error(`File ${filePath} does not exist.`);
|
|
252
|
+
}
|
|
253
|
+
const startComment = '# -- Start coco config --';
|
|
254
|
+
const endComment = '# -- End coco config --';
|
|
255
|
+
const header = '[coco]';
|
|
256
|
+
// Function to generate new content for the coco section
|
|
257
|
+
const getNewContent = async () => {
|
|
258
|
+
const contentLines = [header];
|
|
259
|
+
for (const key in config) {
|
|
260
|
+
// check if string has new lines, if so, wrap in quotes
|
|
261
|
+
if (typeof config[key] === 'string') {
|
|
262
|
+
const value = config[key];
|
|
263
|
+
if (value.includes('\n')) {
|
|
264
|
+
contentLines.push(`\t${key} = ${JSON.stringify(value)}`);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
contentLines.push(`\t${key} = ${config[key]}`);
|
|
269
|
+
}
|
|
270
|
+
return contentLines.join('\n');
|
|
271
|
+
};
|
|
272
|
+
await updateFileSection({
|
|
273
|
+
filePath,
|
|
274
|
+
startComment,
|
|
275
|
+
endComment,
|
|
276
|
+
getNewContent,
|
|
277
|
+
confirmUpdate: true,
|
|
278
|
+
confirmMessage: CONFIG_ALREADY_EXISTS,
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Load .gitignore in project root
|
|
284
|
+
*
|
|
285
|
+
* @param {Config} config
|
|
286
|
+
* @returns
|
|
287
|
+
*/
|
|
288
|
+
function loadGitignore(config) {
|
|
289
|
+
if (fs.existsSync('.gitignore')) {
|
|
290
|
+
const gitignoreContent = fs.readFileSync('.gitignore', 'utf-8');
|
|
291
|
+
config.ignoredFiles = [
|
|
292
|
+
...(config?.ignoredFiles || []),
|
|
293
|
+
...gitignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
return config;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Load .ignore in project root
|
|
300
|
+
*
|
|
301
|
+
* @param {Config} config
|
|
302
|
+
* @returns
|
|
303
|
+
*/
|
|
304
|
+
function loadIgnore(config) {
|
|
305
|
+
if (fs.existsSync('.ignore')) {
|
|
306
|
+
const ignoreContent = fs.readFileSync('.ignore', 'utf-8');
|
|
307
|
+
config.ignoredFiles = [
|
|
308
|
+
...(config?.ignoredFiles || []),
|
|
309
|
+
...ignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
310
|
+
];
|
|
311
|
+
}
|
|
312
|
+
return config;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Load project config
|
|
317
|
+
*
|
|
318
|
+
* @param {Config} config
|
|
319
|
+
* @returns {Config} Updated config
|
|
320
|
+
**/
|
|
321
|
+
function loadProjectConfig(config) {
|
|
322
|
+
// TODO: Add validation based of JSON schema?
|
|
323
|
+
// @see https://github.com/acornejo/jjv
|
|
324
|
+
if (fs.existsSync('.coco.config.json')) {
|
|
325
|
+
const projectConfig = JSON.parse(fs.readFileSync('.coco.config.json', 'utf-8'));
|
|
326
|
+
config = { ...config, ...projectConfig };
|
|
327
|
+
}
|
|
328
|
+
return config;
|
|
329
|
+
}
|
|
330
|
+
const appendToProjectConfig = (filePath, config) => {
|
|
331
|
+
fs.writeFileSync(filePath, JSON.stringify({
|
|
332
|
+
$schema: 'https://git-co.co/schema.json',
|
|
333
|
+
...config,
|
|
334
|
+
}, null, 2));
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Load XDG config
|
|
339
|
+
*
|
|
340
|
+
* @param {Config} config
|
|
341
|
+
* @returns {Config} Updated config
|
|
342
|
+
*/
|
|
343
|
+
function loadXDGConfig(config) {
|
|
344
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
345
|
+
const xdgConfigPath = path.join(xdgConfigHome, 'coco', 'config.json');
|
|
346
|
+
if (fs.existsSync(xdgConfigPath)) {
|
|
347
|
+
const xdgConfig = JSON.parse(fs.readFileSync(xdgConfigPath, 'utf-8'));
|
|
348
|
+
config = { ...config, ...xdgConfig };
|
|
349
|
+
}
|
|
350
|
+
return config;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Load application config
|
|
355
|
+
*
|
|
356
|
+
* Merge config from multiple sources.
|
|
357
|
+
*
|
|
358
|
+
* \* Order of precedence:
|
|
359
|
+
* \* 1. Command line flags
|
|
360
|
+
* \* 2. Environment variables
|
|
361
|
+
* \* 3. Project config
|
|
362
|
+
* \* 4. Git config
|
|
363
|
+
* \* 5. XDG config
|
|
364
|
+
* \* 6. .gitignore
|
|
365
|
+
* \* 7. .ignore
|
|
366
|
+
* \* 8. Default config
|
|
367
|
+
*
|
|
368
|
+
* @returns {Config} application config
|
|
369
|
+
**/
|
|
370
|
+
function loadConfig(argv = {}) {
|
|
371
|
+
// Default config
|
|
372
|
+
let config = DEFAULT_CONFIG;
|
|
373
|
+
config = loadGitignore(config);
|
|
374
|
+
config = loadIgnore(config);
|
|
375
|
+
config = loadXDGConfig(config);
|
|
376
|
+
config = loadGitConfig(config);
|
|
377
|
+
config = loadProjectConfig(config);
|
|
378
|
+
config = loadEnvConfig(config);
|
|
379
|
+
return { ...config, ...argv };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
class Logger {
|
|
383
|
+
constructor(config) {
|
|
384
|
+
this.config = config;
|
|
385
|
+
this.spinner = null;
|
|
386
|
+
}
|
|
387
|
+
log(message, options = { color: 'blue' }) {
|
|
388
|
+
let outputMessage = message;
|
|
389
|
+
if (options.color) {
|
|
390
|
+
outputMessage = chalk[options.color](outputMessage);
|
|
391
|
+
}
|
|
392
|
+
console.log(outputMessage);
|
|
393
|
+
return this;
|
|
394
|
+
}
|
|
395
|
+
verbose(message, options = {}) {
|
|
396
|
+
if (!this.config?.verbose) {
|
|
397
|
+
return this;
|
|
398
|
+
}
|
|
399
|
+
this.log(message, options);
|
|
400
|
+
return this;
|
|
401
|
+
}
|
|
402
|
+
startTimer() {
|
|
403
|
+
this.timerStart = now();
|
|
404
|
+
return this;
|
|
405
|
+
}
|
|
406
|
+
stopTimer(message, options = { color: 'yellow' }) {
|
|
407
|
+
if (!this.config?.verbose || !this.timerStart) {
|
|
408
|
+
return this;
|
|
409
|
+
}
|
|
410
|
+
const elapsedTime = prettyMilliseconds(now() - this.timerStart);
|
|
411
|
+
let outputMessage = message
|
|
412
|
+
? `${message} (β² ${elapsedTime})`
|
|
413
|
+
: `β² ${elapsedTime}`;
|
|
414
|
+
if (options.color) {
|
|
415
|
+
outputMessage = chalk[options.color](outputMessage);
|
|
416
|
+
}
|
|
417
|
+
console.log(outputMessage);
|
|
418
|
+
return this;
|
|
419
|
+
}
|
|
420
|
+
startSpinner(message, options = { color: 'green' }) {
|
|
421
|
+
const spinnerMessage = options.color ? chalk[options.color](message) : message;
|
|
422
|
+
this.spinner = ora(spinnerMessage).start();
|
|
423
|
+
return this;
|
|
424
|
+
}
|
|
425
|
+
stopSpinner(message = '', options = { mode: 'succeed', color: 'green' }) {
|
|
426
|
+
const spinnerMessage = options?.color ? chalk[options.color](message) : message;
|
|
427
|
+
this.spinner?.[options.mode || 'succeed'](spinnerMessage);
|
|
428
|
+
this.spinner = null;
|
|
429
|
+
return this;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function commandExecutor(handler) {
|
|
434
|
+
return async (argv) => {
|
|
435
|
+
const options = loadConfig(argv);
|
|
436
|
+
const logger = new Logger(options);
|
|
437
|
+
try {
|
|
438
|
+
await handler(argv, logger);
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
logger.log('\nFailed to execute command', { color: 'yellow' });
|
|
442
|
+
logger.verbose(`\nError: "${error.message}"`, { color: 'red' });
|
|
443
|
+
logger.log('\nThanks for using coco, make it a great day! ππ€', { color: 'blue' });
|
|
444
|
+
process.exit(0);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Extract the path from a file path string.
|
|
451
|
+
* @param {string} filePath - The full file path.
|
|
452
|
+
* @returns {string} The path portion of the file path.
|
|
453
|
+
*/
|
|
454
|
+
function getPathFromFilePath(filePath) {
|
|
455
|
+
return filePath.split('/').slice(0, -1).join('/');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function summarize(documents, { chain, textSplitter, options }) {
|
|
459
|
+
const { returnIntermediateSteps = false } = options || {};
|
|
460
|
+
const docs = await textSplitter.splitDocuments(documents.map((doc) => new Document(doc)));
|
|
461
|
+
const res = await chain.call({
|
|
462
|
+
input_documents: docs,
|
|
463
|
+
returnIntermediateSteps,
|
|
464
|
+
});
|
|
465
|
+
if (res.error)
|
|
466
|
+
throw new Error(res.error);
|
|
467
|
+
return res.text && res.text.trim();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Create groups from a given node info.
|
|
472
|
+
* @param {DiffNode} node - The node info to start grouping.
|
|
473
|
+
* @returns {DirectoryDiff[]} The groups created.
|
|
474
|
+
*/
|
|
475
|
+
function createDirectoryDiffs(node) {
|
|
476
|
+
const groupByPath = {};
|
|
477
|
+
function traverse(node) {
|
|
478
|
+
node.diffs.forEach((diff) => {
|
|
479
|
+
const path = getPathFromFilePath(diff.file);
|
|
480
|
+
if (!groupByPath[path]) {
|
|
481
|
+
groupByPath[path] = { diffs: [], path, tokenCount: 0 };
|
|
482
|
+
}
|
|
483
|
+
groupByPath[path].diffs.push(diff);
|
|
484
|
+
groupByPath[path].tokenCount += diff.tokenCount;
|
|
485
|
+
});
|
|
486
|
+
node.children.forEach(traverse);
|
|
487
|
+
}
|
|
488
|
+
traverse(node);
|
|
489
|
+
return Object.values(groupByPath);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Summarize a directory diff asynchronously.
|
|
493
|
+
*/
|
|
494
|
+
async function summarizeDirectoryDiff(directory, { chain, textSplitter, tokenizer }) {
|
|
495
|
+
try {
|
|
496
|
+
const directorySummary = await summarize(directory.diffs.map((diff) => ({
|
|
497
|
+
pageContent: diff.diff,
|
|
498
|
+
metadata: {
|
|
499
|
+
file: diff.file,
|
|
500
|
+
summary: diff.summary,
|
|
501
|
+
},
|
|
502
|
+
})), {
|
|
503
|
+
chain,
|
|
504
|
+
textSplitter,
|
|
505
|
+
options: {
|
|
506
|
+
returnIntermediateSteps: true,
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
const newTokenTotal = tokenizer(directorySummary);
|
|
510
|
+
return {
|
|
511
|
+
diffs: directory.diffs,
|
|
512
|
+
path: directory.path,
|
|
513
|
+
summary: directorySummary,
|
|
514
|
+
tokenCount: newTokenTotal,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
console.error(error);
|
|
519
|
+
return directory;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const defaultOutputCallback = (group) => {
|
|
523
|
+
let output = `
|
|
524
|
+
-------\n* changes in "/${group.path}"\n\n`;
|
|
525
|
+
if (group.summary) {
|
|
526
|
+
output += `${group.diffs.map((diff) => ` β’ ${diff.summary}`).join('\n')}\n\nSummary:\n\n${group.summary}\n\n`;
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
output += `${group.diffs.map((diff) => ` β’ ${diff.summary}\n\n${diff.diff}`).join('\n\n')}\n\n`;
|
|
530
|
+
}
|
|
531
|
+
return output;
|
|
532
|
+
};
|
|
533
|
+
async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 2048, textSplitter, chain, handleOutput = defaultOutputCallback, }) {
|
|
534
|
+
const queue = new pQueue({ concurrency: 8 });
|
|
535
|
+
logger.startTimer().startSpinner(`Organizing Diffs...`, { color: 'blue' });
|
|
536
|
+
const directoryDiffs = createDirectoryDiffs(rootDiffNode);
|
|
537
|
+
// Sort by token count descending
|
|
538
|
+
directoryDiffs.sort((a, b) => b.tokenCount - a.tokenCount);
|
|
539
|
+
let totalTokenCount = directoryDiffs.reduce((sum, group) => sum + group.tokenCount, 0);
|
|
540
|
+
logger.stopSpinner('Diffs Organized').stopTimer();
|
|
541
|
+
logger.startSpinner(`Consolidating Diffs`, { color: 'blue' });
|
|
542
|
+
const processingTasks = directoryDiffs.map((group, i) => {
|
|
543
|
+
return queue.add(async () => {
|
|
544
|
+
// If the diff token count is already less than the average req, we can skip summarizing.
|
|
545
|
+
const isLessThanAvgTokenReq = group.tokenCount <= maxTokens / directoryDiffs.length;
|
|
546
|
+
if (totalTokenCount <= maxTokens || isLessThanAvgTokenReq) {
|
|
547
|
+
return group;
|
|
548
|
+
}
|
|
549
|
+
group = await summarizeDirectoryDiff(group, {
|
|
550
|
+
chain,
|
|
551
|
+
textSplitter,
|
|
552
|
+
tokenizer,
|
|
553
|
+
});
|
|
554
|
+
// We need to subtract the old token count and add the new one
|
|
555
|
+
totalTokenCount = totalTokenCount - directoryDiffs[i].tokenCount + group.tokenCount;
|
|
556
|
+
directoryDiffs[i] = group;
|
|
557
|
+
logger
|
|
558
|
+
.verbose(`\n β’ Summarized diffs in "/${group.path}" `, { color: 'blue' })
|
|
559
|
+
.verbose(`\nTotal token count: ${totalTokenCount}`, {
|
|
560
|
+
color: totalTokenCount > maxTokens ? 'yellow' : 'green',
|
|
561
|
+
});
|
|
562
|
+
return group;
|
|
563
|
+
}, { priority: group.tokenCount });
|
|
564
|
+
});
|
|
565
|
+
await Promise.all(processingTasks);
|
|
566
|
+
logger.stopSpinner(`Summarized Diffs`);
|
|
567
|
+
return directoryDiffs.map(handleOutput).join('');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
class DiffTreeNode {
|
|
571
|
+
constructor(path) {
|
|
572
|
+
this.path = [];
|
|
573
|
+
this.files = [];
|
|
574
|
+
this.children = new Map();
|
|
575
|
+
if (path)
|
|
576
|
+
this.path = path;
|
|
577
|
+
}
|
|
578
|
+
addFile(file) {
|
|
579
|
+
this.files.push(file);
|
|
580
|
+
}
|
|
581
|
+
addChild(part, node) {
|
|
582
|
+
this.children.set(part, node);
|
|
161
583
|
}
|
|
162
584
|
getChild(part) {
|
|
163
585
|
return this.children.get(part);
|
|
@@ -338,18 +760,6 @@ function validatePromptTemplate(text, inputVariables) {
|
|
|
338
760
|
return true;
|
|
339
761
|
}
|
|
340
762
|
|
|
341
|
-
const template$2 = `GOAL: Use functional abstractions to summarize the following text
|
|
342
|
-
|
|
343
|
-
RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
|
|
344
|
-
|
|
345
|
-
TEXT:"""{text}"""
|
|
346
|
-
`;
|
|
347
|
-
const inputVariables$2 = ['text'];
|
|
348
|
-
const SUMMARIZE_PROMPT = new PromptTemplate({
|
|
349
|
-
template: template$2,
|
|
350
|
-
inputVariables: inputVariables$2,
|
|
351
|
-
});
|
|
352
|
-
|
|
353
763
|
async function parseDefaultFileDiff(nodeFile, commit = '--staged', git) {
|
|
354
764
|
if (commit !== '--staged') {
|
|
355
765
|
return await git.diff([`${commit}~1..${commit}`, '--', nodeFile.filePath]);
|
|
@@ -431,59 +841,8 @@ async function fileChangeParser({ changes, commit, options: { tokenizer, git, ll
|
|
|
431
841
|
chain: summarizationChain,
|
|
432
842
|
logger,
|
|
433
843
|
});
|
|
434
|
-
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
435
|
-
return summary;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
class Logger {
|
|
439
|
-
constructor(config) {
|
|
440
|
-
this.config = config;
|
|
441
|
-
this.spinner = null;
|
|
442
|
-
}
|
|
443
|
-
log(message, options = { color: 'blue' }) {
|
|
444
|
-
let outputMessage = message;
|
|
445
|
-
if (options.color) {
|
|
446
|
-
outputMessage = chalk[options.color](outputMessage);
|
|
447
|
-
}
|
|
448
|
-
console.log(outputMessage);
|
|
449
|
-
return this;
|
|
450
|
-
}
|
|
451
|
-
verbose(message, options = {}) {
|
|
452
|
-
if (!this.config?.verbose) {
|
|
453
|
-
return this;
|
|
454
|
-
}
|
|
455
|
-
this.log(message, options);
|
|
456
|
-
return this;
|
|
457
|
-
}
|
|
458
|
-
startTimer() {
|
|
459
|
-
this.timerStart = now();
|
|
460
|
-
return this;
|
|
461
|
-
}
|
|
462
|
-
stopTimer(message, options = { color: 'yellow' }) {
|
|
463
|
-
if (!this.config?.verbose || !this.timerStart) {
|
|
464
|
-
return this;
|
|
465
|
-
}
|
|
466
|
-
const elapsedTime = prettyMilliseconds(now() - this.timerStart);
|
|
467
|
-
let outputMessage = message
|
|
468
|
-
? `${message} (β² ${elapsedTime})`
|
|
469
|
-
: `β² ${elapsedTime}`;
|
|
470
|
-
if (options.color) {
|
|
471
|
-
outputMessage = chalk[options.color](outputMessage);
|
|
472
|
-
}
|
|
473
|
-
console.log(outputMessage);
|
|
474
|
-
return this;
|
|
475
|
-
}
|
|
476
|
-
startSpinner(message, options = { color: 'green' }) {
|
|
477
|
-
const spinnerMessage = options.color ? chalk[options.color](message) : message;
|
|
478
|
-
this.spinner = ora(spinnerMessage).start();
|
|
479
|
-
return this;
|
|
480
|
-
}
|
|
481
|
-
stopSpinner(message = '', options = { mode: 'succeed', color: 'green' }) {
|
|
482
|
-
const spinnerMessage = options?.color ? chalk[options.color](message) : message;
|
|
483
|
-
this.spinner?.[options.mode || 'succeed'](spinnerMessage);
|
|
484
|
-
this.spinner = null;
|
|
485
|
-
return this;
|
|
486
|
-
}
|
|
844
|
+
logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
|
|
845
|
+
return summary;
|
|
487
846
|
}
|
|
488
847
|
|
|
489
848
|
const template$1 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
|
|
@@ -511,368 +870,52 @@ function getStatus(file, location = 'index') {
|
|
|
511
870
|
switch (statusCode) {
|
|
512
871
|
case 'A':
|
|
513
872
|
return 'added';
|
|
514
|
-
case 'D':
|
|
515
|
-
return 'deleted';
|
|
516
|
-
case 'M':
|
|
517
|
-
return 'modified';
|
|
518
|
-
case 'R':
|
|
519
|
-
return 'renamed';
|
|
520
|
-
case '?':
|
|
521
|
-
return 'untracked';
|
|
522
|
-
default:
|
|
523
|
-
return 'unknown';
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
else if ('changes' in file && 'binary' in file) {
|
|
527
|
-
if (file.changes === 0)
|
|
528
|
-
return 'untracked';
|
|
529
|
-
if (file.file.includes('=>'))
|
|
530
|
-
return 'renamed';
|
|
531
|
-
if (file.deletions === 0 && file.insertions > 0)
|
|
532
|
-
return 'added';
|
|
533
|
-
if (file.insertions === 0 && file.deletions > 0)
|
|
534
|
-
return 'deleted';
|
|
535
|
-
if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
|
|
536
|
-
return 'modified';
|
|
537
|
-
return 'unknown';
|
|
538
|
-
}
|
|
539
|
-
else {
|
|
540
|
-
throw new Error('Invalid file type');
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function getSummaryText(file, change) {
|
|
545
|
-
const status = change.status || getStatus(file);
|
|
546
|
-
let filePath;
|
|
547
|
-
if ('path' in file) {
|
|
548
|
-
filePath = file.path;
|
|
549
|
-
}
|
|
550
|
-
else if ('file' in file) {
|
|
551
|
-
filePath = change?.filePath || file.file;
|
|
552
|
-
}
|
|
553
|
-
else {
|
|
554
|
-
throw new Error('Invalid file type');
|
|
555
|
-
}
|
|
556
|
-
if (change.oldFilePath) {
|
|
557
|
-
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
558
|
-
}
|
|
559
|
-
return `${status}: ${filePath}`;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Returns a new object with all undefined keys removed
|
|
564
|
-
*
|
|
565
|
-
* @param obj Object to remove undefined keys from
|
|
566
|
-
* @returns
|
|
567
|
-
*/
|
|
568
|
-
function removeUndefined(obj) {
|
|
569
|
-
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Default Config
|
|
574
|
-
*
|
|
575
|
-
* @type {Config}
|
|
576
|
-
*/
|
|
577
|
-
const DEFAULT_CONFIG = {
|
|
578
|
-
service: 'openai/gpt-4',
|
|
579
|
-
verbose: false,
|
|
580
|
-
tokenLimit: 1024,
|
|
581
|
-
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
582
|
-
temperature: 0.4,
|
|
583
|
-
mode: 'stdout',
|
|
584
|
-
ignoredFiles: ['package-lock.json'],
|
|
585
|
-
ignoredExtensions: ['.map', '.lock'],
|
|
586
|
-
defaultBranch: 'main',
|
|
587
|
-
};
|
|
588
|
-
/**
|
|
589
|
-
* Create a named export of all config keys for use in other modules.
|
|
590
|
-
*
|
|
591
|
-
* @see Currently used in `src/lib/config/services/env.ts` to validate all env vars.
|
|
592
|
-
*
|
|
593
|
-
* @type {string[]}
|
|
594
|
-
*/
|
|
595
|
-
const CONFIG_KEYS = Object.keys({
|
|
596
|
-
...DEFAULT_CONFIG,
|
|
597
|
-
huggingFaceHubApiKey: '',
|
|
598
|
-
openAIApiKey: '',
|
|
599
|
-
prompt: '',
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
async function updateFileSection(filePath, startComment, endComment, getNewContent, confirmUpdate = true) {
|
|
603
|
-
const lines = fs__default.existsSync(filePath) ? fs__default.readFileSync(filePath, 'utf-8').split(/\r?\n/) : [];
|
|
604
|
-
const newLines = [];
|
|
605
|
-
let foundSection = false;
|
|
606
|
-
for (let i = 0; i < lines.length; i++) {
|
|
607
|
-
if (lines[i].trim() === startComment) {
|
|
608
|
-
foundSection = true;
|
|
609
|
-
if (confirmUpdate) {
|
|
610
|
-
const confirmOverwrite = await confirm({
|
|
611
|
-
message: `A section already exists in ${filePath}, do you want to override it?`,
|
|
612
|
-
default: false,
|
|
613
|
-
});
|
|
614
|
-
if (!confirmOverwrite) {
|
|
615
|
-
// keep all lines until the end comment
|
|
616
|
-
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
617
|
-
newLines.push(lines[i]);
|
|
618
|
-
i++;
|
|
619
|
-
}
|
|
620
|
-
newLines.push(endComment);
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
newLines.push(startComment);
|
|
625
|
-
// Insert the new content
|
|
626
|
-
const newContent = await getNewContent();
|
|
627
|
-
newLines.push(newContent);
|
|
628
|
-
// Skip the existing content of the section
|
|
629
|
-
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
630
|
-
i++;
|
|
631
|
-
}
|
|
632
|
-
newLines.push(endComment);
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
|
-
if (!foundSection || lines[i].trim() !== endComment) {
|
|
636
|
-
newLines.push(lines[i]);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
// If section wasn't found, append it at the end
|
|
640
|
-
if (!foundSection) {
|
|
641
|
-
newLines.push('\n' + startComment);
|
|
642
|
-
const newContent = await getNewContent();
|
|
643
|
-
newLines.push(newContent);
|
|
644
|
-
newLines.push(endComment);
|
|
645
|
-
}
|
|
646
|
-
// Write the updated contents back to the file
|
|
647
|
-
fs__default.writeFileSync(filePath, newLines.join('\n'));
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/**
|
|
651
|
-
* Load environment variables
|
|
652
|
-
*
|
|
653
|
-
* @param {Config} config
|
|
654
|
-
* @returns {Config} Updated config
|
|
655
|
-
**/
|
|
656
|
-
function loadEnvConfig(config) {
|
|
657
|
-
const envConfig = {};
|
|
658
|
-
CONFIG_KEYS.forEach((key) => {
|
|
659
|
-
const envVarName = toEnvVarName(key);
|
|
660
|
-
const envValue = parseEnvValue(key, process.env[envVarName]);
|
|
661
|
-
if (envValue === undefined)
|
|
662
|
-
return;
|
|
663
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
664
|
-
// @ts-ignore
|
|
665
|
-
envConfig[key] = envValue;
|
|
666
|
-
});
|
|
667
|
-
return { ...config, ...removeUndefined(envConfig) };
|
|
668
|
-
}
|
|
669
|
-
function parseEnvValue(key, value) {
|
|
670
|
-
if (value === undefined) {
|
|
671
|
-
return undefined;
|
|
672
|
-
}
|
|
673
|
-
else if (key === 'tokenLimit' && typeof value === 'string') {
|
|
674
|
-
return parseInt(value);
|
|
675
|
-
}
|
|
676
|
-
else if ((key === 'ignoredFiles' || key === 'ignoredExtensions') &&
|
|
677
|
-
typeof value === 'string' &&
|
|
678
|
-
value.includes(',')) {
|
|
679
|
-
return value.split(',');
|
|
680
|
-
}
|
|
681
|
-
return value;
|
|
682
|
-
}
|
|
683
|
-
function toEnvVarName(key) {
|
|
684
|
-
switch (key) {
|
|
685
|
-
case 'openAIApiKey':
|
|
686
|
-
return 'OPENAI_API_KEY';
|
|
687
|
-
case 'huggingFaceHubApiKey':
|
|
688
|
-
return 'HUGGINGFACE_HUB_API_KEY';
|
|
689
|
-
default:
|
|
690
|
-
return 'COCO_' + key.replace(/([A-Z])/g, '_$1').toUpperCase();
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
function formatEnvValue(value) {
|
|
694
|
-
if (typeof value === 'number') {
|
|
695
|
-
return `${value}`;
|
|
696
|
-
}
|
|
697
|
-
else if (Array.isArray(value)) {
|
|
698
|
-
return `${value.join(',')}`;
|
|
699
|
-
}
|
|
700
|
-
else if (typeof value === 'string') {
|
|
701
|
-
// Escape newlines and tabs in strings
|
|
702
|
-
return `${value.replace(/\n/g, '\\n').replace(/\t/g, '\\t')}`;
|
|
703
|
-
}
|
|
704
|
-
return `${value}`;
|
|
705
|
-
}
|
|
706
|
-
const appendToEnvFile = async (filePath, config) => {
|
|
707
|
-
const startComment = '# -- Start coco config --';
|
|
708
|
-
const endComment = '# -- End coco config --';
|
|
709
|
-
const getNewContent = async () => {
|
|
710
|
-
return Object.entries(config)
|
|
711
|
-
.map(([key, value]) => `${toEnvVarName(key)}=${formatEnvValue(value)}`)
|
|
712
|
-
.join('\n');
|
|
713
|
-
};
|
|
714
|
-
await updateFileSection(filePath, startComment, endComment, getNewContent);
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Load git profile config (from ~/.gitconfig)
|
|
719
|
-
*
|
|
720
|
-
* @param {Config} config
|
|
721
|
-
* @returns {Config} Updated config
|
|
722
|
-
**/
|
|
723
|
-
function loadGitConfig(config) {
|
|
724
|
-
const gitConfigPath = path.join(os.homedir(), '.gitconfig');
|
|
725
|
-
if (fs.existsSync(gitConfigPath)) {
|
|
726
|
-
const gitConfigRaw = fs.readFileSync(gitConfigPath, 'utf-8');
|
|
727
|
-
const gitConfigParsed = ini.parse(gitConfigRaw);
|
|
728
|
-
config = {
|
|
729
|
-
...config,
|
|
730
|
-
service: gitConfigParsed.coco?.model || config.service,
|
|
731
|
-
openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
|
|
732
|
-
huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
|
|
733
|
-
tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
|
|
734
|
-
prompt: gitConfigParsed.coco?.prompt || config.prompt,
|
|
735
|
-
mode: gitConfigParsed.coco?.mode || config.mode,
|
|
736
|
-
temperature: gitConfigParsed.coco?.temperature || config.temperature,
|
|
737
|
-
summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
|
|
738
|
-
ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
|
|
739
|
-
ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
|
|
740
|
-
defaultBranch: gitConfigParsed.coco?.defaultBranch || config.defaultBranch,
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
return config;
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* Appends the provided configuration to a git config file.
|
|
747
|
-
*
|
|
748
|
-
* @param filePath - The path to the .gitconfig
|
|
749
|
-
* @param config - The configuration object to append.
|
|
750
|
-
*/
|
|
751
|
-
const appendToGitConfig = async (filePath, config) => {
|
|
752
|
-
if (!fs.existsSync(filePath)) {
|
|
753
|
-
throw new Error(`File ${filePath} does not exist.`);
|
|
754
|
-
}
|
|
755
|
-
const startComment = '# -- Start coco config --';
|
|
756
|
-
const endComment = '# -- End coco config --';
|
|
757
|
-
const header = '[coco]';
|
|
758
|
-
// Function to generate new content for the coco section
|
|
759
|
-
const getNewContent = async () => {
|
|
760
|
-
const contentLines = [header];
|
|
761
|
-
for (const key in config) {
|
|
762
|
-
// check if string has new lines, if so, wrap in quotes
|
|
763
|
-
if (typeof config[key] === 'string') {
|
|
764
|
-
const value = config[key];
|
|
765
|
-
if (value.includes('\n')) {
|
|
766
|
-
contentLines.push(`\t${key} = ${JSON.stringify(value)}`);
|
|
767
|
-
continue;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
contentLines.push(`\t${key} = ${config[key]}`);
|
|
873
|
+
case 'D':
|
|
874
|
+
return 'deleted';
|
|
875
|
+
case 'M':
|
|
876
|
+
return 'modified';
|
|
877
|
+
case 'R':
|
|
878
|
+
return 'renamed';
|
|
879
|
+
case '?':
|
|
880
|
+
return 'untracked';
|
|
881
|
+
default:
|
|
882
|
+
return 'unknown';
|
|
771
883
|
}
|
|
772
|
-
return contentLines.join('\n');
|
|
773
|
-
};
|
|
774
|
-
// Use the updateFileSection utility
|
|
775
|
-
await updateFileSection(filePath, startComment, endComment, getNewContent);
|
|
776
|
-
};
|
|
777
|
-
|
|
778
|
-
/**
|
|
779
|
-
* Load .gitignore in project root
|
|
780
|
-
*
|
|
781
|
-
* @param {Config} config
|
|
782
|
-
* @returns
|
|
783
|
-
*/
|
|
784
|
-
function loadGitignore(config) {
|
|
785
|
-
if (fs.existsSync('.gitignore')) {
|
|
786
|
-
const gitignoreContent = fs.readFileSync('.gitignore', 'utf-8');
|
|
787
|
-
config.ignoredFiles = [
|
|
788
|
-
...(config?.ignoredFiles || []),
|
|
789
|
-
...gitignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
790
|
-
];
|
|
791
884
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
...(config?.ignoredFiles || []),
|
|
805
|
-
...ignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
|
|
806
|
-
];
|
|
885
|
+
else if ('changes' in file && 'binary' in file) {
|
|
886
|
+
if (file.changes === 0)
|
|
887
|
+
return 'untracked';
|
|
888
|
+
if (file.file.includes('=>'))
|
|
889
|
+
return 'renamed';
|
|
890
|
+
if (file.deletions === 0 && file.insertions > 0)
|
|
891
|
+
return 'added';
|
|
892
|
+
if (file.insertions === 0 && file.deletions > 0)
|
|
893
|
+
return 'deleted';
|
|
894
|
+
if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
|
|
895
|
+
return 'modified';
|
|
896
|
+
return 'unknown';
|
|
807
897
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
/**
|
|
812
|
-
* Load project config
|
|
813
|
-
*
|
|
814
|
-
* @param {Config} config
|
|
815
|
-
* @returns {Config} Updated config
|
|
816
|
-
**/
|
|
817
|
-
function loadProjectConfig(config) {
|
|
818
|
-
// TODO: Add validation based of JSON schema?
|
|
819
|
-
// @see https://github.com/acornejo/jjv
|
|
820
|
-
if (fs.existsSync('.coco.config.json')) {
|
|
821
|
-
const projectConfig = JSON.parse(fs.readFileSync('.coco.config.json', 'utf-8'));
|
|
822
|
-
config = { ...config, ...projectConfig };
|
|
898
|
+
else {
|
|
899
|
+
throw new Error('Invalid file type');
|
|
823
900
|
}
|
|
824
|
-
return config;
|
|
825
901
|
}
|
|
826
|
-
const appendToProjectConfig = (filePath, config) => {
|
|
827
|
-
fs.writeFileSync(filePath, JSON.stringify({
|
|
828
|
-
$schema: 'https://git-co.co/schema.json',
|
|
829
|
-
...config,
|
|
830
|
-
}, null, 2));
|
|
831
|
-
};
|
|
832
902
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
*/
|
|
839
|
-
function loadXDGConfig(config) {
|
|
840
|
-
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
841
|
-
const xdgConfigPath = path.join(xdgConfigHome, 'coco', 'config.json');
|
|
842
|
-
if (fs.existsSync(xdgConfigPath)) {
|
|
843
|
-
const xdgConfig = JSON.parse(fs.readFileSync(xdgConfigPath, 'utf-8'));
|
|
844
|
-
config = { ...config, ...xdgConfig };
|
|
903
|
+
function getSummaryText(file, change) {
|
|
904
|
+
const status = change.status || getStatus(file);
|
|
905
|
+
let filePath;
|
|
906
|
+
if ('path' in file) {
|
|
907
|
+
filePath = file.path;
|
|
845
908
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
* \* 2. Environment variables
|
|
857
|
-
* \* 3. Project config
|
|
858
|
-
* \* 4. Git config
|
|
859
|
-
* \* 5. XDG config
|
|
860
|
-
* \* 6. .gitignore
|
|
861
|
-
* \* 7. .ignore
|
|
862
|
-
* \* 8. Default config
|
|
863
|
-
*
|
|
864
|
-
* @returns {Config} application config
|
|
865
|
-
**/
|
|
866
|
-
function loadConfig(argv = {}) {
|
|
867
|
-
// Default config
|
|
868
|
-
let config = DEFAULT_CONFIG;
|
|
869
|
-
config = loadGitignore(config);
|
|
870
|
-
config = loadIgnore(config);
|
|
871
|
-
config = loadXDGConfig(config);
|
|
872
|
-
config = loadGitConfig(config);
|
|
873
|
-
config = loadProjectConfig(config);
|
|
874
|
-
config = loadEnvConfig(config);
|
|
875
|
-
return { ...config, ...argv };
|
|
909
|
+
else if ('file' in file) {
|
|
910
|
+
filePath = change?.filePath || file.file;
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
throw new Error('Invalid file type');
|
|
914
|
+
}
|
|
915
|
+
if (change.oldFilePath) {
|
|
916
|
+
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
917
|
+
}
|
|
918
|
+
return `${status}: ${filePath}`;
|
|
876
919
|
}
|
|
877
920
|
|
|
878
921
|
const config = loadConfig();
|
|
@@ -960,11 +1003,6 @@ async function noResult({ git, logger }) {
|
|
|
960
1003
|
}
|
|
961
1004
|
}
|
|
962
1005
|
|
|
963
|
-
const isInteractive = (argv) => {
|
|
964
|
-
return argv?.mode === 'interactive' || argv.interactive;
|
|
965
|
-
};
|
|
966
|
-
const SEPERATOR = chalk.blue('----------------');
|
|
967
|
-
|
|
968
1006
|
function logResult(label, result) {
|
|
969
1007
|
console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
|
|
970
1008
|
}
|
|
@@ -1146,7 +1184,7 @@ async function handleResult({ result, mode, interactiveHandler }) {
|
|
|
1146
1184
|
await interactiveHandler(result);
|
|
1147
1185
|
}
|
|
1148
1186
|
else {
|
|
1149
|
-
console.
|
|
1187
|
+
console.warn('No result handler provided for interactive mode.');
|
|
1150
1188
|
logSuccess();
|
|
1151
1189
|
}
|
|
1152
1190
|
break;
|
|
@@ -1184,11 +1222,10 @@ async function createCommit(commitMsg, git) {
|
|
|
1184
1222
|
return await git.commit(commitMsg);
|
|
1185
1223
|
}
|
|
1186
1224
|
|
|
1187
|
-
|
|
1225
|
+
const handler$2 = async (argv, logger) => {
|
|
1188
1226
|
const git = getRepo();
|
|
1189
1227
|
const options = loadConfig(argv);
|
|
1190
1228
|
const { service } = options;
|
|
1191
|
-
const logger = new Logger(options);
|
|
1192
1229
|
const key = getApiKeyForModel(service, options);
|
|
1193
1230
|
const tokenizer = await getTokenCounter(getModelFromService(service));
|
|
1194
1231
|
if (!key) {
|
|
@@ -1200,6 +1237,9 @@ async function handler$2(argv) {
|
|
|
1200
1237
|
maxConcurrency: 10,
|
|
1201
1238
|
});
|
|
1202
1239
|
const INTERACTIVE = isInteractive(options);
|
|
1240
|
+
if (INTERACTIVE) {
|
|
1241
|
+
logger.log(LOGO);
|
|
1242
|
+
}
|
|
1203
1243
|
async function factory() {
|
|
1204
1244
|
const changes = await getChanges({ git });
|
|
1205
1245
|
return changes.staged;
|
|
@@ -1255,7 +1295,7 @@ async function handler$2(argv) {
|
|
|
1255
1295
|
},
|
|
1256
1296
|
mode: MODE,
|
|
1257
1297
|
});
|
|
1258
|
-
}
|
|
1298
|
+
};
|
|
1259
1299
|
|
|
1260
1300
|
/**
|
|
1261
1301
|
* Command line options via yargs
|
|
@@ -1314,7 +1354,7 @@ var commit = {
|
|
|
1314
1354
|
command: 'commit',
|
|
1315
1355
|
desc: 'Generate commit message',
|
|
1316
1356
|
builder: builder$2,
|
|
1317
|
-
handler: handler$2,
|
|
1357
|
+
handler: commandExecutor(handler$2),
|
|
1318
1358
|
options: options$2,
|
|
1319
1359
|
};
|
|
1320
1360
|
|
|
@@ -1391,9 +1431,8 @@ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main
|
|
|
1391
1431
|
return [];
|
|
1392
1432
|
}
|
|
1393
1433
|
|
|
1394
|
-
|
|
1434
|
+
const handler$1 = async (argv, logger) => {
|
|
1395
1435
|
const options = loadConfig(argv);
|
|
1396
|
-
const logger = new Logger(options);
|
|
1397
1436
|
const git = getRepo();
|
|
1398
1437
|
const key = getApiKeyForModel(options.service, options);
|
|
1399
1438
|
if (!key) {
|
|
@@ -1405,6 +1444,9 @@ async function handler$1(argv) {
|
|
|
1405
1444
|
maxConcurrency: 10,
|
|
1406
1445
|
});
|
|
1407
1446
|
const INTERACTIVE = isInteractive(options);
|
|
1447
|
+
if (INTERACTIVE) {
|
|
1448
|
+
logger.log(LOGO);
|
|
1449
|
+
}
|
|
1408
1450
|
async function factory() {
|
|
1409
1451
|
if (options.range) {
|
|
1410
1452
|
const [from, to] = options.range?.split(':');
|
|
@@ -1452,7 +1494,7 @@ async function handler$1(argv) {
|
|
|
1452
1494
|
interactive: INTERACTIVE,
|
|
1453
1495
|
review: {
|
|
1454
1496
|
enableFullRetry: false,
|
|
1455
|
-
}
|
|
1497
|
+
},
|
|
1456
1498
|
},
|
|
1457
1499
|
});
|
|
1458
1500
|
const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
|
|
@@ -1463,7 +1505,7 @@ async function handler$1(argv) {
|
|
|
1463
1505
|
},
|
|
1464
1506
|
mode: MODE,
|
|
1465
1507
|
});
|
|
1466
|
-
}
|
|
1508
|
+
};
|
|
1467
1509
|
|
|
1468
1510
|
/**
|
|
1469
1511
|
* Command line options via yargs
|
|
@@ -1522,39 +1564,138 @@ var changelog = {
|
|
|
1522
1564
|
command: 'changelog',
|
|
1523
1565
|
desc: 'Generate a changelog from a commit range',
|
|
1524
1566
|
builder: builder$1,
|
|
1525
|
-
handler: handler$1,
|
|
1567
|
+
handler: commandExecutor(handler$1),
|
|
1526
1568
|
options: options$1,
|
|
1527
1569
|
};
|
|
1528
1570
|
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1571
|
+
/**
|
|
1572
|
+
* Finds the project root directory starting from the given current directory.
|
|
1573
|
+
* It checks if the `.git` directory or `package.json` file exists in the current directory or any of its parent directories.
|
|
1574
|
+
* If found, it returns the path to the project root directory.
|
|
1575
|
+
* If not found, it throws an error.
|
|
1576
|
+
*
|
|
1577
|
+
* @param currentDir - The current directory to start searching from.
|
|
1578
|
+
* @returns The path to the project root directory.
|
|
1579
|
+
* @throws Error if the project root directory cannot be found.
|
|
1580
|
+
*/
|
|
1581
|
+
function findProjectRoot(currentDir) {
|
|
1582
|
+
const root = path__default.parse(currentDir).root;
|
|
1583
|
+
while (currentDir !== root) {
|
|
1584
|
+
if (fs__default.existsSync(path__default.join(currentDir, '.git')) ||
|
|
1585
|
+
fs__default.existsSync(path__default.join(currentDir, 'package.json'))) {
|
|
1586
|
+
return currentDir;
|
|
1587
|
+
}
|
|
1588
|
+
currentDir = path__default.dirname(currentDir);
|
|
1589
|
+
}
|
|
1590
|
+
throw new Error('Unable to find project root. Are you in the right directory?');
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// Function to execute a command and return a promise
|
|
1594
|
+
function execPromise(command, options = {}) {
|
|
1595
|
+
return new Promise((resolve, reject) => {
|
|
1596
|
+
exec(command, options, (error, stdout, stderr) => {
|
|
1597
|
+
if (error) {
|
|
1598
|
+
reject(`Execution error: ${error}`);
|
|
1599
|
+
}
|
|
1600
|
+
else {
|
|
1601
|
+
resolve({ stdout, stderr });
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1542
1604
|
});
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* Installs a package using npm.
|
|
1609
|
+
*
|
|
1610
|
+
* @param {InstallPackageInput} options - The options for installing the package.
|
|
1611
|
+
* @returns {Promise<boolean>} - A promise that resolves to true if the package is installed successfully, false otherwise.
|
|
1612
|
+
*/
|
|
1613
|
+
async function installNpmPackage({ name, flags = [], cwd = process.cwd(), }) {
|
|
1614
|
+
const { stdout, stderr } = await execPromise(`npm i ${name} ${flags.join(' ')} --yes`, { cwd });
|
|
1615
|
+
if (stderr) {
|
|
1616
|
+
console.error(`Execution error: ${stderr}`);
|
|
1617
|
+
return false;
|
|
1618
|
+
}
|
|
1619
|
+
console.log(stdout);
|
|
1620
|
+
console.error(stderr);
|
|
1621
|
+
return true;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* Checks if a package is installed in a project.
|
|
1626
|
+
*
|
|
1627
|
+
* @param packageName - The name of the package to check.
|
|
1628
|
+
* @param projectPath - The path to the project.
|
|
1629
|
+
* @returns True if the package is installed, false otherwise.
|
|
1630
|
+
*/
|
|
1631
|
+
function isPackageInstalled(packageName, projectPath) {
|
|
1632
|
+
try {
|
|
1633
|
+
// Construct the path to the package.json file
|
|
1634
|
+
const packageJsonPath = path__default.join(projectPath, 'package.json');
|
|
1635
|
+
// Read the package.json file
|
|
1636
|
+
const packageJson = JSON.parse(fs__default.readFileSync(packageJsonPath, 'utf8'));
|
|
1637
|
+
// Check both dependencies and devDependencies
|
|
1638
|
+
const dependencies = packageJson.dependencies || {};
|
|
1639
|
+
const devDependencies = packageJson.devDependencies || {};
|
|
1640
|
+
// Return true if the package is found in either
|
|
1641
|
+
return dependencies.hasOwnProperty(packageName) || devDependencies.hasOwnProperty(packageName);
|
|
1642
|
+
}
|
|
1643
|
+
catch (error) {
|
|
1644
|
+
console.error(`Error checking package installation: ${error.message}`);
|
|
1645
|
+
return false;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// TODO: QoL improvement to import this from `package.json`
|
|
1650
|
+
const packageName = 'git-coco';
|
|
1651
|
+
async function checkAndHandlePackageInstallation({ global = false, logger, }) {
|
|
1652
|
+
try {
|
|
1653
|
+
// Global installation
|
|
1654
|
+
if (global) {
|
|
1655
|
+
logger.startSpinner(`Installing '${packageName}' globally...`, { color: 'blue' });
|
|
1656
|
+
await installNpmPackage({ name: packageName, flags: ['-g'] });
|
|
1657
|
+
logger.stopSpinner(`Installed '${packageName}' globally`);
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
// Project level installation
|
|
1661
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
1662
|
+
let shouldInstall = false;
|
|
1663
|
+
if (isPackageInstalled(packageName, projectRoot)) {
|
|
1664
|
+
shouldInstall = await confirm({
|
|
1665
|
+
message: `'${packageName}' is already installed in '${projectRoot}/package.json', would you like to update?`,
|
|
1666
|
+
default: shouldInstall,
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
else {
|
|
1670
|
+
shouldInstall = true;
|
|
1671
|
+
}
|
|
1672
|
+
if (!shouldInstall) {
|
|
1673
|
+
return;
|
|
1548
1674
|
}
|
|
1675
|
+
logger.startSpinner(`Installing '${packageName}' in project...`, { color: 'blue' });
|
|
1676
|
+
await installNpmPackage({ name: packageName, cwd: projectRoot });
|
|
1677
|
+
logger.stopSpinner(`Installed '${packageName}' in project`);
|
|
1549
1678
|
}
|
|
1550
|
-
|
|
1551
|
-
};
|
|
1552
|
-
|
|
1679
|
+
catch (error) {
|
|
1680
|
+
console.error(`Error: ${error.message}`);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function getPathToUsersGitConfig() {
|
|
1553
1685
|
return path__default.join(os__default.homedir(), '.gitconfig');
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
const
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
async function createProjectFileAndReturnPath(fileName, contents) {
|
|
1689
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
1690
|
+
const configFile = `${projectRoot}/${fileName}`;
|
|
1691
|
+
if (!fs__default.existsSync(configFile)) {
|
|
1692
|
+
fs__default.writeFileSync(configFile, contents || '');
|
|
1693
|
+
}
|
|
1694
|
+
return configFile;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
const handler = async (argv, logger) => {
|
|
1698
|
+
logger.log(LOGO);
|
|
1558
1699
|
const level = await select({
|
|
1559
1700
|
message: 'configure coco at the system or project level:',
|
|
1560
1701
|
choices: [
|
|
@@ -1572,11 +1713,25 @@ async function handler(argv) {
|
|
|
1572
1713
|
});
|
|
1573
1714
|
let configFilePath = '';
|
|
1574
1715
|
switch (level) {
|
|
1575
|
-
case 'system':
|
|
1576
|
-
configFilePath = await handleSystemLevelConfig();
|
|
1577
|
-
break;
|
|
1578
1716
|
case 'project':
|
|
1579
|
-
|
|
1717
|
+
const projectConfiguration = await select({
|
|
1718
|
+
message: 'select type project level configuration:',
|
|
1719
|
+
choices: [
|
|
1720
|
+
{
|
|
1721
|
+
name: '.coco.config.json',
|
|
1722
|
+
value: '.coco.config.json',
|
|
1723
|
+
},
|
|
1724
|
+
{
|
|
1725
|
+
name: '.env',
|
|
1726
|
+
value: '.env',
|
|
1727
|
+
},
|
|
1728
|
+
],
|
|
1729
|
+
});
|
|
1730
|
+
configFilePath = await createProjectFileAndReturnPath(projectConfiguration);
|
|
1731
|
+
break;
|
|
1732
|
+
case 'system':
|
|
1733
|
+
default:
|
|
1734
|
+
configFilePath = getPathToUsersGitConfig();
|
|
1580
1735
|
break;
|
|
1581
1736
|
}
|
|
1582
1737
|
// interactive v.s stdout mode
|
|
@@ -1673,7 +1828,7 @@ async function handler(argv) {
|
|
|
1673
1828
|
// add to config after logging, so that the API key is not logged
|
|
1674
1829
|
config.openAIApiKey = apiKey;
|
|
1675
1830
|
const isApproved = await confirm({
|
|
1676
|
-
message: '
|
|
1831
|
+
message: 'looking good? (API key hidden for security)',
|
|
1677
1832
|
});
|
|
1678
1833
|
if (isApproved) {
|
|
1679
1834
|
if (configFilePath.endsWith('.gitconfig')) {
|
|
@@ -1685,62 +1840,19 @@ async function handler(argv) {
|
|
|
1685
1840
|
else if (configFilePath === '.coco.config.json') {
|
|
1686
1841
|
await appendToProjectConfig(configFilePath, config);
|
|
1687
1842
|
}
|
|
1688
|
-
|
|
1843
|
+
// After config is written, check for package installation
|
|
1844
|
+
await checkAndHandlePackageInstallation({ global: level === 'system', logger });
|
|
1845
|
+
logger.log(`\ninit successful! π¦Ύπ€π`, { color: 'green' });
|
|
1689
1846
|
}
|
|
1690
1847
|
else {
|
|
1691
|
-
logger.log('
|
|
1848
|
+
logger.log('\ninit cancelled.', { color: 'yellow' });
|
|
1692
1849
|
}
|
|
1693
|
-
}
|
|
1850
|
+
};
|
|
1694
1851
|
|
|
1695
1852
|
/**
|
|
1696
1853
|
* Command line options via yargs
|
|
1697
1854
|
*/
|
|
1698
|
-
const options = {
|
|
1699
|
-
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
1700
|
-
openAIApiKey: {
|
|
1701
|
-
type: 'string',
|
|
1702
|
-
description: 'OpenAI API Key',
|
|
1703
|
-
conflicts: 'huggingFaceHubApiKey',
|
|
1704
|
-
},
|
|
1705
|
-
huggingFaceHubApiKey: {
|
|
1706
|
-
type: 'string',
|
|
1707
|
-
description: 'HuggingFace Hub API Key',
|
|
1708
|
-
conflicts: 'openAIApiKey',
|
|
1709
|
-
},
|
|
1710
|
-
tokenLimit: { type: 'number', description: 'Token limit' },
|
|
1711
|
-
prompt: {
|
|
1712
|
-
type: 'string',
|
|
1713
|
-
alias: 'p',
|
|
1714
|
-
description: 'Commit message prompt',
|
|
1715
|
-
},
|
|
1716
|
-
i: {
|
|
1717
|
-
type: 'boolean',
|
|
1718
|
-
alias: 'interactive',
|
|
1719
|
-
description: 'Toggle interactive mode',
|
|
1720
|
-
},
|
|
1721
|
-
s: {
|
|
1722
|
-
type: 'boolean',
|
|
1723
|
-
description: 'Automatically commit staged changes with generated commit message',
|
|
1724
|
-
default: false,
|
|
1725
|
-
},
|
|
1726
|
-
e: {
|
|
1727
|
-
type: 'boolean',
|
|
1728
|
-
alias: 'edit',
|
|
1729
|
-
description: 'Open commit message in editor before proceeding',
|
|
1730
|
-
},
|
|
1731
|
-
summarizePrompt: {
|
|
1732
|
-
type: 'string',
|
|
1733
|
-
description: 'Large file summary prompt',
|
|
1734
|
-
},
|
|
1735
|
-
ignoredFiles: {
|
|
1736
|
-
type: 'array',
|
|
1737
|
-
description: 'Ignored files',
|
|
1738
|
-
},
|
|
1739
|
-
ignoredExtensions: {
|
|
1740
|
-
type: 'array',
|
|
1741
|
-
description: 'Ignored extensions',
|
|
1742
|
-
},
|
|
1743
|
-
};
|
|
1855
|
+
const options = {};
|
|
1744
1856
|
const builder = (yargs) => {
|
|
1745
1857
|
return yargs.options(options);
|
|
1746
1858
|
};
|
|
@@ -1749,7 +1861,7 @@ var init = {
|
|
|
1749
1861
|
command: 'init',
|
|
1750
1862
|
desc: 'Setup coco for a new project or system',
|
|
1751
1863
|
builder,
|
|
1752
|
-
handler,
|
|
1864
|
+
handler: commandExecutor(handler),
|
|
1753
1865
|
options,
|
|
1754
1866
|
};
|
|
1755
1867
|
|