make-folder-txt 2.1.3 → 2.1.4
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.
- package/bin/make-folder-txt.js +24 -4
- package/make-folder-txt.txt +1259 -2
- package/package.json +1 -1
package/bin/make-folder-txt.js
CHANGED
|
@@ -617,7 +617,12 @@ for (let i = 0; i < args.length; i += 1) {
|
|
|
617
617
|
if (arg === "--ignore-folder" || arg === "-ifo") {
|
|
618
618
|
let consumed = 0;
|
|
619
619
|
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
620
|
-
|
|
620
|
+
// Normalize the folder name: remove backslashes, trailing slashes, and leading ./
|
|
621
|
+
let folderName = args[i + 1];
|
|
622
|
+
folderName = folderName.replace(/\\/g, '/'); // Convert backslashes to forward slashes
|
|
623
|
+
folderName = folderName.replace(/^\.?\//, ''); // Remove leading ./ or /
|
|
624
|
+
folderName = folderName.replace(/\/+$/, ''); // Remove trailing slashes
|
|
625
|
+
ignoreDirs.add(folderName);
|
|
621
626
|
i += 1;
|
|
622
627
|
consumed += 1;
|
|
623
628
|
}
|
|
@@ -636,7 +641,12 @@ for (let i = 0; i < args.length; i += 1) {
|
|
|
636
641
|
console.error("Error: --ignore-folder requires a folder name.");
|
|
637
642
|
process.exit(1);
|
|
638
643
|
}
|
|
639
|
-
|
|
644
|
+
// Normalize the folder name
|
|
645
|
+
let folderName = value;
|
|
646
|
+
folderName = folderName.replace(/\\/g, '/'); // Convert backslashes to forward slashes
|
|
647
|
+
folderName = folderName.replace(/^\.?\//, ''); // Remove leading ./ or /
|
|
648
|
+
folderName = folderName.replace(/\/+$/, ''); // Remove trailing slashes
|
|
649
|
+
ignoreDirs.add(folderName);
|
|
640
650
|
continue;
|
|
641
651
|
}
|
|
642
652
|
|
|
@@ -669,7 +679,12 @@ for (let i = 0; i < args.length; i += 1) {
|
|
|
669
679
|
if (arg === "--only-folder" || arg === "-ofo") {
|
|
670
680
|
let consumed = 0;
|
|
671
681
|
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
672
|
-
|
|
682
|
+
// Normalize the folder name
|
|
683
|
+
let folderName = args[i + 1];
|
|
684
|
+
folderName = folderName.replace(/\\/g, '/'); // Convert backslashes to forward slashes
|
|
685
|
+
folderName = folderName.replace(/^\.?\//, ''); // Remove leading ./ or /
|
|
686
|
+
folderName = folderName.replace(/\/+$/, ''); // Remove trailing slashes
|
|
687
|
+
onlyFolders.add(folderName);
|
|
673
688
|
i += 1;
|
|
674
689
|
consumed += 1;
|
|
675
690
|
}
|
|
@@ -688,7 +703,12 @@ for (let i = 0; i < args.length; i += 1) {
|
|
|
688
703
|
console.error("Error: --only-folder requires a folder name.");
|
|
689
704
|
process.exit(1);
|
|
690
705
|
}
|
|
691
|
-
|
|
706
|
+
// Normalize the folder name
|
|
707
|
+
let folderName = value;
|
|
708
|
+
folderName = folderName.replace(/\\/g, '/'); // Convert backslashes to forward slashes
|
|
709
|
+
folderName = folderName.replace(/^\.?\//, ''); // Remove leading ./ or /
|
|
710
|
+
folderName = folderName.replace(/\/+$/, ''); // Remove trailing slashes
|
|
711
|
+
onlyFolders.add(folderName);
|
|
692
712
|
continue;
|
|
693
713
|
}
|
|
694
714
|
|
package/make-folder-txt.txt
CHANGED
|
@@ -8,20 +8,940 @@ PROJECT STRUCTURE
|
|
|
8
8
|
Root: C:\Programming\make-folder-txt
|
|
9
9
|
|
|
10
10
|
make-folder-txt/
|
|
11
|
+
├── .git/ [skipped]
|
|
12
|
+
├── bin/
|
|
13
|
+
│ └── make-folder-txt.js
|
|
14
|
+
├── completion/ [skipped]
|
|
15
|
+
├── LICENSE
|
|
11
16
|
├── package.json
|
|
17
|
+
└── README.md
|
|
12
18
|
|
|
13
|
-
Total files:
|
|
19
|
+
Total files: 4
|
|
14
20
|
|
|
15
21
|
================================================================================
|
|
16
22
|
FILE CONTENTS
|
|
17
23
|
================================================================================
|
|
18
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
|
+
// ── main ──────────────────────────────────────────────────────────────────────
|
|
480
|
+
|
|
481
|
+
const args = process.argv.slice(2);
|
|
482
|
+
|
|
483
|
+
if (args.includes("-v") || args.includes("--version")) {
|
|
484
|
+
console.log(`v${version}`);
|
|
485
|
+
console.log("Built by Muhammad Saad Amin");
|
|
486
|
+
process.exit(0);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
490
|
+
console.log(`
|
|
491
|
+
\x1b[33mmake-folder-txt\x1b[0m
|
|
492
|
+
Dump an entire project folder into a single readable .txt file.
|
|
493
|
+
|
|
494
|
+
\x1b[33mUSAGE\x1b[0m
|
|
495
|
+
make-folder-txt [options]
|
|
496
|
+
|
|
497
|
+
\x1b[33mOPTIONS\x1b[0m
|
|
498
|
+
--ignore-folder, -ifo <names...> Ignore specific folders by name
|
|
499
|
+
--ignore-file, -ifi <names...> Ignore specific files by name
|
|
500
|
+
--only-folder, -ofo <names...> Include only specific folders
|
|
501
|
+
--only-file, -ofi <names...> Include only specific files
|
|
502
|
+
--skip-large <size> Skip files larger than specified size (default: 500KB)
|
|
503
|
+
--no-skip Include all files regardless of size
|
|
504
|
+
--split-method <method> Split output: folder, file, or size
|
|
505
|
+
--split-size <size> Split output when size exceeds limit (requires --split-method size)
|
|
506
|
+
--copy Copy output to clipboard
|
|
507
|
+
--force Include everything (overrides all ignore patterns)
|
|
508
|
+
--help, -h Show this help message
|
|
509
|
+
--version, -v Show version information
|
|
510
|
+
|
|
511
|
+
\x1b[33mEXAMPLES\x1b[0m
|
|
512
|
+
make-folder-txt
|
|
513
|
+
make-folder-txt --copy
|
|
514
|
+
make-folder-txt --force
|
|
515
|
+
make-folder-txt --skip-large 400KB
|
|
516
|
+
make-folder-txt --skip-large 5GB
|
|
517
|
+
make-folder-txt --no-skip
|
|
518
|
+
make-folder-txt --split-method folder
|
|
519
|
+
make-folder-txt --split-method file
|
|
520
|
+
make-folder-txt --split-method size --split-size 5MB
|
|
521
|
+
make-folder-txt --ignore-folder node_modules dist
|
|
522
|
+
make-folder-txt -ifo node_modules dist
|
|
523
|
+
make-folder-txt --ignore-file .env .env.local
|
|
524
|
+
make-folder-txt -ifi .env .env.local
|
|
525
|
+
make-folder-txt --only-folder src docs
|
|
526
|
+
make-folder-txt -ofo src docs
|
|
527
|
+
make-folder-txt --only-file package.json README.md
|
|
528
|
+
make-folder-txt -ofi package.json README.md
|
|
529
|
+
|
|
530
|
+
\x1b[33m.TXTIGNORE FILE\x1b[0m
|
|
531
|
+
Create a .txtignore file in your project root to specify files/folders to ignore.
|
|
532
|
+
Works like .gitignore - supports file names, path patterns, and comments.
|
|
533
|
+
|
|
534
|
+
Example .txtignore:
|
|
535
|
+
node_modules/
|
|
536
|
+
*.log
|
|
537
|
+
.env
|
|
538
|
+
coverage/
|
|
539
|
+
LICENSE
|
|
540
|
+
`);
|
|
541
|
+
process.exit(0);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const ignoreDirs = new Set(IGNORE_DIRS);
|
|
545
|
+
const ignoreFiles = new Set(IGNORE_FILES);
|
|
546
|
+
const onlyFolders = new Set();
|
|
547
|
+
const onlyFiles = new Set();
|
|
548
|
+
let outputArg = null;
|
|
549
|
+
let copyToClipboardFlag = false;
|
|
550
|
+
let forceFlag = false;
|
|
551
|
+
let maxFileSize = 500 * 1024; // Default 500KB
|
|
552
|
+
let noSkipFlag = false;
|
|
553
|
+
let splitMethod = null; // 'folder', 'file', 'size'
|
|
554
|
+
let splitSize = null; // size in bytes
|
|
555
|
+
|
|
556
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
557
|
+
const arg = args[i];
|
|
558
|
+
|
|
559
|
+
if (arg === "--copy") {
|
|
560
|
+
copyToClipboardFlag = true;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (arg === "--force") {
|
|
565
|
+
forceFlag = true;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (arg === "--no-skip") {
|
|
570
|
+
noSkipFlag = true;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (arg === "--skip-large") {
|
|
575
|
+
if (i + 1 >= args.length || args[i + 1].startsWith("-")) {
|
|
576
|
+
console.error("Error: --skip-large requires a size value (e.g., 400KB, 5GB).");
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
maxFileSize = parseFileSize(args[i + 1]);
|
|
580
|
+
i += 1;
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (arg.startsWith("--skip-large=")) {
|
|
585
|
+
const value = arg.slice("--skip-large=".length);
|
|
586
|
+
if (!value) {
|
|
587
|
+
console.error("Error: --skip-large requires a size value (e.g., 400KB, 5GB).");
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
maxFileSize = parseFileSize(value);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (arg === "--split-method") {
|
|
595
|
+
if (i + 1 >= args.length || args[i + 1].startsWith("-")) {
|
|
596
|
+
console.error("Error: --split-method requires a method (folder, file, or size).");
|
|
597
|
+
process.exit(1);
|
|
598
|
+
}
|
|
599
|
+
const method = args[i + 1].toLowerCase();
|
|
600
|
+
if (!['folder', 'file', 'size'].includes(method)) {
|
|
601
|
+
console.error("Error: --split-method must be one of: folder, file, size");
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
splitMethod = method;
|
|
605
|
+
i += 1;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (arg.startsWith("--split-method=")) {
|
|
610
|
+
const value = arg.slice("--split-method=".length);
|
|
611
|
+
if (!value) {
|
|
612
|
+
console.error("Error: --split-method requires a method (folder, file, or size).");
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
const method = value.toLowerCase();
|
|
616
|
+
if (!['folder', 'file', 'size'].includes(method)) {
|
|
617
|
+
console.error("Error: --split-method must be one of: folder, file, size");
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
splitMethod = method;
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (arg === "--split-size") {
|
|
625
|
+
if (i + 1 >= args.length || args[i + 1].startsWith("-")) {
|
|
626
|
+
console.error("Error: --split-size requires a size value (e.g., 5MB, 10MB).");
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
splitSize = parseFileSize(args[i + 1]);
|
|
630
|
+
i += 1;
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (arg.startsWith("--split-size=")) {
|
|
635
|
+
const value = arg.slice("--split-size=".length);
|
|
636
|
+
if (!value) {
|
|
637
|
+
console.error("Error: --split-size requires a size value (e.g., 5MB, 10MB).");
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
splitSize = parseFileSize(value);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (arg === "--ignore-folder" || arg === "-ifo") {
|
|
645
|
+
let consumed = 0;
|
|
646
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
647
|
+
// Normalize the folder name: remove backslashes, trailing slashes, and leading ./
|
|
648
|
+
let folderName = args[i + 1];
|
|
649
|
+
folderName = folderName.replace(/\\/g, '/'); // Convert backslashes to forward slashes
|
|
650
|
+
folderName = folderName.replace(/^\.?\//, ''); // Remove leading ./ or /
|
|
651
|
+
folderName = folderName.replace(/\/+$/, ''); // Remove trailing slashes
|
|
652
|
+
ignoreDirs.add(folderName);
|
|
653
|
+
i += 1;
|
|
654
|
+
consumed += 1;
|
|
655
|
+
}
|
|
656
|
+
if (consumed === 0) {
|
|
657
|
+
console.error("Error: --ignore-folder requires at least one folder name.");
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (arg.startsWith("--ignore-folder=") || arg.startsWith("-ifo=")) {
|
|
664
|
+
const value = arg.startsWith("--ignore-folder=")
|
|
665
|
+
? arg.slice("--ignore-folder=".length)
|
|
666
|
+
: arg.slice("-ifo=".length);
|
|
667
|
+
if (!value) {
|
|
668
|
+
console.error("Error: --ignore-folder requires a folder name.");
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
// Normalize the folder name
|
|
672
|
+
let folderName = value;
|
|
673
|
+
folderName = folderName.replace(/\\/g, '/'); // Convert backslashes to forward slashes
|
|
674
|
+
folderName = folderName.replace(/^\.?\//, ''); // Remove leading ./ or /
|
|
675
|
+
folderName = folderName.replace(/\/+$/, ''); // Remove trailing slashes
|
|
676
|
+
ignoreDirs.add(folderName);
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (arg === "--ignore-file" || arg === "-ifi") {
|
|
681
|
+
let consumed = 0;
|
|
682
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
683
|
+
ignoreFiles.add(args[i + 1]);
|
|
684
|
+
i += 1;
|
|
685
|
+
consumed += 1;
|
|
686
|
+
}
|
|
687
|
+
if (consumed === 0) {
|
|
688
|
+
console.error("Error: --ignore-file requires at least one file name.");
|
|
689
|
+
process.exit(1);
|
|
690
|
+
}
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (arg.startsWith("--ignore-file=") || arg.startsWith("-ifi=")) {
|
|
695
|
+
const value = arg.startsWith("--ignore-file=")
|
|
696
|
+
? arg.slice("--ignore-file=".length)
|
|
697
|
+
: arg.slice("-ifi=".length);
|
|
698
|
+
if (!value) {
|
|
699
|
+
console.error("Error: --ignore-file requires a file name.");
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
ignoreFiles.add(value);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (arg === "--only-folder" || arg === "-ofo") {
|
|
707
|
+
let consumed = 0;
|
|
708
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
709
|
+
// Normalize the folder name
|
|
710
|
+
let folderName = args[i + 1];
|
|
711
|
+
folderName = folderName.replace(/\\/g, '/'); // Convert backslashes to forward slashes
|
|
712
|
+
folderName = folderName.replace(/^\.?\//, ''); // Remove leading ./ or /
|
|
713
|
+
folderName = folderName.replace(/\/+$/, ''); // Remove trailing slashes
|
|
714
|
+
onlyFolders.add(folderName);
|
|
715
|
+
i += 1;
|
|
716
|
+
consumed += 1;
|
|
717
|
+
}
|
|
718
|
+
if (consumed === 0) {
|
|
719
|
+
console.error("Error: --only-folder requires at least one folder name.");
|
|
720
|
+
process.exit(1);
|
|
721
|
+
}
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (arg.startsWith("--only-folder=") || arg.startsWith("-ofo=")) {
|
|
726
|
+
const value = arg.startsWith("--only-folder=")
|
|
727
|
+
? arg.slice("--only-folder=".length)
|
|
728
|
+
: arg.slice("-ofo=".length);
|
|
729
|
+
if (!value) {
|
|
730
|
+
console.error("Error: --only-folder requires a folder name.");
|
|
731
|
+
process.exit(1);
|
|
732
|
+
}
|
|
733
|
+
// Normalize the folder name
|
|
734
|
+
let folderName = value;
|
|
735
|
+
folderName = folderName.replace(/\\/g, '/'); // Convert backslashes to forward slashes
|
|
736
|
+
folderName = folderName.replace(/^\.?\//, ''); // Remove leading ./ or /
|
|
737
|
+
folderName = folderName.replace(/\/+$/, ''); // Remove trailing slashes
|
|
738
|
+
onlyFolders.add(folderName);
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (arg === "--only-file" || arg === "-ofi") {
|
|
743
|
+
let consumed = 0;
|
|
744
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
745
|
+
onlyFiles.add(args[i + 1]);
|
|
746
|
+
i += 1;
|
|
747
|
+
consumed += 1;
|
|
748
|
+
}
|
|
749
|
+
if (consumed === 0) {
|
|
750
|
+
console.error("Error: --only-file requires at least one file name.");
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (arg.startsWith("--only-file=") || arg.startsWith("-ofi=")) {
|
|
757
|
+
const value = arg.startsWith("--only-file=")
|
|
758
|
+
? arg.slice("--only-file=".length)
|
|
759
|
+
: arg.slice("-ofi=".length);
|
|
760
|
+
if (!value) {
|
|
761
|
+
console.error("Error: --only-file requires a file name.");
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
764
|
+
onlyFiles.add(value);
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (arg.startsWith("-")) {
|
|
769
|
+
console.error(`Error: Unknown option "${arg}".`);
|
|
770
|
+
process.exit(1);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (!outputArg) {
|
|
774
|
+
outputArg = arg;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
console.error(`Error: Unexpected argument "${arg}".`);
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Validate split options
|
|
783
|
+
if (splitMethod === 'size' && !splitSize) {
|
|
784
|
+
console.error("Error: --split-method size requires --split-size to be specified.");
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (splitSize && splitMethod !== 'size') {
|
|
789
|
+
console.error("Error: --split-size can only be used with --split-method size.");
|
|
790
|
+
process.exit(1);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const folderPath = process.cwd();
|
|
794
|
+
const rootName = path.basename(folderPath);
|
|
795
|
+
|
|
796
|
+
// Read .txtignore file if it exists
|
|
797
|
+
const txtIgnore = readTxtIgnore(folderPath);
|
|
798
|
+
|
|
799
|
+
const outputFile = outputArg
|
|
800
|
+
? path.resolve(outputArg)
|
|
801
|
+
: path.join(process.cwd(), `${rootName}.txt`);
|
|
802
|
+
|
|
803
|
+
console.log(`\n📂 Scanning: ${folderPath}`);
|
|
804
|
+
|
|
805
|
+
const hasOnlyFilters = onlyFolders.size > 0 || onlyFiles.size > 0;
|
|
806
|
+
const { lines: treeLines, filePaths } = collectFiles(
|
|
807
|
+
folderPath,
|
|
808
|
+
folderPath,
|
|
809
|
+
ignoreDirs,
|
|
810
|
+
ignoreFiles,
|
|
811
|
+
onlyFolders,
|
|
812
|
+
onlyFiles,
|
|
813
|
+
{ hasOnlyFilters, rootName, txtIgnore, force: forceFlag },
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
// ── handle splitting ──────────────────────────────────────────────────────────────
|
|
817
|
+
const effectiveMaxSize = noSkipFlag ? Infinity : maxFileSize;
|
|
818
|
+
|
|
819
|
+
if (splitMethod) {
|
|
820
|
+
console.log(`🔧 Splitting output by: ${splitMethod}`);
|
|
821
|
+
|
|
822
|
+
let results;
|
|
823
|
+
|
|
824
|
+
if (splitMethod === 'folder') {
|
|
825
|
+
results = splitByFolders(treeLines, filePaths, rootName, effectiveMaxSize, forceFlag);
|
|
826
|
+
} else if (splitMethod === 'file') {
|
|
827
|
+
results = splitByFiles(filePaths, rootName, effectiveMaxSize, forceFlag);
|
|
828
|
+
} else if (splitMethod === 'size') {
|
|
829
|
+
results = splitBySize(treeLines, filePaths, rootName, splitSize, effectiveMaxSize, forceFlag);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
console.log(`✅ Done! Created ${results.length} split files:`);
|
|
833
|
+
console.log('');
|
|
834
|
+
|
|
835
|
+
results.forEach((result, index) => {
|
|
836
|
+
if (splitMethod === 'folder') {
|
|
837
|
+
console.log(`📁 Folder: ${result.folder}`);
|
|
838
|
+
} else if (splitMethod === 'file') {
|
|
839
|
+
console.log(`📄 File: ${result.fileName}`);
|
|
840
|
+
} else if (splitMethod === 'size') {
|
|
841
|
+
console.log(`📦 Part ${result.part}`);
|
|
842
|
+
}
|
|
843
|
+
console.log(`📄 Output : ${result.file}`);
|
|
844
|
+
console.log(`📊 Size : ${result.size} KB`);
|
|
845
|
+
console.log(`🗂️ Files : ${result.files}`);
|
|
846
|
+
console.log('');
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
if (copyToClipboardFlag) {
|
|
850
|
+
console.log('⚠️ --copy flag is not compatible with splitting - clipboard copy skipped');
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
process.exit(0);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ── build output (no splitting) ───────────────────────────────────────────────────
|
|
857
|
+
const out = [];
|
|
858
|
+
const divider = "=".repeat(80);
|
|
859
|
+
const subDivider = "-".repeat(80);
|
|
860
|
+
|
|
861
|
+
out.push(divider);
|
|
862
|
+
out.push(`START OF FOLDER: ${rootName}`);
|
|
863
|
+
out.push(divider);
|
|
864
|
+
out.push("");
|
|
865
|
+
|
|
866
|
+
out.push(divider);
|
|
867
|
+
out.push("PROJECT STRUCTURE");
|
|
868
|
+
out.push(divider);
|
|
869
|
+
out.push(`Root: ${folderPath}\n`);
|
|
870
|
+
out.push(`${rootName}/`);
|
|
871
|
+
treeLines.forEach(l => out.push(l));
|
|
872
|
+
out.push("");
|
|
873
|
+
out.push(`Total files: ${filePaths.length}`);
|
|
874
|
+
out.push("");
|
|
875
|
+
|
|
876
|
+
out.push(divider);
|
|
877
|
+
out.push("FILE CONTENTS");
|
|
878
|
+
out.push(divider);
|
|
879
|
+
|
|
880
|
+
filePaths.forEach(({ abs, rel }) => {
|
|
881
|
+
out.push("");
|
|
882
|
+
out.push(subDivider);
|
|
883
|
+
out.push(`FILE: ${rel}`);
|
|
884
|
+
out.push(subDivider);
|
|
885
|
+
const effectiveMaxSize = noSkipFlag ? Infinity : maxFileSize;
|
|
886
|
+
out.push(readContent(abs, forceFlag, effectiveMaxSize));
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
out.push("");
|
|
890
|
+
out.push(divider);
|
|
891
|
+
out.push(`END OF FOLDER: ${rootName}`);
|
|
892
|
+
out.push(divider);
|
|
893
|
+
|
|
894
|
+
fs.writeFileSync(outputFile, out.join("\n"), "utf8");
|
|
895
|
+
|
|
896
|
+
const sizeKB = (fs.statSync(outputFile).size / 1024).toFixed(1);
|
|
897
|
+
console.log(`✅ Done!`);
|
|
898
|
+
console.log(`📄 Output : ${outputFile}`);
|
|
899
|
+
console.log(`📊 Size : ${sizeKB} KB`);
|
|
900
|
+
console.log(`🗂️ Files : ${filePaths.length}`);
|
|
901
|
+
|
|
902
|
+
if (copyToClipboardFlag) {
|
|
903
|
+
const content = fs.readFileSync(outputFile, 'utf8');
|
|
904
|
+
const success = copyToClipboard(content);
|
|
905
|
+
if (success) {
|
|
906
|
+
console.log(`📋 Copied to clipboard!`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
console.log('');
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
--------------------------------------------------------------------------------
|
|
914
|
+
FILE: /LICENSE
|
|
915
|
+
--------------------------------------------------------------------------------
|
|
916
|
+
MIT License
|
|
917
|
+
|
|
918
|
+
Copyright (c) 2026 Muhammad Saad Amin @SENODROOM
|
|
919
|
+
|
|
920
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
921
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
922
|
+
in the Software without restriction, including without limitation the rights
|
|
923
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
924
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
925
|
+
furnished to do so, subject to the following conditions:
|
|
926
|
+
|
|
927
|
+
The above copyright notice and this permission notice shall be included in all
|
|
928
|
+
copies or substantial portions of the Software.
|
|
929
|
+
|
|
930
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
931
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
932
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
933
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
934
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
935
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
936
|
+
SOFTWARE.
|
|
937
|
+
|
|
938
|
+
|
|
19
939
|
--------------------------------------------------------------------------------
|
|
20
940
|
FILE: /package.json
|
|
21
941
|
--------------------------------------------------------------------------------
|
|
22
942
|
{
|
|
23
943
|
"name": "make-folder-txt",
|
|
24
|
-
"version": "2.1.
|
|
944
|
+
"version": "2.1.3",
|
|
25
945
|
"description": "Generate a single .txt file containing the full folder structure and file contents of any project, ignoring node_modules and other junk.",
|
|
26
946
|
"main": "bin/make-folder-txt.js",
|
|
27
947
|
"bin": {
|
|
@@ -47,6 +967,343 @@ FILE: /package.json
|
|
|
47
967
|
}
|
|
48
968
|
|
|
49
969
|
|
|
970
|
+
--------------------------------------------------------------------------------
|
|
971
|
+
FILE: /README.md
|
|
972
|
+
--------------------------------------------------------------------------------
|
|
973
|
+
<div align="center">
|
|
974
|
+
|
|
975
|
+
# 📁 make-folder-txt
|
|
976
|
+
|
|
977
|
+
**Instantly dump your entire project into a single, readable `.txt` file.**
|
|
978
|
+
|
|
979
|
+
[](https://www.npmjs.com/package/make-folder-txt)
|
|
980
|
+
[](https://www.npmjs.com/package/make-folder-txt)
|
|
981
|
+
[](./LICENSE)
|
|
982
|
+
[](https://nodejs.org)
|
|
983
|
+
|
|
984
|
+
Perfect for sharing your codebase with **AI tools**, **teammates**, or **code reviewers** — without zipping files or giving repo access.
|
|
985
|
+
|
|
986
|
+
[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)
|
|
987
|
+
|
|
988
|
+
</div>
|
|
989
|
+
|
|
990
|
+
---
|
|
991
|
+
|
|
992
|
+
## ✨ Why make-folder-txt?
|
|
993
|
+
|
|
994
|
+
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.
|
|
995
|
+
|
|
996
|
+
- ✅ Run it from any project directory — no arguments needed
|
|
997
|
+
- ✅ Built-in help system with `--help` flag
|
|
998
|
+
- ✅ File size control with `--skip-large` and `--no-skip`
|
|
999
|
+
- ✅ Output splitting by folders, files, or size
|
|
1000
|
+
- ✅ Copy to clipboard with `--copy` flag
|
|
1001
|
+
- ✅ Force include everything with `--force` flag
|
|
1002
|
+
- ✅ Generates a clean folder tree + every file's content
|
|
1003
|
+
- ✅ `.txtignore` support (works like `.gitignore`)
|
|
1004
|
+
- ✅ Automatically skips `node_modules`, binaries, and junk files
|
|
1005
|
+
- ✅ Zero dependencies — pure Node.js
|
|
1006
|
+
- ✅ Works on Windows, macOS, and Linux
|
|
1007
|
+
|
|
1008
|
+
---
|
|
1009
|
+
|
|
1010
|
+
## 📦 Installation
|
|
1011
|
+
|
|
1012
|
+
Install globally once, use anywhere:
|
|
1013
|
+
|
|
1014
|
+
```bash
|
|
1015
|
+
npm install -g make-folder-txt
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
---
|
|
1019
|
+
|
|
1020
|
+
## 🚀 Usage
|
|
1021
|
+
|
|
1022
|
+
Navigate into your project folder and run:
|
|
1023
|
+
|
|
1024
|
+
```bash
|
|
1025
|
+
cd my-project
|
|
1026
|
+
make-folder-txt
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
That's it. A `my-project.txt` file will be created in the same directory.
|
|
1030
|
+
|
|
1031
|
+
### 📖 Get Help
|
|
1032
|
+
|
|
1033
|
+
```bash
|
|
1034
|
+
make-folder-txt --help # Show all options and examples
|
|
1035
|
+
make-folder-txt -h # Short version of help
|
|
1036
|
+
make-folder-txt --version # Show version info
|
|
1037
|
+
make-folder-txt -v # Short version of version
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
### 📋 Copy to Clipboard
|
|
1041
|
+
|
|
1042
|
+
```bash
|
|
1043
|
+
make-folder-txt --copy # Generate output and copy to clipboard
|
|
1044
|
+
make-folder-txt --copy --ignore-folder node_modules # Copy filtered output
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
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).
|
|
1048
|
+
|
|
1049
|
+
### 🔥 Force Include Everything
|
|
1050
|
+
|
|
1051
|
+
```bash
|
|
1052
|
+
make-folder-txt --force # Include everything (overrides all ignore patterns)
|
|
1053
|
+
make-folder-txt --force --copy # Include everything and copy to clipboard
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
The `--force` flag overrides all ignore patterns and includes:
|
|
1057
|
+
- `node_modules` and other ignored folders
|
|
1058
|
+
- Binary files (images, executables, etc.)
|
|
1059
|
+
- Large files (no 500 KB limit)
|
|
1060
|
+
- Files in `.txtignore`
|
|
1061
|
+
- System files and other normally skipped content
|
|
1062
|
+
|
|
1063
|
+
Use this when you need a complete, unfiltered dump of your entire project.
|
|
1064
|
+
|
|
1065
|
+
### 📏 File Size Control
|
|
1066
|
+
|
|
1067
|
+
```bash
|
|
1068
|
+
make-folder-txt --skip-large 400KB # Skip files larger than 400KB
|
|
1069
|
+
make-folder-txt --skip-large 5GB # Skip files larger than 5GB
|
|
1070
|
+
make-folder-txt --skip-large 1.5MB # Skip files larger than 1.5MB
|
|
1071
|
+
make-folder-txt --no-skip # Include all files regardless of size
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
**Default behavior**: Files larger than 500KB are skipped by default.
|
|
1075
|
+
|
|
1076
|
+
**Supported size units**:
|
|
1077
|
+
- **B** - Bytes
|
|
1078
|
+
- **KB** - Kilobytes (1024 bytes)
|
|
1079
|
+
- **MB** - Megabytes (1024 KB)
|
|
1080
|
+
- **GB** - Gigabytes (1024 MB)
|
|
1081
|
+
- **TB** - Terabytes (1024 GB)
|
|
1082
|
+
|
|
1083
|
+
**Examples:**
|
|
1084
|
+
```bash
|
|
1085
|
+
# More restrictive - skip anything over 100KB
|
|
1086
|
+
make-folder-txt --skip-large 100KB
|
|
1087
|
+
|
|
1088
|
+
# More permissive - allow files up to 10MB
|
|
1089
|
+
make-folder-txt --skip-large 10MB
|
|
1090
|
+
|
|
1091
|
+
# Include everything - no size limits
|
|
1092
|
+
make-folder-txt --no-skip
|
|
1093
|
+
|
|
1094
|
+
# Combine with other options
|
|
1095
|
+
make-folder-txt --skip-large 2MB --ignore-folder node_modules
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1098
|
+
**Size format**: Accepts decimal numbers (e.g., `1.5MB`, `0.5GB`) and various units.
|
|
1099
|
+
|
|
1100
|
+
### 📂 Output Splitting
|
|
1101
|
+
|
|
1102
|
+
```bash
|
|
1103
|
+
make-folder-txt --split-method folder # Split by folders
|
|
1104
|
+
make-folder-txt --split-method file # Split by files
|
|
1105
|
+
make-folder-txt --split-method size --split-size 5MB # Split by file size
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
**Split Methods:**
|
|
1109
|
+
- **`folder`** - Creates separate files for each folder
|
|
1110
|
+
- **`file`** - Creates separate files for each individual file
|
|
1111
|
+
- **`size`** - Splits output when content exceeds specified size
|
|
1112
|
+
|
|
1113
|
+
**Examples:**
|
|
1114
|
+
```bash
|
|
1115
|
+
# Split by folders - creates folder-name.txt for each folder
|
|
1116
|
+
make-folder-txt --split-method folder
|
|
1117
|
+
|
|
1118
|
+
# Split by files - creates filename.txt for each file
|
|
1119
|
+
make-folder-txt --split-method file
|
|
1120
|
+
|
|
1121
|
+
# Split by size - creates part-1.txt, part-2.txt, etc.
|
|
1122
|
+
make-folder-txt --split-method size --split-size 5MB
|
|
1123
|
+
|
|
1124
|
+
# Combine with other options
|
|
1125
|
+
make-folder-txt --split-method size --split-size 2MB --ignore-folder node_modules
|
|
1126
|
+
```
|
|
1127
|
+
|
|
1128
|
+
**Output Files:**
|
|
1129
|
+
- **Folder method**: `projectname-foldername.txt`
|
|
1130
|
+
- **File method**: `projectname-filename.txt`
|
|
1131
|
+
- **Size method**: `projectname-part-1.txt`, `projectname-part-2.txt`, etc.
|
|
1132
|
+
|
|
1133
|
+
**Note**: Splitting is not compatible with `--copy` flag.
|
|
1134
|
+
|
|
1135
|
+
Ignore specific folders/files by name:
|
|
1136
|
+
|
|
1137
|
+
```bash
|
|
1138
|
+
make-folder-txt --ignore-folder examples extensions docs
|
|
1139
|
+
make-folder-txt -ifo examples extensions docs # shorthand
|
|
1140
|
+
make-folder-txt --ignore-folder examples extensions "docs and explaination"
|
|
1141
|
+
make-folder-txt --ignore-folder examples extensions docs --ignore-file LICENSE
|
|
1142
|
+
make-folder-txt --ignore-file .env .env.local secrets.txt
|
|
1143
|
+
make-folder-txt -ifi .env .env.local secrets.txt # shorthand
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
Use a `.txtignore` file (works like `.gitignore`):
|
|
1147
|
+
|
|
1148
|
+
```bash
|
|
1149
|
+
# Create a .txtignore file in your project root
|
|
1150
|
+
echo "node_modules/" > .txtignore
|
|
1151
|
+
echo "*.log" >> .txtignore
|
|
1152
|
+
echo ".env" >> .txtignore
|
|
1153
|
+
echo "coverage/" >> .txtignore
|
|
1154
|
+
|
|
1155
|
+
# The tool will automatically read and respect .txtignore patterns
|
|
1156
|
+
make-folder-txt
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
The `.txtignore` file supports:
|
|
1160
|
+
- File and folder names (one per line)
|
|
1161
|
+
- Wildcard patterns (`*.log`, `temp-*`)
|
|
1162
|
+
- Comments (lines starting with `#`)
|
|
1163
|
+
- Folder patterns with trailing slash (`docs/`)
|
|
1164
|
+
|
|
1165
|
+
Include only specific folders/files by name (everything else is ignored):
|
|
1166
|
+
|
|
1167
|
+
```bash
|
|
1168
|
+
make-folder-txt --only-folder src docs
|
|
1169
|
+
make-folder-txt -ofo src docs # shorthand
|
|
1170
|
+
make-folder-txt --only-file package.json README.md
|
|
1171
|
+
make-folder-txt -ofi package.json README.md # shorthand
|
|
1172
|
+
make-folder-txt --only-folder src --only-file package.json
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
---
|
|
1176
|
+
|
|
1177
|
+
## 🎯 Real World Examples
|
|
1178
|
+
|
|
1179
|
+
**Sharing with an AI tool (ChatGPT, Claude, etc.):**
|
|
1180
|
+
|
|
1181
|
+
```bash
|
|
1182
|
+
cd "C:\Web Development\my-app\backend"
|
|
1183
|
+
make-folder-txt
|
|
1184
|
+
# → backend.txt created, ready to paste into any AI chat
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
**On macOS / Linux:**
|
|
1188
|
+
|
|
1189
|
+
```bash
|
|
1190
|
+
cd /home/user/projects/my-app
|
|
1191
|
+
make-folder-txt
|
|
1192
|
+
# → my-app.txt created
|
|
1193
|
+
```
|
|
1194
|
+
|
|
1195
|
+
---
|
|
1196
|
+
|
|
1197
|
+
## 📄 Output Format
|
|
1198
|
+
|
|
1199
|
+
The generated `.txt` file is structured in two clear sections:
|
|
1200
|
+
|
|
1201
|
+
```
|
|
1202
|
+
================================================================================
|
|
1203
|
+
START OF FOLDER: my-project
|
|
1204
|
+
================================================================================
|
|
1205
|
+
|
|
1206
|
+
================================================================================
|
|
1207
|
+
PROJECT STRUCTURE
|
|
1208
|
+
================================================================================
|
|
1209
|
+
Root: C:\Web Development\my-project
|
|
1210
|
+
|
|
1211
|
+
my-project/
|
|
1212
|
+
├── src/
|
|
1213
|
+
│ ├── controllers/
|
|
1214
|
+
│ │ └── userController.js
|
|
1215
|
+
│ ├── models/
|
|
1216
|
+
│ │ └── User.js
|
|
1217
|
+
│ └── index.js
|
|
1218
|
+
├── node_modules/ [skipped]
|
|
1219
|
+
├── package.json
|
|
1220
|
+
└── README.md
|
|
1221
|
+
|
|
1222
|
+
Total files: 5
|
|
1223
|
+
|
|
1224
|
+
================================================================================
|
|
1225
|
+
FILE CONTENTS
|
|
1226
|
+
================================================================================
|
|
1227
|
+
|
|
1228
|
+
--------------------------------------------------------------------------------
|
|
1229
|
+
FILE: /src/index.js
|
|
1230
|
+
--------------------------------------------------------------------------------
|
|
1231
|
+
const express = require('express');
|
|
1232
|
+
...
|
|
1233
|
+
|
|
1234
|
+
--------------------------------------------------------------------------------
|
|
1235
|
+
FILE: /package.json
|
|
1236
|
+
--------------------------------------------------------------------------------
|
|
1237
|
+
{
|
|
1238
|
+
"name": "my-project",
|
|
1239
|
+
...
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
================================================================================
|
|
1243
|
+
END OF FOLDER: my-project
|
|
1244
|
+
================================================================================
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
---
|
|
1248
|
+
|
|
1249
|
+
## 🚫 What Gets Skipped
|
|
1250
|
+
|
|
1251
|
+
The tool is smart about what it ignores so your output stays clean and readable.
|
|
1252
|
+
|
|
1253
|
+
| Category | Details |
|
|
1254
|
+
| --------------- | -------------------------------------------------------------- |
|
|
1255
|
+
| 📁 Folders | `node_modules`, `.git`, `.next`, `dist`, `build`, `.cache` |
|
|
1256
|
+
| 🖼️ Binary files | Images (`.png`, `.jpg`, `.gif`...), fonts, videos, executables |
|
|
1257
|
+
| 📦 Archives | `.zip`, `.tar`, `.gz`, `.rar`, `.7z` |
|
|
1258
|
+
| 🔤 Font files | `.woff`, `.woff2`, `.ttf`, `.eot`, `.otf` |
|
|
1259
|
+
| 📋 Lock files | `package-lock.json`, `yarn.lock` |
|
|
1260
|
+
| 📏 Large files | Any file over **500 KB** |
|
|
1261
|
+
| 🗑️ System files | `.DS_Store`, `Thumbs.db`, `desktop.ini` |
|
|
1262
|
+
| 📄 Output file | The generated `foldername.txt` file (to avoid infinite loops) |
|
|
1263
|
+
| 📝 .txtignore | Any files/folders specified in `.txtignore` file |
|
|
1264
|
+
|
|
1265
|
+
Binary and skipped files are noted in the output as `[binary / skipped]` so you always know what was omitted.
|
|
1266
|
+
|
|
1267
|
+
---
|
|
1268
|
+
|
|
1269
|
+
## 🛠️ Requirements
|
|
1270
|
+
|
|
1271
|
+
- **Node.js** v14.0.0 or higher
|
|
1272
|
+
- No other dependencies
|
|
1273
|
+
|
|
1274
|
+
---
|
|
1275
|
+
|
|
1276
|
+
## 🤝 Contributing
|
|
1277
|
+
|
|
1278
|
+
Contributions, issues, and feature requests are welcome!
|
|
1279
|
+
|
|
1280
|
+
1. Fork the repository
|
|
1281
|
+
2. Create your feature branch: `git checkout -b feature/my-feature`
|
|
1282
|
+
3. Commit your changes: `git commit -m 'Add my feature'`
|
|
1283
|
+
4. Push to the branch: `git push origin feature/my-feature`
|
|
1284
|
+
5. Open a Pull Request
|
|
1285
|
+
|
|
1286
|
+
---
|
|
1287
|
+
|
|
1288
|
+
## 👤 Author
|
|
1289
|
+
|
|
1290
|
+
**Muhammad Saad Amin**
|
|
1291
|
+
|
|
1292
|
+
---
|
|
1293
|
+
|
|
1294
|
+
## 📝 License
|
|
1295
|
+
|
|
1296
|
+
This project is licensed under the **MIT License** — feel free to use it in personal and commercial projects.
|
|
1297
|
+
|
|
1298
|
+
---
|
|
1299
|
+
|
|
1300
|
+
<div align="center">
|
|
1301
|
+
|
|
1302
|
+
If this tool saved you time, consider giving it a ⭐ on npm!
|
|
1303
|
+
|
|
1304
|
+
</div>
|
|
1305
|
+
|
|
1306
|
+
|
|
50
1307
|
================================================================================
|
|
51
1308
|
END OF FOLDER: make-folder-txt
|
|
52
1309
|
================================================================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "make-folder-txt",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4",
|
|
4
4
|
"description": "Generate a single .txt file containing the full folder structure and file contents of any project, ignoring node_modules and other junk.",
|
|
5
5
|
"main": "bin/make-folder-txt.js",
|
|
6
6
|
"bin": {
|