cloving 0.1.14 → 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 CHANGED
@@ -1,17 +1,32 @@
1
- # Cloving
1
+ # Cloving - CLI Code Generation with AI
2
2
 
3
- Cloving is an innovative CLI tool designed to seamlessly integrate AI into various aspects of the developer workflow. It enhances productivity and code quality by leveraging AI for tasks such as generating commit messages, conducting code reviews, creating unit tests, and even assisting with entire project lifecycles.
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,25 +120,63 @@ 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
123
+ ### Code Reviews
116
124
 
117
- Generate unit tests for changes or specific files:
125
+ Get an AI-powered code review:
118
126
  ```bash
119
- cloving generate unit-tests
127
+ cloving generate review
120
128
  ```
121
129
  Options:
122
130
  - `-s, --silent`: Run without asking for confirmation
123
- - `-f, --files <filenames...>`: Specify filenames for the unit tests
124
131
  - `-m, --model <model>`: Select the model to use
125
132
 
126
- ### Code Reviews
133
+ ### Generating Code
127
134
 
128
- Get an AI-powered code review:
135
+ Generate code based on a prompt:
129
136
  ```bash
130
- cloving generate review
137
+ cloving generate code
138
+ ```
139
+ Options:
140
+ - `--save`: Automatically save all files after generating the code
141
+ - `-i, --interactive`: Auto-save generated code and then show a new prompt with existing context to revise the code further
142
+ - `-s, --silent`: Run the command without asking for confirmation of submitting prompts
143
+ - `-m, --model <model>`: Select the model to use
144
+ - `-p, --prompt <prompt>`: Specify the prompt to use
145
+ - `-f, --files <filenames...>`: Specify filenames of files with context to use for generating code
146
+
147
+ ### Generating Shell Commands
148
+
149
+ Generate a shell command based on a prompt:
150
+ ```bash
151
+ cloving generate shell
152
+ ```
153
+ or
154
+ ```bash
155
+ cloving generate sh
156
+ ```
157
+ Options:
158
+ - `-s, --silent`: Run without asking for confirmation of submitting prompts
159
+ - `-m, --model <model>`: Select the model to use
160
+ - `-p, --prompt <prompt>`: Specify the prompt to use
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
131
176
  ```
132
177
  Options:
133
178
  - `-s, --silent`: Run without asking for confirmation
179
+ - `-f, --files <filenames...>`: Specify filenames for the unit tests
134
180
  - `-m, --model <model>`: Select the model to use
135
181
 
136
182
  ### Project Commands
@@ -159,7 +205,7 @@ Cloving now includes commands for managing entire projects:
159
205
 
160
206
  ## Command Line Options
161
207
 
162
- Usage: `cloving [options] [command]`
208
+ Usage: `cloving [command] [options]`
163
209
 
164
210
  Options:
165
211
  - `-V, --version`: Output the version number
@@ -172,6 +218,9 @@ Commands:
172
218
  - `commit`: Alias for cloving generate commit
173
219
  - `generate`: Generate various items like unit-tests and code reviews
174
220
  - `commit`: Generate a commit message and commit the changes
221
+ - `code`: Generate code based on a prompt
222
+ - `context`: Generate context for building a prompt outside of cloving
223
+ - `shell`: Generate a shell command based on a prompt
175
224
  - `unit-tests`: Generate unit tests
176
225
  - `review`: Review the code for committed changes
177
226
  - `project`: Commands for managing Cloving projects
@@ -34,6 +34,7 @@ class OpenAIAdapter {
34
34
  exports.OpenAIAdapter = OpenAIAdapter;
35
35
  OpenAIAdapter.supportedModels = [
36
36
  'openai:gpt:4o',
37
+ 'openai:gpt:4o-mini',
37
38
  'openai:gpt:4-turbo',
38
39
  'openai:gpt:3.5-turbo',
39
40
  'openai:text-embedding:3-large',
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const inquirer_1 = __importDefault(require("inquirer"));
7
7
  const cli_highlight_1 = __importDefault(require("cli-highlight"));
8
8
  const copy_paste_1 = __importDefault(require("copy-paste"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
9
11
  const command_utils_1 = require("../../utils/command_utils");
10
12
  const config_utils_1 = require("../../utils/config_utils");
11
13
  const generateContextPrompt = (files, contextFiles) => {
@@ -25,24 +27,36 @@ ${contextFileContents}
25
27
  ${files.join('\n')}`;
