cloving 0.1.0 → 0.1.1-8.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/README.md +168 -40
- package/dist/cloving_gpt/adapters/claude.js +6 -8
- package/dist/cloving_gpt/adapters/gemini.js +62 -0
- package/dist/cloving_gpt/adapters/mistral.js +46 -0
- package/dist/cloving_gpt/adapters/openai.js +7 -6
- package/dist/cloving_gpt/index.js +64 -63
- package/dist/commands/commit.js +2 -2
- package/dist/commands/config.js +86 -19
- package/dist/commands/generate/code.js +337 -0
- package/dist/commands/generate/commit.js +69 -0
- package/dist/commands/generate/context.js +75 -0
- package/dist/commands/generate/fizzbuzz.js +35 -0
- package/dist/commands/generate/review.js +146 -0
- package/dist/commands/generate/sh.js +95 -0
- package/dist/commands/generate/shell.js +111 -0
- package/dist/commands/generate/unit_tests.js +314 -0
- package/dist/commands/init.js +86 -33
- package/dist/commands/models.js +5 -2
- package/dist/commands/project/build.js +88 -0
- package/dist/commands/project/complete.js +12 -0
- package/dist/commands/project/init.js +82 -0
- package/dist/commands/project/plan.js +108 -0
- package/dist/commands/review.js +2 -2
- package/dist/commands/unit_tests.js +108 -77
- package/dist/index.js +111 -21
- package/dist/package.json +76 -0
- package/dist/utils/command_utils.js +36 -70
- package/dist/utils/config_utils.js +170 -0
- package/dist/utils/git_utils.js +37 -44
- package/dist/utils/string_utils.js +72 -18
- package/package.json +28 -17
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const cli_highlight_1 = __importDefault(require("cli-highlight"));
|
|
7
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
8
|
+
const copy_paste_1 = __importDefault(require("copy-paste"));
|
|
9
|
+
const cloving_gpt_1 = __importDefault(require("../../cloving_gpt"));
|
|
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 review = async (options) => {
|
|
14
|
+
options.silent = (0, config_utils_1.getConfig)(options).globalSilent || false;
|
|
15
|
+
const gpt = new cloving_gpt_1.default(options);
|
|
16
|
+
try {
|
|
17
|
+
// Define the prompt for analysis
|
|
18
|
+
const gitDiff = await (0, git_utils_1.getGitDiff)();
|
|
19
|
+
const promptText = `==== begin diff =====
|
|
20
|
+
${gitDiff}
|
|
21
|
+
==== end diff =====
|
|
22
|
+
|
|
23
|
+
### Example of a well-structured response
|
|
24
|
+
|
|
25
|
+
# Code Review: Enhanced Code Generation and File Handling
|
|
26
|
+
|
|
27
|
+
## Changes Overview
|
|
28
|
+
|
|
29
|
+
These changes primarily focus on improving the code generation process and adding more robust file handling capabilities. The main modifications include:
|
|
30
|
+
|
|
31
|
+
1. Enhancing the code generation prompt with a structured example response.
|
|
32
|
+
2. Implementing a new function to extract file names and contents from the generated code.
|
|
33
|
+
3. Adding a new option to save generated code directly to files.
|
|
34
|
+
4. Improving the copy-to-clipboard functionality to allow copying individual files.
|
|
35
|
+
5. Removing an unused import in the review.ts file.
|
|
36
|
+
|
|
37
|
+
## Reason for Changes
|
|
38
|
+
|
|
39
|
+
The changes aim to enhance the usability and flexibility of the code generation tool by providing clearer prompts, better file management, and more control over the generated code.
|
|
40
|
+
|
|
41
|
+
## Detailed Description
|
|
42
|
+
|
|
43
|
+
1. **Enhanced Code Generation Prompt** - The \`generateCodePrompt\` function now includes a structured example of a well-formatted response. This helps guide the AI to produce more consistent and usable output, making it easier to parse and use the generated code.
|
|
44
|
+
2. **File Extraction Function** - A new function \`extractFilesAndContent\` has been added to parse the raw code command and extract individual file names and their contents. This enables more granular control over the generated code, allowing for file-specific operations.
|
|
45
|
+
3. **Save to File Option** - A new 'Save Source Code to Files' option has been added to the user action menu. This feature allows users to save generated code directly to files in the appropriate directory structure.
|
|
46
|
+
4. **Improved Copy to Clipboard** - The 'Copy Source Code to Clipboard' option has been enhanced to allow users to choose specific files to copy, rather than copying all generated code at once.
|
|
47
|
+
5. **Removed Unused Import** - The unused import of \`execFileSync\` from 'child_process' has been removed from the review.ts file, improving code cleanliness.
|
|
48
|
+
|
|
49
|
+
## Potential Bugs and Recommended Fixes
|
|
50
|
+
|
|
51
|
+
### 1. Possible undefined access in \`extractFilesAndContent\`
|
|
52
|
+
|
|
53
|
+
\`\`\`typescript
|
|
54
|
+
const contentMatch = regex.exec(rawCodeCommand)
|
|
55
|
+
if (contentMatch) {
|
|
56
|
+
files.push(fileName)
|
|
57
|
+
let content = contentMatch[1]
|
|
58
|
+
// ...
|
|
59
|
+
}
|
|
60
|
+
\`\`\`
|
|
61
|
+
|
|
62
|
+
There's a potential for \`contentMatch[1]\` to be undefined if the regex doesn't capture any groups. To fix this, add a null check:
|
|
63
|
+
|
|
64
|
+
\`\`\`typescript
|
|
65
|
+
const contentMatch = regex.exec(rawCodeCommand)
|
|
66
|
+
if (contentMatch && contentMatch[1]) {
|
|
67
|
+
files.push(fileName)
|
|
68
|
+
let content = contentMatch[1]
|
|
69
|
+
// ...
|
|
70
|
+
}
|
|
71
|
+
\`\`\`
|
|
72
|
+
|
|
73
|
+
### Request
|
|
74
|
+
|
|
75
|
+
Do not use any data from the example response structure, only use the structure.
|
|
76
|
+
I would like you to explain why these change are being made and document a description of these changes.
|
|
77
|
+
Also list any bugs in the new code as well as recommended fixes for those bugs with code examples.
|
|
78
|
+
Format the output of this code review in Markdown format.`;
|
|
79
|
+
// get the analysis
|
|
80
|
+
const analysis = await gpt.generateText({ prompt: promptText });
|
|
81
|
+
// Print the analysis to the console
|
|
82
|
+
(0, string_utils_1.parseMarkdownInstructions)(analysis).map(code => {
|
|
83
|
+
if (code.trim().startsWith('```')) {
|
|
84
|
+
const lines = code.split('\n');
|
|
85
|
+
const language = code.match(/```(\w+)/)?.[1] || 'plaintext';
|
|
86
|
+
console.log(lines[0]);
|
|
87
|
+
try {
|
|
88
|
+
console.log((0, cli_highlight_1.default)(lines.slice(1, -1).join('\n'), { language }));
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
// don't highlight if it fails
|
|
92
|
+
console.log(lines.slice(1, -1).join('\n'));
|
|
93
|
+
}
|
|
94
|
+
console.log(lines.slice(-1)[0]);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.log((0, cli_highlight_1.default)(code, { language: 'markdown' }));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
// Modified Function to extract specific sections from the analysis
|
|
101
|
+
const extractSection = (section) => {
|
|
102
|
+
const regex = new RegExp(`## ${section}[\\s\\S]*?(?=\\n## Potential Bugs and Recommended Fixes|$)`, 'g');
|
|
103
|
+
const match = analysis.match(regex);
|
|
104
|
+
return match ? match[0].trim() : '';
|
|
105
|
+
};
|
|
106
|
+
// Prompt the user with options for copying to clipboard
|
|
107
|
+
const clipboardOption = await (0, prompts_1.select)({
|
|
108
|
+
message: 'What would you like to copy to the clipboard?',
|
|
109
|
+
choices: [
|
|
110
|
+
{ name: 'Copy only the Changes Overview', value: 'Changes Overview' },
|
|
111
|
+
{ name: 'Copy only the Potential Bugs and Recommended Fixes', value: 'Potential Bugs and Recommended Fixes' },
|
|
112
|
+
{ name: 'Copy everything', value: 'Everything' },
|
|
113
|
+
{ name: 'Done', value: 'Done' }
|
|
114
|
+
]
|
|
115
|
+
});
|
|
116
|
+
let contentToCopy = '';
|
|
117
|
+
switch (clipboardOption) {
|
|
118
|
+
case 'Changes Overview':
|
|
119
|
+
contentToCopy = extractSection('Changes Overview');
|
|
120
|
+
break;
|
|
121
|
+
case 'Potential Bugs and Recommended Fixes':
|
|
122
|
+
contentToCopy = extractSection('Potential Bugs and Recommended Fixes');
|
|
123
|
+
break;
|
|
124
|
+
case 'Everything':
|
|
125
|
+
contentToCopy = analysis;
|
|
126
|
+
break;
|
|
127
|
+
case 'Done':
|
|
128
|
+
console.log('No content copied to clipboard.');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (contentToCopy) {
|
|
132
|
+
copy_paste_1.default.copy(contentToCopy, (err) => {
|
|
133
|
+
if (err) {
|
|
134
|
+
console.error('Error: Unable to copy to clipboard.', err);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
console.log('Selected content copied to clipboard');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
console.error('Error during analysis:', error.message);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
exports.default = review;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
7
|
+
const cli_highlight_1 = __importDefault(require("cli-highlight"));
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const string_utils_1 = require("../../utils/string_utils");
|
|
10
|
+
const command_utils_1 = require("../../utils/command_utils");
|
|
11
|
+
const config_utils_1 = require("../../utils/config_utils");
|
|
12
|
+
const cloving_gpt_1 = __importDefault(require("../../cloving_gpt"));
|
|
13
|
+
const generateShellPrompt = (prompt) => {
|
|
14
|
+
const shell = (0, child_process_1.execSync)('echo $SHELL').toString().trim();
|
|
15
|
+
const os = (0, child_process_1.execSync)('echo $OSTYPE').toString().trim();
|
|
16
|
+
return `Generate an executable ${shell} script that works on ${os}. Try to make it a single line if possible and as simple and straightforward as possible.
|
|
17
|
+
|
|
18
|
+
Do not add any commentary or context to the message other than the commit message itself.
|
|
19
|
+
|
|
20
|
+
An example of the output for this should look like the following:
|
|
21
|
+
|
|
22
|
+
\`\`\`sh
|
|
23
|
+
find . -type f -name "*.ts" -exec sed -i '' 's/old/new/g' {} +
|
|
24
|
+
\`\`\`
|
|
25
|
+
|
|
26
|
+
Don't use that script, it is only an example.
|
|
27
|
+
|
|
28
|
+
Here is the description of what I want the script to do:
|
|
29
|
+
|
|
30
|
+
${prompt}`;
|
|
31
|
+
};
|
|
32
|
+
const generateExplainShellPrompt = (prompt) => {
|
|
33
|
+
return `${prompt}
|
|
34
|
+
|
|
35
|
+
Please briefly explain how this shell script works.`;
|
|
36
|
+
};
|
|
37
|
+
const sh = async (options) => {
|
|
38
|
+
let { prompt } = options;
|
|
39
|
+
options.silent = (0, config_utils_1.getConfig)(options).globalSilent || false;
|
|
40
|
+
const gpt = new cloving_gpt_1.default(options);
|
|
41
|
+
try {
|
|
42
|
+
if (!prompt) {
|
|
43
|
+
prompt = await (0, command_utils_1.promptUser)('What would you like to do: ');
|
|
44
|
+
}
|
|
45
|
+
// Generate the prompt for commit message
|
|
46
|
+
const shellPrompt = generateShellPrompt(prompt);
|
|
47
|
+
// Instantiate ClovingGPT and get the commit message
|
|
48
|
+
const rawShellCommand = await gpt.generateText({ prompt: shellPrompt });
|
|
49
|
+
// Clean the commit message using extractMarkdown
|
|
50
|
+
const generatedShell = (0, string_utils_1.extractMarkdown)(rawShellCommand);
|
|
51
|
+
// remove #! from the generated shell script
|
|
52
|
+
const generatedShellWithoutShebang = generatedShell.replace(/^#!.*?\s/, '');
|
|
53
|
+
console.log((0, cli_highlight_1.default)(generatedShellWithoutShebang));
|
|
54
|
+
// Inquirer prompt for further actions
|
|
55
|
+
const { action } = await inquirer_1.default.prompt([
|
|
56
|
+
{
|
|
57
|
+
type: 'list',
|
|
58
|
+
name: 'action',
|
|
59
|
+
message: 'What would you like to do?',
|
|
60
|
+
choices: [
|
|
61
|
+
{ name: 'Execute', value: 'execute' },
|
|
62
|
+
{ name: 'Revise', value: 'revise' },
|
|
63
|
+
{ name: 'Explain', value: 'explain' },
|
|
64
|
+
{ name: 'Copy to Clipboard', value: 'copy' },
|
|
65
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
switch (action) {
|
|
70
|
+
case 'execute':
|
|
71
|
+
(0, child_process_1.execSync)(generatedShellWithoutShebang, { stdio: 'inherit' });
|
|
72
|
+
break;
|
|
73
|
+
case 'revise':
|
|
74
|
+
// Logic to revise the prompt (not implemented in this example)
|
|
75
|
+
console.log('Revise option selected. Implement revision logic here.');
|
|
76
|
+
break;
|
|
77
|
+
case 'explain':
|
|
78
|
+
const explainPrompt = generateExplainShellPrompt(generatedShellWithoutShebang);
|
|
79
|
+
const explainShellCommand = await gpt.generateText({ prompt: explainPrompt });
|
|
80
|
+
console.log((0, cli_highlight_1.default)(explainShellCommand, { language: 'markdown' }));
|
|
81
|
+
break;
|
|
82
|
+
case 'copy':
|
|
83
|
+
(0, child_process_1.execFileSync)('pbcopy', { input: generatedShellWithoutShebang });
|
|
84
|
+
console.log('Script copied to clipboard.');
|
|
85
|
+
break;
|
|
86
|
+
case 'cancel':
|
|
87
|
+
console.log('Operation cancelled.');
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error('Could not generate shell script');
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
exports.default = sh;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
const cli_highlight_1 = __importDefault(require("cli-highlight"));
|
|
8
|
+
const copy_paste_1 = __importDefault(require("copy-paste"));
|
|
9
|
+
const string_utils_1 = require("../../utils/string_utils");
|
|
10
|
+
const config_utils_1 = require("../../utils/config_utils");
|
|
11
|
+
const cloving_gpt_1 = __importDefault(require("../../cloving_gpt"));
|
|
12
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
13
|
+
const generateShellPrompt = (prompt) => {
|
|
14
|
+
const shell = (0, child_process_1.execSync)('echo $SHELL').toString().trim();
|
|
15
|
+
const os = (0, child_process_1.execSync)('echo $OSTYPE').toString().trim();
|
|
16
|
+
return `Generate an executable ${shell} script that works on ${os}. Try to make it a single line if possible and as simple and straightforward as possible.
|
|
17
|
+
|
|
18
|
+
Do not add any commentary or context to the message other than the commit message itself.
|
|
19
|
+
|
|
20
|
+
An example of the output for this should look like the following:
|
|
21
|
+
|
|
22
|
+
\`\`\`sh
|
|
23
|
+
find . -type f -name "*.ts" -exec sed -i '' 's/old/new/g' {} +
|
|
24
|
+
\`\`\`
|
|
25
|
+
|
|
26
|
+
Don't use that script, it is only an example.
|
|
27
|
+
|
|
28
|
+
Here is the description of what I want the script to do:
|
|
29
|
+
|
|
30
|
+
${prompt}`;
|
|
31
|
+
};
|
|
32
|
+
const generateExplainShellPrompt = (prompt) => {
|
|
33
|
+
return `${prompt}
|
|
34
|
+
|
|
35
|
+
## Task
|
|
36
|
+
|
|
37
|
+
Please briefly explain how this shell script works.`;
|
|
38
|
+
};
|
|
39
|
+
const generateShell = async (gpt, prompt) => {
|
|
40
|
+
const shellPrompt = generateShellPrompt(prompt);
|
|
41
|
+
return await gpt.generateText({ prompt: shellPrompt });
|
|
42
|
+
};
|
|
43
|
+
const displayGeneratedShell = (rawShellCommand) => {
|
|
44
|
+
const generatedShell = (0, string_utils_1.extractMarkdown)(rawShellCommand);
|
|
45
|
+
const generatedShellWithoutShebang = generatedShell.replace(/^#!.*?\s/, '');
|
|
46
|
+
console.log((0, cli_highlight_1.default)(generatedShellWithoutShebang));
|
|
47
|
+
};
|
|
48
|
+
const handleUserAction = async (gpt, rawShellCommand, prompt) => {
|
|
49
|
+
const generatedShell = (0, string_utils_1.extractMarkdown)(rawShellCommand);
|
|
50
|
+
const generatedShellWithoutShebang = generatedShell.replace(/^#!.*?\s/, '');
|
|
51
|
+
const action = await (0, prompts_1.select)({
|
|
52
|
+
message: 'What would you like to do?',
|
|
53
|
+
choices: [
|
|
54
|
+
{ name: 'Execute', value: 'execute' },
|
|
55
|
+
{ name: 'Revise', value: 'revise' },
|
|
56
|
+
{ name: 'Explain', value: 'explain' },
|
|
57
|
+
{ name: 'Copy to Clipboard', value: 'copy' },
|
|
58
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
switch (action) {
|
|
62
|
+
case 'execute':
|
|
63
|
+
(0, child_process_1.execSync)(generatedShellWithoutShebang, { stdio: 'inherit' });
|
|
64
|
+
break;
|
|
65
|
+
case 'revise':
|
|
66
|
+
const newPrompt = await (0, prompts_1.input)({
|
|
67
|
+
message: 'How would you like to modify the output:',
|
|
68
|
+
});
|
|
69
|
+
const newRawShellCommand = await generateShell(gpt, newPrompt);
|
|
70
|
+
displayGeneratedShell(newRawShellCommand);
|
|
71
|
+
await handleUserAction(gpt, newRawShellCommand, newPrompt);
|
|
72
|
+
break;
|
|
73
|
+
case 'explain':
|
|
74
|
+
const explainPrompt = generateExplainShellPrompt(generatedShellWithoutShebang);
|
|
75
|
+
const explainShellCommand = await gpt.generateText({ prompt: explainPrompt });
|
|
76
|
+
console.log((0, cli_highlight_1.default)(explainShellCommand, { language: 'markdown' }));
|
|
77
|
+
break;
|
|
78
|
+
case 'copy':
|
|
79
|
+
copy_paste_1.default.copy(generatedShellWithoutShebang, (err) => {
|
|
80
|
+
if (err) {
|
|
81
|
+
console.error('Error: Unable to copy to clipboard.', err);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log('Script copied to clipboard.');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
break;
|
|
88
|
+
case 'cancel':
|
|
89
|
+
console.log('Operation cancelled.');
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const shell = async (options) => {
|
|
94
|
+
let { prompt: userPrompt } = options;
|
|
95
|
+
options.silent = (0, config_utils_1.getConfig)(options).globalSilent || false;
|
|
96
|
+
const gpt = new cloving_gpt_1.default(options);
|
|
97
|
+
try {
|
|
98
|
+
if (!userPrompt) {
|
|
99
|
+
userPrompt = await (0, prompts_1.input)({
|
|
100
|
+
message: 'What would you like to do: '
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const rawShellCommand = await generateShell(gpt, userPrompt);
|
|
104
|
+
displayGeneratedShell(rawShellCommand);
|
|
105
|
+
await handleUserAction(gpt, rawShellCommand, userPrompt);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
console.error('Could not generate shell script', error);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
exports.default = shell;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = require("fs");
|
|
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 path_1 = __importDefault(require("path"));
|
|
11
|
+
const git_utils_1 = require("../../utils/git_utils");
|
|
12
|
+
const config_utils_1 = require("../../utils/config_utils");
|
|
13
|
+
const string_utils_1 = require("../../utils/string_utils");
|
|
14
|
+
const cloving_gpt_1 = __importDefault(require("../../cloving_gpt"));
|
|
15
|
+
const extractChangedFiles = (gitDiff) => {
|
|
16
|
+
const fileRegex = /diff --git a\/(.+?) b\/(.+?)\n/g;
|
|
17
|
+
const files = new Set();
|
|
18
|
+
let match;
|
|
19
|
+
while ((match = fileRegex.exec(gitDiff)) !== null) {
|
|
20
|
+
files.add(match[1]);
|
|
21
|
+
}
|
|
22
|
+
return Array.from(files);
|
|
23
|
+
};
|
|
24
|
+
const generatePrompt = (files, srcFiles, testFiles, gitDiff) => {
|
|
25
|
+
const filesList = files.length > 0 ? files.join('\n') : 'No files provided';
|
|
26
|
+
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.
|
|
27
|
+
Also, list any test files that might be relevant to these files.
|
|
28
|
+
|
|
29
|
+
### List of Code Files
|
|
30
|
+
|
|
31
|
+
${filesList}
|
|
32
|
+
|
|
33
|
+
### List of All Source Files
|
|
34
|
+
|
|
35
|
+
${srcFiles}
|
|
36
|
+
|
|
37
|
+
### List of all test files
|
|
38
|
+
|
|
39
|
+
${testFiles}
|
|
40
|
+
|
|
41
|
+
${gitDiff ? '### Git Diff Content' : ''}
|
|
42
|
+
|
|
43
|
+
${gitDiff || ''}
|
|
44
|
+
|
|
45
|
+
## Example Output
|
|
46
|
+
|
|
47
|
+
Here are the files that are relevant:
|
|
48
|
+
|
|
49
|
+
\`\`\`plaintext
|
|
50
|
+
## files
|
|
51
|
+
|
|
52
|
+
app/models/foo.rb
|
|
53
|
+
app/views/baz/bar.html.erb
|
|
54
|
+
|
|
55
|
+
## relevant test files
|
|
56
|
+
|
|
57
|
+
test/models/foo_test.rb
|
|
58
|
+
test/controllers/baz_controller_test.rb
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
### Prompt
|
|
62
|
+
|
|
63
|
+
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.
|
|
64
|
+
Also, list any test files that might be relevant to these files.`;
|
|
65
|
+
};
|
|
66
|
+
const handleUserAction = async (analysis, autoSave = false) => {
|
|
67
|
+
const [files, fileContents] = (0, string_utils_1.extractFilesAndContent)(analysis);
|
|
68
|
+
if (autoSave) {
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
if (fileContents[file]) {
|
|
71
|
+
const filePath = path_1.default.resolve(file);
|
|
72
|
+
await fs_1.promises.mkdir(path_1.default.dirname(filePath), { recursive: true });
|
|
73
|
+
await fs_1.promises.writeFile(filePath, fileContents[file]);
|
|
74
|
+
console.log(`${file} has been saved.`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log(`File content not found for ${file}.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
console.log('All unit test files have been saved.');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const action = await (0, prompts_1.select)({
|
|
84
|
+
message: 'What would you like to do?',
|
|
85
|
+
choices: [
|
|
86
|
+
{ name: 'Save a Unit Test File', value: 'save' },
|
|
87
|
+
{ name: 'Save All Unit Test Files', value: 'saveAll' },
|
|
88
|
+
{ name: 'Copy Unit Test to Clipboard', value: 'copyTest' },
|
|
89
|
+
{ name: 'Copy Entire Response to Clipboard', value: 'copyAll' },
|
|
90
|
+
{ name: 'Done', value: 'done' },
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
switch (action) {
|
|
94
|
+
case 'save':
|
|
95
|
+
let saveAnother = true;
|
|
96
|
+
while (saveAnother) {
|
|
97
|
+
const fileToSave = await (0, prompts_1.select)({
|
|
98
|
+
message: 'Which unit test file do you want to save?',
|
|
99
|
+
choices: files.map(file => ({ name: file, value: file })),
|
|
100
|
+
});
|
|
101
|
+
if (fileContents[fileToSave]) {
|
|
102
|
+
const filePath = path_1.default.resolve(fileToSave);
|
|
103
|
+
await fs_1.promises.mkdir(path_1.default.dirname(filePath), { recursive: true });
|
|
104
|
+
await fs_1.promises.writeFile(filePath, fileContents[fileToSave]);
|
|
105
|
+
console.log(`${fileToSave} has been saved.`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.log('File content not found.');
|
|
109
|
+
}
|
|
110
|
+
saveAnother = await (0, prompts_1.confirm)({
|
|
111
|
+
message: 'Do you want to save another file?',
|
|
112
|
+
default: false,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
case 'saveAll':
|
|
117
|
+
for (const file of files) {
|
|
118
|
+
if (fileContents[file]) {
|
|
119
|
+
const filePath = path_1.default.resolve(file);
|
|
120
|
+
await fs_1.promises.mkdir(path_1.default.dirname(filePath), { recursive: true });
|
|
121
|
+
await fs_1.promises.writeFile(filePath, fileContents[file]);
|
|
122
|
+
console.log(`${file} has been saved.`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
console.log(`File content not found for ${file}.`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
console.log('All unit test files have been saved.');
|
|
129
|
+
break;
|
|
130
|
+
case 'copyTest':
|
|
131
|
+
const fileToCopy = await (0, prompts_1.select)({
|
|
132
|
+
message: 'Which unit test file do you want to copy to the clipboard?',
|
|
133
|
+
choices: files.map(file => ({ name: file, value: file })),
|
|
134
|
+
});
|
|
135
|
+
if (fileContents[fileToCopy]) {
|
|
136
|
+
process.nextTick(() => {
|
|
137
|
+
require('copy-paste').copy(fileContents[fileToCopy], () => {
|
|
138
|
+
console.log(`${fileToCopy} copied to clipboard`);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
console.log('File content not found.');
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
case 'copyAll':
|
|
147
|
+
process.nextTick(() => {
|
|
148
|
+
require('copy-paste').copy(analysis, () => {
|
|
149
|
+
console.log('Entire response copied to clipboard');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
break;
|
|
153
|
+
case 'done':
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const unitTests = async (options) => {
|
|
158
|
+
const { files, save } = options;
|
|
159
|
+
const gpt = new cloving_gpt_1.default(options);
|
|
160
|
+
const allSrcFiles = await (0, config_utils_1.getAllFiles)(options, true);
|
|
161
|
+
const testingDirectory = (0, config_utils_1.getTestingDirectory)();
|
|
162
|
+
if (allSrcFiles.length === 0 || !testingDirectory) {
|
|
163
|
+
console.error('Could not find any source files to generate unit tests. Please run: cloving init. Then try again.');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
const testFiles = (0, child_process_1.execFileSync)('find', [testingDirectory, '-type', 'f']).toString().trim();
|
|
167
|
+
let contextFiles;
|
|
168
|
+
if (files) {
|
|
169
|
+
console.log('Generating unit tests for the provided files...');
|
|
170
|
+
console.log('Gathering a list of files related to the changes...');
|
|
171
|
+
const prompt = generatePrompt(files, allSrcFiles.join('\n'), testFiles);
|
|
172
|
+
contextFiles = await gpt.generateText({ prompt });
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.log('Generating unit tests for the git diff between this branch and the default branch...');
|
|
176
|
+
console.log('Gathering a list of files related to the changes...');
|
|
177
|
+
const gitDiff = await (0, git_utils_1.getGitDiff)();
|
|
178
|
+
const changedFiles = extractChangedFiles(gitDiff);
|
|
179
|
+
const prompt = generatePrompt(changedFiles, allSrcFiles.join('\n'), testFiles, gitDiff);
|
|
180
|
+
contextFiles = await gpt.generateText({ prompt });
|
|
181
|
+
}
|
|
182
|
+
contextFiles = (0, string_utils_1.extractMarkdown)(contextFiles);
|
|
183
|
+
console.log(contextFiles);
|
|
184
|
+
console.log('\nBuilding the tests for these files...');
|
|
185
|
+
// Initialize variables
|
|
186
|
+
const lines = [];
|
|
187
|
+
const context = [];
|
|
188
|
+
// Read each line of the context files
|
|
189
|
+
contextFiles.split('\n').forEach((line) => {
|
|
190
|
+
if (line.trim()) {
|
|
191
|
+
lines.push(line.trim());
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
for (const codeFile of lines) {
|
|
195
|
+
if (await fs_1.promises.stat(codeFile).then(stat => stat.isFile()).catch(() => false)) {
|
|
196
|
+
const fileContents = await fs_1.promises.readFile(codeFile, 'utf-8');
|
|
197
|
+
context.push(`### Contents of **${codeFile}\n\n${fileContents.split('\n').map(line => ` ${line}`).join('\n')}**\n\n`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Generate the message for unit test creation
|
|
201
|
+
let analysis = '';
|
|
202
|
+
if ((files || []).length > 0) {
|
|
203
|
+
const message = `Create unit tests for the List of Files. Always show filenames for the generated code.
|
|
204
|
+
|
|
205
|
+
## List of Files
|
|
206
|
+
${(files || []).join('\n')}
|
|
207
|
+
|
|
208
|
+
## Example Response Structure
|
|
209
|
+
|
|
210
|
+
Here are the unit tests for the provided list of files:
|
|
211
|
+
|
|
212
|
+
1. **tests/utils/model_utils.test.ts**
|
|
213
|
+
|
|
214
|
+
\`\`\`typescript
|
|
215
|
+
import { getModel } from '../../src/utils/model_utils'
|
|
216
|
+
|
|
217
|
+
describe('modelUtils', () => {
|
|
218
|
+
const originalEnv = process.env
|
|
219
|
+
|
|
220
|
+
beforeEach(() => {
|
|
221
|
+
jest.resetModules()
|
|
222
|
+
process.env = { ...originalEnv }
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
afterAll(() => {
|
|
226
|
+
process.env = originalEnv
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('getModel should return default model when CLOVING_MODEL is not set', () => {
|
|
230
|
+
delete process.env.CLOVING_MODEL
|
|
231
|
+
const result = getModel()
|
|
232
|
+
expect(result).toBe('claude:claude-3-5-sonnet-20240620')
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('getModel should return CLOVING_MODEL when it is set', () => {
|
|
236
|
+
process.env.CLOVING_MODEL = 'openai:gpt-4'
|
|
237
|
+
const result = getModel()
|
|
238
|
+
expect(result).toBe('openai:gpt-4')
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
\`\`\`
|
|
242
|
+
|
|
243
|
+
## Context
|
|
244
|
+
|
|
245
|
+
${context.join('\n\n')}
|
|
246
|
+
|
|
247
|
+
## Prompt
|
|
248
|
+
|
|
249
|
+
Create unit tests for the List of Files. Always show filenames for the generated code.`;
|
|
250
|
+
// Get the model and analysis using ClovingGPT
|
|
251
|
+
analysis = await gpt.generateText({ prompt: message });
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const message = `Create unit tests for the Code Diff. Always show filenames for the generated code.
|
|
255
|
+
|
|
256
|
+
## Code Diff
|
|
257
|
+
|
|
258
|
+
${await (0, git_utils_1.getGitDiff)()}
|
|
259
|
+
|
|
260
|
+
## Context
|
|
261
|
+
|
|
262
|
+
${context.join('\n\n')}
|
|
263
|
+
|
|
264
|
+
## Example Response Structure
|
|
265
|
+
|
|
266
|
+
Here are the unit tests for the code diff:
|
|
267
|
+
|
|
268
|
+
1. **tests/utils/model_utils.test.ts**
|
|
269
|
+
|
|
270
|
+
\`\`\`typescript
|
|
271
|
+
import { getModel } from '../../src/utils/model_utils'
|
|
272
|
+
|
|
273
|
+
describe('modelUtils', () => {
|
|
274
|
+
const originalEnv = process.env
|
|
275
|
+
|
|
276
|
+
beforeEach(() => {
|
|
277
|
+
jest.resetModules()
|
|
278
|
+
process.env = { ...originalEnv }
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
afterAll(() => {
|
|
282
|
+
process.env = originalEnv
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('getModel should return default model when CLOVING_MODEL is not set', () => {
|
|
286
|
+
delete process.env.CLOVING_MODEL
|
|
287
|
+
const result = getModel()
|
|
288
|
+
expect(result).toBe('claude:claude-3-5-sonnet-20240620')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test('getModel should return CLOVING_MODEL when it is set', () => {
|
|
292
|
+
process.env.CLOVING_MODEL = 'openai:gpt-4'
|
|
293
|
+
const result = getModel()
|
|
294
|
+
expect(result).toBe('openai:gpt-4')
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
\`\`\`
|
|
298
|
+
|
|
299
|
+
## Prompt
|
|
300
|
+
|
|
301
|
+
Create unit tests for the Code Diff. Always show filenames for the generated code.`;
|
|
302
|
+
// Get the model and analysis using ClovingGPT
|
|
303
|
+
analysis = await gpt.generateText({ prompt: message });
|
|
304
|
+
}
|
|
305
|
+
if (analysis) {
|
|
306
|
+
console.log((0, cli_highlight_1.default)(analysis));
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
console.log('No unit tests generated.');
|
|
310
|
+
}
|
|
311
|
+
// Add the new user action handling
|
|
312
|
+
await handleUserAction(analysis, save);
|
|
313
|
+
};
|
|
314
|
+
exports.default = unitTests;
|