repomeld 2.0.3 โ†’ 2.0.4

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 +118 -251
  2. package/bin/cli.js +209 -332
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  > Meld your entire repo into a single file โ€” perfect for AI context, code reviews & sharing.
4
4
 
5
- > ๐Ÿ’ผ **Author [`susheelhbti@gmail.com`](mailto:susheelhbti@gmail.com) is open to freelance & full-time work.**
5
+ ---
6
+
7
+ > ## ๐Ÿ’ผ Open to Work
8
+ > **The author is available for freelance & full-time work.**
9
+ > ๐Ÿ“ง [susheelhbti@gmail.com](mailto:susheelhbti@gmail.com) โ€” reach out anytime!
6
10
 
7
11
  ---
8
12
 
@@ -21,320 +25,181 @@ cd your-project
21
25
  repomeld
22
26
  ```
23
27
 
24
- **No flags needed.** repomeld will walk you through 7 quick questions and configure itself:
25
-
26
- ```
27
- โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
28
- โ•‘ repomeld v1.2.0 โ•‘
29
- โ•‘ Meld your repo into one file ๐Ÿ”ฅ โ•‘
30
- โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
31
-
32
- ๐Ÿ’ผ Author susheelhbti@gmail.com is open to freelance & full-time work
33
-
34
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
35
- โ”‚ Interactive setup โ€” answer 7 quick questions โ”‚
36
- โ”‚ Press Enter to accept the [default] value โ”‚
37
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
38
-
39
- ๐Ÿ“„ 1/7 Output filename? [repomeld_output.txt]
40
- >
41
-
42
- ๐ŸŽจ 2/7 Header style?
43
- 1) banner โ€” clear dividers with file info (default)
44
- 2) markdown โ€” great for pasting into AI prompts
45
- 3) minimal โ€” filename only, no extra formatting
46
- >
47
-
48
- ๐Ÿ” 3/7 Filter to specific file extensions?
49
- e.g. js ts php py (leave blank = include ALL files)
50
- >
51
-
52
- ๐Ÿ—๏ธ 4/7 Enable structure-only mode? (y/n) [n]
53
- Strips function bodies โ€” keeps class names + signatures only.
54
- Cuts a 400-line file to ~30 lines. Perfect for AI architecture review.
55
- >
56
-
57
- ๐Ÿ”— 5/7 Generate a code review graph? (y/n) [n]
58
- Adds a Mermaid call graph at the top of the output.
59
- AI models read Mermaid natively โ€” shows which files connect to which.
60
- >
61
-
62
- ๐Ÿ“‹ 6/7 Include a table of contents? (y/n) [y]
63
- >
64
-
65
- ๐Ÿงช 7/7 Dry run only? (y/n) [n]
66
- Preview which files would be included without writing anything.
67
- >
68
- ```
69
-
70
- After answering, it shows a summary and runs:
71
-
72
- ```
73
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
74
- โ”‚ Your configuration โ”‚
75
- โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
76
- โ”‚ Output : repomeld_output.txt โ”‚
77
- โ”‚ Style : markdown โ”‚
78
- โ”‚ Extensions : php ts โ”‚
79
- โ”‚ Structure only : yes โœ… โ”‚
80
- โ”‚ Review graph : yes โœ… โ”‚
81
- โ”‚ Table of cont. : yes โ”‚
82
- โ”‚ Dry run : no โ”‚
83
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
84
- ```
28
+ That's it. repomeld walks your project, skips noise (node_modules, lock files, build folders, etc.) and writes everything into one readable file.
85
29
 
86
30
  ---
87
31
 
88
- ## Skip the Wizard (Power Users)
32
+ ## Auto-Numbered Output โ€” No Overwriting
89
33
 
90
- If you pass any flags, repomeld skips the wizard and runs directly:
34
+ Every time you run repomeld it creates a **new numbered file** so previous runs are never lost:
91
35
 
92
- ```bash
93
- repomeld --ext ts tsx --style markdown --output context.md
94
36
  ```
95
-
96
- Force the wizard at any time with:
97
-
98
- ```bash
99
- repomeld --wizard
37
+ repomeld_output.txt โ† first run
38
+ repomeld_output__2.txt โ† second run
39
+ repomeld_output__3.txt โ† third run
100
40
  ```
101
41
 
102
- ---
42
+ All previous output files are also **automatically excluded** from the next run's content โ€” so you'll never get repomeld's own output included inside itself.
103
43
 
104
- ## New Features in v1.2.0
105
-
106
- ### `--structure-only` โ€” Skeleton mode
44
+ ---
107
45
 
108
- Extracts **class names + function signatures** from every file. Strips all body code.
46
+ ## All Options
109
47
 
110
- ```bash
111
- repomeld --structure-only
112
48
  ```
49
+ Usage: repomeld [options]
113
50
 
114
- **Before** (400 lines of PHP):
115
- ```php
116
- public function available(Request $request)
117
- {
118
- $validated = $request->validate([...]);
119
- $guards = SecurityGuard::with('user')
120
- ->where('is_available', true)
121
- ...
122
- // 50 more lines
123
- }
124
- ```
51
+ Options:
52
+ -V, --version Show version number
53
+ -h, --help Show help
125
54
 
126
- **After** (1 line):
127
- ```php
128
- public function available(Request $request) { ... }
129
- ```
55
+ Output:
56
+ -o, --output <filename> Output file name
57
+ Default: "repomeld_output.txt"
58
+ Auto-numbered if the file already exists.
130
59
 
131
- Supports **PHP, JavaScript, TypeScript, Python**. Other languages fall back to the first 30 lines.
60
+ Filtering:
61
+ -e, --ext <exts...> Only include files with these extensions
62
+ e.g. --ext js ts jsx tsx
132
63
 
133
- Perfect for:
134
- - Giving AI a full architecture overview without hitting context limits
135
- - Code review focusing on structure, not implementation
136
- - Onboarding โ€” understand a codebase in seconds
64
+ --include <patterns...> Only include files whose path matches a pattern
65
+ e.g. --include src/
137
66
 
138
- ---
67
+ --exclude <patterns...> Skip files whose path matches a pattern
68
+ e.g. --exclude test spec __tests__
139
69
 
140
- ### `--review-graph` โ€” Mermaid call graph
70
+ -i, --ignore <names...> Extra folder or file names to ignore
71
+ e.g. --ignore dist .next coverage
141
72
 
142
- Generates a **Mermaid flowchart** at the top of your output showing which files import/call which.
73
+ --force-include <names...> Force-include something that would normally be ignored
74
+ e.g. --force-include vendor bootstrap
143
75
 
144
- ```bash
145
- repomeld --review-graph
146
- ```
76
+ --max-size <kb> Skip files larger than N kilobytes
77
+ Default: 500
147
78
 
148
- Output (inside the file, readable by AI natively):
79
+ Formatting:
80
+ -s, --style <style> Header style for each file block:
81
+ banner โ€” clear dividers with file info (default)
82
+ markdown โ€” fenced code blocks, great for AI prompts
83
+ minimal โ€” filename only, no extra formatting
149
84
 
150
- ````
151
- ```mermaid
152
- flowchart TD
153
- %% Code Review Graph โ€” generated by repomeld
85
+ --no-toc Don't include a table of contents at the top
154
86
 
