repomeld 2.0.5 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -116
- package/bin/cli.js +16 -480
- package/package.json +24 -41
- package/src/core/fileScanner.js +47 -0
- package/src/core/formatter.js +51 -0
- package/src/core/ignoreBuilder.js +41 -0
- package/src/core/pathResolver.js +24 -0
- package/src/core/progress.js +29 -0
- package/src/index.js +192 -0
- package/src/updates/updateChecker.js +43 -0
- package/src/utils/backup.js +48 -0
- package/src/utils/constants.js +40 -0
- package/src/utils/helpers.js +37 -0
package/bin/cli.js
CHANGED
|
@@ -1,499 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const fs = require("fs").promises;
|
|
4
|
-
const fsSync = require("fs");
|
|
5
|
-
const path = require("path");
|
|
6
3
|
const { program } = require("commander");
|
|
7
|
-
const
|
|
8
|
-
const {
|
|
9
|
-
const readline = require("readline");
|
|
10
|
-
const os = require("os");
|
|
11
|
-
|
|
12
|
-
const VERSION = "1.0.0";
|
|
13
|
-
const PACKAGE_NAME = "repomeld";
|
|
14
|
-
|
|
15
|
-
// Normalize paths for cross-platform compatibility
|
|
16
|
-
const normalizePath = (p) => p.split(path.sep).join('/');
|
|
17
|
-
|
|
18
|
-
const DEFAULT_IGNORE = [
|
|
19
|
-
"node_modules",
|
|
20
|
-
".git",
|
|
21
|
-
".env",
|
|
22
|
-
".env.local",
|
|
23
|
-
".env.production",
|
|
24
|
-
".DS_Store",
|
|
25
|
-
"package-lock.json",
|
|
26
|
-
"yarn.lock",
|
|
27
|
-
"pnpm-lock.yaml",
|
|
28
|
-
".next",
|
|
29
|
-
".nuxt",
|
|
30
|
-
"dist",
|
|
31
|
-
"build",
|
|
32
|
-
".cache"
|
|
33
|
-
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
const LANGUAGE_MAP = {
|
|
37
|
-
js: "javascript", jsx: "javascript", ts: "typescript", tsx: "typescript",
|
|
38
|
-
py: "python", rb: "ruby", java: "java", cpp: "cpp", c: "c",
|
|
39
|
-
cs: "csharp", go: "go", rs: "rust", php: "php", swift: "swift",
|
|
40
|
-
kt: "kotlin", html: "html", css: "css", scss: "scss", json: "json",
|
|
41
|
-
yaml: "yaml", yml: "yaml", md: "markdown", sh: "bash", bash: "bash",
|
|
42
|
-
toml: "toml", xml: "xml", sql: "sql", graphql: "graphql",
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
async function loadIgnoreConfig() {
|
|
46
|
-
const configPath = path.resolve(process.cwd(), "repomeld.ignore.json");
|
|
47
|
-
try {
|
|
48
|
-
const data = JSON.parse(await fs.readFile(configPath, "utf8"));
|
|
49
|
-
if (Array.isArray(data.ignore)) return data.ignore;
|
|
50
|
-
} catch {
|
|
51
|
-
// File doesn't exist or invalid JSON, use defaults
|
|
52
|
-
}
|
|
53
|
-
return [];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function getLanguage(filePath) {
|
|
57
|
-
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
58
|
-
return LANGUAGE_MAP[ext] || "";
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function getAllFilesWithIgnore(dirPath, ig, forceIncludePatterns, rootDir = process.cwd(), progress = null) {
|
|
62
|
-
const fileList = [];
|
|
63
|
-
const stack = [{ dirPath, relativePath: '.' }];
|
|
64
|
-
|
|
65
|
-
while (stack.length) {
|
|
66
|
-
const { dirPath: currentDir, relativePath: currentRelative } = stack.pop();
|
|
67
|
-
|
|
68
|
-
let entries;
|
|
69
|
-
try {
|
|
70
|
-
entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
71
|
-
} catch (err) {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
for (const entry of entries) {
|
|
76
|
-
const fullPath = path.join(currentDir, entry.name);
|
|
77
|
-
const relativePath = path.join(currentRelative, entry.name);
|
|
78
|
-
const normalizedPath = normalizePath(relativePath);
|
|
79
|
-
|
|
80
|
-
// Check force-include first - these always go through
|
|
81
|
-
const isForceIncluded = forceIncludePatterns && forceIncludePatterns.some(pattern =>
|
|
82
|
-
normalizedPath.includes(pattern) || entry.name.includes(pattern)
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
// Only check ignore if not force-included
|
|
86
|
-
if (!isForceIncluded && ig.ignores(normalizedPath)) {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (entry.isDirectory()) {
|
|
91
|
-
stack.push({ dirPath: fullPath, relativePath });
|
|
92
|
-
} else if (entry.isFile()) {
|
|
93
|
-
fileList.push(fullPath);
|
|
94
|
-
if (progress && fileList.length % 100 === 0) {
|
|
95
|
-
progress.update(fileList.length);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return fileList;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function buildIgnoreFilter(options) {
|
|
105
|
-
const ig = ignore();
|
|
106
|
-
|
|
107
|
-
// Add default ignores
|
|
108
|
-
ig.add(DEFAULT_IGNORE);
|
|
109
|
-
|
|
110
|
-
// Add custom config ignores
|
|
111
|
-
const customIgnores = await loadIgnoreConfig();
|
|
112
|
-
ig.add(customIgnores);
|
|
113
|
-
|
|
114
|
-
// Add CLI ignores
|
|
115
|
-
if (options.ignore && options.ignore.length) {
|
|
116
|
-
ig.add(options.ignore);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Add .gitignore patterns if not disabled
|
|
120
|
-
if (!options.noGitignore) {
|
|
121
|
-
try {
|
|
122
|
-
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
123
|
-
const gitignoreContent = await fs.readFile(gitignorePath, "utf8");
|
|
124
|
-
ig.add(gitignoreContent);
|
|
125
|
-
} catch {
|
|
126
|
-
// No .gitignore file, ignore silently
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return { ig, forceInclude: options.forceInclude || null };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function matchesExtensions(filePath, exts) {
|
|
134
|
-
if (!exts || exts.length === 0) return true;
|
|
135
|
-
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
136
|
-
return exts.map((e) => e.replace(/^\./, "").toLowerCase()).includes(ext);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function matchesPattern(filePath, patterns) {
|
|
140
|
-
if (!patterns || patterns.length === 0) return false;
|
|
141
|
-
const rel = path.relative(process.cwd(), filePath);
|
|
142
|
-
return patterns.some((p) => rel.includes(p) || path.basename(filePath).includes(p));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function formatSize(bytes) {
|
|
146
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
147
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
148
|
-
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
149
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function formatDuration(ms) {
|
|
153
|
-
if (ms < 1000) return `${ms}ms`;
|
|
154
|
-
const seconds = (ms / 1000).toFixed(1);
|
|
155
|
-
return `${seconds}s`;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
class ProgressIndicator {
|
|
159
|
-
constructor(total, prefix = '') {
|
|
160
|
-
this.total = total;
|
|
161
|
-
this.prefix = prefix;
|
|
162
|
-
this.current = 0;
|
|
163
|
-
this.startTime = Date.now();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
update(current) {
|
|
167
|
-
this.current = current;
|
|
168
|
-
this.render();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
increment() {
|
|
172
|
-
this.current++;
|
|
173
|
-
this.render();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
render() {
|
|
177
|
-
const percent = (this.current / this.total * 100).toFixed(1);
|
|
178
|
-
const elapsed = Date.now() - this.startTime;
|
|
179
|
-
const rate = this.current / (elapsed / 1000);
|
|
180
|
-
const eta = rate > 0 ? ((this.total - this.current) / rate).toFixed(0) : '?';
|
|
181
|
-
|
|
182
|
-
process.stdout.write(`\r${this.prefix} ${this.current}/${this.total} files (${percent}%) | ${formatDuration(elapsed)} elapsed | ETA: ${eta}s`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
finish() {
|
|
186
|
-
const elapsed = Date.now() - this.startTime;
|
|
187
|
-
console.log(`\r${this.prefix} ✅ Completed ${this.current}/${this.total} files in ${formatDuration(elapsed)}`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function printBanner() {
|
|
192
|
-
console.log(`
|
|
193
|
-
╔══════════════════════════════════════════════════════╗
|
|
194
|
-
║ repomeld v${VERSION} ║
|
|
195
|
-
║ Meld your repo into one file 🔥 ║
|
|
196
|
-
╠══════════════════════════════════════════════════════╣
|
|
197
|
-
║ 💼 Author available for freelance & full-time work ║
|
|
198
|
-
║ 📧 susheelhbti@gmail.com ║
|
|
199
|
-
╚══════════════════════════════════════════════════════╝`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function buildHeader(style, relativePath, filePath, lineCount, showMeta, stats) {
|
|
203
|
-
const lang = getLanguage(filePath);
|
|
204
|
-
const meta = showMeta ? ` [${lineCount} lines | ${formatSize(stats.size)}${lang ? " | " + lang : ""}]` : "";
|
|
205
|
-
|
|
206
|
-
if (style === "markdown") {
|
|
207
|
-
return `\n## 📄 ${relativePath}${meta}\n\n\`\`\`${lang}\n`;
|
|
208
|
-
}
|
|
209
|
-
if (style === "minimal") {
|
|
210
|
-
return `\n# ${relativePath}\n`;
|
|
211
|
-
}
|
|
212
|
-
const divider = "─".repeat(60);
|
|
213
|
-
return `\n${divider}\n FILE: ${relativePath}${meta}\n${divider}\n\n`;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function buildFooter(style) {
|
|
217
|
-
if (style === "markdown") return "\n```\n";
|
|
218
|
-
return "\n";
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function buildTableOfContents(files, cwd) {
|
|
222
|
-
let toc = "TABLE OF CONTENTS\n" + "═".repeat(60) + "\n";
|
|
223
|
-
files.forEach((f, i) => {
|
|
224
|
-
const rel = path.relative(cwd, f);
|
|
225
|
-
toc += ` ${String(i + 1).padStart(3, " ")}. ${rel}\n`;
|
|
226
|
-
});
|
|
227
|
-
toc += "═".repeat(60) + "\n\n";
|
|
228
|
-
return toc;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function resolveOutputPath(desiredPath) {
|
|
232
|
-
try {
|
|
233
|
-
await fs.access(desiredPath);
|
|
234
|
-
const ext = path.extname(desiredPath);
|
|
235
|
-
const base = desiredPath.slice(0, desiredPath.length - ext.length);
|
|
236
|
-
let counter = 2;
|
|
237
|
-
while (true) {
|
|
238
|
-
const candidate = `${base}__${counter}${ext}`;
|
|
239
|
-
try {
|
|
240
|
-
await fs.access(candidate);
|
|
241
|
-
counter++;
|
|
242
|
-
} catch {
|
|
243
|
-
return { path: candidate, number: counter };
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
} catch {
|
|
247
|
-
return { path: desiredPath, number: null };
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async function isRepomeldOutput(filePath, baseOutputName) {
|
|
252
|
-
const fileName = path.basename(filePath);
|
|
253
|
-
const ext = path.extname(baseOutputName);
|
|
254
|
-
const base = path.basename(baseOutputName, ext);
|
|
255
|
-
const pattern = new RegExp(`^${escapeRegex(base)}(__\\d+)?${escapeRegex(ext)}$`);
|
|
256
|
-
return pattern.test(fileName);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function escapeRegex(str) {
|
|
260
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function checkForUpdates() {
|
|
264
|
-
return new Promise((resolve) => {
|
|
265
|
-
const http = require('http');
|
|
266
|
-
const options = {
|
|
267
|
-
hostname: 'registry.npmjs.org',
|
|
268
|
-
path: `/${PACKAGE_NAME}/latest`,
|
|
269
|
-
method: 'GET',
|
|
270
|
-
timeout: 3000
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
const req = http.request(options, (res) => {
|
|
274
|
-
let data = '';
|
|
275
|
-
res.on('data', chunk => data += chunk);
|
|
276
|
-
res.on('end', () => {
|
|
277
|
-
try {
|
|
278
|
-
const json = JSON.parse(data);
|
|
279
|
-
if (json.version && json.version !== VERSION) {
|
|
280
|
-
resolve({ hasUpdate: true, latestVersion: json.version });
|
|
281
|
-
} else {
|
|
282
|
-
resolve({ hasUpdate: false });
|
|
283
|
-
}
|
|
284
|
-
} catch {
|
|
285
|
-
resolve({ hasUpdate: false });
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
req.on('error', () => resolve({ hasUpdate: false }));
|
|
291
|
-
req.on('timeout', () => {
|
|
292
|
-
req.destroy();
|
|
293
|
-
resolve({ hasUpdate: false });
|
|
294
|
-
});
|
|
295
|
-
req.end();
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function showUpdateMessage(currentVersion, latestVersion) {
|
|
300
|
-
console.log(`\n${'═'.repeat(60)}`);
|
|
301
|
-
console.log(` ⭐ New version available: ${currentVersion} → ${latestVersion}`);
|
|
302
|
-
console.log(` 📦 Update with: npm install -g ${PACKAGE_NAME}@latest`);
|
|
303
|
-
console.log(`${'═'.repeat(60)}\n`);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
async function repomeld(options) {
|
|
307
|
-
const startTime = Date.now();
|
|
308
|
-
printBanner();
|
|
309
|
-
|
|
310
|
-
const cwd = process.cwd();
|
|
311
|
-
|
|
312
|
-
const { path: outputFile, number: outputNumber } = await resolveOutputPath(path.resolve(cwd, options.output));
|
|
313
|
-
const outputBaseName = path.basename(options.output);
|
|
314
|
-
|
|
315
|
-
const { ig, forceInclude } = await buildIgnoreFilter(options);
|
|
316
|
-
const filterExts = options.ext || [];
|
|
317
|
-
const maxFileSizeBytes = (parseFloat(options.maxSize) || 500) * 1024;
|
|
318
|
-
const headerStyle = options.style || "banner";
|
|
319
|
-
const showMeta = !options.noMeta;
|
|
320
|
-
const showToc = !options.noToc;
|
|
321
|
-
const dryRun = options.dryRun || false;
|
|
322
|
-
const include = options.include || [];
|
|
323
|
-
const exclude = options.exclude || [];
|
|
324
|
-
const linesBefore = parseInt(options.linesBefore) || 0;
|
|
325
|
-
const linesAfter = parseInt(options.linesAfter) || 0;
|
|
326
|
-
|
|
327
|
-
console.log(`\n 📂 Source : ${cwd}`);
|
|
328
|
-
console.log(` 📄 Output : ${path.relative(cwd, outputFile)}`);
|
|
329
|
-
console.log(` 🎨 Style : ${headerStyle}`);
|
|
330
|
-
if (!options.noGitignore) console.log(` 📁 .gitignore respected`);
|
|
331
|
-
if (filterExts.length) console.log(` 🔍 Filter : .${filterExts.join(", .")}`);
|
|
332
|
-
if (forceInclude && forceInclude.length) console.log(` 📌 Force : ${forceInclude.join(", ")}`);
|
|
333
|
-
if (dryRun) console.log(` 🧪 Dry run : no file will be written`);
|
|
334
|
-
console.log();
|
|
335
|
-
|
|
336
|
-
console.log(` 🔍 Scanning files...`);
|
|
337
|
-
const scanStartTime = Date.now();
|
|
338
|
-
let allFiles = await getAllFilesWithIgnore(cwd, ig, forceInclude, cwd);
|
|
339
|
-
console.log(` ✅ Found ${allFiles.length} files in ${formatDuration(Date.now() - scanStartTime)}`);
|
|
340
|
-
|
|
341
|
-
// Apply additional filters
|
|
342
|
-
if (filterExts.length) {
|
|
343
|
-
allFiles = allFiles.filter(f => matchesExtensions(f, filterExts));
|
|
344
|
-
}
|
|
345
|
-
if (include.length) {
|
|
346
|
-
allFiles = allFiles.filter(f => matchesPattern(f, include));
|
|
347
|
-
}
|
|
348
|
-
if (exclude.length) {
|
|
349
|
-
allFiles = allFiles.filter(f => !matchesPattern(f, exclude));
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Remove repomeld output files
|
|
353
|
-
const filteredFiles = [];
|
|
354
|
-
for (const file of allFiles) {
|
|
355
|
-
const isOutput = await isRepomeldOutput(file, outputBaseName);
|
|
356
|
-
if (!isOutput) filteredFiles.push(file);
|
|
357
|
-
}
|
|
358
|
-
allFiles = filteredFiles;
|
|
359
|
-
|
|
360
|
-
if (allFiles.length === 0) {
|
|
361
|
-
console.log(" ⚠️ No matching files found.\n");
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
console.log(` 📝 Processing ${allFiles.length} files...\n`);
|
|
366
|
-
|
|
367
|
-
let combinedContent = "";
|
|
368
|
-
let skipped = 0;
|
|
369
|
-
let included = 0;
|
|
370
|
-
let totalLines = 0;
|
|
371
|
-
const includedFiles = [];
|
|
372
|
-
|
|
373
|
-
const progress = new ProgressIndicator(allFiles.length, ' ');
|
|
374
|
-
|
|
375
|
-
for (let i = 0; i < allFiles.length; i++) {
|
|
376
|
-
const filePath = allFiles[i];
|
|
377
|
-
const relativePath = path.relative(cwd, filePath);
|
|
378
|
-
|
|
379
|
-
progress.update(i + 1);
|
|
380
|
-
|
|
381
|
-
const isBinary = await isBinaryFile(filePath).catch(() => true);
|
|
382
|
-
if (isBinary) {
|
|
383
|
-
skipped++;
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const stats = await fs.stat(filePath);
|
|
388
|
-
if (stats.size > maxFileSizeBytes) {
|
|
389
|
-
skipped++;
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
try {
|
|
394
|
-
let content = await fs.readFile(filePath, "utf8");
|
|
395
|
-
|
|
396
|
-
if (options.trim) {
|
|
397
|
-
content = content.trim();
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (linesBefore > 0 || linesAfter > 0) {
|
|
401
|
-
const lines = content.split("\n");
|
|
402
|
-
const start = Math.min(linesBefore, lines.length);
|
|
403
|
-
const end = linesAfter > 0 ? Math.max(0, lines.length - linesAfter) : lines.length;
|
|
404
|
-
content = lines.slice(start, end).join("\n");
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const lineCount = content.split("\n").length;
|
|
408
|
-
totalLines += lineCount;
|
|
409
|
-
includedFiles.push(filePath);
|
|
410
|
-
|
|
411
|
-
combinedContent += buildHeader(headerStyle, relativePath, filePath, lineCount, showMeta, stats);
|
|
412
|
-
combinedContent += content;
|
|
413
|
-
combinedContent += buildFooter(headerStyle);
|
|
414
|
-
|
|
415
|
-
included++;
|
|
416
|
-
} catch (err) {
|
|
417
|
-
skipped++;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
progress.finish();
|
|
422
|
-
|
|
423
|
-
// Build final output
|
|
424
|
-
let finalOutput = "";
|
|
425
|
-
const timestamp = new Date().toISOString();
|
|
426
|
-
finalOutput += `# Generated by repomeld v${VERSION}\n`;
|
|
427
|
-
finalOutput += `# Date : ${timestamp}\n`;
|
|
428
|
-
finalOutput += `# Source : ${cwd}\n`;
|
|
429
|
-
finalOutput += `# Files : ${included}\n`;
|
|
430
|
-
finalOutput += `# Lines : ${totalLines}\n`;
|
|
431
|
-
finalOutput += `# Author : susheelhbti@gmail.com — available for freelance & full-time work\n\n`;
|
|
432
|
-
|
|
433
|
-
if (showToc && includedFiles.length > 0) {
|
|
434
|
-
finalOutput += buildTableOfContents(includedFiles, cwd);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
finalOutput += combinedContent;
|
|
438
|
-
|
|
439
|
-
if (!dryRun && includedFiles.length > 0) {
|
|
440
|
-
await fs.writeFile(outputFile, finalOutput, "utf8");
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const outputSize = formatSize(Buffer.byteLength(finalOutput, "utf8"));
|
|
444
|
-
const totalTime = Date.now() - startTime;
|
|
445
|
-
|
|
446
|
-
console.log(`
|
|
447
|
-
✨ repomeld complete!
|
|
448
|
-
─────────────────────────────────────────────────
|
|
449
|
-
✅ Included : ${included} files
|
|
450
|
-
⏭ Skipped : ${skipped} files
|
|
451
|
-
📏 Lines : ${totalLines}
|
|
452
|
-
💾 Size : ${outputSize}
|
|
453
|
-
⏱️ Time : ${formatDuration(totalTime)}
|
|
454
|
-
📄 Output : ${path.relative(cwd, outputFile)}${dryRun ? " (dry run — not written)" : ""}
|
|
455
|
-
─────────────────────────────────────────────────`);
|
|
456
|
-
|
|
457
|
-
console.log(`\n 💼 Need a developer? susheelhbti@gmail.com`);
|
|
458
|
-
|
|
459
|
-
// Check for updates (non-blocking, no prompt)
|
|
460
|
-
if (!options.noUpdateCheck) {
|
|
461
|
-
const updateInfo = await checkForUpdates();
|
|
462
|
-
if (updateInfo.hasUpdate) {
|
|
463
|
-
showUpdateMessage(VERSION, updateInfo.latestVersion);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// ─── CLI Definition ───────────────────────────────────────────
|
|
4
|
+
const { repomeld } = require("../src/index");
|
|
5
|
+
const { VERSION } = require("../src/utils/constants");
|
|
469
6
|
|
|
470
7
|
program
|
|
471
8
|
.name("repomeld")
|
|
472
|
-
.description("Meld your entire repo into a single file — perfect for AI context
|
|
9
|
+
.description("Meld your entire repo into a single file — perfect for AI context & code reviews")
|
|
473
10
|
.version(VERSION)
|
|
474
|
-
|
|
475
|
-
.option("-o, --output <filename>", "Output file name", "repomeld_output.txt")
|
|
11
|
+
.option("-o, --output <filename>", "Output filename", "repomeld_output.txt")
|
|
476
12
|
.option("-e, --ext <exts...>", "Only include specific extensions")
|
|
477
|
-
.option("--include <patterns...>", "
|
|
13
|
+
.option("--include <patterns...>", "Force include files matching patterns")
|
|
478
14
|
.option("--exclude <patterns...>", "Exclude files matching patterns")
|
|
479
|
-
.option("-i, --ignore <names...>", "
|
|
480
|
-
.option("--force-include <names...>", "Force
|
|
481
|
-
.option("--max-size <kb>", "
|
|
482
|
-
.option("--no-gitignore", "
|
|
15
|
+
.option("-i, --ignore <names...>", "Additional ignore patterns")
|
|
16
|
+
.option("--force-include <names...>", "Force include even if ignored")
|
|
17
|
+
.option("--max-size <kb>", "Maximum file size in KB", "500")
|
|
18
|
+
.option("--no-gitignore", "Don't respect .gitignore")
|
|
483
19
|
.option("-s, --style <style>", "Header style: banner | markdown | minimal", "banner")
|
|
484
20
|
.option("--no-toc", "Disable table of contents")
|
|
485
21
|
.option("--no-meta", "Hide file metadata")
|
|
486
|
-
.option("--trim", "Trim
|
|
487
|
-
.option("--lines-before <n>", "Skip first N lines
|
|
488
|
-
.option("--lines-after <n>", "Skip last N lines
|
|
489
|
-
.option("--dry-run", "Preview without writing
|
|
490
|
-
.option("--no-
|
|
491
|
-
|
|
22
|
+
.option("--trim", "Trim whitespace from each file")
|
|
23
|
+
.option("--lines-before <n>", "Skip first N lines", parseInt)
|
|
24
|
+
.option("--lines-after <n>", "Skip last N lines", parseInt)
|
|
25
|
+
.option("--dry-run", "Preview without writing files")
|
|
26
|
+
.option("--no-backup", "Skip backup zip creation")
|
|
27
|
+
.option("--no-update-check", "Skip update check")
|
|
492
28
|
.action(async (options) => {
|
|
493
29
|
try {
|
|
494
30
|
await repomeld(options);
|
|
495
31
|
} catch (error) {
|
|
496
|
-
console.error(`\n
|
|
32
|
+
console.error(`\n❌ Error: ${error.message}`);
|
|
497
33
|
if (process.env.DEBUG) console.error(error);
|
|
498
34
|
process.exit(1);
|
|
499
35
|
}
|
package/package.json
CHANGED
|
@@ -1,58 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "repomeld",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Meld your entire repo into a single file — perfect for AI context
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"description": "Meld your entire repo into a single file — perfect for AI context & code reviews",
|
|
5
5
|
"main": "bin/cli.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"repomeld": "bin/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/cli.js",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
|
|
11
|
+
"dev": "node bin/cli.js --dry-run",
|
|
12
|
+
"test": "node bin/cli.js --dry-run --no-update-check"
|
|
13
|
+
|
|
14
14
|
},
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"merge",
|
|
21
|
-
"concat",
|
|
22
|
-
"ai-context",
|
|
23
|
-
"code-review",
|
|
24
|
-
"repomeld",
|
|
25
|
-
"git",
|
|
26
|
-
"repository"
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"src/",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
27
20
|
],
|
|
28
|
-
"author": "Susheel <susheelhbti@gmail.com>",
|
|
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",
|
|
38
21
|
"dependencies": {
|
|
22
|
+
"archiver": "^6.0.1",
|
|
39
23
|
"commander": "^11.1.0",
|
|
40
|
-
"ignore": "^5.3.
|
|
24
|
+
"ignore": "^5.3.0",
|
|
41
25
|
"isbinaryfile": "^5.0.0"
|
|
42
26
|
},
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
},
|
|
27
|
+
"keywords": ["cli", "repo", "combiner", "ai-context", "code-review", "mermaid", "dependency-graph"],
|
|
28
|
+
"author": "Susheel <susheelhbti@gmail.com>",
|
|
29
|
+
"license": "MIT",
|
|
47
30
|
"engines": {
|
|
48
31
|
"node": ">=14.0.0"
|
|
49
32
|
},
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/susheel/repomeld.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/susheel/repomeld/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/susheel/repomeld#readme"
|
|
41
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const fs = require("fs").promises;
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { normalizePath } = require("../utils/constants");
|
|
4
|
+
|
|
5
|
+
async function getAllFilesWithIgnore(dirPath, ig, forceIncludePatterns) {
|
|
6
|
+
const fileList = [];
|
|
7
|
+
const stack = [{ dirPath, relativePath: '.' }];
|
|
8
|
+
|
|
9
|
+
while (stack.length) {
|
|
10
|
+
const { dirPath: currentDir, relativePath: currentRelative } = stack.pop();
|
|
11
|
+
|
|
12
|
+
let entries;
|
|
13
|
+
try {
|
|
14
|
+
entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
15
|
+
} catch {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
21
|
+
const relativePath = path.join(currentRelative, entry.name);
|
|
22
|
+
const normalizedPath = normalizePath(relativePath);
|
|
23
|
+
|
|
24
|
+
// Check force include first - improved pattern matching
|
|
25
|
+
const isForceIncluded = forceIncludePatterns?.some(pattern => {
|
|
26
|
+
// Support exact matches and path contains
|
|
27
|
+
const patternClean = pattern.replace(/^\.\//, '').replace(/\/$/, '');
|
|
28
|
+
return normalizedPath === patternClean ||
|
|
29
|
+
normalizedPath.includes(patternClean) ||
|
|
30
|
+
entry.name === patternClean ||
|
|
31
|
+
normalizedPath.startsWith(patternClean + '/');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Only check ignore if not force-included
|
|
35
|
+
if (!isForceIncluded && ig.ignores(normalizedPath)) continue;
|
|
36
|
+
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
stack.push({ dirPath: fullPath, relativePath });
|
|
39
|
+
} else if (entry.isFile()) {
|
|
40
|
+
fileList.push(fullPath);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return fileList;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { getAllFilesWithIgnore };
|