cloving 0.3.18 → 0.3.19

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.
Files changed (94) hide show
  1. package/.DS_Store +0 -0
  2. package/.aider.chat.history.md +16978 -0
  3. package/.aider.input.history +15 -0
  4. package/.aider.tags.cache.v3/cache.db +0 -0
  5. package/.aider.tags.cache.v3/cache.db-shm +0 -0
  6. package/.aider.tags.cache.v3/cache.db-wal +0 -0
  7. package/.cursorrules +77 -0
  8. package/.github/workflows/auto-release.yml +83 -0
  9. package/.github/workflows/ci.yml +32 -0
  10. package/.github/workflows/npm-publish.yml +34 -0
  11. package/.gitignore +67 -0
  12. package/.husky/_/.gitignore +1 -0
  13. package/.husky/_/applypatch-msg +2 -0
  14. package/.husky/_/commit-msg +2 -0
  15. package/.husky/_/h +22 -0
  16. package/.husky/_/husky.sh +9 -0
  17. package/.husky/_/post-applypatch +2 -0
  18. package/.husky/_/post-checkout +2 -0
  19. package/.husky/_/post-commit +2 -0
  20. package/.husky/_/post-merge +2 -0
  21. package/.husky/_/post-rewrite +2 -0
  22. package/.husky/_/pre-applypatch +2 -0
  23. package/.husky/_/pre-auto-gc +2 -0
  24. package/.husky/_/pre-commit +2 -0
  25. package/.husky/_/pre-merge-commit +2 -0
  26. package/.husky/_/pre-push +2 -0
  27. package/.husky/_/pre-rebase +2 -0
  28. package/.husky/_/prepare-commit-msg +2 -0
  29. package/.husky/pre-commit +1 -0
  30. package/.husky/pre-push +1 -0
  31. package/.prettierrc.json +7 -0
  32. package/.tool-versions +2 -0
  33. package/SECURITY.md +15 -0
  34. package/cloving.json +67 -0
  35. package/dist/managers/CommitManager.js +8 -2
  36. package/dist/package.json +1 -1
  37. package/jest.config.js +11 -0
  38. package/package.json +1 -1
  39. package/src/cloving_gpt/adapters/azureopenai.ts +83 -0
  40. package/src/cloving_gpt/adapters/claude.ts +140 -0
  41. package/src/cloving_gpt/adapters/gemini.ts +146 -0
  42. package/src/cloving_gpt/adapters/index.ts +43 -0
  43. package/src/cloving_gpt/adapters/mistral.ts +142 -0
  44. package/src/cloving_gpt/adapters/ollama.ts +136 -0
  45. package/src/cloving_gpt/adapters/openai.ts +113 -0
  46. package/src/cloving_gpt/index.ts +229 -0
  47. package/src/commands/chat.ts +12 -0
  48. package/src/commands/config.ts +192 -0
  49. package/src/commands/generate/code.ts +18 -0
  50. package/src/commands/generate/commit.ts +9 -0
  51. package/src/commands/generate/context.ts +93 -0
  52. package/src/commands/generate/docs.ts +9 -0
  53. package/src/commands/generate/review.ts +9 -0
  54. package/src/commands/generate/shell.ts +9 -0
  55. package/src/commands/generate/unit_tests.ts +9 -0
  56. package/src/commands/init.ts +9 -0
  57. package/src/commands/models.ts +21 -0
  58. package/src/commands/proxy.ts +9 -0
  59. package/src/commands/tokens.ts +82 -0
  60. package/src/index.ts +197 -0
  61. package/src/managers/BlockManager.ts +126 -0
  62. package/src/managers/ChatManager.ts +755 -0
  63. package/src/managers/ChunkManager.ts +31 -0
  64. package/src/managers/CodeManager.ts +226 -0
  65. package/src/managers/CommitManager.ts +71 -0
  66. package/src/managers/DocsManager.ts +101 -0
  67. package/src/managers/InitManager.ts +263 -0
  68. package/src/managers/ProxyManager.ts +240 -0
  69. package/src/managers/ReviewManager.ts +154 -0
  70. package/src/managers/ShellManager.ts +172 -0
  71. package/src/managers/StreamManager.ts +353 -0
  72. package/src/managers/UnitTestManager.ts +293 -0
  73. package/src/utils/config_utils.ts +271 -0
  74. package/src/utils/git_utils.ts +230 -0
  75. package/src/utils/model_utils.ts +3 -0
  76. package/src/utils/prompt_utils.ts +375 -0
  77. package/src/utils/prompts/CODEGEN_COULDNT_APPLY.md +5 -0
  78. package/src/utils/prompts/CODEGEN_EXAMPLES.md +82 -0
  79. package/src/utils/prompts/CODEGEN_INSTRUCTIONS.md +43 -0
  80. package/src/utils/prompts/DOCS_INSTRUCTIONS.md +9 -0
  81. package/src/utils/prompts/INIT_INSTRUCTIONS.md +132 -0
  82. package/src/utils/prompts/REVIEW_INSTRUCTIONS.md +49 -0
  83. package/src/utils/prompts/SHELL_INSTRUCTIONS.md +15 -0
  84. package/src/utils/prompts/SPECIAL_FILES.md +66 -0
  85. package/src/utils/prompts/index.ts +37 -0
  86. package/src/utils/string_utils.ts +552 -0
  87. package/src/utils/types.ts +127 -0
  88. package/tests/commands/generate/unit_tests.test.ts +27 -0
  89. package/tests/utils/git_utils.test.ts +116 -0
  90. package/tests/utils/model_utils.test.ts +27 -0
  91. package/tests/utils/prompt_utils.test.ts +33 -0
  92. package/tests/utils/string_utils.test.ts +560 -0
  93. package/tsconfig.json +14 -0
  94. package/yarn.lock +5823 -0