155
- src_index_js["๐Ÿ“„ index.js\n[repomeld, getAllFiles, buildHeader]"]
156
- src_utils_js["๐Ÿ“„ utils.js\n[formatSize, getLanguage]"]
157
- src_graph_js["๐Ÿ“„ graph.js\n[buildReviewGraph]"]
87
+ --no-meta Hide per-file metadata (line count, size, language)
158
88
 
159
- src_index_js --> src_utils_js
160
- src_index_js --> src_graph_js
161
- ```
162
- ````
89
+ --trim Trim leading/trailing whitespace from each file
163
90
 
164
- AI models like Claude, GPT-4, and Gemini read Mermaid natively โ€” no image needed.
91
+ Advanced:
92
+ --lines-before <n> Skip the first N lines of every file
93
+ --lines-after <n> Skip the last N lines of every file
94
+ --dry-run Preview which files would be included โ€” nothing is written
95
+ ```
165
96
 
166
97
  ---
167
98
 
168
- ### Power combo: both together
99
+ ## Examples
169
100
 
170
101
  ```bash
171
- repomeld --structure-only --review-graph
172
- ```
102
+ # Basic โ€” include everything, auto-numbered output
103
+ repomeld
173
104
 
174
- AI gets:
175
- 1. **The map** โ€” which files connect to which (graph)
176
- 2. **The skeleton** โ€” every class and function signature (structure)
177
- 3. **Zero noise** โ€” no implementation details
105
+ # Only TypeScript files
106
+ repomeld --ext ts tsx
178
107
 
179
- This is the best way to give AI a complete understanding of a large codebase without wasting context.
108
+ # Only files inside src/
109
+ repomeld --include src/
180
110
 
181
- ---
111
+ # Skip test files
112
+ repomeld --exclude test spec __tests__
182
113
 
183
- ## Smart Auto-Ignore
184
-
185
- repomeld automatically skips common public libraries and vendor files.
186
-
187
- | Category | Examples |
188
- |---|---|
189
- | Project meta | `package.json`, `README.md`, `yarn.lock`, `.gitignore` |
190
- | Build output | `dist/`, `build/`, `.next/`, `.nuxt/`, `coverage/` |
191
- | Vendor folders | `vendor/`, `libs/`, `plugins/`, `assets/vendor/` |
192
- | Bootstrap | `bootstrap.min.css`, `bootstrap.bundle.min.js` |
193
- | jQuery | `jquery.min.js`, `jquery-ui.min.js` |
194
- | Font Awesome | `all.min.css`, `fontawesome-free/` |
195
- | Tailwind | `tailwind.min.css`, `tailwindcss/` |
196
- | Chart / D3 / Three | `chart.min.js`, `d3.min.js`, `three.min.js` |
197
- | Axios / Lodash | `axios.min.js`, `lodash.min.js` |
198
- | Rich text editors | `tinymce/`, `ckeditor/`, `codemirror/` |
199
- | Admin templates | `AdminLTE/`, `metronic/`, `coreui/` |
200
- | Env / Secrets | `.env`, `.env.local`, `.env.production` |
114
+ # Markdown style โ€” great for pasting into AI chats
115
+ repomeld --style markdown --output context.md
201
116
 
202
- ---
117
+ # Preview what would be included without writing anything
118
+ repomeld --dry-run
203
119
 
204
- ## All Options
120
+ # Ignore extra folders on top of defaults
121
+ repomeld --ignore coverage logs tmp
205
122
 
