repomeld 2.0.3 → 2.0.5
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 +337 -226
- package/bin/cli.js +367 -370
- package/package.json +32 -6
package/bin/cli.js
CHANGED
|
@@ -1,39 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const fs = require("fs");
|
|
3
|
+
const fs = require("fs").promises;
|
|
4
|
+
const fsSync = require("fs");
|
|
4
5
|
const path = require("path");
|
|
5
|
-
const readline = require("readline");
|
|
6
6
|
const { program } = require("commander");
|
|
7
|
+
const ignore = require("ignore");
|
|
8
|
+
const { isBinaryFile } = require("isbinaryfile");
|
|
9
|
+
const readline = require("readline");
|
|
10
|
+
const os = require("os");
|
|
7
11
|
|
|
8
|
-
const VERSION = "1.
|
|
12
|
+
const VERSION = "1.0.0";
|
|
13
|
+
const PACKAGE_NAME = "repomeld";
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
// Normalize paths for cross-platform compatibility
|
|
16
|
+
const normalizePath = (p) => p.split(path.sep).join('/');
|
|
11
17
|
|
|
12
18
|
const DEFAULT_IGNORE = [
|
|
13
|
-
"node_modules",
|
|
14
|
-
".
|
|
15
|
-
".
|
|
16
|
-
"
|
|
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
|
+
|
|
17
34
|
];
|
|
18
35
|
|
|
19
|
-
function loadIgnoreConfig() {
|
|
20
|
-
const configPath = path.resolve(process.cwd(), "repomeld.ignore.json");
|
|
21
|
-
const pkgDir = path.resolve(__dirname, "..", "repomeld.ignore.json");
|
|
22
|
-
for (const loc of [configPath, pkgDir]) {
|
|
23
|
-
if (fs.existsSync(loc)) {
|
|
24
|
-
try {
|
|
25
|
-
const data = JSON.parse(fs.readFileSync(loc, "utf8"));
|
|
26
|
-
if (Array.isArray(data.ignore)) return data.ignore;
|
|
27
|
-
} catch {
|
|
28
|
-
console.warn(` ⚠️ Could not parse ${loc}, using defaults.`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return [];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const IGNORE_FROM_CONFIG = loadIgnoreConfig();
|
|
36
|
-
|
|
37
36
|
const LANGUAGE_MAP = {
|
|
38
37
|
js: "javascript", jsx: "javascript", ts: "typescript", tsx: "typescript",
|
|
39
38
|
py: "python", rb: "ruby", java: "java", cpp: "cpp", c: "c",
|
|
@@ -43,36 +42,92 @@ const LANGUAGE_MAP = {
|
|
|
43
42
|
toml: "toml", xml: "xml", sql: "sql", graphql: "graphql",
|
|
44
43
|
};
|
|
45
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
|
+
|
|
46
56
|
function getLanguage(filePath) {
|
|
47
57
|
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
48
58
|
return LANGUAGE_MAP[ext] || "";
|
|
49
59
|
}
|
|
50
60
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
}
|
|
63
99
|
}
|
|
100
|
+
|
|
64
101
|
return fileList;
|
|
65
102
|
}
|
|
66
103
|
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 };
|
|
76
131
|
}
|
|
77
132
|
|
|
78
133
|
function matchesExtensions(filePath, exts) {
|
|
@@ -90,415 +145,357 @@ function matchesPattern(filePath, patterns) {
|
|
|
90
145
|
function formatSize(bytes) {
|
|
91
146
|
if (bytes < 1024) return `${bytes} B`;
|
|
92
147
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
93
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
148
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
149
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
94
150
|
}
|
|
95
151
|
|
|
96
|
-
|
|
152
|
+
function formatDuration(ms) {
|
|
153
|
+
if (ms < 1000) return `${ms}ms`;
|
|
154
|
+
const seconds = (ms / 1000).toFixed(1);
|
|
155
|
+
return `${seconds}s`;
|
|
156
|
+
}
|
|
97
157
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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)}`);
|
|
106
188
|
}
|
|
107
|
-
return candidate;
|
|
108
189
|
}
|
|
109
190
|
|
|
110
|
-
// ─── BANNER ───────────────────────────────────────────────────────────────────
|
|
111
|
-
|
|
112
191
|
function printBanner() {
|
|
113
192
|
console.log(`
|
|
114
|
-
|
|
115
|
-
║
|
|
116
|
-
║
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
╚══════════════════════════════════════════════════════╝`);
|
|
120
200
|
}
|
|
121
201
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
function buildHeader(relativePath, filePath, lineCount, showMeta) {
|
|
202
|
+
function buildHeader(style, relativePath, filePath, lineCount, showMeta, stats) {
|
|
125
203
|
const lang = getLanguage(filePath);
|
|
126
|
-
const
|
|
127
|
-
|
|
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
|
+
}
|
|
128
212
|
const divider = "─".repeat(60);
|
|
129
213
|
return `\n${divider}\n FILE: ${relativePath}${meta}\n${divider}\n\n`;
|
|
130
214
|
}
|
|
131
215
|
|
|
132
|
-
function buildFooter() {
|
|
216
|
+
function buildFooter(style) {
|
|
217
|
+
if (style === "markdown") return "\n```\n";
|
|
133
218
|
return "\n";
|
|
134
219
|
}
|
|
135
220
|
|
|
136
|
-
// ─── TABLE OF CONTENTS ────────────────────────────────────────────────────────
|
|
137
|
-
|
|
138
221
|
function buildTableOfContents(files, cwd) {
|
|
139
222
|
let toc = "TABLE OF CONTENTS\n" + "═".repeat(60) + "\n";
|
|
140
223
|
files.forEach((f, i) => {
|
|
141
|
-
|
|
224
|
+
const rel = path.relative(cwd, f);
|
|
225
|
+
toc += ` ${String(i + 1).padStart(3, " ")}. ${rel}\n`;
|
|
142
226
|
});
|
|
143
227
|
toc += "═".repeat(60) + "\n\n";
|
|
144
228
|
return toc;
|
|
145
229
|
}
|
|
146
230
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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;
|
|
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 };
|
|
228
244
|
}
|
|
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
245
|
}
|
|
242
|
-
|
|
246
|
+
} catch {
|
|
247
|
+
return { path: desiredPath, number: null };
|
|
243
248
|
}
|
|
244
|
-
|
|
245
|
-
return lines.slice(0, 30).join("\n") + "\n... (structure extraction not supported for this language)";
|
|
246
249
|
}
|
|
247
250
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
const
|
|
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
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return mermaid + "```\n";
|
|
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);
|
|
306
257
|
}
|
|
307
258
|
|
|
308
|
-
|
|
259
|
+
function escapeRegex(str) {
|
|
260
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
261
|
+
}
|
|
309
262
|
|
|
310
|
-
function
|
|
311
|
-
return new Promise((resolve) =>
|
|
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
|
+
});
|
|
312
297
|
}
|
|
313
298
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
console.log(
|
|
318
|
-
console.log(
|
|
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;
|
|
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`);
|
|
351
304
|
}
|
|
352
305
|
|
|
353
|
-
|
|
306
|
+
async function repomeld(options) {
|
|
307
|
+
const startTime = Date.now();
|
|
308
|
+
printBanner();
|
|
354
309
|
|
|
355
|
-
function repomeld(options) {
|
|
356
310
|
const cwd = process.cwd();
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
options.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const outputFile = resolveOutputPath(cwd, options.output || "repomeld_output.txt");
|
|
364
|
-
const forceInclude = options.forceInclude || [];
|
|
365
|
-
const rawIgnore = [...DEFAULT_IGNORE, ...IGNORE_FROM_CONFIG, ...(options.ignore || [])];
|
|
366
|
-
const ignoreList = forceInclude.length
|
|
367
|
-
? rawIgnore.filter(ig => !forceInclude.some(fi => ig.includes(fi) || fi.includes(ig)))
|
|
368
|
-
: rawIgnore;
|
|
369
|
-
const filterExts = options.ext || [];
|
|
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 || [];
|
|
370
317
|
const maxFileSizeBytes = (parseFloat(options.maxSize) || 500) * 1024;
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
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}`);
|
|
380
328
|
console.log(` 📄 Output : ${path.relative(cwd, outputFile)}`);
|
|
381
|
-
console.log(` 🎨 Style :
|
|
329
|
+
console.log(` 🎨 Style : ${headerStyle}`);
|
|
330
|
+
if (!options.noGitignore) console.log(` 📁 .gitignore respected`);
|
|
382
331
|
if (filterExts.length) console.log(` 🔍 Filter : .${filterExts.join(", .")}`);
|
|
383
|
-
if (
|
|
384
|
-
console.log(`
|
|
385
|
-
if (dryRun) console.log(` 🧪 Dry run : no file will be written`);
|
|
332
|
+
if (forceInclude && forceInclude.length) console.log(` 📌 Force : ${forceInclude.join(", ")}`);
|
|
333
|
+
if (dryRun) console.log(` 🧪 Dry run : no file will be written`);
|
|
386
334
|
console.log();
|
|
387
335
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
allFiles = allFiles.filter(f => path.resolve(f) !== outputFile);
|
|
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)}`);
|
|
393
340
|
|
|
394
|
-
|
|
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;
|
|
395
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
|
+
|
|
396
367
|
let combinedContent = "";
|
|
397
|
-
let skipped = 0
|
|
368
|
+
let skipped = 0;
|
|
369
|
+
let included = 0;
|
|
370
|
+
let totalLines = 0;
|
|
398
371
|
const includedFiles = [];
|
|
399
|
-
|
|
400
|
-
|
|
372
|
+
|
|
373
|
+
const progress = new ProgressIndicator(allFiles.length, ' ');
|
|
374
|
+
|
|
375
|
+
for (let i = 0; i < allFiles.length; i++) {
|
|
376
|
+
const filePath = allFiles[i];
|
|
401
377
|
const relativePath = path.relative(cwd, filePath);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
+
|
|
405
393
|
try {
|
|
406
|
-
let content = fs.
|
|
407
|
-
|
|
394
|
+
let content = await fs.readFile(filePath, "utf8");
|
|
395
|
+
|
|
396
|
+
if (options.trim) {
|
|
397
|
+
content = content.trim();
|
|
398
|
+
}
|
|
399
|
+
|
|
408
400
|
if (linesBefore > 0 || linesAfter > 0) {
|
|
409
401
|
const lines = content.split("\n");
|
|
410
|
-
|
|
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");
|
|
411
405
|
}
|
|
412
|
-
|
|
406
|
+
|
|
413
407
|
const lineCount = content.split("\n").length;
|
|
414
408
|
totalLines += lineCount;
|
|
415
409
|
includedFiles.push(filePath);
|
|
416
|
-
|
|
410
|
+
|
|
411
|
+
combinedContent += buildHeader(headerStyle, relativePath, filePath, lineCount, showMeta, stats);
|
|
417
412
|
combinedContent += content;
|
|
418
|
-
combinedContent += buildFooter();
|
|
419
|
-
|
|
413
|
+
combinedContent += buildFooter(headerStyle);
|
|
414
|
+
|
|
420
415
|
included++;
|
|
421
416
|
} catch (err) {
|
|
422
|
-
console.log(` ❌ Error: ${relativePath} — ${err.message}`);
|
|
423
417
|
skipped++;
|
|
424
418
|
}
|
|
425
419
|
}
|
|
426
|
-
|
|
427
|
-
|
|
420
|
+
|
|
421
|
+
progress.finish();
|
|
422
|
+
|
|
423
|
+
// Build final output
|
|
428
424
|
let finalOutput = "";
|
|
425
|
+
const timestamp = new Date().toISOString();
|
|
429
426
|
finalOutput += `# Generated by repomeld v${VERSION}\n`;
|
|
430
|
-
finalOutput += `# Date : ${
|
|
427
|
+
finalOutput += `# Date : ${timestamp}\n`;
|
|
431
428
|
finalOutput += `# Source : ${cwd}\n`;
|
|
432
429
|
finalOutput += `# Files : ${included}\n`;
|
|
433
430
|
finalOutput += `# Lines : ${totalLines}\n`;
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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";
|
|
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);
|
|
443
435
|
}
|
|
444
|
-
|
|
445
|
-
// File tree (always)
|
|
446
|
-
finalOutput += buildFileTree(includedFiles, cwd);
|
|
447
|
-
|
|
448
|
-
// Table of contents (always)
|
|
449
|
-
finalOutput += buildTableOfContents(includedFiles, cwd);
|
|
450
|
-
|
|
451
|
-
// File contents
|
|
436
|
+
|
|
452
437
|
finalOutput += combinedContent;
|
|
453
|
-
|
|
454
|
-
if (!dryRun
|
|
455
|
-
|
|
438
|
+
|
|
439
|
+
if (!dryRun && includedFiles.length > 0) {
|
|
440
|
+
await fs.writeFile(outputFile, finalOutput, "utf8");
|
|
441
|
+
}
|
|
442
|
+
|
|
456
443
|
const outputSize = formatSize(Buffer.byteLength(finalOutput, "utf8"));
|
|
444
|
+
const totalTime = Date.now() - startTime;
|
|
445
|
+
|
|
457
446
|
console.log(`
|
|
458
447
|
✨ repomeld complete!
|
|
459
|
-
|
|
448
|
+
─────────────────────────────────────────────────
|
|
460
449
|
✅ Included : ${included} files
|
|
461
450
|
⏭ Skipped : ${skipped} files
|
|
462
451
|
📏 Lines : ${totalLines}
|
|
463
452
|
💾 Size : ${outputSize}
|
|
453
|
+
⏱️ Time : ${formatDuration(totalTime)}
|
|
464
454
|
📄 Output : ${path.relative(cwd, outputFile)}${dryRun ? " (dry run — not written)" : ""}
|
|
465
|
-
|
|
466
|
-
|
|
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
|
+
}
|
|
467
466
|
}
|
|
468
467
|
|
|
469
|
-
// ─── CLI
|
|
468
|
+
// ─── CLI Definition ───────────────────────────────────────────
|
|
470
469
|
|
|
471
470
|
program
|
|
472
471
|
.name("repomeld")
|
|
473
472
|
.description("Meld your entire repo into a single file — perfect for AI context, code reviews & sharing")
|
|
474
473
|
.version(VERSION)
|
|
475
|
-
|
|
476
|
-
.option("-
|
|
477
|
-
.option("--
|
|
478
|
-
.option("--
|
|
479
|
-
.option("
|
|
480
|
-
.option("
|
|
481
|
-
.option("--
|
|
482
|
-
.option("--
|
|
483
|
-
.option("--
|
|
484
|
-
.option("
|
|
485
|
-
.option("--
|
|
486
|
-
.option("--
|
|
487
|
-
.option("--
|
|
488
|
-
.option("--
|
|
474
|
+
|
|
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 even if ignored")
|
|
481
|
+
.option("--max-size <kb>", "Skip files larger than N KB", "500")
|
|
482
|
+
.option("--no-gitignore", "Ignore .gitignore file")
|
|
483
|
+
.option("-s, --style <style>", "Header style: banner | markdown | minimal", "banner")
|
|
484
|
+
.option("--no-toc", "Disable table of contents")
|
|
485
|
+
.option("--no-meta", "Hide file metadata")
|
|
486
|
+
.option("--trim", "Trim leading/trailing whitespace per file")
|
|
487
|
+
.option("--lines-before <n>", "Skip first N lines of each file", parseInt)
|
|
488
|
+
.option("--lines-after <n>", "Skip last N lines of each file", parseInt)
|
|
489
|
+
.option("--dry-run", "Preview without writing output")
|
|
490
|
+
.option("--no-update-check", "Skip checking for updates")
|
|
491
|
+
|
|
489
492
|
.action(async (options) => {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (options.wizard || !hasFlags) {
|
|
498
|
-
const wizardOpts = await runWizard();
|
|
499
|
-
repomeld({ ...options, ...wizardOpts });
|
|
500
|
-
} else {
|
|
501
|
-
repomeld(options);
|
|
493
|
+
try {
|
|
494
|
+
await repomeld(options);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
console.error(`\n ❌ Error: ${error.message}`);
|
|
497
|
+
if (process.env.DEBUG) console.error(error);
|
|
498
|
+
process.exit(1);
|
|
502
499
|
}
|
|
503
500
|
});
|
|
504
501
|
|