cloving 0.3.4 → 0.3.5
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/cloving_gpt/adapters/claude.js +7 -5
- package/dist/cloving_gpt/adapters/gemini.js +14 -9
- package/dist/cloving_gpt/adapters/mistral.js +9 -7
- package/dist/cloving_gpt/adapters/ollama.js +7 -7
- package/dist/cloving_gpt/adapters/openai.js +9 -7
- package/dist/cloving_gpt/index.js +12 -7
- package/dist/commands/config.js +7 -7
- package/dist/commands/generate/context.js +10 -3
- package/dist/commands/generate/unit_tests.js +3 -261
- package/dist/commands/init.js +6 -6
- package/dist/commands/tokens.js +12 -4
- package/dist/index.js +3 -6
- package/dist/managers/ChatManager.js +77 -33
- package/dist/managers/CodeManager.js +19 -5
- package/dist/managers/CommitManager.js +6 -3
- package/dist/managers/ProxyManager.js +22 -7
- package/dist/managers/ReviewManager.js +26 -10
- package/dist/managers/ShellManager.js +5 -2
- package/dist/managers/UnitTestManager.js +243 -0
- package/dist/package.json +13 -8
- package/dist/utils/command_utils.js +86 -34
- package/dist/utils/config_utils.js +15 -5
- package/dist/utils/git_utils.js +7 -4
- package/dist/utils/string_utils.js +119 -12
- package/package.json +13 -8
|
@@ -7,7 +7,7 @@ class ClaudeAdapter {
|
|
|
7
7
|
this.anthropicVersion = '2023-06-01';
|
|
8
8
|
}
|
|
9
9
|
static listSupportedModels() {
|
|
10
|
-
ClaudeAdapter.supportedModels.forEach(model => {
|
|
10
|
+
ClaudeAdapter.supportedModels.forEach((model) => {
|
|
11
11
|
console.log(model);
|
|
12
12
|
});
|
|
13
13
|
}
|
|
@@ -18,7 +18,7 @@ class ClaudeAdapter {
|
|
|
18
18
|
return {
|
|
19
19
|
'x-api-key': apiKey,
|
|
20
20
|
'Content-Type': 'application/json',
|
|
21
|
-
'anthropic-version': this.anthropicVersion
|
|
21
|
+
'anthropic-version': this.anthropicVersion,
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
getPayload(request, stream = false) {
|
|
@@ -30,10 +30,12 @@ class ClaudeAdapter {
|
|
|
30
30
|
return {
|
|
31
31
|
model: this.model.replace(':', '-'),
|
|
32
32
|
system,
|
|
33
|
-
messages: request.messages && request.messages.length > 0
|
|
33
|
+
messages: request.messages && request.messages.length > 0
|
|
34
|
+
? request.messages
|
|
35
|
+
: [{ role: 'user', content: request.prompt }],
|
|
34
36
|
max_tokens: request.maxTokens || 4096,
|
|
35
37
|
temperature: request.temperature || 0.2,
|
|
36
|
-
stream
|
|
38
|
+
stream,
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
41
|
extractResponse(data) {
|
|
@@ -67,7 +69,7 @@ class ClaudeAdapter {
|
|
|
67
69
|
const output = parsedObject?.delta?.text || '';
|
|
68
70
|
return {
|
|
69
71
|
output,
|
|
70
|
-
lastChar
|
|
72
|
+
lastChar,
|
|
71
73
|
};
|
|
72
74
|
}
|
|
73
75
|
catch (error) {
|
|
@@ -6,7 +6,7 @@ class GeminiAdapter {
|
|
|
6
6
|
this.model = model;
|
|
7
7
|
}
|
|
8
8
|
static async listSupportedModels() {
|
|
9
|
-
GeminiAdapter.supportedModels.forEach(model => {
|
|
9
|
+
GeminiAdapter.supportedModels.forEach((model) => {
|
|
10
10
|
console.log(model);
|
|
11
11
|
});
|
|
12
12
|
}
|
|
@@ -21,7 +21,7 @@ class GeminiAdapter {
|
|
|
21
21
|
getHeaders(apiKey) {
|
|
22
22
|
return {
|
|
23
23
|
'Content-Type': 'application/json',
|
|
24
|
-
'x-goog-api-key': apiKey
|
|
24
|
+
'x-goog-api-key': apiKey,
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
getPayload(request, stream = false) {
|
|
@@ -31,22 +31,27 @@ class GeminiAdapter {
|
|
|
31
31
|
role: 'user', // or 'model' if generated from the model
|
|
32
32
|
parts: [
|
|
33
33
|
{
|
|
34
|
-
text: request.prompt
|
|
35
|
-
}
|
|
36
|
-
]
|
|
34
|
+
text: request.prompt,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
37
|
},
|
|
38
38
|
],
|
|
39
39
|
generationConfig: {
|
|
40
40
|
temperature: request.temperature || 0.2,
|
|
41
41
|
topK: 40,
|
|
42
42
|
topP: 0.95,
|
|
43
|
-
maxOutputTokens: 1024
|
|
44
|
-
}
|
|
43
|
+
maxOutputTokens: 1024,
|
|
44
|
+
},
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
extractResponse(data) {
|
|
48
48
|
try {
|
|
49
|
-
if (data &&
|
|
49
|
+
if (data &&
|
|
50
|
+
data.candidates &&
|
|
51
|
+
data.candidates[0] &&
|
|
52
|
+
data.candidates[0].content &&
|
|
53
|
+
data.candidates[0].content.parts &&
|
|
54
|
+
data.candidates[0].content.parts[0]) {
|
|
50
55
|
return data.candidates[0].content.parts[0].text;
|
|
51
56
|
}
|
|
52
57
|
else {
|
|
@@ -74,7 +79,7 @@ class GeminiAdapter {
|
|
|
74
79
|
const output = parsedObject?.candidates[0]?.content?.parts[0]?.text || '';
|
|
75
80
|
return {
|
|
76
81
|
output,
|
|
77
|
-
lastChar
|
|
82
|
+
lastChar,
|
|
78
83
|
};
|
|
79
84
|
}
|
|
80
85
|
catch (error) {
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.MistralAdapter = void 0;
|
|
4
4
|
class MistralAdapter {
|
|
5
5
|
static listSupportedModels() {
|
|
6
|
-
MistralAdapter.supportedModels.forEach(model => {
|
|
6
|
+
MistralAdapter.supportedModels.forEach((model) => {
|
|
7
7
|
console.log(model);
|
|
8
8
|
});
|
|
9
9
|
}
|
|
@@ -15,23 +15,25 @@ class MistralAdapter {
|
|
|
15
15
|
}
|
|
16
16
|
getHeaders(apiKey) {
|
|
17
17
|
return {
|
|
18
|
-
|
|
19
|
-
'Content-Type': 'application/json'
|
|
18
|
+
Authorization: `Bearer ${apiKey}`,
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
getPayload(request, stream = false) {
|
|
23
23
|
return {
|
|
24
24
|
model: this.model.replace(':', '-'),
|
|
25
|
-
messages: request.messages && request.messages.length > 0
|
|
25
|
+
messages: request.messages && request.messages.length > 0
|
|
26
|
+
? request.messages
|
|
27
|
+
: [{ role: 'user', content: request.prompt }],
|
|
26
28
|
max_tokens: request.maxTokens,
|
|
27
29
|
temperature: request.temperature || 0.2,
|
|
28
|
-
stream
|
|
30
|
+
stream,
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
extractResponse(data) {
|
|
32
34
|
return data.choices[0].message.content;
|
|
33
35
|
}
|
|
34
|
-
// data example:
|
|
36
|
+
// data example:
|
|
35
37
|
// {
|
|
36
38
|
// id: 'chatcmpl-9tL477A98d54qvVQqJT010bm8BLJl',
|
|
37
39
|
// object: 'chat.completion.chunk',
|
|
@@ -64,7 +66,7 @@ class MistralAdapter {
|
|
|
64
66
|
const output = parsedObject?.choices[0]?.delta?.content || '';
|
|
65
67
|
return {
|
|
66
68
|
output,
|
|
67
|
-
lastChar
|
|
69
|
+
lastChar,
|
|
68
70
|
};
|
|
69
71
|
}
|
|
70
72
|
catch (error) {
|
|
@@ -13,13 +13,13 @@ class OllamaAdapter {
|
|
|
13
13
|
try {
|
|
14
14
|
const endpoint = 'http://localhost:11434/api/tags';
|
|
15
15
|
const headers = {
|
|
16
|
-
'Content-Type': 'application/json'
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
17
|
};
|
|
18
18
|
const response = await axios_1.default.get(endpoint, { headers });
|
|
19
19
|
const data = response.data;
|
|
20
20
|
if (data && Array.isArray(data.models)) {
|
|
21
21
|
OllamaAdapter.supportedModels = data.models.map((model) => `ollama:${model.name}`);
|
|
22
|
-
OllamaAdapter.supportedModels.forEach(model => {
|
|
22
|
+
OllamaAdapter.supportedModels.forEach((model) => {
|
|
23
23
|
console.log(model);
|
|
24
24
|
});
|
|
25
25
|
}
|
|
@@ -36,15 +36,15 @@ class OllamaAdapter {
|
|
|
36
36
|
}
|
|
37
37
|
getHeaders(apiKey) {
|
|
38
38
|
return {
|
|
39
|
-
|
|
40
|
-
'Content-Type': 'application/json'
|
|
39
|
+
Authorization: `Bearer ${apiKey}`,
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
getPayload(request, stream = false) {
|
|
44
44
|
return {
|
|
45
45
|
model: this.model,
|
|
46
46
|
prompt: request.prompt,
|
|
47
|
-
stream
|
|
47
|
+
stream,
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
extractResponse(data) {
|
|
@@ -62,7 +62,7 @@ class OllamaAdapter {
|
|
|
62
62
|
throw error;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
// data example:
|
|
65
|
+
// data example:
|
|
66
66
|
// {
|
|
67
67
|
// model: 'dolphin-llama3:latest',
|
|
68
68
|
// created_at: '2024-08-06T21:04:34.616355Z',
|
|
@@ -84,7 +84,7 @@ class OllamaAdapter {
|
|
|
84
84
|
const output = parsedObject?.response || '';
|
|
85
85
|
return {
|
|
86
86
|
output,
|
|
87
|
-
lastChar
|
|
87
|
+
lastChar,
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
catch (error) {
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.OpenAIAdapter = void 0;
|
|
4
4
|
class OpenAIAdapter {
|
|
5
5
|
static listSupportedModels() {
|
|
6
|
-
OpenAIAdapter.supportedModels.forEach(model => {
|
|
6
|
+
OpenAIAdapter.supportedModels.forEach((model) => {
|
|
7
7
|
console.log(model);
|
|
8
8
|
});
|
|
9
9
|
}
|
|
@@ -15,23 +15,25 @@ class OpenAIAdapter {
|
|
|
15
15
|
}
|
|
16
16
|
getHeaders(apiKey) {
|
|
17
17
|
return {
|
|
18
|
-
|
|
19
|
-
'Content-Type': 'application/json'
|
|
18
|
+
Authorization: `Bearer ${apiKey}`,
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
getPayload(request, stream = false) {
|
|
23
23
|
return {
|
|
24
24
|
model: this.model.replace(':', '-'),
|
|
25
|
-
messages: request.messages && request.messages.length > 0
|
|
25
|
+
messages: request.messages && request.messages.length > 0
|
|
26
|
+
? request.messages
|
|
27
|
+
: [{ role: 'user', content: request.prompt }],
|
|
26
28
|
max_tokens: request.maxTokens,
|
|
27
29
|
temperature: request.temperature || 0.2,
|
|
28
|
-
stream
|
|
30
|
+
stream,
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
extractResponse(data) {
|
|
32
34
|
return data.choices[0].message.content;
|
|
33
35
|
}
|
|
34
|
-
// data example:
|
|
36
|
+
// data example:
|
|
35
37
|
// {
|
|
36
38
|
// id: 'chatcmpl-9tL477A98d54qvVQqJT010bm8BLJl',
|
|
37
39
|
// object: 'chat.completion.chunk',
|
|
@@ -64,7 +66,7 @@ class OpenAIAdapter {
|
|
|
64
66
|
const output = parsedObject?.choices[0]?.delta?.content || '';
|
|
65
67
|
return {
|
|
66
68
|
output,
|
|
67
|
-
lastChar
|
|
69
|
+
lastChar,
|
|
68
70
|
};
|
|
69
71
|
}
|
|
70
72
|
catch (error) {
|
|
@@ -10,6 +10,7 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
10
10
|
const child_process_1 = require("child_process");
|
|
11
11
|
const process_1 = __importDefault(require("process"));
|
|
12
12
|
const prompts_1 = require("@inquirer/prompts");
|
|
13
|
+
const colors_1 = __importDefault(require("colors"));
|
|
13
14
|
const claude_1 = require("./adapters/claude");
|
|
14
15
|
const openai_1 = require("./adapters/openai");
|
|
15
16
|
const mistral_1 = require("./adapters/mistral");
|
|
@@ -58,11 +59,13 @@ class ClovingGPT {
|
|
|
58
59
|
async reviewPrompt(prompt, messages, endpoint) {
|
|
59
60
|
if (this.silent)
|
|
60
61
|
return prompt;
|
|
61
|
-
const fullPrompt = messages
|
|
62
|
+
const fullPrompt = messages
|
|
63
|
+
.map((m) => (m.role === 'user' ? `# Task\n\n${m.content}` : m.content))
|
|
64
|
+
.join('\n\n');
|
|
62
65
|
const tokenCount = Math.ceil(fullPrompt.length / 4).toLocaleString();
|
|
63
66
|
const reviewPrompt = await (0, prompts_1.confirm)({
|
|
64
67
|
message: `Do you want to review the ~${tokenCount} token prompt before sending it to ${endpoint}?`,
|
|
65
|
-
default: true
|
|
68
|
+
default: true,
|
|
66
69
|
});
|
|
67
70
|
if (reviewPrompt) {
|
|
68
71
|
const tempFile = path_1.default.join(os_1.default.tmpdir(), `cloving_prompt_${Date.now()}.txt`);
|
|
@@ -86,7 +89,7 @@ class ClovingGPT {
|
|
|
86
89
|
}
|
|
87
90
|
const confirmPrompt = await (0, prompts_1.confirm)({
|
|
88
91
|
message: `Are you sure you want to continue?`,
|
|
89
|
-
default: true
|
|
92
|
+
default: true,
|
|
90
93
|
});
|
|
91
94
|
if (!confirmPrompt) {
|
|
92
95
|
resolve(null);
|
|
@@ -125,16 +128,18 @@ class ClovingGPT {
|
|
|
125
128
|
data: payload,
|
|
126
129
|
headers: headers,
|
|
127
130
|
responseType: 'stream',
|
|
128
|
-
timeout: this.timeout
|
|
131
|
+
timeout: this.timeout,
|
|
129
132
|
});
|
|
130
133
|
}
|
|
131
134
|
catch (err) {
|
|
132
135
|
const error = err;
|
|
133
136
|
if (error.response && error.response.status === 429) {
|
|
134
137
|
attempt++;
|
|
135
|
-
const tokenCount = payload.messages
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
const tokenCount = payload.messages
|
|
139
|
+
.reduce((acc, message) => acc + Math.ceil(message.content.length / 4), 0)
|
|
140
|
+
.toLocaleString();
|
|
141
|
+
console.warn(`${colors_1.default.red.bold('ERROR')} Rate limit error for this ${tokenCount} token prompt. Possibly past the token limit for this AI API. Try including fewer code files. Retrying in ${delay / 1000} seconds... (Attempt ${attempt} of ${maxRetries})`);
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
138
143
|
delay *= 2; // Exponential backoff
|
|
139
144
|
}
|
|
140
145
|
else {
|
package/dist/commands/config.js
CHANGED
|
@@ -17,20 +17,20 @@ const config = async () => {
|
|
|
17
17
|
}
|
|
18
18
|
let continueConfig = true;
|
|
19
19
|
while (continueConfig) {
|
|
20
|
-
const providers = [...new Set(models.map(model => model.split(':')[0]))];
|
|
20
|
+
const providers = [...new Set(models.map((model) => model.split(':')[0]))];
|
|
21
21
|
const selectedProvider = await (0, prompts_1.select)({
|
|
22
22
|
message: 'Select a provider:',
|
|
23
|
-
choices: providers.map(provider => ({ name: provider, value: provider })),
|
|
23
|
+
choices: providers.map((provider) => ({ name: provider, value: provider })),
|
|
24
24
|
});
|
|
25
|
-
const providerModels = models.filter(model => model.startsWith(selectedProvider));
|
|
26
|
-
const modelCategories = [...new Set(providerModels.map(model => model.split(':')[1]))];
|
|
25
|
+
const providerModels = models.filter((model) => model.startsWith(selectedProvider));
|
|
26
|
+
const modelCategories = [...new Set(providerModels.map((model) => model.split(':')[1]))];
|
|
27
27
|
const selectedCategory = await (0, prompts_1.select)({
|
|
28
28
|
message: 'Select a category:',
|
|
29
|
-
choices: modelCategories.map(category => ({ name: category, value: category })),
|
|
29
|
+
choices: modelCategories.map((category) => ({ name: category, value: category })),
|
|
30
30
|
});
|
|
31
|
-
const specificModels = providerModels.filter(model => model.split(':')[1] === selectedCategory);
|
|
31
|
+
const specificModels = providerModels.filter((model) => model.split(':')[1] === selectedCategory);
|
|
32
32
|
const modelIndex = await (0, prompts_1.select)({
|
|
33
|
-
message:
|
|
33
|
+
message: "Select an AI model you'd like to use:",
|
|
34
34
|
choices: specificModels.map((model, index) => ({ name: model, value: index })),
|
|
35
35
|
pageSize: specificModels.length,
|
|
36
36
|
});
|
|
@@ -12,8 +12,12 @@ const command_utils_1 = require("../../utils/command_utils");
|
|
|
12
12
|
const config_utils_1 = require("../../utils/config_utils");
|
|
13
13
|
const generateContextPrompt = (files, contextFiles) => {
|
|
14
14
|
const specialFileContents = (0, command_utils_1.collectSpecialFileContents)();
|
|
15
|
-
const specialFiles = Object.keys(specialFileContents)
|
|
16
|
-
|
|
15
|
+
const specialFiles = Object.keys(specialFileContents)
|
|
16
|
+
.map((file) => `### Contents of ${file}\n\n${JSON.stringify(specialFileContents[file], null, 2)}\n\n`)
|
|
17
|
+
.join('\n');
|
|
18
|
+
const contextFileContents = Object.keys(contextFiles)
|
|
19
|
+
.map((file) => `### Contents of ${file}\n\n${contextFiles[file]}\n\n`)
|
|
20
|
+
.join('\n');
|
|
17
21
|
return `### Description of App
|
|
18
22
|
|
|
19
23
|
${JSON.stringify((0, config_utils_1.getClovingConfig)(), null, 2)}
|
|
@@ -35,7 +39,10 @@ const context = async (options) => {
|
|
|
35
39
|
if (files) {
|
|
36
40
|
for (const file of files) {
|
|
37
41
|
const filePath = path_1.default.resolve(file);
|
|
38
|
-
if (await fs_1.default.promises
|
|
42
|
+
if (await fs_1.default.promises
|
|
43
|
+
.stat(filePath)
|
|
44
|
+
.then((stat) => stat.isFile())
|
|
45
|
+
.catch(() => false)) {
|
|
39
46
|
const content = await fs_1.default.promises.readFile(filePath, 'utf-8');
|
|
40
47
|
contextFiles[file] = content;
|
|
41
48
|
}
|
|
@@ -3,267 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const
|
|
7
|
-
const child_process_1 = require("child_process");
|
|
8
|
-
const cli_highlight_1 = __importDefault(require("cli-highlight"));
|
|
9
|
-
const prompts_1 = require("@inquirer/prompts");
|
|
10
|
-
const git_utils_1 = require("../../utils/git_utils");
|
|
11
|
-
const config_utils_1 = require("../../utils/config_utils");
|
|
12
|
-
const string_utils_1 = require("../../utils/string_utils");
|
|
13
|
-
const cloving_gpt_1 = __importDefault(require("../../cloving_gpt"));
|
|
14
|
-
const extractChangedFiles = (gitDiff) => {
|
|
15
|
-
const fileRegex = /diff --git a\/(.+?) b\/(.+?)\n/g;
|
|
16
|
-
const files = new Set();
|
|
17
|
-
let match;
|
|
18
|
-
while ((match = fileRegex.exec(gitDiff)) !== null) {
|
|
19
|
-
files.add(match[1]);
|
|
20
|
-
}
|
|
21
|
-
return Array.from(files);
|
|
22
|
-
};
|
|
23
|
-
const generatePrompt = (files, srcFiles, testFiles, gitDiff) => {
|
|
24
|
-
const filesList = files.length > 0 ? files.join('\n') : 'No files provided';
|
|
25
|
-
return `Enumerate all the files in the provided list${gitDiff ? ' and git diff' : ''} as well as the file names of anything that these files interact with.
|
|
26
|
-
Also, list any test files that might be relevant to these files.
|
|
27
|
-
|
|
28
|
-
### List of Code Files
|
|
29
|
-
|
|
30
|
-
${filesList}
|
|
31
|
-
|
|
32
|
-
### List of All Source Files
|
|
33
|
-
|
|
34
|
-
${srcFiles}
|
|
35
|
-
|
|
36
|
-
### List of all test files
|
|
37
|
-
|
|
38
|
-
${testFiles}
|
|
39
|
-
|
|
40
|
-
${gitDiff ? '### Git Diff Content' : ''}
|
|
41
|
-
|
|
42
|
-
${gitDiff || ''}
|
|
43
|
-
|
|
44
|
-
## Example Output
|
|
45
|
-
|
|
46
|
-
Here are the files that are relevant:
|
|
47
|
-
|
|
48
|
-
\`\`\`plaintext
|
|
49
|
-
## files
|
|
50
|
-
|
|
51
|
-
app/models/foo.rb
|
|
52
|
-
app/views/baz/bar.html.erb
|
|
53
|
-
|
|
54
|
-
## relevant test files
|
|
55
|
-
|
|
56
|
-
test/models/foo_test.rb
|
|
57
|
-
test/controllers/baz_controller_test.rb
|
|
58
|
-
\`\`\`
|
|
59
|
-
|
|
60
|
-
### Prompt
|
|
61
|
-
|
|
62
|
-
Enumerate all the files in the provided list${gitDiff ? ' and git diff' : ''} as well as the file names of anything that these files interact with.
|
|
63
|
-
Also, list any test files that might be relevant to these files.`;
|
|
64
|
-
};
|
|
65
|
-
const handleUserAction = async (analysis, autoSave = false) => {
|
|
66
|
-
const blocks = (0, string_utils_1.extractCurrentNewBlocks)(analysis);
|
|
67
|
-
if (autoSave) {
|
|
68
|
-
await (0, string_utils_1.applyAndSaveCurrentNewBlocks)(blocks);
|
|
69
|
-
console.log('All unit test files have been saved.');
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const files = Array.from(new Set(blocks.map(block => block.filePath)));
|
|
73
|
-
const action = await (0, prompts_1.select)({
|
|
74
|
-
message: 'What would you like to do?',
|
|
75
|
-
choices: [
|
|
76
|
-
{ name: 'Save a Unit Test File', value: 'save' },
|
|
77
|
-
{ name: 'Save All Unit Test Files', value: 'saveAll' },
|
|
78
|
-
{ name: 'Copy Entire Response to Clipboard', value: 'copyAll' },
|
|
79
|
-
{ name: 'Done', value: 'done' },
|
|
80
|
-
],
|
|
81
|
-
});
|
|
82
|
-
switch (action) {
|
|
83
|
-
case 'save':
|
|
84
|
-
let saveAnother = true;
|
|
85
|
-
while (saveAnother) {
|
|
86
|
-
const fileToSave = await (0, prompts_1.select)({
|
|
87
|
-
message: 'Which unit test file do you want to save?',
|
|
88
|
-
choices: files.map(file => ({ name: file, value: file })),
|
|
89
|
-
});
|
|
90
|
-
await (0, string_utils_1.applyAndSaveCurrentNewBlocks)(blocks.filter(block => block.filePath === fileToSave));
|
|
91
|
-
saveAnother = await (0, prompts_1.confirm)({
|
|
92
|
-
message: 'Do you want to save another file?',
|
|
93
|
-
default: false,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
break;
|
|
97
|
-
case 'saveAll':
|
|
98
|
-
await (0, string_utils_1.applyAndSaveCurrentNewBlocks)(blocks);
|
|
99
|
-
console.log('All unit test files have been saved.');
|
|
100
|
-
break;
|
|
101
|
-
case 'copyAll':
|
|
102
|
-
process.nextTick(() => {
|
|
103
|
-
require('copy-paste').copy(analysis, () => {
|
|
104
|
-
console.log('Entire response copied to clipboard');
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
break;
|
|
108
|
-
case 'done':
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
};
|
|
6
|
+
const UnitTestManager_1 = __importDefault(require("../../managers/UnitTestManager"));
|
|
112
7
|
const unitTests = async (options) => {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
const allSrcFiles = await (0, config_utils_1.getAllFiles)(options, true);
|
|
116
|
-
const testingDirectory = (0, config_utils_1.getTestingDirectory)();
|
|
117
|
-
if (allSrcFiles.length === 0 || !testingDirectory) {
|
|
118
|
-
console.error('Could not find any source files to generate unit tests. Please run: cloving init. Then try again.');
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
const testFiles = (0, child_process_1.execFileSync)('find', [testingDirectory, '-type', 'f']).toString().trim();
|
|
122
|
-
let contextFiles;
|
|
123
|
-
if (files) {
|
|
124
|
-
console.log('Generating unit tests for the provided files...');
|
|
125
|
-
console.log('Gathering a list of files related to the changes...');
|
|
126
|
-
const prompt = generatePrompt(files, allSrcFiles.join('\n'), testFiles);
|
|
127
|
-
contextFiles = await gpt.generateText({ prompt });
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
console.log('Generating unit tests for the git diff between this branch and the default branch...');
|
|
131
|
-
console.log('Gathering a list of files related to the changes...');
|
|
132
|
-
const gitDiff = await (0, git_utils_1.getGitDiff)();
|
|
133
|
-
const changedFiles = extractChangedFiles(gitDiff);
|
|
134
|
-
const prompt = generatePrompt(changedFiles, allSrcFiles.join('\n'), testFiles, gitDiff);
|
|
135
|
-
contextFiles = await gpt.generateText({ prompt });
|
|
136
|
-
}
|
|
137
|
-
contextFiles = (0, string_utils_1.extractMarkdown)(contextFiles);
|
|
138
|
-
console.log(contextFiles);
|
|
139
|
-
console.log('\nBuilding the tests for these files...');
|
|
140
|
-
// Initialize variables
|
|
141
|
-
const lines = [];
|
|
142
|
-
const context = [];
|
|
143
|
-
// Read each line of the context files
|
|
144
|
-
contextFiles.split('\n').forEach((line) => {
|
|
145
|
-
if (line.trim()) {
|
|
146
|
-
lines.push(line.trim());
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
for (const codeFile of lines) {
|
|
150
|
-
if (await fs_1.promises.stat(codeFile).then(stat => stat.isFile()).catch(() => false)) {
|
|
151
|
-
const fileContents = await fs_1.promises.readFile(codeFile, 'utf-8');
|
|
152
|
-
context.push(`### Contents of **${codeFile}\n\n${fileContents.split('\n').map(line => ` ${line}`).join('\n')}**\n\n`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// Generate the message for unit test creation
|
|
156
|
-
let analysis = '';
|
|
157
|
-
if ((files || []).length > 0) {
|
|
158
|
-
const message = `Create unit tests for the List of Files. Always show filenames for the generated code.
|
|
159
|
-
|
|
160
|
-
## List of Files
|
|
161
|
-
${(files || []).join('\n')}
|
|
162
|
-
|
|
163
|
-
## Example Response Structure
|
|
164
|
-
|
|
165
|
-
Here are the unit tests for the provided list of files:
|
|
166
|
-
|
|
167
|
-
1. **tests/utils/model_utils.test.ts**
|
|
168
|
-
|
|
169
|
-
\`\`\`typescript
|
|
170
|
-
import { getModel } from '../../src/utils/model_utils'
|
|
171
|
-
|
|
172
|
-
describe('modelUtils', () => {
|
|
173
|
-
const originalEnv = process.env
|
|
174
|
-
|
|
175
|
-
beforeEach(() => {
|
|
176
|
-
jest.resetModules()
|
|
177
|
-
process.env = { ...originalEnv }
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
afterAll(() => {
|
|
181
|
-
process.env = originalEnv
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
test('getModel should return default model when CLOVING_MODEL is not set', () => {
|
|
185
|
-
delete process.env.CLOVING_MODEL
|
|
186
|
-
const result = getModel()
|
|
187
|
-
expect(result).toBe('claude:claude-3-5-sonnet-20240620')
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
test('getModel should return CLOVING_MODEL when it is set', () => {
|
|
191
|
-
process.env.CLOVING_MODEL = 'openai:gpt-4'
|
|
192
|
-
const result = getModel()
|
|
193
|
-
expect(result).toBe('openai:gpt-4')
|
|
194
|
-
})
|
|
195
|
-
})
|
|
196
|
-
\`\`\`
|
|
197
|
-
|
|
198
|
-
## Context
|
|
199
|
-
|
|
200
|
-
${context.join('\n\n')}
|
|
201
|
-
|
|
202
|
-
## Prompt
|
|
203
|
-
|
|
204
|
-
Create unit tests for the List of Files. Always show filenames for the generated code.`;
|
|
205
|
-
// Get the model and analysis using ClovingGPT
|
|
206
|
-
analysis = await gpt.generateText({ prompt: message });
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
const message = `Create unit tests for the Code Diff. Always show filenames for the generated code.
|
|
210
|
-
|
|
211
|
-
## Code Diff
|
|
212
|
-
|
|
213
|
-
${await (0, git_utils_1.getGitDiff)()}
|
|
214
|
-
|
|
215
|
-
## Context
|
|
216
|
-
|
|
217
|
-
${context.join('\n\n')}
|
|
218
|
-
|
|
219
|
-
## Example Response Structure
|
|
220
|
-
|
|
221
|
-
Here are the unit tests for the code diff:
|
|
222
|
-
|
|
223
|
-
1. **tests/utils/model_utils.test.ts**
|
|
224
|
-
|
|
225
|
-
\`\`\`typescript
|
|
226
|
-
import { getModel } from '../../src/utils/model_utils'
|
|
227
|
-
|
|
228
|
-
describe('modelUtils', () => {
|
|
229
|
-
const originalEnv = process.env
|
|
230
|
-
|
|
231
|
-
beforeEach(() => {
|
|
232
|
-
jest.resetModules()
|
|
233
|
-
process.env = { ...originalEnv }
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
afterAll(() => {
|
|
237
|
-
process.env = originalEnv
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
test('getModel should return default model when CLOVING_MODEL is not set', () => {
|
|
241
|
-
delete process.env.CLOVING_MODEL
|
|
242
|
-
const result = getModel()
|
|
243
|
-
expect(result).toBe('claude:claude-3-5-sonnet-20240620')
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
test('getModel should return CLOVING_MODEL when it is set', () => {
|
|
247
|
-
process.env.CLOVING_MODEL = 'openai:gpt-4'
|
|
248
|
-
const result = getModel()
|
|
249
|
-
expect(result).toBe('openai:gpt-4')
|
|
250
|
-
})
|
|
251
|
-
})
|
|
252
|
-
\`\`\`
|
|
253
|
-
|
|
254
|
-
## Prompt
|
|
255
|
-
|
|
256
|
-
Create unit tests for the Code Diff. Always show filenames for the generated code.`;
|
|
257
|
-
// Get the model and analysis using ClovingGPT
|
|
258
|
-
analysis = await gpt.generateText({ prompt: message });
|
|
259
|
-
}
|
|
260
|
-
if (analysis) {
|
|
261
|
-
console.log((0, cli_highlight_1.default)(analysis));
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
console.log('No unit tests generated.');
|
|
265
|
-
}
|
|
266
|
-
// Add the new user action handling
|
|
267
|
-
await handleUserAction(analysis, save);
|
|
8
|
+
const unitTestManager = new UnitTestManager_1.default(options);
|
|
9
|
+
await unitTestManager.generateUnitTests();
|
|
268
10
|
};
|
|
269
11
|
exports.default = unitTests;
|