-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcbfg.js
executable file
·262 lines (244 loc) · 9.21 KB
/
cbfg.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
#!/usr/bin/env node
/**
* @fileoverview Codebase Bundler for Grok. A CLI tool to bundle codebase files from a directory into bundle(s) to share with Grok 2 AI.
* Note: This content is partially AI-generated.
*/
import { readFile, readdir, stat, writeFile, access } from 'node:fs/promises';
import { join, basename, sep } from 'node:path';
import { createInterface } from 'node:readline';
// ANSI Colors
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
};
// Wrapper for console.log with color support
const colorLog = (message, color = colors.reset) =>
console.log(`${color}${message}${colors.reset}`);
const MAX_FILE_SIZE = 100 * 1024; // 100 KB per file limit
const HARD_TOTAL_CHAR_LIMIT = 30000; // Hard limit for total characters
const LOG_EVERY_N_FILES = 100; // Log progress every 100 files
let bundleCount = 0;
let totalCharacters = 0;
/**
* Asks for user confirmation before proceeding with a potentially dangerous operation.
* @param {string} message - The confirmation message to display to the user.
* @returns {Promise<boolean>} A promise that resolves to true if the user confirms, false otherwise.
*/
const confirmAction = async (message) => {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) =>
rl.question(`${colors.yellow}${message}${colors.reset}`, (answer) => {
rl.close();
resolve(
answer.trim().toLowerCase() === 'y' ||
answer.trim().toLowerCase() === 'yes',
);
}),
);
};
/**
* Formats file content with path comment and markdown code block.
* @param {string} filePath - The path of the file.
* @param {string} content - The content of the file.
* @returns {string} Formatted content with markdown code block.
*/
const formatFileContent = (filePath, content) =>
`// File: ${filePath}\n\`\`\`\n${content}\n\`\`\`\n`;
/**
* Writes the current bundle to a file and resets the bundle.
* @param {Array<string>} bundle - The content of files in the current bundle.
*/
const writeBundle = async (bundle) => {
bundleCount++;
const outputPath = join(
process.cwd(),
`bundled_codebase_${bundleCount}.txt`,
);
try {
await writeFile(outputPath, bundle.join(''), 'utf-8');
totalCharacters += bundle.join('').length;
colorLog(`Bundle ${bundleCount} written.`, colors.green);
} catch (writeErr) {
colorLog(`Error writing bundle ${bundleCount}:`, colors.red, writeErr);
}
};
/**
* Manages adding a file to the current bundle or starting a new one if necessary.
*/
const addToBundle = (() => {
let currentBundle = [];
let currentChars = 0;
return {
add: async (filePath, content) => {
const formattedContent = formatFileContent(filePath, content);
const fileChars = formattedContent.length;
if (currentChars + fileChars > HARD_TOTAL_CHAR_LIMIT) {
await writeBundle(currentBundle);
currentBundle = [formattedContent];
currentChars = fileChars;
} else {
currentBundle.push(formattedContent);
currentChars += fileChars;
}
},
writeCurrentBundle: async () => {
if (currentBundle.length) {
await writeBundle(currentBundle);
currentBundle = [];
currentChars = 0;
}
},
};
})();
/**
* Recursively reads files in a directory, ignoring specified directories and files.
* @param {string} dir - The directory to scan.
* @param {Array<string>} [ignored=[]] - Items to ignore.
* @param {string} rootDir - The root directory initially provided by the user.
* @returns {Promise<number>} A promise resolving to the file count.
*/
const readFiles = async (dir, ignored = [], rootDir) => {
let fileCount = 0;
try {
for (const file of await readdir(dir)) {
const filePath = join(dir, file);
const stats = await stat(filePath);
if (stats.isDirectory()) {
// Use the whole path for directory checks
const relativePath = filePath;
if (
!ignored.some((ignoredItem) => {
const normalizedItem = ignoredItem.replace(/\/$/, '');
const segments = relativePath.split(sep);
return segments.some(
(segment) =>
segment === normalizedItem ||
segment === basename(normalizedItem),
);
})
) {
fileCount += await readFiles(filePath, ignored, rootDir);
}
} else if (
stats.size <= MAX_FILE_SIZE &&
!ignored.some((ignoredItem) => {
// Check for an exact file match or if the filename alone is listed in ignored
const fileName = basename(filePath);
return (
fileName === ignoredItem ||
filePath === ignoredItem ||
(ignoredItem.includes('/') &&
filePath.startsWith(ignoredItem))
);
})
) {
try {
// Attempt to read the file as UTF-8. If successful, add it to the bundle.
const content = await readFile(filePath, 'utf-8');
await addToBundle.add(filePath, content);
fileCount++;
if (fileCount % LOG_EVERY_N_FILES === 0)
colorLog(
`Processed ${fileCount} files...`,
colors.green,
);
} catch (error) {
// If reading as UTF-8 fails, skip this file
if (
error instanceof Error &&
error.name === 'RangeError' &&
error.message.includes('Invalid UTF-8')
) {
colorLog(
`Skipping file ${filePath} as it is not UTF-8 compatible: ${error.message}`,
colors.yellow,
);
} else {
// Log any other error
colorLog(
`Error reading file ${filePath}: ${error.message}`,
colors.red,
);
}
}
} else {
const sizeKB = (stats.size / 1024).toFixed(2);
const reason =
stats.size > MAX_FILE_SIZE
? `exceeds ${MAX_FILE_SIZE / 1024} KB. File size: ${sizeKB} KB`
: 'is in the ignore list.';
colorLog(
`Skipping file ${filePath} as it ${reason}`,
colors.yellow,
);
}
}
} catch (dirErr) {
colorLog(`Error reading directory ${dir}:`, colors.red, dirErr);
}
return fileCount;
};
/**
* Main execution function to handle CLI input and orchestrate bundling.
*/
const main = async () => {
const args = process.argv.slice(2);
if (args.length < 1) {
colorLog('Usage: cbfg <directoryToScan> [ignore...]', colors.yellow);
process.exit(1);
}
const directoryToScan = args[0];
const ignored = args.slice(1);
try {
await access(directoryToScan);
const confirmed = await confirmAction(
`Are you sure you want to scan ${directoryToScan}? (y/n) `,
);
if (!confirmed) {
colorLog('Operation cancelled by user.', colors.red);
return;
}
const finalCount = await readFiles(
directoryToScan,
ignored,
directoryToScan,
);
await addToBundle.writeCurrentBundle(); // Write any remaining bundle
colorLog(
`Total number of files processed: ${finalCount}`,
colors.green,
);
colorLog(`Bundles written to directory: ${process.cwd()}`, colors.cyan);
colorLog(
`Total number of bundles written: ${bundleCount}`,
colors.green,
);
colorLog(
`Total characters written across all bundles: ${totalCharacters}`,
colors.green,
);
colorLog(
"Inform Grok you are about to post the entire codebase. Grok should only respond when you say you are finished. Start pasting bundles into Grok's prompt box.",
colors.yellow,
);
} catch {
colorLog(
`The specified directory ${directoryToScan} does not exist or cannot be accessed.`,
colors.red,
);
process.exit(1);
}
};
// Execute the main function, handling any top-level errors
main().catch((err) => {
colorLog(`An unexpected error occurred: ${err.message}`, colors.red);
process.exit(1);
});