26
28
  };
27
29
  const context = async (options) => {
30
+ let { files } = options;
28
31
  options.silent = (0, config_utils_1.getConfig)(options).globalSilent || false;
29
32
  const allSrcFiles = await (0, config_utils_1.getAllFiles)(options, false);
30
33
  let contextFiles = {};
31
34
  try {
32
- let includeMoreFiles = true;
33
- while (includeMoreFiles) {
34
- const { contextFile } = await inquirer_1.default.prompt([
35
- {
36
- type: 'input',
37
- name: 'contextFile',
38
- message: `Enter the relative path of a file or directory you would like to include as context (or press enter to continue):`,
39
- },
40
- ]);
41
- if (contextFile) {
42
- contextFiles = await (0, command_utils_1.addFileOrDirectoryToContext)(contextFile, contextFiles, options);
35
+ if (files) {
36
+ for (const file of files) {
37
+ const filePath = path_1.default.resolve(file);
38
+ if (await fs_1.default.promises.stat(filePath).then(stat => stat.isFile()).catch(() => false)) {
39
+ const content = await fs_1.default.promises.readFile(filePath, 'utf-8');
40
+ contextFiles[file] = content;
41
+ }
43
42
  }
44
- else {
45
- includeMoreFiles = false;
43
+ }
44
+ else {
45
+ let includeMoreFiles = true;
46
+ while (includeMoreFiles) {
47
+ const { contextFile } = await inquirer_1.default.prompt([
48
+ {
49
+ type: 'input',
50
+ name: 'contextFile',
51
+ message: `Enter the relative path of a file or directory you would like to include as context (or press enter to continue):`,
52
+ },
53
+ ]);
54
+ if (contextFile) {
55
+ contextFiles = await (0, command_utils_1.addFileOrDirectoryToContext)(contextFile, contextFiles, options);
56
+ }
57
+ else {
58
+ includeMoreFiles = false;
59
+ }
46
60
  }
47
61
  }
48
62
  const contextPrompt = generateContextPrompt(allSrcFiles, contextFiles);
@@ -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 `### List of Code Files
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
- Give me this output format for your answer:
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
- ### Request
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
- .then(() => fs_1.promises.writeFile(filePath, fileContents[fileToSave]))
96
- .then(() => console.log(`${fileToSave} has been saved.`))
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
- .then(() => fs_1.promises.writeFile(filePath, fileContents[file]))
119
- .then(() => console.log(`${file} has been saved.`))
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 ${codeFile}\n\n${fileContents}\n\n### End of ${codeFile}`);
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 = `please create unit tests for these files:
220
+ const message = `Create unit tests for the List of Files. Always show filenames for the generated code.
199
221
 
200
- ## Begin list of files
222
+ ## List of Files
201
223
  ${(files || []).join('\n')}
202
- ## End list of files
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
- ${context.join('\n\n')}`;
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 = `please create unit tests for these changes:
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
- console.log((0, cli_highlight_1.default)(analysis));
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
@@ -86,11 +86,13 @@ generate
86
86
  generate
87
87
  .command('context')
88
88
  .description('Create a context string for generating code')
89
+ .option('-f, --files <filenames...>', 'Specify filenames of files with context')
89
90
  .action(context_1.default);
90
91
  generate
91
92
  .command('unit-tests')
92
93
  .alias('u')
93
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')
94
96
  .option('-s, --silent', 'Run the command without asking for confirmation of submitting prompts')
95
97
  .option('-f, --files <filenames...>', 'Specify filenames for the unit tests')
96
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloving",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {
@@ -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
- return gitDiff;
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];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloving",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {