repomeld 2.0.4 ā 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 +206 -27
- package/bin/cli.js +29 -373
- package/package.json +33 -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
|
+
[](https://www.npmjs.com/package/repomeld)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
5
8
|
---
|
|
6
9
|
|
|
7
10
|
> ## š¼ Open to Work
|
|
@@ -10,12 +13,35 @@
|
|
|
10
13
|
|
|
11
14
|
---
|
|
12
15
|
|
|
16
|
+
## ⨠Features
|
|
17
|
+
|
|
18
|
+
- š **Fast & Efficient** - Async scanning with real-time progress and binary caching
|
|
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** - Intelligent caching for binary file detection
|
|
23
|
+
- š¦ **Single File Output** - Perfect for AI context windows
|
|
24
|
+
- š **Auto-Numbering** - Never overwrites existing files
|
|
25
|
+
- šæ **Zip Backup** - Creates auto-numbered backups in `repomeld_zips/` folder
|
|
26
|
+
- š **Update Notifications** - Non-intrusive version checking
|
|
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
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
13
33
|
## Install
|
|
14
34
|
|
|
15
35
|
```bash
|
|
16
36
|
npm install -g repomeld
|
|
17
37
|
```
|
|
18
38
|
|
|
39
|
+
Or use without installing:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx repomeld
|
|
43
|
+
```
|
|
44
|
+
|
|
19
45
|
---
|
|
20
46
|
|
|
21
47
|
## Quick Start
|
|
@@ -25,7 +51,7 @@ cd your-project
|
|
|
25
51
|
repomeld
|
|
26
52
|
```
|
|
27
53
|
|
|
28
|
-
That's it. repomeld walks your project,
|
|
54
|
+
That's it. repomeld walks your project, respects `.gitignore`, skips binary files, and writes everything into one readable file with optional dependency graphs.
|
|
29
55
|
|
|
30
56
|
---
|
|
31
57
|
|
|
@@ -37,9 +63,13 @@ Every time you run repomeld it creates a **new numbered file** so previous runs
|
|
|
37
63
|
repomeld_output.txt ā first run
|
|
38
64
|
repomeld_output__2.txt ā second run
|
|
39
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
|
|
40
70
|
```
|
|
41
71
|
|
|
42
|
-
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.
|
|
43
73
|
|
|
44
74
|
---
|
|
45
75
|
|
|
@@ -76,6 +106,8 @@ Filtering:
|
|
|
76
106
|
--max-size <kb> Skip files larger than N kilobytes
|
|
77
107
|
Default: 500
|
|
78
108
|
|
|
109
|
+
--no-gitignore Ignore .gitignore file (include everything)
|
|
110
|
+
|
|
79
111
|
Formatting:
|
|
80
112
|
-s, --style <style> Header style for each file block:
|
|
81
113
|
banner ā clear dividers with file info (default)
|
|
@@ -92,6 +124,8 @@ Advanced:
|
|
|
92
124
|
--lines-before <n> Skip the first N lines of every file
|
|
93
125
|
--lines-after <n> Skip the last N lines of every file
|
|
94
126
|
--dry-run Preview which files would be included ā nothing is written
|
|
127
|
+
--no-backup Skip creating backup zip file
|
|
128
|
+
--no-update-check Skip checking for updates
|
|
95
129
|
```
|
|
96
130
|
|
|
97
131
|
---
|
|
@@ -120,6 +154,9 @@ repomeld --dry-run
|
|
|
120
154
|
# Ignore extra folders on top of defaults
|
|
121
155
|
repomeld --ignore coverage logs tmp
|
|
122
156
|
|
|
157
|
+
# Respect gitignore (default) or ignore it
|
|
158
|
+
repomeld --no-gitignore # include everything
|
|
159
|
+
|
|
123
160
|
# Only small files ā skip anything over 100 KB
|
|
124
161
|
repomeld --max-size 100
|
|
125
162
|
|
|
@@ -131,6 +168,9 @@ repomeld --no-toc --no-meta
|
|
|
131
168
|
|
|
132
169
|
# Combine filters
|
|
133
170
|
repomeld --ext php --include Controllers --exclude test --style markdown
|
|
171
|
+
|
|
172
|
+
# Skip backup creation
|
|
173
|
+
repomeld --no-backup
|
|
134
174
|
```
|
|
135
175
|
|
|
136
176
|
---
|
|
@@ -147,68 +187,207 @@ repomeld automatically skips these so your output stays clean:
|
|
|
147
187
|
| Lock files | `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml` |
|
|
148
188
|
| Build output | `dist/`, `build/`, `.next/`, `.nuxt/`, `.cache/` |
|
|
149
189
|
| OS files | `.DS_Store` |
|
|
150
|
-
| Project meta | `package.json`, `README.md` |
|
|
151
190
|
| repomeld output | `repomeld_output.txt` and all `repomeld_output__N.txt` files |
|
|
152
191
|
|
|
153
|
-
|
|
192
|
+
**Note:** `package.json` and `README.md` are **NOT** ignored by default ā they contain important context for AI tools and code reviews.
|
|
154
193
|
|
|
155
194
|
---
|
|
156
195
|
|
|
157
|
-
## Custom Ignore
|
|
196
|
+
## Custom Ignore Rules
|
|
158
197
|
|
|
159
|
-
|
|
198
|
+
### Method 1: repomeld.ignore.json
|
|
199
|
+
|
|
200
|
+
Create a `repomeld.ignore.json` in your project root for comprehensive ignore patterns:
|
|
160
201
|
|
|
161
202
|
```json
|
|
162
203
|
{
|
|
204
|
+
"_comment": "repomeld.ignore.json ā auto-ignored files and folders",
|
|
163
205
|
"ignore": [
|
|
164
206
|
"coverage",
|
|
165
207
|
"logs",
|
|
166
208
|
"tmp",
|
|
167
|
-
"*.min.js"
|
|
209
|
+
"*.min.js",
|
|
210
|
+
"**/generated/**",
|
|
211
|
+
"vendor/**/bootstrap*",
|
|
212
|
+
"**/jquery*",
|
|
213
|
+
"**/fontawesome*"
|
|
168
214
|
]
|
|
169
215
|
}
|
|
170
216
|
```
|
|
171
217
|
|
|
172
218
|
These are merged with the defaults every time repomeld runs.
|
|
173
219
|
|
|
220
|
+
### Method 2: .gitignore
|
|
221
|
+
|
|
222
|
+
repomeld automatically respects your `.gitignore` file. Use `--no-gitignore` to override.
|
|
223
|
+
|
|
224
|
+
### Method 3: CLI --ignore
|
|
225
|
+
|
|
226
|
+
Override on the command line:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
repomeld --ignore temp logs "*.tmp"
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Backup Zip Files
|
|
235
|
+
|
|
236
|
+
When repomeld runs, it automatically creates a backup zip file in the `repomeld_zips/` folder:
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
repomeld_output.txt
|
|
240
|
+
repomeld_zips/
|
|
241
|
+
āāā repomeld_output.zip ā contains all included files + output
|
|
242
|
+
|
|
243
|
+
repomeld_output__2.txt
|
|
244
|
+
repomeld_zips/
|
|
245
|
+
āāā repomeld_output__2.zip ā corresponding backup
|
|
246
|
+
|
|
247
|
+
repomeld_output__3.txt
|
|
248
|
+
repomeld_zips/
|
|
249
|
+
āāā repomeld_output__3.zip ā and so on...
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The zip file contains:
|
|
253
|
+
- All source files included in the run (preserving folder structure)
|
|
254
|
+
- The repomeld output file itself
|
|
255
|
+
|
|
256
|
+
To disable backups: `repomeld --no-backup`
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
## Performance Optimizations
|
|
262
|
+
|
|
263
|
+
repomeld is optimized for large codebases:
|
|
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
|
|
271
|
+
|
|
272
|
+
Example output:
|
|
273
|
+
```
|
|
274
|
+
š Scanning files...
|
|
275
|
+
ā
Found 2453 files in 1.2s
|
|
276
|
+
|
|
277
|
+
š Processing 2453 files...
|
|
278
|
+
|
|
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.
|
|
286
|
+
```
|
|
287
|
+
|
|
174
288
|
---
|
|
175
289
|
|
|
176
|
-
##
|
|
290
|
+
## Use Cases
|
|
177
291
|
|
|
178
|
-
|
|
292
|
+
### š¤ AI Context Preparation
|
|
293
|
+
```bash
|
|
294
|
+
repomeld --ext js ts jsx py --style markdown --max-size 200
|
|
295
|
+
```
|
|
179
296
|
|
|
297
|
+
### š Code Review
|
|
298
|
+
```bash
|
|
299
|
+
repomeld --include src/ --exclude test --style minimal --no-meta
|
|
180
300
|
```
|
|
181
|
-
# Generated by repomeld v1.0.0
|
|
182
|
-
# Date : 2025-04-20T10:00:00.000Z
|
|
183
|
-
# Source : /your/project
|
|
184
|
-
# Files : 12
|
|
185
|
-
# Lines : 847
|
|
186
301
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
3. src/config.js
|
|
192
|
-
...
|
|
193
|
-
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
302
|
+
### š¾ Full Project Backup
|
|
303
|
+
```bash
|
|
304
|
+
repomeld --force-include . --max-size 10000 --no-toc --no-meta
|
|
305
|
+
```
|
|
194
306
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
307
|
+
### š Documentation Generation
|
|
308
|
+
```bash
|
|
309
|
+
repomeld --ext md --include docs --style markdown --output documentation.md
|
|
310
|
+
```
|
|
198
311
|
|
|
199
|
-
|
|
312
|
+
### š Debug Specific Feature
|
|
313
|
+
```bash
|
|
314
|
+
repomeld --include feature-name --ext js css --output feature-context.txt
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### šŗļø Dependency Analysis
|
|
318
|
+
```bash
|
|
319
|
+
repomeld --include src --style markdown --output analysis.md
|
|
320
|
+
# Then render the Mermaid graph in the output
|
|
200
321
|
```
|
|
201
322
|
|
|
202
|
-
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
## Development
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
# Clone the repo
|
|
331
|
+
git clone https://github.com/susheel/repomeld.git
|
|
332
|
+
cd repomeld
|
|
333
|
+
|
|
334
|
+
# Install dependencies
|
|
335
|
+
npm install
|
|
336
|
+
|
|
337
|
+
# Run locally
|
|
338
|
+
npm start -- --dry-run
|
|
339
|
+
|
|
340
|
+
# Link for global testing
|
|
341
|
+
npm link
|
|
342
|
+
repomeld --help
|
|
343
|
+
|
|
344
|
+
# Run tests
|
|
345
|
+
npm test
|
|
346
|
+
```
|
|
203
347
|
|
|
204
348
|
---
|
|
205
349
|
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Contributing
|
|
354
|
+
|
|
355
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
356
|
+
|
|
357
|
+
1. Fork the repository
|
|
358
|
+
2. Create your feature branch (`git checkout -b feature/amazing`)
|
|
359
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
360
|
+
4. Push to the branch (`git push origin feature/amazing`)
|
|
361
|
+
5. Open a Pull Request
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
|
|
206
367
|
## License
|
|
207
368
|
|
|
208
|
-
MIT
|
|
369
|
+
MIT Ā© [Susheel](mailto:susheelhbti@gmail.com)
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Support & Contact
|
|
374
|
+
|
|
375
|
+
- š **Issues**: [GitHub Issues](https://github.com/susheel/repomeld/issues)
|
|
376
|
+
- š§ **Email**: [susheelhbti@gmail.com](mailto:susheelhbti@gmail.com)
|
|
377
|
+
- š¼ **Hire Me**: Available for freelance and full-time opportunities
|
|
209
378
|
|
|
210
379
|
---
|
|
211
380
|
|
|
212
381
|
> ## š¼ Hire the Author
|
|
213
382
|
> Built by a developer available for **freelance and full-time opportunities**.
|
|
214
383
|
> Got a project? Let's talk ā š§ **[susheelhbti@gmail.com](mailto:susheelhbti@gmail.com)**
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Star History
|
|
388
|
+
|
|
389
|
+
[](https://star-history.com/#susheel/repomeld&Date)
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
**Made with ā¤ļø for developers who need better context for AI tools**
|
package/bin/cli.js
CHANGED
|
@@ -1,382 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
3
|
const { program } = require("commander");
|
|
6
|
-
|
|
7
|
-
const VERSION = "
|
|
8
|
-
|
|
9
|
-
const DEFAULT_IGNORE = [
|
|
10
|
-
"node_modules",
|
|
11
|
-
".git",
|
|
12
|
-
".env",
|
|
13
|
-
".env.local",
|
|
14
|
-
".env.production",
|
|
15
|
-
".DS_Store",
|
|
16
|
-
"package-lock.json",
|
|
17
|
-
"yarn.lock",
|
|
18
|
-
"pnpm-lock.yaml",
|
|
19
|
-
".next",
|
|
20
|
-
".nuxt",
|
|
21
|
-
"dist",
|
|
22
|
-
"build",
|
|
23
|
-
".cache",
|
|
24
|
-
"package.json",
|
|
25
|
-
"README.md",
|
|
26
|
-
];
|
|
27
|
-
|
|
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
|
-
const LANGUAGE_MAP = {
|
|
49
|
-
js: "javascript", jsx: "javascript", ts: "typescript", tsx: "typescript",
|
|
50
|
-
py: "python", rb: "ruby", java: "java", cpp: "cpp", c: "c",
|
|
51
|
-
cs: "csharp", go: "go", rs: "rust", php: "php", swift: "swift",
|
|
52
|
-
kt: "kotlin", html: "html", css: "css", scss: "scss", json: "json",
|
|
53
|
-
yaml: "yaml", yml: "yaml", md: "markdown", sh: "bash", bash: "bash",
|
|
54
|
-
toml: "toml", xml: "xml", sql: "sql", graphql: "graphql",
|
|
55
|
-
};
|
|
56
|
-
|
|
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;
|
|
64
|
-
try {
|
|
65
|
-
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
66
|
-
} catch {
|
|
67
|
-
return fileList;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
for (const entry of entries) {
|
|
71
|
-
const fullPath = path.join(dirPath, entry.name);
|
|
72
|
-
const relativePath = path.relative(process.cwd(), fullPath);
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
ignoreList.some(
|
|
76
|
-
(ig) => entry.name === ig || relativePath.startsWith(ig + path.sep) || relativePath === ig
|
|
77
|
-
)
|
|
78
|
-
) {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (entry.isDirectory()) {
|
|
83
|
-
getAllFiles(fullPath, ignoreList, fileList);
|
|
84
|
-
} else if (entry.isFile()) {
|
|
85
|
-
fileList.push(fullPath);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return fileList;
|
|
90
|
-
}
|
|
91
|
-
|
|
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;
|
|
100
|
-
}
|
|
101
|
-
return false;
|
|
102
|
-
} catch {
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function matchesExtensions(filePath, exts) {
|
|
108
|
-
if (!exts || exts.length === 0) return true;
|
|
109
|
-
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
110
|
-
return exts.map((e) => e.replace(/^\./, "").toLowerCase()).includes(ext);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function matchesPattern(filePath, patterns) {
|
|
114
|
-
if (!patterns || patterns.length === 0) return false;
|
|
115
|
-
const rel = path.relative(process.cwd(), filePath);
|
|
116
|
-
return patterns.some((p) => rel.includes(p) || path.basename(filePath).includes(p));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function formatSize(bytes) {
|
|
120
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
121
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
122
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function printBanner() {
|
|
126
|
-
console.log(`
|
|
127
|
-
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
128
|
-
ā repomeld v${VERSION} ā
|
|
129
|
-
ā Meld your repo into one file š„ ā
|
|
130
|
-
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā£
|
|
131
|
-
ā š¼ Author available for freelance & full-time work ā
|
|
132
|
-
ā š§ susheelhbti@gmail.com ā
|
|
133
|
-
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function buildHeader(style, relativePath, filePath, lineCount, showMeta) {
|
|
137
|
-
const lang = getLanguage(filePath);
|
|
138
|
-
const size = formatSize(fs.statSync(filePath).size);
|
|
139
|
-
const meta = showMeta ? ` [${lineCount} lines | ${size}${lang ? " | " + lang : ""}]` : "";
|
|
140
|
-
|
|
141
|
-
if (style === "markdown") {
|
|
142
|
-
return `\n## š ${relativePath}${meta}\n\n\`\`\`${lang}\n`;
|
|
143
|
-
}
|
|
144
|
-
if (style === "minimal") {
|
|
145
|
-
return `\n# ${relativePath}\n`;
|
|
146
|
-
}
|
|
147
|
-
// default: banner style
|
|
148
|
-
const divider = "ā".repeat(60);
|
|
149
|
-
return `\n${divider}\n FILE: ${relativePath}${meta}\n${divider}\n\n`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function buildFooter(style) {
|
|
153
|
-
if (style === "markdown") return "\n```\n";
|
|
154
|
-
return "\n";
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function buildTableOfContents(files, cwd) {
|
|
158
|
-
let toc = "TABLE OF CONTENTS\n" + "ā".repeat(60) + "\n";
|
|
159
|
-
files.forEach((f, i) => {
|
|
160
|
-
const rel = path.relative(cwd, f);
|
|
161
|
-
toc += ` ${String(i + 1).padStart(3, " ")}. ${rel}\n`;
|
|
162
|
-
});
|
|
163
|
-
toc += "ā".repeat(60) + "\n\n";
|
|
164
|
-
return toc;
|
|
165
|
-
}
|
|
166
|
-
|
|
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++;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
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) {
|
|
191
|
-
const fileName = path.basename(filePath);
|
|
192
|
-
const ext = path.extname(baseOutputName);
|
|
193
|
-
const base = path.basename(baseOutputName, ext);
|
|
194
|
-
const pattern = new RegExp(`^${escapeRegex(base)}(__\\d+)?${escapeRegex(ext)}$`);
|
|
195
|
-
return pattern.test(fileName);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function escapeRegex(str) {
|
|
199
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function repomeld(options) {
|
|
203
|
-
printBanner();
|
|
204
|
-
|
|
205
|
-
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);
|
|
210
|
-
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;
|
|
217
|
-
const filterExts = options.ext || [];
|
|
218
|
-
const maxFileSizeBytes = (parseFloat(options.maxSize) || 500) * 1024;
|
|
219
|
-
const headerStyle = options.style || "banner";
|
|
220
|
-
const showMeta = !options.noMeta;
|
|
221
|
-
const showToc = !options.noToc;
|
|
222
|
-
const dryRun = options.dryRun || false;
|
|
223
|
-
const include = options.include || [];
|
|
224
|
-
const exclude = options.exclude || [];
|
|
225
|
-
const linesBefore = parseInt(options.linesBefore) || 0;
|
|
226
|
-
const linesAfter = parseInt(options.linesAfter) || 0;
|
|
227
|
-
|
|
228
|
-
console.log(`\n š Source : ${cwd}`);
|
|
229
|
-
console.log(` š Output : ${path.relative(cwd, outputFile)}`);
|
|
230
|
-
console.log(` šØ Style : ${headerStyle}`);
|
|
231
|
-
if (filterExts.length) console.log(` š Filter : .${filterExts.join(", .")}`);
|
|
232
|
-
if (forceInclude.length) console.log(` š Force : ${forceInclude.join(", ")}`);
|
|
233
|
-
if (dryRun) console.log(` š§Ŗ Dry run : no file will be written`);
|
|
234
|
-
console.log();
|
|
235
|
-
|
|
236
|
-
let allFiles = getAllFiles(cwd, ignoreList);
|
|
237
|
-
|
|
238
|
-
// Filter by extension
|
|
239
|
-
if (filterExts.length) {
|
|
240
|
-
allFiles = allFiles.filter((f) => matchesExtensions(f, filterExts));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Include pattern filter
|
|
244
|
-
if (include.length) {
|
|
245
|
-
allFiles = allFiles.filter((f) => matchesPattern(f, include));
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Exclude pattern filter
|
|
249
|
-
if (exclude.length) {
|
|
250
|
-
allFiles = allFiles.filter((f) => !matchesPattern(f, exclude));
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Remove ALL repomeld output files (current run + any previous numbered ones)
|
|
254
|
-
allFiles = allFiles.filter((f) => !isRepomeldOutput(f, outputBaseName));
|
|
255
|
-
|
|
256
|
-
if (allFiles.length === 0) {
|
|
257
|
-
console.log(" ā ļø No matching files found.\n");
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
let combinedContent = "";
|
|
262
|
-
let skipped = 0;
|
|
263
|
-
let included = 0;
|
|
264
|
-
let totalLines = 0;
|
|
265
|
-
const includedFiles = [];
|
|
266
|
-
|
|
267
|
-
for (const filePath of allFiles) {
|
|
268
|
-
const relativePath = path.relative(cwd, filePath);
|
|
269
|
-
|
|
270
|
-
if (isBinaryFile(filePath)) {
|
|
271
|
-
console.log(` ā Binary : ${relativePath}`);
|
|
272
|
-
skipped++;
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const stat = fs.statSync(filePath);
|
|
277
|
-
if (stat.size > maxFileSizeBytes) {
|
|
278
|
-
console.log(` ā Too large: ${relativePath} (${formatSize(stat.size)})`);
|
|
279
|
-
skipped++;
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
let content = fs.readFileSync(filePath, "utf8");
|
|
285
|
-
|
|
286
|
-
if (options.trim) {
|
|
287
|
-
content = content.trim();
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (linesBefore > 0 || linesAfter > 0) {
|
|
291
|
-
const lines = content.split("\n");
|
|
292
|
-
const start = linesBefore;
|
|
293
|
-
const end = linesAfter > 0 ? lines.length - linesAfter : lines.length;
|
|
294
|
-
content = lines.slice(start, end).join("\n");
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const lineCount = content.split("\n").length;
|
|
298
|
-
totalLines += lineCount;
|
|
299
|
-
includedFiles.push(filePath);
|
|
300
|
-
|
|
301
|
-
combinedContent += buildHeader(headerStyle, relativePath, filePath, lineCount, showMeta);
|
|
302
|
-
combinedContent += content;
|
|
303
|
-
combinedContent += buildFooter(headerStyle);
|
|
304
|
-
|
|
305
|
-
console.log(` ā
${relativePath}`);
|
|
306
|
-
included++;
|
|
307
|
-
} catch (err) {
|
|
308
|
-
console.log(` ā Error: ${relativePath} ā ${err.message}`);
|
|
309
|
-
skipped++;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Build final output
|
|
314
|
-
let finalOutput = "";
|
|
315
|
-
|
|
316
|
-
const timestamp = new Date().toISOString();
|
|
317
|
-
finalOutput += `# Generated by repomeld v${VERSION}\n`;
|
|
318
|
-
finalOutput += `# Date : ${timestamp}\n`;
|
|
319
|
-
finalOutput += `# Source : ${cwd}\n`;
|
|
320
|
-
finalOutput += `# Files : ${included}\n`;
|
|
321
|
-
finalOutput += `# Lines : ${totalLines}\n`;
|
|
322
|
-
finalOutput += `# Author : susheelhbti@gmail.com ā available for freelance & full-time work\n\n`;
|
|
323
|
-
|
|
324
|
-
if (showToc) {
|
|
325
|
-
finalOutput += buildTableOfContents(includedFiles, cwd);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
finalOutput += combinedContent;
|
|
329
|
-
|
|
330
|
-
if (!dryRun) {
|
|
331
|
-
fs.writeFileSync(outputFile, finalOutput, "utf8");
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const outputSize = formatSize(Buffer.byteLength(finalOutput, "utf8"));
|
|
335
|
-
|
|
336
|
-
console.log(`
|
|
337
|
-
⨠repomeld complete!
|
|
338
|
-
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
339
|
-
ā
Included : ${included} files
|
|
340
|
-
ā Skipped : ${skipped} files
|
|
341
|
-
š Lines : ${totalLines}
|
|
342
|
-
š¾ Size : ${outputSize}
|
|
343
|
-
š Output : ${path.relative(cwd, outputFile)}${dryRun ? " (dry run ā not written)" : ""}
|
|
344
|
-
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
345
|
-
š¼ Need a developer? susheelhbti@gmail.com
|
|
346
|
-
`);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// āāā CLI Definition āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
4
|
+
const { repomeld } = require("../src/index");
|
|
5
|
+
const { VERSION } = require("../src/utils/constants");
|
|
350
6
|
|
|
351
7
|
program
|
|
352
8
|
.name("repomeld")
|
|
353
|
-
.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")
|
|
354
10
|
.version(VERSION)
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
.option("
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
.option("-
|
|
361
|
-
.option("--
|
|
362
|
-
.option("--
|
|
363
|
-
.option("-
|
|
364
|
-
.option("--
|
|
365
|
-
.option("--
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
.option("-
|
|
369
|
-
.option("--
|
|
370
|
-
.option("--no-
|
|
371
|
-
.option("--
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
11
|
+
.option("-o, --output <filename>", "Output filename", "repomeld_output.txt")
|
|
12
|
+
.option("-e, --ext <exts...>", "Only include specific extensions")
|
|
13
|
+
.option("--include <patterns...>", "Force include files matching patterns")
|
|
14
|
+
.option("--exclude <patterns...>", "Exclude files matching patterns")
|
|
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")
|
|
19
|
+
.option("-s, --style <style>", "Header style: banner | markdown | minimal", "banner")
|
|
20
|
+
.option("--no-toc", "Disable table of contents")
|
|
21
|
+
.option("--no-meta", "Hide file metadata")
|
|
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")
|
|
28
|
+
.action(async (options) => {
|
|
29
|
+
try {
|
|
30
|
+
await repomeld(options);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(`\nā Error: ${error.message}`);
|
|
33
|
+
if (process.env.DEBUG) console.error(error);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
380
36
|
});
|
|
381
37
|
|
|
382
|
-
program.parse(process.argv);
|
|
38
|
+
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
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": {
|
|
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,39 @@
|
|
|
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
|
-
"
|
|
27
|
-
|
|
39
|
+
"archiver": "^7.0.1",
|
|
40
|
+
"commander": "^11.1.0",
|
|
41
|
+
"ignore": "^5.3.2",
|
|
42
|
+
"isbinaryfile": "^5.0.7"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"eslint": "^8.56.0",
|
|
46
|
+
"prettier": "^3.1.1"
|
|
28
47
|
},
|
|
29
48
|
"engines": {
|
|
30
49
|
"node": ">=14.0.0"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"bin/",
|
|
53
|
+
"README.md",
|
|
54
|
+
"LICENSE"
|
|
55
|
+
],
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
31
58
|
}
|
|
32
59
|
}
|