cloving 0.3.2 ā 0.3.4
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/dist/index.js +2 -9
- package/dist/managers/ChatManager.js +84 -39
- package/dist/package.json +1 -1
- package/dist/utils/command_utils.js +23 -3
- package/dist/utils/string_utils.js +94 -56
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,8 +5,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const commander_1 = require("commander");
|
|
8
|
-
const fs_1 = require("fs");
|
|
9
|
-
const path_1 = require("path");
|
|
10
8
|
const config_1 = __importDefault(require("./commands/config"));
|
|
11
9
|
const init_1 = __importDefault(require("./commands/init"));
|
|
12
10
|
const proxy_1 = __importDefault(require("./commands/proxy"));
|
|
@@ -19,17 +17,12 @@ const commit_1 = __importDefault(require("./commands/generate/commit"));
|
|
|
19
17
|
const context_1 = __importDefault(require("./commands/generate/context"));
|
|
20
18
|
const unit_tests_1 = __importDefault(require("./commands/generate/unit_tests"));
|
|
21
19
|
const review_1 = __importDefault(require("./commands/generate/review"));
|
|
22
|
-
|
|
23
|
-
const getPackageVersion = () => {
|
|
24
|
-
const packagePath = (0, path_1.join)(__dirname, 'package.json');
|
|
25
|
-
const packageJson = JSON.parse((0, fs_1.readFileSync)(packagePath, 'utf8'));
|
|
26
|
-
return packageJson.version;
|
|
27
|
-
};
|
|
20
|
+
const command_utils_1 = require("./utils/command_utils");
|
|
28
21
|
const program = new commander_1.Command();
|
|
29
22
|
program
|
|
30
23
|
.name('cloving')
|
|
31
24
|
.description('Integrate AI into your development workflow for generating commit messages, code reviews, and unit tests.')
|
|
32
|
-
.version(getPackageVersion());
|
|
25
|
+
.version((0, command_utils_1.getPackageVersion)());
|
|
33
26
|
program
|
|
34
27
|
.command('config')
|
|
35
28
|
.description('Configure cloving with your API key and models to use')
|
|
@@ -7,6 +7,8 @@ const copy_paste_1 = __importDefault(require("copy-paste"));
|
|
|
7
7
|
const readline_1 = __importDefault(require("readline"));
|
|
8
8
|
const child_process_1 = require("child_process");
|
|
9
9
|
const colors_1 = __importDefault(require("colors"));
|
|
10
|
+
const axios_1 = __importDefault(require("axios"));
|
|
11
|
+
const cli_highlight_1 = __importDefault(require("cli-highlight"));
|
|
10
12
|
const CommitManager_1 = __importDefault(require("./CommitManager"));
|
|
11
13
|
const ChunkManager_1 = __importDefault(require("./ChunkManager"));
|
|
12
14
|
const ReviewManager_1 = __importDefault(require("./ReviewManager"));
|
|
@@ -42,6 +44,8 @@ class ChatManager {
|
|
|
42
44
|
this.contextFiles = {};
|
|
43
45
|
this.prompt = '';
|
|
44
46
|
this.isProcessing = false;
|
|
47
|
+
this.isSilent = false;
|
|
48
|
+
this.isSilent = options.silent || false;
|
|
45
49
|
options.stream = true;
|
|
46
50
|
options.silent = true;
|
|
47
51
|
this.gpt = new cloving_gpt_1.default(options);
|
|
@@ -59,6 +63,7 @@ class ChatManager {
|
|
|
59
63
|
* @returns {Promise<void>}
|
|
60
64
|
*/
|
|
61
65
|
async initialize() {
|
|
66
|
+
await this.checkForLatestVersion();
|
|
62
67
|
await this.loadContextFiles();
|
|
63
68
|
console.log(`\nš š š ${colors_1.default.bold('Welcome to Cloving REPL')} š š š\n`);
|
|
64
69
|
this.displayAvailableCommands();
|
|
@@ -81,6 +86,27 @@ class ChatManager {
|
|
|
81
86
|
console.log(` - ${colors_1.default.yellow(colors_1.default.bold('help'))} Display this help message`);
|
|
82
87
|
console.log(` - ${colors_1.default.yellow(colors_1.default.bold('exit'))} Quit this session`);
|
|
83
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Checks for the latest version of the app from GitHub and compares it with the current version.
|
|
91
|
+
* If a new version is available, it notifies the user.
|
|
92
|
+
* @private
|
|
93
|
+
* @returns {Promise<void>}
|
|
94
|
+
*/
|
|
95
|
+
async checkForLatestVersion() {
|
|
96
|
+
try {
|
|
97
|
+
const response = await axios_1.default.get('https://api.github.com/repos/cloving-ai/cloving-cli/releases/latest');
|
|
98
|
+
const latestVersion = response.data.tag_name;
|
|
99
|
+
const currentVersion = `v${(0, command_utils_1.getPackageVersion)()}`;
|
|
100
|
+
if (latestVersion !== currentVersion) {
|
|
101
|
+
console.log(colors_1.default.bgWhite.black(`\n š A new version of Cloving is available: ${latestVersion} `));
|
|
102
|
+
console.log(colors_1.default.bgWhite.black(` You are currently using version: ${currentVersion} `));
|
|
103
|
+
console.log(colors_1.default.bgWhite.black(` To upgrade, run: ${colors_1.default.bgWhite.black.bold('npm install -g cloving@latest')} `));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// ignore errors
|
|
108
|
+
}
|
|
109
|
+
}
|
|
84
110
|
/**
|
|
85
111
|
* Sets up event listeners for the readline interface and process input.
|
|
86
112
|
* @private
|
|
@@ -313,11 +339,7 @@ class ChatManager {
|
|
|
313
339
|
try {
|
|
314
340
|
this.contextFiles = await (0, command_utils_1.addFileOrDirectoryToContext)(filePath, this.contextFiles, this.options);
|
|
315
341
|
console.log(`\nAdded ${colors_1.default.bold(colors_1.default.green(filePath))} to this chat session's context`);
|
|
316
|
-
|
|
317
|
-
const updatedSystemPrompt = (0, command_utils_1.generateCodegenPrompt)(this.contextFiles);
|
|
318
|
-
this.chatHistory[0] = { role: 'user', content: updatedSystemPrompt };
|
|
319
|
-
// Log the update
|
|
320
|
-
console.log(colors_1.default.yellow(`Updated chat context with new file(s)`));
|
|
342
|
+
this.refreshContext();
|
|
321
343
|
}
|
|
322
344
|
catch (error) {
|
|
323
345
|
console.error(`Failed to add ${colors_1.default.bold(colors_1.default.red(filePath))} to this chat session's context:`, error);
|
|
@@ -331,6 +353,7 @@ class ChatManager {
|
|
|
331
353
|
refreshContext() {
|
|
332
354
|
const updatedSystemPrompt = (0, command_utils_1.generateCodegenPrompt)(this.contextFiles);
|
|
333
355
|
this.chatHistory[0] = { role: 'user', content: updatedSystemPrompt };
|
|
356
|
+
this.chatHistory[1] = { role: 'assistant', content: 'What would you like to do?' };
|
|
334
357
|
}
|
|
335
358
|
isRemoveCommand(command) {
|
|
336
359
|
return command.startsWith('rm ');
|
|
@@ -470,9 +493,50 @@ class ChatManager {
|
|
|
470
493
|
this.isProcessing = true;
|
|
471
494
|
this.chunkManager = new ChunkManager_1.default();
|
|
472
495
|
try {
|
|
473
|
-
this.refreshContext();
|
|
474
|
-
this.initializeChatHistory(input);
|
|
496
|
+
this.refreshContext();
|
|
475
497
|
this.prompt = this.generatePrompt(input);
|
|
498
|
+
this.addChatHistory(this.prompt);
|
|
499
|
+
if (!this.isSilent) {
|
|
500
|
+
const fullPrompt = this.chatHistory.map(m => {
|
|
501
|
+
const content = m.content.split('\n').map(line => {
|
|
502
|
+
if (line.startsWith('###')) {
|
|
503
|
+
return colors_1.default.red.bold(line);
|
|
504
|
+
}
|
|
505
|
+
else if (line.startsWith('##')) {
|
|
506
|
+
return colors_1.default.yellow.bold(line);
|
|
507
|
+
}
|
|
508
|
+
return line;
|
|
509
|
+
}).join('\n');
|
|
510
|
+
return m.role === 'user' ? `${colors_1.default.yellow.bold('## User Prompt')}\n\n${content}` : `${colors_1.default.green.bold('## š Cloving\'s Response š')}\n\n${content}`;
|
|
511
|
+
}).join('\n\n');
|
|
512
|
+
// Review the prompt using the REPL
|
|
513
|
+
const tokenCount = Math.ceil(fullPrompt.length / 4).toLocaleString();
|
|
514
|
+
console.log(`The generated prompt is approximately ${tokenCount} tokens long. Would you like to review the prompt before sending it? ${colors_1.default.gray('(Y/n)')}`);
|
|
515
|
+
const reviewPrompt = await new Promise((resolve) => {
|
|
516
|
+
this.rl.question(colors_1.default.green.bold('cloving> '), (answer) => {
|
|
517
|
+
resolve(answer.trim().toLowerCase());
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
if (reviewPrompt === 'y' || reviewPrompt === '') {
|
|
521
|
+
console.log(colors_1.default.gray.bold('\n---------------------- PROMPT START ----------------------\n'));
|
|
522
|
+
const promptParts = fullPrompt.split('\n```');
|
|
523
|
+
const highlightedPrompt = promptParts.map((part, index) => index % 2 === 1 ? (0, cli_highlight_1.default)(part, { language: 'typescript' }) : part).join('\n```');
|
|
524
|
+
console.log(highlightedPrompt);
|
|
525
|
+
console.log(colors_1.default.gray.bold('\n---------------------- PROMPT END ----------------------\n'));
|
|
526
|
+
console.log(`Do you want to proceed with this prompt? ${colors_1.default.gray('(Y/n)')}`);
|
|
527
|
+
const confirmPrompt = await new Promise((resolve) => {
|
|
528
|
+
this.rl.question(colors_1.default.green.bold('cloving> '), (answer) => {
|
|
529
|
+
resolve(answer.trim().toLowerCase());
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
if (confirmPrompt !== 'y' && confirmPrompt !== '') {
|
|
533
|
+
console.log('Operation cancelled by user.');
|
|
534
|
+
this.isProcessing = false;
|
|
535
|
+
this.rl.prompt();
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
476
540
|
const responseStream = await this.gpt.streamText({ prompt: this.prompt, messages: this.chatHistory });
|
|
477
541
|
let accumulatedContent = '';
|
|
478
542
|
this.handleResponseStream(responseStream, accumulatedContent);
|
|
@@ -481,12 +545,7 @@ class ChatManager {
|
|
|
481
545
|
this.handleError(err);
|
|
482
546
|
}
|
|
483
547
|
}
|
|
484
|
-
|
|
485
|
-
if (this.chatHistory.length === 0) {
|
|
486
|
-
const systemPrompt = (0, command_utils_1.generateCodegenPrompt)(this.contextFiles);
|
|
487
|
-
this.chatHistory.push({ role: 'user', content: systemPrompt });
|
|
488
|
-
this.chatHistory.push({ role: 'assistant', content: 'What would you like to do?' });
|
|
489
|
-
}
|
|
548
|
+
addChatHistory(input) {
|
|
490
549
|
this.chatHistory.push({ role: 'user', content: input });
|
|
491
550
|
}
|
|
492
551
|
handleResponseStream(responseStream, accumulatedContent) {
|
|
@@ -520,16 +579,16 @@ class ChatManager {
|
|
|
520
579
|
this.isProcessing = false;
|
|
521
580
|
process.stdout.write(`
|
|
522
581
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
582
|
+
You can follow up with another request or:
|
|
583
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"save"'))} to save all the changes to files
|
|
584
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"commit"'))} to commit the changes to git with a AI - generated message
|
|
585
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"copy"'))} to copy the last response to clipboard
|
|
586
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"review"'))} to start a code review
|
|
587
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"add <file-path>"'))} to add a file to the chat context
|
|
588
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"rm <pattern>"'))} to remove files from the chat context
|
|
589
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"ls <pattern>"'))} to list files in the chat context
|
|
590
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"git <command>"'))} to run a git command
|
|
591
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"exit"'))} to quit this session
|
|
533
592
|
`);
|
|
534
593
|
this.rl.prompt();
|
|
535
594
|
}
|
|
@@ -552,7 +611,7 @@ class ChatManager {
|
|
|
552
611
|
break;
|
|
553
612
|
}
|
|
554
613
|
const promptTokens = Math.ceil(this.prompt.length / 4).toLocaleString();
|
|
555
|
-
console.error(`Error processing a ${promptTokens} token prompt
|
|
614
|
+
console.error(`Error processing a ${promptTokens} token prompt: `, errorMessage, `(${errorNumber}) \n`);
|
|
556
615
|
this.isProcessing = false;
|
|
557
616
|
this.rl.prompt();
|
|
558
617
|
}
|
|
@@ -580,27 +639,13 @@ class ChatManager {
|
|
|
580
639
|
* accurate and relevant responses.
|
|
581
640
|
*/
|
|
582
641
|
generatePrompt(prompt) {
|
|
583
|
-
const contextFileContents = Object.keys(this.contextFiles)
|
|
584
|
-
.map((file) => `### Contents of ${file}\n\n${this.contextFiles[file]}\n\n`)
|
|
585
|
-
.join('\n');
|
|
586
|
-
const allButLast = this.chatHistory.slice(0, -1).map(msg => `${msg.role.toUpperCase()}: ${msg.content}`).join('\n\n');
|
|
587
642
|
return `### Request
|
|
588
643
|
|
|
589
644
|
${prompt}
|
|
590
645
|
|
|
591
|
-
${contextFileContents}
|
|
592
|
-
|
|
593
|
-
### Full Chat History Context
|
|
594
|
-
|
|
595
|
-
${allButLast}
|
|
596
|
-
|
|
597
|
-
### Current Request
|
|
598
|
-
|
|
599
|
-
${prompt}
|
|
600
|
-
|
|
601
646
|
### Note
|
|
602
647
|
|
|
603
|
-
Whenever possible, break up the changes into pieces and make sure every change is in its own CURRENT/NEW block.`;
|
|
648
|
+
Whenever possible, break up the changes into pieces and make sure every change is in its own CURRENT / NEW block.`;
|
|
604
649
|
}
|
|
605
650
|
handleClose() {
|
|
606
651
|
console.log('Goodbye!');
|
package/dist/package.json
CHANGED
|
@@ -3,20 +3,37 @@ 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.generateShellPrompt = exports.generateCodegenPrompt = exports.SPECIAL_FILES = void 0;
|
|
6
|
+
exports.addFileOrDirectoryToContext = exports.readFileContent = exports.fetchModels = exports.runCommand = exports.generateFileList = exports.getAllFilesInDirectory = exports.checkForSpecialFiles = exports.collectSpecialFileContents = exports.generateShellPrompt = exports.generateCodegenPrompt = exports.getPackageVersion = exports.SPECIAL_FILES = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
10
|
const isbinaryfile_1 = require("isbinaryfile");
|
|
11
11
|
const colors_1 = __importDefault(require("colors"));
|
|
12
|
+
const path_2 = require("path");
|
|
12
13
|
const config_utils_1 = require("./config_utils");
|
|
13
14
|
exports.SPECIAL_FILES = [
|
|
14
15
|
'package.json', 'Gemfile', 'requirements.txt', 'Pipfile', 'pyproject.toml', 'pom.xml', 'build.gradle',
|
|
15
16
|
'.csproj', 'packages.config', 'composer.json', 'CMakeLists.txt', 'conanfile.txt', 'conanfile.py',
|
|
16
17
|
'go.mod', 'Cargo.toml', 'Package.swift', 'build.gradle.kts', 'Podfile', 'Cartfile', 'cpanfile',
|
|
17
18
|
'DESCRIPTION', 'mix.exs', 'build.sbt', 'pubspec.yaml', 'stack.yaml', 'cabal.project', 'Project.toml',
|
|
18
|
-
'rockspec', 'rebar.config', 'project.clj', 'tsconfig.json'
|
|
19
|
+
'rockspec', 'rebar.config', 'project.clj', 'tsconfig.json', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yaml',
|
|
20
|
+
'.eslintrc.yml', '.eslintrc', '.prettierrc', '.prettierrc.js', '.prettierrc.json', '.prettierrc.yaml',
|
|
21
|
+
'.prettierrc.yml', '.prettierrc', '.stylelintrc', '.stylelintrc.json', '.stylelintrc.yaml', '.stylelintrc.yml',
|
|
22
|
+
'.stylelintrc', 'jest.config.js', 'jest.config.json', 'jest.config.ts', 'jest.config.cjs', 'jest.config.mjs',
|
|
23
|
+
'webpack.config.js', 'webpack.config.ts', 'webpack.config.cjs', 'webpack.config.mjs', 'webpack.config.common.js',
|
|
19
24
|
];
|
|
25
|
+
const getPackageVersion = () => {
|
|
26
|
+
const packagePath = (0, path_2.join)(__dirname, '..', 'package.json');
|
|
27
|
+
const packageJson = JSON.parse(fs_1.default.readFileSync(packagePath, 'utf8'));
|
|
28
|
+
return packageJson.version;
|
|
29
|
+
};
|
|
30
|
+
exports.getPackageVersion = getPackageVersion;
|
|
31
|
+
const renderAsString = (contextFile) => {
|
|
32
|
+
if (typeof contextFile === 'string') {
|
|
33
|
+
return contextFile;
|
|
34
|
+
}
|
|
35
|
+
return JSON.stringify(contextFile, null, 2);
|
|
36
|
+
};
|
|
20
37
|
const generateCodegenPrompt = (contextFilesContent) => {
|
|
21
38
|
const instructions = `## AI Code Generation Instructions
|
|
22
39
|
|
|
@@ -52,13 +69,16 @@ const generateCodegenPrompt = (contextFilesContent) => {
|
|
|
52
69
|
7. **Close the block with three backticks.**
|
|
53
70
|
\`\`\``;
|
|
54
71
|
const specialFileContents = (0, exports.collectSpecialFileContents)();
|
|
55
|
-
|
|
72
|
+
// detect if specialFileContents[file] is a string or an object
|
|
73
|
+
const specialFiles = Object.keys(specialFileContents).map((file) => `### Contents of **${file}**\n\n\`\`\`\n${renderAsString(specialFileContents[file])}\n\`\`\`\n\n`).join('\n');
|
|
56
74
|
const contextFileContents = Object.keys(contextFilesContent).map((file) => `### Contents of **${file}**\n\n\`\`\`\n${contextFilesContent[file]}\n\`\`\`\n\n`).join('\n');
|
|
57
75
|
const prompt = `${instructions}
|
|
58
76
|
|
|
59
77
|
## Description of App
|
|
60
78
|
|
|
79
|
+
\`\`\`json
|
|
61
80
|
${JSON.stringify((0, config_utils_1.getClovingConfig)(), null, 2)}
|
|
81
|
+
\`\`\`
|
|
62
82
|
|
|
63
83
|
## Special Files
|
|
64
84
|
|
|
@@ -3,7 +3,7 @@ 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.applyAndSaveCurrentNewBlocks = exports.extractCurrentNewBlocks = exports.parseMarkdownInstructions = exports.extractMarkdown = exports.extractJsonMetadata = exports.estimateTokens = void 0;
|
|
6
|
+
exports.checkBlocksApplicability = exports.applyAndSaveCurrentNewBlocks = exports.processBlock = exports.updateFileContent = exports.writeFileContent = exports.ensureDirectoryExists = exports.extractCurrentNewBlocks = exports.extractBlock = exports.findBlockIndices = 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
9
|
const colors_1 = __importDefault(require("colors"));
|
|
@@ -35,13 +35,13 @@ exports.estimateTokens = estimateTokens;
|
|
|
35
35
|
* or a partial JSON string.
|
|
36
36
|
*
|
|
37
37
|
* @example
|
|
38
|
-
* const aiResponse = '```json\n{"key": "value"}\n```\nOther text'
|
|
39
|
-
* const jsonMetadata = extractJsonMetadata(aiResponse)
|
|
38
|
+
* const aiResponse = '```json\n{"key": "value"}\n```\nOther text'
|
|
39
|
+
* const jsonMetadata = extractJsonMetadata(aiResponse)
|
|
40
40
|
* // jsonMetadata will be '{"key": "value"}'
|
|
41
41
|
*
|
|
42
42
|
* @example
|
|
43
|
-
* const plainResponse = 'Some text {"key": "value"} more text'
|
|
44
|
-
* const jsonMetadata = extractJsonMetadata(plainResponse)
|
|
43
|
+
* const plainResponse = 'Some text {"key": "value"} more text'
|
|
44
|
+
* const jsonMetadata = extractJsonMetadata(plainResponse)
|
|
45
45
|
* // jsonMetadata will be '{"key": "value"}'
|
|
46
46
|
*/
|
|
47
47
|
const extractJsonMetadata = (response) => {
|
|
@@ -145,12 +145,14 @@ const findBlockIndices = (input, startIndex) => {
|
|
|
145
145
|
end: blockEnd
|
|
146
146
|
};
|
|
147
147
|
};
|
|
148
|
+
exports.findBlockIndices = findBlockIndices;
|
|
148
149
|
const extractBlock = (input, indices) => {
|
|
149
150
|
const filePath = input.slice(indices.start + BLOCK_START.length, indices.filePathEnd).trim();
|
|
150
151
|
const currentContent = input.slice(indices.filePathEnd + 1, indices.divider).trim();
|
|
151
152
|
const newContent = input.slice(indices.divider + BLOCK_DIVIDER.length, indices.end).trim();
|
|
152
153
|
return { filePath, currentContent, newContent };
|
|
153
154
|
};
|
|
155
|
+
exports.extractBlock = extractBlock;
|
|
154
156
|
/**
|
|
155
157
|
* Extracts CURRENT/NEW blocks from the given input string.
|
|
156
158
|
*
|
|
@@ -169,12 +171,12 @@ const extractBlock = (input, indices) => {
|
|
|
169
171
|
* @example
|
|
170
172
|
* const input = `
|
|
171
173
|
* <<<<<<< CURRENT path/to/file.ts
|
|
172
|
-
* const oldCode = 'old'
|
|
174
|
+
* const oldCode = 'old'
|
|
173
175
|
* =======
|
|
174
|
-
* const newCode = 'new'
|
|
176
|
+
* const newCode = 'new'
|
|
175
177
|
* >>>>>>> NEW
|
|
176
|
-
*
|
|
177
|
-
* const blocks = extractCurrentNewBlocks(input)
|
|
178
|
+
* `
|
|
179
|
+
* const blocks = extractCurrentNewBlocks(input)
|
|
178
180
|
* // blocks will contain one CurrentNewBlock object with the parsed information
|
|
179
181
|
*/
|
|
180
182
|
const extractCurrentNewBlocks = (input) => {
|
|
@@ -183,10 +185,10 @@ const extractCurrentNewBlocks = (input) => {
|
|
|
183
185
|
const blocks = [];
|
|
184
186
|
let currentIndex = 0;
|
|
185
187
|
while (currentIndex < input.length) {
|
|
186
|
-
const indices = findBlockIndices(input, currentIndex);
|
|
188
|
+
const indices = (0, exports.findBlockIndices)(input, currentIndex);
|
|
187
189
|
if (!indices)
|
|
188
190
|
break;
|
|
189
|
-
blocks.push(extractBlock(input, indices));
|
|
191
|
+
blocks.push((0, exports.extractBlock)(input, indices));
|
|
190
192
|
currentIndex = indices.end + BLOCK_END.length;
|
|
191
193
|
}
|
|
192
194
|
return blocks;
|
|
@@ -206,10 +208,12 @@ const readFileContent = async (filePath) => {
|
|
|
206
208
|
const ensureDirectoryExists = async (filePath) => {
|
|
207
209
|
await fs_1.default.promises.mkdir(path_1.default.dirname(filePath), { recursive: true });
|
|
208
210
|
};
|
|
211
|
+
exports.ensureDirectoryExists = ensureDirectoryExists;
|
|
209
212
|
const writeFileContent = async (filePath, content) => {
|
|
210
|
-
await ensureDirectoryExists(filePath);
|
|
213
|
+
await (0, exports.ensureDirectoryExists)(filePath);
|
|
211
214
|
await fs_1.default.promises.writeFile(filePath, content);
|
|
212
215
|
};
|
|
216
|
+
exports.writeFileContent = writeFileContent;
|
|
213
217
|
/**
|
|
214
218
|
* Updates the content of a file by replacing a specified block of current content with new content.
|
|
215
219
|
*
|
|
@@ -224,61 +228,55 @@ const writeFileContent = async (filePath, content) => {
|
|
|
224
228
|
* @example
|
|
225
229
|
* const currentContent = `
|
|
226
230
|
* function example() {
|
|
227
|
-
* console.log('Old content')
|
|
231
|
+
* console.log('Old content')
|
|
228
232
|
* }
|
|
229
|
-
*
|
|
233
|
+
* `
|
|
230
234
|
* const block = {
|
|
231
|
-
* currentContent: "console.log('Old content')
|
|
232
|
-
* newContent: "console.log('New content')
|
|
233
|
-
* }
|
|
234
|
-
* const updatedContent = updateFileContent(currentContent, block)
|
|
235
|
+
* currentContent: "console.log('Old content')",
|
|
236
|
+
* newContent: "console.log('New content')"
|
|
237
|
+
* }
|
|
238
|
+
* const updatedContent = updateFileContent(currentContent, block)
|
|
235
239
|
* // updatedContent will be:
|
|
236
240
|
* // function example() {
|
|
237
|
-
* // console.log('New content')
|
|
241
|
+
* // console.log('New content')
|
|
238
242
|
* // }
|
|
239
243
|
*/
|
|
240
244
|
const updateFileContent = (currentContent, block) => {
|
|
245
|
+
// 1) if block.currentContent.trim() is empty then return block.newContent
|
|
241
246
|
if (block.currentContent.trim() === '') {
|
|
242
247
|
return block.newContent;
|
|
243
248
|
}
|
|
244
|
-
//
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
//
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
249
|
+
// 2) split currentContent and block.currentContent into new lines and trim() all the lines, then put back the new lines
|
|
250
|
+
const currentLines = currentContent.split('\n').map(line => line.trim());
|
|
251
|
+
const blockLines = block.currentContent.split('\n').map(line => line.trim());
|
|
252
|
+
// 3) see if the trimmed block.currentContent matches a section of code in the trimmed currentContent
|
|
253
|
+
const blockContent = blockLines.join('\n');
|
|
254
|
+
const currentContentTrimmed = currentLines.join('\n');
|
|
255
|
+
// 4) if so, make a variable named matchingLine with the line number of which line the block.currentContent starts on in currentContent
|
|
256
|
+
const matchingLine = currentContentTrimmed.indexOf(blockContent);
|
|
257
|
+
if (matchingLine === -1) {
|
|
258
|
+
return currentContent; // If no match is found, return the original content
|
|
259
|
+
}
|
|
260
|
+
// 5) compare the first line of block.currentContent with the matchingLine line of currentContent, putting any preceeding spaces that are in currentContent but not in block.currentContent into a variable named preceedingSpaces
|
|
261
|
+
const firstLineIndex = currentContent.indexOf(blockLines[0]);
|
|
262
|
+
const precedingSpaces = currentContent.substring(firstLineIndex, firstLineIndex + currentLines[0].length).match(/^\s*/);
|
|
263
|
+
// 6) add preceedingSpaces to the start of each line in both block.currentContent and block.newContent
|
|
264
|
+
if (precedingSpaces) {
|
|
265
|
+
const precedingSpacesValue = precedingSpaces[0];
|
|
266
|
+
const indentedNewContent = block.newContent.split('\n').map(line => precedingSpacesValue + line).join('\n');
|
|
267
|
+
// 7) finally, do a simple currentContent.replace(block.currentContent, block.newContent) with the modified (added preceeding spaces to match) variables
|
|
268
|
+
return currentContent.replace(block.currentContent, indentedNewContent);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
return currentContent.replace(block.currentContent, block.newContent);
|
|
251
272
|
}
|
|
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
273
|
};
|
|
274
|
+
exports.updateFileContent = updateFileContent;
|
|
277
275
|
const processBlock = async (block, index) => {
|
|
278
276
|
const filePath = path_1.default.resolve(block.filePath);
|
|
279
277
|
let fileContent = await readFileContent(filePath);
|
|
280
278
|
if (fileContent === '') {
|
|
281
|
-
await writeFileContent(filePath, block.newContent);
|
|
279
|
+
await (0, exports.writeFileContent)(filePath, block.newContent);
|
|
282
280
|
console.log(`[${index + 1}] ${colors_1.default.green.bold(block.filePath)} has been ${colors_1.default.green.bold('created')}`);
|
|
283
281
|
return;
|
|
284
282
|
}
|
|
@@ -286,10 +284,11 @@ const processBlock = async (block, index) => {
|
|
|
286
284
|
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
285
|
return;
|
|
288
286
|
}
|
|
289
|
-
const updatedContent = updateFileContent(fileContent, block);
|
|
290
|
-
await writeFileContent(filePath, updatedContent);
|
|
287
|
+
const updatedContent = (0, exports.updateFileContent)(fileContent, block);
|
|
288
|
+
await (0, exports.writeFileContent)(filePath, updatedContent);
|
|
291
289
|
console.log(`[${index + 1}] ${colors_1.default.green.bold(block.filePath)} has been ${colors_1.default.yellow.bold('updated')}`);
|
|
292
290
|
};
|
|
291
|
+
exports.processBlock = processBlock;
|
|
293
292
|
/**
|
|
294
293
|
* Applies and saves the changes specified in the CurrentNewBlock array.
|
|
295
294
|
*
|
|
@@ -310,14 +309,14 @@ const processBlock = async (block, index) => {
|
|
|
310
309
|
* const blocks = [
|
|
311
310
|
* { filePath: 'path/to/file.ts', currentContent: 'old code', newContent: 'new code' },
|
|
312
311
|
* { filePath: 'path/to/newfile.ts', currentContent: '', newContent: 'new file content' }
|
|
313
|
-
* ]
|
|
314
|
-
* await applyAndSaveCurrentNewBlocks(blocks)
|
|
312
|
+
* ]
|
|
313
|
+
* await applyAndSaveCurrentNewBlocks(blocks)
|
|
315
314
|
*/
|
|
316
315
|
const applyAndSaveCurrentNewBlocks = async (blocks) => {
|
|
317
316
|
console.log(`\nApplying and saving current/new blocks...\n`);
|
|
318
317
|
for (const [index, block] of blocks.entries()) {
|
|
319
318
|
try {
|
|
320
|
-
await processBlock(block, index);
|
|
319
|
+
await (0, exports.processBlock)(block, index);
|
|
321
320
|
}
|
|
322
321
|
catch (error) {
|
|
323
322
|
console.error(`Error processing ${colors_1.default.red(block.filePath)}:`, error);
|
|
@@ -325,3 +324,42 @@ const applyAndSaveCurrentNewBlocks = async (blocks) => {
|
|
|
325
324
|
}
|
|
326
325
|
};
|
|
327
326
|
exports.applyAndSaveCurrentNewBlocks = applyAndSaveCurrentNewBlocks;
|
|
327
|
+
/**
|
|
328
|
+
* Checks if all the blocks can be applied without making any changes.
|
|
329
|
+
*
|
|
330
|
+
* This function iterates through an array of CurrentNewBlock objects and checks
|
|
331
|
+
* if each block can be applied to its respective file. It returns a tuple where
|
|
332
|
+
* the first element is a boolean indicating if all blocks can be applied, and
|
|
333
|
+
* the second element is a string summarizing the success or failure of each block.
|
|
334
|
+
*
|
|
335
|
+
* @param {CurrentNewBlock[]} blocks - An array of CurrentNewBlock objects to be checked.
|
|
336
|
+
* @returns {Promise<[boolean, string]>} A promise that resolves to a tuple containing
|
|
337
|
+
* a boolean and a summary string.
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* const blocks = [
|
|
341
|
+
* { filePath: 'path/to/file.ts', currentContent: 'old code', newContent: 'new code' },
|
|
342
|
+
* { filePath: 'path/to/newfile.ts', currentContent: '', newContent: 'new file content' }
|
|
343
|
+
* ]
|
|
344
|
+
* const [canApply, summary] = await checkBlocksApplicability(blocks)
|
|
345
|
+
*/
|
|
346
|
+
const checkBlocksApplicability = async (blocks) => {
|
|
347
|
+
let allApplicable = true;
|
|
348
|
+
const summary = [];
|
|
349
|
+
for (const [index, block] of blocks.entries()) {
|
|
350
|
+
const filePath = path_1.default.resolve(block.filePath);
|
|
351
|
+
let fileContent = await readFileContent(filePath);
|
|
352
|
+
if (fileContent === '') {
|
|
353
|
+
summary.push(`[${index + 1}] ${colors_1.default.green.bold(block.filePath)} can be ${colors_1.default.green.bold('created')}`);
|
|
354
|
+
}
|
|
355
|
+
else if (block.currentContent.trim() !== '' && !fileContent.includes(block.currentContent)) {
|
|
356
|
+
allApplicable = false;
|
|
357
|
+
summary.push(`[${index + 1}] ${colors_1.default.red.bold(block.filePath)} ${colors_1.default.red.bold('cannot be applied: Current content not found')}`);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
summary.push(`[${index + 1}] ${colors_1.default.green.bold(block.filePath)} can be ${colors_1.default.yellow.bold('updated')}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return [allApplicable, summary.join('\n')];
|
|
364
|
+
};
|
|
365
|
+
exports.checkBlocksApplicability = checkBlocksApplicability;
|