cloving 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,12 +3,13 @@ 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
- exports.addFileOrDirectoryToContext = exports.readFileContent = exports.fetchModels = exports.runCommand = exports.generateFileList = exports.getAllFilesInDirectory = exports.checkForSpecialFiles = exports.collectSpecialFileContents = exports.SPECIAL_FILES = void 0;
7
- const child_process_1 = require("child_process");
6
+ exports.addFileOrDirectoryToContext = exports.readFileContent = exports.fetchModels = exports.runCommand = exports.generateFileList = exports.getAllFilesInDirectory = exports.checkForSpecialFiles = exports.collectSpecialFileContents = exports.generateShellPrompt = exports.generateCodegenPrompt = exports.SPECIAL_FILES = void 0;
8
7
  const fs_1 = __importDefault(require("fs"));
9
8
  const path_1 = __importDefault(require("path"));
10
- const config_utils_1 = require("./config_utils");
9
+ const child_process_1 = require("child_process");
11
10
  const isbinaryfile_1 = require("isbinaryfile");
11
+ const colors_1 = __importDefault(require("colors"));
12
+ const config_utils_1 = require("./config_utils");
12
13
  exports.SPECIAL_FILES = [
13
14
  'package.json', 'Gemfile', 'requirements.txt', 'Pipfile', 'pyproject.toml', 'pom.xml', 'build.gradle',
14
15
  '.csproj', 'packages.config', 'composer.json', 'CMakeLists.txt', 'conanfile.txt', 'conanfile.py',
@@ -16,6 +17,164 @@ exports.SPECIAL_FILES = [
16
17
  'DESCRIPTION', 'mix.exs', 'build.sbt', 'pubspec.yaml', 'stack.yaml', 'cabal.project', 'Project.toml',
17
18
  'rockspec', 'rebar.config', 'project.clj', 'tsconfig.json'
18
19
  ];
20
+ const generateCodegenPrompt = (contextFilesContent) => {
21
+ const instructions = `## AI Code Generation Instructions
22
+
23
+ ### General Guidelines
24
+
25
+ 1. **Respond as an expert software developer.**
26
+ 2. **Follow best practices and the latest standards found in the **Description of App** section.**
27
+ 3. **Adhere to existing conventions and libraries in the codebase.**
28
+
29
+ ### Request Handling
30
+
31
+ 1. **Try to understand the request, but if anything is unclear, ask questions.**
32
+ 2. **Decide if you need to propose edits to existing files not in the chat.**
33
+ - If yes, provide the full path names and ask the user to add them.
34
+ - Wait for user approval before proceeding.
35
+ 3. **Propose changes using *CURRENT/NEW* Blocks.**
36
+ 4. **Show the smallest possible *CURRENT* section that uniquely identifies the code.**
37
+ 5. **To move code, use two *CURRENT/NEW* blocks: one to remove and one to add.**
38
+ 6. **For new files or to replace an existing file, the *CURRENT* block is empty.**
39
+
40
+ ### *CURRENT/NEW* Block Format
41
+
42
+ 1. **Start a block with three backticks and the language name.**
43
+ \`\`\`typescript
44
+ 2. **Next line: seven <, CURRENT, and the file path.**
45
+ <<<<<<< CURRENT path/to/file.ts
46
+ 3. **Include the exact existing code to be changed.**
47
+ 4. **Divide with seven =.**
48
+ =======
49
+ 5. **Add the new code.**
50
+ 6. **End with seven > and NEW.**
51
+ >>>>>>> NEW
52
+ 7. **Close the block with three backticks.**
53
+ \`\`\``;
54
+ const specialFileContents = (0, exports.collectSpecialFileContents)();
55
+ const specialFiles = Object.keys(specialFileContents).map((file) => `### Contents of **${file}**\n\n\`\`\`\n${JSON.stringify(specialFileContents[file], null, 2)}\n\`\`\`\n\n`).join('\n');
56
+ const contextFileContents = Object.keys(contextFilesContent).map((file) => `### Contents of **${file}**\n\n\`\`\`\n${contextFilesContent[file]}\n\`\`\`\n\n`).join('\n');
57
+ const prompt = `${instructions}
58
+
59
+ ## Description of App
60
+
61
+ ${JSON.stringify((0, config_utils_1.getClovingConfig)(), null, 2)}
62
+
63
+ ## Special Files
64
+
65
+ ${specialFiles.length > 0 ? specialFiles : 'No special files provided.'}
66
+
67
+ ## Context Files
68
+
69
+ ${contextFileContents.length > 0 ? contextFileContents : 'No context files provided.'}
70
+
71
+ ## Directory structure
72
+
73
+ ${Object.keys(contextFilesContent).join('\n')}
74
+
75
+ ${instructions}
76
+
77
+ ### Example 1: Changing a Variable
78
+
79
+ \`\`\`typescript
80
+ <<<<<<< CURRENT path/to/file.ts
81
+ const myVariable = 'exact'
82
+ const oldVariable = 1
83
+ =======
84
+ const myVariable = 'exact'
85
+ const newVariable = 2
86
+ >>>>>>> NEW
87
+ \`\`\`
88
+
89
+ ### Example 2: Create a New File
90
+
91
+ \`\`\`ruby
92
+ <<<<<<< CURRENT path/to/newfile.rb
93
+ =======
94
+ def new_function
95
+ puts 'Hello, world!'
96
+ end
97
+ >>>>>>> NEW
98
+ \`\`\`
99
+
100
+ ### Example 3: Moving a Function
101
+
102
+ Removing it from path/to/file.py
103
+
104
+ \`\`\`python
105
+ <<<<<<< CURRENT path/to/file.py
106
+ def old_function():
107
+ return 'Old function'
108
+ =======
109
+ >>>>>>> NEW
110
+ \`\`\`
111
+
112
+ Adding it to path/to/newfile.py
113
+
114
+ \`\`\`python
115
+ <<<<<<< CURRENT path/to/newfile.py
116
+ def existing_function():
117
+ return 'Existing function'
118
+
119
+ =======
120
+ def existing_function():
121
+ return 'Existing function'
122
+
123
+ def old_function():
124
+ return 'Old function'
125
+ >>>>>>> NEW
126
+ \`\`\`
127
+
128
+ ### Example 4: Removing a Function
129
+
130
+ \`\`\`java
131
+ <<<<<<< CURRENT path/to/file.java
132
+ public void oldFunction() {
133
+ System.out.println("Old function")
134
+ }
135
+ =======
136
+ >>>>>>> NEW
137
+ \`\`\`
138
+
139
+ ### Example 5: Replace a file
140
+
141
+ This will replace the entire contents of **path/to/file.rb** with the new content:
142
+
143
+ \`\`\`ruby
144
+ <<<<<<< CURRENT path/to/file.rb
145
+ =======
146
+ def new_function
147
+ puts 'Hello, world!'
148
+ end
149
+ >>>>>>> NEW
150
+ \`\`\`
151
+
152
+ ## Don't Invent Code That Isn't Provided in Context
153
+
154
+ A file might be referenced by just the file name without the full path, but the code must be provided in the *Context Files* section.
155
+
156
+ Unless you believe you are creating a whole new file, never ever under any circumstances make up code in the CURRENT block that was not provided to you in the *Context Files* section.
157
+
158
+ If a required file hasn't been provided in the *Context Files* section, stop everything and ask the user to provide it in the context by adding -f path/to/file to the command or add path/to/file in the interactive chat.`;
159
+ return prompt;
160
+ };
161
+ exports.generateCodegenPrompt = generateCodegenPrompt;
162
+ const generateShellPrompt = () => {
163
+ const shell = (0, child_process_1.execSync)('echo $SHELL').toString().trim();
164
+ const os = (0, child_process_1.execSync)('echo $OSTYPE').toString().trim();
165
+ 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.
166
+
167
+ Do not add any commentary or context to the message other than the commit message itself.
168
+
169
+ An example of the output for this should look like the following:
170
+
171
+ \`\`\`sh
172
+ find . -type f -name "*.ts" -exec sed -i '' 's/old/new/g' {} +
173
+ \`\`\`
174
+
175
+ Don't use that script, it is only an example.`;
176
+ };
177
+ exports.generateShellPrompt = generateShellPrompt;
19
178
  const collectSpecialFileContents = () => {
20
179
  const specialFileContents = {};
21
180
  for (const file of exports.SPECIAL_FILES) {
@@ -110,27 +269,43 @@ const readFileContent = (file) => {
110
269
  }
111
270
  };
112
271
  exports.readFileContent = readFileContent;
113
- const addFileOrDirectoryToContext = async (contextFile, contextFiles, options) => {
114
- const filePath = path_1.default.resolve(contextFile);
115
- if (await fs_1.default.promises.stat(filePath).then(stat => stat.isDirectory()).catch(() => false)) {
116
- // Add all files in the specified directory
117
- const files = await (0, config_utils_1.getAllFiles)(options, false);
118
- for (const file of files) {
119
- // if the file is in the same directory as the context file, add it to the context
120
- if (path_1.default.dirname(file) === path_1.default.dirname(filePath)) {
121
- const content = await fs_1.default.promises.readFile(file, 'utf-8');
122
- contextFiles[path_1.default.relative(process.cwd(), file)] = content;
272
+ const addFileToContext = async (filePath, contextFiles, baseDir) => {
273
+ const relativePath = path_1.default.relative(baseDir, filePath);
274
+ if (await (0, isbinaryfile_1.isBinaryFile)(filePath)) {
275
+ return;
276
+ }
277
+ const content = await fs_1.default.promises.readFile(filePath, 'utf-8');
278
+ contextFiles[relativePath] = content;
279
+ };
280
+ const addDirectoryToContext = async (dirPath, contextFiles, baseDir) => {
281
+ const entries = await fs_1.default.promises.readdir(dirPath, { withFileTypes: true });
282
+ for (const entry of entries) {
283
+ const fullPath = path_1.default.join(dirPath, entry.name);
284
+ if (entry.isDirectory()) {
285
+ if (entry.name !== 'node_modules' && entry.name !== '.git') {
286
+ await addDirectoryToContext(fullPath, contextFiles, baseDir);
123
287
  }
124
288
  }
125
- console.log(`Added contents of ${contextFile} to context.`);
289
+ else {
290
+ await addFileToContext(fullPath, contextFiles, baseDir);
291
+ }
126
292
  }
127
- else if (await fs_1.default.promises.stat(filePath).then(stat => stat.isFile()).catch(() => false)) {
128
- const content = await fs_1.default.promises.readFile(filePath, 'utf-8');
129
- contextFiles[contextFile] = content;
130
- console.log(`Added ${filePath} to context.`);
293
+ };
294
+ const addFileOrDirectoryToContext = async (contextFile, contextFiles, options) => {
295
+ const filePath = path_1.default.resolve(contextFile);
296
+ const baseDir = process.cwd();
297
+ try {
298
+ const stats = await fs_1.default.promises.stat(filePath);
299
+ if (stats.isDirectory()) {
300
+ await addDirectoryToContext(filePath, contextFiles, baseDir);
301
+ }
302
+ else if (stats.isFile()) {
303
+ await addFileToContext(filePath, contextFiles, baseDir);
304
+ }
131
305
  }
132
- else {
133
- console.log(`File or directory ${contextFile} does not exist.`);
306
+ catch (error) {
307
+ console.error(`${colors_1.default.bold('Error')}: File or directory "${contextFile}" does not exist`);
308
+ process.exit(1);
134
309
  }
135
310
  return contextFiles;
136
311
  };
@@ -3,9 +3,13 @@ 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
- exports.saveGeneratedFiles = exports.extractFilesAndContent = exports.parseMarkdownInstructions = exports.extractMarkdown = exports.extractJsonMetadata = exports.estimateTokens = void 0;
6
+ exports.applyAndSaveCurrentNewBlocks = exports.extractCurrentNewBlocks = exports.parseMarkdownInstructions = exports.extractMarkdown = exports.extractJsonMetadata = exports.estimateTokens = void 0;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
+ const colors_1 = __importDefault(require("colors"));
10
+ const BLOCK_START = '\n<<<<<<< CURRENT';
11
+ const BLOCK_DIVIDER = '\n=======';
12
+ const BLOCK_END = '\n>>>>>>> NEW';
9
13
  // Function to estimate token count
10
14
  const estimateTokens = async (text) => {
11
15
  const charCount = text.length;
@@ -13,7 +17,33 @@ const estimateTokens = async (text) => {
13
17
  return tokenCount;
14
18
  };
15
19
  exports.estimateTokens = estimateTokens;
16
- // Function to extract JSON metadata from the AI response
20
+ /**
21
+ * Extracts JSON metadata from an AI response string.
22
+ *
23
+ * This function attempts to extract a valid JSON string from the given response.
24
+ * It handles responses that may contain the JSON within a code block or as plain text.
25
+ *
26
+ * The function performs the following steps:
27
+ * 1. Looks for a ```json code block and extracts its content if found.
28
+ * 2. If no code block is found, it treats the entire response as potential JSON.
29
+ * 3. Trims the extracted string to remove leading/trailing whitespace.
30
+ * 4. Removes any text before the first '{' and after the last '}'.
31
+ *
32
+ * @param {string} response - The AI response string that may contain JSON metadata.
33
+ * @returns {string} A string containing only the JSON part of the response.
34
+ * If no valid JSON structure is found, it may return an empty string
35
+ * or a partial JSON string.
36
+ *
37
+ * @example
38
+ * const aiResponse = '```json\n{"key": "value"}\n```\nOther text';
39
+ * const jsonMetadata = extractJsonMetadata(aiResponse);
40
+ * // jsonMetadata will be '{"key": "value"}'
41
+ *
42
+ * @example
43
+ * const plainResponse = 'Some text {"key": "value"} more text';
44
+ * const jsonMetadata = extractJsonMetadata(plainResponse);
45
+ * // jsonMetadata will be '{"key": "value"}'
46
+ */
17
47
  const extractJsonMetadata = (response) => {
18
48
  let jsonString;
19
49
  // Extract the ```json block from the response
@@ -96,70 +126,202 @@ const parseMarkdownInstructions = (input) => {
96
126
  return result;
97
127
  };
98
128
  exports.parseMarkdownInstructions = parseMarkdownInstructions;
99
- const extractFilesAndContent = (rawCodeCommand) => {
100
- if (!rawCodeCommand)
101
- return {};
102
- const files = [];
103
- const fileContents = {};
129
+ const findBlockIndices = (input, startIndex) => {
130
+ const blockStart = input.indexOf(BLOCK_START, startIndex);
131
+ if (blockStart === -1)
132
+ return null;
133
+ const filePathStart = blockStart + BLOCK_START.length;
134
+ const filePathEnd = input.indexOf('\n', filePathStart);
135
+ const dividerIndex = input.indexOf(BLOCK_DIVIDER, filePathEnd);
136
+ if (dividerIndex === -1)
137
+ return null;
138
+ const blockEnd = input.indexOf(BLOCK_END, dividerIndex);
139
+ if (blockEnd === -1)
140
+ return null;
141
+ return {
142
+ start: blockStart,
143
+ filePathEnd,
144
+ divider: dividerIndex,
145
+ end: blockEnd
146
+ };
147
+ };
148
+ const extractBlock = (input, indices) => {
149
+ const filePath = input.slice(indices.start + BLOCK_START.length, indices.filePathEnd).trim();
150
+ const currentContent = input.slice(indices.filePathEnd + 1, indices.divider).trim();
151
+ const newContent = input.slice(indices.divider + BLOCK_DIVIDER.length, indices.end).trim();
152
+ return { filePath, currentContent, newContent };
153
+ };
154
+ /**
155
+ * Extracts CURRENT/NEW blocks from the given input string.
156
+ *
157
+ * This function parses the input string to find and extract all CURRENT/NEW blocks.
158
+ * Each block contains information about file paths, current content, and new content.
159
+ *
160
+ * The blocks are defined by specific delimiters:
161
+ * - Start: '<<<<<<< CURRENT'
162
+ * - Divider: '======='
163
+ * - End: '>>>>>>> NEW'
164
+ *
165
+ * @param {string} [input] - The input string containing CURRENT/NEW blocks.
166
+ * @returns {CurrentNewBlock[]} An array of CurrentNewBlock objects, each representing a parsed block.
167
+ * If the input is undefined or empty, an empty array is returned.
168
+ *
169
+ * @example
170
+ * const input = `
171
+ * <<<<<<< CURRENT path/to/file.ts
172
+ * const oldCode = 'old';
173
+ * =======
174
+ * const newCode = 'new';
175
+ * >>>>>>> NEW
176
+ * `;
177
+ * const blocks = extractCurrentNewBlocks(input);
178
+ * // blocks will contain one CurrentNewBlock object with the parsed information
179
+ */
180
+ const extractCurrentNewBlocks = (input) => {
181
+ if (!input)
182
+ return [];
183
+ const blocks = [];
104
184
  let currentIndex = 0;
105
- const commandLength = rawCodeCommand.length;
106
- while (currentIndex < commandLength) {
107
- let fileNameStart = rawCodeCommand.indexOf('**', currentIndex);
108
- if (fileNameStart === -1)
109
- break;
110
- let fileNameEnd = rawCodeCommand.indexOf('**', fileNameStart + 2);
111
- if (fileNameEnd === -1)
112
- break;
113
- let nextBoldStart = rawCodeCommand.indexOf('**', fileNameEnd + 2);
114
- let nextCodeStart = rawCodeCommand.indexOf('\n```', fileNameEnd + 2);
115
- while (nextBoldStart !== -1 && nextCodeStart > nextBoldStart) {
116
- fileNameStart = nextBoldStart;
117
- fileNameEnd = rawCodeCommand.indexOf('**', fileNameStart + 2);
118
- nextBoldStart = rawCodeCommand.indexOf('**', fileNameEnd + 2);
119
- nextCodeStart = rawCodeCommand.indexOf('\n```', fileNameEnd + 2);
120
- }
121
- const fileName = rawCodeCommand.slice(fileNameStart + 2, fileNameEnd).trim();
122
- files.push(fileName);
123
- const codeBlockStart = rawCodeCommand.indexOf('\n```', fileNameEnd);
124
- if (codeBlockStart === -1)
125
- break;
126
- let codeBlockEnd = codeBlockStart + 4;
127
- while (codeBlockEnd < commandLength) {
128
- if (rawCodeCommand.slice(codeBlockEnd, codeBlockEnd + 5) === '\n```\n') {
129
- break;
130
- }
131
- else if (rawCodeCommand.slice(codeBlockEnd, codeBlockEnd + 4) === '\n```' && codeBlockEnd + 4 === commandLength) {
132
- break;
133
- }
134
- codeBlockEnd++;
135
- }
136
- if (codeBlockEnd >= commandLength)
185
+ while (currentIndex < input.length) {
186
+ const indices = findBlockIndices(input, currentIndex);
187
+ if (!indices)
137
188
  break;
138
- let content = rawCodeCommand.slice(codeBlockStart + 4, codeBlockEnd).trim();
139
- const firstNewlineIndex = content.indexOf('\n');
140
- if (firstNewlineIndex !== -1) {
141
- content = content.slice(firstNewlineIndex + 1);
142
- }
143
- else {
144
- content = content.replace(/^\w+\s*/, '');
189
+ blocks.push(extractBlock(input, indices));
190
+ currentIndex = indices.end + BLOCK_END.length;
191
+ }
192
+ return blocks;
193
+ };
194
+ exports.extractCurrentNewBlocks = extractCurrentNewBlocks;
195
+ const readFileContent = async (filePath) => {
196
+ try {
197
+ return await fs_1.default.promises.readFile(filePath, 'utf-8');
198
+ }
199
+ catch (error) {
200
+ if (error.code === 'ENOENT') {
201
+ return '';
145
202
  }
146
- fileContents[fileName] = content.trim();
147
- currentIndex = codeBlockEnd + 5;
203
+ throw error;
204
+ }
205
+ };
206
+ const ensureDirectoryExists = async (filePath) => {
207
+ await fs_1.default.promises.mkdir(path_1.default.dirname(filePath), { recursive: true });
208
+ };
209
+ const writeFileContent = async (filePath, content) => {
210
+ await ensureDirectoryExists(filePath);
211
+ await fs_1.default.promises.writeFile(filePath, content);
212
+ };
213
+ /**
214
+ * Updates the content of a file by replacing a specified block of current content with new content.
215
+ *
216
+ * This function searches for the `currentContent` within the `currentContent` of the file.
217
+ * If found, it replaces it with the `newContent` from the `block`, preserving the indentation
218
+ * level of the original content.
219
+ *
220
+ * @param {string} currentContent - The existing content of the file.
221
+ * @param {CurrentNewBlock} block - An object containing the current and new content to be replaced.
222
+ * @returns {string} The updated file content with the specified block replaced.
223
+ *
224
+ * @example
225
+ * const currentContent = `
226
+ * function example() {
227
+ * console.log('Old content');
228
+ * }
229
+ * `;
230
+ * const block = {
231
+ * currentContent: "console.log('Old content');",
232
+ * newContent: "console.log('New content');"
233
+ * };
234
+ * const updatedContent = updateFileContent(currentContent, block);
235
+ * // updatedContent will be:
236
+ * // function example() {
237
+ * // console.log('New content');
238
+ * // }
239
+ */
240
+ const updateFileContent = (currentContent, block) => {
241
+ if (block.currentContent.trim() === '') {
242
+ return block.newContent;
148
243
  }
149
- return fileContents;
244
+ // Normalize the current content for comparison
245
+ const normalizedCurrentContent = block.currentContent.trim();
246
+ const normalizedFileContent = currentContent.replace(/\r\n/g, '\n');
247
+ // Find the index of the normalized current content in the file content
248
+ const index = normalizedFileContent.indexOf(normalizedCurrentContent);
249
+ if (index === -1) {
250
+ return currentContent;
251
+ }
252
+ // Determine the indentation level of the current content in the file
253
+ const linesBefore = normalizedFileContent.substring(0, index).split('\n');
254
+ const lastLineBefore = linesBefore[linesBefore.length - 1];
255
+ const indentationMatch = lastLineBefore.match(/^\s*/);
256
+ const indentation = indentationMatch ? indentationMatch[0] : '';
257
+ // Calculate missing leading spaces in the current content
258
+ const currentContentLines = block.currentContent.split('\n');
259
+ const fileContentLines = normalizedFileContent.substring(index, index + normalizedCurrentContent.length).split('\n');
260
+ const missingSpaces = currentContentLines.map((line, i) => {
261
+ const fileLine = fileContentLines[i] || '';
262
+ const lineIndentationMatch = fileLine.match(/^\s*/);
263
+ const lineIndentation = lineIndentationMatch ? lineIndentationMatch[0] : '';
264
+ return Math.max(0, lineIndentation.length - line.length + line.trimStart().length);
265
+ });
266
+ // Apply the adjusted indentation to the new content
267
+ const indentedNewContent = block.newContent
268
+ .split('\n')
269
+ .map((line, i) => {
270
+ const additionalSpaces = ' '.repeat(missingSpaces[i] || 0);
271
+ return line.trim() ? indentation + additionalSpaces + line : line;
272
+ })
273
+ .join('\n');
274
+ // Replace the current content with the indented new content
275
+ return currentContent.replace(block.currentContent, indentedNewContent);
276
+ };
277
+ const processBlock = async (block, index) => {
278
+ const filePath = path_1.default.resolve(block.filePath);
279
+ let fileContent = await readFileContent(filePath);
280
+ if (fileContent === '') {
281
+ await writeFileContent(filePath, block.newContent);
282
+ console.log(`[${index + 1}] ${colors_1.default.green.bold(block.filePath)} has been ${colors_1.default.green.bold('created')}`);
283
+ return;
284
+ }
285
+ if (block.currentContent.trim() !== '' && !fileContent.includes(block.currentContent)) {
286
+ console.log(`[${index + 1}] ${colors_1.default.green.bold(block.filePath)} ${colors_1.default.red.bold(`ERROR: Current content not found to replace in the file`)}`);
287
+ return;
288
+ }
289
+ const updatedContent = updateFileContent(fileContent, block);
290
+ await writeFileContent(filePath, updatedContent);
291
+ console.log(`[${index + 1}] ${colors_1.default.green.bold(block.filePath)} has been ${colors_1.default.yellow.bold('updated')}`);
150
292
  };
151
- exports.extractFilesAndContent = extractFilesAndContent;
152
- const saveGeneratedFiles = async (fileContents) => {
153
- for (const file of Object.keys(fileContents)) {
154
- if (fileContents[file]) {
155
- const filePath = path_1.default.resolve(file);
156
- await fs_1.default.promises.mkdir(path_1.default.dirname(filePath), { recursive: true });
157
- await fs_1.default.promises.writeFile(filePath, fileContents[file]);
158
- console.log(`${file} has been saved.`);
293
+ /**
294
+ * Applies and saves the changes specified in the CurrentNewBlock array.
295
+ *
296
+ * This function iterates through an array of CurrentNewBlock objects, each representing
297
+ * a change to be made to a file. For each block, it attempts to process the change
298
+ * by either creating a new file, updating an existing file, or reporting an error
299
+ * if the current content doesn't match.
300
+ *
301
+ * The function provides console output to inform the user about the status of each
302
+ * file operation (created, updated, or error).
303
+ *
304
+ * @param {CurrentNewBlock[]} blocks - An array of CurrentNewBlock objects representing the changes to be applied.
305
+ * @returns {Promise<void>} A promise that resolves when all blocks have been processed.
306
+ *
307
+ * @throws Will log errors to the console if any block processing fails, but won't stop execution for other blocks.
308
+ *
309
+ * @example
310
+ * const blocks = [
311
+ * { filePath: 'path/to/file.ts', currentContent: 'old code', newContent: 'new code' },
312
+ * { filePath: 'path/to/newfile.ts', currentContent: '', newContent: 'new file content' }
313
+ * ];
314
+ * await applyAndSaveCurrentNewBlocks(blocks);
315
+ */
316
+ const applyAndSaveCurrentNewBlocks = async (blocks) => {
317
+ console.log(`\nApplying and saving current/new blocks...\n`);
318
+ for (const [index, block] of blocks.entries()) {
319
+ try {
320
+ await processBlock(block, index);
159
321
  }
160
- else {
161
- console.log(`File content not found for ${file}.`);
322
+ catch (error) {
323
+ console.error(`Error processing ${colors_1.default.red(block.filePath)}:`, error);
162
324
  }
163
325
  }
164
326
  };
165
- exports.saveGeneratedFiles = saveGeneratedFiles;
327
+ exports.applyAndSaveCurrentNewBlocks = applyAndSaveCurrentNewBlocks;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloving",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "packageManager": "yarn@1.22.22",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -36,25 +36,26 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@inquirer/prompts": "^5.3.6",
39
+ "@inquirer/prompts": "^5.3.8",
40
40
  "axios": "^1.7.3",
41
41
  "cli-highlight": "^2.1.11",
42
+ "colors": "^1.4.0",
42
43
  "commander": "^12.1.0",
43
44
  "copy-paste": "^1.5.3",
44
- "eslint": "^9.8.0",
45
+ "eslint": "^9.9.0",
45
46
  "eslint-config-prettier": "^9.1.0",
46
47
  "eslint-config-raine": "^0.5.0",
47
48
  "eslint-config-standard": "^17.1.0",
48
49
  "eslint-plugin-import": "^2.29.1",
49
- "eslint-plugin-jsdoc": "^48.11.0",
50
- "eslint-plugin-n": "^17.10.1",
51
- "eslint-plugin-promise": "^7.0.0",
50
+ "eslint-plugin-jsdoc": "^50.1.0",
51
+ "eslint-plugin-n": "^17.10.2",
52
+ "eslint-plugin-promise": "^7.1.0",
52
53
  "express": "^4.19.2",
53
- "ignore": "^5.3.1",
54
+ "ignore": "^5.3.2",
54
55
  "ini": "^4.1.3",
55
56
  "isbinaryfile": "^5.0.2",
56
57
  "nock": "^13.5.4",
57
- "vite": "^5.3.5",
58
+ "vite": "^5.4.0",
58
59
  "vite-node": "^2.0.5"
59
60
  },
60
61
  "devDependencies": {
@@ -64,11 +65,11 @@
64
65
  "@types/ini": "^4.1.1",
65
66
  "@types/inquirer": "^9.0.7",
66
67
  "@types/jest": "^29.5.12",
67
- "@types/node": "^22.1.0",
68
+ "@types/node": "^22.2.0",
68
69
  "husky": "^9.1.4",
69
70
  "jest": "^29.7.0",
70
71
  "lint-prepush": "^2.2.2",
71
- "lint-staged": "^15.2.8",
72
+ "lint-staged": "^15.2.9",
72
73
  "ts-jest": "^29.2.4",
73
74
  "typescript": "^5.5.4"
74
75
  },