cloving 0.1.15 → 0.1.16
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 +40 -32
- package/dist/cloving_gpt/adapters/openai.js +1 -0
- package/dist/commands/generate/unit_tests.js +133 -28
- package/dist/index.js +1 -0
- package/dist/package.json +1 -1
- package/dist/utils/git_utils.js +3 -1
- package/dist/utils/string_utils.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
|
-
# Cloving CLI
|
|
1
|
+
# Cloving - CLI Code Generation with AI
|
|
2
2
|
|
|
3
|
-
Cloving is
|
|
3
|
+
Cloving CLI is a command line utility which integrates AI into your development workflow. It provides AI-powered code generation, commit message generation, code reviews, unit test generation, and more.
|
|
4
|
+
|
|
5
|
+
What makes the Cloving CLI different from other AI tools is that it builds sophisticated context-aware prompts to generate code, commit messages, and more.
|
|
6
|
+
|
|
7
|
+
Cloving gets to know your specific projects, the frameworks you use, the coding style you prefer, and the patterns you follow, and does its best to generate code that fits your existing project so that it actually works.
|
|
8
|
+
|
|
9
|
+
It delivers usable code that you can save directly into your project instead of copying and pasting code snippets from chat interactions.
|
|
10
|
+
|
|
11
|
+
**Read the [introduction announcement](https://cloving.ai/tutorials/introducing-the-cloving-cli).**
|
|
12
|
+
|
|
13
|
+
> [!WARNING]
|
|
14
|
+
>
|
|
15
|
+
> This project is still in the early stages of development and is not yet stable.
|
|
16
|
+
> Please use it with caution and [report any issues](https://github.com/cloving-ai/cloving-cli/issues) you encounter
|
|
4
17
|
|
|
5
18
|
## Features
|
|
6
19
|
|
|
20
|
+
- **Context-aware AI code generation**
|
|
7
21
|
- **AI-powered Git commit message generation**
|
|
8
22
|
- **Automated code reviews with AI insights**
|
|
9
23
|
- **AI-assisted unit test generation**
|
|
10
|
-
- **Seamless integration with existing development workflows**
|
|
11
|
-
- **Customizable AI prompts for different tasks**
|
|
12
|
-
- **Project initialization, planning, and building with AI assistance**
|
|
13
24
|
- **Support for multiple AI models including OpenAI, Claude, and Ollama**
|
|
14
25
|
|
|
26
|
+
## Important
|
|
27
|
+
|
|
28
|
+
The cloving will ask you before submitting any prompt to the AI model. You can use the `-s` or `--silent` flag to run the command without asking for confirmation, but by default you will have the chance to review all prompts with all the context before submitting them. This is to ensure that you are aware of what is being sent to the AI model and can make any necessary changes before submitting the prompt.
|
|
29
|
+
|
|
15
30
|
## Prerequisites
|
|
16
31
|
|
|
17
32
|
- Node.js (version 20 or higher)
|
|
@@ -91,13 +106,6 @@ Options:
|
|
|
91
106
|
- `-s, --silent`: Run without asking for confirmation of submitting prompts
|
|
92
107
|
- `-m, --model <model>`: Select the model to use
|
|
93
108
|
|
|
94
|
-
### Listing Available Models
|
|
95
|
-
|
|
96
|
-
List the available AI models:
|
|
97
|
-
```bash
|
|
98
|
-
cloving models
|
|
99
|
-
```
|
|
100
|
-
|
|
101
109
|
### Generating Commit Messages
|
|
102
110
|
|
|
103
111
|
Generate an AI-powered commit message:
|
|
@@ -112,17 +120,6 @@ Options:
|
|
|
112
120
|
- `-s, --silent`: Run without asking for confirmation
|
|
113
121
|
- `-m, --model <model>`: Select the model to use
|
|
114
122
|
|
|
115
|
-
### Generating Unit Tests
|
|
116
|
-
|
|
117
|
-
Generate unit tests for changes or specific files:
|
|
118
|
-
```bash
|
|
119
|
-
cloving generate unit-tests
|
|
120
|
-
```
|
|
121
|
-
Options:
|
|
122
|
-
- `-s, --silent`: Run without asking for confirmation
|
|
123
|
-
- `-f, --files <filenames...>`: Specify filenames for the unit tests
|
|
124
|
-
- `-m, --model <model>`: Select the model to use
|
|
125
|
-
|
|
126
123
|
### Code Reviews
|
|
127
124
|
|
|
128
125
|
Get an AI-powered code review:
|
|
@@ -147,15 +144,6 @@ Options:
|
|
|
147
144
|
- `-p, --prompt <prompt>`: Specify the prompt to use
|
|
148
145
|
- `-f, --files <filenames...>`: Specify filenames of files with context to use for generating code
|
|
149
146
|
|
|
150
|
-
### Generating Context
|
|
151
|
-
|
|
152
|
-
Create a context string for generating prompts outside of the cloving cli:
|
|
153
|
-
```bash
|
|
154
|
-
cloving generate context
|
|
155
|
-
```
|
|
156
|
-
Options:
|
|
157
|
-
- `-f, --files <filenames...>`: Specify filenames of files with context
|
|
158
|
-
|
|
159
147
|
### Generating Shell Commands
|
|
160
148
|
|
|
161
149
|
Generate a shell command based on a prompt:
|
|
@@ -171,6 +159,26 @@ Options:
|
|
|
171
159
|
- `-m, --model <model>`: Select the model to use
|
|
172
160
|
- `-p, --prompt <prompt>`: Specify the prompt to use
|
|
173
161
|
|
|
162
|
+
### Generating Context
|
|
163
|
+
|
|
164
|
+
Create a context string for generating prompts outside of the cloving cli:
|
|
165
|
+
```bash
|
|
166
|
+
cloving generate context
|
|
167
|
+
```
|
|
168
|
+
Options:
|
|
169
|
+
- `-f, --files <filenames...>`: Specify filenames of files with context
|
|
170
|
+
|
|
171
|
+
### Generating Unit Tests
|
|
172
|
+
|
|
173
|
+
Generate unit tests for changes or specific files:
|
|
174
|
+
```bash
|
|
175
|
+
cloving generate unit-tests
|
|
176
|
+
```
|
|
177
|
+
Options:
|
|
178
|
+
- `-s, --silent`: Run without asking for confirmation
|
|
179
|
+
- `-f, --files <filenames...>`: Specify filenames for the unit tests
|
|
180
|
+
- `-m, --model <model>`: Select the model to use
|
|
181
|
+
|
|
174
182
|
### Project Commands
|
|
175
183
|
|
|
176
184
|
Cloving now includes commands for managing entire projects:
|
|
@@ -23,7 +23,10 @@ const extractChangedFiles = (gitDiff) => {
|
|
|
23
23
|
};
|
|
24
24
|
const generatePrompt = (files, srcFiles, testFiles, gitDiff) => {
|
|
25
25
|
const filesList = files.length > 0 ? files.join('\n') : 'No files provided';
|
|
26
|
-
return
|
|
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
|
|
27
30
|
|
|
28
31
|
${filesList}
|
|
29
32
|
|
|
@@ -35,13 +38,13 @@ ${srcFiles}
|
|
|
35
38
|
|
|
36
39
|
${testFiles}
|
|
37
40
|
|
|
38
|
-
### Git Diff Content
|
|
41
|
+
${gitDiff ? '### Git Diff Content' : ''}
|
|
39
42
|
|
|
40
|
-
${gitDiff}
|
|
43
|
+
${gitDiff || ''}
|
|
41
44
|
|
|
42
45
|
## Example Output
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
Here are the files that are relevant:
|
|
45
48
|
|
|
46
49
|
\`\`\`plaintext
|
|
47
50
|
## files
|
|
@@ -55,14 +58,28 @@ test/models/foo_test.rb
|
|
|
55
58
|
test/controllers/baz_controller_test.rb
|
|
56
59
|
\`\`\`
|
|
57
60
|
|
|
58
|
-
###
|
|
59
|
-
|
|
60
|
-
Please 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.
|
|
61
|
+
### Prompt
|
|
61
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.
|
|
62
64
|
Also, list any test files that might be relevant to these files.`;
|
|
63
65
|
};
|
|
64
|
-
const handleUserAction = async (analysis) => {
|
|
66
|
+
const handleUserAction = async (analysis, autoSave = false) => {
|
|
65
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
|
+
}
|
|
66
83
|
const { action } = await inquirer_1.default.prompt([
|
|
67
84
|
{
|
|
68
85
|
type: 'list',
|
|
@@ -91,10 +108,9 @@ const handleUserAction = async (analysis) => {
|
|
|
91
108
|
]);
|
|
92
109
|
if (fileContents[fileToSave]) {
|
|
93
110
|
const filePath = path_1.default.resolve(fileToSave);
|
|
94
|
-
fs_1.promises.mkdir(path_1.default.dirname(filePath), { recursive: true })
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
.catch((error) => console.error(`Error saving ${fileToSave}:`, error));
|
|
111
|
+
await fs_1.promises.mkdir(path_1.default.dirname(filePath), { recursive: true });
|
|
112
|
+
await fs_1.promises.writeFile(filePath, fileContents[fileToSave]);
|
|
113
|
+
console.log(`${fileToSave} has been saved.`);
|
|
98
114
|
}
|
|
99
115
|
else {
|
|
100
116
|
console.log('File content not found.');
|
|
@@ -114,10 +130,9 @@ const handleUserAction = async (analysis) => {
|
|
|
114
130
|
for (const file of files) {
|
|
115
131
|
if (fileContents[file]) {
|
|
116
132
|
const filePath = path_1.default.resolve(file);
|
|
117
|
-
fs_1.promises.mkdir(path_1.default.dirname(filePath), { recursive: true })
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
.catch((error) => console.error(`Error saving ${file}:`, error));
|
|
133
|
+
await fs_1.promises.mkdir(path_1.default.dirname(filePath), { recursive: true });
|
|
134
|
+
await fs_1.promises.writeFile(filePath, fileContents[file]);
|
|
135
|
+
console.log(`${file} has been saved.`);
|
|
121
136
|
}
|
|
122
137
|
else {
|
|
123
138
|
console.log(`File content not found for ${file}.`);
|
|
@@ -157,7 +172,7 @@ const handleUserAction = async (analysis) => {
|
|
|
157
172
|
}
|
|
158
173
|
};
|
|
159
174
|
const unitTests = async (options) => {
|
|
160
|
-
const { files } = options;
|
|
175
|
+
const { files, save } = options;
|
|
161
176
|
const gpt = new cloving_gpt_1.default(options);
|
|
162
177
|
const allSrcFiles = await (0, config_utils_1.getAllFiles)(options, true);
|
|
163
178
|
const testingDirectory = (0, config_utils_1.getTestingDirectory)();
|
|
@@ -168,15 +183,22 @@ const unitTests = async (options) => {
|
|
|
168
183
|
const testFiles = (0, child_process_1.execFileSync)('find', [testingDirectory, '-type', 'f']).toString().trim();
|
|
169
184
|
let contextFiles;
|
|
170
185
|
if (files) {
|
|
186
|
+
console.log('Generating unit tests for the provided files...');
|
|
187
|
+
console.log('Gathering a list of files related to the changes...');
|
|
171
188
|
const prompt = generatePrompt(files, allSrcFiles.join('\n'), testFiles);
|
|
172
189
|
contextFiles = await gpt.generateText({ prompt });
|
|
173
190
|
}
|
|
174
191
|
else {
|
|
192
|
+
console.log('Generating unit tests for the git diff between this branch and the default branch...');
|
|
193
|
+
console.log('Gathering a list of files related to the changes...');
|
|
175
194
|
const gitDiff = await (0, git_utils_1.getGitDiff)();
|
|
176
195
|
const changedFiles = extractChangedFiles(gitDiff);
|
|
177
196
|
const prompt = generatePrompt(changedFiles, allSrcFiles.join('\n'), testFiles, gitDiff);
|
|
178
197
|
contextFiles = await gpt.generateText({ prompt });
|
|
179
198
|
}
|
|
199
|
+
contextFiles = (0, string_utils_1.extractMarkdown)(contextFiles);
|
|
200
|
+
console.log(contextFiles);
|
|
201
|
+
console.log('\nBuilding the tests for these files...');
|
|
180
202
|
// Initialize variables
|
|
181
203
|
const lines = [];
|
|
182
204
|
const context = [];
|
|
@@ -189,38 +211,121 @@ const unitTests = async (options) => {
|
|
|
189
211
|
for (const codeFile of lines) {
|
|
190
212
|
if (await fs_1.promises.stat(codeFile).then(stat => stat.isFile()).catch(() => false)) {
|
|
191
213
|
const fileContents = await fs_1.promises.readFile(codeFile, 'utf-8');
|
|
192
|
-
context.push(`### Contents of
|
|
214
|
+
context.push(`### Contents of **${codeFile}\n\n${fileContents.split('\n').map(line => ` ${line}`).join('\n')}**\n\n`);
|
|
193
215
|
}
|
|
194
216
|
}
|
|
195
217
|
// Generate the message for unit test creation
|
|
196
218
|
let analysis = '';
|
|
197
219
|
if ((files || []).length > 0) {
|
|
198
|
-
const message = `
|
|
220
|
+
const message = `Create unit tests for the List of Files. Always show filenames for the generated code.
|
|
199
221
|
|
|
200
|
-
##
|
|
222
|
+
## List of Files
|
|
201
223
|
${(files || []).join('\n')}
|
|
202
|
-
|
|
224
|
+
|
|
225
|
+
## Example Response Structure
|
|
226
|
+
|
|
227
|
+
Here are the unit tests for the provided list of files:
|
|
228
|
+
|
|
229
|
+
1. **tests/utils/model_utils.test.ts**
|
|
230
|
+
|
|
231
|
+
\`\`\`typescript
|
|
232
|
+
import { getModel } from '../../src/utils/model_utils'
|
|
233
|
+
|
|
234
|
+
describe('modelUtils', () => {
|
|
235
|
+
const originalEnv = process.env
|
|
236
|
+
|
|
237
|
+
beforeEach(() => {
|
|
238
|
+
jest.resetModules()
|
|
239
|
+
process.env = { ...originalEnv }
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
afterAll(() => {
|
|
243
|
+
process.env = originalEnv
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('getModel should return default model when CLOVING_MODEL is not set', () => {
|
|
247
|
+
delete process.env.CLOVING_MODEL
|
|
248
|
+
const result = getModel()
|
|
249
|
+
expect(result).toBe('claude:claude-3-5-sonnet-20240620')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test('getModel should return CLOVING_MODEL when it is set', () => {
|
|
253
|
+
process.env.CLOVING_MODEL = 'openai:gpt-4'
|
|
254
|
+
const result = getModel()
|
|
255
|
+
expect(result).toBe('openai:gpt-4')
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
\`\`\`
|
|
203
259
|
|
|
204
260
|
## Context
|
|
205
|
-
|
|
261
|
+
|
|
262
|
+
${context.join('\n\n')}
|
|
263
|
+
|
|
264
|
+
## Prompt
|
|
265
|
+
|
|
266
|
+
Create unit tests for the List of Files. Always show filenames for the generated code.`;
|
|
206
267
|
// Get the model and analysis using ClovingGPT
|
|
207
268
|
analysis = await gpt.generateText({ prompt: message });
|
|
208
269
|
}
|
|
209
270
|
else {
|
|
210
|
-
const message = `
|
|
271
|
+
const message = `Create unit tests for the Code Diff. Always show filenames for the generated code.
|
|
272
|
+
|
|
273
|
+
## Code Diff
|
|
211
274
|
|
|
212
|
-
## Begin diff
|
|
213
275
|
${await (0, git_utils_1.getGitDiff)()}
|
|
214
|
-
## End diff
|
|
215
276
|
|
|
216
277
|
## Context
|
|
217
278
|
|
|
218
|
-
${context.join('\n\n')}
|
|
279
|
+
${context.join('\n\n')}
|
|
280
|
+
|
|
281
|
+
## Example Response Structure
|
|
282
|
+
|
|
283
|
+
Here are the unit tests for the code diff:
|
|
284
|
+
|
|
285
|
+
1. **tests/utils/model_utils.test.ts**
|
|
286
|
+
|
|
287
|
+
\`\`\`typescript
|
|
288
|
+
import { getModel } from '../../src/utils/model_utils'
|
|
289
|
+
|
|
290
|
+
describe('modelUtils', () => {
|
|
291
|
+
const originalEnv = process.env
|
|
292
|
+
|
|
293
|
+
beforeEach(() => {
|
|
294
|
+
jest.resetModules()
|
|
295
|
+
process.env = { ...originalEnv }
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
afterAll(() => {
|
|
299
|
+
process.env = originalEnv
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test('getModel should return default model when CLOVING_MODEL is not set', () => {
|
|
303
|
+
delete process.env.CLOVING_MODEL
|
|
304
|
+
const result = getModel()
|
|
305
|
+
expect(result).toBe('claude:claude-3-5-sonnet-20240620')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('getModel should return CLOVING_MODEL when it is set', () => {
|
|
309
|
+
process.env.CLOVING_MODEL = 'openai:gpt-4'
|
|
310
|
+
const result = getModel()
|
|
311
|
+
expect(result).toBe('openai:gpt-4')
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
\`\`\`
|
|
315
|
+
|
|
316
|
+
## Prompt
|
|
317
|
+
|
|
318
|
+
Create unit tests for the Code Diff. Always show filenames for the generated code.`;
|
|
219
319
|
// Get the model and analysis using ClovingGPT
|
|
220
320
|
analysis = await gpt.generateText({ prompt: message });
|
|
221
321
|
}
|
|
222
|
-
|
|
322
|
+
if (analysis) {
|
|
323
|
+
console.log((0, cli_highlight_1.default)(analysis));
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
console.log('No unit tests generated.');
|
|
327
|
+
}
|
|
223
328
|
// Add the new user action handling
|
|
224
|
-
await handleUserAction(analysis);
|
|
329
|
+
await handleUserAction(analysis, save);
|
|
225
330
|
};
|
|
226
331
|
exports.default = unitTests;
|
package/dist/index.js
CHANGED
|
@@ -92,6 +92,7 @@ generate
|
|
|
92
92
|
.command('unit-tests')
|
|
93
93
|
.alias('u')
|
|
94
94
|
.description('Generate unit tests (if you don\'t specify filenames, it will generate tests for commited changes that differ from the main/master branch)')
|
|
95
|
+
.option('--save', 'Automatically save all files after generating the unit tests')
|
|
95
96
|
.option('-s, --silent', 'Run the command without asking for confirmation of submitting prompts')
|
|
96
97
|
.option('-f, --files <filenames...>', 'Specify filenames for the unit tests')
|
|
97
98
|
.option('-m, --model <model>', 'Select the model to use (e.g., openai, claude, ollama, ollama:llama3, claude:claude-3-5-sonnet-20240620)')
|
package/dist/package.json
CHANGED
package/dist/utils/git_utils.js
CHANGED
|
@@ -20,7 +20,9 @@ const getGitDiff = async () => {
|
|
|
20
20
|
try {
|
|
21
21
|
const defaultBranchName = await (0, exports.getDefaultBranchName)();
|
|
22
22
|
const gitDiff = (0, child_process_1.execSync)(`git diff ${defaultBranchName} --`).toString().trim();
|
|
23
|
-
|
|
23
|
+
// Indent each line by four spaces
|
|
24
|
+
const indentedDiff = gitDiff.split('\n').map(line => ` ${line}`).join('\n');
|
|
25
|
+
return indentedDiff;
|
|
24
26
|
}
|
|
25
27
|
catch (error) {
|
|
26
28
|
console.error('Error getting git diff:', error.message);
|
|
@@ -92,6 +92,8 @@ const parseMarkdownInstructions = (input) => {
|
|
|
92
92
|
};
|
|
93
93
|
exports.parseMarkdownInstructions = parseMarkdownInstructions;
|
|
94
94
|
const extractFilesAndContent = (rawCodeCommand) => {
|
|
95
|
+
if (!rawCodeCommand)
|
|
96
|
+
return [[], {}];
|
|
95
97
|
const files = [];
|
|
96
98
|
const fileContents = {};
|
|
97
99
|
const matches = rawCodeCommand.match(/(\*{2})([^\*]+)(\*{2})/g);
|
|
@@ -107,7 +109,7 @@ const extractFilesAndContent = (rawCodeCommand) => {
|
|
|
107
109
|
let content = contentMatch[1];
|
|
108
110
|
// Remove the first word after the opening triple backticks
|
|
109
111
|
content = content.split('\n').map((line, idx) => idx === 0 ? line.replace(/^\w+\s*/, '') : line).join('\n');
|
|
110
|
-
fileContents[fileName] = content;
|
|
112
|
+
fileContents[fileName] = content.trim();
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
return [files, fileContents];
|