@@ -0,0 +1,31 @@
1
+ import { EventEmitter } from 'events'
2
+
3
+ // A chunk is a JSON object with different keys depending on the AI service provider
4
+ //
5
+ // Some providers produce single response in a single chunk
6
+ //
7
+ // Others produce multiple responses in a single chunk
8
+ //
9
+ // This Chunk Manager's job is to buffer the chunks and emit them as a single string
10
+ // without automatically clearing the buffer until clearBuffer is called.
11
+ // That way, the consumer of the chunk can process it and clear the buffer after
12
+ // they have received enough data.
13
+ class ChunkManager extends EventEmitter {
14
+ private buffer: string = ''
15
+
16
+ addChunk(chunk: string | undefined) {
17
+ if (!chunk) return
18
+ this.buffer += chunk
19
+ this.emit('content', this.buffer)
20
+ }
21
+
22
+ clearBuffer(lastChar: number) {
23
+ this.buffer = this.buffer.substring(lastChar)
24
+ }
25
+
26
+ getBuffer() {
27
+ return this.buffer
28
+ }
29
+ }
30
+
31
+ export default ChunkManager
@@ -0,0 +1,226 @@
1
+ import { select, input, confirm } from '@inquirer/prompts'
2
+ import { AxiosError } from 'axios'
3
+ import highlight from 'cli-highlight'
4
+ import colors from 'colors'
5
+
6
+ import ClovingGPT from '../cloving_gpt'
7
+
8
+ import { getConfig } from '../utils/config_utils'
9
+ import { generateCodegenPrompt, addFileOrDirectoryToContext } from '../utils/prompt_utils'
10
+ import {
11
+ checkBlocksApplicability,
12
+ extractCurrentNewBlocks,
13
+ applyAndSaveCurrentNewBlocks,
14
+ } from '../utils/string_utils'
15
+ import { CODEGEN_COULDNT_APPLY } from '../utils/prompts'
16
+ import type { ClovingGPTOptions } from '../utils/types'
17
+ import StreamManager from './StreamManager'
18
+
19
+ class CodeManager extends StreamManager {
20
+ constructor(options: ClovingGPTOptions) {
21
+ super(options)
22
+ options.silent = getConfig(options).globalSilent || false
23
+ options.stream = true
24
+ this.gpt = new ClovingGPT(options)
25
+ }
26
+
27
+ public async initialize(): Promise<void> {
28
+ try {
29
+ if (this.options.files) {
30
+ for (const file of this.options.files) {
31
+ this.contextFiles = await addFileOrDirectoryToContext(
32
+ file,
33
+ this.contextFiles,
34
+ this.options,
35
+ )
36
+ }
37
+ } else {
38
+ let includeMoreFiles = true
39
+
40
+ while (includeMoreFiles) {
41
+ const contextFile = await input({
42
+ message: `Enter the relative path of a file or directory you would like to include as context (or press enter to continue): `,
43
+ })
44
+
45
+ if (contextFile) {
46
+ this.contextFiles = await addFileOrDirectoryToContext(
47
+ contextFile,
48
+ this.contextFiles,
49
+ this.options,
50
+ )
51
+ } else {
52
+ includeMoreFiles = false
53
+ }
54
+ }
55
+ }
56
+
57
+ if (!this.options.prompt) {
58
+ const userPrompt = await input({
59
+ message: 'What would you like the code to do:',
60
+ })
61
+ this.options.prompt = userPrompt
62
+ }
63
+
64
+ await this.generateCode(this.options.prompt)
65
+ } catch (err) {
66
+ const error = err as AxiosError
67
+ console.error('Could not generate code', error.message)
68
+ }
69
+ }
70
+
71
+ public async generateCode(userPrompt: string): Promise<void> {
72
+ await this.checkForLatestVersion()
73
+ await this.loadContextFiles()
74
+
75
+ if (this.chatHistory.length === 0) {
76
+ this.addUserPrompt(generateCodegenPrompt(this.contextFiles))
77
+ this.addAssistantResponse('What would you like to do?')
78
+ }
79
+
80
+ this.addUserPrompt(userPrompt)
81
+
82
+ try {
83
+ const responseStream = await this.gpt.streamText({
84
+ prompt: userPrompt,
85
+ messages: this.chatHistory,
86
+ })
87
+
88
+ this.handleResponseStream(responseStream)
89
+ } catch (err) {
90
+ const error = err as AxiosError
91
+ console.error('Error streaming response:', error.message)
92
+ }
93
+ }
94
+
95
+ private async handleUserAction(response: string): Promise<void> {
96
+ const currentNewBlocks = extractCurrentNewBlocks(response)
97
+ const files = Array.from(new Set(currentNewBlocks.map((block) => block.filePath)))
98
+
99
+ if (this.options.save) {
100
+ await applyAndSaveCurrentNewBlocks(currentNewBlocks)
101
+ return
102
+ }
103
+
104
+ const action = await select({
105
+ message: 'What would you like to do?',
106
+ choices: [
107
+ { name: 'Revise', value: 'revise' },
108
+ { name: 'Explain', value: 'explain' },
109
+ { name: 'Save a Source Code File', value: 'save' },
110
+ { name: 'Save All Source Code Files', value: 'saveAll' },
111
+ { name: 'Copy Source Code to Clipboard', value: 'copySource' },
112
+ { name: 'Copy Entire Response to Clipboard', value: 'copyAll' },
113
+ { name: 'Done', value: 'done' },
114
+ ],
115
+ })
116
+
117
+ switch (action) {
118
+ case 'revise':
119
+ const newPrompt = await input({
120
+ message: 'How would you like to modify the output:',
121
+ })
122
+ let includeMoreFiles = true
123
+ while (includeMoreFiles) {
124
+ const contextFile = await input({
125
+ message:
126
+ 'Enter the relative path of a file or directory you would like to include as context (or press enter to continue):',
127
+ })
128
+
129
+ if (contextFile) {
130
+ this.contextFiles = await addFileOrDirectoryToContext(
131
+ contextFile,
132
+ this.contextFiles,
133
+ this.options,
134
+ )
135
+ } else {
136
+ includeMoreFiles = false
137
+ }
138
+ }
139
+ await this.generateCode(newPrompt)
140
+ break
141
+ case 'explain':
142
+ const explainPrompt = this.generateExplainCodePrompt(response)
143
+ this.chatHistory.push({ role: 'user', content: explainPrompt })
144
+ const explainCodeCommand = await this.gpt.generateText({
145
+ prompt: explainPrompt,
146
+ messages: this.chatHistory,
147
+ })
148
+ console.log(highlight(explainCodeCommand, { language: 'markdown' }))
149
+ break
150
+ case 'save':
151
+ let saveAnother = true
152
+ while (saveAnother) {
153
+ const fileToSave = await select({
154
+ message: 'Which file do you want to save?',
155
+ choices: files.map((file) => ({ name: file, value: file })),
156
+ })
157
+
158
+ await applyAndSaveCurrentNewBlocks(
159
+ currentNewBlocks.filter((block) => block.filePath === fileToSave),
160
+ )
161
+
162
+ const saveMore = await confirm({
163
+ message: 'Do you want to save another file?',
164
+ default: true,
165
+ })
166
+
167
+ saveAnother = saveMore
168
+ }
169
+ break
170
+ case 'saveAll':
171
+ await applyAndSaveCurrentNewBlocks(currentNewBlocks)
172
+ console.log(colors.green('\nAll files have been saved.'))
173
+ break
174
+ case 'done':
175
+ break
176
+ }
177
+ }
178
+
179
+ private generateExplainCodePrompt(prompt: string): string {
180
+ return `${prompt}
181
+
182
+ # Task
183
+
184
+ Please briefly explain how the code works in this.`
185
+ }
186
+
187
+ /**
188
+ * Finalizes the response processing.
189
+ * This method should be overridden to provide specific finalization behavior.
190
+ * @protected
191
+ */
192
+ protected async finalizeResponse(): Promise<void> {
193
+ this.addAssistantResponse(this.responseString)
194
+ this.isProcessing = false
195
+
196
+ if (this.responseString !== '') {
197
+ const currentNewBlocks = extractCurrentNewBlocks(this.responseString)
198
+ const [canApply, summary] = await checkBlocksApplicability(currentNewBlocks)
199
+ if (!canApply) {
200
+ await this.handleUnapplicableBlocks(summary)
201
+ } else {
202
+ this.retryCount = 0 // Reset the retry count on successful application
203
+ }
204
+ }
205
+
206
+ await this.handleUserAction(this.responseString)
207
+ }
208
+
209
+ private async handleUnapplicableBlocks(summary: string): Promise<void> {
210
+ if (this.retryCount < this.maxRetries) {
211
+ console.log(
212
+ `\n\n${summary}\n\n${colors.yellow.bold('WARNING')} Some of the provided code blocks could not be automatically applied. Retrying (Attempt ${this.retryCount + 1}/${this.maxRetries})...\n`,
213
+ )
214
+
215
+ this.retryCount++
216
+ await this.generateCode(`${CODEGEN_COULDNT_APPLY}\n\n${summary}`)
217
+ } else {
218
+ console.log(
219
+ `\n\n${colors.red.bold('ERROR')} Failed to generate a diff that could be cleanly applied after ${this.maxRetries} attempts. Please review the changes manually and try again.\n`,
220
+ )
221
+ this.retryCount = 0 // Reset the retry count for future attempts
222
+ }
223
+ }
224
+ }
225
+
226
+ export default CodeManager
@@ -0,0 +1,71 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { execFileSync, execSync } from 'child_process'
4
+ import { extractMarkdown } from '../utils/string_utils'
5
+ import { generateCommitMessagePrompt } from '../utils/git_utils'
6
+ import { getConfig } from '../utils/config_utils'
7
+ import ClovingGPT from '../cloving_gpt'
8
+ import type { ClovingGPTOptions } from '../utils/types'
9
+ import type { AxiosError } from 'axios'
10
+
11
+ class CommitManager {
12
+ private gpt: ClovingGPT
13
+
14
+ constructor(options: ClovingGPTOptions) {
15
+ options.silent = getConfig(options).globalSilent || false
16
+ this.gpt = new ClovingGPT(options)
17
+ }
18
+
19
+ private cleanCommitMessage(message: string): string {
20
+ // Remove markdown code block formatting if present, including any text before and after
21
+ const codeBlockRegex = /.*?```.*\n(.*?)\n```.*/m
22
+ const match = message.match(codeBlockRegex)
23
+ return match ? match[1].trim() : message.trim()
24
+ }
25
+
26
+ public async generateCommit(): Promise<void> {
27
+ try {
28
+ // Get the git diff
29
+ const diff = execSync('git diff HEAD').toString().trim()
30
+
31
+ // Check if the diff is blank
32
+ if (!diff) {
33
+ console.error('No changes to commit.')
34
+ return
35
+ }
36
+
37
+ // Generate the prompt for commit message
38
+ const prompt = generateCommitMessagePrompt(diff)
39
+
40
+ // Instantiate ClovingGPT and get the commit message
41
+ const rawCommitMessage = await this.gpt.generateText({ prompt })
42
+
43
+ // Clean the commit message using extractMarkdown and remove code blocks
44
+ const commitMessage = this.cleanCommitMessage(extractMarkdown(rawCommitMessage))
45
+
46
+ // Write the commit message to a temporary file
47
+ const tempCommitFilePath = path.join('.git', 'SUGGESTED_COMMIT_EDITMSG')
48
+ fs.writeFileSync(tempCommitFilePath, commitMessage)
49
+
50
+ // Commit the changes using the generated commit message
51
+ try {
52
+ execFileSync('git', ['commit', '-a', '--edit', '--file', tempCommitFilePath], {
53
+ stdio: 'inherit',
54
+ })
55
+ } catch (commitError) {
56
+ // If commit is canceled (non-zero exit), handle it here
57
+ console.log('Commit was canceled or failed.')
58
+ }
59
+
60
+ // Remove the temporary file using fs
61
+ fs.unlink(tempCommitFilePath, (err) => {
62
+ if (err) throw err
63
+ })
64
+ } catch (err) {
65
+ const error = err as AxiosError
66
+ console.error('Could not generate commit message:', error.message)
67
+ }
68
+ }
69
+ }
70
+
71
+ export default CommitManager
@@ -0,0 +1,101 @@
1
+ import { confirm } from '@inquirer/prompts'
2
+ import colors from 'colors'
3
+
4
+ import ClovingGPT from '../cloving_gpt'
5
+ import { getConfig } from '../utils/config_utils'
6
+ import { CODEGEN_COULDNT_APPLY } from '../utils/prompts'
7
+ import { generateDocsPrompt } from '../utils/prompt_utils'
8
+ import {
9
+ applyAndSaveCurrentNewBlocks,
10
+ extractCurrentNewBlocks,
11
+ checkBlocksApplicability,
12
+ } from '../utils/string_utils'
13
+ import type { ClovingGPTOptions } from '../utils/types'
14
+ import StreamManager from './StreamManager'
15
+
16
+ /**
17
+ * Manages the generation of documentation for specified files.
18
+ */
19
+ class DocsManager extends StreamManager {
20
+ /**
21
+ * Creates a new DocumentationManager instance.
22
+ * @param {ClovingGPTOptions} options - Configuration options for the DocumentationManager.
23
+ */
24
+ constructor(options: ClovingGPTOptions) {
25
+ super(options)
26
+ options.silent = getConfig(options).globalSilent || false
27
+ options.stream = true
28
+ this.gpt = new ClovingGPT(options)
29
+ }
30
+
31
+ /**
32
+ * Generates documentation for the specified files.
33
+ * @returns {Promise<void>}
34
+ */
35
+ public async generateDocs(extraPrompt?: string): Promise<void> {
36
+ try {
37
+ this.isProcessing = true
38
+ if (this.retryCount === 0) {
39
+ await this.checkForLatestVersion()
40
+ }
41
+ await this.loadContextFiles()
42
+ this.addUserPrompt(`${generateDocsPrompt(this.contextFiles)}\n\n${extraPrompt || ''}`)
43
+
44
+ const responseStream = await this.gpt.streamText({
45
+ prompt: this.prompt,
46
+ messages: this.chatHistory,
47
+ })
48
+
49
+ this.handleResponseStream(responseStream)
50
+ } catch (error) {
51
+ console.error(colors.red('Error generating documentation:'), error)
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Finalizes the response processing.
57
+ * This method should be overridden to provide specific finalization behavior.
58
+ * @protected
59
+ */
60
+ protected async finalizeResponse(): Promise<void> {
61
+ this.addAssistantResponse(this.responseString)
62
+ this.isProcessing = false
63
+
64
+ if (this.responseString !== '') {
65
+ const currentNewBlocks = extractCurrentNewBlocks(this.responseString)
66
+ const [canApply, summary] = await checkBlocksApplicability(currentNewBlocks)
67
+ if (canApply && currentNewBlocks.length > 0) {
68
+ const shouldSave = await confirm({
69
+ message: '\n\nDo you want to save the generated documentation?',
70
+ default: true,
71
+ })
72
+
73
+ if (shouldSave) {
74
+ await applyAndSaveCurrentNewBlocks(currentNewBlocks)
75
+ console.log(colors.green('\nDocumentation has been saved to the respective files.'))
76
+ }
77
+ } else {
78
+ if (this.retryCount < this.maxRetries) {
79
+ console.log(
80
+ `\n\n${summary}\n\n${colors.yellow.bold('WARNING')} Some of the provided code blocks could not be automatically applied. Retrying (Attempt ${this.retryCount + 1}/${this.maxRetries})...\n`,
81
+ )
82
+
83
+ this.retryCount++
84
+ this.generateDocs(`\n\n${CODEGEN_COULDNT_APPLY}\n\n${summary}`)
85
+
86
+ return
87
+ } else {
88
+ console.log(
89
+ `\n\n${colors.red.bold('ERROR')} Failed to generate a diff that could be cleanly applied after ${this.maxRetries} attempts. Please review the changes manually and try again.\n`,
90
+ )
91
+ this.retryCount = 0 // Reset the retry count for future attempts
92
+ }
93
+ }
94
+ }
95
+
96
+ // Reset responseString after processing
97
+ this.responseString = ''
98
+ }
99
+ }
100
+
101
+ export default DocsManager
@@ -0,0 +1,263 @@
1
+ import fs from 'fs'
2
+ import os from 'os'
3
+ import path from 'path'
4
+ import colors from 'colors'
5
+ import ignore from 'ignore'
6
+ import { promises as fsPromises } from 'fs'
7
+ import { confirm } from '@inquirer/prompts'
8
+
9
+ import ClovingGPT from '../cloving_gpt'
10
+ import { extractJsonMetadata } from '../utils/string_utils'
11
+ import { getConfig } from '../utils/config_utils'
12
+ import {
13
+ generateFileList,
14
+ collectSpecialFileContents,
15
+ checkForSpecialFiles,
16
+ } from '../utils/prompt_utils'
17
+ import { INIT_INSTRUCTIONS } from '../utils/prompts'
18
+ import type { ClovingGPTOptions, ChatMessage } from '../utils/types'
19
+
20
+ class InitManager {
21
+ private gpt: ClovingGPT
22
+ private chatHistory: ChatMessage[] = []
23
+
24
+ constructor(private options: ClovingGPTOptions) {
25
+ this.options.silent = getConfig(this.options).globalSilent || false
26
+ this.gpt = new ClovingGPT(this.options)
27
+ this.chatHistory.push({
28
+ role: 'system',
29
+ content: INIT_INSTRUCTIONS,
30
+ })
31
+ }
32
+
33
+ /**
34
+ * Initializes the InitManager by collecting special file contents, checking for dependencies,
35
+ * and processing the AI response for the project details.
36
+ *
37
+ * @return {Promise<void>} A promise that resolves when the initialization is complete.
38
+ */
39
+ public async initialize(): Promise<void> {
40
+ const specialFileContents = collectSpecialFileContents()
41
+ const specialFileNames = Object.keys(specialFileContents).map((file) => ' - ' + file)
42
+
43
+ this.logInitializationMessage(specialFileNames)
44
+
45
+ const config = getConfig(this.options)
46
+ if (!config || !config?.models) {
47
+ console.error('No cloving configuration found. Please run `cloving config`')
48
+ return
49
+ }
50
+
51
+ if (!checkForSpecialFiles()) {
52
+ console.error(
53
+ 'No dependencies files detected. Please add a dependency file (e.g. package.json, Gemfile, requirements.txt, etc.) to your project and run `cloving init` again.',
54
+ )
55
+ return
56
+ }
57
+
58
+ const tempFilePath = path.join(os.tmpdir(), `describe_${Date.now()}.tmp`)
59
+
60
+ try {
61
+ const limitedFileList = await this.getFilteredFileList()
62
+
63
+ const projectDetails = {
64
+ files: limitedFileList,
65
+ specialFiles: specialFileContents,
66
+ }
67
+
68
+ const [success, errorMessage] = await this.processAIResponse(projectDetails, tempFilePath)
69
+
70
+ if (!success) {
71
+ console.log(errorMessage)
72
+ return
73
+ }
74
+
75
+ await this.promptUserForReview()
76
+
77
+ // Clean up
78
+ await fsPromises.unlink(tempFilePath)
79
+ } catch (error) {
80
+ console.error('Error describing the project:', (error as Error).message)
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Checks if a directory exists at the specified path.
86
+ *
87
+ * @param {string} directory - The path of the directory to check.
88
+ * @return {Promise<boolean>} A promise that resolves to true if the directory exists, false otherwise.
89
+ */
90
+ private async directoryExists(directory: string): Promise<boolean> {
91
+ try {
92
+ const stats = await fsPromises.stat(directory)
93
+ return stats.isDirectory()
94
+ } catch (error) {
95
+ return false
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Returns a filtered list of files, excluding those ignored by the .gitignore file.
101
+ *
102
+ * @return {string[]} A list of file paths, limited to the first 100 files.
103
+ */
104
+ private async getFilteredFileList(): Promise<string[]> {
105
+ const gitignorePath = path.join(process.cwd(), '.gitignore')
106
+ const ig = ignore()
107
+ if (fs.existsSync(gitignorePath)) {
108
+ const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8')
109
+ ig.add(gitignoreContent)
110
+ }
111
+
112
+ const fileList = await generateFileList()
113
+ const filteredFileList = fileList.filter((file) => {
114
+ try {
115
+ return !ig.ignores(file)
116
+ } catch (error) {
117
+ return false
118
+ }
119
+ })
120
+
121
+ return filteredFileList.slice(0, 100)
122
+ }
123
+
124
+ /**
125
+ * Logs the initialization message based on the special file names and silent mode.
126
+ *
127
+ * @param {string[]} specialFileNames - A list of special file names to be analyzed.
128
+ * @return {void} No return value.
129
+ */
130
+ private logInitializationMessage(specialFileNames: string[]): void {
131
+ if (!this.options.silent) {
132
+ if (specialFileNames.length > 0) {
133
+ console.log(`Cloving will analyze the list of files and the contents of the following files:
134
+
135
+ ${specialFileNames.join('\n')}
136
+
137
+ Cloving will send AI a request to summarize the technologies used in this project.
138
+
139
+ This will provide better context for future Cloving requests.`)
140
+ } else {
141
+ console.log(`
142
+ This script will analyze the list of files in the current directory using GPT to summarize the
143
+ technologies used. This will provide better context for future Cloving requests.
144
+ `)
145
+ }
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Processes the AI response by generating a prompt, sending it to the AI,
151
+ * and saving the response to a temporary file and cloving.json.
152
+ *
153
+ * @param {any} projectDetails - A JSON object describing the project.
154
+ * @param {string} tempFilePath - The path to the temporary file.
155
+ * @param {string} errorMessage - An error message if there was an error.
156
+ * @return {[boolean, string]} A promise that resolves with an array containing a boolean
157
+ * indicating whether the AI response was successfully processed,
158
+ * and a message explaining any errors.
159
+ */
160
+ private async processAIResponse(
161
+ projectDetails: any,
162
+ tempFilePath: string,
163
+ errorMessage: string = '',
164
+ ): Promise<[boolean, string]> {
165
+ const prompt =
166
+ errorMessage !== ''
167
+ ? errorMessage
168
+ : `Here is a JSON object describing my project:
169
+ ${JSON.stringify(projectDetails, null, 2)}`
170
+ this.chatHistory.push({ role: 'user', content: prompt })
171
+ const aiChatResponse = await this.gpt.generateText({ prompt, messages: this.chatHistory })
172
+ this.chatHistory.push({ role: 'assistant', content: aiChatResponse })
173
+ let cleanAiChatResponse = extractJsonMetadata(aiChatResponse)
174
+ const [validSyntax, syntaxErrorMessage] = this.checkClovingJsonSyntax(cleanAiChatResponse)
175
+
176
+ if (!validSyntax) {
177
+ console.log(syntaxErrorMessage)
178
+ console.log('Retrying...')
179
+ return this.processAIResponse(projectDetails, tempFilePath, syntaxErrorMessage)
180
+ } else {
181
+ await fsPromises.writeFile(tempFilePath, cleanAiChatResponse)
182
+
183
+ // Save the AI chat response to cloving.json
184
+ await fsPromises.writeFile('cloving.json', cleanAiChatResponse)
185
+ console.log(`Project data saved to ${colors.green.bold('cloving.json')}`)
186
+ return [true, '']
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Checks the syntax of the generated cloving.json file.
192
+ *
193
+ * @param {string} cleanAiChatResponse - The cleaned AI chat response.
194
+ * @return {[boolean, string]} An array where the first element is a boolean indicating
195
+ * whether the syntax was valid, and the second element is a
196
+ * message explaining any errors.
197
+ */
198
+ private checkClovingJsonSyntax(cleanAiChatResponse: string): [boolean, string] {
199
+ try {
200
+ JSON.parse(cleanAiChatResponse)
201
+ } catch (error) {
202
+ return [
203
+ false,
204
+ `${colors.red.bold('ERROR:')} parsing cloving.json: ${colors.red.bold((error as Error).message)}`,
205
+ ]
206
+ }
207
+
208
+ const clovingJson = JSON.parse(cleanAiChatResponse)
209
+
210
+ const directoriesToCheck = [
211
+ ...clovingJson.languages.map((language: any) => ({
212
+ type: 'Language',
213
+ name: language.name,
214
+ directory: language.directory,
215
+ })),
216
+ ...clovingJson.frameworks.map((framework: any) => ({
217
+ type: 'Framework',
218
+ name: framework.name,
219
+ directory: framework.directory,
220
+ })),
221
+ ...clovingJson.testingFrameworks.map((testingFramework: any) => ({
222
+ type: 'Testing Framework',
223
+ name: testingFramework.name,
224
+ directory: testingFramework.directory,
225
+ })),
226
+ ]
227
+
228
+ const missingDirectories = directoriesToCheck.filter((item) => !fs.existsSync(item.directory))
229
+
230
+ if (missingDirectories.length > 0) {
231
+ const missingDirMessages = missingDirectories.map(
232
+ (item) => `${item.type} ${colors.bold(item.name)}: ${colors.red.bold(item.directory)}`,
233
+ )
234
+ return [
235
+ false,
236
+ `${colors.red.bold('ERROR:')} The following directories were not found:\n${missingDirMessages.join('\n')}`,
237
+ ]
238
+ }
239
+
240
+ return [true, '']
241
+ }
242
+
243
+ /**
244
+ * Prompts the user to review the generated data if the `silent` option is not set.
245
+ * If the user confirms, reads the content of 'cloving.json' and logs it to the console.
246
+ *
247
+ * @return {Promise<void>} A promise that resolves when the review is complete.
248
+ */
249
+ private async promptUserForReview(): Promise<void> {
250
+ if (!this.options.silent) {
251
+ const reviewAnswer = await confirm({
252
+ message: 'Do you want to review the generated data?',
253
+ default: true,
254
+ })
255
+ if (reviewAnswer) {
256
+ const clovingJsonContent = await fsPromises.readFile('cloving.json', 'utf-8')
257
+ console.log(clovingJsonContent)
258
+ }
259
+ }
260
+ }
261
+ }
262
+
263
+ export default InitManager