make-folder-txt 2.2.0 → 2.2.1

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.
@@ -0,0 +1,1577 @@
1
+ ================================================================================
2
+ START OF FOLDER: make-folder-txt
3
+ ================================================================================
4
+
5
+ ================================================================================
6
+ PROJECT STRUCTURE
7
+ ================================================================================
8
+ Root: C:\Programming\make-folder-txt
9
+
10
+ make-folder-txt/
11
+ ├── .git/ [skipped]
12
+ ├── bin/
13
+ │ └── make-folder-txt.js
14
+ ├── .txtconfig
15
+ ├── LICENSE
16
+ ├── package.json
17
+ └── README.md
18
+
19
+ Total files: 5
20
+
21
+ ================================================================================
22
+ FILE CONTENTS
23
+ ================================================================================
24
+
25
+ --------------------------------------------------------------------------------
26
+ FILE: /bin/make-folder-txt.js
27
+ --------------------------------------------------------------------------------
28
+ #!/usr/bin/env node
29
+
30
+ const fs = require("fs");
31
+ const path = require("path");
32
+ const { version } = require("../package.json");
33
+ const { execSync } = require("child_process");
34
+
35
+ // ── config ────────────────────────────────────────────────────────────────────
36
+ const IGNORE_DIRS = new Set(["node_modules", ".git", ".next", "dist", "build", ".cache"]);
37
+ const IGNORE_FILES = new Set([".DS_Store", "Thumbs.db", "desktop.ini", ".txtignore"]);
38
+
39
+ const BINARY_EXTS = new Set([
40
+ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".svg", ".webp",
41
+ ".pdf", ".zip", ".tar", ".gz", ".rar", ".7z",
42
+ ".exe", ".dll", ".so", ".dylib", ".bin",
43
+ ".mp3", ".mp4", ".wav", ".avi", ".mov",
44
+ ".woff", ".woff2", ".ttf", ".eot", ".otf",
45
+ ".lock",
46
+ ]);
47
+
48
+ // ── helpers ───────────────────────────────────────────────────────────────────
49
+
50
+ function readTxtIgnore(rootDir) {
51
+ const txtIgnorePath = path.join(rootDir, '.txtignore');
52
+ const ignorePatterns = new Set();
53
+
54
+ try {
55
+ const content = fs.readFileSync(txtIgnorePath, 'utf8');
56
+ const lines = content.split('\n')
57
+ .map(line => line.trim())
58
+ .filter(line => line && !line.startsWith('#'));
59
+
60
+ lines.forEach(line => ignorePatterns.add(line));
61
+ } catch (err) {
62
+ // .txtignore doesn't exist or can't be read - that's fine
63
+ }
64
+
65
+ return ignorePatterns;
66
+ }
67
+
68
+ function copyToClipboard(text) {
69
+ try {
70
+ if (process.platform === 'win32') {
71
+ // Windows
72
+ execSync(`echo ${JSON.stringify(text).replace(/"/g, '""')} | clip`, { stdio: 'ignore' });
73
+ } else if (process.platform === 'darwin') {
74
+ // macOS
75
+ execSync(`echo ${JSON.stringify(text)} | pbcopy`, { stdio: 'ignore' });
76
+ } else {
77
+ // Linux (requires xclip or xsel)
78
+ try {
79
+ execSync(`echo ${JSON.stringify(text)} | xclip -selection clipboard`, { stdio: 'ignore' });
80
+ } catch {
81
+ try {
82
+ execSync(`echo ${JSON.stringify(text)} | xsel --clipboard --input`, { stdio: 'ignore' });
83
+ } catch {
84
+ console.warn('⚠️ Could not copy to clipboard. Install xclip or xsel on Linux.');
85
+ return false;
86
+ }
87
+ }
88
+ }
89
+ return true;
90
+ } catch (err) {
91
+ console.warn('⚠️ Could not copy to clipboard:', err.message);
92
+ return false;
93
+ }
94
+ }
95
+
96
+ function collectFiles(
97
+ dir,
98
+ rootDir,
99
+ ignoreDirs,
100
+ ignoreFiles,
101
+ onlyFolders,
102
+ onlyFiles,
103
+ options = {},
104
+ ) {
105
+ const {
106
+ indent = "",
107
+ lines = [],
108
+ filePaths = [],
109
+ inSelectedFolder = false,
110
+ hasOnlyFilters = false,
111
+ rootName = "",
112
+ txtIgnore = new Set(),
113
+ force = false,
114
+ } = options;
115
+
116
+ let entries;
117
+ try {
118
+ entries = fs.readdirSync(dir, { withFileTypes: true });
119
+ } catch {
120
+ return { lines, filePaths, hasIncluded: false };
121
+ }
122
+
123
+ entries.sort((a, b) => {
124
+ if (a.isDirectory() === b.isDirectory()) return a.name.localeCompare(b.name);
125
+ return a.isDirectory() ? -1 : 1;
126
+ });
127
+
128
+ entries.forEach((entry, idx) => {
129
+ const isLast = idx === entries.length - 1;
130
+ const connector = isLast ? "└── " : "├── ";
131
+ const childIndent = indent + (isLast ? " " : "│ ");
132
+
133
+ if (entry.isDirectory()) {
134
+ if (!force && ignoreDirs.has(entry.name)) {
135
+ if (!hasOnlyFilters) {
136
+ lines.push(`${indent}${connector}${entry.name}/ [skipped]`);
137
+ }
138
+ return;
139
+ }
140
+
141
+ // Get relative path for .txtignore pattern matching
142
+ const relPathForIgnore = path.relative(rootDir, path.join(dir, entry.name)).split(path.sep).join("/");
143
+
144
+ // Check against .txtignore patterns (both dirname and relative path) unless force is enabled
145
+ if (!force && (txtIgnore.has(entry.name) || txtIgnore.has(`${entry.name}/`) || txtIgnore.has(relPathForIgnore) || txtIgnore.has(`${relPathForIgnore}/`) || txtIgnore.has(`/${relPathForIgnore}/`))) {
146
+ if (!hasOnlyFilters) {
147
+ lines.push(`${indent}${connector}${entry.name}/ [skipped]`);
148
+ }
149
+ return;
150
+ }
151
+
152
+ const childPath = path.join(dir, entry.name);
153
+ const childInSelectedFolder = inSelectedFolder || onlyFolders.has(entry.name);
154
+ const childLines = [];
155
+ const childFiles = [];
156
+ const child = collectFiles(
157
+ childPath,
158
+ rootDir,
159
+ ignoreDirs,
160
+ ignoreFiles,
161
+ onlyFolders,
162
+ onlyFiles,
163
+ {
164
+ indent: childIndent,
165
+ lines: childLines,
166
+ filePaths: childFiles,
167
+ inSelectedFolder: childInSelectedFolder,
168
+ hasOnlyFilters,
169
+ rootName,
170
+ txtIgnore,
171
+ force,
172
+ },
173
+ );
174
+
175
+ const explicitlySelectedFolder = hasOnlyFilters && onlyFolders.has(entry.name);
176
+ const shouldIncludeDir = !hasOnlyFilters || child.hasIncluded || explicitlySelectedFolder;
177
+
178
+ if (shouldIncludeDir) {
179
+ lines.push(`${indent}${connector}${entry.name}/`);
180
+ lines.push(...child.lines);
181
+ filePaths.push(...child.filePaths);
182
+ }
183
+ } else {
184
+ if (!force && ignoreFiles.has(entry.name)) return;
185
+
186
+ // Get relative path for .txtignore pattern matching
187
+ const relPathForIgnore = path.relative(rootDir, path.join(dir, entry.name)).split(path.sep).join("/");
188
+
189
+ // Check against .txtignore patterns (both filename and relative path) unless force is enabled
190
+ if (!force && (txtIgnore.has(entry.name) || txtIgnore.has(relPathForIgnore) || txtIgnore.has(`/${relPathForIgnore}`))) {
191
+ return;
192
+ }
193
+
194
+ // Ignore .txt files that match the folder name (e.g., foldername.txt) unless force is enabled
195
+ if (!force && entry.name.endsWith('.txt') && entry.name === `${rootName}.txt`) return;
196
+
197
+ const shouldIncludeFile = !hasOnlyFilters || inSelectedFolder || onlyFiles.has(entry.name);
198
+ if (!shouldIncludeFile) return;
199
+
200
+ lines.push(`${indent}${connector}${entry.name}`);
201
+ const relPath = "/" + path.relative(rootDir, path.join(dir, entry.name)).split(path.sep).join("/");
202
+ filePaths.push({ abs: path.join(dir, entry.name), rel: relPath });
203
+ }
204
+ });
205
+
206
+ return { lines, filePaths, hasIncluded: filePaths.length > 0 || lines.length > 0 };
207
+ }
208
+
209
+ function parseFileSize(sizeStr) {
210
+ const units = {
211
+ 'B': 1,
212
+ 'KB': 1024,
213
+ 'MB': 1024 * 1024,
214
+ 'GB': 1024 * 1024 * 1024,
215
+ 'TB': 1024 * 1024 * 1024 * 1024
216
+ };
217
+
218
+ const match = sizeStr.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)$/i);
219
+ if (!match) {
220
+ console.error(`Error: Invalid size format "${sizeStr}". Use format like "500KB", "2MB", "1GB".`);
221
+ process.exit(1);
222
+ }
223
+
224
+ const value = parseFloat(match[1]);
225
+ const unit = match[2].toUpperCase();
226
+ return Math.floor(value * units[unit]);
227
+ }
228
+
229
+ function readContent(absPath, force = false, maxFileSize = 500 * 1024) {
230
+ const ext = path.extname(absPath).toLowerCase();
231
+ if (!force && BINARY_EXTS.has(ext)) return "[binary / skipped]";
232
+ try {
233
+ const stat = fs.statSync(absPath);
234
+ if (!force && stat.size > maxFileSize) {
235
+ const sizeStr = stat.size < 1024 ? `${stat.size} B` :
236
+ stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB` :
237
+ stat.size < 1024 * 1024 * 1024 ? `${(stat.size / (1024 * 1024)).toFixed(1)} MB` :
238
+ `${(stat.size / (1024 * 1024 * 1024)).toFixed(1)} GB`;
239
+ return `[file too large: ${sizeStr} – skipped]`;
240
+ }
241
+ return fs.readFileSync(absPath, "utf8");
242
+ } catch (err) {
243
+ return `[could not read file: ${err.message}]`;
244
+ }
245
+ }
246
+
247
+ function splitByFolders(treeLines, filePaths, rootName, effectiveMaxSize, forceFlag) {
248
+ const folders = new Map();
249
+
250
+ // Group files by folder
251
+ filePaths.forEach(({ abs, rel }) => {
252
+ const folderPath = path.dirname(rel);
253
+ const folderKey = folderPath === '/' ? rootName : folderPath.slice(1);
254
+
255
+ if (!folders.has(folderKey)) {
256
+ folders.set(folderKey, []);
257
+ }
258
+ folders.get(folderKey).push({ abs, rel });
259
+ });
260
+
261
+ const results = [];
262
+
263
+ folders.forEach((files, folderName) => {
264
+ const out = [];
265
+ const divider = "=".repeat(80);
266
+ const subDivider = "-".repeat(80);
267
+
268
+ out.push(divider);
269
+ out.push(`START OF FOLDER: ${folderName}`);
270
+ out.push(divider);
271
+ out.push("");
272
+
273
+ // Add folder structure (only this folder's structure)
274
+ const folderTreeLines = treeLines.filter(line =>
275
+ line.includes(folderName + '/') || line === `${rootName}/`
276
+ );
277
+
278
+ out.push(divider);
279
+ out.push("PROJECT STRUCTURE");
280
+ out.push(divider);
281
+ out.push(`Root: ${folderPath}\n`);
282
+ out.push(`${rootName}/`);
283
+ folderTreeLines.forEach(l => out.push(l));
284
+ out.push("");
285
+ out.push(`Total files in this folder: ${files.length}`);
286
+ out.push("");
287
+
288
+ out.push(divider);
289
+ out.push("FILE CONTENTS");
290
+ out.push(divider);
291
+
292
+ files.forEach(({ abs, rel }) => {
293
+ out.push("");
294
+ out.push(subDivider);
295
+ out.push(`FILE: ${rel}`);
296
+ out.push(subDivider);
297
+ out.push(readContent(abs, forceFlag, effectiveMaxSize));
298
+ });
299
+
300
+ out.push("");
301
+ out.push(divider);
302
+ out.push(`END OF FOLDER: ${folderName}`);
303
+ out.push(divider);
304
+
305
+ const fileName = `${rootName}-${folderName.replace(/[\/\\]/g, '-')}.txt`;
306
+ const filePath = path.join(process.cwd(), fileName);
307
+
308
+ fs.writeFileSync(filePath, out.join("\n"), "utf8");
309
+ const sizeKB = (fs.statSync(filePath).size / 1024).toFixed(1);
310
+
311
+ results.push({
312
+ file: filePath,
313
+ size: sizeKB,
314
+ files: files.length,
315
+ folder: folderName
316
+ });
317
+ });
318
+
319
+ return results;
320
+ }
321
+
322
+ function splitByFiles(filePaths, rootName, effectiveMaxSize, forceFlag) {
323
+ const results = [];
324
+
325
+ filePaths.forEach(({ abs, rel }) => {
326
+ const out = [];
327
+ const divider = "=".repeat(80);
328
+ const subDivider = "-".repeat(80);
329
+ const fileName = path.basename(rel, path.extname(rel));
330
+
331
+ out.push(divider);
332
+ out.push(`FILE: ${rel}`);
333
+ out.push(divider);
334
+ out.push("");
335
+
336
+ out.push(divider);
337
+ out.push("FILE CONTENTS");
338
+ out.push(divider);
339
+ out.push(readContent(abs, forceFlag, effectiveMaxSize));
340
+
341
+ out.push("");
342
+ out.push(divider);
343
+ out.push(`END OF FILE: ${rel}`);
344
+ out.push(divider);
345
+
346
+ const outputFileName = `${rootName}-${fileName}.txt`;
347
+ const filePath = path.join(process.cwd(), outputFileName);
348
+
349
+ fs.writeFileSync(filePath, out.join("\n"), "utf8");
350
+ const sizeKB = (fs.statSync(filePath).size / 1024).toFixed(1);
351
+
352
+ results.push({
353
+ file: filePath,
354
+ size: sizeKB,
355
+ files: 1,
356
+ fileName: fileName
357
+ });
358
+ });
359
+
360
+ return results;
361
+ }
362
+
363
+ function splitBySize(treeLines, filePaths, rootName, splitSize, effectiveMaxSize, forceFlag) {
364
+ const results = [];
365
+ let currentPart = 1;
366
+ let currentSize = 0;
367
+ let currentFiles = [];
368
+
369
+ const divider = "=".repeat(80);
370
+ const subDivider = "-".repeat(80);
371
+
372
+ // Start with header
373
+ let out = [];
374
+ out.push(divider);
375
+ out.push(`START OF FOLDER: ${rootName} (Part ${currentPart})`);
376
+ out.push(divider);
377
+ out.push("");
378
+
379
+ out.push(divider);
380
+ out.push("PROJECT STRUCTURE");
381
+ out.push(divider);
382
+ out.push(`Root: ${folderPath}\n`);
383
+ out.push(`${rootName}/`);
384
+ treeLines.forEach(l => out.push(l));
385
+ out.push("");
386
+ out.push(`Total files: ${filePaths.length}`);
387
+ out.push("");
388
+
389
+ out.push(divider);
390
+ out.push("FILE CONTENTS");
391
+ out.push(divider);
392
+
393
+ filePaths.forEach(({ abs, rel }) => {
394
+ const content = readContent(abs, forceFlag, effectiveMaxSize);
395
+ const fileContent = [
396
+ "",
397
+ subDivider,
398
+ `FILE: ${rel}`,
399
+ subDivider,
400
+ content
401
+ ];
402
+
403
+ const contentSize = fileContent.join("\n").length;
404
+
405
+ // Check if adding this file would exceed the split size
406
+ if (currentSize + contentSize > splitSize && currentFiles.length > 0) {
407
+ // Finish current part
408
+ out.push("");
409
+ out.push(divider);
410
+ out.push(`END OF FOLDER: ${rootName} (Part ${currentPart})`);
411
+ out.push(divider);
412
+
413
+ // Write current part
414
+ const fileName = `${rootName}-part-${currentPart}.txt`;
415
+ const filePath = path.join(process.cwd(), fileName);
416
+ fs.writeFileSync(filePath, out.join("\n"), "utf8");
417
+ const sizeKB = (fs.statSync(filePath).size / 1024).toFixed(1);
418
+
419
+ results.push({
420
+ file: filePath,
421
+ size: sizeKB,
422
+ files: currentFiles.length,
423
+ part: currentPart
424
+ });
425
+
426
+ // Start new part
427
+ currentPart++;
428
+ currentSize = 0;
429
+ currentFiles = [];
430
+
431
+ out = [];
432
+ out.push(divider);
433
+ out.push(`START OF FOLDER: ${rootName} (Part ${currentPart})`);
434
+ out.push(divider);
435
+ out.push("");
436
+
437
+ out.push(divider);
438
+ out.push("PROJECT STRUCTURE");
439
+ out.push(divider);
440
+ out.push(`Root: ${folderPath}\n`);
441
+ out.push(`${rootName}/`);
442
+ treeLines.forEach(l => out.push(l));
443
+ out.push("");
444
+ out.push(`Total files: ${filePaths.length}`);
445
+ out.push("");
446
+
447
+ out.push(divider);
448
+ out.push("FILE CONTENTS");
449
+ out.push(divider);
450
+ }
451
+
452
+ // Add file to current part
453
+ out.push(...fileContent);
454
+ currentSize += contentSize;
455
+ currentFiles.push(rel);
456
+ });
457
+
458
+ // Write final part
459
+ out.push("");
460
+ out.push(divider);
461
+ out.push(`END OF FOLDER: ${rootName} (Part ${currentPart})`);
462
+ out.push(divider);
463
+
464
+ const fileName = `${rootName}-part-${currentPart}.txt`;
465
+ const filePath = path.join(process.cwd(), fileName);
466
+ fs.writeFileSync(filePath, out.join("\n"), "utf8");
467
+ const sizeKB = (fs.statSync(filePath).size / 1024).toFixed(1);
468
+
469
+ results.push({
470
+ file: filePath,
471
+ size: sizeKB,
472
+ files: currentFiles.length,
473
+ part: currentPart
474
+ });
475
+
476
+ return results;
477
+ }
478
+
479
+ // ── configuration ────────────────────────────────────────────────────────────────
480
+
481
+ function createInteractiveConfig() {
482
+ const readline = require('readline');
483
+ const fs = require('fs');
484
+ const path = require('path');
485
+ const os = require('os');
486
+
487
+ const rl = readline.createInterface({
488
+ input: process.stdin,
489
+ output: process.stdout
490
+ });
491
+
492
+ console.log('\n🔧 make-folder-txt Configuration Setup');
493
+ console.log('=====================================\n');
494
+
495
+ return new Promise((resolve) => {
496
+ const config = {
497
+ maxFileSize: '500KB',
498
+ splitMethod: 'none',
499
+ splitSize: '5MB',
500
+ copyToClipboard: false,
501
+ ignoreFolders: [],
502
+ ignoreFiles: []
503
+ };
504
+
505
+ let currentStep = 0;
506
+ const questions = [
507
+ {
508
+ key: 'maxFileSize',
509
+ question: 'Maximum file size to include (e.g., 500KB, 2MB, 1GB): ',
510
+ default: '500KB',
511
+ validate: (value) => {
512
+ if (!value.trim()) return true;
513
+ const validUnits = ['B', 'KB', 'MB', 'GB', 'TB'];
514
+ const match = value.match(/^(\d+(?:\.\d+)?)\s*([A-Z]+)$/i);
515
+ if (!match) return 'Please enter a valid size (e.g., 500KB, 2MB, 1GB)';
516
+ if (!validUnits.includes(match[2].toUpperCase())) return `Invalid unit. Use: ${validUnits.join(', ')}`;
517
+ return true;
518
+ }
519
+ },
520
+ {
521
+ key: 'splitMethod',
522
+ question: 'Split output method (none, folder, file, size): ',
523
+ default: 'none',
524
+ validate: (value) => {
525
+ const validMethods = ['none', 'folder', 'file', 'size'];
526
+ if (!validMethods.includes(value.toLowerCase())) return `Please choose: ${validMethods.join(', ')}`;
527
+ return true;
528
+ }
529
+ },
530
+ {
531
+ key: 'splitSize',
532
+ question: 'Split size when using size method (e.g., 5MB, 10MB): ',
533
+ default: '5MB',
534
+ ask: () => config.splitMethod === 'size',
535
+ validate: (value) => {
536
+ if (!value.trim()) return true;
537
+ const validUnits = ['B', 'KB', 'MB', 'GB', 'TB'];
538
+ const match = value.match(/^(\d+(?:\.\d+)?)\s*([A-Z]+)$/i);
539
+ if (!match) return 'Please enter a valid size (e.g., 5MB, 10MB)';
540
+ if (!validUnits.includes(match[2].toUpperCase())) return `Invalid unit. Use: ${validUnits.join(', ')}`;
541
+ return true;
542
+ }
543
+ },
544
+ {
545
+ key: 'copyToClipboard',
546
+ question: 'Copy to clipboard automatically? (y/n): ',
547
+ default: 'n',
548
+ validate: (value) => {
549
+ const answer = value.toLowerCase();
550
+ if (!['y', 'n', 'yes', 'no'].includes(answer)) return 'Please enter y/n or yes/no';
551
+ return true;
552
+ },
553
+ transform: (value) => ['y', 'yes'].includes(value.toLowerCase())
554
+ },
555
+ {
556
+ key: 'ignoreFolders',
557
+ question: 'Ignore folders (comma-separated, or press Enter to skip): ',
558
+ default: '',
559
+ transform: (value) => value ? value.split(',').map(f => f.trim()).filter(f => f) : []
560
+ },
561
+ {
562
+ key: 'ignoreFiles',
563
+ question: 'Ignore files (comma-separated, or press Enter to skip): ',
564
+ default: '',
565
+ transform: (value) => value ? value.split(',').map(f => f.trim()).filter(f => f) : []
566
+ }
567
+ ];
568
+
569
+ function askQuestion() {
570
+ if (currentStep >= questions.length) {
571
+ // Save configuration
572
+ saveConfig();
573
+ return;
574
+ }
575
+
576
+ const q = questions[currentStep];
577
+
578
+ // Skip if conditional ask returns false
579
+ if (q.ask && !q.ask()) {
580
+ currentStep++;
581
+ askQuestion();
582
+ return;
583
+ }
584
+
585
+ const defaultValue = typeof q.default === 'function' ? q.default() : q.default;
586
+ rl.question(q.question + (defaultValue ? `(${defaultValue}) ` : ''), (answer) => {
587
+ const value = answer.trim() || defaultValue;
588
+
589
+ // Validate input
590
+ if (q.validate) {
591
+ const validation = q.validate(value);
592
+ if (validation !== true) {
593
+ console.log(`❌ ${validation}`);
594
+ askQuestion();
595
+ return;
596
+ }
597
+ }
598
+
599
+ // Transform value if needed
600
+ if (q.transform) {
601
+ config[q.key] = q.transform(value);
602
+ } else {
603
+ config[q.key] = value;
604
+ }
605
+
606
+ currentStep++;
607
+ askQuestion();
608
+ });
609
+ }
610
+
611
+ function saveConfig() {
612
+ try {
613
+ // Create .txtconfig file
614
+ const configPath = path.join(process.cwd(), '.txtconfig');
615
+ const configContent = `{
616
+ "maxFileSize": "${config.maxFileSize}",
617
+ "splitMethod": "${config.splitMethod}",
618
+ "splitSize": "${config.splitSize}",
619
+ "copyToClipboard": ${config.copyToClipboard},
620
+ "ignoreFolders": ${JSON.stringify(config.ignoreFolders)},
621
+ "ignoreFiles": ${JSON.stringify(config.ignoreFiles)}
622
+ }`;
623
+
624
+ fs.writeFileSync(configPath, configContent);
625
+
626
+ // Update .txtignore if there are ignore patterns
627
+ if (config.ignoreFolders.length > 0 || config.ignoreFiles.length > 0) {
628
+ const ignorePath = path.join(process.cwd(), '.txtignore');
629
+ let ignoreContent = '';
630
+
631
+ // Read existing content if file exists
632
+ if (fs.existsSync(ignorePath)) {
633
+ ignoreContent = fs.readFileSync(ignorePath, 'utf8');
634
+ if (!ignoreContent.endsWith('\n')) ignoreContent += '\n';
635
+ }
636
+
637
+ // Add new ignore patterns
638
+ const newPatterns = [
639
+ ...config.ignoreFolders.map(f => f.endsWith('/') ? f : `${f}/`),
640
+ ...config.ignoreFiles
641
+ ];
642
+
643
+ if (newPatterns.length > 0) {
644
+ ignoreContent += '\n# Added by make-folder-txt config\n';
645
+ ignoreContent += newPatterns.join('\n') + '\n';
646
+ fs.writeFileSync(ignorePath, ignoreContent);
647
+ }
648
+ }
649
+
650
+ console.log('\n✅ Configuration saved successfully!');
651
+ console.log(`📄 Config file: ${configPath}`);
652
+ if (config.ignoreFolders.length > 0 || config.ignoreFiles.length > 0) {
653
+ console.log(`📝 Ignore patterns added to .txtignore`);
654
+ }
655
+ console.log('\n💡 You can now run: make-folder-txt --use-config');
656
+
657
+ } catch (err) {
658
+ console.error('❌ Error saving configuration:', err.message);
659
+ } finally {
660
+ rl.close();
661
+ resolve();
662
+ }
663
+ }
664
+
665
+ askQuestion();
666
+ });
667
+ }
668
+
669
+ function loadConfig() {
670
+ const fs = require('fs');
671
+ const path = require('path');
672
+
673
+ try {
674
+ const configPath = path.join(process.cwd(), '.txtconfig');
675
+ if (!fs.existsSync(configPath)) {
676
+ console.error('❌ .txtconfig file not found. Run --make-config first.');
677
+ process.exit(1);
678
+ }
679
+
680
+ const configContent = fs.readFileSync(configPath, 'utf8');
681
+ return JSON.parse(configContent);
682
+ } catch (err) {
683
+ console.error('❌ Error loading configuration:', err.message);
684
+ process.exit(1);
685
+ }
686
+ }
687
+
688
+ // ── main ──────────────────────────────────────────────────────────────────────
689
+
690
+ async function main() {
691
+ const args = process.argv.slice(2);
692
+
693
+ if (args.includes("-v") || args.includes("--version")) {
694
+ console.log(`v${version}`);
695
+ console.log("Built by Muhammad Saad Amin");
696
+ process.exit(0);
697
+ }
698
+
699
+ if (args.includes("--make-config")) {
700
+ await createInteractiveConfig();
701
+ process.exit(0);
702
+ }
703
+
704
+ if (args.includes("--use-config")) {
705
+ const config = loadConfig();
706
+
707
+ // Apply config to command line arguments
708
+ const newArgs = [];
709
+
710
+ // Add max file size if not default
711
+ if (config.maxFileSize !== '500KB') {
712
+ newArgs.push('--skip-large', config.maxFileSize);
713
+ }
714
+
715
+ // Add split method if not none
716
+ if (config.splitMethod !== 'none') {
717
+ newArgs.push('--split-method', config.splitMethod);
718
+ if (config.splitMethod === 'size') {
719
+ newArgs.push('--split-size', config.splitSize);
720
+ }
721
+ }
722
+
723
+ // Add copy to clipboard if true
724
+ if (config.copyToClipboard) {
725
+ newArgs.push('--copy');
726
+ }
727
+
728
+ // Add ignore folders
729
+ if (config.ignoreFolders.length > 0) {
730
+ newArgs.push('--ignore-folder', ...config.ignoreFolders);
731
+ }
732
+
733
+ // Add ignore files
734
+ if (config.ignoreFiles.length > 0) {
735
+ newArgs.push('--ignore-file', ...config.ignoreFiles);
736
+ }
737
+
738
+ // Replace args with config-based args
739
+ args.splice(0, args.length, ...newArgs);
740
+ }
741
+
742
+ if (args.includes("--help") || args.includes("-h")) {
743
+ console.log(`
744
+ \x1b[33mmake-folder-txt\x1b[0m
745
+ Dump an entire project folder into a single readable .txt file.
746
+
747
+ \x1b[33mUSAGE\x1b[0m
748
+ make-folder-txt [options]
749
+
750
+ \x1b[33mOPTIONS\x1b[0m
751
+ --ignore-folder, -ifo <names...> Ignore specific folders by name
752
+ --ignore-file, -ifi <names...> Ignore specific files by name
753
+ --only-folder, -ofo <names...> Include only specific folders
754
+ --only-file, -ofi <names...> Include only specific files
755
+ --skip-large <size> Skip files larger than specified size (default: 500KB)
756
+ --no-skip Include all files regardless of size
757
+ --split-method <method> Split output: folder, file, or size
758
+ --split-size <size> Split output when size exceeds limit (requires --split-method size)
759
+ --copy Copy output to clipboard
760
+ --force Include everything (overrides all ignore patterns)
761
+ --make-config Create interactive configuration
762
+ --use-config Use configuration from .txtconfig file
763
+ --help, -h Show this help message
764
+ --version, -v Show version information
765
+
766
+ \x1b[33mEXAMPLES\x1b[0m
767
+ make-folder-txt
768
+ make-folder-txt --copy
769
+ make-folder-txt --force
770
+ make-folder-txt --skip-large 400KB
771
+ make-folder-txt --skip-large 5GB
772
+ make-folder-txt --no-skip
773
+ make-folder-txt --split-method folder
774
+ make-folder-txt --split-method file
775
+ make-folder-txt --split-method size --split-size 5MB
776
+ make-folder-txt --ignore-folder node_modules dist
777
+ make-folder-txt -ifo node_modules dist
778
+ make-folder-txt --ignore-file .env .env.local
779
+ make-folder-txt -ifi .env .env.local
780
+ make-folder-txt --only-folder src docs
781
+ make-folder-txt -ofo src docs
782
+ make-folder-txt --only-file package.json README.md
783
+ make-folder-txt -ofi package.json README.md
784
+ make-folder-txt --make-config
785
+ make-folder-txt --use-config
786
+
787
+ \x1b[33m.TXTIGNORE FILE\x1b[0m
788
+ Create a .txtignore file in your project root to specify files/folders to ignore.
789
+ Works like .gitignore - supports file names, path patterns, and comments.
790
+
791
+ Example .txtignore:
792
+ node_modules/
793
+ *.log
794
+ .env
795
+ coverage/
796
+ LICENSE
797
+ `);
798
+ process.exit(0);
799
+ }
800
+
801
+ const ignoreDirs = new Set(IGNORE_DIRS);
802
+ const ignoreFiles = new Set(IGNORE_FILES);
803
+ const onlyFolders = new Set();
804
+ const onlyFiles = new Set();
805
+ let outputArg = null;
806
+ let copyToClipboardFlag = false;
807
+ let forceFlag = false;
808
+ let maxFileSize = 500 * 1024; // Default 500KB
809
+ let noSkipFlag = false;
810
+ let splitMethod = null; // 'folder', 'file', 'size'
811
+ let splitSize = null; // size in bytes
812
+
813
+ for (let i = 0; i < args.length; i += 1) {
814
+ const arg = args[i];
815
+
816
+ if (arg === "--copy") {
817
+ copyToClipboardFlag = true;
818
+ continue;
819
+ }
820
+
821
+ if (arg === "--force") {
822
+ forceFlag = true;
823
+ continue;
824
+ }
825
+
826
+ if (arg === "--no-skip") {
827
+ noSkipFlag = true;
828
+ continue;
829
+ }
830
+
831
+ if (arg === "--skip-large") {
832
+ if (i + 1 >= args.length || args[i + 1].startsWith("-")) {
833
+ console.error("Error: --skip-large requires a size value (e.g., 400KB, 5GB).");
834
+ process.exit(1);
835
+ }
836
+ maxFileSize = parseFileSize(args[i + 1]);
837
+ i += 1;
838
+ continue;
839
+ }
840
+
841
+ if (arg.startsWith("--skip-large=")) {
842
+ const value = arg.slice("--skip-large=".length);
843
+ if (!value) {
844
+ console.error("Error: --skip-large requires a size value (e.g., 400KB, 5GB).");
845
+ process.exit(1);
846
+ }
847
+ maxFileSize = parseFileSize(value);
848
+ continue;
849
+ }
850
+
851
+ if (arg === "--split-method") {
852
+ if (i + 1 >= args.length || args[i + 1].startsWith("-")) {
853
+ console.error("Error: --split-method requires a method (folder, file, or size).");
854
+ process.exit(1);
855
+ }
856
+ const method = args[i + 1].toLowerCase();
857
+ if (!['folder', 'file', 'size'].includes(method)) {
858
+ console.error("Error: --split-method must be one of: folder, file, size");
859
+ process.exit(1);
860
+ }
861
+ splitMethod = method;
862
+ i += 1;
863
+ continue;
864
+ }
865
+
866
+ if (arg.startsWith("--split-method=")) {
867
+ const value = arg.slice("--split-method=".length);
868
+ if (!value) {
869
+ console.error("Error: --split-method requires a method (folder, file, or size).");
870
+ process.exit(1);
871
+ }
872
+ const method = value.toLowerCase();
873
+ if (!['folder', 'file', 'size'].includes(method)) {
874
+ console.error("Error: --split-method must be one of: folder, file, size");
875
+ process.exit(1);
876
+ }
877
+ splitMethod = method;
878
+ continue;
879
+ }
880
+
881
+ if (arg === "--split-size") {
882
+ if (i + 1 >= args.length || args[i + 1].startsWith("-")) {
883
+ console.error("Error: --split-size requires a size value (e.g., 5MB, 10MB).");
884
+ process.exit(1);
885
+ }
886
+ splitSize = parseFileSize(args[i + 1]);
887
+ i += 1;
888
+ continue;
889
+ }
890
+
891
+ if (arg.startsWith("--split-size=")) {
892
+ const value = arg.slice("--split-size=".length);
893
+ if (!value) {
894
+ console.error("Error: --split-size requires a size value (e.g., 5MB, 10MB).");
895
+ process.exit(1);
896
+ }
897
+ splitSize = parseFileSize(value);
898
+ continue;
899
+ }
900
+
901
+ if (arg === "--ignore-folder" || arg === "-ifo") {
902
+ let consumed = 0;
903
+ while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
904
+ // Normalize the folder name: remove backslashes, trailing slashes, and leading ./
905
+ let folderName = args[i + 1];
906
+ folderName = folderName.replace(/\\/g, '/'); // Convert backslashes to forward slashes
907
+ folderName = folderName.replace(/^\.?\//, ''); // Remove leading ./ or /
908
+ folderName = folderName.replace(/\/+$/, ''); // Remove trailing slashes
909
+ ignoreDirs.add(folderName);
910
+ i += 1;
911
+ consumed += 1;
912
+ }
913
+ if (consumed === 0) {
914
+ console.error("Error: --ignore-folder requires at least one folder name.");
915
+ process.exit(1);
916
+ }
917
+ continue;
918
+ }
919
+
920
+ if (arg.startsWith("--ignore-folder=") || arg.startsWith("-ifo=")) {
921
+ const value = arg.startsWith("--ignore-folder=")
922
+ ? arg.slice("--ignore-folder=".length)
923
+ : arg.slice("-ifo=".length);
924
+ if (!value) {
925
+ console.error("Error: --ignore-folder requires a folder name.");
926
+ process.exit(1);
927
+ }
928
+ // Normalize the folder name
929
+ let folderName = value;
930
+ folderName = folderName.replace(/\\/g, '/'); // Convert backslashes to forward slashes
931
+ folderName = folderName.replace(/^\.?\//, ''); // Remove leading ./ or /
932
+ folderName = folderName.replace(/\/+$/, ''); // Remove trailing slashes
933
+ ignoreDirs.add(folderName);
934
+ continue;
935
+ }
936
+
937
+ if (arg === "--ignore-file" || arg === "-ifi") {
938
+ let consumed = 0;
939
+ while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
940
+ ignoreFiles.add(args[i + 1]);
941
+ i += 1;
942
+ consumed += 1;
943
+ }
944
+ if (consumed === 0) {
945
+ console.error("Error: --ignore-file requires at least one file name.");
946
+ process.exit(1);
947
+ }
948
+ continue;
949
+ }
950
+
951
+ if (arg.startsWith("--ignore-file=") || arg.startsWith("-ifi=")) {
952
+ const value = arg.startsWith("--ignore-file=")
953
+ ? arg.slice("--ignore-file=".length)
954
+ : arg.slice("-ifi=".length);
955
+ if (!value) {
956
+ console.error("Error: --ignore-file requires a file name.");
957
+ process.exit(1);
958
+ }
959
+ ignoreFiles.add(value);
960
+ continue;
961
+ }
962
+
963
+ if (arg === "--only-folder" || arg === "-ofo") {
964
+ let consumed = 0;
965
+ while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
966
+ // Normalize the folder name
967
+ let folderName = args[i + 1];
968
+ folderName = folderName.replace(/\\/g, '/'); // Convert backslashes to forward slashes
969
+ folderName = folderName.replace(/^\.?\//, ''); // Remove leading ./ or /
970
+ folderName = folderName.replace(/\/+$/, ''); // Remove trailing slashes
971
+ onlyFolders.add(folderName);
972
+ i += 1;
973
+ consumed += 1;
974
+ }
975
+ if (consumed === 0) {
976
+ console.error("Error: --only-folder requires at least one folder name.");
977
+ process.exit(1);
978
+ }
979
+ continue;
980
+ }
981
+
982
+ if (arg.startsWith("--only-folder=") || arg.startsWith("-ofo=")) {
983
+ const value = arg.startsWith("--only-folder=")
984
+ ? arg.slice("--only-folder=".length)
985
+ : arg.slice("-ofo=".length);
986
+ if (!value) {
987
+ console.error("Error: --only-folder requires a folder name.");
988
+ process.exit(1);
989
+ }
990
+ // Normalize the folder name
991
+ let folderName = value;
992
+ folderName = folderName.replace(/\\/g, '/'); // Convert backslashes to forward slashes
993
+ folderName = folderName.replace(/^\.?\//, ''); // Remove leading ./ or /
994
+ folderName = folderName.replace(/\/+$/, ''); // Remove trailing slashes
995
+ onlyFolders.add(folderName);
996
+ continue;
997
+ }
998
+
999
+ if (arg === "--only-file" || arg === "-ofi") {
1000
+ let consumed = 0;
1001
+ while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
1002
+ onlyFiles.add(args[i + 1]);
1003
+ i += 1;
1004
+ consumed += 1;
1005
+ }
1006
+ if (consumed === 0) {
1007
+ console.error("Error: --only-file requires at least one file name.");
1008
+ process.exit(1);
1009
+ }
1010
+ continue;
1011
+ }
1012
+
1013
+ if (arg.startsWith("--only-file=") || arg.startsWith("-ofi=")) {
1014
+ const value = arg.startsWith("--only-file=")
1015
+ ? arg.slice("--only-file=".length)
1016
+ : arg.slice("-ofi=".length);
1017
+ if (!value) {
1018
+ console.error("Error: --only-file requires a file name.");
1019
+ process.exit(1);
1020
+ }
1021
+ onlyFiles.add(value);
1022
+ continue;
1023
+ }
1024
+
1025
+ // Unknown argument
1026
+ console.error(`Error: Unknown option "${arg}"`);
1027
+ console.error("Use --help for available options.");
1028
+ process.exit(1);
1029
+ }
1030
+
1031
+ // Validate split options
1032
+ if (splitMethod === 'size' && !splitSize) {
1033
+ console.error("Error: --split-method size requires --split-size to be specified");
1034
+ process.exit(1);
1035
+ }
1036
+
1037
+ if (splitSize && splitMethod !== 'size') {
1038
+ console.error("Error: --split-size can only be used with --split-method size");
1039
+ process.exit(1);
1040
+ }
1041
+
1042
+ // ── config ────────────────────────────────────────────────────────────────────────
1043
+
1044
+ const folderPath = process.cwd();
1045
+ const rootName = path.basename(folderPath);
1046
+ const txtIgnore = readTxtIgnore(folderPath);
1047
+
1048
+ // ── build tree & collect file paths ───────────────────────────────────────────────
1049
+
1050
+ const { lines: treeLines, filePaths } = collectFiles(
1051
+ folderPath,
1052
+ folderPath,
1053
+ ignoreDirs,
1054
+ ignoreFiles,
1055
+ onlyFolders,
1056
+ onlyFiles,
1057
+ { rootName, txtIgnore, force: forceFlag }
1058
+ );
1059
+
1060
+ // ── build output filename ───────────────────────────────────────────────────────────
1061
+
1062
+ let outputFile = outputArg || path.join(folderPath, `${rootName}.txt`);
1063
+
1064
+ // ── handle output splitting ─────────────────────────────────────────────────────────
1065
+
1066
+ const effectiveMaxSize = noSkipFlag ? Infinity : maxFileSize;
1067
+
1068
+ if (splitMethod) {
1069
+ console.log(`🔧 Splitting output by: ${splitMethod}`);
1070
+
1071
+ let results;
1072
+
1073
+ if (splitMethod === 'folder') {
1074
+ results = splitByFolders(treeLines, filePaths, rootName, effectiveMaxSize, forceFlag);
1075
+ } else if (splitMethod === 'file') {
1076
+ results = splitByFiles(filePaths, rootName, effectiveMaxSize, forceFlag);
1077
+ } else if (splitMethod === 'size') {
1078
+ results = splitBySize(treeLines, filePaths, rootName, splitSize, effectiveMaxSize, forceFlag);
1079
+ }
1080
+
1081
+ console.log(`✅ Done! Created ${results.length} split files:`);
1082
+ console.log('');
1083
+
1084
+ results.forEach((result, index) => {
1085
+ if (splitMethod === 'folder') {
1086
+ console.log(`📁 Folder: ${result.folder}`);
1087
+ } else if (splitMethod === 'file') {
1088
+ console.log(`📄 File: ${result.fileName}`);
1089
+ } else if (splitMethod === 'size') {
1090
+ console.log(`📦 Part ${result.part}`);
1091
+ }
1092
+ console.log(`📄 Output : ${result.file}`);
1093
+ console.log(`📊 Size : ${result.size} KB`);
1094
+ console.log(`🗂️ Files : ${result.files}`);
1095
+ console.log('');
1096
+ });
1097
+
1098
+ if (copyToClipboardFlag) {
1099
+ console.log('⚠️ --copy flag is not compatible with splitting - clipboard copy skipped');
1100
+ }
1101
+
1102
+ process.exit(0);
1103
+ }
1104
+
1105
+ // ── build output (no splitting) ───────────────────────────────────────────────────
1106
+ const out = [];
1107
+ const divider = "=".repeat(80);
1108
+ const subDivider = "-".repeat(80);
1109
+
1110
+ out.push(divider);
1111
+ out.push(`START OF FOLDER: ${rootName}`);
1112
+ out.push(divider);
1113
+ out.push("");
1114
+
1115
+ out.push(divider);
1116
+ out.push("PROJECT STRUCTURE");
1117
+ out.push(divider);
1118
+ out.push(`Root: ${folderPath}\n`);
1119
+
1120
+ out.push(`${rootName}/`);
1121
+ treeLines.forEach(l => out.push(l));
1122
+ out.push("");
1123
+ out.push(`Total files: ${filePaths.length}`);
1124
+ out.push("");
1125
+
1126
+ out.push(divider);
1127
+ out.push("FILE CONTENTS");
1128
+ out.push(divider);
1129
+
1130
+ filePaths.forEach(({ abs, rel }) => {
1131
+ out.push("");
1132
+ out.push(subDivider);
1133
+ out.push(`FILE: ${rel}`);
1134
+ out.push(subDivider);
1135
+ out.push(readContent(abs, forceFlag, effectiveMaxSize));
1136
+ });
1137
+
1138
+ out.push("");
1139
+ out.push(divider);
1140
+ out.push(`END OF FOLDER: ${rootName}`);
1141
+ out.push(divider);
1142
+
1143
+ fs.writeFileSync(outputFile, out.join("\n"), "utf8");
1144
+
1145
+ const sizeKB = (fs.statSync(outputFile).size / 1024).toFixed(1);
1146
+ console.log(`✅ Done!`);
1147
+ console.log(`📄 Output : ${outputFile}`);
1148
+ console.log(`📊 Size : ${sizeKB} KB`);
1149
+ console.log(`🗂️ Files : ${filePaths.length}`);
1150
+
1151
+ if (copyToClipboardFlag) {
1152
+ const content = fs.readFileSync(outputFile, 'utf8');
1153
+ const success = copyToClipboard(content);
1154
+ if (success) {
1155
+ console.log(`📋 Copied to clipboard!`);
1156
+ }
1157
+ }
1158
+
1159
+ console.log('');
1160
+ }
1161
+
1162
+ // Run the main function
1163
+ main().catch(err => {
1164
+ console.error('❌ Error:', err.message);
1165
+ process.exit(1);
1166
+ });
1167
+
1168
+
1169
+ --------------------------------------------------------------------------------
1170
+ FILE: /.txtconfig
1171
+ --------------------------------------------------------------------------------
1172
+ {
1173
+ "maxFileSize": "400KB",
1174
+ "splitMethod": "none",
1175
+ "splitSize": "5MB",
1176
+ "copyToClipboard": false,
1177
+ "ignoreFolders": ["skip"],
1178
+ "ignoreFiles": ["skip"]
1179
+ }
1180
+
1181
+ --------------------------------------------------------------------------------
1182
+ FILE: /LICENSE
1183
+ --------------------------------------------------------------------------------
1184
+ MIT License
1185
+
1186
+ Copyright (c) 2026 Muhammad Saad Amin @SENODROOM
1187
+
1188
+ Permission is hereby granted, free of charge, to any person obtaining a copy
1189
+ of this software and associated documentation files (the "Software"), to deal
1190
+ in the Software without restriction, including without limitation the rights
1191
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1192
+ copies of the Software, and to permit persons to whom the Software is
1193
+ furnished to do so, subject to the following conditions:
1194
+
1195
+ The above copyright notice and this permission notice shall be included in all
1196
+ copies or substantial portions of the Software.
1197
+
1198
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1199
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1200
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1201
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1202
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1203
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1204
+ SOFTWARE.
1205
+
1206
+
1207
+ --------------------------------------------------------------------------------
1208
+ FILE: /package.json
1209
+ --------------------------------------------------------------------------------
1210
+ {
1211
+ "name": "make-folder-txt",
1212
+ "version": "2.2.0",
1213
+ "description": "Generate a single .txt file containing the full folder structure and file contents of any project, ignoring node_modules and other junk.",
1214
+ "main": "bin/make-folder-txt.js",
1215
+ "bin": {
1216
+ "make-folder-txt": "bin/make-folder-txt.js"
1217
+ },
1218
+ "scripts": {
1219
+ "test": "echo \"No tests yet\" && exit 0"
1220
+ },
1221
+ "keywords": [
1222
+ "folder",
1223
+ "dump",
1224
+ "project",
1225
+ "structure",
1226
+ "txt",
1227
+ "cli",
1228
+ "export"
1229
+ ],
1230
+ "author": "Muhammad Saad Amin",
1231
+ "license": "MIT",
1232
+ "engines": {
1233
+ "node": ">=14.0.0"
1234
+ }
1235
+ }
1236
+
1237
+
1238
+ --------------------------------------------------------------------------------
1239
+ FILE: /README.md
1240
+ --------------------------------------------------------------------------------
1241
+ <div align="center">
1242
+
1243
+ # 📁 make-folder-txt
1244
+
1245
+ **Instantly dump your entire project into a single, readable `.txt` file.**
1246
+
1247
+ [![npm version](https://img.shields.io/npm/v/make-folder-txt?color=crimson&style=flat-square)](https://www.npmjs.com/package/make-folder-txt)
1248
+ [![npm downloads](https://img.shields.io/npm/dm/make-folder-txt?color=orange&style=flat-square)](https://www.npmjs.com/package/make-folder-txt)
1249
+ [![license](https://img.shields.io/npm/l/make-folder-txt?color=blue&style=flat-square)](./LICENSE)
1250
+ [![node](https://img.shields.io/node/v/make-folder-txt?color=green&style=flat-square)](https://nodejs.org)
1251
+
1252
+ Perfect for sharing your codebase with **AI tools**, **teammates**, or **code reviewers** — without zipping files or giving repo access.
1253
+
1254
+ [Installation](#-installation) · [Usage](#-usage) · [Help](#-get-help) · [Copy to Clipboard](#-copy-to-clipboard) · [Force Include Everything](#-force-include-everything) · [File Size Control](#-file-size-control) · [Output Splitting](#-output-splitting) · [Output Format](#-output-format) · [What Gets Skipped](#-what-gets-skipped) · [Contributing](#-contributing)
1255
+
1256
+ </div>
1257
+
1258
+ ---
1259
+
1260
+ ## ✨ Why make-folder-txt?
1261
+
1262
+ Ever needed to share your entire project with ChatGPT, Claude, or a teammate — but copy-pasting every file one by one is painful? **make-folder-txt** solves that in one command.
1263
+
1264
+ - ✅ Run it from any project directory — no arguments needed
1265
+ - ✅ Built-in help system with `--help` flag
1266
+ - ✅ File size control with `--skip-large` and `--no-skip`
1267
+ - ✅ Output splitting by folders, files, or size
1268
+ - ✅ Copy to clipboard with `--copy` flag
1269
+ - ✅ Force include everything with `--force` flag
1270
+ - ✅ Generates a clean folder tree + every file's content
1271
+ - ✅ `.txtignore` support (works like `.gitignore`)
1272
+ - ✅ Automatically skips `node_modules`, binaries, and junk files
1273
+ - ✅ Zero dependencies — pure Node.js
1274
+ - ✅ Works on Windows, macOS, and Linux
1275
+
1276
+ ---
1277
+
1278
+ ## 📦 Installation
1279
+
1280
+ Install globally once, use anywhere:
1281
+
1282
+ ```bash
1283
+ npm install -g make-folder-txt
1284
+ ```
1285
+
1286
+ ---
1287
+
1288
+ ## 🚀 Usage
1289
+
1290
+ Navigate into your project folder and run:
1291
+
1292
+ ```bash
1293
+ cd my-project
1294
+ make-folder-txt
1295
+ ```
1296
+
1297
+ That's it. A `my-project.txt` file will be created in the same directory.
1298
+
1299
+ ### 📖 Get Help
1300
+
1301
+ ```bash
1302
+ make-folder-txt --help # Show all options and examples
1303
+ make-folder-txt -h # Short version of help
1304
+ make-folder-txt --version # Show version info
1305
+ make-folder-txt -v # Short version of version
1306
+ ```
1307
+
1308
+ ### 📋 Copy to Clipboard
1309
+
1310
+ ```bash
1311
+ make-folder-txt --copy # Generate output and copy to clipboard
1312
+ make-folder-txt --copy --ignore-folder node_modules # Copy filtered output
1313
+ ```
1314
+
1315
+ The `--copy` flag automatically copies the generated output to your system clipboard, making it easy to paste directly into AI tools, emails, or documents. Works on Windows, macOS, and Linux (requires `xclip` or `xsel` on Linux).
1316
+
1317
+ ### 🔥 Force Include Everything
1318
+
1319
+ ```bash
1320
+ make-folder-txt --force # Include everything (overrides all ignore patterns)
1321
+ make-folder-txt --force --copy # Include everything and copy to clipboard
1322
+ ```
1323
+
1324
+ The `--force` flag overrides all ignore patterns and includes:
1325
+ - `node_modules` and other ignored folders
1326
+ - Binary files (images, executables, etc.)
1327
+ - Large files (no 500 KB limit)
1328
+ - Files in `.txtignore`
1329
+ - System files and other normally skipped content
1330
+
1331
+ Use this when you need a complete, unfiltered dump of your entire project.
1332
+
1333
+ ### 📏 File Size Control
1334
+
1335
+ ```bash
1336
+ make-folder-txt --skip-large 400KB # Skip files larger than 400KB
1337
+ make-folder-txt --skip-large 5GB # Skip files larger than 5GB
1338
+ make-folder-txt --skip-large 1.5MB # Skip files larger than 1.5MB
1339
+ make-folder-txt --no-skip # Include all files regardless of size
1340
+ ```
1341
+
1342
+ **Default behavior**: Files larger than 500KB are skipped by default.
1343
+
1344
+ **Supported size units**:
1345
+ - **B** - Bytes
1346
+ - **KB** - Kilobytes (1024 bytes)
1347
+ - **MB** - Megabytes (1024 KB)
1348
+ - **GB** - Gigabytes (1024 MB)
1349
+ - **TB** - Terabytes (1024 GB)
1350
+
1351
+ **Examples:**
1352
+ ```bash
1353
+ # More restrictive - skip anything over 100KB
1354
+ make-folder-txt --skip-large 100KB
1355
+
1356
+ # More permissive - allow files up to 10MB
1357
+ make-folder-txt --skip-large 10MB
1358
+
1359
+ # Include everything - no size limits
1360
+ make-folder-txt --no-skip
1361
+
1362
+ # Combine with other options
1363
+ make-folder-txt --skip-large 2MB --ignore-folder node_modules
1364
+ ```
1365
+
1366
+ **Size format**: Accepts decimal numbers (e.g., `1.5MB`, `0.5GB`) and various units.
1367
+
1368
+ ### 📂 Output Splitting
1369
+
1370
+ ```bash
1371
+ make-folder-txt --split-method folder # Split by folders
1372
+ make-folder-txt --split-method file # Split by files
1373
+ make-folder-txt --split-method size --split-size 5MB # Split by file size
1374
+ ```
1375
+
1376
+ **Split Methods:**
1377
+ - **`folder`** - Creates separate files for each folder
1378
+ - **`file`** - Creates separate files for each individual file
1379
+ - **`size`** - Splits output when content exceeds specified size
1380
+
1381
+ **Examples:**
1382
+ ```bash
1383
+ # Split by folders - creates folder-name.txt for each folder
1384
+ make-folder-txt --split-method folder
1385
+
1386
+ # Split by files - creates filename.txt for each file
1387
+ make-folder-txt --split-method file
1388
+
1389
+ # Split by size - creates part-1.txt, part-2.txt, etc.
1390
+ make-folder-txt --split-method size --split-size 5MB
1391
+
1392
+ # Combine with other options
1393
+ make-folder-txt --split-method size --split-size 2MB --ignore-folder node_modules
1394
+ ```
1395
+
1396
+ **Output Files:**
1397
+ - **Folder method**: `projectname-foldername.txt`
1398
+ - **File method**: `projectname-filename.txt`
1399
+ - **Size method**: `projectname-part-1.txt`, `projectname-part-2.txt`, etc.
1400
+
1401
+ **Note**: Splitting is not compatible with `--copy` flag.
1402
+
1403
+ Ignore specific folders/files by name:
1404
+
1405
+ ```bash
1406
+ make-folder-txt --ignore-folder examples extensions docs
1407
+ make-folder-txt -ifo examples extensions docs # shorthand
1408
+ make-folder-txt --ignore-folder examples extensions "docs and explaination"
1409
+ make-folder-txt --ignore-folder examples extensions docs --ignore-file LICENSE
1410
+ make-folder-txt --ignore-file .env .env.local secrets.txt
1411
+ make-folder-txt -ifi .env .env.local secrets.txt # shorthand
1412
+ ```
1413
+
1414
+ Use a `.txtignore` file (works like `.gitignore`):
1415
+
1416
+ ```bash
1417
+ # Create a .txtignore file in your project root
1418
+ echo "node_modules/" > .txtignore
1419
+ echo "*.log" >> .txtignore
1420
+ echo ".env" >> .txtignore
1421
+ echo "coverage/" >> .txtignore
1422
+
1423
+ # The tool will automatically read and respect .txtignore patterns
1424
+ make-folder-txt
1425
+ ```
1426
+
1427
+ The `.txtignore` file supports:
1428
+ - File and folder names (one per line)
1429
+ - Wildcard patterns (`*.log`, `temp-*`)
1430
+ - Comments (lines starting with `#`)
1431
+ - Folder patterns with trailing slash (`docs/`)
1432
+
1433
+ Include only specific folders/files by name (everything else is ignored):
1434
+
1435
+ ```bash
1436
+ make-folder-txt --only-folder src docs
1437
+ make-folder-txt -ofo src docs # shorthand
1438
+ make-folder-txt --only-file package.json README.md
1439
+ make-folder-txt -ofi package.json README.md # shorthand
1440
+ make-folder-txt --only-folder src --only-file package.json
1441
+ ```
1442
+
1443
+ ---
1444
+
1445
+ ## 🎯 Real World Examples
1446
+
1447
+ **Sharing with an AI tool (ChatGPT, Claude, etc.):**
1448
+
1449
+ ```bash
1450
+ cd "C:\Web Development\my-app\backend"
1451
+ make-folder-txt
1452
+ # → backend.txt created, ready to paste into any AI chat
1453
+ ```
1454
+
1455
+ **On macOS / Linux:**
1456
+
1457
+ ```bash
1458
+ cd /home/user/projects/my-app
1459
+ make-folder-txt
1460
+ # → my-app.txt created
1461
+ ```
1462
+
1463
+ ---
1464
+
1465
+ ## 📄 Output Format
1466
+
1467
+ The generated `.txt` file is structured in two clear sections:
1468
+
1469
+ ```
1470
+ ================================================================================
1471
+ START OF FOLDER: my-project
1472
+ ================================================================================
1473
+
1474
+ ================================================================================
1475
+ PROJECT STRUCTURE
1476
+ ================================================================================
1477
+ Root: C:\Web Development\my-project
1478
+
1479
+ my-project/
1480
+ ├── src/
1481
+ │ ├── controllers/
1482
+ │ │ └── userController.js
1483
+ │ ├── models/
1484
+ │ │ └── User.js
1485
+ │ └── index.js
1486
+ ├── node_modules/ [skipped]
1487
+ ├── package.json
1488
+ └── README.md
1489
+
1490
+ Total files: 5
1491
+
1492
+ ================================================================================
1493
+ FILE CONTENTS
1494
+ ================================================================================
1495
+
1496
+ --------------------------------------------------------------------------------
1497
+ FILE: /src/index.js
1498
+ --------------------------------------------------------------------------------
1499
+ const express = require('express');
1500
+ ...
1501
+
1502
+ --------------------------------------------------------------------------------
1503
+ FILE: /package.json
1504
+ --------------------------------------------------------------------------------
1505
+ {
1506
+ "name": "my-project",
1507
+ ...
1508
+ }
1509
+
1510
+ ================================================================================
1511
+ END OF FOLDER: my-project
1512
+ ================================================================================
1513
+ ```
1514
+
1515
+ ---
1516
+
1517
+ ## 🚫 What Gets Skipped
1518
+
1519
+ The tool is smart about what it ignores so your output stays clean and readable.
1520
+
1521
+ | Category | Details |
1522
+ | --------------- | -------------------------------------------------------------- |
1523
+ | 📁 Folders | `node_modules`, `.git`, `.next`, `dist`, `build`, `.cache` |
1524
+ | 🖼️ Binary files | Images (`.png`, `.jpg`, `.gif`...), fonts, videos, executables |
1525
+ | 📦 Archives | `.zip`, `.tar`, `.gz`, `.rar`, `.7z` |
1526
+ | 🔤 Font files | `.woff`, `.woff2`, `.ttf`, `.eot`, `.otf` |
1527
+ | 📋 Lock files | `package-lock.json`, `yarn.lock` |
1528
+ | 📏 Large files | Any file over **500 KB** |
1529
+ | 🗑️ System files | `.DS_Store`, `Thumbs.db`, `desktop.ini` |
1530
+ | 📄 Output file | The generated `foldername.txt` file (to avoid infinite loops) |
1531
+ | 📝 .txtignore | Any files/folders specified in `.txtignore` file |
1532
+
1533
+ Binary and skipped files are noted in the output as `[binary / skipped]` so you always know what was omitted.
1534
+
1535
+ ---
1536
+
1537
+ ## 🛠️ Requirements
1538
+
1539
+ - **Node.js** v14.0.0 or higher
1540
+ - No other dependencies
1541
+
1542
+ ---
1543
+
1544
+ ## 🤝 Contributing
1545
+
1546
+ Contributions, issues, and feature requests are welcome!
1547
+
1548
+ 1. Fork the repository
1549
+ 2. Create your feature branch: `git checkout -b feature/my-feature`
1550
+ 3. Commit your changes: `git commit -m 'Add my feature'`
1551
+ 4. Push to the branch: `git push origin feature/my-feature`
1552
+ 5. Open a Pull Request
1553
+
1554
+ ---
1555
+
1556
+ ## 👤 Author
1557
+
1558
+ **Muhammad Saad Amin**
1559
+
1560
+ ---
1561
+
1562
+ ## 📝 License
1563
+
1564
+ This project is licensed under the **MIT License** — feel free to use it in personal and commercial projects.
1565
+
1566
+ ---
1567
+
1568
+ <div align="center">
1569
+
1570
+ If this tool saved you time, consider giving it a ⭐ on npm!
1571
+
1572
+ </div>
1573
+
1574
+
1575
+ ================================================================================
1576
+ END OF FOLDER: make-folder-txt
1577
+ ================================================================================