Skip to content

Commit 20e8257

Browse files
committed
feat: add size limit enforcement and dynamic output filename
- Set the default output filename to the current directory name - Implement size limit enforcement with the -l/--limit option - Add --auto-exclude option to automatically exclude large files - Improve UI with separate file/directory sections in selection menu - Add size estimation to show if output will exceed the limit - Fix parseInt issue with limit option - Allow to override auto-excluded files through the UI
1 parent 4ed63ba commit 20e8257

File tree

2 files changed

+200
-44
lines changed

2 files changed

+200
-44
lines changed

README.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,23 @@ code2prompt-manager [options]
2929
- `-d, --directory <path>`: Directory to scan (default: current directory)
3030
- `-e, --extra-exclude <patterns>`: Additional exclude patterns (comma-separated)
3131
- `-i, --include <patterns>`: Include patterns (comma-separated)
32-
- `-O, --output-file <file>`: Output file name (default: codebase.md)
32+
- `-O, --output-file <file>`: Output file name (default: current directory name + .md)
3333
- `-F, --output-format <format>`: Output format: markdown, json, or xml (default: markdown)
3434
- `--include-priority`: Include files in case of conflict between include and exclude patterns
3535
- `--full-directory-tree`: List the full directory tree
3636
- `-c, --encoding <encoding>`: Optional tokenizer to use for token count
3737
- `--line-numbers`: Add line numbers to the source code
3838
- `-n, --no-execute`: Only show the command, don't execute it
39+
- `--auto-exclude`: Automatically exclude files to stay under size limit
3940

4041
## How it Works
4142

4243
1. The tool scans your codebase directory
4344
2. Files and directories are sorted by size (largest first)
44-
3. An interactive UI lets you select which files to exclude
45-
4. The tool generates and executes the appropriate code2prompt command
45+
3. When using `--auto-exclude`, the tool automatically selects large files to exclude to meet the size limit
46+
4. An interactive UI lets you select which files to exclude or include
47+
5. The tool calculates the estimated output file size based on your selections
48+
6. The tool generates and executes the appropriate code2prompt command
4649

4750
## Example
4851

@@ -53,6 +56,9 @@ code2prompt-manager
5356
# Set a custom size limit (in KB)
5457
code2prompt-manager --limit 350
5558

59+
# Automatically exclude large files to stay under the limit
60+
code2prompt-manager --limit 350 --auto-exclude
61+
5662
# Specify a custom output file
5763
code2prompt-manager --output-file my-project.md
5864

@@ -84,6 +90,15 @@ The tool automatically excludes common large directories and files:
8490

8591
You can add or remove excludes through the interactive selection.
8692

93+
## Size Limit Enforcement
94+
95+
The `-l, --limit` option sets a target size limit for your output file:
96+
97+
- The tool will show you the estimated output size based on your selections
98+
- If you're over the limit, it will warn you with color-coded indicators
99+
- Use `--auto-exclude` to have the tool automatically exclude the largest files to meet your limit
100+
- You can still manually adjust the selection after auto-exclude
101+
87102
## Tips for Reducing File Size
88103

89104
1. Exclude test files and directories
@@ -94,4 +109,4 @@ You can add or remove excludes through the interactive selection.
94109

95110
## License
96111

97-
MIT
112+
MIT

index.js

Lines changed: 181 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ program
2121

2222
// Command line options
2323
program
24-
.option('-l, --limit <size>', 'Size limit for the generated MD file in KB', parseInt, 400)
24+
.option('-l, --limit <size>', 'Size limit for the generated MD file in KB', (value) => parseInt(value, 10), 400)
2525
.option('-d, --directory <path>', 'Directory to scan', '.')
2626
.option('-e, --extra-exclude <patterns>', 'Additional exclude patterns (comma-separated)')
2727
.option('-i, --include <patterns>', 'Include patterns (comma-separated)')
@@ -31,7 +31,8 @@ program
3131
.option('--full-directory-tree', 'List the full directory tree')
3232
.option('-c, --encoding <encoding>', 'Optional tokenizer to use for token count (cl100k, p50k, etc.)')
3333
.option('--line-numbers', 'Add line numbers to the source code')
34-
.option('-n, --no-execute', 'Only show the command, don\'t execute it');
34+
.option('-n, --no-execute', 'Only show the command, don\'t execute it')
35+
.option('--auto-exclude', 'Automatically exclude files to stay under size limit', false);
3536

3637
program.parse(process.argv);
3738
const options = program.opts();
@@ -174,6 +175,53 @@ function scanDirectory(rootDir) {
174175
return items.sort((a, b) => b.size - a.size);
175176
}
176177