206
- ```
207
- Usage: repomeld [options]
123
+ # Only small files โ€” skip anything over 100 KB
124
+ repomeld --max-size 100
208
125
 
209
- Options:
210
- -V, --version Show version
211
- -h, --help Show help
212
- --wizard Interactive step-by-step setup
213
-
214
- Output:
215
- -o, --output <filename> Output file name (default: "repomeld_output.txt")
216
-
217
- Filtering:
218
- -e, --ext <exts...> Only include specific extensions
219
- e.g. --ext js ts jsx tsx
220
- --include <patterns...> Only include files matching patterns
221
- e.g. --include src/
222
- --exclude <patterns...> Exclude files matching patterns
223
- e.g. --exclude test spec
224
- -i, --ignore <names...> Extra folders/files to ignore
225
- --force-include <names...> Force-include files ignored by default
226
- --max-size <kb> Skip files larger than N KB (default: 500)
227
-
228
- Formatting:
229
- -s, --style <style> Header style: banner | markdown | minimal
230
- --no-toc Disable table of contents
231
- --no-meta Hide file metadata (lines, size, lang)
232
- --trim Trim leading/trailing whitespace per file
233
-
234
- Advanced:
235
- --lines-before <n> Skip first N lines of each file
236
- --lines-after <n> Skip last N lines of each file
237
- --dry-run Preview without writing output
238
-
239
- AI / Code Review:
240
- --structure-only Extract class + function signatures only (no body)
241
- --review-graph Prepend a Mermaid call graph to the output
242
- ```
126
+ # Force-include a vendor file that's normally ignored
127
+ repomeld --force-include bootstrap
243
128
 
244
- ---
129
+ # No table of contents, no metadata
130
+ repomeld --no-toc --no-meta
245
131
 
246
- ## Examples
132
+ # Combine filters
133
+ repomeld --ext php --include Controllers --exclude test --style markdown
134
+ ```
247
135
 
248
- ```bash
249
- # Wizard mode (auto when no flags given)
250
- repomeld
136
+ ---
251
137
 
252
- # Force wizard even with flags
253
- repomeld --wizard
138
+ ## Auto-Ignored by Default
254
139
 
255
- # Only PHP controllers, structure only
256
- repomeld --ext php --include Controllers --structure-only
140
+ repomeld automatically skips these so your output stays clean:
257
141
 
258
- # Full code + call graph
259
- repomeld --review-graph
142
+ | Category | What's skipped |
143
+ |-----------------|-----------------------------------------------------|
144
+ | Dependencies | `node_modules/` |
145
+ | Version control | `.git/` |
146
+ | Env / secrets | `.env`, `.env.local`, `.env.production` |
147
+ | Lock files | `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml` |
148
+ | Build output | `dist/`, `build/`, `.next/`, `.nuxt/`, `.cache/` |
149
+ | OS files | `.DS_Store` |
150
+ | Project meta | `package.json`, `README.md` |
151
+ | repomeld output | `repomeld_output.txt` and all `repomeld_output__N.txt` files |
260
152
 
261
- # Best for AI: skeleton + graph, markdown style
262
- repomeld --structure-only --review-graph --style markdown --output ai_context.md
153
+ You can add your own permanent ignore rules via a `repomeld.ignore.json` file (see below).
263
154
 
264
- # Only TypeScript inside src/
265
- repomeld --ext ts tsx --include src/
155
+ ---
266
156
 
267
- # Exclude tests
268
- repomeld --exclude test spec __tests__
157
+ ## Custom Ignore File
269
158
 
270
- # Dry run โ€” preview only
271
- repomeld --dry-run
159
+ Create a `repomeld.ignore.json` in your project root:
272
160
 
273
- # Large repo: structure only, skip files over 100 KB
274
- repomeld --structure-only --max-size 100
161
+ ```json
162
+ {
163
+ "ignore": [
164
+ "coverage",
165
+ "logs",
166
+ "tmp",
167
+ "*.min.js"
168
+ ]
169
+ }
275
170
  ```
276
171
 
172
+ These are merged with the defaults every time repomeld runs.
173
+
277
174
  ---
278
175
 
279
176
  ## Output Format
280
177
 
281
- ### With `--review-graph`
178
+ Each run produces a file like this:
282
179
 
283
180
  ```
284
- # Generated by repomeld v1.2.0
285
- # Date : 2024-01-01T00:00:00.000Z
181
+ # Generated by repomeld v1.0.0
182
+ # Date : 2025-04-20T10:00:00.000Z
286
183
  # Source : /your/project
287
- # Files : 8
288
- # Lines : 312
289
- # Graph : review-graph enabled
290
- # Author : susheelhbti@gmail.com (open to work)
184
+ # Files : 12
185
+ # Lines : 847
291
186
 
187
+ TABLE OF CONTENTS
292
188
  โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
293
- CODE REVIEW GRAPH
189
+ 1. src/index.js
190
+ 2. src/utils.js
191
+ 3. src/config.js
192
+ ...
294
193
  โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
295
194
 
296
- ```mermaid
297
- flowchart TD
298
- ...
299
- ```
195
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
196
+ FILE: src/index.js [120 lines | 3.2 KB | javascript]
197
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
300
198
 
301
- TABLE OF CONTENTS
302
- ...
199
+ ... file contents ...
303
200
  ```
304
201
 
