make-folder-txt 2.2.10 → 2.3.0

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.
@@ -1,1258 +0,0 @@
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
- ├── bin/
12
- │ └── make-folder-txt.js
13
-
14
- Total files: 1
15
-
16
- ================================================================================
17
- FILE CONTENTS
18
- ================================================================================
19
-
20
- --------------------------------------------------------------------------------
21
- FILE: /bin/make-folder-txt.js
22
- --------------------------------------------------------------------------------
23
- #!/usr/bin/env node
24
-
25
- const fs = require("fs");
26
- const path = require("path");
27
- const { version } = require("../package.json");
28
- const { execSync } = require("child_process");
29
-
30
- // ── config ────────────────────────────────────────────────────────────────────
31
- const IGNORE_DIRS = new Set(["node_modules", ".git", ".next", "dist", "build", ".cache"]);
32
- const IGNORE_FILES = new Set([".DS_Store", "Thumbs.db", "desktop.ini", ".txtignore"]);
33
-
34
- const BINARY_EXTS = new Set([
35
- ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".svg", ".webp",
36
- ".pdf", ".zip", ".tar", ".gz", ".rar", ".7z",
37
- ".exe", ".dll", ".so", ".dylib", ".bin",
38
- ".mp3", ".mp4", ".wav", ".avi", ".mov",
39
- ".woff", ".woff2", ".ttf", ".eot", ".otf",
40
- ".lock",
41
- ]);
42
-
43
- // ── helpers ───────────────────────────────────────────────────────────────────
44
-
45
- function readTxtIgnore(rootDir) {
46
- const txtIgnorePath = path.join(rootDir, '.txtignore');
47
- const ignorePatterns = new Set();
48
-
49
- try {
50
- const content = fs.readFileSync(txtIgnorePath, 'utf8');
51
- const lines = content.split('\n')
52
- .map(line => line.trim())
53
- .filter(line => line && !line.startsWith('#'));
54
-
55
- lines.forEach(line => ignorePatterns.add(line));
56
- } catch (err) {
57
- // .txtignore doesn't exist or can't be read - that's fine
58
- }
59
-
60
- return ignorePatterns;
61
- }
62
-
63
- function copyToClipboard(text) {
64
- try {
65
- if (process.platform === 'win32') {
66
- // Windows - use PowerShell for better handling of large content
67
- const tempFile = require('os').tmpdir() + '\\make-folder-txt-clipboard-temp.txt';
68
- require('fs').writeFileSync(tempFile, text, 'utf8');
69
- execSync(`powershell -Command "Get-Content '${tempFile}' | Set-Clipboard"`, { stdio: 'ignore' });
70
- require('fs').unlinkSync(tempFile);
71
- } else if (process.platform === 'darwin') {
72
- // macOS
73
- execSync(`echo ${JSON.stringify(text)} | pbcopy`, { stdio: 'ignore' });
74
- } else {
75
- // Linux (requires xclip or xsel)
76
- try {
77
- execSync(`echo ${JSON.stringify(text)} | xclip -selection clipboard`, { stdio: 'ignore' });
78
- } catch {
79
- try {
80
- execSync(`echo ${JSON.stringify(text)} | xsel --clipboard --input`, { stdio: 'ignore' });
81
- } catch {
82
- console.warn('⚠️ Could not copy to clipboard. Install xclip or xsel on Linux.');
83
- return false;
84
- }
85
- }
86
- }
87
- return true;
88
- } catch (err) {
89
- console.warn('⚠️ Could not copy to clipboard: ' + err.message);
90
- return false;
91
- }
92
- }
93
-
94
- function collectFiles(
95
- dir,
96
- rootDir,
97
- ignoreDirs,
98
- ignoreFiles,
99
- onlyFolders,
100
- onlyFiles,
101
- options = {},
102
- ) {
103
- const {
104
- indent = "",
105
- lines = [],
106
- filePaths = [],
107
- inSelectedFolder = false,
108
- hasOnlyFilters = false,
109
- rootName = "",
110
- txtIgnore = new Set(),
111
- force = false,
112
- rootOnlyInclude = false,
113
- } = options;
114
-
115
- let entries;
116
- try {
117
- entries = fs.readdirSync(dir, { withFileTypes: true });
118
- } catch {
119
- return { lines, filePaths, hasIncluded: false };
120
- }
121
-
122
- entries.sort((a, b) => {
123
- if (a.isDirectory() === b.isDirectory()) return a.name.localeCompare(b.name);
124
- return a.isDirectory() ? -1 : 1;
125
- });
126
-
127
- entries.forEach((entry, idx) => {
128
- const isLast = idx === entries.length - 1;
129
- const connector = isLast ? "└── " : "├── ";
130
- const childIndent = indent + (isLast ? " " : "│ ");
131
-
132
- if (entry.isDirectory()) {
133
- if (!force && ignoreDirs.has(entry.name)) {
134
- if (!hasOnlyFilters) {
135
- lines.push(`${indent}${connector}${entry.name}/ [skipped]`);
136
- }
137
- return;
138
- }
139
-
140
- // Get relative path for .txtignore pattern matching
141
- const relPathForIgnore = path.relative(rootDir, path.join(dir, entry.name)).split(path.sep).join("/");
142
-
143
- // Check against .txtignore patterns (both dirname and relative path) unless force is enabled
144
- if (!force && (txtIgnore.has(entry.name) || txtIgnore.has(`${entry.name}/`) || txtIgnore.has(relPathForIgnore) || txtIgnore.has(`${relPathForIgnore}/`) || txtIgnore.has(`/${relPathForIgnore}/`))) {
145
- if (!hasOnlyFilters) {
146
- lines.push(`${indent}${connector}${entry.name}/ [skipped]`);
147
- }
148
- return;
149
- }
150
-
151
- const childPath = path.join(dir, entry.name);
152
- const childInSelectedFolder = inSelectedFolder || onlyFolders.has(entry.name);
153
- const childLines = [];
154
- const childFiles = [];
155
-
156
- // If rootOnlyInclude is true, only include root-level items
157
- let child;
158
- if (rootOnlyInclude && dir !== rootDir) {
159
- // Don't recurse into subdirectories when rootOnlyInclude is true
160
- child = { lines: [], filePaths: [], hasIncluded: false };
161
- } else {
162
- child = collectFiles(
163
- childPath,
164
- rootDir,
165
- ignoreDirs,
166
- ignoreFiles,
167
- onlyFolders,
168
- onlyFiles,
169
- {
170
- indent: childIndent,
171
- lines: childLines,
172
- filePaths: childFiles,
173
- inSelectedFolder: childInSelectedFolder,
174
- hasOnlyFilters,
175
- rootName,
176
- txtIgnore,
177
- force,
178
- rootOnlyInclude,
179
- },
180
- );
181
- }
182
-
183
- const explicitlySelectedFolder = hasOnlyFilters && onlyFolders.has(entry.name);
184
- // For root-only include, only include if we're at the root level and it matches
185
- const shouldIncludeDir = !hasOnlyFilters ||
186
- (rootOnlyInclude && dir === rootDir && onlyFolders.has(entry.name)) ||
187
- (!rootOnlyInclude && child.hasIncluded) ||
188
- explicitlySelectedFolder;
189
-
190
- if (shouldIncludeDir) {
191
- lines.push(`${indent}${connector}${entry.name}/`);
192
- lines.push(...child.lines);
193
- filePaths.push(...child.filePaths);
194
- }
195
- } else {
196
- if (!force && ignoreFiles.has(entry.name)) return;
197
-
198
- // Get relative path for .txtignore pattern matching
199
- const relPathForIgnore = path.relative(rootDir, path.join(dir, entry.name)).split(path.sep).join("/");
200
-
201
- // Check against .txtignore patterns (both filename and relative path) unless force is enabled
202
- if (!force && (txtIgnore.has(entry.name) || txtIgnore.has(relPathForIgnore) || txtIgnore.has(`/${relPathForIgnore}`))) {
203
- return;
204
- }
205
-
206
- // Ignore .txt files that match the folder name (e.g., foldername.txt) unless force is enabled
207
- if (!force && entry.name.endsWith('.txt') && entry.name === `${rootName}.txt`) return;
208
-
209
- // Check if this file matches any of the onlyFiles patterns
210
- const shouldIncludeFile = !hasOnlyFilters || inSelectedFolder ||
211
- onlyFiles.has(entry.name) ||
212
- onlyFiles.has(relPathForIgnore) ||
213
- onlyFiles.has(`/${relPathForIgnore}`);
214
-
215
- if (!shouldIncludeFile) return;
216
-
217
- lines.push(`${indent}${connector}${entry.name}`);
218
- const relPath = "/" + path.relative(rootDir, path.join(dir, entry.name)).split(path.sep).join("/");
219
- filePaths.push({ abs: path.join(dir, entry.name), rel: relPath });
220
- }
221
- });
222
-
223
- return { lines, filePaths, hasIncluded: filePaths.length > 0 || lines.length > 0 };
224
- }
225
-
226
- function parseFileSize(sizeStr) {
227
- const units = {
228
- 'B': 1,
229
- 'KB': 1024,
230
- 'MB': 1024 * 1024,
231
- 'GB': 1024 * 1024 * 1024,
232
- 'TB': 1024 * 1024 * 1024 * 1024
233
- };
234
-
235
- const match = sizeStr.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)$/i);
236
- if (!match) {
237
- console.error(`Error: Invalid size format "${sizeStr}". Use format like "500KB", "2MB", "1GB".`);
238
- process.exit(1);
239
- }
240
-
241
- const value = parseFloat(match[1]);
242
- const unit = match[2].toUpperCase();
243
- return Math.floor(value * units[unit]);
244
- }
245
-
246
- function readContent(absPath, force = false, maxFileSize = 500 * 1024) {
247
- const ext = path.extname(absPath).toLowerCase();
248
- if (!force && BINARY_EXTS.has(ext)) return "[binary / skipped]";
249
- try {
250
- const stat = fs.statSync(absPath);
251
- if (!force && stat.size > maxFileSize) {
252
- const sizeStr = stat.size < 1024 ? `${stat.size} B` :
253
- stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB` :
254
- stat.size < 1024 * 1024 * 1024 ? `${(stat.size / (1024 * 1024)).toFixed(1)} MB` :
255
- `${(stat.size / (1024 * 1024 * 1024)).toFixed(1)} GB`;
256
- return `[file too large: ${sizeStr} – skipped]`;
257
- }
258
- return fs.readFileSync(absPath, "utf8");
259
- } catch (err) {
260
- return `[could not read file: ${err.message}]`;
261
- }
262
- }
263
-
264
- function splitByFolders(treeLines, filePaths, rootName, effectiveMaxSize, forceFlag) {
265
- const folders = new Map();
266
-
267
- // Group files by folder
268
- filePaths.forEach(({ abs, rel }) => {
269
- const folderPath = path.dirname(rel);
270
- const folderKey = folderPath === '/' ? rootName : folderPath.slice(1);
271
-
272
- if (!folders.has(folderKey)) {
273
- folders.set(folderKey, []);
274
- }
275
- folders.get(folderKey).push({ abs, rel });
276
- });
277
-
278
- const results = [];
279
-
280
- folders.forEach((files, folderName) => {
281
- const out = [];
282
- const divider = "=".repeat(80);
283
- const subDivider = "-".repeat(80);
284
-
285
- out.push(divider);
286
- out.push(`START OF FOLDER: ${folderName}`);
287
- out.push(divider);
288
- out.push("");
289
-
290
- // Add folder structure (only this folder's structure)
291
- const folderTreeLines = treeLines.filter(line =>
292
- line.includes(folderName + '/') || line === `${rootName}/`
293
- );
294
-
295
- out.push(divider);
296
- out.push("PROJECT STRUCTURE");
297
- out.push(divider);
298
- out.push(`Root: ${folderPath}\n`);
299
- out.push(`${rootName}/`);
300
- folderTreeLines.forEach(l => out.push(l));
301
- out.push("");
302
- out.push(`Total files in this folder: ${files.length}`);
303
- out.push("");
304
-
305
- out.push(divider);
306
- out.push("FILE CONTENTS");
307
- out.push(divider);
308
-
309
- files.forEach(({ abs, rel }) => {
310
- out.push("");
311
- out.push(subDivider);
312
- out.push(`FILE: ${rel}`);
313
- out.push(subDivider);
314
- out.push(readContent(abs, forceFlag, effectiveMaxSize));
315
- });
316
-
317
- out.push("");
318
- out.push(divider);
319
- out.push(`END OF FOLDER: ${folderName}`);
320
- out.push(divider);
321
-
322
- const fileName = `${rootName}-${folderName.replace(/[\/\\]/g, '-')}.txt`;
323
- const filePath = path.join(process.cwd(), fileName);
324
-
325
- fs.writeFileSync(filePath, out.join("\n"), "utf8");
326
- const sizeKB = (fs.statSync(filePath).size / 1024).toFixed(1);
327
-
328
- results.push({
329
- file: filePath,
330
- size: sizeKB,
331
- files: files.length,
332
- folder: folderName
333
- });
334
- });
335
-
336
- return results;
337
- }
338
-
339
- function splitByFiles(filePaths, rootName, effectiveMaxSize, forceFlag) {
340
- const results = [];
341
-
342
- filePaths.forEach(({ abs, rel }) => {
343
- const out = [];
344
- const divider = "=".repeat(80);
345
- const subDivider = "-".repeat(80);
346
- const fileName = path.basename(rel, path.extname(rel));
347
-
348
- out.push(divider);
349
- out.push(`FILE: ${rel}`);
350
- out.push(divider);
351
- out.push("");
352
-
353
- out.push(divider);
354
- out.push("FILE CONTENTS");
355
- out.push(divider);
356
- out.push(readContent(abs, forceFlag, effectiveMaxSize));
357
-
358
- out.push("");
359
- out.push(divider);
360
- out.push(`END OF FILE: ${rel}`);
361
- out.push(divider);
362
-
363
- const outputFileName = `${rootName}-${fileName}.txt`;
364
- const filePath = path.join(process.cwd(), outputFileName);
365
-
366
- fs.writeFileSync(filePath, out.join("\n"), "utf8");
367
- const sizeKB = (fs.statSync(filePath).size / 1024).toFixed(1);
368
-
369
- results.push({
370
- file: filePath,
371
- size: sizeKB,
372
- files: 1,
373
- fileName: fileName
374
- });
375
- });
376
-
377
- return results;
378
- }
379
-
380
- function splitBySize(treeLines, filePaths, rootName, splitSize, effectiveMaxSize, forceFlag) {
381
- const results = [];
382
- let currentPart = 1;
383
- let currentSize = 0;
384
- let currentFiles = [];
385
-
386
- const divider = "=".repeat(80);
387
- const subDivider = "-".repeat(80);
388
-
389
- // Start with header
390
- let out = [];
391
- out.push(divider);
392
- out.push(`START OF FOLDER: ${rootName} (Part ${currentPart})`);
393
- out.push(divider);
394
- out.push("");
395
-
396
- out.push(divider);
397
- out.push("PROJECT STRUCTURE");
398
- out.push(divider);
399
- out.push(`Root: ${folderPath}\n`);
400
- out.push(`${rootName}/`);
401
- treeLines.forEach(l => out.push(l));
402
- out.push("");
403
- out.push(`Total files: ${filePaths.length}`);
404
- out.push("");
405
-
406
- out.push(divider);
407
- out.push("FILE CONTENTS");
408
- out.push(divider);
409
-
410
- filePaths.forEach(({ abs, rel }) => {
411
- const content = readContent(abs, forceFlag, effectiveMaxSize);
412
- const fileContent = [
413
- "",
414
- subDivider,
415
- `FILE: ${rel}`,
416
- subDivider,
417
- content
418
- ];
419
-
420
- const contentSize = fileContent.join("\n").length;
421
-
422
- // Check if adding this file would exceed the split size
423
- if (currentSize + contentSize > splitSize && currentFiles.length > 0) {
424
- // Finish current part
425
- out.push("");
426
- out.push(divider);
427
- out.push(`END OF FOLDER: ${rootName} (Part ${currentPart})`);
428
- out.push(divider);
429
-
430
- // Write current part
431
- const fileName = `${rootName}-part-${currentPart}.txt`;
432
- const filePath = path.join(process.cwd(), fileName);
433
- fs.writeFileSync(filePath, out.join("\n"), "utf8");
434
- const sizeKB = (fs.statSync(filePath).size / 1024).toFixed(1);
435
-
436
- results.push({
437
- file: filePath,
438
- size: sizeKB,
439
- files: currentFiles.length,
440
- part: currentPart
441
- });
442
-
443
- // Start new part
444
- currentPart++;
445
- currentSize = 0;
446
- currentFiles = [];
447
-
448
- out = [];
449
- out.push(divider);
450
- out.push(`START OF FOLDER: ${rootName} (Part ${currentPart})`);
451
- out.push(divider);
452
- out.push("");
453
-
454
- out.push(divider);
455
- out.push("PROJECT STRUCTURE");
456
- out.push(divider);
457
- out.push(`Root: ${folderPath}\n`);
458
- out.push(`${rootName}/`);
459
- treeLines.forEach(l => out.push(l));
460
- out.push("");
461
- out.push(`Total files: ${filePaths.length}`);
462
- out.push("");
463
-
464
- out.push(divider);
465
- out.push("FILE CONTENTS");
466
- out.push(divider);
467
- }
468
-
469
- // Add file to current part
470
- out.push(...fileContent);
471
- currentSize += contentSize;
472
- currentFiles.push(rel);
473
- });
474
-
475
- // Write final part
476
- out.push("");
477
- out.push(divider);
478
- out.push(`END OF FOLDER: ${rootName} (Part ${currentPart})`);
479
- out.push(divider);
480
-
481
- const fileName = `${rootName}-part-${currentPart}.txt`;
482
- const filePath = path.join(process.cwd(), fileName);
483
- fs.writeFileSync(filePath, out.join("\n"), "utf8");
484
- const sizeKB = (fs.statSync(filePath).size / 1024).toFixed(1);
485
-
486
- results.push({
487
- file: filePath,
488
- size: sizeKB,
489
- files: currentFiles.length,
490
- part: currentPart
491
- });
492
-
493
- return results;
494
- }
495
-
496
- // ── configuration ────────────────────────────────────────────────────────────────
497
-
498
- function createInteractiveConfig() {
499
- const readline = require('readline');
500
- const fs = require('fs');
501
- const path = require('path');
502
- const os = require('os');
503
-
504
- const rl = readline.createInterface({
505
- input: process.stdin,
506
- output: process.stdout
507
- });
508
-
509
- console.log('\n🔧 make-folder-txt Configuration Setup');
510
- console.log('=====================================\n');
511
-
512
- return new Promise((resolve) => {
513
- const config = {
514
- maxFileSize: '500KB',
515
- splitMethod: 'none',
516
- splitSize: '5MB',
517
- copyToClipboard: false
518
- };
519
-
520
- let currentStep = 0;
521
- const questions = [
522
- {
523
- key: 'maxFileSize',
524
- question: 'Maximum file size to include (e.g., 500KB, 2MB, 1GB): ',
525
- default: '500KB',
526
- validate: (value) => {
527
- if (!value.trim()) return true;
528
- const validUnits = ['B', 'KB', 'MB', 'GB', 'TB'];
529
- const match = value.match(/^(\d+(?:\.\d+)?)\s*([A-Z]+)$/i);
530
- if (!match) return 'Please enter a valid size (e.g., 500KB, 2MB, 1GB)';
531
- if (!validUnits.includes(match[2].toUpperCase())) return `Invalid unit. Use: ${validUnits.join(', ')}`;
532
- return true;
533
- }
534
- },
535
- {
536
- key: 'splitMethod',
537
- question: 'Split output method (none, folder, file, size): ',
538
- default: 'none',
539
- validate: (value) => {
540
- const validMethods = ['none', 'folder', 'file', 'size'];
541
- if (!validMethods.includes(value.toLowerCase())) return `Please choose: ${validMethods.join(', ')}`;
542
- return true;
543
- }
544
- },
545
- {
546
- key: 'splitSize',
547
- question: 'Split size when using size method (e.g., 5MB, 10MB): ',
548
- default: '5MB',
549
- ask: () => config.splitMethod === 'size',
550
- validate: (value) => {
551
- if (!value.trim()) return true;
552
- const validUnits = ['B', 'KB', 'MB', 'GB', 'TB'];
553
- const match = value.match(/^(\d+(?:\.\d+)?)\s*([A-Z]+)$/i);
554
- if (!match) return 'Please enter a valid size (e.g., 5MB, 10MB)';
555
- if (!validUnits.includes(match[2].toUpperCase())) return `Invalid unit. Use: ${validUnits.join(', ')}`;
556
- return true;
557
- }
558
- },
559
- {
560
- key: 'copyToClipboard',
561
- question: 'Copy to clipboard automatically? (y/n): ',
562
- default: 'n',
563
- validate: (value) => {
564
- const answer = value.toLowerCase();
565
- if (!['y', 'n', 'yes', 'no'].includes(answer)) return 'Please enter y/n or yes/no';
566
- return true;
567
- },
568
- transform: (value) => ['y', 'yes'].includes(value.toLowerCase())
569
- },
570
- {
571
- key: 'addToTxtIgnore',
572
- question: 'Add ignore patterns to .txtignore file? (y/n): ',
573
- default: 'n',
574
- validate: (value) => {
575
- const answer = value.toLowerCase();
576
- if (!['y', 'n', 'yes', 'no'].includes(answer)) return 'Please enter y/n or yes/no';
577
- return true;
578
- },
579
- transform: (value) => ['y', 'yes'].includes(value.toLowerCase())
580
- },
581
- {
582
- key: 'ignoreFolders',
583
- question: 'Ignore folders (comma-separated, or press Enter to skip): ',
584
- default: '',
585
- ask: () => config.addToTxtIgnore,
586
- transform: (value) => {
587
- if (!value || value.trim() === '') return [];
588
- return value.split(',').map(f => f.trim()).filter(f => f);
589
- }
590
- },
591
- {
592
- key: 'ignoreFiles',
593
- question: 'Ignore files (comma-separated, or press Enter to skip): ',
594
- default: '',
595
- ask: () => config.addToTxtIgnore,
596
- transform: (value) => {
597
- if (!value || value.trim() === '') return [];
598
- return value.split(',').map(f => f.trim()).filter(f => f);
599
- }
600
- }
601
- ];
602
-
603
- function askQuestion() {
604
- if (currentStep >= questions.length) {
605
- // Save configuration
606
- saveConfig();
607
- return;
608
- }
609
-
610
- const q = questions[currentStep];
611
-
612
- // Skip if conditional ask returns false
613
- if (q.ask && !q.ask()) {
614
- currentStep++;
615
- askQuestion();
616
- return;
617
- }
618
-
619
- const defaultValue = typeof q.default === 'function' ? q.default() : q.default;
620
- rl.question(q.question + (defaultValue ? `(${defaultValue}) ` : ''), (answer) => {
621
- const value = answer.trim() || defaultValue;
622
-
623
- // Validate input
624
- if (q.validate) {
625
- const validation = q.validate(value);
626
- if (validation !== true) {
627
- console.log(`❌ ${validation}`);
628
- askQuestion();
629
- return;
630
- }
631
- }
632
-
633
- // Transform value if needed
634
- if (q.transform) {
635
- config[q.key] = q.transform(value);
636
- } else {
637
- config[q.key] = value;
638
- }
639
-
640
- currentStep++;
641
- askQuestion();
642
- });
643
- }
644
-
645
- function saveConfig() {
646
- try {
647
- // Create .txtconfig file with proper formatting
648
- const configPath = path.join(process.cwd(), '.txtconfig');
649
- const configContent = `{
650
- "maxFileSize": "${config.maxFileSize}",
651
- "splitMethod": "${config.splitMethod}",
652
- "splitSize": "${config.splitSize}",
653
- "copyToClipboard": ${config.copyToClipboard}
654
- }`;
655
-
656
- fs.writeFileSync(configPath, configContent);
657
-
658
- // Update .txtignore if user wants to add ignore patterns
659
- if (config.addToTxtIgnore && (config.ignoreFolders.length > 0 || config.ignoreFiles.length > 0)) {
660
- const ignorePath = path.join(process.cwd(), '.txtignore');
661
- let ignoreContent = '';
662
-
663
- // Read existing content if file exists
664
- if (fs.existsSync(ignorePath)) {
665
- ignoreContent = fs.readFileSync(ignorePath, 'utf8');
666
- if (!ignoreContent.endsWith('\n')) ignoreContent += '\n';
667
- }
668
-
669
- // Add new ignore patterns
670
- const newPatterns = [
671
- ...config.ignoreFolders.map(f => f.endsWith('/') ? f : `${f}/`),
672
- ...config.ignoreFiles
673
- ];
674
-
675
- if (newPatterns.length > 0) {
676
- ignoreContent += '\n# Added by make-folder-txt config\n';
677
- ignoreContent += newPatterns.join('\n') + '\n';
678
- fs.writeFileSync(ignorePath, ignoreContent);
679
- }
680
- }
681
-
682
- console.log('\n✅ Configuration saved successfully!');
683
- console.log(`📄 Config file: ${configPath}`);
684
- if (config.addToTxtIgnore && (config.ignoreFolders.length > 0 || config.ignoreFiles.length > 0)) {
685
- console.log(`📝 Ignore patterns added to .txtignore`);
686
- }
687
- console.log('\n💡 Your settings will now be used automatically!');
688
- console.log('🔄 Run --delete-config to reset to defaults');
689
-
690
- } catch (err) {
691
- console.error('❌ Error saving configuration:', err.message);
692
- } finally {
693
- rl.close();
694
- resolve();
695
- }
696
- }
697
-
698
- askQuestion();
699
- });
700
- }
701
-
702
- function loadConfig() {
703
- const fs = require('fs');
704
- const path = require('path');
705
-
706
- try {
707
- const configPath = path.join(process.cwd(), '.txtconfig');
708
- if (!fs.existsSync(configPath)) {
709
- console.error('❌ .txtconfig file not found. Run --make-config first.');
710
- process.exit(1);
711
- }
712
-
713
- const configContent = fs.readFileSync(configPath, 'utf8');
714
- return JSON.parse(configContent);
715
- } catch (err) {
716
- console.error('❌ Error loading configuration:', err.message);
717
- process.exit(1);
718
- }
719
- }
720
-
721
- // ── main ──────────────────────────────────────────────────────────────────────
722
-
723
- async function main() {
724
- const args = process.argv.slice(2);
725
-
726
- if (args.includes("-v") || args.includes("--version")) {
727
- console.log(`v${version}`);
728
- console.log("Built by Muhammad Saad Amin");
729
- process.exit(0);
730
- }
731
-
732
- if (args.includes("--make-config")) {
733
- await createInteractiveConfig();
734
- process.exit(0);
735
- }
736
-
737
- if (args.includes("--delete-config")) {
738
- try {
739
- const fs = require('fs');
740
- const path = require('path');
741
- const configPath = path.join(process.cwd(), '.txtconfig');
742
-
743
- if (fs.existsSync(configPath)) {
744
- fs.unlinkSync(configPath);
745
- console.log('✅ Configuration file deleted successfully!');
746
- console.log('🔄 Tool will now use default settings');
747
- } else {
748
- console.log('ℹ️ No configuration file found - already using defaults');
749
- }
750
- } catch (err) {
751
- console.error('❌ Error deleting configuration:', err.message);
752
- }
753
- process.exit(0);
754
- }
755
-
756
- // Auto-use config if it exists and no other flags are provided
757
- const configPath = path.join(process.cwd(), '.txtconfig');
758
- const hasConfig = fs.existsSync(configPath);
759
- const hasOtherFlags = args.length > 0 && !args.includes('--help') && !args.includes('-h') && !args.includes('--version') && !args.includes('-v');
760
-
761
- if (hasConfig && !hasOtherFlags) {
762
- const config = loadConfig();
763
-
764
- // Apply config to command line arguments
765
- const newArgs = [];
766
-
767
- // Add max file size if not default
768
- if (config.maxFileSize !== '500KB') {
769
- newArgs.push('--skip-large', config.maxFileSize);
770
- }
771
-
772
- // Add split method if not none
773
- if (config.splitMethod !== 'none') {
774
- newArgs.push('--split-method', config.splitMethod);
775
- if (config.splitMethod === 'size') {
776
- newArgs.push('--split-size', config.splitSize);
777
- }
778
- }
779
-
780
- // Add copy to clipboard if true
781
- if (config.copyToClipboard) {
782
- newArgs.push('--copy');
783
- }
784
-
785
- // Replace args with config-based args
786
- args.splice(0, args.length, ...newArgs);
787
- console.log('🔧 Using saved configuration (use --delete-config to reset to defaults)');
788
- }
789
-
790
- if (args.includes("--help") || args.includes("-h")) {
791
- console.log(`
792
- \x1b[33mmake-folder-txt\x1b[0m
793
- Dump an entire project folder into a single readable .txt file.
794
-
795
- \x1b[33mUSAGE\x1b[0m
796
- make-folder-txt [options]
797
-
798
- \x1b[33mOPTIONS\x1b[0m
799
- --ignore-folder, -ifo <names...> Ignore specific folders by name
800
- --ignore-file, -ifi <names...> Ignore specific files by name
801
- --only-folder, -ofo <names...> Include only specific folders
802
- --only-file, -ofi <names...> Include only specific files
803
- --skip-large <size> Skip files larger than specified size (default: 500KB)
804
- --no-skip Include all files regardless of size
805
- --split-method <method> Split output: folder, file, or size
806
- --split-size <size> Split output when size exceeds limit (requires --split-method size)
807
- --copy Copy output to clipboard
808
- --force Include everything (overrides all ignore patterns)
809
- --make-config Create interactive configuration
810
- --delete-config Delete configuration and reset to defaults
811
- --help, -h Show this help message
812
- --version, -v Show version information
813
-
814
- \x1b[33mEXAMPLES\x1b[0m
815
- make-folder-txt
816
- make-folder-txt --copy
817
- make-folder-txt --force
818
- make-folder-txt --skip-large 400KB
819
- make-folder-txt --skip-large 5GB
820
- make-folder-txt --no-skip
821
- make-folder-txt --split-method folder
822
- make-folder-txt --split-method file
823
- make-folder-txt --split-method size --split-size 5MB
824
- make-folder-txt --ignore-folder node_modules dist
825
- make-folder-txt -ifo node_modules dist
826
- make-folder-txt --ignore-file .env .env.local
827
- make-folder-txt -ifi .env .env.local
828
- make-folder-txt --only-folder src docs
829
- make-folder-txt -ofo src docs
830
- make-folder-txt --only-file package.json README.md
831
- make-folder-txt -ofi package.json README.md
832
- make-folder-txt --make-config
833
- make-folder-txt --delete-config
834
-
835
- \x1b[33m.TXTIGNORE FILE\x1b[0m
836
- Create a .txtignore file in your project root to specify files/folders to ignore.
837
- Works like .gitignore - supports file names, path patterns, and comments.
838
-
839
- Example .txtignore:
840
- node_modules/
841
- *.log
842
- .env
843
- coverage/
844
- LICENSE
845
- `);
846
- process.exit(0);
847
- }
848
-
849
- const ignoreDirs = new Set(IGNORE_DIRS);
850
- const ignoreFiles = new Set(IGNORE_FILES);
851
- const onlyFolders = new Set();
852
- const onlyFiles = new Set();
853
- let outputArg = null;
854
- let copyToClipboardFlag = false;
855
- let forceFlag = false;
856
- let maxFileSize = 500 * 1024; // Default 500KB
857
- let noSkipFlag = false;
858
- let splitMethod = null; // 'folder', 'file', 'size'
859
- let splitSize = null; // size in bytes
860
- let rootOnlyInclude = false; // For /include flag
861
-
862
- for (let i = 0; i < args.length; i += 1) {
863
- const arg = args[i];
864
-
865
- if (arg === "--copy") {
866
- copyToClipboardFlag = true;
867
- continue;
868
- }
869
-
870
- if (arg === "--force") {
871
- forceFlag = true;
872
- continue;
873
- }
874
-
875
- if (arg === "--no-skip") {
876
- noSkipFlag = true;
877
- continue;
878
- }
879
-
880
- if (arg === "--skip-large") {
881
- if (i + 1 >= args.length || args[i + 1].startsWith("-")) {
882
- console.error("Error: --skip-large requires a size value (e.g., 400KB, 5GB).");
883
- process.exit(1);
884
- }
885
- maxFileSize = parseFileSize(args[i + 1]);
886
- i += 1;
887
- continue;
888
- }
889
-
890
- if (arg.startsWith("--skip-large=")) {
891
- const value = arg.slice("--skip-large=".length);
892
- if (!value) {
893
- console.error("Error: --skip-large requires a size value (e.g., 400KB, 5GB).");
894
- process.exit(1);
895
- }
896
- maxFileSize = parseFileSize(value);
897
- continue;
898
- }
899
-
900
- if (arg === "--split-method") {
901
- if (i + 1 >= args.length || args[i + 1].startsWith("-")) {
902
- console.error("Error: --split-method requires a method (folder, file, or size).");
903
- process.exit(1);
904
- }
905
- const method = args[i + 1].toLowerCase();
906
- if (!['folder', 'file', 'size'].includes(method)) {
907
- console.error("Error: --split-method must be one of: folder, file, size");
908
- process.exit(1);
909
- }
910
- splitMethod = method;
911
- i += 1;
912
- continue;
913
- }
914
-
915
- if (arg.startsWith("--split-method=")) {
916
- const value = arg.slice("--split-method=".length);
917
- if (!value) {
918
- console.error("Error: --split-method requires a method (folder, file, or size).");
919
- process.exit(1);
920
- }
921
- const method = value.toLowerCase();
922
- if (!['folder', 'file', 'size'].includes(method)) {
923
- console.error("Error: --split-method must be one of: folder, file, size");
924
- process.exit(1);
925
- }
926
- splitMethod = method;
927
- continue;
928
- }
929
-
930
- if (arg === "--split-size") {
931
- if (i + 1 >= args.length || args[i + 1].startsWith("-")) {
932
- console.error("Error: --split-size requires a size value (e.g., 5MB, 10MB).");
933
- process.exit(1);
934
- }
935
- splitSize = parseFileSize(args[i + 1]);
936
- i += 1;
937
- continue;
938
- }
939
-
940
- if (arg.startsWith("--split-size=")) {
941
- const value = arg.slice("--split-size=".length);
942
- if (!value) {
943
- console.error("Error: --split-size requires a size value (e.g., 5MB, 10MB).");
944
- process.exit(1);
945
- }
946
- splitSize = parseFileSize(value);
947
- continue;
948
- }
949
-
950
- if (arg === "--ignore-folder" || arg === "-ifo") {
951
- let consumed = 0;
952
- while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
953
- let folderName = args[i + 1];
954
-
955
- // Check for /folder syntax (root-only include)
956
- if (folderName.startsWith("/")) {
957
- const cleanFolderName = folderName.slice(1); // Remove leading /
958
- onlyFolders.add(cleanFolderName);
959
- rootOnlyInclude = true;
960
- i += 1;
961
- consumed += 1;
962
- } else {
963
- // Normal folder path (could be nested like folder/subfolder)
964
- onlyFolders.add(folderName);
965
- i += 1;
966
- consumed += 1;
967
- }
968
- }
969
- if (consumed === 0) {
970
- console.error("Error: --ignore-folder requires at least one folder name.");
971
- process.exit(1);
972
- }
973
- continue;
974
- }
975
-
976
- if (arg.startsWith("--ignore-folder=") || arg.startsWith("-ifo=")) {
977
- const value = arg.startsWith("--ignore-folder=")
978
- ? arg.slice("--ignore-folder=".length)
979
- : arg.slice("-ifo=".length);
980
- if (!value) {
981
- console.error("Error: --ignore-folder requires a folder name.");
982
- process.exit(1);
983
- }
984
-
985
- // Check for /folder syntax (root-only include)
986
- if (value.startsWith("/")) {
987
- const cleanFolderName = value.slice(1); // Remove leading /
988
- onlyFolders.add(cleanFolderName);
989
- rootOnlyInclude = true;
990
- } else {
991
- // Normal folder path (could be nested like folder/subfolder)
992
- onlyFolders.add(value);
993
- }
994
- continue;
995
- }
996
-
997
- if (arg === "--ignore-file" || arg === "-ifi") {
998
- let consumed = 0;
999
- while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
1000
- ignoreFiles.add(args[i + 1]);
1001
- i += 1;
1002
- consumed += 1;
1003
- }
1004
- if (consumed === 0) {
1005
- console.error("Error: --ignore-file requires at least one file name.");
1006
- process.exit(1);
1007
- }
1008
- continue;
1009
- }
1010
-
1011
- if (arg.startsWith("--ignore-file=") || arg.startsWith("-ifi=")) {
1012
- const value = arg.startsWith("--ignore-file=")
1013
- ? arg.slice("--ignore-file=".length)
1014
- : arg.slice("-ifi=".length);
1015
- if (!value) {
1016
- console.error("Error: --ignore-file requires a file name.");
1017
- process.exit(1);
1018
- }
1019
- ignoreFiles.add(value);
1020
- continue;
1021
- }
1022
-
1023
- if (arg === "--only-folder" || arg === "-ofo") {
1024
- let consumed = 0;
1025
- while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
1026
- // Normalize the folder name
1027
- let folderName = args[i + 1];
1028
- folderName = folderName.replace(/\\/g, '/'); // Convert backslashes to forward slashes
1029
- folderName = folderName.replace(/^\.?\//, ''); // Remove leading ./ or /
1030
- folderName = folderName.replace(/\/+$/, ''); // Remove trailing slashes
1031
- onlyFolders.add(folderName);
1032
- i += 1;
1033
- consumed += 1;
1034
- }
1035
- if (consumed === 0) {
1036
- console.error("Error: --only-folder requires at least one folder name.");
1037
- process.exit(1);
1038
- }
1039
- continue;
1040
- }
1041
-
1042
- if (arg.startsWith("--only-folder=") || arg.startsWith("-ofo=")) {
1043
- const value = arg.startsWith("--only-folder=")
1044
- ? arg.slice("--only-folder=".length)
1045
- : arg.slice("-ofo=".length);
1046
- if (!value) {
1047
- console.error("Error: --only-folder requires a folder name.");
1048
- process.exit(1);
1049
- }
1050
- // Normalize the folder name
1051
- let folderName = value;
1052
- folderName = folderName.replace(/\\/g, '/'); // Convert backslashes to forward slashes
1053
- folderName = folderName.replace(/^\.?\//, ''); // Remove leading ./ or /
1054
- folderName = folderName.replace(/\/+$/, ''); // Remove trailing slashes
1055
- onlyFolders.add(folderName);
1056
- continue;
1057
- }
1058
-
1059
- if (arg === "--only-file" || arg === "-ofi") {
1060
- let consumed = 0;
1061
- while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
1062
- let fileName = args[i + 1];
1063
-
1064
- // Check for /file syntax (root-only include)
1065
- if (fileName.startsWith("/")) {
1066
- const cleanFileName = fileName.slice(1); // Remove leading /
1067
- onlyFiles.add(cleanFileName);
1068
- rootOnlyInclude = true;
1069
- i += 1;
1070
- consumed += 1;
1071
- } else {
1072
- // Normal file path (could be nested like folder/file.ext)
1073
- onlyFiles.add(fileName);
1074
- i += 1;
1075
- consumed += 1;
1076
- }
1077
- }
1078
- if (consumed === 0) {
1079
- console.error("Error: --only-file requires at least one file name.");
1080
- process.exit(1);
1081
- }
1082
- continue;
1083
- }
1084
-
1085
- if (arg.startsWith("--only-file=") || arg.startsWith("-ofi=")) {
1086
- const value = arg.startsWith("--only-file=")
1087
- ? arg.slice("--only-file=".length)
1088
- : arg.slice("-ofi=".length);
1089
- if (!value) {
1090
- console.error("Error: --only-file requires a file name.");
1091
- process.exit(1);
1092
- }
1093
-
1094
- // Check for /file syntax (root-only include)
1095
- if (value.startsWith("/")) {
1096
- const cleanFileName = value.slice(1); // Remove leading /
1097
- onlyFiles.add(cleanFileName);
1098
- rootOnlyInclude = true;
1099
- } else {
1100
- // Normal file path (could be nested like folder/file.ext)
1101
- onlyFiles.add(value);
1102
- }
1103
- continue;
1104
- }
1105
-
1106
- // Unknown argument
1107
- console.error(`Error: Unknown option "${arg}"`);
1108
- console.error("Use --help for available options.");
1109
- process.exit(1);
1110
- }
1111
-
1112
- // Validate split options
1113
- if (splitMethod === 'size' && !splitSize) {
1114
- console.error("Error: --split-method size requires --split-size to be specified");
1115
- process.exit(1);
1116
- }
1117
-
1118
- if (splitSize && splitMethod !== 'size') {
1119
- console.error("Error: --split-size can only be used with --split-method size");
1120
- process.exit(1);
1121
- }
1122
-
1123
- // ── config ────────────────────────────────────────────────────────────────────────
1124
-
1125
- const folderPath = process.cwd();
1126
- const rootName = path.basename(folderPath);
1127
- const txtIgnore = readTxtIgnore(folderPath);
1128
-
1129
- // ── build tree & collect file paths ───────────────────────────────────────────────
1130
-
1131
- const { lines: treeLines, filePaths } = collectFiles(
1132
- folderPath,
1133
- folderPath,
1134
- ignoreDirs,
1135
- ignoreFiles,
1136
- onlyFolders,
1137
- onlyFiles,
1138
- {
1139
- rootName,
1140
- txtIgnore,
1141
- force: forceFlag,
1142
- hasOnlyFilters: onlyFolders.size > 0 || onlyFiles.size > 0,
1143
- rootOnlyInclude
1144
- }
1145
- );
1146
-
1147
- // ── build output filename ───────────────────────────────────────────────────────────
1148
-
1149
- let outputFile = outputArg || path.join(folderPath, `${rootName}.txt`);
1150
-
1151
- // ── handle output splitting ─────────────────────────────────────────────────────────
1152
-
1153
- const effectiveMaxSize = noSkipFlag ? Infinity : maxFileSize;
1154
-
1155
- if (splitMethod) {
1156
- console.log(`🔧 Splitting output by: ${splitMethod}`);
1157
-
1158
- let results;
1159
-
1160
- if (splitMethod === 'folder') {
1161
- results = splitByFolders(treeLines, filePaths, rootName, effectiveMaxSize, forceFlag);
1162
- } else if (splitMethod === 'file') {
1163
- results = splitByFiles(filePaths, rootName, effectiveMaxSize, forceFlag);
1164
- } else if (splitMethod === 'size') {
1165
- results = splitBySize(treeLines, filePaths, rootName, splitSize, effectiveMaxSize, forceFlag);
1166
- }
1167
-
1168
- console.log(`✅ Done! Created ${results.length} split files:`);
1169
- console.log('');
1170
-
1171
- results.forEach((result, index) => {
1172
- if (splitMethod === 'folder') {
1173
- console.log(`📁 Folder: ${result.folder}`);
1174
- } else if (splitMethod === 'file') {
1175
- console.log(`📄 File: ${result.fileName}`);
1176
- } else if (splitMethod === 'size') {
1177
- console.log(`📦 Part ${result.part}`);
1178
- }
1179
- console.log(`📄 Output : ${result.file}`);
1180
- console.log(`📊 Size : ${result.size} KB`);
1181
- console.log(`🗂️ Files : ${result.files}`);
1182
- console.log('');
1183
- });
1184
-
1185
- if (copyToClipboardFlag) {
1186
- console.log('⚠️ --copy flag is not compatible with splitting - clipboard copy skipped');
1187
- }
1188
-
1189
- process.exit(0);
1190
- }
1191
-
1192
- // ── build output (no splitting) ───────────────────────────────────────────────────
1193
- const out = [];
1194
- const divider = "=".repeat(80);
1195
- const subDivider = "-".repeat(80);
1196
-
1197
- out.push(divider);
1198
- out.push(`START OF FOLDER: ${rootName}`);
1199
- out.push(divider);
1200
- out.push("");
1201
-
1202
- out.push(divider);
1203
- out.push("PROJECT STRUCTURE");
1204
- out.push(divider);
1205
- out.push(`Root: ${folderPath}\n`);
1206
-
1207
- out.push(`${rootName}/`);
1208
- treeLines.forEach(l => out.push(l));
1209
- out.push("");
1210
- out.push(`Total files: ${filePaths.length}`);
1211
- out.push("");
1212
-
1213
- out.push(divider);
1214
- out.push("FILE CONTENTS");
1215
- out.push(divider);
1216
-
1217
- filePaths.forEach(({ abs, rel }) => {
1218
- out.push("");
1219
- out.push(subDivider);
1220
- out.push(`FILE: ${rel}`);
1221
- out.push(subDivider);
1222
- out.push(readContent(abs, forceFlag, effectiveMaxSize));
1223
- });
1224
-
1225
- out.push("");
1226
- out.push(divider);
1227
- out.push(`END OF FOLDER: ${rootName}`);
1228
- out.push(divider);
1229
-
1230
- fs.writeFileSync(outputFile, out.join("\n"), "utf8");
1231
-
1232
- const sizeKB = (fs.statSync(outputFile).size / 1024).toFixed(1);
1233
- console.log(`✅ Done!`);
1234
- console.log(`📄 Output : ${outputFile}`);
1235
- console.log(`📊 Size : ${sizeKB} KB`);
1236
- console.log(`🗂️ Files : ${filePaths.length}`);
1237
-
1238
- if (copyToClipboardFlag) {
1239
- const content = fs.readFileSync(outputFile, 'utf8');
1240
- const success = copyToClipboard(content);
1241
- if (success) {
1242
- console.log(`📋 Copied to clipboard!`);
1243
- }
1244
- }
1245
-
1246
- console.log('');
1247
- }
1248
-
1249
- // Run the main function
1250
- main().catch(err => {
1251
- console.error('❌ Error:', err.message);
1252
- process.exit(1);
1253
- });
1254
-
1255
-
1256
- ================================================================================
1257
- END OF FOLDER: make-folder-txt
1258
- ================================================================================