178+
// Estimate the final size after excluding files
179+
function estimateFinalSize(items, excludePatterns) {
180+
let totalSize = 0;
181+
182+
// Helper function to check if a file/directory is excluded
183+
function isExcluded(itemPath, isDirectory) {
184+
const itemPathWithWildcard = isDirectory ? `${itemPath}/**` : itemPath;
185+
186+
return excludePatterns.some(pattern => {
187+
// For directory patterns with '/**' - match pattern exactly
188+
if (pattern.endsWith('/**')) {
189+
const dirName = pattern.slice(0, -3);
190+
191+
// For top-level directories
192+
if (!dirName.includes('/')) {
193+
return itemPath === dirName || itemPath.startsWith(dirName + '/');
194+
}
195+
// For path-specified directories
196+
else {
197+
return itemPath === dirName || itemPath.startsWith(dirName + '/');
198+
}
199+
}
200+
// For exact file matches (no wildcards)
201+
else if (!pattern.includes('*')) {
202+
return itemPath === pattern;
203+
}
204+
// For other wildcard patterns
205+
else {
206+
const regex = new RegExp('^' + pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*') + '$');
207+
return regex.test(itemPath);
208+
}
209+
});
210+
}
211+
212+
// Sum sizes of all files that are not excluded
213+
items.forEach(item => {
214+
if (!item.isDirectory && !isExcluded(item.path, item.isDirectory)) {
215+
totalSize += item.size;
216+
}
217+
});
218+
219+
// Add some overhead for markdown formatting
220+
const markdownOverhead = Math.min(items.length * 100, 50 * 1024); // ~100 bytes per file, max 50KB
221+
222+
return totalSize + markdownOverhead;
223+
}
224+
177225
// Main function
178226
async function main() {
179227
try {
@@ -223,59 +271,152 @@ async function main() {
223271
// Combine defaults with extras
224272
const allDefaultExcludes = [...defaultExcludes, ...extraExcludes];
225273

226-
// Create choices for selection
227-
const choices = items.map(item => {
228-
const sizeStr = item.prettySize.padStart(10);
229-
const pathStr = item.path + (item.isDirectory ? '/' : '');
230-
231-
return {
232-
name: `${sizeStr}${pathStr}`,
233-
value: item.isDirectory ? `${item.path}/**` : item.path,
234-
short: item.path,
235-
checked: allDefaultExcludes.some(pattern => {
236-
// For directory patterns with '/**' - match pattern exactly
237-
if (pattern.endsWith('/**')) {
238-
const dirName = pattern.slice(0, -3);
239-
240-
// For top-level directories (e.g., "styles/**")
241-
if (!dirName.includes('/')) {
242-
// Match the exact directory or its direct children only
243-
return item.path === dirName ||
244-
(item.path.startsWith(dirName + '/') && !item.path.substring(dirName.length + 1).includes('/'));
274+
// Calculate initial size with just default excludes
275+
const initialSize = estimateFinalSize(items, allDefaultExcludes);
276+
const sizeLimit = options.limit * 1024; // Convert KB to bytes
277+
278+
console.log(chalk.blue(`\nSize limit: ${formatSize(sizeLimit)} (${options.limit} KB)`));
279+
console.log(chalk.blue(`Estimated size with default excludes: ${formatSize(initialSize)}`));
280+
281+
if (initialSize > sizeLimit) {
282+
console.log(chalk.yellow(`\nWARNING: Current selection exceeds size limit by ${formatSize(initialSize - sizeLimit)}`));
283+
}
284+
285+
// Create choices for selection and sort by size (largest first)
286+
const choices = items
287+
.filter(item => !item.isDirectory) // Only include files in the choices
288+
.sort((a, b) => b.size - a.size)
289+
.map(item => {
290+
const sizeStr = item.prettySize.padStart(10);
291+
const pathStr = item.path;
292+
293+
return {
294+
name: `${sizeStr}${pathStr}`,
295+
value: item.path,
296+
short: item.path,
297+
size: item.size, // Store size for auto-exclude feature
298+
checked: allDefaultExcludes.some(pattern => {
299+
// For exact file matches (no wildcards)
300+
if (!pattern.includes('*')) {
301+
return item.path === pattern;
245302
}
246-
// For path-specified directories (e.g., "components/annual-report-2022/styles/**")
303+
// For wildcard patterns
247304
else {
248-
// Match the exact directory or its direct children only
249-
return item.path === dirName ||
250-
item.path.startsWith(dirName + '/');
305+
const regex = new RegExp('^' + pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*') + '$');
306+
return regex.test(item.path);
251307
}
252-
}
253-
// For exact file matches (no wildcards)
254-
else if (!pattern.includes('*')) {
255-
return item.path === pattern;
256-
}
257-
// For other wildcard patterns
258-
else {
259-
const regex = new RegExp('^' + pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*') + '$');
260-
return regex.test(item.path);
261-
}
262-
})
263-
};
264-
});
308+
})
309+
};
310+
});
311+
312+
// Add directories as separate section
313+
const directoryChoices = items
314+
.filter(item => item.isDirectory)
315+
.sort((a, b) => b.size - a.size)
316+
.map(item => {
317+
const sizeStr = item.prettySize.padStart(10);
318+
const pathStr = item.path + '/';
319+
320+
return {
321+
name: `${sizeStr}${pathStr}`,
322+
value: `${item.path}/**`,
323+
short: item.path,
324+
checked: allDefaultExcludes.some(pattern => {
325+
// For directory patterns with '/**' - match pattern exactly
326+
if (pattern.endsWith('/**')) {
327+
const dirName = pattern.slice(0, -3);
328+
329+
// For top-level directories (e.g., "styles/**")
330+
if (!dirName.includes('/')) {
331+
return item.path === dirName ||
332+
(item.path.startsWith(dirName + '/') && !item.path.substring(dirName.length + 1).includes('/'));
333+
}
334+
// For path-specified directories
335+
else {
336+
return item.path === dirName ||
337+
item.path.startsWith(dirName + '/');
338+
}
339+
}
340+
// For other patterns
341+
else {
342+
const regex = new RegExp('^' + pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*') + '$');
343+
return regex.test(item.path);
344+
}
345+
})
346+
};
347+
});
348+
349+
// Auto-exclude large files if needed and requested
350+
let autoExcluded = [];
351+
if (options.autoExclude && initialSize > sizeLimit) {
352+
let remainingSize = initialSize;
353+
const targetSize = sizeLimit * 0.95; // Target 95% of limit to allow some margin
354+
355+
// Sort files by size (largest first) that aren't already excluded
356+
const filesToConsider = [...choices]
357+
.filter(choice => !choice.checked)
358+
.sort((a, b) => b.size - a.size);
359+
360+
for (const file of filesToConsider) {
361+
if (remainingSize > targetSize) {
362+
file.checked = true;
363+
autoExcluded.push(file.value);
364+
remainingSize -= file.size;
365+
} else {
366+
break;
367+
}
368+
}
369+
370+
if (autoExcluded.length > 0) {
371+
console.log(chalk.yellow(`\nAuto-excluded ${autoExcluded.length} files to meet size limit:`));
372+
autoExcluded.forEach(file => {
373+
console.log(chalk.yellow(` - ${file}`));
374+
});
375+
console.log(chalk.green(`New estimated size: ${formatSize(remainingSize)}`));
376+
}
377+
}
378+
379+
// Combine file and directory choices for the selection UI
380+
const allChoices = [
381+
new inquirer.Separator(' === Files (sorted by size) === '),
382+
...choices,
383+
new inquirer.Separator(' === Directories === '),
384+
...directoryChoices
385+
];
265386

266387
// Show selection UI
267388
const { selectedExcludes } = await inquirer.prompt([
268389
{
269390
type: 'checkbox',
270391
name: 'selectedExcludes',
271392
message: 'Select files/directories to EXCLUDE (sorted by size - Space to toggle, Enter to confirm):',
272-
choices,
393+
choices: allChoices,
273394
pageSize: 20
274395
}
275396
]);
276397

398+
// For auto-excluded files, we should ONLY consider them if they are still in the selectedExcludes
399+
// This is the correct approach - the user's final selection is the source of truth
400+
401+
console.log(chalk.blue(`\nFinal user selection: ${selectedExcludes.length} items`));
402+
403+
// Log any auto-excluded files that were unselected by the user
404+
const unselectedAutoExcludes = autoExcluded.filter(item => !selectedExcludes.includes(item));
405+
if (unselectedAutoExcludes.length > 0) {
406+
console.log(chalk.yellow(`You un-selected ${unselectedAutoExcludes.length} auto-excluded files that will be INCLUDED in the output:`));
407+
unselectedAutoExcludes.forEach(file => {
408+
console.log(chalk.yellow(` + ${file}`));
409+
});
410+
}
411+
412+
// The final list should ONLY contain what's in the user's selection plus default excludes
413+
const finalExcludes = [...defaultExcludes, ...extraExcludes, ...selectedExcludes];
414+
const finalSize = estimateFinalSize(items, finalExcludes);
415+
416+
console.log(chalk.blue(`\nFinal estimated size: ${formatSize(finalSize)} ${finalSize > sizeLimit ? chalk.red(`(exceeds limit by ${formatSize(finalSize - sizeLimit)})`) : chalk.green('(within limit)')}`));
417+
277418
// Combine default and selected excludes, removing duplicates
278-
const allExcludes = Array.from(new Set([...defaultExcludes, ...extraExcludes, ...selectedExcludes]));
419+
const allExcludes = Array.from(new Set(finalExcludes));
279420

280421
// Create the code2prompt command
281422
let cmd = `code2prompt`;

0 commit comments

Comments
 (0)