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.
Files changed (3) hide show
  1. package/README.md +337 -226
  2. package/bin/cli.js +367 -370
  3. 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.2.0";
12
+ const VERSION = "1.0.0";
13
+ const PACKAGE_NAME = "repomeld";
9
14
 
10
- const AUTHOR_NOTE = " 💼 Author susheelhbti@gmail.com is open to freelance & full-time work";
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", ".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",
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 getAllFiles(dirPath, ignoreList, fileList = []) {
52
- let entries;
53
- try { entries = fs.readdirSync(dirPath, { withFileTypes: true }); }
54
- catch { return fileList; }
55
- for (const entry of entries) {
56
- const fullPath = path.join(dirPath, entry.name);
57
- 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);
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 isBinaryFile(filePath) {
68
- try {
69
- const buffer = Buffer.alloc(512);
70
- const fd = fs.openSync(filePath, "r");
71
- const bytesRead = fs.readSync(fd, buffer, 0, 512, 0);
72
- fs.closeSync(fd);
73
- for (let i = 0; i < bytesRead; i++) if (buffer[i] === 0) return true;
74
- return false;
75
- } catch { return true; }
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
- // ─── AUTO-INCREMENT OUTPUT FILENAME ──────────────────────────────────────────
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
- 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++;
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
- repomeld v${VERSION}
116
- Meld your repo into one file 🔥
117
- ╚══════════════════════════════════════╝`);
118
- console.log(AUTHOR_NOTE);
119
- 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
+ ╚══════════════════════════════════════════════════════╝`);
120
200
  }
121
201
 
122
- // ─── HEADER / FOOTER ─────────────────────────────────────────────────────────
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 size = formatSize(fs.statSync(filePath).size);
127
- const meta = showMeta ? ` [${lineCount} lines | ${size}${lang ? " | " + lang : ""}]` : "";
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
- toc += ` ${String(i + 1).padStart(3, " ")}. ${path.relative(cwd, f)}\n`;
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
- // ─── 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;
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
- return output.join("\n");
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
- // ─── 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
- }
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
- // ─── INTERACTIVE WIZARD ───────────────────────────────────────────────────────
259
+ function escapeRegex(str) {
260
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
261
+ }
309
262
 
310
- function ask(rl, question) {
311
- return new Promise((resolve) => rl.question(question, 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
- 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;
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
- // ─── CORE ─────────────────────────────────────────────────────────────────────
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
- // Always banner, always review graph, always TOC
359
- options.style = "banner";
360
- options.reviewGraph = true;
361
- options.noToc = false;
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 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}`);
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 : banner`);
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 (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`);
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
- 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);
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
- if (allFiles.length === 0) { console.log(" ⚠️ No matching files found.\n"); return; }
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, included = 0, totalLines = 0;
368
+ let skipped = 0;
369
+ let included = 0;
370
+ let totalLines = 0;
398
371
  const includedFiles = [];
399
-
400
- for (const filePath of allFiles) {
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
- if (isBinaryFile(filePath)) { console.log(` ⏭ Binary : ${relativePath}`); skipped++; continue; }
403
- const stat = fs.statSync(filePath);
404
- if (stat.size > maxFileSizeBytes) { console.log(` ⏭ Too large: ${relativePath} (${formatSize(stat.size)})`); skipped++; continue; }
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.readFileSync(filePath, "utf8");
407
- if (options.trim) content = content.trim();
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
- content = lines.slice(linesBefore, linesAfter > 0 ? lines.length - linesAfter : lines.length).join("\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");
411
405
  }
412
- if (structureOnly) content = extractStructure(content, filePath);
406
+
413
407
  const lineCount = content.split("\n").length;
414
408
  totalLines += lineCount;
415
409
  includedFiles.push(filePath);
416
- combinedContent += buildHeader(relativePath, filePath, lineCount, showMeta);
410
+
411
+ combinedContent += buildHeader(headerStyle, relativePath, filePath, lineCount, showMeta, stats);
417
412
  combinedContent += content;
418
- combinedContent += buildFooter();
419
- console.log(` ✅ ${relativePath}`);
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
- // Build final output: metadata → review graph → file tree → TOC → file contents
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 : ${new Date().toISOString()}\n`;
427
+ finalOutput += `# Date : ${timestamp}\n`;
431
428
  finalOutput += `# Source : ${cwd}\n`;
432
429
  finalOutput += `# Files : ${included}\n`;
433
430
  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";
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) fs.writeFileSync(outputFile, finalOutput, "utf8");
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
- console.log(AUTHOR_NOTE + "\n");
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
- .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)")
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
- 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);
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