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