cloving 0.3.19 → 0.3.20
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 -0
- package/dist/managers/CommitManager.js +30 -16
- package/dist/package.json +1 -1
- package/package.json +1 -1
- package/.DS_Store +0 -0
- package/.aider.chat.history.md +0 -16978
- package/.aider.input.history +0 -15
- package/.aider.tags.cache.v3/cache.db +0 -0
- package/.aider.tags.cache.v3/cache.db-shm +0 -0
- package/.aider.tags.cache.v3/cache.db-wal +0 -0
- package/.cursorrules +0 -77
- package/.github/workflows/auto-release.yml +0 -83
- package/.github/workflows/ci.yml +0 -32
- package/.github/workflows/npm-publish.yml +0 -34
- package/.gitignore +0 -67
- package/.husky/_/.gitignore +0 -1
- package/.husky/_/applypatch-msg +0 -2
- package/.husky/_/commit-msg +0 -2
- package/.husky/_/h +0 -22
- package/.husky/_/husky.sh +0 -9
- package/.husky/_/post-applypatch +0 -2
- package/.husky/_/post-checkout +0 -2
- package/.husky/_/post-commit +0 -2
- package/.husky/_/post-merge +0 -2
- package/.husky/_/post-rewrite +0 -2
- package/.husky/_/pre-applypatch +0 -2
- package/.husky/_/pre-auto-gc +0 -2
- package/.husky/_/pre-commit +0 -2
- package/.husky/_/pre-merge-commit +0 -2
- package/.husky/_/pre-push +0 -2
- package/.husky/_/pre-rebase +0 -2
- package/.husky/_/prepare-commit-msg +0 -2
- package/.husky/pre-commit +0 -1
- package/.husky/pre-push +0 -1
- package/.prettierrc.json +0 -7
- package/.tool-versions +0 -2
- package/SECURITY.md +0 -15
- package/cloving.json +0 -67
- package/jest.config.js +0 -11
- package/src/cloving_gpt/adapters/azureopenai.ts +0 -83
- package/src/cloving_gpt/adapters/claude.ts +0 -140
- package/src/cloving_gpt/adapters/gemini.ts +0 -146
- package/src/cloving_gpt/adapters/index.ts +0 -43
- package/src/cloving_gpt/adapters/mistral.ts +0 -142
- package/src/cloving_gpt/adapters/ollama.ts +0 -136
- package/src/cloving_gpt/adapters/openai.ts +0 -113
- package/src/cloving_gpt/index.ts +0 -229
- package/src/commands/chat.ts +0 -12
- package/src/commands/config.ts +0 -192
- package/src/commands/generate/code.ts +0 -18
- package/src/commands/generate/commit.ts +0 -9
- package/src/commands/generate/context.ts +0 -93
- package/src/commands/generate/docs.ts +0 -9
- package/src/commands/generate/review.ts +0 -9
- package/src/commands/generate/shell.ts +0 -9
- package/src/commands/generate/unit_tests.ts +0 -9
- package/src/commands/init.ts +0 -9
- package/src/commands/models.ts +0 -21
- package/src/commands/proxy.ts +0 -9
- package/src/commands/tokens.ts +0 -82
- package/src/index.ts +0 -197
- package/src/managers/BlockManager.ts +0 -126
- package/src/managers/ChatManager.ts +0 -755
- package/src/managers/ChunkManager.ts +0 -31
- package/src/managers/CodeManager.ts +0 -226
- package/src/managers/CommitManager.ts +0 -71
- package/src/managers/DocsManager.ts +0 -101
- package/src/managers/InitManager.ts +0 -263
- package/src/managers/ProxyManager.ts +0 -240
- package/src/managers/ReviewManager.ts +0 -154
- package/src/managers/ShellManager.ts +0 -172
- package/src/managers/StreamManager.ts +0 -353
- package/src/managers/UnitTestManager.ts +0 -293
- package/src/utils/config_utils.ts +0 -271
- package/src/utils/git_utils.ts +0 -230
- package/src/utils/model_utils.ts +0 -3
- package/src/utils/prompt_utils.ts +0 -375
- package/src/utils/prompts/CODEGEN_COULDNT_APPLY.md +0 -5
- package/src/utils/prompts/CODEGEN_EXAMPLES.md +0 -82
- package/src/utils/prompts/CODEGEN_INSTRUCTIONS.md +0 -43
- package/src/utils/prompts/DOCS_INSTRUCTIONS.md +0 -9
- package/src/utils/prompts/INIT_INSTRUCTIONS.md +0 -132
- package/src/utils/prompts/REVIEW_INSTRUCTIONS.md +0 -49
- package/src/utils/prompts/SHELL_INSTRUCTIONS.md +0 -15
- package/src/utils/prompts/SPECIAL_FILES.md +0 -66
- package/src/utils/prompts/index.ts +0 -37
- package/src/utils/string_utils.ts +0 -552
- package/src/utils/types.ts +0 -127
- package/tests/commands/generate/unit_tests.test.ts +0 -27
- package/tests/utils/git_utils.test.ts +0 -116
- package/tests/utils/model_utils.test.ts +0 -27
- package/tests/utils/prompt_utils.test.ts +0 -33
- package/tests/utils/string_utils.test.ts +0 -560
- package/tsconfig.json +0 -14
- package/yarn.lock +0 -5823
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
import fs from 'fs'
|
|
2
|
-
import axios from 'axios'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import { EventEmitter } from 'events'
|
|
5
|
-
import colors from 'colors'
|
|
6
|
-
import highlight from 'cli-highlight'
|
|
7
|
-
|
|
8
|
-
import ClovingGPT from '../cloving_gpt'
|
|
9
|
-
import ChunkManager from './ChunkManager'
|
|
10
|
-
import BlockManager from './BlockManager'
|
|
11
|
-
|
|
12
|
-
import { getPackageVersion } from '../utils/prompt_utils'
|
|
13
|
-
import { getNearestClovingConfig } from '../utils/config_utils'
|
|
14
|
-
import { addFileOrDirectoryToContext, generateCodegenPrompt } from '../utils/prompt_utils'
|
|
15
|
-
import type { CurrentNewBlock, ChatMessage } from '../utils/types'
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* StreamManager class
|
|
19
|
-
*
|
|
20
|
-
* This class extends EventEmitter and manages the streaming of responses from an AI model.
|
|
21
|
-
* It handles the processing of chunks, manages code blocks, and provides animation for code generation.
|
|
22
|
-
*/
|
|
23
|
-
class StreamManager extends EventEmitter {
|
|
24
|
-
protected gpt: ClovingGPT
|
|
25
|
-
protected prompt: string = ''
|
|
26
|
-
protected responseString: string = ''
|
|
27
|
-
protected contextFiles: Record<string, string> = {}
|
|
28
|
-
protected chatHistory: ChatMessage[] = []
|
|
29
|
-
protected chunkManager: ChunkManager
|
|
30
|
-
protected blockManager: BlockManager
|
|
31
|
-
protected isProcessing: boolean = false
|
|
32
|
-
protected animationInterval: NodeJS.Timeout | null = null
|
|
33
|
-
protected retryCount: number = 0
|
|
34
|
-
protected maxRetries: number = 3
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Creates an instance of StreamManager.
|
|
38
|
-
* @param {any} options - Configuration options for the StreamManager.
|
|
39
|
-
*/
|
|
40
|
-
constructor(protected options: any) {
|
|
41
|
-
super()
|
|
42
|
-
this.gpt = new ClovingGPT(options)
|
|
43
|
-
this.chunkManager = new ChunkManager()
|
|
44
|
-
this.blockManager = new BlockManager()
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Checks for the latest version of the app from GitHub and compares it with the current version.
|
|
49
|
-
* If a new version is available, it notifies the user.
|
|
50
|
-
* @private
|
|
51
|
-
* @returns {Promise<void>}
|
|
52
|
-
*/
|
|
53
|
-
protected async checkForLatestVersion(): Promise<void> {
|
|
54
|
-
try {
|
|
55
|
-
const response = await axios.get(
|
|
56
|
-
'https://api.github.com/repos/cloving-ai/cloving-cli/releases/latest',
|
|
57
|
-
)
|
|
58
|
-
const latestVersion = response.data.tag_name
|
|
59
|
-
const currentVersion = `v${getPackageVersion()}`
|
|
60
|
-
|
|
61
|
-
if (latestVersion !== currentVersion) {
|
|
62
|
-
console.log(
|
|
63
|
-
colors.bgWhite.black(`\n 🚀 A new version of Cloving is available: ${latestVersion} `),
|
|
64
|
-
)
|
|
65
|
-
console.log(
|
|
66
|
-
colors.bgWhite.black(` You are currently using version: ${currentVersion} `),
|
|
67
|
-
)
|
|
68
|
-
console.log(
|
|
69
|
-
colors.bgWhite.black(
|
|
70
|
-
` To upgrade, run: ${colors.bgWhite.black.bold('npm install -g cloving@latest')} `,
|
|
71
|
-
),
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
} catch (error) {
|
|
75
|
-
// ignore errors
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Handles the response stream from the AI model.
|
|
81
|
-
* @param {any} responseStream - The stream of responses from the AI model.
|
|
82
|
-
*/
|
|
83
|
-
protected handleResponseStream(responseStream: any) {
|
|
84
|
-
this.responseString = ''
|
|
85
|
-
this.setupChunkManagerEvents()
|
|
86
|
-
this.setupBlockManagerEvents()
|
|
87
|
-
this.setupResponseStreamEvents(responseStream)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Sets up event listeners for the BlockManager.
|
|
92
|
-
* @private
|
|
93
|
-
*/
|
|
94
|
-
private setupBlockManagerEvents(): void {
|
|
95
|
-
this.blockManager = new BlockManager()
|
|
96
|
-
|
|
97
|
-
this.blockManager.on('startGeneratingCode', () => {
|
|
98
|
-
this.setupAnimationInterval()
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
this.blockManager.on('endGeneratingCode', () => {
|
|
102
|
-
this.clearAnimation()
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
this.blockManager.on('content', (content: string) => {
|
|
106
|
-
process.stdout.write(content)
|
|
107
|
-
this.responseString += content
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
this.blockManager.on(
|
|
111
|
-
'codeBlock',
|
|
112
|
-
(codeBlock: { currentNewBlock?: CurrentNewBlock; raw?: string }) => {
|
|
113
|
-
this.handleCodeBlock(codeBlock)
|
|
114
|
-
},
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Sets up an animation interval for code generation visualization.
|
|
120
|
-
* @private
|
|
121
|
-
*/
|
|
122
|
-
private setupAnimationInterval(): void {
|
|
123
|
-
process.stdout.write(colors.gray.bold('\nGenerating code... '))
|
|
124
|
-
let animationIndex = 0
|
|
125
|
-
const animationChars = ['/', '|', '\\', '-']
|
|
126
|
-
this.animationInterval = setInterval(() => {
|
|
127
|
-
process.stdout.write('\b'.repeat(1))
|
|
128
|
-
process.stdout.write(colors.gray.bold(animationChars[animationIndex]))
|
|
129
|
-
animationIndex = (animationIndex + 1) % animationChars.length
|
|
130
|
-
}, 100)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Clears the animation interval.
|
|
135
|
-
* @private
|
|
136
|
-
*/
|
|
137
|
-
private clearAnimation(): void {
|
|
138
|
-
if (!this.animationInterval) return
|
|
139
|
-
clearInterval(this.animationInterval)
|
|
140
|
-
process.stdout.write('\b'.repeat(100))
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Handles the processing of a code block.
|
|
145
|
-
* @private
|
|
146
|
-
* @param {Object} codeBlock - The code block to process.
|
|
147
|
-
* @param {CurrentNewBlock} [codeBlock.currentNewBlock] - The current and new content of the code block.
|
|
148
|
-
* @param {string} [codeBlock.raw] - The raw content of the code block.
|
|
149
|
-
*/
|
|
150
|
-
private handleCodeBlock(codeBlock: { currentNewBlock?: CurrentNewBlock; raw?: string }): void {
|
|
151
|
-
const { currentNewBlock, raw } = codeBlock
|
|
152
|
-
if (currentNewBlock?.currentContent && currentNewBlock?.newContent) {
|
|
153
|
-
this.displayCurrentNewBlock(currentNewBlock)
|
|
154
|
-
} else if (raw) {
|
|
155
|
-
this.displayRawCodeBlock(raw)
|
|
156
|
-
} else {
|
|
157
|
-
process.stdout.write(` \n`)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
this.responseString += raw || currentNewBlock
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Displays the current and new content of a code block.
|
|
165
|
-
* @private
|
|
166
|
-
* @param {CurrentNewBlock} currentNewBlock - The current and new content of the code block.
|
|
167
|
-
*/
|
|
168
|
-
private displayCurrentNewBlock(currentNewBlock: CurrentNewBlock): void {
|
|
169
|
-
const language = currentNewBlock.language.split('\n')[0]
|
|
170
|
-
process.stdout.write(
|
|
171
|
-
colors.gray.bold(`\`\`\`${language} \n`),
|
|
172
|
-
)
|
|
173
|
-
process.stdout.write(colors.gray.bold(`<<<<<<< CURRENT ${currentNewBlock.filePath}\n`))
|
|
174
|
-
try {
|
|
175
|
-
process.stdout.write(highlight(currentNewBlock.currentContent))
|
|
176
|
-
} catch (error) {
|
|
177
|
-
process.stdout.write(currentNewBlock.currentContent)
|
|
178
|
-
}
|
|
179
|
-
process.stdout.write(colors.gray.bold('\n=======\n'))
|
|
180
|
-
try {
|
|
181
|
-
process.stdout.write(highlight(currentNewBlock.newContent))
|
|
182
|
-
} catch (error) {
|
|
183
|
-
process.stdout.write(currentNewBlock.newContent)
|
|
184
|
-
}
|
|
185
|
-
process.stdout.write(colors.gray.bold('\n>>>>>>> NEW\n```'))
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Displays a raw code block.
|
|
190
|
-
* @private
|
|
191
|
-
* @param {string} raw - The raw content of the code block.
|
|
192
|
-
*/
|
|
193
|
-
private displayRawCodeBlock(raw: string): void {
|
|
194
|
-
process.stdout.write(` \n`)
|
|
195
|
-
process.stdout.write(highlight(raw))
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Sets up event listeners for the ChunkManager.
|
|
200
|
-
* @private
|
|
201
|
-
*/
|
|
202
|
-
private setupChunkManagerEvents(): void {
|
|
203
|
-
this.chunkManager = new ChunkManager()
|
|
204
|
-
this.chunkManager.on('content', (buffer: string) => {
|
|
205
|
-
this.processChunkManagerContent(buffer)
|
|
206
|
-
})
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Processes the content received from the ChunkManager.
|
|
211
|
-
* @private
|
|
212
|
-
* @param {string} buffer - The buffer containing the chunk content.
|
|
213
|
-
*/
|
|
214
|
-
private processChunkManagerContent(buffer: string): void {
|
|
215
|
-
let convertedStream = this.gpt.convertStream(buffer)
|
|
216
|
-
|
|
217
|
-
while (convertedStream !== null) {
|
|
218
|
-
const { output, lastChar } = convertedStream
|
|
219
|
-
this.blockManager.addContent(output)
|
|
220
|
-
this.chunkManager.clearBuffer(lastChar)
|
|
221
|
-
buffer = buffer.slice(lastChar)
|
|
222
|
-
convertedStream = this.gpt.convertStream(buffer)
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Sets up event listeners for the response stream.
|
|
228
|
-
* @private
|
|
229
|
-
* @param {any} responseStream - The stream of responses from the AI model.
|
|
230
|
-
*/
|
|
231
|
-
private setupResponseStreamEvents(responseStream: any): void {
|
|
232
|
-
responseStream.data.on('data', this.handleResponseStreamData.bind(this))
|
|
233
|
-
responseStream.data.on('end', () => this.finalizeResponse())
|
|
234
|
-
responseStream.data.on('error', this.handleResponseStreamError.bind(this))
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Handles the data received from the response stream.
|
|
239
|
-
* @private
|
|
240
|
-
* @param {Buffer | undefined} chunk - The chunk of data received from the stream.
|
|
241
|
-
*/
|
|
242
|
-
private handleResponseStreamData(chunk: Buffer | undefined): void {
|
|
243
|
-
if (!chunk) return
|
|
244
|
-
const chunkString = chunk.toString()
|
|
245
|
-
this.chunkManager.addChunk(chunkString)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Handles errors that occur during the response stream.
|
|
250
|
-
* @private
|
|
251
|
-
* @param {Error} error - The error that occurred.
|
|
252
|
-
*/
|
|
253
|
-
private handleResponseStreamError(error: Error): void {
|
|
254
|
-
console.error('Error streaming response:', error)
|
|
255
|
-
this.isProcessing = false
|
|
256
|
-
process.stdout.write('\n')
|
|
257
|
-
this.emit('prompt')
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Finalizes the response processing.
|
|
262
|
-
* This method should be overridden in subclasses to provide specific finalization behavior.
|
|
263
|
-
* @protected
|
|
264
|
-
*/
|
|
265
|
-
protected finalizeResponse(): void {
|
|
266
|
-
this.isProcessing = false
|
|
267
|
-
this.emit('responseFinalized', this.responseString)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Loads context files specified in the options or defaults to the primary language's directory.
|
|
272
|
-
* @private
|
|
273
|
-
* @returns {Promise<void>}
|
|
274
|
-
*/
|
|
275
|
-
protected async loadContextFiles(): Promise<void> {
|
|
276
|
-
try {
|
|
277
|
-
const config = getNearestClovingConfig().config
|
|
278
|
-
const primaryLanguage = config.languages.find((lang) => lang.primary)
|
|
279
|
-
const defaultDirectory = primaryLanguage ? primaryLanguage.directory : '.'
|
|
280
|
-
const testingDirectories =
|
|
281
|
-
config.testingFrameworks?.map((framework) => framework.directory) || []
|
|
282
|
-
const testingDirectory = testingDirectories[0]
|
|
283
|
-
|
|
284
|
-
let files = this.options.files || [defaultDirectory, testingDirectory].filter(Boolean)
|
|
285
|
-
if (files.length > 0) {
|
|
286
|
-
console.log(`\nBuilding AI prompt context...\n`)
|
|
287
|
-
}
|
|
288
|
-
for (const file of files) {
|
|
289
|
-
// Skip if the file is not a string
|
|
290
|
-
if (!file) continue
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
const previousCount = Object.keys(this.contextFiles).length
|
|
294
|
-
this.contextFiles = await addFileOrDirectoryToContext(
|
|
295
|
-
file,
|
|
296
|
-
this.contextFiles,
|
|
297
|
-
this.options,
|
|
298
|
-
)
|
|
299
|
-
const newCount = Object.keys(this.contextFiles).length
|
|
300
|
-
const addedCount = newCount - previousCount
|
|
301
|
-
|
|
302
|
-
const totalTokens = this.calculateTotalTokens()
|
|
303
|
-
|
|
304
|
-
console.log(colors.cyan(`📁 Loaded context from: ${colors.bold(file)}`))
|
|
305
|
-
console.log(colors.green(` ✅ Added ${addedCount} file(s) to context`))
|
|
306
|
-
console.log(
|
|
307
|
-
colors.yellow(` 📊 Total tokens in context: ${totalTokens.toLocaleString()}\n`),
|
|
308
|
-
)
|
|
309
|
-
} catch (fileError) {
|
|
310
|
-
console.error(colors.red(`Error loading context from ${file}:`), fileError)
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
} catch (error) {
|
|
314
|
-
console.error(colors.red('Error loading context files:'), error)
|
|
315
|
-
throw new Error('Failed to load context files')
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
protected calculateTotalTokens(): number {
|
|
320
|
-
return Object.values(this.contextFiles).reduce((total, content) => {
|
|
321
|
-
return total + Math.ceil(content.length / 4)
|
|
322
|
-
}, 0)
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
protected async refreshContext() {
|
|
326
|
-
await this.reloadContextFiles()
|
|
327
|
-
const updatedSystemPrompt = generateCodegenPrompt(this.contextFiles)
|
|
328
|
-
this.chatHistory[0] = { role: 'user', content: updatedSystemPrompt }
|
|
329
|
-
this.chatHistory[1] = { role: 'assistant', content: 'What would you like to do?' }
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
protected async reloadContextFiles() {
|
|
333
|
-
for (const filePath in this.contextFiles) {
|
|
334
|
-
try {
|
|
335
|
-
const content = await fs.promises.readFile(path.resolve(filePath), 'utf8')
|
|
336
|
-
this.contextFiles[filePath] = content
|
|
337
|
-
} catch (error) {
|
|
338
|
-
console.error(`Error reading file ${filePath}:`, error)
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
protected addUserPrompt(content: string) {
|
|
344
|
-
this.prompt = content
|
|
345
|
-
this.chatHistory.push({ role: 'user', content })
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
protected addAssistantResponse(content: string) {
|
|
349
|
-
this.chatHistory.push({ role: 'assistant', content })
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
export default StreamManager
|
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'fs'
|
|
2
|
-
import { execFileSync } from 'child_process'
|
|
3
|
-
import highlight from 'cli-highlight'
|
|
4
|
-
import { select, confirm } from '@inquirer/prompts'
|
|
5
|
-
|
|
6
|
-
import { getGitDiff } from '../utils/git_utils'
|
|
7
|
-
import { CODEGEN_INSTRUCTIONS } from '../utils/prompts'
|
|
8
|
-
import { getTestingDirectory, getAllFiles } from '../utils/config_utils'
|
|
9
|
-
import {
|
|
10
|
-
extractCurrentNewBlocks,
|
|
11
|
-
applyAndSaveCurrentNewBlocks,
|
|
12
|
-
extractMarkdown,
|
|
13
|
-
} from '../utils/string_utils'
|
|
14
|
-
import ClovingGPT from '../cloving_gpt'
|
|
15
|
-
import type { ClovingGPTOptions, ChatMessage } from '../utils/types'
|
|
16
|
-
|
|
17
|
-
class UnitTestManager {
|
|
18
|
-
private gpt: ClovingGPT
|
|
19
|
-
private chatHistory: ChatMessage[] = []
|
|
20
|
-
|
|
21
|
-
constructor(private options: ClovingGPTOptions) {
|
|
22
|
-
this.gpt = new ClovingGPT(options)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
private extractChangedFiles(gitDiff: string): string[] {
|
|
26
|
-
const fileRegex = /diff --git a\/(.+?) b\/(.+?)\n/g
|
|
27
|
-
const files = new Set<string>()
|
|
28
|
-
let match
|
|
29
|
-
|
|
30
|
-
while ((match = fileRegex.exec(gitDiff)) !== null) {
|
|
31
|
-
files.add(match[1])
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return Array.from(files)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
private generateUnitTestPrompt(
|
|
38
|
-
files: string[],
|
|
39
|
-
srcFiles: string,
|
|
40
|
-
testFiles: string,
|
|
41
|
-
gitDiff?: string,
|
|
42
|
-
): string {
|
|
43
|
-
if (this.chatHistory.length === 0) {
|
|
44
|
-
const systemPrompt = `### Example of a well-structured response
|
|
45
|
-
|
|
46
|
-
# Unit Test Generation
|
|
47
|
-
|
|
48
|
-
## Files Overview
|
|
49
|
-
|
|
50
|
-
These are the files that are relevant for generating unit tests:
|
|
51
|
-
|
|
52
|
-
1. **app/models/foo.rb**
|
|
53
|
-
2. **app/views/baz/bar.html.erb**
|
|
54
|
-
|
|
55
|
-
## Relevant Test Files
|
|
56
|
-
|
|
57
|
-
1. **test/models/foo_test.rb**
|
|
58
|
-
2. **test/controllers/baz_controller_test.rb**
|
|
59
|
-
|
|
60
|
-
## Task
|
|
61
|
-
|
|
62
|
-
Generate unit tests for the files listed above. Always show filenames for the generated code.`
|
|
63
|
-
|
|
64
|
-
this.chatHistory.push({ role: 'user', content: systemPrompt })
|
|
65
|
-
this.chatHistory.push({ role: 'assistant', content: 'What would you like to do?' })
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const filesList = files.length > 0 ? files.join('\n') : 'No files provided'
|
|
69
|
-
const prompt = `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.
|
|
70
|
-
Also, list any test files that might be relevant to these files.
|
|
71
|
-
|
|
72
|
-
### List of Code Files
|
|
73
|
-
|
|
74
|
-
${filesList}
|
|
75
|
-
|
|
76
|
-
### List of All Source Files
|
|
77
|
-
|
|
78
|
-
${srcFiles}
|
|
79
|
-
|
|
80
|
-
### List of all test files
|
|
81
|
-
|
|
82
|
-
${testFiles}
|
|
83
|
-
|
|
84
|
-
${gitDiff ? '### Git Diff Content' : ''}
|
|
85
|
-
|
|
86
|
-
${gitDiff || ''}
|
|
87
|
-
|
|
88
|
-
## Example Output
|
|
89
|
-
|
|
90
|
-
Here are the files that are relevant:
|
|
91
|
-
|
|
92
|
-
\`\`\`plaintext
|
|
93
|
-
## files
|
|
94
|
-
|
|
95
|
-
app/models/foo.rb
|
|
96
|
-
app/views/baz/bar.html.erb
|
|
97
|
-
|
|
98
|
-
## relevant test files
|
|
99
|
-
|
|
100
|
-
test/models/foo_test.rb
|
|
101
|
-
test/controllers/baz_controller_test.rb
|
|
102
|
-
\`\`\`
|
|
103
|
-
|
|
104
|
-
### Prompt
|
|
105
|
-
|
|
106
|
-
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.
|
|
107
|
-
Also, list any test files that might be relevant to these files.`
|
|
108
|
-
this.chatHistory.push({ role: 'user', content: prompt })
|
|
109
|
-
return prompt
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private async handleUserAction(analysis: string, autoSave: boolean = false): Promise<void> {
|
|
113
|
-
const blocks = extractCurrentNewBlocks(analysis)
|
|
114
|
-
|
|
115
|
-
if (autoSave) {
|
|
116
|
-
await applyAndSaveCurrentNewBlocks(blocks)
|
|
117
|
-
return
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const files = Array.from(new Set(blocks.map((block) => block.filePath)))
|
|
121
|
-
|
|
122
|
-
const action = await select({
|
|
123
|
-
message: 'What would you like to do?',
|
|
124
|
-
choices: [
|
|
125
|
-
{ name: 'Save a Unit Test File', value: 'save' },
|
|
126
|
-
{ name: 'Save All Unit Test Files', value: 'saveAll' },
|
|
127
|
-
{ name: 'Copy Entire Response to Clipboard', value: 'copyAll' },
|
|
128
|
-
{ name: 'Done', value: 'done' },
|
|
129
|
-
],
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
switch (action) {
|
|
133
|
-
case 'save':
|
|
134
|
-
let saveAnother = true
|
|
135
|
-
while (saveAnother) {
|
|
136
|
-
const fileToSave = await select({
|
|
137
|
-
message: 'Which unit test file do you want to save?',
|
|
138
|
-
choices: files.map((file) => ({ name: file, value: file })),
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
await applyAndSaveCurrentNewBlocks(
|
|
142
|
-
blocks.filter((block) => block.filePath === fileToSave),
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
saveAnother = await confirm({
|
|
146
|
-
message: 'Do you want to save another file?',
|
|
147
|
-
default: false,
|
|
148
|
-
})
|
|
149
|
-
}
|
|
150
|
-
break
|
|
151
|
-
case 'saveAll':
|
|
152
|
-
await applyAndSaveCurrentNewBlocks(blocks)
|
|
153
|
-
console.log('All unit test files have been saved.')
|
|
154
|
-
break
|
|
155
|
-
case 'copyAll':
|
|
156
|
-
process.nextTick(() => {
|
|
157
|
-
require('copy-paste').copy(analysis, () => {
|
|
158
|
-
console.log('Entire response copied to clipboard')
|
|
159
|
-
})
|
|
160
|
-
})
|
|
161
|
-
break
|
|
162
|
-
case 'done':
|
|
163
|
-
break
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
public async generateUnitTests() {
|
|
168
|
-
const { files, save } = this.options
|
|
169
|
-
|
|
170
|
-
const allSrcFiles = await getAllFiles(this.options, true)
|
|
171
|
-
const testingDirectory = getTestingDirectory()
|
|
172
|
-
|
|
173
|
-
if (allSrcFiles.length === 0 || !testingDirectory) {
|
|
174
|
-
console.error(
|
|
175
|
-
'Could not find any source files to generate unit tests. Please run: cloving init. Then try again.',
|
|
176
|
-
)
|
|
177
|
-
process.exit(1)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const testFiles = execFileSync('find', [testingDirectory, '-type', 'f']).toString().trim()
|
|
181
|
-
let contextFiles: string
|
|
182
|
-
|
|
183
|
-
if (files) {
|
|
184
|
-
console.log('Generating unit tests for the provided files...')
|
|
185
|
-
console.log('Gathering a list of files related to the changes...')
|
|
186
|
-
const prompt = this.generateUnitTestPrompt(files, allSrcFiles.join('\n'), testFiles)
|
|
187
|
-
contextFiles = await this.gpt.generateText({ prompt, messages: this.chatHistory })
|
|
188
|
-
} else {
|
|
189
|
-
console.log(
|
|
190
|
-
'Generating unit tests for the git diff between this branch and the default branch...',
|
|
191
|
-
)
|
|
192
|
-
console.log('Gathering a list of files related to the changes...')
|
|
193
|
-
const gitDiff = await getGitDiff()
|
|
194
|
-
const changedFiles = this.extractChangedFiles(gitDiff)
|
|
195
|
-
const prompt = this.generateUnitTestPrompt(
|
|
196
|
-
changedFiles,
|
|
197
|
-
allSrcFiles.join('\n'),
|
|
198
|
-
testFiles,
|
|
199
|
-
gitDiff,
|
|
200
|
-
)
|
|
201
|
-
contextFiles = await this.gpt.generateText({ prompt, messages: this.chatHistory })
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
this.chatHistory.push({ role: 'assistant', content: contextFiles })
|
|
205
|
-
contextFiles = extractMarkdown(contextFiles)
|
|
206
|
-
|
|
207
|
-
console.log(contextFiles)
|
|
208
|
-
console.log('\nBuilding the tests for these files...')
|
|
209
|
-
|
|
210
|
-
// Initialize variables
|
|
211
|
-
const lines: string[] = []
|
|
212
|
-
const context: string[] = []
|
|
213
|
-
|
|
214
|
-
// Read each line of the context files
|
|
215
|
-
contextFiles.split('\n').forEach((line) => {
|
|
216
|
-
if (line.trim()) {
|
|
217
|
-
lines.push(line.trim())
|
|
218
|
-
}
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
for (const codeFile of lines) {
|
|
222
|
-
if (
|
|
223
|
-
await fs
|
|
224
|
-
.stat(codeFile)
|
|
225
|
-
.then((stat) => stat.isFile())
|
|
226
|
-
.catch(() => false)
|
|
227
|
-
) {
|
|
228
|
-
const fileContents = await fs.readFile(codeFile, 'utf-8')
|
|
229
|
-
context.push(
|
|
230
|
-
`### Contents of **${codeFile}\n\n${fileContents
|
|
231
|
-
.split('\n')
|
|
232
|
-
.map((line) => ` ${line}`)
|
|
233
|
-
.join('\n')}**\n\n`,
|
|
234
|
-
)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Generate the message for unit test creation
|
|
239
|
-
let analysis = ''
|
|
240
|
-
|
|
241
|
-
if ((files || []).length > 0) {
|
|
242
|
-
const message = `Create unit tests for the List of Files. Always show filenames for the generated code.
|
|
243
|
-
|
|
244
|
-
## List of Files
|
|
245
|
-
${(files || []).join('\n')}
|
|
246
|
-
|
|
247
|
-
${CODEGEN_INSTRUCTIONS}
|
|
248
|
-
|
|
249
|
-
## Context
|
|
250
|
-
|
|
251
|
-
${context.join('\n\n')}
|
|
252
|
-
|
|
253
|
-
## Prompt
|
|
254
|
-
|
|
255
|
-
Create unit tests for the List of Files. Always show filenames for the generated code.`
|
|
256
|
-
|
|
257
|
-
// Get the model and analysis using ClovingGPT
|
|
258
|
-
this.chatHistory.push({ role: 'user', content: message })
|
|
259
|
-
analysis = await this.gpt.generateText({ prompt: message, messages: this.chatHistory })
|
|
260
|
-
} else {
|
|
261
|
-
const message = `Create unit tests for the Code Diff. Always show filenames for the generated code.
|
|
262
|
-
|
|
263
|
-
## Code Diff
|
|
264
|
-
|
|
265
|
-
${await getGitDiff()}
|
|
266
|
-
|
|
267
|
-
## Context
|
|
268
|
-
|
|
269
|
-
${context.join('\n\n')}
|
|
270
|
-
|
|
271
|
-
${CODEGEN_INSTRUCTIONS}
|
|
272
|
-
|
|
273
|
-
## Prompt
|
|
274
|
-
|
|
275
|
-
Create unit tests for the Code Diff. Always show filenames for the generated code.`
|
|
276
|
-
|
|
277
|
-
// Get the model and analysis using ClovingGPT
|
|
278
|
-
this.chatHistory.push({ role: 'user', content: message })
|
|
279
|
-
analysis = await this.gpt.generateText({ prompt: message, messages: this.chatHistory })
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (analysis) {
|
|
283
|
-
console.log(highlight(analysis))
|
|
284
|
-
} else {
|
|
285
|
-
console.log('No unit tests generated.')
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Add the new user action handling
|
|
289
|
-
await this.handleUserAction(analysis, save)
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
export default UnitTestManager
|