305
- ### With `--structure-only` (PHP example)
306
-
307
- ```php
308
- namespace App\Http\Controllers;
309
- use App\Models\SecurityGuard;
310
- use App\Models\SecurityRequest;
311
-
312
- class GuardController extends Controller
313
- {
314
- /** Get guard's own profile */
315
- public function profile() { ... }
316
- public function updateProfile(Request $request) { ... }
317
- public function bookings() { ... }
318
- public function available(Request $request) { ... }
319
- public function toggleAvailability() { ... }
320
- public function rate(Request $request, $id) { ... }
321
- }
322
- ```
323
-
324
- ---
325
-
326
- ## Publish / Update
327
-
328
- ```bash
329
- npm version patch # 1.2.0 โ†’ 1.2.1
330
- npm publish
331
- ```
332
-
333
- ---
334
-
335
- ## Contributing
336
-
337
- Found a popular library that should be in the ignore list? Open a PR!
202
+ With `--style markdown` each file becomes a fenced code block โ€” paste directly into Claude, ChatGPT, or any AI tool.
338
203
 
339
204
  ---
340
205
 
@@ -344,4 +209,6 @@ MIT
344
209
 
345
210
  ---
346
211
 
347
- > ๐Ÿ’ผ **Looking to hire?** The author [`susheelhbti@gmail.com`](mailto:susheelhbti@gmail.com) is available for freelance and full-time work.
212
+ > ## ๐Ÿ’ผ Hire the Author
213
+ > Built by a developer available for **freelance and full-time opportunities**.
214
+ > Got a project? Let's talk โ€” ๐Ÿ“ง **[susheelhbti@gmail.com](mailto:susheelhbti@gmail.com)**
package/bin/cli.js CHANGED
@@ -2,18 +2,27 @@
2
2
 
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
- const readline = require("readline");
6
5
  const { program } = require("commander");
7
6
 
8
- const VERSION = "1.2.0";
9
-
10
- const AUTHOR_NOTE = " ๐Ÿ’ผ Author susheelhbti@gmail.com is open to freelance & full-time work";
7
+ const VERSION = "1.0.0";
11
8
 
12
9
  const DEFAULT_IGNORE = [
13
- "node_modules", ".git", ".env", ".env.local", ".env.production",
14
- ".DS_Store", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
15
- ".next", ".nuxt", "dist", "build", ".cache", "package.json",
16
- "README.md", "repomeld_output.txt",
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",
17
26
  ];
18
27
 
