@@ -21,7 +21,7 @@ program
2121
2222// Command line options
2323program
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
3637program . parse ( process . argv ) ;
3738const 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
178226async 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