make-folder-txt 1.4.1 → 1.4.3
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 +397 -392
- package/package.json +1 -1
package/bin/make-folder-txt.js
CHANGED
|
@@ -1,392 +1,397 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const { version } = require("../package.json");
|
|
6
|
-
|
|
7
|
-
// ── config ────────────────────────────────────────────────────────────────────
|
|
8
|
-
const IGNORE_DIRS = new Set(["node_modules", ".git", ".next", "dist", "build", ".cache"]);
|
|
9
|
-
const IGNORE_FILES = new Set([".DS_Store", "Thumbs.db", "desktop.ini", ".txtignore"]);
|
|
10
|
-
|
|
11
|
-
const BINARY_EXTS = new Set([
|
|
12
|
-
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".svg", ".webp",
|
|
13
|
-
".pdf", ".zip", ".tar", ".gz", ".rar", ".7z",
|
|
14
|
-
".exe", ".dll", ".so", ".dylib", ".bin",
|
|
15
|
-
".mp3", ".mp4", ".wav", ".avi", ".mov",
|
|
16
|
-
".woff", ".woff2", ".ttf", ".eot", ".otf",
|
|
17
|
-
".lock",
|
|
18
|
-
]);
|
|
19
|
-
|
|
20
|
-
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
function readTxtIgnore(rootDir) {
|
|
23
|
-
const txtIgnorePath = path.join(rootDir, '.txtignore');
|
|
24
|
-
const ignorePatterns = new Set();
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
const content = fs.readFileSync(txtIgnorePath, 'utf8');
|
|
28
|
-
const lines = content.split('\n')
|
|
29
|
-
.map(line => line.trim())
|
|
30
|
-
.filter(line => line && !line.startsWith('#'));
|
|
31
|
-
|
|
32
|
-
lines.forEach(line => ignorePatterns.add(line));
|
|
33
|
-
} catch (err) {
|
|
34
|
-
// .txtignore doesn't exist or can't be read - that's fine
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return ignorePatterns;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function collectFiles(
|
|
41
|
-
dir,
|
|
42
|
-
rootDir,
|
|
43
|
-
ignoreDirs,
|
|
44
|
-
ignoreFiles,
|
|
45
|
-
onlyFolders,
|
|
46
|
-
onlyFiles,
|
|
47
|
-
options = {},
|
|
48
|
-
) {
|
|
49
|
-
const {
|
|
50
|
-
indent = "",
|
|
51
|
-
lines = [],
|
|
52
|
-
filePaths = [],
|
|
53
|
-
inSelectedFolder = false,
|
|
54
|
-
hasOnlyFilters = false,
|
|
55
|
-
rootName = "",
|
|
56
|
-
txtIgnore = new Set(),
|
|
57
|
-
} = options;
|
|
58
|
-
|
|
59
|
-
let entries;
|
|
60
|
-
try {
|
|
61
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
62
|
-
} catch {
|
|
63
|
-
return { lines, filePaths, hasIncluded: false };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
entries.sort((a, b) => {
|
|
67
|
-
if (a.isDirectory() === b.isDirectory()) return a.name.localeCompare(b.name);
|
|
68
|
-
return a.isDirectory() ? -1 : 1;
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
entries.forEach((entry, idx) => {
|
|
72
|
-
const isLast = idx === entries.length - 1;
|
|
73
|
-
const connector = isLast ? "└── " : "├── ";
|
|
74
|
-
const childIndent = indent + (isLast ? " " : "│ ");
|
|
75
|
-
|
|
76
|
-
if (entry.isDirectory()) {
|
|
77
|
-
if (ignoreDirs.has(entry.name)) {
|
|
78
|
-
if (!hasOnlyFilters) {
|
|
79
|
-
lines.push(`${indent}${connector}${entry.name}/ [skipped]`);
|
|
80
|
-
}
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Get relative path for .txtignore pattern matching
|
|
85
|
-
const relPathForIgnore = path.relative(rootDir, path.join(dir, entry.name)).split(path.sep).join("/");
|
|
86
|
-
|
|
87
|
-
// Check against .txtignore patterns (both dirname and relative path)
|
|
88
|
-
if (txtIgnore.has(entry.name) || txtIgnore.has(`${entry.name}/`) || txtIgnore.has(relPathForIgnore) || txtIgnore.has(`${relPathForIgnore}/`) || txtIgnore.has(`/${relPathForIgnore}/`)) {
|
|
89
|
-
if (!hasOnlyFilters) {
|
|
90
|
-
lines.push(`${indent}${connector}${entry.name}/ [skipped]`);
|
|
91
|
-
}
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const childPath = path.join(dir, entry.name);
|
|
96
|
-
const childInSelectedFolder = inSelectedFolder || onlyFolders.has(entry.name);
|
|
97
|
-
const childLines = [];
|
|
98
|
-
const childFiles = [];
|
|
99
|
-
const child = collectFiles(
|
|
100
|
-
childPath,
|
|
101
|
-
rootDir,
|
|
102
|
-
ignoreDirs,
|
|
103
|
-
ignoreFiles,
|
|
104
|
-
onlyFolders,
|
|
105
|
-
onlyFiles,
|
|
106
|
-
{
|
|
107
|
-
indent: childIndent,
|
|
108
|
-
lines: childLines,
|
|
109
|
-
filePaths: childFiles,
|
|
110
|
-
inSelectedFolder: childInSelectedFolder,
|
|
111
|
-
hasOnlyFilters,
|
|
112
|
-
rootName,
|
|
113
|
-
txtIgnore,
|
|
114
|
-
},
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
const explicitlySelectedFolder = hasOnlyFilters && onlyFolders.has(entry.name);
|
|
118
|
-
const shouldIncludeDir = !hasOnlyFilters || child.hasIncluded || explicitlySelectedFolder;
|
|
119
|
-
|
|
120
|
-
if (shouldIncludeDir) {
|
|
121
|
-
lines.push(`${indent}${connector}${entry.name}/`);
|
|
122
|
-
lines.push(...child.lines);
|
|
123
|
-
filePaths.push(...child.filePaths);
|
|
124
|
-
}
|
|
125
|
-
} else {
|
|
126
|
-
if (ignoreFiles.has(entry.name)) return;
|
|
127
|
-
|
|
128
|
-
// Get relative path for .txtignore pattern matching
|
|
129
|
-
const relPathForIgnore = path.relative(rootDir, path.join(dir, entry.name)).split(path.sep).join("/");
|
|
130
|
-
|
|
131
|
-
// Check against .txtignore patterns (both filename and relative path)
|
|
132
|
-
if (txtIgnore.has(entry.name) || txtIgnore.has(relPathForIgnore) || txtIgnore.has(`/${relPathForIgnore}`)) {
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Ignore .txt files that match the folder name (e.g., foldername.txt)
|
|
137
|
-
if (entry.name.endsWith('.txt') && entry.name === `${rootName}.txt`) return;
|
|
138
|
-
|
|
139
|
-
const shouldIncludeFile = !hasOnlyFilters || inSelectedFolder || onlyFiles.has(entry.name);
|
|
140
|
-
if (!shouldIncludeFile) return;
|
|
141
|
-
|
|
142
|
-
lines.push(`${indent}${connector}${entry.name}`);
|
|
143
|
-
const relPath = "/" + path.relative(rootDir, path.join(dir, entry.name)).split(path.sep).join("/");
|
|
144
|
-
filePaths.push({ abs: path.join(dir, entry.name), rel: relPath });
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
return { lines, filePaths, hasIncluded: filePaths.length > 0 || lines.length > 0 };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function readContent(absPath) {
|
|
152
|
-
const ext = path.extname(absPath).toLowerCase();
|
|
153
|
-
if (BINARY_EXTS.has(ext)) return "[binary / skipped]";
|
|
154
|
-
try {
|
|
155
|
-
const stat = fs.statSync(absPath);
|
|
156
|
-
if (stat.size > 500 * 1024) {
|
|
157
|
-
return `[file too large: ${(stat.size / 1024).toFixed(1)} KB – skipped]`;
|
|
158
|
-
}
|
|
159
|
-
return fs.readFileSync(absPath, "utf8");
|
|
160
|
-
} catch (err) {
|
|
161
|
-
return `[could not read file: ${err.message}]`;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ── main ──────────────────────────────────────────────────────────────────────
|
|
166
|
-
|
|
167
|
-
const args = process.argv.slice(2);
|
|
168
|
-
|
|
169
|
-
if (args.includes("-v") || args.includes("--version")) {
|
|
170
|
-
console.log(`v${version}`);
|
|
171
|
-
console.log("Built by Muhammad Saad Amin");
|
|
172
|
-
process.exit(0);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
176
|
-
console.log(`
|
|
177
|
-
\x1b[33mmake-folder-txt\x1b[0m
|
|
178
|
-
Dump an entire project folder into a single readable .txt file.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
--
|
|
185
|
-
--
|
|
186
|
-
--
|
|
187
|
-
--
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
make-folder-txt
|
|
193
|
-
make-folder-txt --
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
folderPath
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
out
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
out.push(divider);
|
|
360
|
-
out.push(
|
|
361
|
-
out.push(divider);
|
|
362
|
-
out.push(
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
out.push("");
|
|
366
|
-
out.push(
|
|
367
|
-
out.push(
|
|
368
|
-
|
|
369
|
-
out.push(
|
|
370
|
-
out.push("
|
|
371
|
-
out.push(
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
out.push(
|
|
382
|
-
out.push(
|
|
383
|
-
out.push(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { version } = require("../package.json");
|
|
6
|
+
|
|
7
|
+
// ── config ────────────────────────────────────────────────────────────────────
|
|
8
|
+
const IGNORE_DIRS = new Set(["node_modules", ".git", ".next", "dist", "build", ".cache"]);
|
|
9
|
+
const IGNORE_FILES = new Set([".DS_Store", "Thumbs.db", "desktop.ini", ".txtignore"]);
|
|
10
|
+
|
|
11
|
+
const BINARY_EXTS = new Set([
|
|
12
|
+
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".svg", ".webp",
|
|
13
|
+
".pdf", ".zip", ".tar", ".gz", ".rar", ".7z",
|
|
14
|
+
".exe", ".dll", ".so", ".dylib", ".bin",
|
|
15
|
+
".mp3", ".mp4", ".wav", ".avi", ".mov",
|
|
16
|
+
".woff", ".woff2", ".ttf", ".eot", ".otf",
|
|
17
|
+
".lock",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function readTxtIgnore(rootDir) {
|
|
23
|
+
const txtIgnorePath = path.join(rootDir, '.txtignore');
|
|
24
|
+
const ignorePatterns = new Set();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const content = fs.readFileSync(txtIgnorePath, 'utf8');
|
|
28
|
+
const lines = content.split('\n')
|
|
29
|
+
.map(line => line.trim())
|
|
30
|
+
.filter(line => line && !line.startsWith('#'));
|
|
31
|
+
|
|
32
|
+
lines.forEach(line => ignorePatterns.add(line));
|
|
33
|
+
} catch (err) {
|
|
34
|
+
// .txtignore doesn't exist or can't be read - that's fine
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return ignorePatterns;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function collectFiles(
|
|
41
|
+
dir,
|
|
42
|
+
rootDir,
|
|
43
|
+
ignoreDirs,
|
|
44
|
+
ignoreFiles,
|
|
45
|
+
onlyFolders,
|
|
46
|
+
onlyFiles,
|
|
47
|
+
options = {},
|
|
48
|
+
) {
|
|
49
|
+
const {
|
|
50
|
+
indent = "",
|
|
51
|
+
lines = [],
|
|
52
|
+
filePaths = [],
|
|
53
|
+
inSelectedFolder = false,
|
|
54
|
+
hasOnlyFilters = false,
|
|
55
|
+
rootName = "",
|
|
56
|
+
txtIgnore = new Set(),
|
|
57
|
+
} = options;
|
|
58
|
+
|
|
59
|
+
let entries;
|
|
60
|
+
try {
|
|
61
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
62
|
+
} catch {
|
|
63
|
+
return { lines, filePaths, hasIncluded: false };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
entries.sort((a, b) => {
|
|
67
|
+
if (a.isDirectory() === b.isDirectory()) return a.name.localeCompare(b.name);
|
|
68
|
+
return a.isDirectory() ? -1 : 1;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
entries.forEach((entry, idx) => {
|
|
72
|
+
const isLast = idx === entries.length - 1;
|
|
73
|
+
const connector = isLast ? "└── " : "├── ";
|
|
74
|
+
const childIndent = indent + (isLast ? " " : "│ ");
|
|
75
|
+
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
if (ignoreDirs.has(entry.name)) {
|
|
78
|
+
if (!hasOnlyFilters) {
|
|
79
|
+
lines.push(`${indent}${connector}${entry.name}/ [skipped]`);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Get relative path for .txtignore pattern matching
|
|
85
|
+
const relPathForIgnore = path.relative(rootDir, path.join(dir, entry.name)).split(path.sep).join("/");
|
|
86
|
+
|
|
87
|
+
// Check against .txtignore patterns (both dirname and relative path)
|
|
88
|
+
if (txtIgnore.has(entry.name) || txtIgnore.has(`${entry.name}/`) || txtIgnore.has(relPathForIgnore) || txtIgnore.has(`${relPathForIgnore}/`) || txtIgnore.has(`/${relPathForIgnore}/`)) {
|
|
89
|
+
if (!hasOnlyFilters) {
|
|
90
|
+
lines.push(`${indent}${connector}${entry.name}/ [skipped]`);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const childPath = path.join(dir, entry.name);
|
|
96
|
+
const childInSelectedFolder = inSelectedFolder || onlyFolders.has(entry.name);
|
|
97
|
+
const childLines = [];
|
|
98
|
+
const childFiles = [];
|
|
99
|
+
const child = collectFiles(
|
|
100
|
+
childPath,
|
|
101
|
+
rootDir,
|
|
102
|
+
ignoreDirs,
|
|
103
|
+
ignoreFiles,
|
|
104
|
+
onlyFolders,
|
|
105
|
+
onlyFiles,
|
|
106
|
+
{
|
|
107
|
+
indent: childIndent,
|
|
108
|
+
lines: childLines,
|
|
109
|
+
filePaths: childFiles,
|
|
110
|
+
inSelectedFolder: childInSelectedFolder,
|
|
111
|
+
hasOnlyFilters,
|
|
112
|
+
rootName,
|
|
113
|
+
txtIgnore,
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const explicitlySelectedFolder = hasOnlyFilters && onlyFolders.has(entry.name);
|
|
118
|
+
const shouldIncludeDir = !hasOnlyFilters || child.hasIncluded || explicitlySelectedFolder;
|
|
119
|
+
|
|
120
|
+
if (shouldIncludeDir) {
|
|
121
|
+
lines.push(`${indent}${connector}${entry.name}/`);
|
|
122
|
+
lines.push(...child.lines);
|
|
123
|
+
filePaths.push(...child.filePaths);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
if (ignoreFiles.has(entry.name)) return;
|
|
127
|
+
|
|
128
|
+
// Get relative path for .txtignore pattern matching
|
|
129
|
+
const relPathForIgnore = path.relative(rootDir, path.join(dir, entry.name)).split(path.sep).join("/");
|
|
130
|
+
|
|
131
|
+
// Check against .txtignore patterns (both filename and relative path)
|
|
132
|
+
if (txtIgnore.has(entry.name) || txtIgnore.has(relPathForIgnore) || txtIgnore.has(`/${relPathForIgnore}`)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Ignore .txt files that match the folder name (e.g., foldername.txt)
|
|
137
|
+
if (entry.name.endsWith('.txt') && entry.name === `${rootName}.txt`) return;
|
|
138
|
+
|
|
139
|
+
const shouldIncludeFile = !hasOnlyFilters || inSelectedFolder || onlyFiles.has(entry.name);
|
|
140
|
+
if (!shouldIncludeFile) return;
|
|
141
|
+
|
|
142
|
+
lines.push(`${indent}${connector}${entry.name}`);
|
|
143
|
+
const relPath = "/" + path.relative(rootDir, path.join(dir, entry.name)).split(path.sep).join("/");
|
|
144
|
+
filePaths.push({ abs: path.join(dir, entry.name), rel: relPath });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return { lines, filePaths, hasIncluded: filePaths.length > 0 || lines.length > 0 };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readContent(absPath) {
|
|
152
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
153
|
+
if (BINARY_EXTS.has(ext)) return "[binary / skipped]";
|
|
154
|
+
try {
|
|
155
|
+
const stat = fs.statSync(absPath);
|
|
156
|
+
if (stat.size > 500 * 1024) {
|
|
157
|
+
return `[file too large: ${(stat.size / 1024).toFixed(1)} KB – skipped]`;
|
|
158
|
+
}
|
|
159
|
+
return fs.readFileSync(absPath, "utf8");
|
|
160
|
+
} catch (err) {
|
|
161
|
+
return `[could not read file: ${err.message}]`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── main ──────────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
const args = process.argv.slice(2);
|
|
168
|
+
|
|
169
|
+
if (args.includes("-v") || args.includes("--version")) {
|
|
170
|
+
console.log(`v${version}`);
|
|
171
|
+
console.log("Built by Muhammad Saad Amin");
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
176
|
+
console.log(`
|
|
177
|
+
\x1b[33mmake-folder-txt\x1b[0m
|
|
178
|
+
Dump an entire project folder into a single readable .txt file.
|
|
179
|
+
|
|
180
|
+
\x1b[33mUSAGE\x1b[0m
|
|
181
|
+
make-folder-txt [options]
|
|
182
|
+
|
|
183
|
+
\x1b[33mOPTIONS\x1b[0m
|
|
184
|
+
--ignore-folder <names...> Ignore specific folders by name
|
|
185
|
+
--ignore-file <names...> Ignore specific files by name
|
|
186
|
+
--only-folder <names...> Include only specific folders
|
|
187
|
+
--only-file <names...> Include only specific files
|
|
188
|
+
--help, -h Show this help message
|
|
189
|
+
--version, -v Show version information
|
|
190
|
+
|
|
191
|
+
\x1b[33mEXAMPLES\x1b[0m
|
|
192
|
+
make-folder-txt
|
|
193
|
+
make-folder-txt --ignore-folder node_modules dist
|
|
194
|
+
make-folder-txt --ignore-file .env .env.local
|
|
195
|
+
make-folder-txt --only-folder src docs
|
|
196
|
+
make-folder-txt --only-file package.json README.md
|
|
197
|
+
|
|
198
|
+
\x1b[33m.TXTIGNORE FILE\x1b[0m
|
|
199
|
+
Create a .txtignore file in your project root to specify files/folders to ignore.
|
|
200
|
+
Works like .gitignore - supports file names, path patterns, and comments.
|
|
201
|
+
|
|
202
|
+
Example .txtignore:
|
|
203
|
+
node_modules/
|
|
204
|
+
*.log
|
|
205
|
+
.env
|
|
206
|
+
coverage/
|
|
207
|
+
LICENSE
|
|
208
|
+
`);
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const ignoreDirs = new Set(IGNORE_DIRS);
|
|
213
|
+
const ignoreFiles = new Set(IGNORE_FILES);
|
|
214
|
+
const onlyFolders = new Set();
|
|
215
|
+
const onlyFiles = new Set();
|
|
216
|
+
let outputArg = null;
|
|
217
|
+
|
|
218
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
219
|
+
const arg = args[i];
|
|
220
|
+
|
|
221
|
+
if (arg === "--ignore-folder") {
|
|
222
|
+
let consumed = 0;
|
|
223
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
224
|
+
ignoreDirs.add(args[i + 1]);
|
|
225
|
+
i += 1;
|
|
226
|
+
consumed += 1;
|
|
227
|
+
}
|
|
228
|
+
if (consumed === 0) {
|
|
229
|
+
console.error("Error: --ignore-folder requires at least one folder name.");
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (arg.startsWith("--ignore-folder=")) {
|
|
236
|
+
const value = arg.slice("--ignore-folder=".length);
|
|
237
|
+
if (!value) {
|
|
238
|
+
console.error("Error: --ignore-folder requires a folder name.");
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
ignoreDirs.add(value);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (arg === "--ignore-file") {
|
|
246
|
+
let consumed = 0;
|
|
247
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
248
|
+
ignoreFiles.add(args[i + 1]);
|
|
249
|
+
i += 1;
|
|
250
|
+
consumed += 1;
|
|
251
|
+
}
|
|
252
|
+
if (consumed === 0) {
|
|
253
|
+
console.error("Error: --ignore-file requires at least one file name.");
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (arg.startsWith("--ignore-file=")) {
|
|
260
|
+
const value = arg.slice("--ignore-file=".length);
|
|
261
|
+
if (!value) {
|
|
262
|
+
console.error("Error: --ignore-file requires a file name.");
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
ignoreFiles.add(value);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (arg === "--only-folder") {
|
|
270
|
+
let consumed = 0;
|
|
271
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
272
|
+
onlyFolders.add(args[i + 1]);
|
|
273
|
+
i += 1;
|
|
274
|
+
consumed += 1;
|
|
275
|
+
}
|
|
276
|
+
if (consumed === 0) {
|
|
277
|
+
console.error("Error: --only-folder requires at least one folder name.");
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (arg.startsWith("--only-folder=")) {
|
|
284
|
+
const value = arg.slice("--only-folder=".length);
|
|
285
|
+
if (!value) {
|
|
286
|
+
console.error("Error: --only-folder requires a folder name.");
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
onlyFolders.add(value);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (arg === "--only-file") {
|
|
294
|
+
let consumed = 0;
|
|
295
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
296
|
+
onlyFiles.add(args[i + 1]);
|
|
297
|
+
i += 1;
|
|
298
|
+
consumed += 1;
|
|
299
|
+
}
|
|
300
|
+
if (consumed === 0) {
|
|
301
|
+
console.error("Error: --only-file requires at least one file name.");
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (arg.startsWith("--only-file=")) {
|
|
308
|
+
const value = arg.slice("--only-file=".length);
|
|
309
|
+
if (!value) {
|
|
310
|
+
console.error("Error: --only-file requires a file name.");
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
onlyFiles.add(value);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (arg.startsWith("-")) {
|
|
318
|
+
console.error(`Error: Unknown option "${arg}".`);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!outputArg) {
|
|
323
|
+
outputArg = arg;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
console.error(`Error: Unexpected argument "${arg}".`);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const folderPath = process.cwd();
|
|
332
|
+
const rootName = path.basename(folderPath);
|
|
333
|
+
|
|
334
|
+
// Read .txtignore file if it exists
|
|
335
|
+
const txtIgnore = readTxtIgnore(folderPath);
|
|
336
|
+
|
|
337
|
+
const outputFile = outputArg
|
|
338
|
+
? path.resolve(outputArg)
|
|
339
|
+
: path.join(process.cwd(), `${rootName}.txt`);
|
|
340
|
+
|
|
341
|
+
console.log(`\n📂 Scanning: ${folderPath}`);
|
|
342
|
+
|
|
343
|
+
const hasOnlyFilters = onlyFolders.size > 0 || onlyFiles.size > 0;
|
|
344
|
+
const { lines: treeLines, filePaths } = collectFiles(
|
|
345
|
+
folderPath,
|
|
346
|
+
folderPath,
|
|
347
|
+
ignoreDirs,
|
|
348
|
+
ignoreFiles,
|
|
349
|
+
onlyFolders,
|
|
350
|
+
onlyFiles,
|
|
351
|
+
{ hasOnlyFilters, rootName, txtIgnore },
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// ── build output ──────────────────────────────────────────────────────────────
|
|
355
|
+
const out = [];
|
|
356
|
+
const divider = "=".repeat(80);
|
|
357
|
+
const subDivider = "-".repeat(80);
|
|
358
|
+
|
|
359
|
+
out.push(divider);
|
|
360
|
+
out.push(`START OF FOLDER: ${rootName}`);
|
|
361
|
+
out.push(divider);
|
|
362
|
+
out.push("");
|
|
363
|
+
|
|
364
|
+
out.push(divider);
|
|
365
|
+
out.push("PROJECT STRUCTURE");
|
|
366
|
+
out.push(divider);
|
|
367
|
+
out.push(`Root: ${folderPath}\n`);
|
|
368
|
+
out.push(`${rootName}/`);
|
|
369
|
+
treeLines.forEach(l => out.push(l));
|
|
370
|
+
out.push("");
|
|
371
|
+
out.push(`Total files: ${filePaths.length}`);
|
|
372
|
+
out.push("");
|
|
373
|
+
|
|
374
|
+
out.push(divider);
|
|
375
|
+
out.push("FILE CONTENTS");
|
|
376
|
+
out.push(divider);
|
|
377
|
+
|
|
378
|
+
filePaths.forEach(({ abs, rel }) => {
|
|
379
|
+
out.push("");
|
|
380
|
+
out.push(subDivider);
|
|
381
|
+
out.push(`FILE: ${rel}`);
|
|
382
|
+
out.push(subDivider);
|
|
383
|
+
out.push(readContent(abs));
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
out.push("");
|
|
387
|
+
out.push(divider);
|
|
388
|
+
out.push(`END OF FOLDER: ${rootName}`);
|
|
389
|
+
out.push(divider);
|
|
390
|
+
|
|
391
|
+
fs.writeFileSync(outputFile, out.join("\n"), "utf8");
|
|
392
|
+
|
|
393
|
+
const sizeKB = (fs.statSync(outputFile).size / 1024).toFixed(1);
|
|
394
|
+
console.log(`✅ Done!`);
|
|
395
|
+
console.log(`📄 Output : ${outputFile}`);
|
|
396
|
+
console.log(`📊 Size : ${sizeKB} KB`);
|
|
397
|
+
console.log(`🗂️ Files : ${filePaths.length}\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "make-folder-txt",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
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": {
|