19
28
  function loadIgnoreConfig() {
@@ -23,7 +32,9 @@ function loadIgnoreConfig() {
23
32
  if (fs.existsSync(loc)) {
24
33
  try {
25
34
  const data = JSON.parse(fs.readFileSync(loc, "utf8"));
26
- if (Array.isArray(data.ignore)) return data.ignore;
35
+ if (Array.isArray(data.ignore)) {
36
+ return data.ignore;
37
+ }
27
38
  } catch {
28
39
  console.warn(` โš ๏ธ Could not parse ${loc}, using defaults.`);
29
40
  }
@@ -50,17 +61,31 @@ function getLanguage(filePath) {
50
61
 
51
62
  function getAllFiles(dirPath, ignoreList, fileList = []) {
52
63
  let entries;
53
- try { entries = fs.readdirSync(dirPath, { withFileTypes: true }); }
54
- catch { return fileList; }
64
+ try {
65
+ entries = fs.readdirSync(dirPath, { withFileTypes: true });
66
+ } catch {
67
+ return fileList;
68
+ }
69
+
55
70
  for (const entry of entries) {
56
71
  const fullPath = path.join(dirPath, entry.name);
57
72
  const relativePath = path.relative(process.cwd(), fullPath);
58
- if (ignoreList.some(
59
- (ig) => entry.name === ig || relativePath.startsWith(ig + path.sep) || relativePath === ig
60
- )) continue;
61
- if (entry.isDirectory()) getAllFiles(fullPath, ignoreList, fileList);
62
- else if (entry.isFile()) fileList.push(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
+ }
63
87
  }
88
+
64
89
  return fileList;
65
90
  }
66
91
 
@@ -70,9 +95,13 @@ function isBinaryFile(filePath) {
70
95
  const fd = fs.openSync(filePath, "r");
71
96
  const bytesRead = fs.readSync(fd, buffer, 0, 512, 0);
72
97
  fs.closeSync(fd);
73
- for (let i = 0; i < bytesRead; i++) if (buffer[i] === 0) return true;
98
+ for (let i = 0; i < bytesRead; i++) {
99
+ if (buffer[i] === 0) return true;
100
+ }
74
101
  return false;
75
- } catch { return true; }
102
+ } catch {
103
+ return true;
104
+ }
76
105
  }
77
106
 
78
107
  function matchesExtensions(filePath, exts) {
@@ -93,329 +122,186 @@ function formatSize(bytes) {
93
122
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
94
123
  }
95
124
 
96
- // โ”€โ”€โ”€ AUTO-INCREMENT OUTPUT FILENAME โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
97
-
98
- function resolveOutputPath(cwd, filename) {
99
- const ext = path.extname(filename);
100
- const base = path.basename(filename, ext);
101
- let candidate = path.resolve(cwd, filename);
102
- let counter = 1;
103
- while (fs.existsSync(candidate)) {
104
- candidate = path.resolve(cwd, `${base}_${counter}${ext}`);
105
- counter++;
106
- }
107
- return candidate;
108
- }
109
-
110
- // โ”€โ”€โ”€ BANNER โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
111
-
112
125
  function printBanner() {
113
126
  console.log(`
114
- โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
115
- โ•‘ repomeld v${VERSION} โ•‘
116
- โ•‘ Meld your repo into one file ๐Ÿ”ฅ โ•‘
117
- โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•`);
118
- console.log(AUTHOR_NOTE);
119
- 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
+ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•`);
120
134
  }
121
135
 
122
- // โ”€โ”€โ”€ HEADER / FOOTER โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
123
-
124
- function buildHeader(relativePath, filePath, lineCount, showMeta) {
136
+ function buildHeader(style, relativePath, filePath, lineCount, showMeta) {
125
137
  const lang = getLanguage(filePath);
126
138
  const size = formatSize(fs.statSync(filePath).size);
127
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
128
148
  const divider = "โ”€".repeat(60);
129
149
  return `\n${divider}\n FILE: ${relativePath}${meta}\n${divider}\n\n`;
130
150
  }
131
151
 
132
- function buildFooter() {
152
+ function buildFooter(style) {
153
+ if (style === "markdown") return "\n```\n";
133
154
  return "\n";
134
155
  }
135
156
 
136
- // โ”€โ”€โ”€ TABLE OF CONTENTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
137
-
138
157
  function buildTableOfContents(files, cwd) {
139
158
  let toc = "TABLE OF CONTENTS\n" + "โ•".repeat(60) + "\n";
140
159
  files.forEach((f, i) => {
141
- toc += ` ${String(i + 1).padStart(3, " ")}. ${path.relative(cwd, f)}\n`;
160
+ const rel = path.relative(cwd, f);
161
+ toc += ` ${String(i + 1).padStart(3, " ")}. ${rel}\n`;
142
162
  });
143
163
  toc += "โ•".repeat(60) + "\n\n";
144
164
  return toc;
145
165
  }
146
166
 
147
- // โ”€โ”€โ”€ FILE TREE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
148
-
149
- function buildFileTree(files, cwd) {
150
- const tree = {};
151
- for (const f of files) {
152
- const parts = path.relative(cwd, f).split(path.sep);
153
- let node = tree;
154
- for (const part of parts) {
155
- node[part] = node[part] || {};
156
- node = node[part];
157
- }
158
- }
159
-
160
- function render(node, prefix = "") {
161
- const keys = Object.keys(node);
162
- return keys.map((key, i) => {
163
- const isLast = i === keys.length - 1;
164
- const connector = isLast ? "โ””โ”€โ”€ " : "โ”œโ”€โ”€ ";
165
- const childPfx = isLast ? " " : "โ”‚ ";
166
- const children = Object.keys(node[key]).length
167
- ? "\n" + render(node[key], prefix + childPfx)
168
- : "";
169
- return prefix + connector + key + children;
170
- }).join("\n");
171
- }
172
-
173
- return "FILE TREE\n" + "โ•".repeat(60) + "\n"
174
- + render(tree) + "\n"
175
- + "โ•".repeat(60) + "\n\n";
176
- }
177
-
178
- // โ”€โ”€โ”€ STRUCTURE EXTRACTION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
179
-
180
- function extractStructure(content, filePath) {
181
- const lang = getLanguage(filePath);
182
- const lines = content.split("\n");
183
- const output = [];
184
-
185
- if (lang === "php") {
186
- let insideClass = false;
187
- let braceDepth = 0;
188
- let classStartDepth = 0;
189
- for (const line of lines) {
190
- const trimmed = line.trim();
191
- if (trimmed.startsWith("namespace ") || trimmed.startsWith("use ")) { output.push(line); continue; }
192
- const classMatch = trimmed.match(/^(abstract\s+class|class|interface|trait)\s+(\w+)(\s+extends\s+\w+)?(\s+implements\s+[\w,\s]+)?/);
193
- if (classMatch) {
194
- if (output.length && output[output.length - 1] !== "") output.push("");
195
- output.push(line); insideClass = true; classStartDepth = braceDepth;
196
- }
197
- braceDepth += (line.match(/\{/g) || []).length;
198
- braceDepth -= (line.match(/\}/g) || []).length;
199
- if (insideClass && braceDepth <= classStartDepth && trimmed === "}") {
200
- output.push("}"); insideClass = false; output.push(""); continue;
201
- }
202
- if (trimmed.startsWith("/**") || trimmed.startsWith("*") || trimmed.startsWith("*/")) {
203
- if (insideClass) output.push(line); continue;
204
- }
205
- if (insideClass) {
206
- const methodMatch = trimmed.match(/^(public|protected|private|static|abstract|final|\s)*(function\s+\w+\s*\([^)]*\)(\s*:\s*[\w\?\|\\]+)?)/);
207
- if (methodMatch) output.push(` ${methodMatch[0].trim()} { ... }`);
208
- if (trimmed.match(/^(public|protected|private|const)\s+/) && trimmed.endsWith(";") && !trimmed.includes("function"))
209
- output.push(` ${trimmed}`);
210
- }
211
- }
212
- return output.join("\n");
213
- }
214
-
215
- if (["javascript", "typescript"].includes(lang)) {
216
- for (const line of lines) {
217
- const trimmed = line.trim();
218
- if (trimmed.match(/^(export\s+)?(abstract\s+)?class\s+\w+/)) { output.push(line); continue; }
219
- if (trimmed.match(/^(export\s+)?(async\s+)?function\s+\w+\s*\(/)) {
220
- output.push(`${line.match(/^\s*/)[0]}${trimmed.replace(/\{[\s\S]*$/, "{ ... }")}`); continue;
221
- }
222
- if (trimmed.match(/^(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?\(/)) {
223
- output.push(`${line.match(/^\s*/)[0]}${trimmed.split("=>")[0].trim()} => { ... }`); continue;
224
- }
225
- if (trimmed.match(/^(async\s+)?(\w+)\s*\([^)]*\)\s*(\:\s*[\w<>\[\]]+)?\s*\{/) &&
226
- !["if", "while", "for", "switch"].some(k => trimmed.startsWith(k))) {
227
- output.push(`${line.match(/^\s*/)[0]}${trimmed.replace(/\{[\s\S]*$/, "{ ... }")}`); continue;
228
- }
229
- if (trimmed.startsWith("import ")) output.push(line);
230
- if (trimmed === "}") output.push(line);
231
- }
232
- return output.join("\n");
233
- }
234
-
235
- if (lang === "python") {
236
- for (const line of lines) {
237
- const t = line.trim();
238
- if (t.startsWith("class ") || t.startsWith("def ") || t.startsWith("async def "))
239
- output.push(line.replace(/:[\s\S]*$/, ":"));
240
- if (t.startsWith("import ") || t.startsWith("from ")) output.push(line);
241
- }
242
- return output.join("\n");
243
- }
244
-
245
- return lines.slice(0, 30).join("\n") + "\n... (structure extraction not supported for this language)";
246
- }
247
-
248
- // โ”€โ”€โ”€ REVIEW GRAPH โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
249
-
250
- function buildReviewGraph(files, cwd) {
251
- const fileFunctions = {};
252
- const fileImports = {};
253
-
254
- for (const filePath of files.slice(0, 25)) {
255
- const lang = getLanguage(filePath);
256
- if (!["javascript", "typescript", "php", "python"].includes(lang)) continue;
257
- let content;
258
- try { content = fs.readFileSync(filePath, "utf8"); } catch { continue; }
259
- const rel = path.relative(cwd, filePath);
260
- const nodeId = rel.replace(/[^a-zA-Z0-9]/g, "_");
261
- fileFunctions[rel] = { nodeId, functions: [] };
262
- fileImports[rel] = [];
263
- for (const line of content.split("\n")) {
264
- const t = line.trim();
265
- const fnMatch = t.match(/^(export\s+)?(async\s+)?function\s+(\w+)/) ||
266
- t.match(/^(public|protected|private|static)?\s*(function\s+(\w+))/) ||
267
- t.match(/^def\s+(\w+)/);
268
- if (fnMatch) {
269
- const name = fnMatch[3] || fnMatch[2] || fnMatch[1];
270
- if (name && name.length > 2 && !["if", "for", "while", "switch"].includes(name))
271
- fileFunctions[rel].functions.push(name);
272
- }
273
- const imp = t.match(/require\(['"]([^'"]+)['"]\)/) ||
274
- t.match(/^import\s+.*from\s+['"]([^'"]+)['"]/) ||
275
- t.match(/^use\s+(App\\[^;]+)/);
276
- if (imp) fileImports[rel].push(imp[1]);
277
- }
278
- fileFunctions[rel].functions = [...new Set(fileFunctions[rel].functions)].slice(0, 5);
279
- }
280
-
281
- const fileList = Object.keys(fileFunctions);
282
- if (fileList.length === 0) return null;
283
-
284
- let mermaid = "```mermaid\nflowchart TD\n %% Code Review Graph โ€” generated by repomeld\n\n";
285
- for (const rel of fileList) {
286
- const { nodeId, functions } = fileFunctions[rel];
287
- const label = path.basename(rel);
288
- const fnList = functions.length ? `\\n[${functions.join(", ")}]` : "";
289
- mermaid += ` ${nodeId}["๐Ÿ“„ ${label}${fnList}"]\n`;
290
- }
291
- mermaid += "\n";
292
- let edgeCount = 0;
293
- for (const rel of fileList) {
294
- for (const imp of fileImports[rel]) {
295
- const matched = fileList.find(f => {
296
- const base = path.basename(f, path.extname(f));
297
- return imp.endsWith(base) || imp.includes(base);
298
- });
299
- if (matched && matched !== rel && edgeCount < 40) {
300
- mermaid += ` ${fileFunctions[rel].nodeId} --> ${fileFunctions[matched].nodeId}\n`;
301
- edgeCount++;
302
- }
303
- }
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++;
304
183
  }
305
- return mermaid + "```\n";
306
184
  }
307
185
 
308
- // โ”€โ”€โ”€ INTERACTIVE WIZARD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
309
-
310
- function ask(rl, question) {
311
- return new Promise((resolve) => rl.question(question, resolve));
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);
312
196
  }
313
197
 
314
- async function runWizard() {
315
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
316
-
317
- console.log(" โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”");
318
- console.log(" โ”‚ Interactive setup โ€” answer 1 quick question โ”‚");
319
- console.log(" โ”‚ Press Enter to accept the [default] value โ”‚");
320
- console.log(" โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜\n");
321
-
322
- const opts = {};
323
-
324
- // 1 โ€” Extensions
325
- console.log(" ๐Ÿ” 1/1 Filter to specific extensions?");
326
- console.log(" e.g. js ts py (leave blank = all files)");
327
- const extRaw = await ask(rl, " > ");
328
- opts.ext = extRaw.trim() ? extRaw.trim().split(/\s+/) : [];
329
-
330
- // hardcoded defaults โ€” not optional
331
- opts.output = "repomeld_output.txt";
332
- opts.style = "banner";
333
- opts.reviewGraph = true;
334
- opts.noToc = false;
335
- opts.dryRun = false;
336
- opts.structureOnly = false;
337
-
338
- rl.close();
339
-
340
- // Summary
341
- console.log("\n โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”");
342
- console.log(" โ”‚ Your configuration โ”‚");
343
- console.log(" โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค");
344
- console.log(` โ”‚ Extensions : ${(opts.ext.length ? opts.ext.join(", ") : "all").padEnd(33)}โ”‚`);
345
- console.log(` โ”‚ Style : banner (always) โ”‚`);
346
- console.log(` โ”‚ Graph : yes (always) โ”‚`);
347
- console.log(` โ”‚ TOC : yes (always) โ”‚`);
348
- console.log(" โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜\n");
349
-
350
- return opts;
198
+ function escapeRegex(str) {
199
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
351
200
  }
352
201
 
353
- // โ”€โ”€โ”€ CORE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
354
-
355
202
  function repomeld(options) {
203
+ printBanner();
204
+
356
205
  const cwd = process.cwd();
357
206
 
358
- // Always banner, always review graph, always TOC
359
- options.style = "banner";
360
- options.reviewGraph = true;
361
- options.noToc = false;
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);
362
211
 
363
- const outputFile = resolveOutputPath(cwd, options.output || "repomeld_output.txt");
364
212
  const forceInclude = options.forceInclude || [];
365
213
  const rawIgnore = [...DEFAULT_IGNORE, ...IGNORE_FROM_CONFIG, ...(options.ignore || [])];
366
214
  const ignoreList = forceInclude.length
367
- ? rawIgnore.filter(ig => !forceInclude.some(fi => ig.includes(fi) || fi.includes(ig)))
215
+ ? rawIgnore.filter((ig) => !forceInclude.some((fi) => ig.includes(fi) || fi.includes(ig)))
368
216
  : rawIgnore;
369
- const filterExts = options.ext || [];
217
+ const filterExts = options.ext || [];
370
218
  const maxFileSizeBytes = (parseFloat(options.maxSize) || 500) * 1024;
371
- const showMeta = options.noMeta !== true;
372
- const dryRun = options.dryRun || false;
373
- const include = options.include || [];
374
- const exclude = options.exclude || [];
375
- const linesBefore = parseInt(options.linesBefore) || 0;
376
- const linesAfter = parseInt(options.linesAfter) || 0;
377
- const structureOnly = options.structureOnly || false;
378
-
379
- console.log(` ๐Ÿ“‚ Source : ${cwd}`);
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}`);
380
229
  console.log(` ๐Ÿ“„ Output : ${path.relative(cwd, outputFile)}`);
381
- console.log(` ๐ŸŽจ Style : banner`);
230
+ console.log(` ๐ŸŽจ Style : ${headerStyle}`);
382
231
  if (filterExts.length) console.log(` ๐Ÿ” Filter : .${filterExts.join(", .")}`);
383
- if (structureOnly) console.log(` ๐Ÿ—๏ธ Mode : structure only`);
384
- console.log(` ๐Ÿ”— Graph : review graph enabled`);
385
- if (dryRun) console.log(` ๐Ÿงช Dry run : no file will be written`);
232
+ if (forceInclude.length) console.log(` ๐Ÿ“Œ Force : ${forceInclude.join(", ")}`);
233
+ if (dryRun) console.log(` ๐Ÿงช Dry run : no file will be written`);
386
234
  console.log();
387
235
 
388
236
  let allFiles = getAllFiles(cwd, ignoreList);
389
- if (filterExts.length) allFiles = allFiles.filter(f => matchesExtensions(f, filterExts));
390
- if (include.length) allFiles = allFiles.filter(f => matchesPattern(f, include));
391
- if (exclude.length) allFiles = allFiles.filter(f => !matchesPattern(f, exclude));
392
- allFiles = allFiles.filter(f => path.resolve(f) !== outputFile);
393
237
 
394
- if (allFiles.length === 0) { console.log(" โš ๏ธ No matching files found.\n"); return; }
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
+ }
395
260
 
396
261
  let combinedContent = "";
397
- let skipped = 0, included = 0, totalLines = 0;
262
+ let skipped = 0;
263
+ let included = 0;
264
+ let totalLines = 0;
398
265
  const includedFiles = [];
399
266
 
400
267
  for (const filePath of allFiles) {
401
268
  const relativePath = path.relative(cwd, filePath);
402
- if (isBinaryFile(filePath)) { console.log(` โญ Binary : ${relativePath}`); skipped++; continue; }
269
+
270
+ if (isBinaryFile(filePath)) {
271
+ console.log(` โญ Binary : ${relativePath}`);
272
+ skipped++;
273
+ continue;
274
+ }
275
+
403
276
  const stat = fs.statSync(filePath);
404
- if (stat.size > maxFileSizeBytes) { console.log(` โญ Too large: ${relativePath} (${formatSize(stat.size)})`); skipped++; continue; }
277
+ if (stat.size > maxFileSizeBytes) {
278
+ console.log(` โญ Too large: ${relativePath} (${formatSize(stat.size)})`);
279
+ skipped++;
280
+ continue;
281
+ }
282
+
405
283
  try {
406
284
  let content = fs.readFileSync(filePath, "utf8");
407
- if (options.trim) content = content.trim();
285
+
286
+ if (options.trim) {
287
+ content = content.trim();
288
+ }
289
+
408
290
  if (linesBefore > 0 || linesAfter > 0) {
409
291
  const lines = content.split("\n");
410
- content = lines.slice(linesBefore, linesAfter > 0 ? lines.length - linesAfter : lines.length).join("\n");
292
+ const start = linesBefore;
293
+ const end = linesAfter > 0 ? lines.length - linesAfter : lines.length;
294
+ content = lines.slice(start, end).join("\n");
411
295
  }
412
- if (structureOnly) content = extractStructure(content, filePath);
296
+
413
297
  const lineCount = content.split("\n").length;
414
298
  totalLines += lineCount;
415
299
  includedFiles.push(filePath);
416
- combinedContent += buildHeader(relativePath, filePath, lineCount, showMeta);
300
+
301
+ combinedContent += buildHeader(headerStyle, relativePath, filePath, lineCount, showMeta);
417
302
  combinedContent += content;
418
- combinedContent += buildFooter();
303
+ combinedContent += buildFooter(headerStyle);
304
+
419
305
  console.log(` โœ… ${relativePath}`);
420
306
  included++;
421
307
  } catch (err) {
@@ -424,82 +310,73 @@ function repomeld(options) {
424
310
  }
425
311
  }
426
312
 
427
- // Build final output: metadata โ†’ review graph โ†’ file tree โ†’ TOC โ†’ file contents
313
+ // Build final output
428
314
  let finalOutput = "";
315
+
316
+ const timestamp = new Date().toISOString();
429
317
  finalOutput += `# Generated by repomeld v${VERSION}\n`;
430
- finalOutput += `# Date : ${new Date().toISOString()}\n`;
318
+ finalOutput += `# Date : ${timestamp}\n`;
431
319
  finalOutput += `# Source : ${cwd}\n`;
432
320
  finalOutput += `# Files : ${included}\n`;
433
321
  finalOutput += `# Lines : ${totalLines}\n`;
434
- if (structureOnly) finalOutput += `# Mode : structure-only (signatures)\n`;
435
- finalOutput += `# Graph : review-graph enabled\n`;
436
- finalOutput += `# Author : susheelhbti@gmail.com (open to work)\n\n`;
437
-
438
- // Review graph (always)
439
- const graph = buildReviewGraph(includedFiles, cwd);
440
- if (graph) {
441
- finalOutput += "โ•".repeat(60) + "\n CODE REVIEW GRAPH\n" + "โ•".repeat(60) + "\n\n";
442
- finalOutput += graph + "\n";
443
- }
444
-
445
- // File tree (always)
446
- finalOutput += buildFileTree(includedFiles, cwd);
322
+ finalOutput += `# Author : susheelhbti@gmail.com โ€” available for freelance & full-time work\n\n`;
447
323
 
448
- // Table of contents (always)
449
- finalOutput += buildTableOfContents(includedFiles, cwd);
324
+ if (showToc) {
325
+ finalOutput += buildTableOfContents(includedFiles, cwd);
326
+ }
450
327
 
451
- // File contents
452
328
  finalOutput += combinedContent;
453
329
 
454
- if (!dryRun) fs.writeFileSync(outputFile, finalOutput, "utf8");
330
+ if (!dryRun) {
331
+ fs.writeFileSync(outputFile, finalOutput, "utf8");
332
+ }
455
333
 
456
334
  const outputSize = formatSize(Buffer.byteLength(finalOutput, "utf8"));
335
+
457
336
  console.log(`
458
337
  โœจ repomeld complete!
459
- โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
338
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
460
339
  โœ… Included : ${included} files
461
340
  โญ Skipped : ${skipped} files
462
341
  ๐Ÿ“ Lines : ${totalLines}
463
342
  ๐Ÿ’พ Size : ${outputSize}
464
343
  ๐Ÿ“„ Output : ${path.relative(cwd, outputFile)}${dryRun ? " (dry run โ€” not written)" : ""}
344
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
345
+ ๐Ÿ’ผ Need a developer? susheelhbti@gmail.com
465
346
  `);
466
- console.log(AUTHOR_NOTE + "\n");
467
347
  }
468
348
 
469
- // โ”€โ”€โ”€ CLI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
349
+ // โ”€โ”€โ”€ CLI Definition โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
470
350
 
471
351
  program
472
352
  .name("repomeld")
473
353
  .description("Meld your entire repo into a single file โ€” perfect for AI context, code reviews & sharing")
474
354
  .version(VERSION)
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 ignored by default")
481
- .option("--max-size <kb>", "Skip files larger than N KB", "500")
482
- .option("--no-meta", "Hide file metadata")
483
- .option("--trim", "Trim leading/trailing whitespace per file")
484
- .option("--lines-before <n>", "Skip first N lines of each file")
485
- .option("--lines-after <n>", "Skip last N lines of each file")
486
- .option("--dry-run", "Preview without writing output")
487
- .option("--structure-only", "Extract signatures only โ€” no body code")
488
- .option("--wizard", "Interactive step-by-step setup (auto when no flags given)")
489
- .action(async (options) => {
490
- printBanner();
491
-
492
- // Auto-wizard when user runs plain `repomeld` with no flags
493
- const hasFlags = options.ext || options.include || options.exclude ||
494
- options.structureOnly || options.output !== "repomeld_output.txt" ||
495
- options.dryRun;
496
-
497
- if (options.wizard || !hasFlags) {
498
- const wizardOpts = await runWizard();
499
- repomeld({ ...options, ...wizardOpts });
500
- } else {
501
- repomeld(options);
502
- }
355
+
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);
503
380
  });
504
381
 
505
- program.parse(process.argv);
382
+ program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repomeld",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
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": {