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.
- package/README.md +1 -1
- package/dist/cloving_gpt/adapters/claude.js +7 -4
- package/dist/cloving_gpt/adapters/gemini.js +2 -1
- package/dist/cloving_gpt/adapters/mistral.js +1 -1
- package/dist/cloving_gpt/adapters/openai.js +1 -1
- package/dist/cloving_gpt/index.js +16 -7
- package/dist/commands/generate/code.js +3 -13
- package/dist/commands/generate/commit.js +3 -43
- package/dist/commands/generate/init.js +15 -0
- package/dist/commands/generate/review.js +1 -5
- package/dist/commands/generate/shell.js +3 -103
- package/dist/commands/generate/unit_tests.js +5 -51
- package/dist/commands/init.js +62 -53
- package/dist/file2.js +4 -0
- package/dist/managers/ChatManager.js +438 -128
- package/dist/managers/CodeManager.js +66 -185
- package/dist/managers/CommitManager.js +55 -0
- package/dist/managers/ProxyManager.js +2 -2
- package/dist/managers/ReviewManager.js +28 -23
- package/dist/managers/ShellManager.js +106 -0
- package/dist/newfile.js +2 -0
- package/dist/package.json +11 -10
- package/dist/utils/command_utils.js +195 -20
- package/dist/utils/string_utils.js +222 -60
- package/package.json +11 -10
|
@@ -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
|
|
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
|
|
114
|
-
const
|
|
115
|
-
if (await
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
289
|
+
else {
|
|
290
|
+
await addFileToContext(fullPath, contextFiles, baseDir);
|
|
291
|
+
}
|
|
126
292
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
console.
|
|
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.
|
|
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
|
-
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
console.
|
|
322
|
+
catch (error) {
|
|
323
|
+
console.error(`Error processing ${colors_1.default.red(block.filePath)}:`, error);
|
|
162
324
|
}
|
|
163
325
|
}
|
|
164
326
|
};
|
|
165
|
-
exports.
|
|
327
|
+
exports.applyAndSaveCurrentNewBlocks = applyAndSaveCurrentNewBlocks;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloving",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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.
|
|
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": "^
|
|
50
|
-
"eslint-plugin-n": "^17.10.
|
|
51
|
-
"eslint-plugin-promise": "^7.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
72
|
+
"lint-staged": "^15.2.9",
|
|
72
73
|
"ts-jest": "^29.2.4",
|
|
73
74
|
"typescript": "^5.5.4"
|
|
74
75
|
},
|