repomeld 2.0.2 → 2.0.3

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 (2) hide show
  1. package/bin/cli.js +98 -77
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -93,6 +93,22 @@ function formatSize(bytes) {
93
93
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
94
94
  }
95
95
 
96
+ // ─── AUTO-INCREMENT OUTPUT FILENAME ──────────────────────────────────────────
97
+
98
+ function resolveOutputPath(cwd, filename) {
99
+ const ext = path.extname(filename);
100
+ const base = path.basename(filename, ext);
101
+ let candidate = path.resolve(cwd, filename);
102
+ let counter = 1;
103
+ while (fs.existsSync(candidate)) {
104
+ candidate = path.resolve(cwd, `${base}_${counter}${ext}`);
105
+ counter++;
106
+ }
107
+ return candidate;
108
+ }
109
+
110
+ // ─── BANNER ───────────────────────────────────────────────────────────────────
111
+
96
112
  function printBanner() {
97
113
  console.log(`
98
114
  ╔══════════════════════════════════════╗
@@ -103,20 +119,22 @@ function printBanner() {
103
119
  console.log();
104
120
  }
105
121
 
106
- function buildHeader(style, relativePath, filePath, lineCount, showMeta) {
122
+ // ─── HEADER / FOOTER ─────────────────────────────────────────────────────────
123
+
124
+ function buildHeader(relativePath, filePath, lineCount, showMeta) {
107
125
  const lang = getLanguage(filePath);
108
126
  const size = formatSize(fs.statSync(filePath).size);
109
127
  const meta = showMeta ? ` [${lineCount} lines | ${size}${lang ? " | " + lang : ""}]` : "";
110
- if (style === "markdown") return `\n## 📄 ${relativePath}${meta}\n\n\`\`\`${lang}\n`;
111
- if (style === "minimal") return `\n# ${relativePath}\n`;
112
128
  const divider = "─".repeat(60);
113
129
  return `\n${divider}\n FILE: ${relativePath}${meta}\n${divider}\n\n`;
114
130
  }
115
131
 
116
- function buildFooter(style) {
117
- return style === "markdown" ? "\n```\n" : "\n";
132
+ function buildFooter() {
133
+ return "\n";
118
134
  }
119
135
 
136
+ // ─── TABLE OF CONTENTS ────────────────────────────────────────────────────────
137
+
120
138
  function buildTableOfContents(files, cwd) {
121
139
  let toc = "TABLE OF CONTENTS\n" + "═".repeat(60) + "\n";
122
140
  files.forEach((f, i) => {
@@ -126,6 +144,37 @@ function buildTableOfContents(files, cwd) {
126
144
  return toc;
127
145
  }
128
146
 
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
+
129
178
  // ─── STRUCTURE EXTRACTION ─────────────────────────────────────────────────────
130
179
 
131
180
  function extractStructure(content, filePath) {
@@ -174,7 +223,7 @@ function extractStructure(content, filePath) {
174
223
  output.push(`${line.match(/^\s*/)[0]}${trimmed.split("=>")[0].trim()} => { ... }`); continue;
175
224
  }
176
225
  if (trimmed.match(/^(async\s+)?(\w+)\s*\([^)]*\)\s*(\:\s*[\w<>\[\]]+)?\s*\{/) &&
177
- !["if","while","for","switch"].some(k => trimmed.startsWith(k))) {
226
+ !["if", "while", "for", "switch"].some(k => trimmed.startsWith(k))) {
178
227
  output.push(`${line.match(/^\s*/)[0]}${trimmed.replace(/\{[\s\S]*$/, "{ ... }")}`); continue;
179
228
  }
180
229
  if (trimmed.startsWith("import ")) output.push(line);
@@ -218,7 +267,7 @@ function buildReviewGraph(files, cwd) {
218
267
  t.match(/^def\s+(\w+)/);
219
268
  if (fnMatch) {
220
269
  const name = fnMatch[3] || fnMatch[2] || fnMatch[1];
221
- if (name && name.length > 2 && !["if","for","while","switch"].includes(name))
270
+ if (name && name.length > 2 && !["if", "for", "while", "switch"].includes(name))
222
271
  fileFunctions[rel].functions.push(name);
223
272
  }
224
273
  const imp = t.match(/require\(['"]([^'"]+)['"]\)/) ||
@@ -266,55 +315,25 @@ async function runWizard() {
266
315
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
267
316
 
268
317
  console.log(" ┌─────────────────────────────────────────────────┐");
269
- console.log(" │ Interactive setup — answer 7 quick questions │");
318
+ console.log(" │ Interactive setup — answer 1 quick question │");
270
319
  console.log(" │ Press Enter to accept the [default] value │");
271
320
  console.log(" └─────────────────────────────────────────────────┘\n");
272
321
 
273
322
  const opts = {};
274
323
 
275
- // 1 — Output file
276
- const outRaw = await ask(rl, " 📄 1/7 Output filename? [repomeld_output.txt]\n > ");
277
- opts.output = outRaw.trim() || "repomeld_output.txt";
278
-
279
- // 2 — Style
280
- console.log("\n 🎨 2/7 Header style?");
281
- console.log(" 1) banner — clear dividers with file info (default)");
282
- console.log(" 2) markdown — great for pasting into AI prompts");
283
- console.log(" 3) minimal — filename only, no extra formatting");
284
- const styleRaw = await ask(rl, " > ");
285
- opts.style = ({ "2": "markdown", "3": "minimal" })[styleRaw.trim()] || "banner";
286
-
287
- // 3 — Extensions
288
- console.log("\n 🔍 3/7 Filter to specific file extensions?");
289
- console.log(" e.g. js ts php py (leave blank = include ALL files)");
324
+ // 1 — Extensions
325
+ console.log(" 🔍 1/1 Filter to specific extensions?");
326
+ console.log(" e.g. js ts py (leave blank = all files)");
290
327
  const extRaw = await ask(rl, " > ");
291
328
  opts.ext = extRaw.trim() ? extRaw.trim().split(/\s+/) : [];
292
329
 
293
- // 4Structure only
294
- console.log("\n 🏗️ 4/7 Enable structure-only mode? (y/n) [n]");
295
- console.log(" Strips function bodies — keeps class names + signatures only.");
296
- console.log(" Cuts a 400-line file to ~30 lines. Perfect for AI architecture review.");
297
- const structRaw = await ask(rl, " > ");
298
- opts.structureOnly = structRaw.trim().toLowerCase() === "y";
299
-
300
- // 5 — Review graph
301
- console.log("\n 🔗 5/7 Generate a code review graph? (y/n) [n]");
302
- console.log(" Adds a Mermaid call graph at the top of the output.");
303
- console.log(" AI models read Mermaid natively — shows which files connect to which.");
304
- const graphRaw = await ask(rl, " > ");
305
- opts.reviewGraph = graphRaw.trim().toLowerCase() === "y";
306
-
307
- // 6 — Table of contents
308
- console.log("\n 📋 6/7 Include a table of contents? (y/n) [y]");
309
- console.log(" Lists every included file at the top — useful for large repos.");
310
- const tocRaw = await ask(rl, " > ");
311
- opts.noToc = tocRaw.trim().toLowerCase() === "n";
312
-
313
- // 7 — Dry run
314
- console.log("\n 🧪 7/7 Dry run only? (y/n) [n]");
315
- console.log(" Preview which files would be included without writing anything.");
316
- const dryRaw = await ask(rl, " > ");
317
- opts.dryRun = dryRaw.trim().toLowerCase() === "y";
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;
318
337
 
319
338
  rl.close();
320
339
 
@@ -322,13 +341,10 @@ async function runWizard() {
322
341
  console.log("\n ┌─────────────────────────────────────────────────┐");
323
342
  console.log(" │ Your configuration │");
324
343
  console.log(" ├─────────────────────────────────────────────────┤");
325
- console.log(` │ Output : ${opts.output.padEnd(29)}│`);
326
- console.log(` │ Style : ${opts.style.padEnd(29)}│`);
327
- console.log(` │ Extensions : ${(opts.ext.length ? opts.ext.join(", ") : "all").padEnd(29)}│`);
328
- console.log(` │ Structure only : ${(opts.structureOnly ? "yes ✅" : "no").padEnd(29)}│`);
329
- console.log(` │ Review graph : ${(opts.reviewGraph ? "yes ✅" : "no").padEnd(29)}│`);
330
- console.log(` │ Table of cont. : ${(opts.noToc ? "no" : "yes").padEnd(29)}│`);
331
- console.log(` │ Dry run : ${(opts.dryRun ? "yes 🧪" : "no").padEnd(29)}│`);
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) │`);
332
348
  console.log(" └─────────────────────────────────────────────────┘\n");
333
349
 
334
350
  return opts;
@@ -338,7 +354,13 @@ async function runWizard() {
338
354
 
339
355
  function repomeld(options) {
340
356
  const cwd = process.cwd();
341
- const outputFile = path.resolve(cwd, options.output);
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");
342
364
  const forceInclude = options.forceInclude || [];
343
365
  const rawIgnore = [...DEFAULT_IGNORE, ...IGNORE_FROM_CONFIG, ...(options.ignore || [])];
344
366
  const ignoreList = forceInclude.length
@@ -346,23 +368,20 @@ function repomeld(options) {
346
368
  : rawIgnore;
347
369
  const filterExts = options.ext || [];
348
370
  const maxFileSizeBytes = (parseFloat(options.maxSize) || 500) * 1024;
349
- const headerStyle = options.style || "banner";
350
371
  const showMeta = options.noMeta !== true;
351
- const showToc = options.noToc !== true;
352
372
  const dryRun = options.dryRun || false;
353
373
  const include = options.include || [];
354
374
  const exclude = options.exclude || [];
355
375
  const linesBefore = parseInt(options.linesBefore) || 0;
356
376
  const linesAfter = parseInt(options.linesAfter) || 0;
357
377
  const structureOnly = options.structureOnly || false;
358
- const reviewGraph = options.reviewGraph || false;
359
378
 
360
379
  console.log(` 📂 Source : ${cwd}`);
361
380
  console.log(` 📄 Output : ${path.relative(cwd, outputFile)}`);
362
- console.log(` 🎨 Style : ${headerStyle}`);
381
+ console.log(` 🎨 Style : banner`);
363
382
  if (filterExts.length) console.log(` 🔍 Filter : .${filterExts.join(", .")}`);
364
383
  if (structureOnly) console.log(` 🏗️ Mode : structure only`);
365
- if (reviewGraph) console.log(` 🔗 Graph : review graph enabled`);
384
+ console.log(` 🔗 Graph : review graph enabled`);
366
385
  if (dryRun) console.log(` 🧪 Dry run : no file will be written`);
367
386
  console.log();
368
387
 
@@ -394,9 +413,9 @@ function repomeld(options) {
394
413
  const lineCount = content.split("\n").length;
395
414
  totalLines += lineCount;
396
415
  includedFiles.push(filePath);
397
- combinedContent += buildHeader(headerStyle, relativePath, filePath, lineCount, showMeta);
416
+ combinedContent += buildHeader(relativePath, filePath, lineCount, showMeta);
398
417
  combinedContent += content;
399
- combinedContent += buildFooter(headerStyle);
418
+ combinedContent += buildFooter();
400
419
  console.log(` ✅ ${relativePath}`);
401
420
  included++;
402
421
  } catch (err) {
@@ -405,6 +424,7 @@ function repomeld(options) {
405
424
  }
406
425
  }
407
426
 
427
+ // Build final output: metadata → review graph → file tree → TOC → file contents
408
428
  let finalOutput = "";
409
429
  finalOutput += `# Generated by repomeld v${VERSION}\n`;
410
430
  finalOutput += `# Date : ${new Date().toISOString()}\n`;
@@ -412,18 +432,23 @@ function repomeld(options) {
412
432
  finalOutput += `# Files : ${included}\n`;
413
433
  finalOutput += `# Lines : ${totalLines}\n`;
414
434
  if (structureOnly) finalOutput += `# Mode : structure-only (signatures)\n`;
415
- if (reviewGraph) finalOutput += `# Graph : review-graph enabled\n`;
435
+ finalOutput += `# Graph : review-graph enabled\n`;
416
436
  finalOutput += `# Author : susheelhbti@gmail.com (open to work)\n\n`;
417
437
 
418
- if (reviewGraph) {
419
- const graph = buildReviewGraph(includedFiles, cwd);
420
- if (graph) {
421
- finalOutput += "═".repeat(60) + "\n CODE REVIEW GRAPH\n" + "═".repeat(60) + "\n\n";
422
- finalOutput += graph + "\n";
423
- }
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";
424
443
  }
425
444
 
426
- if (showToc) finalOutput += buildTableOfContents(includedFiles, cwd);
445
+ // File tree (always)
446
+ finalOutput += buildFileTree(includedFiles, cwd);
447
+
448
+ // Table of contents (always)
449
+ finalOutput += buildTableOfContents(includedFiles, cwd);
450
+
451
+ // File contents
427
452
  finalOutput += combinedContent;
428
453
 
429
454
  if (!dryRun) fs.writeFileSync(outputFile, finalOutput, "utf8");
@@ -436,7 +461,7 @@ function repomeld(options) {
436
461
  ⏭ Skipped : ${skipped} files
437
462
  📏 Lines : ${totalLines}
438
463
  💾 Size : ${outputSize}
439
- 📄 Output : ${options.output}${dryRun ? " (dry run — not written)" : ""}
464
+ 📄 Output : ${path.relative(cwd, outputFile)}${dryRun ? " (dry run — not written)" : ""}
440
465
  `);
441
466
  console.log(AUTHOR_NOTE + "\n");
442
467
  }
@@ -454,23 +479,19 @@ program
454
479
  .option("-i, --ignore <names...>", "Extra folders/files to ignore")
455
480
  .option("--force-include <names...>", "Force-include files ignored by default")
456
481
  .option("--max-size <kb>", "Skip files larger than N KB", "500")
457
- .option("-s, --style <style>", "Header style: banner | markdown | minimal","banner")
458
- .option("--no-toc", "Disable table of contents")
459
482
  .option("--no-meta", "Hide file metadata")
460
483
  .option("--trim", "Trim leading/trailing whitespace per file")
461
484
  .option("--lines-before <n>", "Skip first N lines of each file")
462
485
  .option("--lines-after <n>", "Skip last N lines of each file")
463
486
  .option("--dry-run", "Preview without writing output")
464
487
  .option("--structure-only", "Extract signatures only — no body code")
465
- .option("--review-graph", "Prepend a Mermaid call graph to output")
466
488
  .option("--wizard", "Interactive step-by-step setup (auto when no flags given)")
467
489
  .action(async (options) => {
468
490
  printBanner();
469
491
 
470
492
  // Auto-wizard when user runs plain `repomeld` with no flags
471
493
  const hasFlags = options.ext || options.include || options.exclude ||
472
- options.structureOnly || options.reviewGraph ||
473
- options.style !== "banner" || options.output !== "repomeld_output.txt" ||
494
+ options.structureOnly || options.output !== "repomeld_output.txt" ||
474
495
  options.dryRun;
475
496
 
476
497
  if (options.wizard || !hasFlags) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repomeld",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "Meld your entire repo into a single file — perfect for AI context, code reviews & sharing",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {