repomeld 2.0.5 ā 3.0.0
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/README.md +51 -116
- package/bin/cli.js +16 -480
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -15,16 +15,18 @@
|
|
|
15
15
|
|
|
16
16
|
## ⨠Features
|
|
17
17
|
|
|
18
|
-
- š **Fast & Efficient** - Async scanning with real-time progress
|
|
18
|
+
- š **Fast & Efficient** - Async scanning with real-time progress and binary caching
|
|
19
19
|
- šØ **Multiple Styles** - Banner, Markdown, or Minimal output
|
|
20
20
|
- š **Smart Filtering** - Extension, pattern, and size-based filtering
|
|
21
21
|
- š **Gitignore Support** - Respects your .gitignore rules automatically
|
|
22
|
-
- š¾ **Binary Detection** -
|
|
22
|
+
- š¾ **Binary Detection** - Intelligent caching for binary file detection
|
|
23
23
|
- š¦ **Single File Output** - Perfect for AI context windows
|
|
24
24
|
- š **Auto-Numbering** - Never overwrites existing files
|
|
25
|
-
- šæ **Zip Backup** - Creates
|
|
26
|
-
- š **Update Notifications** -
|
|
25
|
+
- šæ **Zip Backup** - Creates auto-numbered backups in `repomeld_zips/` folder
|
|
26
|
+
- š **Update Notifications** - Non-intrusive version checking
|
|
27
27
|
- šÆ **Force Include** - Override ignore rules when needed
|
|
28
|
+
- š **Dependency Graph** - Optional Mermaid diagram of file dependencies
|
|
29
|
+
- š **Cross-Platform** - Works perfectly on Windows, macOS, and Linux
|
|
28
30
|
|
|
29
31
|
---
|
|
30
32
|
|
|
@@ -49,7 +51,7 @@ cd your-project
|
|
|
49
51
|
repomeld
|
|
50
52
|
```
|
|
51
53
|
|
|
52
|
-
That's it. repomeld walks your project, respects `.gitignore`, skips binary files, and writes everything into one readable file.
|
|
54
|
+
That's it. repomeld walks your project, respects `.gitignore`, skips binary files, and writes everything into one readable file with optional dependency graphs.
|
|
53
55
|
|
|
54
56
|
---
|
|
55
57
|
|
|
@@ -61,9 +63,13 @@ Every time you run repomeld it creates a **new numbered file** so previous runs
|
|
|
61
63
|
repomeld_output.txt ā first run
|
|
62
64
|
repomeld_output__2.txt ā second run
|
|
63
65
|
repomeld_output__3.txt ā third run
|
|
66
|
+
repomeld_zips/ ā backup folder
|
|
67
|
+
āāā repomeld_output.zip
|
|
68
|
+
āāā repomeld_output__2.zip
|
|
69
|
+
āāā repomeld_output__3.zip
|
|
64
70
|
```
|
|
65
71
|
|
|
66
|
-
All previous output files are
|
|
72
|
+
All previous output files and zips are **automatically excluded** from the next run ā so you'll never get repomeld's own output included inside itself.
|
|
67
73
|
|
|
68
74
|
---
|
|
69
75
|
|
|
@@ -191,16 +197,20 @@ repomeld automatically skips these so your output stays clean:
|
|
|
191
197
|
|
|
192
198
|
### Method 1: repomeld.ignore.json
|
|
193
199
|
|
|
194
|
-
Create a `repomeld.ignore.json` in your project root:
|
|
200
|
+
Create a `repomeld.ignore.json` in your project root for comprehensive ignore patterns:
|
|
195
201
|
|
|
196
202
|
```json
|
|
197
203
|
{
|
|
204
|
+
"_comment": "repomeld.ignore.json ā auto-ignored files and folders",
|
|
198
205
|
"ignore": [
|
|
199
206
|
"coverage",
|
|
200
207
|
"logs",
|
|
201
208
|
"tmp",
|
|
202
209
|
"*.min.js",
|
|
203
|
-
"**/generated/**"
|
|
210
|
+
"**/generated/**",
|
|
211
|
+
"vendor/**/bootstrap*",
|
|
212
|
+
"**/jquery*",
|
|
213
|
+
"**/fontawesome*"
|
|
204
214
|
]
|
|
205
215
|
}
|
|
206
216
|
```
|
|
@@ -223,19 +233,19 @@ repomeld --ignore temp logs "*.tmp"
|
|
|
223
233
|
|
|
224
234
|
## Backup Zip Files
|
|
225
235
|
|
|
226
|
-
When repomeld runs, it automatically creates a backup zip file in the `
|
|
236
|
+
When repomeld runs, it automatically creates a backup zip file in the `repomeld_zips/` folder:
|
|
227
237
|
|
|
228
238
|
```
|
|
229
239
|
repomeld_output.txt
|
|
230
|
-
|
|
240
|
+
repomeld_zips/
|
|
231
241
|
āāā repomeld_output.zip ā contains all included files + output
|
|
232
242
|
|
|
233
243
|
repomeld_output__2.txt
|
|
234
|
-
|
|
244
|
+
repomeld_zips/
|
|
235
245
|
āāā repomeld_output__2.zip ā corresponding backup
|
|
236
246
|
|
|
237
247
|
repomeld_output__3.txt
|
|
238
|
-
|
|
248
|
+
repomeld_zips/
|
|
239
249
|
āāā repomeld_output__3.zip ā and so on...
|
|
240
250
|
```
|
|
241
251
|
|
|
@@ -246,74 +256,18 @@ The zip file contains:
|
|
|
246
256
|
To disable backups: `repomeld --no-backup`
|
|
247
257
|
|
|
248
258
|
---
|
|
259
|
+
|
|
249
260
|
|
|
250
|
-
##
|
|
251
|
-
|
|
252
|
-
Each run produces a file like this:
|
|
253
|
-
|
|
254
|
-
```
|
|
255
|
-
# Generated by repomeld v2.0.4
|
|
256
|
-
# Date : 2025-04-23T10:00:00.000Z
|
|
257
|
-
# Source : /your/project
|
|
258
|
-
# Files : 12
|
|
259
|
-
# Lines : 847
|
|
260
|
-
|
|
261
|
-
TABLE OF CONTENTS
|
|
262
|
-
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
263
|
-
1. src/index.js
|
|
264
|
-
2. src/utils.js
|
|
265
|
-
3. src/config.js
|
|
266
|
-
...
|
|
267
|
-
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
268
|
-
|
|
269
|
-
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
270
|
-
FILE: src/index.js [120 lines | 3.2 KB | javascript]
|
|
271
|
-
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
272
|
-
|
|
273
|
-
... file contents ...
|
|
274
|
-
```
|
|
275
|
-
|
|
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
|
|
261
|
+
## Performance Optimizations
|
|
311
262
|
|
|
312
263
|
repomeld is optimized for large codebases:
|
|
313
|
-
|
|
314
|
-
-
|
|
315
|
-
-
|
|
316
|
-
-
|
|
264
|
+
|
|
265
|
+
- **Async file scanning** - Non-blocking operations
|
|
266
|
+
- **Binary caching** - Extension-based detection cache
|
|
267
|
+
- **Real-time progress** - Shows ETA and completion percentage
|
|
268
|
+
- **Memory efficient** - Processes files in streams
|
|
269
|
+
- **Smart filtering** - Early filtering to reduce processing
|
|
270
|
+
- **Handles 50,000+ files** - Tested on large monorepos
|
|
317
271
|
|
|
318
272
|
Example output:
|
|
319
273
|
```
|
|
@@ -322,8 +276,13 @@ Example output:
|
|
|
322
276
|
|
|
323
277
|
š Processing 2453 files...
|
|
324
278
|
|
|
325
|
-
|
|
326
|
-
ā
Completed 2453/2453 files in 4.7s
|
|
279
|
+
Processing: 1245/2453 files (50.7%) | 2.3s elapsed
|
|
280
|
+
Processing: ā
Completed 2453/2453 files in 4.7s
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Memory warning** for extremely large repos (>20,000 files):
|
|
284
|
+
```
|
|
285
|
+
ā ļø Large repository detected (~25347 files). Memory usage may be high.
|
|
327
286
|
```
|
|
328
287
|
|
|
329
288
|
---
|
|
@@ -355,29 +314,15 @@ repomeld --ext md --include docs --style markdown --output documentation.md
|
|
|
355
314
|
repomeld --include feature-name --ext js css --output feature-context.txt
|
|
356
315
|
```
|
|
357
316
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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.
|
|
317
|
+
### šŗļø Dependency Analysis
|
|
318
|
+
```bash
|
|
319
|
+
repomeld --include src --style markdown --output analysis.md
|
|
320
|
+
# Then render the Mermaid graph in the output
|
|
321
|
+
```
|
|
373
322
|
|
|
374
|
-
|
|
375
|
-
A: Use `--dry-run` to preview without writing.
|
|
323
|
+
---
|
|
376
324
|
|
|
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
325
|
|
|
380
|
-
---
|
|
381
326
|
|
|
382
327
|
## Development
|
|
383
328
|
|
|
@@ -395,8 +340,14 @@ npm start -- --dry-run
|
|
|
395
340
|
# Link for global testing
|
|
396
341
|
npm link
|
|
397
342
|
repomeld --help
|
|
343
|
+
|
|
344
|
+
# Run tests
|
|
345
|
+
npm test
|
|
398
346
|
```
|
|
399
347
|
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
|
|
400
351
|
---
|
|
401
352
|
|
|
402
353
|
## Contributing
|
|
@@ -411,23 +362,7 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|
|
411
362
|
|
|
412
363
|
---
|
|
413
364
|
|
|
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
365
|
|
|
427
|
-
### v1.0.0
|
|
428
|
-
- Initial release with basic functionality
|
|
429
|
-
|
|
430
|
-
---
|
|
431
366
|
|
|
432
367
|
## License
|
|
433
368
|
|
|
@@ -455,4 +390,4 @@ MIT Ā© [Susheel](mailto:susheelhbti@gmail.com)
|
|
|
455
390
|
|
|
456
391
|
---
|
|
457
392
|
|
|
458
|
-
**Made with ā¤ļø for developers who need better context for AI tools**
|
|
393
|
+
**Made with ā¤ļø for developers who need better context for AI tools**
|
package/bin/cli.js
CHANGED
|
@@ -1,499 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const fs = require("fs").promises;
|
|
4
|
-
const fsSync = require("fs");
|
|
5
|
-
const path = require("path");
|
|
6
3
|
const { program } = require("commander");
|
|
7
|
-
const
|
|
8
|
-
const {
|
|
9
|
-
const readline = require("readline");
|
|
10
|
-
const os = require("os");
|
|
11
|
-
|
|
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('/');
|
|
17
|
-
|
|
18
|
-
const DEFAULT_IGNORE = [
|
|
19
|
-
"node_modules",
|
|
20
|
-
".git",
|
|
21
|
-
".env",
|
|
22
|
-
".env.local",
|
|
23
|
-
".env.production",
|
|
24
|
-
".DS_Store",
|
|
25
|
-
"package-lock.json",
|
|
26
|
-
"yarn.lock",
|
|
27
|
-
"pnpm-lock.yaml",
|
|
28
|
-
".next",
|
|
29
|
-
".nuxt",
|
|
30
|
-
"dist",
|
|
31
|
-
"build",
|
|
32
|
-
".cache"
|
|
33
|
-
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
const LANGUAGE_MAP = {
|
|
37
|
-
js: "javascript", jsx: "javascript", ts: "typescript", tsx: "typescript",
|
|
38
|
-
py: "python", rb: "ruby", java: "java", cpp: "cpp", c: "c",
|
|
39
|
-
cs: "csharp", go: "go", rs: "rust", php: "php", swift: "swift",
|
|
40
|
-
kt: "kotlin", html: "html", css: "css", scss: "scss", json: "json",
|
|
41
|
-
yaml: "yaml", yml: "yaml", md: "markdown", sh: "bash", bash: "bash",
|
|
42
|
-
toml: "toml", xml: "xml", sql: "sql", graphql: "graphql",
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
async function loadIgnoreConfig() {
|
|
46
|
-
const configPath = path.resolve(process.cwd(), "repomeld.ignore.json");
|
|
47
|
-
try {
|
|
48
|
-
const data = JSON.parse(await fs.readFile(configPath, "utf8"));
|
|
49
|
-
if (Array.isArray(data.ignore)) return data.ignore;
|
|
50
|
-
} catch {
|
|
51
|
-
// File doesn't exist or invalid JSON, use defaults
|
|
52
|
-
}
|
|
53
|
-
return [];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function getLanguage(filePath) {
|
|
57
|
-
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
58
|
-
return LANGUAGE_MAP[ext] || "";
|
|
59
|
-
}
|
|
60
|
-
|
|
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) {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
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
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return fileList;
|
|
102
|
-
}
|
|
103
|
-
|
|
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
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return { ig, forceInclude: options.forceInclude || null };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function matchesExtensions(filePath, exts) {
|
|
134
|
-
if (!exts || exts.length === 0) return true;
|
|
135
|
-
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
136
|
-
return exts.map((e) => e.replace(/^\./, "").toLowerCase()).includes(ext);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function matchesPattern(filePath, patterns) {
|
|
140
|
-
if (!patterns || patterns.length === 0) return false;
|
|
141
|
-
const rel = path.relative(process.cwd(), filePath);
|
|
142
|
-
return patterns.some((p) => rel.includes(p) || path.basename(filePath).includes(p));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function formatSize(bytes) {
|
|
146
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
147
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
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
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function printBanner() {
|
|
192
|
-
console.log(`
|
|
193
|
-
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
194
|
-
ā repomeld v${VERSION} ā
|
|
195
|
-
ā Meld your repo into one file š„ ā
|
|
196
|
-
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā£
|
|
197
|
-
ā š¼ Author available for freelance & full-time work ā
|
|
198
|
-
ā š§ susheelhbti@gmail.com ā
|
|
199
|
-
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function buildHeader(style, relativePath, filePath, lineCount, showMeta, stats) {
|
|
203
|
-
const lang = getLanguage(filePath);
|
|
204
|
-
const meta = showMeta ? ` [${lineCount} lines | ${formatSize(stats.size)}${lang ? " | " + lang : ""}]` : "";
|
|
205
|
-
|
|
206
|
-
if (style === "markdown") {
|
|
207
|
-
return `\n## š ${relativePath}${meta}\n\n\`\`\`${lang}\n`;
|
|
208
|
-
}
|
|
209
|
-
if (style === "minimal") {
|
|
210
|
-
return `\n# ${relativePath}\n`;
|
|
211
|
-
}
|
|
212
|
-
const divider = "ā".repeat(60);
|
|
213
|
-
return `\n${divider}\n FILE: ${relativePath}${meta}\n${divider}\n\n`;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function buildFooter(style) {
|
|
217
|
-
if (style === "markdown") return "\n```\n";
|
|
218
|
-
return "\n";
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function buildTableOfContents(files, cwd) {
|
|
222
|
-
let toc = "TABLE OF CONTENTS\n" + "ā".repeat(60) + "\n";
|
|
223
|
-
files.forEach((f, i) => {
|
|
224
|
-
const rel = path.relative(cwd, f);
|
|
225
|
-
toc += ` ${String(i + 1).padStart(3, " ")}. ${rel}\n`;
|
|
226
|
-
});
|
|
227
|
-
toc += "ā".repeat(60) + "\n\n";
|
|
228
|
-
return toc;
|
|
229
|
-
}
|
|
230
|
-
|
|
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 };
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async function isRepomeldOutput(filePath, baseOutputName) {
|
|
252
|
-
const fileName = path.basename(filePath);
|
|
253
|
-
const ext = path.extname(baseOutputName);
|
|
254
|
-
const base = path.basename(baseOutputName, ext);
|
|
255
|
-
const pattern = new RegExp(`^${escapeRegex(base)}(__\\d+)?${escapeRegex(ext)}$`);
|
|
256
|
-
return pattern.test(fileName);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function escapeRegex(str) {
|
|
260
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
261
|
-
}
|
|
262
|
-
|
|
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();
|
|
308
|
-
printBanner();
|
|
309
|
-
|
|
310
|
-
const cwd = process.cwd();
|
|
311
|
-
|
|
312
|
-
const { path: outputFile, number: outputNumber } = await resolveOutputPath(path.resolve(cwd, options.output));
|
|
313
|
-
const outputBaseName = path.basename(options.output);
|
|
314
|
-
|
|
315
|
-
const { ig, forceInclude } = await buildIgnoreFilter(options);
|
|
316
|
-
const filterExts = options.ext || [];
|
|
317
|
-
const maxFileSizeBytes = (parseFloat(options.maxSize) || 500) * 1024;
|
|
318
|
-
const headerStyle = options.style || "banner";
|
|
319
|
-
const showMeta = !options.noMeta;
|
|
320
|
-
const showToc = !options.noToc;
|
|
321
|
-
const dryRun = options.dryRun || false;
|
|
322
|
-
const include = options.include || [];
|
|
323
|
-
const exclude = options.exclude || [];
|
|
324
|
-
const linesBefore = parseInt(options.linesBefore) || 0;
|
|
325
|
-
const linesAfter = parseInt(options.linesAfter) || 0;
|
|
326
|
-
|
|
327
|
-
console.log(`\n š Source : ${cwd}`);
|
|
328
|
-
console.log(` š Output : ${path.relative(cwd, outputFile)}`);
|
|
329
|
-
console.log(` šØ Style : ${headerStyle}`);
|
|
330
|
-
if (!options.noGitignore) console.log(` š .gitignore respected`);
|
|
331
|
-
if (filterExts.length) console.log(` š Filter : .${filterExts.join(", .")}`);
|
|
332
|
-
if (forceInclude && forceInclude.length) console.log(` š Force : ${forceInclude.join(", ")}`);
|
|
333
|
-
if (dryRun) console.log(` š§Ŗ Dry run : no file will be written`);
|
|
334
|
-
console.log();
|
|
335
|
-
|
|
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)}`);
|
|
340
|
-
|
|
341
|
-
// Apply additional filters
|
|
342
|
-
if (filterExts.length) {
|
|
343
|
-
allFiles = allFiles.filter(f => matchesExtensions(f, filterExts));
|
|
344
|
-
}
|
|
345
|
-
if (include.length) {
|
|
346
|
-
allFiles = allFiles.filter(f => matchesPattern(f, include));
|
|
347
|
-
}
|
|
348
|
-
if (exclude.length) {
|
|
349
|
-
allFiles = allFiles.filter(f => !matchesPattern(f, exclude));
|
|
350
|
-
}
|
|
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;
|
|
359
|
-
|
|
360
|
-
if (allFiles.length === 0) {
|
|
361
|
-
console.log(" ā ļø No matching files found.\n");
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
console.log(` š Processing ${allFiles.length} files...\n`);
|
|
366
|
-
|
|
367
|
-
let combinedContent = "";
|
|
368
|
-
let skipped = 0;
|
|
369
|
-
let included = 0;
|
|
370
|
-
let totalLines = 0;
|
|
371
|
-
const includedFiles = [];
|
|
372
|
-
|
|
373
|
-
const progress = new ProgressIndicator(allFiles.length, ' ');
|
|
374
|
-
|
|
375
|
-
for (let i = 0; i < allFiles.length; i++) {
|
|
376
|
-
const filePath = allFiles[i];
|
|
377
|
-
const relativePath = path.relative(cwd, filePath);
|
|
378
|
-
|
|
379
|
-
progress.update(i + 1);
|
|
380
|
-
|
|
381
|
-
const isBinary = await isBinaryFile(filePath).catch(() => true);
|
|
382
|
-
if (isBinary) {
|
|
383
|
-
skipped++;
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const stats = await fs.stat(filePath);
|
|
388
|
-
if (stats.size > maxFileSizeBytes) {
|
|
389
|
-
skipped++;
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
try {
|
|
394
|
-
let content = await fs.readFile(filePath, "utf8");
|
|
395
|
-
|
|
396
|
-
if (options.trim) {
|
|
397
|
-
content = content.trim();
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (linesBefore > 0 || linesAfter > 0) {
|
|
401
|
-
const lines = content.split("\n");
|
|
402
|
-
const start = Math.min(linesBefore, lines.length);
|
|
403
|
-
const end = linesAfter > 0 ? Math.max(0, lines.length - linesAfter) : lines.length;
|
|
404
|
-
content = lines.slice(start, end).join("\n");
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const lineCount = content.split("\n").length;
|
|
408
|
-
totalLines += lineCount;
|
|
409
|
-
includedFiles.push(filePath);
|
|
410
|
-
|
|
411
|
-
combinedContent += buildHeader(headerStyle, relativePath, filePath, lineCount, showMeta, stats);
|
|
412
|
-
combinedContent += content;
|
|
413
|
-
combinedContent += buildFooter(headerStyle);
|
|
414
|
-
|
|
415
|
-
included++;
|
|
416
|
-
} catch (err) {
|
|
417
|
-
skipped++;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
progress.finish();
|
|
422
|
-
|
|
423
|
-
// Build final output
|
|
424
|
-
let finalOutput = "";
|
|
425
|
-
const timestamp = new Date().toISOString();
|
|
426
|
-
finalOutput += `# Generated by repomeld v${VERSION}\n`;
|
|
427
|
-
finalOutput += `# Date : ${timestamp}\n`;
|
|
428
|
-
finalOutput += `# Source : ${cwd}\n`;
|
|
429
|
-
finalOutput += `# Files : ${included}\n`;
|
|
430
|
-
finalOutput += `# Lines : ${totalLines}\n`;
|
|
431
|
-
finalOutput += `# Author : susheelhbti@gmail.com ā available for freelance & full-time work\n\n`;
|
|
432
|
-
|
|
433
|
-
if (showToc && includedFiles.length > 0) {
|
|
434
|
-
finalOutput += buildTableOfContents(includedFiles, cwd);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
finalOutput += combinedContent;
|
|
438
|
-
|
|
439
|
-
if (!dryRun && includedFiles.length > 0) {
|
|
440
|
-
await fs.writeFile(outputFile, finalOutput, "utf8");
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const outputSize = formatSize(Buffer.byteLength(finalOutput, "utf8"));
|
|
444
|
-
const totalTime = Date.now() - startTime;
|
|
445
|
-
|
|
446
|
-
console.log(`
|
|
447
|
-
⨠repomeld complete!
|
|
448
|
-
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
449
|
-
ā
Included : ${included} files
|
|
450
|
-
ā Skipped : ${skipped} files
|
|
451
|
-
š Lines : ${totalLines}
|
|
452
|
-
š¾ Size : ${outputSize}
|
|
453
|
-
ā±ļø Time : ${formatDuration(totalTime)}
|
|
454
|
-
š Output : ${path.relative(cwd, outputFile)}${dryRun ? " (dry run ā not written)" : ""}
|
|
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
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// āāā CLI Definition āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
4
|
+
const { repomeld } = require("../src/index");
|
|
5
|
+
const { VERSION } = require("../src/utils/constants");
|
|
469
6
|
|
|
470
7
|
program
|
|
471
8
|
.name("repomeld")
|
|
472
|
-
.description("Meld your entire repo into a single file ā perfect for AI context
|
|
9
|
+
.description("Meld your entire repo into a single file ā perfect for AI context & code reviews")
|
|
473
10
|
.version(VERSION)
|
|
474
|
-
|
|
475
|
-
.option("-o, --output <filename>", "Output file name", "repomeld_output.txt")
|
|
11
|
+
.option("-o, --output <filename>", "Output filename", "repomeld_output.txt")
|
|
476
12
|
.option("-e, --ext <exts...>", "Only include specific extensions")
|
|
477
|
-
.option("--include <patterns...>", "
|
|
13
|
+
.option("--include <patterns...>", "Force include files matching patterns")
|
|
478
14
|
.option("--exclude <patterns...>", "Exclude files matching patterns")
|
|
479
|
-
.option("-i, --ignore <names...>", "
|
|
480
|
-
.option("--force-include <names...>", "Force
|
|
481
|
-
.option("--max-size <kb>", "
|
|
482
|
-
.option("--no-gitignore", "
|
|
15
|
+
.option("-i, --ignore <names...>", "Additional ignore patterns")
|
|
16
|
+
.option("--force-include <names...>", "Force include even if ignored")
|
|
17
|
+
.option("--max-size <kb>", "Maximum file size in KB", "500")
|
|
18
|
+
.option("--no-gitignore", "Don't respect .gitignore")
|
|
483
19
|
.option("-s, --style <style>", "Header style: banner | markdown | minimal", "banner")
|
|
484
20
|
.option("--no-toc", "Disable table of contents")
|
|
485
21
|
.option("--no-meta", "Hide file metadata")
|
|
486
|
-
.option("--trim", "Trim
|
|
487
|
-
.option("--lines-before <n>", "Skip first N lines
|
|
488
|
-
.option("--lines-after <n>", "Skip last N lines
|
|
489
|
-
.option("--dry-run", "Preview without writing
|
|
490
|
-
.option("--no-
|
|
491
|
-
|
|
22
|
+
.option("--trim", "Trim whitespace from each file")
|
|
23
|
+
.option("--lines-before <n>", "Skip first N lines", parseInt)
|
|
24
|
+
.option("--lines-after <n>", "Skip last N lines", parseInt)
|
|
25
|
+
.option("--dry-run", "Preview without writing files")
|
|
26
|
+
.option("--no-backup", "Skip backup zip creation")
|
|
27
|
+
.option("--no-update-check", "Skip update check")
|
|
492
28
|
.action(async (options) => {
|
|
493
29
|
try {
|
|
494
30
|
await repomeld(options);
|
|
495
31
|
} catch (error) {
|
|
496
|
-
console.error(`\n
|
|
32
|
+
console.error(`\nā Error: ${error.message}`);
|
|
497
33
|
if (process.env.DEBUG) console.error(error);
|
|
498
34
|
process.exit(1);
|
|
499
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "repomeld",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
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": {
|
|
@@ -36,9 +36,10 @@
|
|
|
36
36
|
},
|
|
37
37
|
"homepage": "https://github.com/sakshsky/repomeld#readme",
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"archiver": "^7.0.1",
|
|
39
40
|
"commander": "^11.1.0",
|
|
40
41
|
"ignore": "^5.3.2",
|
|
41
|
-
"isbinaryfile": "^5.0.
|
|
42
|
+
"isbinaryfile": "^5.0.7"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"eslint": "^8.56.0",
|