repomeld 1.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.
Files changed (3) hide show
  1. package/README.md +147 -0
  2. package/bin/cli.js +311 -0
  3. package/package.json +31 -0
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # repomeld 🔥
2
+
3
+ > Meld your entire repo into a single file — perfect for AI context, code reviews & sharing.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g repomeld
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ cd your-project
15
+ repomeld
16
+ ```
17
+
18
+ Creates `repomeld_output.txt` with all your files combined, a table of contents, and file metadata.
19
+
20
+ ---
21
+
22
+ ## All Options
23
+
24
+ ```
25
+ Usage: repomeld [options]
26
+
27
+ Options:
28
+ -V, --version Show version
29
+ -h, --help Show help
30
+
31
+ Output:
32
+ -o, --output <filename> Output file name (default: "repomeld_output.txt")
33
+
34
+ Filtering:
35
+ -e, --ext <exts...> Only include specific extensions
36
+ --include <patterns...> Only include files matching patterns
37
+ --exclude <patterns...> Exclude files matching patterns
38
+ -i, --ignore <names...> Extra folders/files to ignore
39
+ --max-size <kb> Skip files larger than N KB (default: 500)
40
+
41
+ Formatting:
42
+ -s, --style <style> Header style: banner | markdown | minimal (default: banner)
43
+ --no-toc Disable table of contents
44
+ --no-meta Hide file metadata (lines, size, lang)
45
+ --trim Trim leading/trailing whitespace per file
46
+
47
+ Advanced:
48
+ --lines-before <n> Skip first N lines of each file
49
+ --lines-after <n> Skip last N lines of each file
50
+ --dry-run Preview what would be included — don't write output
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Examples
56
+
57
+ ```bash
58
+ # Basic usage
59
+ repomeld
60
+
61
+ # Only JavaScript and TypeScript files
62
+ repomeld --ext js ts jsx tsx
63
+
64
+ # Only files inside src/ folder
65
+ repomeld --include src/
66
+
67
+ # Exclude test files
68
+ repomeld --exclude test spec __tests__
69
+
70
+ # Markdown output style (great for AI prompts)
71
+ repomeld --style markdown --output context.md
72
+
73
+ # Minimal headers
74
+ repomeld --style minimal
75
+
76
+ # Custom output file
77
+ repomeld --output all_code.txt
78
+
79
+ # Skip large files (over 100 KB)
80
+ repomeld --max-size 100
81
+
82
+ # No table of contents, no metadata
83
+ repomeld --no-toc --no-meta
84
+
85
+ # Dry run — see what would be included without writing
86
+ repomeld --dry-run
87
+
88
+ # Ignore extra folders
89
+ repomeld --ignore dist .next coverage
90
+
91
+ # Combine options
92
+ repomeld --ext ts tsx --include src/ --style markdown --output ai_context.md
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Output Format (banner style — default)
98
+
99
+ ```
100
+ # Generated by repomeld v1.0.0
101
+ # Date : 2024-01-01T00:00:00.000Z
102
+ # Source : /your/project
103
+ # Files : 12
104
+ # Lines : 843
105
+
106
+ TABLE OF CONTENTS
107
+ ════════════════════════════════════════════════════════════
108
+ 1. src/index.ts
109
+ 2. src/utils/helper.ts
110
+ ...
111
+ ════════════════════════════════════════════════════════════
112
+
113
+ ────────────────────────────────────────────────────────────
114
+ FILE: src/index.ts [42 lines | 1.2 KB | typescript]
115
+ ────────────────────────────────────────────────────────────
116
+
117
+ <file contents>
118
+ ```
119
+
120
+ ## Output Format (markdown style)
121
+
122
+ ````
123
+ ## 📄 src/index.ts [42 lines | 1.2 KB | typescript]
124
+
125
+ ```typescript
126
+ <file contents>
127
+ ```
128
+ ````
129
+
130
+ ---
131
+
132
+ ## Auto-ignored (always skipped)
133
+
134
+ - `node_modules/`, `.git/`, `dist/`, `build/`, `.next/`, `.nuxt/`, `.cache/`
135
+ - `.env`, `.env.local`, `.env.production`
136
+ - `.DS_Store`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`
137
+ - Binary files (images, executables, etc.)
138
+ - The output file itself
139
+
140
+ ---
141
+
142
+ ## Publish / Update
143
+
144
+ ```bash
145
+ npm version patch # 1.0.0 → 1.0.1
146
+ npm publish
147
+ ```
package/bin/cli.js ADDED
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { program } = require("commander");
6
+
7
+ const VERSION = "1.0.0";
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
+ ];
25
+
26
+ const LANGUAGE_MAP = {
27
+ js: "javascript", jsx: "javascript", ts: "typescript", tsx: "typescript",
28
+ py: "python", rb: "ruby", java: "java", cpp: "cpp", c: "c",
29
+ cs: "csharp", go: "go", rs: "rust", php: "php", swift: "swift",
30
+ kt: "kotlin", html: "html", css: "css", scss: "scss", json: "json",
31
+ yaml: "yaml", yml: "yaml", md: "markdown", sh: "bash", bash: "bash",
32
+ toml: "toml", xml: "xml", sql: "sql", graphql: "graphql",
33
+ };
34
+
35
+ function getLanguage(filePath) {
36
+ const ext = path.extname(filePath).slice(1).toLowerCase();
37
+ return LANGUAGE_MAP[ext] || "";
38
+ }
39
+
40
+ function getAllFiles(dirPath, ignoreList, fileList = []) {
41
+ let entries;
42
+ try {
43
+ entries = fs.readdirSync(dirPath, { withFileTypes: true });
44
+ } catch {
45
+ return fileList;
46
+ }
47
+
48
+ for (const entry of entries) {
49
+ const fullPath = path.join(dirPath, entry.name);
50
+ const relativePath = path.relative(process.cwd(), fullPath);
51
+
52
+ if (
53
+ ignoreList.some(
54
+ (ig) => entry.name === ig || relativePath.startsWith(ig + path.sep) || relativePath === ig
55
+ )
56
+ ) {
57
+ continue;
58
+ }
59
+
60
+ if (entry.isDirectory()) {
61
+ getAllFiles(fullPath, ignoreList, fileList);
62
+ } else if (entry.isFile()) {
63
+ fileList.push(fullPath);
64
+ }
65
+ }
66
+
67
+ return fileList;
68
+ }
69
+
70
+ function isBinaryFile(filePath) {
71
+ try {
72
+ const buffer = Buffer.alloc(512);
73
+ const fd = fs.openSync(filePath, "r");
74
+ const bytesRead = fs.readSync(fd, buffer, 0, 512, 0);
75
+ fs.closeSync(fd);
76
+ for (let i = 0; i < bytesRead; i++) {
77
+ if (buffer[i] === 0) return true;
78
+ }
79
+ return false;
80
+ } catch {
81
+ return true;
82
+ }
83
+ }
84
+
85
+ function matchesExtensions(filePath, exts) {
86
+ if (!exts || exts.length === 0) return true;
87
+ const ext = path.extname(filePath).slice(1).toLowerCase();
88
+ return exts.map((e) => e.replace(/^\./, "").toLowerCase()).includes(ext);
89
+ }
90
+
91
+ function matchesPattern(filePath, patterns) {
92
+ if (!patterns || patterns.length === 0) return false;
93
+ const rel = path.relative(process.cwd(), filePath);
94
+ return patterns.some((p) => rel.includes(p) || path.basename(filePath).includes(p));
95
+ }
96
+
97
+ function formatSize(bytes) {
98
+ if (bytes < 1024) return `${bytes} B`;
99
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
100
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
101
+ }
102
+
103
+ function printBanner() {
104
+ console.log(`
105
+ ╔══════════════════════════════════╗
106
+ ║ repomeld v${VERSION} ║
107
+ ║ Meld your repo into one file ║
108
+ ╚══════════════════════════════════╝`);
109
+ }
110
+
111
+ function buildHeader(style, relativePath, filePath, lineCount, showMeta) {
112
+ const lang = getLanguage(filePath);
113
+ const size = formatSize(fs.statSync(filePath).size);
114
+ const meta = showMeta ? ` [${lineCount} lines | ${size}${lang ? " | " + lang : ""}]` : "";
115
+
116
+ if (style === "markdown") {
117
+ return `\n## 📄 ${relativePath}${meta}\n\n\`\`\`${lang}\n`;
118
+ }
119
+ if (style === "minimal") {
120
+ return `\n# ${relativePath}\n`;
121
+ }
122
+ // default: banner style
123
+ const divider = "─".repeat(60);
124
+ return `\n${divider}\n FILE: ${relativePath}${meta}\n${divider}\n\n`;
125
+ }
126
+
127
+ function buildFooter(style) {
128
+ if (style === "markdown") return "\n```\n";
129
+ return "\n";
130
+ }
131
+
132
+ function buildTableOfContents(files, cwd) {
133
+ let toc = "TABLE OF CONTENTS\n" + "═".repeat(60) + "\n";
134
+ files.forEach((f, i) => {
135
+ const rel = path.relative(cwd, f);
136
+ toc += ` ${String(i + 1).padStart(3, " ")}. ${rel}\n`;
137
+ });
138
+ toc += "═".repeat(60) + "\n\n";
139
+ return toc;
140
+ }
141
+
142
+ function repomeld(options) {
143
+ printBanner();
144
+
145
+ const cwd = process.cwd();
146
+ const outputFile = path.resolve(cwd, options.output);
147
+ const ignoreList = [...DEFAULT_IGNORE, ...(options.ignore || [])];
148
+ const filterExts = options.ext || [];
149
+ const maxFileSizeBytes = (parseFloat(options.maxSize) || 500) * 1024;
150
+ const headerStyle = options.style || "banner";
151
+ const showMeta = !options.noMeta;
152
+ const showToc = !options.noToc;
153
+ const dryRun = options.dryRun || false;
154
+ const include = options.include || [];
155
+ const exclude = options.exclude || [];
156
+ const linesBefore = parseInt(options.linesBefore) || 0;
157
+ const linesAfter = parseInt(options.linesAfter) || 0;
158
+
159
+ console.log(`\n 📂 Source : ${cwd}`);
160
+ console.log(` 📄 Output : ${path.relative(cwd, outputFile)}`);
161
+ console.log(` 🎨 Style : ${headerStyle}`);
162
+ if (filterExts.length) console.log(` 🔍 Filter : .${filterExts.join(", .")}`);
163
+ if (dryRun) console.log(` 🧪 Dry run : no file will be written`);
164
+ console.log();
165
+
166
+ let allFiles = getAllFiles(cwd, ignoreList);
167
+
168
+ // Filter by extension
169
+ if (filterExts.length) {
170
+ allFiles = allFiles.filter((f) => matchesExtensions(f, filterExts));
171
+ }
172
+
173
+ // Include pattern filter
174
+ if (include.length) {
175
+ allFiles = allFiles.filter((f) => matchesPattern(f, include));
176
+ }
177
+
178
+ // Exclude pattern filter
179
+ if (exclude.length) {
180
+ allFiles = allFiles.filter((f) => !matchesPattern(f, exclude));
181
+ }
182
+
183
+ // Remove output file from list
184
+ allFiles = allFiles.filter((f) => path.resolve(f) !== outputFile);
185
+
186
+ if (allFiles.length === 0) {
187
+ console.log(" ⚠️ No matching files found.\n");
188
+ return;
189
+ }
190
+
191
+ let combinedContent = "";
192
+ let skipped = 0;
193
+ let included = 0;
194
+ let totalLines = 0;
195
+ const includedFiles = [];
196
+
197
+ for (const filePath of allFiles) {
198
+ const relativePath = path.relative(cwd, filePath);
199
+
200
+ if (isBinaryFile(filePath)) {
201
+ console.log(` ⏭ Binary : ${relativePath}`);
202
+ skipped++;
203
+ continue;
204
+ }
205
+
206
+ const stat = fs.statSync(filePath);
207
+ if (stat.size > maxFileSizeBytes) {
208
+ console.log(` ⏭ Too large: ${relativePath} (${formatSize(stat.size)})`);
209
+ skipped++;
210
+ continue;
211
+ }
212
+
213
+ try {
214
+ let content = fs.readFileSync(filePath, "utf8");
215
+
216
+ // Trim leading/trailing blank lines if requested
217
+ if (options.trim) {
218
+ content = content.trim();
219
+ }
220
+
221
+ // Slice specific lines
222
+ if (linesBefore > 0 || linesAfter > 0) {
223
+ const lines = content.split("\n");
224
+ const start = linesBefore;
225
+ const end = linesAfter > 0 ? lines.length - linesAfter : lines.length;
226
+ content = lines.slice(start, end).join("\n");
227
+ }
228
+
229
+ const lineCount = content.split("\n").length;
230
+ totalLines += lineCount;
231
+ includedFiles.push(filePath);
232
+
233
+ combinedContent += buildHeader(headerStyle, relativePath, filePath, lineCount, showMeta);
234
+ combinedContent += content;
235
+ combinedContent += buildFooter(headerStyle);
236
+
237
+ console.log(` ✅ ${relativePath}`);
238
+ included++;
239
+ } catch (err) {
240
+ console.log(` ❌ Error: ${relativePath} — ${err.message}`);
241
+ skipped++;
242
+ }
243
+ }
244
+
245
+ // Build final output
246
+ let finalOutput = "";
247
+
248
+ // Top-level comment
249
+ const timestamp = new Date().toISOString();
250
+ finalOutput += `# Generated by repomeld v${VERSION}\n`;
251
+ finalOutput += `# Date : ${timestamp}\n`;
252
+ finalOutput += `# Source : ${cwd}\n`;
253
+ finalOutput += `# Files : ${included}\n`;
254
+ finalOutput += `# Lines : ${totalLines}\n\n`;
255
+
256
+ if (showToc) {
257
+ finalOutput += buildTableOfContents(includedFiles, cwd);
258
+ }
259
+
260
+ finalOutput += combinedContent;
261
+
262
+ if (!dryRun) {
263
+ fs.writeFileSync(outputFile, finalOutput, "utf8");
264
+ }
265
+
266
+ const outputSize = formatSize(Buffer.byteLength(finalOutput, "utf8"));
267
+
268
+ console.log(`
269
+ ✨ repomeld complete!
270
+ ─────────────────────────────
271
+ ✅ Included : ${included} files
272
+ ⏭ Skipped : ${skipped} files
273
+ 📏 Lines : ${totalLines}
274
+ 💾 Size : ${outputSize}
275
+ 📄 Output : ${options.output}${dryRun ? " (dry run — not written)" : ""}
276
+ `);
277
+ }
278
+
279
+ // ─── CLI Definition ───────────────────────────────────────────
280
+
281
+ program
282
+ .name("repomeld")
283
+ .description("Meld your entire repo into a single file — perfect for AI context, code reviews & sharing")
284
+ .version(VERSION)
285
+
286
+ // Output
287
+ .option("-o, --output <filename>", "Output file name", "repomeld_output.txt")
288
+
289
+ // Filtering
290
+ .option("-e, --ext <exts...>", "Only include specific extensions e.g. --ext js ts jsx")
291
+ .option("--include <patterns...>", "Only include files matching patterns e.g. --include src/")
292
+ .option("--exclude <patterns...>", "Exclude files matching patterns e.g. --exclude test spec")
293
+ .option("-i, --ignore <names...>", "Extra folders/files to ignore e.g. --ignore dist .next")
294
+ .option("--max-size <kb>", "Skip files larger than N KB (default 500)","500")
295
+
296
+ // Formatting
297
+ .option("-s, --style <style>", "Header style: banner | markdown | minimal (default: banner)", "banner")
298
+ .option("--no-toc", "Disable table of contents")
299
+ .option("--no-meta", "Hide file metadata (lines, size, lang)")
300
+ .option("--trim", "Trim leading/trailing whitespace per file")
301
+
302
+ // Advanced
303
+ .option("--lines-before <n>", "Skip first N lines of each file")
304
+ .option("--lines-after <n>", "Skip last N lines of each file")
305
+ .option("--dry-run", "Preview what would be included — don't write output")
306
+
307
+ .action((options) => {
308
+ repomeld(options);
309
+ });
310
+
311
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "repomeld",
3
+ "version": "1.0.0",
4
+ "description": "Meld your entire repo into a single file — perfect for AI context, code reviews & sharing",
5
+ "main": "bin/cli.js",
6
+ "bin": {
7
+ "repomeld": "bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/cli.js"
11
+ },
12
+ "keywords": [
13
+ "cli",
14
+ "repo",
15
+ "file",
16
+ "combiner",
17
+ "merge",
18
+ "concat",
19
+ "ai-context",
20
+ "code-review",
21
+ "repomeld"
22
+ ],
23
+ "author": "",
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "commander": "^11.0.0"
27
+ },
28
+ "engines": {
29
+ "node": ">=14.0.0"
30
+ }
31
+ }