repomeld 2.0.4 → 2.0.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.
Files changed (3) hide show
  1. package/README.md +253 -9
  2. package/bin/cli.js +288 -168
  3. package/package.json +32 -6
package/README.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  > Meld your entire repo into a single file — perfect for AI context, code reviews & sharing.
4
4
 
5
+ [![npm version](https://badge.fury.io/js/repomeld.svg)](https://www.npmjs.com/package/repomeld)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
5
8
  ---
6
9
 
7
10
  > ## šŸ’¼ Open to Work
@@ -10,12 +13,33 @@
10
13
 
11
14
  ---
12
15
 
16
+ ## ✨ Features
17
+
18
+ - šŸš€ **Fast & Efficient** - Async scanning with real-time progress
19
+ - šŸŽØ **Multiple Styles** - Banner, Markdown, or Minimal output
20
+ - šŸ” **Smart Filtering** - Extension, pattern, and size-based filtering
21
+ - šŸ“ **Gitignore Support** - Respects your .gitignore rules automatically
22
+ - šŸ’¾ **Binary Detection** - Intelligently skips binary files
23
+ - šŸ“¦ **Single File Output** - Perfect for AI context windows
24
+ - šŸ”„ **Auto-Numbering** - Never overwrites existing files
25
+ - šŸ’æ **Zip Backup** - Creates timestamped backups of all included files
26
+ - šŸ”” **Update Notifications** - Know when new versions are available
27
+ - šŸŽÆ **Force Include** - Override ignore rules when needed
28
+
29
+ ---
30
+
13
31
  ## Install
14
32
 
15
33
  ```bash
16
34
  npm install -g repomeld
17
35
  ```
18
36
 
37
+ Or use without installing:
38
+
39
+ ```bash
40
+ npx repomeld
41
+ ```
42
+
19
43
  ---
20
44
 
21
45
  ## Quick Start
@@ -25,7 +49,7 @@ cd your-project
25
49
  repomeld
26
50
  ```
27
51
 
28
- That's it. repomeld walks your project, skips noise (node_modules, lock files, build folders, etc.) and writes everything into one readable file.
52
+ That's it. repomeld walks your project, respects `.gitignore`, skips binary files, and writes everything into one readable file.
29
53
 
30
54
  ---
31
55
 
@@ -76,6 +100,8 @@ Filtering:
76
100
  --max-size <kb> Skip files larger than N kilobytes
77
101
  Default: 500
78
102
 
103
+ --no-gitignore Ignore .gitignore file (include everything)
104
+
79
105
  Formatting:
80
106
  -s, --style <style> Header style for each file block:
81
107
  banner — clear dividers with file info (default)
@@ -92,6 +118,8 @@ Advanced:
92
118
  --lines-before <n> Skip the first N lines of every file
93
119
  --lines-after <n> Skip the last N lines of every file
94
120
  --dry-run Preview which files would be included — nothing is written
121
+ --no-backup Skip creating backup zip file
122
+ --no-update-check Skip checking for updates
95
123
  ```
96
124
 
97
125
  ---
@@ -120,6 +148,9 @@ repomeld --dry-run
120
148
  # Ignore extra folders on top of defaults
121
149
  repomeld --ignore coverage logs tmp
122
150
 
151
+ # Respect gitignore (default) or ignore it
152
+ repomeld --no-gitignore # include everything
153
+
123
154
  # Only small files — skip anything over 100 KB
124
155
  repomeld --max-size 100
125
156
 
@@ -131,6 +162,9 @@ repomeld --no-toc --no-meta
131
162
 
132
163
  # Combine filters
133
164
  repomeld --ext php --include Controllers --exclude test --style markdown
165
+
166
+ # Skip backup creation
167
+ repomeld --no-backup
134
168
  ```
135
169
 
136
170
  ---
@@ -147,14 +181,15 @@ repomeld automatically skips these so your output stays clean:
147
181
  | Lock files | `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml` |
148
182
  | Build output | `dist/`, `build/`, `.next/`, `.nuxt/`, `.cache/` |
149
183
  | OS files | `.DS_Store` |
150
- | Project meta | `package.json`, `README.md` |
151
184
  | repomeld output | `repomeld_output.txt` and all `repomeld_output__N.txt` files |
152
185
 
153
- You can add your own permanent ignore rules via a `repomeld.ignore.json` file (see below).
186
+ **Note:** `package.json` and `README.md` are **NOT** ignored by default — they contain important context for AI tools and code reviews.
154
187
 
155
188
  ---
156
189
 
157
- ## Custom Ignore File
190
+ ## Custom Ignore Rules
191
+
192
+ ### Method 1: repomeld.ignore.json
158
193
 
159
194
  Create a `repomeld.ignore.json` in your project root:
160
195
 
@@ -164,13 +199,52 @@ Create a `repomeld.ignore.json` in your project root:
164
199
  "coverage",
165
200
  "logs",
166
201
  "tmp",
167
- "*.min.js"
202
+ "*.min.js",
203
+ "**/generated/**"
168
204
  ]
169
205
  }
170
206
  ```
171
207
 
172
208
  These are merged with the defaults every time repomeld runs.
173
209
 
210
+ ### Method 2: .gitignore
211
+
212
+ repomeld automatically respects your `.gitignore` file. Use `--no-gitignore` to override.
213
+
214
+ ### Method 3: CLI --ignore
215
+
216
+ Override on the command line:
217
+
218
+ ```bash
219
+ repomeld --ignore temp logs "*.tmp"
220
+ ```
221
+
222
+ ---
223
+
224
+ ## Backup Zip Files
225
+
226
+ When repomeld runs, it automatically creates a backup zip file in the `repomeld_repomeld/` folder:
227
+
228
+ ```
229
+ repomeld_output.txt
230
+ repomeld_repomeld/
231
+ └── repomeld_output.zip ← contains all included files + output
232
+
233
+ repomeld_output__2.txt
234
+ repomeld_repomeld/
235
+ └── repomeld_output__2.zip ← corresponding backup
236
+
237
+ repomeld_output__3.txt
238
+ repomeld_repomeld/
239
+ └── repomeld_output__3.zip ← and so on...
240
+ ```
241
+
242
+ The zip file contains:
243
+ - All source files included in the run (preserving folder structure)
244
+ - The repomeld output file itself
245
+
246
+ To disable backups: `repomeld --no-backup`
247
+
174
248
  ---
175
249
 
176
250
  ## Output Format
@@ -178,8 +252,8 @@ These are merged with the defaults every time repomeld runs.
178
252
  Each run produces a file like this:
179
253
 
180
254
  ```
181
- # Generated by repomeld v1.0.0
182
- # Date : 2025-04-20T10:00:00.000Z
255
+ # Generated by repomeld v2.0.4
256
+ # Date : 2025-04-23T10:00:00.000Z
183
257
  # Source : /your/project
184
258
  # Files : 12
185
259
  # Lines : 847
@@ -199,16 +273,186 @@ TABLE OF CONTENTS
199
273
  ... file contents ...
200
274
  ```
201
275
 
202
- With `--style markdown` each file becomes a fenced code block — paste directly into Claude, ChatGPT, or any AI tool.
276
+ ### Markdown Style Example
277
+
278
+ With `--style markdown` each file becomes a fenced code block:
279
+
280
+ ```markdown
281
+ ## šŸ“„ src/index.js [120 lines | 3.2 KB | javascript]
282
+
283
+ ```javascript
284
+ // Your code here
285
+ ```
286
+
287
+ ## šŸ“„ src/utils.js [45 lines | 1.1 KB | javascript]
288
+
289
+ ```javascript
290
+ // More code here
291
+ ```
292
+ ```
293
+
294
+ Perfect for pasting directly into Claude, ChatGPT, Cursor, or any AI tool!
295
+
296
+ ### Minimal Style Example
297
+
298
+ With `--style minimal`:
299
+
300
+ ```
301
+ # src/index.js
302
+ your code here
303
+
304
+ # src/utils.js
305
+ more code here
306
+ ```
307
+
308
+ ---
309
+
310
+ ## Performance
311
+
312
+ repomeld is optimized for large codebases:
313
+ - Async file scanning (non-blocking)
314
+ - Real-time progress indicator with ETA
315
+ - Memory-efficient streaming
316
+ - Handles repos with 10,000+ files easily
317
+
318
+ Example output:
319
+ ```
320
+ šŸ” Scanning files...
321
+ āœ… Found 2453 files in 1.2s
322
+
323
+ šŸ“ Processing 2453 files...
324
+
325
+ [1245/2453] files (50.7%) | 2.3s elapsed | ETA: 2s
326
+ āœ… Completed 2453/2453 files in 4.7s
327
+ ```
328
+
329
+ ---
330
+
331
+ ## Use Cases
332
+
333
+ ### šŸ¤– AI Context Preparation
334
+ ```bash
335
+ repomeld --ext js ts jsx py --style markdown --max-size 200
336
+ ```
337
+
338
+ ### šŸ“‹ Code Review
339
+ ```bash
340
+ repomeld --include src/ --exclude test --style minimal --no-meta
341
+ ```
342
+
343
+ ### šŸ’¾ Full Project Backup
344
+ ```bash
345
+ repomeld --force-include . --max-size 10000 --no-toc --no-meta
346
+ ```
347
+
348
+ ### šŸ“š Documentation Generation
349
+ ```bash
350
+ repomeld --ext md --include docs --style markdown --output documentation.md
351
+ ```
352
+
353
+ ### šŸ” Debug Specific Feature
354
+ ```bash
355
+ repomeld --include feature-name --ext js css --output feature-context.txt
356
+ ```
357
+
358
+ ---
359
+
360
+ ## FAQ
361
+
362
+ **Q: Why are package.json and README.md included now?**
363
+ A: They were removed in early versions but added back because they provide essential context for AI tools and code reviewers.
364
+
365
+ **Q: How do I ignore package.json?**
366
+ A: Add it to `repomeld.ignore.json` or use `--ignore package.json`
367
+
368
+ **Q: Can I use this in CI/CD?**
369
+ A: Yes! Use `--no-update-check` and `--no-backup` for automated environments.
370
+
371
+ **Q: Does it work on Windows?**
372
+ A: Yes! Paths are normalized for cross-platform compatibility.
373
+
374
+ **Q: How do I get just the file list without content?**
375
+ A: Use `--dry-run` to preview without writing.
376
+
377
+ **Q: My binary files are being included?**
378
+ A: repomeld uses intelligent binary detection. If something slips through, use `--ext` to filter specific extensions.
379
+
380
+ ---
381
+
382
+ ## Development
383
+
384
+ ```bash
385
+ # Clone the repo
386
+ git clone https://github.com/susheel/repomeld.git
387
+ cd repomeld
388
+
389
+ # Install dependencies
390
+ npm install
391
+
392
+ # Run locally
393
+ npm start -- --dry-run
394
+
395
+ # Link for global testing
396
+ npm link
397
+ repomeld --help
398
+ ```
399
+
400
+ ---
401
+
402
+ ## Contributing
403
+
404
+ Contributions are welcome! Please feel free to submit a Pull Request.
405
+
406
+ 1. Fork the repository
407
+ 2. Create your feature branch (`git checkout -b feature/amazing`)
408
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
409
+ 4. Push to the branch (`git push origin feature/amazing`)
410
+ 5. Open a Pull Request
411
+
412
+ ---
413
+
414
+ ## Changelog
415
+
416
+ ### v2.0.4 (Current)
417
+ - āœ… Added gitignore support with `ignore` package
418
+ - āœ… Added zip backup feature (`repomeld_repomeld/` folder)
419
+ - āœ… Added update notifications (non-intrusive)
420
+ - āœ… Improved performance with async operations
421
+ - āœ… Added progress indicator with ETA
422
+ - āœ… Fixed Windows path compatibility
423
+ - āœ… Improved binary detection
424
+ - āœ… Added force-include for override scenarios
425
+ - āœ… Removed `package.json` and `README.md` from default ignores
426
+
427
+ ### v1.0.0
428
+ - Initial release with basic functionality
203
429
 
204
430
  ---
205
431
 
206
432
  ## License
207
433
 
208
- MIT
434
+ MIT Ā© [Susheel](mailto:susheelhbti@gmail.com)
435
+
436
+ ---
437
+
438
+ ## Support & Contact
439
+
440
+ - šŸ› **Issues**: [GitHub Issues](https://github.com/susheel/repomeld/issues)
441
+ - šŸ“§ **Email**: [susheelhbti@gmail.com](mailto:susheelhbti@gmail.com)
442
+ - šŸ’¼ **Hire Me**: Available for freelance and full-time opportunities
209
443
 
210
444
  ---
211
445
 
212
446
  > ## šŸ’¼ Hire the Author
213
447
  > Built by a developer available for **freelance and full-time opportunities**.
214
448
  > Got a project? Let's talk — šŸ“§ **[susheelhbti@gmail.com](mailto:susheelhbti@gmail.com)**
449
+
450
+ ---
451
+
452
+ ## Star History
453
+
454
+ [![Star History Chart](https://api.star-history.com/svg?repos=susheel/repomeld&type=Date)](https://star-history.com/#susheel/repomeld&Date)
455
+
456
+ ---
457
+
458
+ **Made with ā¤ļø for developers who need better context for AI tools**
package/bin/cli.js CHANGED
@@ -1,16 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require("fs");
3
+ const fs = require("fs").promises;
4
+ const fsSync = require("fs");
4
5
  const path = require("path");
5
6
  const { program } = require("commander");
7
+ const ignore = require("ignore");
8
+ const { isBinaryFile } = require("isbinaryfile");
9
+ const readline = require("readline");
10
+ const os = require("os");
6
11
 
7
12
  const VERSION = "1.0.0";
13
+ const PACKAGE_NAME = "repomeld";
14
+
15
+ // Normalize paths for cross-platform compatibility
16
+ const normalizePath = (p) => p.split(path.sep).join('/');
8
17
 
9
18
  const DEFAULT_IGNORE = [
10
19
  "node_modules",
11
20
  ".git",
12
21
  ".env",
13
- ".env.local",
22
+ ".env.local",
14
23
  ".env.production",
15
24
  ".DS_Store",
16
25
  "package-lock.json",
@@ -20,31 +29,10 @@ const DEFAULT_IGNORE = [
20
29
  ".nuxt",
21
30
  "dist",
22
31
  "build",
23
- ".cache",
24
- "package.json",
25
- "README.md",
32
+ ".cache"
33
+
26
34
  ];
27
35
 
28
- function loadIgnoreConfig() {
29
- const configPath = path.resolve(process.cwd(), "repomeld.ignore.json");
30
- const pkgDir = path.resolve(__dirname, "..", "repomeld.ignore.json");
31
- for (const loc of [configPath, pkgDir]) {
32
- if (fs.existsSync(loc)) {
33
- try {
34
- const data = JSON.parse(fs.readFileSync(loc, "utf8"));
35
- if (Array.isArray(data.ignore)) {
36
- return data.ignore;
37
- }
38
- } catch {
39
- console.warn(` āš ļø Could not parse ${loc}, using defaults.`);
40
- }
41
- }
42
- }
43
- return [];
44
- }
45
-
46
- const IGNORE_FROM_CONFIG = loadIgnoreConfig();
47
-
48
36
  const LANGUAGE_MAP = {
49
37
  js: "javascript", jsx: "javascript", ts: "typescript", tsx: "typescript",
50
38
  py: "python", rb: "ruby", java: "java", cpp: "cpp", c: "c",
@@ -54,54 +42,92 @@ const LANGUAGE_MAP = {
54
42
  toml: "toml", xml: "xml", sql: "sql", graphql: "graphql",
55
43
  };
56
44
 
57
- function getLanguage(filePath) {
58
- const ext = path.extname(filePath).slice(1).toLowerCase();
59
- return LANGUAGE_MAP[ext] || "";
60
- }
61
-
62
- function getAllFiles(dirPath, ignoreList, fileList = []) {
63
- let entries;
45
+ async function loadIgnoreConfig() {
46
+ const configPath = path.resolve(process.cwd(), "repomeld.ignore.json");
64
47
  try {
65
- entries = fs.readdirSync(dirPath, { withFileTypes: true });
48
+ const data = JSON.parse(await fs.readFile(configPath, "utf8"));
49
+ if (Array.isArray(data.ignore)) return data.ignore;
66
50
  } catch {
67
- return fileList;
51
+ // File doesn't exist or invalid JSON, use defaults
68
52
  }
53
+ return [];
54
+ }
69
55
 
70
- for (const entry of entries) {
71
- const fullPath = path.join(dirPath, entry.name);
72
- const relativePath = path.relative(process.cwd(), fullPath);
56
+ function getLanguage(filePath) {
57
+ const ext = path.extname(filePath).slice(1).toLowerCase();
58
+ return LANGUAGE_MAP[ext] || "";
59
+ }
73
60
 
74
- if (
75
- ignoreList.some(
76
- (ig) => entry.name === ig || relativePath.startsWith(ig + path.sep) || relativePath === ig
77
- )
78
- ) {
61
+ async function getAllFilesWithIgnore(dirPath, ig, forceIncludePatterns, rootDir = process.cwd(), progress = null) {
62
+ const fileList = [];
63
+ const stack = [{ dirPath, relativePath: '.' }];
64
+
65
+ while (stack.length) {
66
+ const { dirPath: currentDir, relativePath: currentRelative } = stack.pop();
67
+
68
+ let entries;
69
+ try {
70
+ entries = await fs.readdir(currentDir, { withFileTypes: true });
71
+ } catch (err) {
79
72
  continue;
80
73
  }
81
-
82
- if (entry.isDirectory()) {
83
- getAllFiles(fullPath, ignoreList, fileList);
84
- } else if (entry.isFile()) {
85
- fileList.push(fullPath);
74
+
75
+ for (const entry of entries) {
76
+ const fullPath = path.join(currentDir, entry.name);
77
+ const relativePath = path.join(currentRelative, entry.name);
78
+ const normalizedPath = normalizePath(relativePath);
79
+
80
+ // Check force-include first - these always go through
81
+ const isForceIncluded = forceIncludePatterns && forceIncludePatterns.some(pattern =>
82
+ normalizedPath.includes(pattern) || entry.name.includes(pattern)
83
+ );
84
+
85
+ // Only check ignore if not force-included
86
+ if (!isForceIncluded && ig.ignores(normalizedPath)) {
87
+ continue;
88
+ }
89
+
90
+ if (entry.isDirectory()) {
91
+ stack.push({ dirPath: fullPath, relativePath });
92
+ } else if (entry.isFile()) {
93
+ fileList.push(fullPath);
94
+ if (progress && fileList.length % 100 === 0) {
95
+ progress.update(fileList.length);
96
+ }
97
+ }
86
98
  }
87
99
  }
88
-
100
+
89
101
  return fileList;
90
102
  }
91
103
 
92
- function isBinaryFile(filePath) {
93
- try {
94
- const buffer = Buffer.alloc(512);
95
- const fd = fs.openSync(filePath, "r");
96
- const bytesRead = fs.readSync(fd, buffer, 0, 512, 0);
97
- fs.closeSync(fd);
98
- for (let i = 0; i < bytesRead; i++) {
99
- if (buffer[i] === 0) return true;
104
+ async function buildIgnoreFilter(options) {
105
+ const ig = ignore();
106
+
107
+ // Add default ignores
108
+ ig.add(DEFAULT_IGNORE);
109
+
110
+ // Add custom config ignores
111
+ const customIgnores = await loadIgnoreConfig();
112
+ ig.add(customIgnores);
113
+
114
+ // Add CLI ignores
115
+ if (options.ignore && options.ignore.length) {
116
+ ig.add(options.ignore);
117
+ }
118
+
119
+ // Add .gitignore patterns if not disabled
120
+ if (!options.noGitignore) {
121
+ try {
122
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
123
+ const gitignoreContent = await fs.readFile(gitignorePath, "utf8");
124
+ ig.add(gitignoreContent);
125
+ } catch {
126
+ // No .gitignore file, ignore silently
100
127
  }
101
- return false;
102
- } catch {
103
- return true;
104
128
  }
129
+
130
+ return { ig, forceInclude: options.forceInclude || null };
105
131
  }
106
132
 
107
133
  function matchesExtensions(filePath, exts) {
@@ -119,7 +145,47 @@ function matchesPattern(filePath, patterns) {
119
145
  function formatSize(bytes) {
120
146
  if (bytes < 1024) return `${bytes} B`;
121
147
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
122
- return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
148
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
149
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
150
+ }
151
+
152
+ function formatDuration(ms) {
153
+ if (ms < 1000) return `${ms}ms`;
154
+ const seconds = (ms / 1000).toFixed(1);
155
+ return `${seconds}s`;
156
+ }
157
+
158
+ class ProgressIndicator {
159
+ constructor(total, prefix = '') {
160
+ this.total = total;
161
+ this.prefix = prefix;
162
+ this.current = 0;
163
+ this.startTime = Date.now();
164
+ }
165
+
166
+ update(current) {
167
+ this.current = current;
168
+ this.render();
169
+ }
170
+
171
+ increment() {
172
+ this.current++;
173
+ this.render();
174
+ }
175
+
176
+ render() {
177
+ const percent = (this.current / this.total * 100).toFixed(1);
178
+ const elapsed = Date.now() - this.startTime;
179
+ const rate = this.current / (elapsed / 1000);
180
+ const eta = rate > 0 ? ((this.total - this.current) / rate).toFixed(0) : '?';
181
+
182
+ process.stdout.write(`\r${this.prefix} ${this.current}/${this.total} files (${percent}%) | ${formatDuration(elapsed)} elapsed | ETA: ${eta}s`);
183
+ }
184
+
185
+ finish() {
186
+ const elapsed = Date.now() - this.startTime;
187
+ console.log(`\r${this.prefix} āœ… Completed ${this.current}/${this.total} files in ${formatDuration(elapsed)}`);
188
+ }
123
189
  }
124
190
 
125
191
  function printBanner() {
@@ -133,10 +199,9 @@ function printBanner() {
133
199
  ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•`);
134
200
  }
135
201
 
136
- function buildHeader(style, relativePath, filePath, lineCount, showMeta) {
202
+ function buildHeader(style, relativePath, filePath, lineCount, showMeta, stats) {
137
203
  const lang = getLanguage(filePath);
138
- const size = formatSize(fs.statSync(filePath).size);
139
- const meta = showMeta ? ` [${lineCount} lines | ${size}${lang ? " | " + lang : ""}]` : "";
204
+ const meta = showMeta ? ` [${lineCount} lines | ${formatSize(stats.size)}${lang ? " | " + lang : ""}]` : "";
140
205
 
141
206
  if (style === "markdown") {
142
207
  return `\n## šŸ“„ ${relativePath}${meta}\n\n\`\`\`${lang}\n`;
@@ -144,7 +209,6 @@ function buildHeader(style, relativePath, filePath, lineCount, showMeta) {
144
209
  if (style === "minimal") {
145
210
  return `\n# ${relativePath}\n`;
146
211
  }
147
- // default: banner style
148
212
  const divider = "─".repeat(60);
149
213
  return `\n${divider}\n FILE: ${relativePath}${meta}\n${divider}\n\n`;
150
214
  }
@@ -164,30 +228,27 @@ function buildTableOfContents(files, cwd) {
164
228
  return toc;
165
229
  }
166
230
 
167
- /**
168
- * Given a desired output path like "repomeld_output.txt", returns a
169
- * numbered path that does not yet exist, e.g. "repomeld_output__2.txt".
170
- * If the base name doesn't exist yet, returns it unchanged.
171
- */
172
- function resolveOutputPath(desiredPath) {
173
- if (!fs.existsSync(desiredPath)) return desiredPath;
174
-
175
- const ext = path.extname(desiredPath);
176
- const base = desiredPath.slice(0, desiredPath.length - ext.length);
177
-
178
- let counter = 2;
179
- while (true) {
180
- const candidate = `${base}__${counter}${ext}`;
181
- if (!fs.existsSync(candidate)) return candidate;
182
- counter++;
231
+ async function resolveOutputPath(desiredPath) {
232
+ try {
233
+ await fs.access(desiredPath);
234
+ const ext = path.extname(desiredPath);
235
+ const base = desiredPath.slice(0, desiredPath.length - ext.length);
236
+ let counter = 2;
237
+ while (true) {
238
+ const candidate = `${base}__${counter}${ext}`;
239
+ try {
240
+ await fs.access(candidate);
241
+ counter++;
242
+ } catch {
243
+ return { path: candidate, number: counter };
244
+ }
245
+ }
246
+ } catch {
247
+ return { path: desiredPath, number: null };
183
248
  }
184
249
  }
185
250
 
186
- /**
187
- * Returns true if the given filePath looks like a repomeld output file
188
- * (matches the base name pattern with optional __N suffix).
189
- */
190
- function isRepomeldOutput(filePath, baseOutputName) {
251
+ async function isRepomeldOutput(filePath, baseOutputName) {
191
252
  const fileName = path.basename(filePath);
192
253
  const ext = path.extname(baseOutputName);
193
254
  const base = path.basename(baseOutputName, ext);
@@ -199,21 +260,59 @@ function escapeRegex(str) {
199
260
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
200
261
  }
201
262
 
202
- function repomeld(options) {
263
+ async function checkForUpdates() {
264
+ return new Promise((resolve) => {
265
+ const http = require('http');
266
+ const options = {
267
+ hostname: 'registry.npmjs.org',
268
+ path: `/${PACKAGE_NAME}/latest`,
269
+ method: 'GET',
270
+ timeout: 3000
271
+ };
272
+
273
+ const req = http.request(options, (res) => {
274
+ let data = '';
275
+ res.on('data', chunk => data += chunk);
276
+ res.on('end', () => {
277
+ try {
278
+ const json = JSON.parse(data);
279
+ if (json.version && json.version !== VERSION) {
280
+ resolve({ hasUpdate: true, latestVersion: json.version });
281
+ } else {
282
+ resolve({ hasUpdate: false });
283
+ }
284
+ } catch {
285
+ resolve({ hasUpdate: false });
286
+ }
287
+ });
288
+ });
289
+
290
+ req.on('error', () => resolve({ hasUpdate: false }));
291
+ req.on('timeout', () => {
292
+ req.destroy();
293
+ resolve({ hasUpdate: false });
294
+ });
295
+ req.end();
296
+ });
297
+ }
298
+
299
+ function showUpdateMessage(currentVersion, latestVersion) {
300
+ console.log(`\n${'═'.repeat(60)}`);
301
+ console.log(` ⭐ New version available: ${currentVersion} → ${latestVersion}`);
302
+ console.log(` šŸ“¦ Update with: npm install -g ${PACKAGE_NAME}@latest`);
303
+ console.log(`${'═'.repeat(60)}\n`);
304
+ }
305
+
306
+ async function repomeld(options) {
307
+ const startTime = Date.now();
203
308
  printBanner();
204
309
 
205
310
  const cwd = process.cwd();
206
-
207
- // Resolve a unique (non-colliding) output path
208
- const desiredOutput = path.resolve(cwd, options.output);
209
- const outputFile = resolveOutputPath(desiredOutput);
311
+
312
+ const { path: outputFile, number: outputNumber } = await resolveOutputPath(path.resolve(cwd, options.output));
210
313
  const outputBaseName = path.basename(options.output);
211
-
212
- const forceInclude = options.forceInclude || [];
213
- const rawIgnore = [...DEFAULT_IGNORE, ...IGNORE_FROM_CONFIG, ...(options.ignore || [])];
214
- const ignoreList = forceInclude.length
215
- ? rawIgnore.filter((ig) => !forceInclude.some((fi) => ig.includes(fi) || fi.includes(ig)))
216
- : rawIgnore;
314
+
315
+ const { ig, forceInclude } = await buildIgnoreFilter(options);
217
316
  const filterExts = options.ext || [];
218
317
  const maxFileSizeBytes = (parseFloat(options.maxSize) || 500) * 1024;
219
318
  const headerStyle = options.style || "banner";
@@ -228,91 +327,101 @@ function repomeld(options) {
228
327
  console.log(`\n šŸ“‚ Source : ${cwd}`);
229
328
  console.log(` šŸ“„ Output : ${path.relative(cwd, outputFile)}`);
230
329
  console.log(` šŸŽØ Style : ${headerStyle}`);
330
+ if (!options.noGitignore) console.log(` šŸ“ .gitignore respected`);
231
331
  if (filterExts.length) console.log(` šŸ” Filter : .${filterExts.join(", .")}`);
232
- if (forceInclude.length) console.log(` šŸ“Œ Force : ${forceInclude.join(", ")}`);
332
+ if (forceInclude && forceInclude.length) console.log(` šŸ“Œ Force : ${forceInclude.join(", ")}`);
233
333
  if (dryRun) console.log(` 🧪 Dry run : no file will be written`);
234
334
  console.log();
235
335
 
236
- let allFiles = getAllFiles(cwd, ignoreList);
336
+ console.log(` šŸ” Scanning files...`);
337
+ const scanStartTime = Date.now();
338
+ let allFiles = await getAllFilesWithIgnore(cwd, ig, forceInclude, cwd);
339
+ console.log(` āœ… Found ${allFiles.length} files in ${formatDuration(Date.now() - scanStartTime)}`);
237
340
 
238
- // Filter by extension
341
+ // Apply additional filters
239
342
  if (filterExts.length) {
240
- allFiles = allFiles.filter((f) => matchesExtensions(f, filterExts));
343
+ allFiles = allFiles.filter(f => matchesExtensions(f, filterExts));
241
344
  }
242
-
243
- // Include pattern filter
244
345
  if (include.length) {
245
- allFiles = allFiles.filter((f) => matchesPattern(f, include));
346
+ allFiles = allFiles.filter(f => matchesPattern(f, include));
246
347
  }
247
-
248
- // Exclude pattern filter
249
348
  if (exclude.length) {
250
- allFiles = allFiles.filter((f) => !matchesPattern(f, exclude));
349
+ allFiles = allFiles.filter(f => !matchesPattern(f, exclude));
251
350
  }
252
-
253
- // Remove ALL repomeld output files (current run + any previous numbered ones)
254
- allFiles = allFiles.filter((f) => !isRepomeldOutput(f, outputBaseName));
351
+
352
+ // Remove repomeld output files
353
+ const filteredFiles = [];
354
+ for (const file of allFiles) {
355
+ const isOutput = await isRepomeldOutput(file, outputBaseName);
356
+ if (!isOutput) filteredFiles.push(file);
357
+ }
358
+ allFiles = filteredFiles;
255
359
 
256
360
  if (allFiles.length === 0) {
257
361
  console.log(" āš ļø No matching files found.\n");
258
362
  return;
259
363
  }
260
364
 
365
+ console.log(` šŸ“ Processing ${allFiles.length} files...\n`);
366
+
261
367
  let combinedContent = "";
262
368
  let skipped = 0;
263
369
  let included = 0;
264
370
  let totalLines = 0;
265
371
  const includedFiles = [];
266
-
267
- for (const filePath of allFiles) {
372
+
373
+ const progress = new ProgressIndicator(allFiles.length, ' ');
374
+
375
+ for (let i = 0; i < allFiles.length; i++) {
376
+ const filePath = allFiles[i];
268
377
  const relativePath = path.relative(cwd, filePath);
269
-
270
- if (isBinaryFile(filePath)) {
271
- console.log(` ā­ Binary : ${relativePath}`);
378
+
379
+ progress.update(i + 1);
380
+
381
+ const isBinary = await isBinaryFile(filePath).catch(() => true);
382
+ if (isBinary) {
272
383
  skipped++;
273
384
  continue;
274
385
  }
275
-
276
- const stat = fs.statSync(filePath);
277
- if (stat.size > maxFileSizeBytes) {
278
- console.log(` ā­ Too large: ${relativePath} (${formatSize(stat.size)})`);
386
+
387
+ const stats = await fs.stat(filePath);
388
+ if (stats.size > maxFileSizeBytes) {
279
389
  skipped++;
280
390
  continue;
281
391
  }
282
-
392
+
283
393
  try {
284
- let content = fs.readFileSync(filePath, "utf8");
285
-
394
+ let content = await fs.readFile(filePath, "utf8");
395
+
286
396
  if (options.trim) {
287
397
  content = content.trim();
288
398
  }
289
-
399
+
290
400
  if (linesBefore > 0 || linesAfter > 0) {
291
401
  const lines = content.split("\n");
292
- const start = linesBefore;
293
- const end = linesAfter > 0 ? lines.length - linesAfter : lines.length;
402
+ const start = Math.min(linesBefore, lines.length);
403
+ const end = linesAfter > 0 ? Math.max(0, lines.length - linesAfter) : lines.length;
294
404
  content = lines.slice(start, end).join("\n");
295
405
  }
296
-
406
+
297
407
  const lineCount = content.split("\n").length;
298
408
  totalLines += lineCount;
299
409
  includedFiles.push(filePath);
300
-
301
- combinedContent += buildHeader(headerStyle, relativePath, filePath, lineCount, showMeta);
410
+
411
+ combinedContent += buildHeader(headerStyle, relativePath, filePath, lineCount, showMeta, stats);
302
412
  combinedContent += content;
303
413
  combinedContent += buildFooter(headerStyle);
304
-
305
- console.log(` āœ… ${relativePath}`);
414
+
306
415
  included++;
307
416
  } catch (err) {
308
- console.log(` āŒ Error: ${relativePath} — ${err.message}`);
309
417
  skipped++;
310
418
  }
311
419
  }
312
-
420
+
421
+ progress.finish();
422
+
313
423
  // Build final output
314
424
  let finalOutput = "";
315
-
316
425
  const timestamp = new Date().toISOString();
317
426
  finalOutput += `# Generated by repomeld v${VERSION}\n`;
318
427
  finalOutput += `# Date : ${timestamp}\n`;
@@ -320,19 +429,20 @@ function repomeld(options) {
320
429
  finalOutput += `# Files : ${included}\n`;
321
430
  finalOutput += `# Lines : ${totalLines}\n`;
322
431
  finalOutput += `# Author : susheelhbti@gmail.com — available for freelance & full-time work\n\n`;
323
-
324
- if (showToc) {
432
+
433
+ if (showToc && includedFiles.length > 0) {
325
434
  finalOutput += buildTableOfContents(includedFiles, cwd);
326
435
  }
327
-
436
+
328
437
  finalOutput += combinedContent;
329
-
330
- if (!dryRun) {
331
- fs.writeFileSync(outputFile, finalOutput, "utf8");
438
+
439
+ if (!dryRun && includedFiles.length > 0) {
440
+ await fs.writeFile(outputFile, finalOutput, "utf8");
332
441
  }
333
-
442
+
334
443
  const outputSize = formatSize(Buffer.byteLength(finalOutput, "utf8"));
335
-
444
+ const totalTime = Date.now() - startTime;
445
+
336
446
  console.log(`
337
447
  ✨ repomeld complete!
338
448
  ─────────────────────────────────────────────────
@@ -340,10 +450,19 @@ function repomeld(options) {
340
450
  ā­ Skipped : ${skipped} files
341
451
  šŸ“ Lines : ${totalLines}
342
452
  šŸ’¾ Size : ${outputSize}
453
+ ā±ļø Time : ${formatDuration(totalTime)}
343
454
  šŸ“„ Output : ${path.relative(cwd, outputFile)}${dryRun ? " (dry run — not written)" : ""}
344
- ─────────────────────────────────────────────────
345
- šŸ’¼ Need a developer? susheelhbti@gmail.com
346
- `);
455
+ ─────────────────────────────────────────────────`);
456
+
457
+ console.log(`\n šŸ’¼ Need a developer? susheelhbti@gmail.com`);
458
+
459
+ // Check for updates (non-blocking, no prompt)
460
+ if (!options.noUpdateCheck) {
461
+ const updateInfo = await checkForUpdates();
462
+ if (updateInfo.hasUpdate) {
463
+ showUpdateMessage(VERSION, updateInfo.latestVersion);
464
+ }
465
+ }
347
466
  }
348
467
 
349
468
  // ─── CLI Definition ───────────────────────────────────────────
@@ -353,30 +472,31 @@ program
353
472
  .description("Meld your entire repo into a single file — perfect for AI context, code reviews & sharing")
354
473
  .version(VERSION)
355
474
 
356
- // Output
357
- .option("-o, --output <filename>", "Output file name", "repomeld_output.txt")
358
-
359
- // Filtering
360
- .option("-e, --ext <exts...>", "Only include specific extensions e.g. --ext js ts jsx")
361
- .option("--include <patterns...>", "Only include files matching patterns e.g. --include src/")
362
- .option("--exclude <patterns...>", "Exclude files matching patterns e.g. --exclude test spec")
363
- .option("-i, --ignore <names...>", "Extra folders/files to ignore e.g. --ignore dist .next")
364
- .option("--force-include <names...>", "Force-include files ignored by default e.g. --force-include vendor")
365
- .option("--max-size <kb>", "Skip files larger than N KB (default 500)","500")
366
-
367
- // Formatting
368
- .option("-s, --style <style>", "Header style: banner | markdown | minimal (default: banner)", "banner")
369
- .option("--no-toc", "Disable table of contents")
370
- .option("--no-meta", "Hide file metadata (lines, size, lang)")
371
- .option("--trim", "Trim leading/trailing whitespace per file")
372
-
373
- // Advanced
374
- .option("--lines-before <n>", "Skip first N lines of each file")
375
- .option("--lines-after <n>", "Skip last N lines of each file")
376
- .option("--dry-run", "Preview what would be included — don't write output")
377
-
378
- .action((options) => {
379
- repomeld(options);
475
+ .option("-o, --output <filename>", "Output file name", "repomeld_output.txt")
476
+ .option("-e, --ext <exts...>", "Only include specific extensions")
477
+ .option("--include <patterns...>", "Only include files matching patterns")
478
+ .option("--exclude <patterns...>", "Exclude files matching patterns")
479
+ .option("-i, --ignore <names...>", "Extra folders/files to ignore")
480
+ .option("--force-include <names...>", "Force-include files even if ignored")
481
+ .option("--max-size <kb>", "Skip files larger than N KB", "500")
482
+ .option("--no-gitignore", "Ignore .gitignore file")
483
+ .option("-s, --style <style>", "Header style: banner | markdown | minimal", "banner")
484
+ .option("--no-toc", "Disable table of contents")
485
+ .option("--no-meta", "Hide file metadata")
486
+ .option("--trim", "Trim leading/trailing whitespace per file")
487
+ .option("--lines-before <n>", "Skip first N lines of each file", parseInt)
488
+ .option("--lines-after <n>", "Skip last N lines of each file", parseInt)
489
+ .option("--dry-run", "Preview without writing output")
490
+ .option("--no-update-check", "Skip checking for updates")
491
+
492
+ .action(async (options) => {
493
+ try {
494
+ await repomeld(options);
495
+ } catch (error) {
496
+ console.error(`\n āŒ Error: ${error.message}`);
497
+ if (process.env.DEBUG) console.error(error);
498
+ process.exit(1);
499
+ }
380
500
  });
381
501
 
382
- program.parse(process.argv);
502
+ program.parse(process.argv);
package/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "repomeld",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "description": "Meld your entire repo into a single file — perfect for AI context, code reviews & sharing",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
7
7
  "repomeld": "bin/cli.js"
8
8
  },
9
9
  "scripts": {
10
- "start": "node bin/cli.js"
10
+ "start": "node bin/cli.js",
11
+ "test": "node bin/cli.js --dry-run",
12
+ "prepublishOnly": "npm run test",
13
+ "postinstall": "node -e \"console.log('\\nšŸ“¦ repomeld installed successfully! Run: repomeld --help\\n')\""
11
14
  },
12
15
  "keywords": [
13
16
  "cli",
@@ -18,15 +21,38 @@
18
21
  "concat",
19
22
  "ai-context",
20
23
  "code-review",
21
- "repomeld"
24
+ "repomeld",
25
+ "git",
26
+ "repository"
22
27
  ],
23
- "author": "",
28
+ "author": "Susheel <susheelhbti@gmail.com>",
24
29
  "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/sakshsky/repomeld.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/sakshsky/repomeld/issues"
36
+ },
37
+ "homepage": "https://github.com/sakshsky/repomeld#readme",
25
38
  "dependencies": {
26
- "commander": "^11.0.0"
27
-
39
+ "commander": "^11.1.0",
40
+ "ignore": "^5.3.2",
41
+ "isbinaryfile": "^5.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "eslint": "^8.56.0",
45
+ "prettier": "^3.1.1"
28
46
  },
29
47
  "engines": {
30
48
  "node": ">=14.0.0"
49
+ },
50
+ "files": [
51
+ "bin/",
52
+ "README.md",
53
+ "LICENSE"
54
+ ],
55
+ "publishConfig": {
56
+ "access": "public"
31
57
  }
32
58
  }