make-folder-txt 2.2.4 → 2.2.6

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