repomeld 3.0.0 → 3.0.2
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/package.json +24 -42
- package/src/core/fileScanner.js +78 -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/package.json
CHANGED
|
@@ -1,59 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "repomeld",
|
|
3
|
-
"version": "3.0.
|
|
4
|
-
"description": "Meld your entire repo into a single file — perfect for AI context
|
|
3
|
+
"version": "3.0.2",
|
|
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
|
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"archiver": "^6.0.1",
|
|
23
|
+
"commander": "^11.1.0",
|
|
24
|
+
"ignore": "^5.3.0",
|
|
25
|
+
"isbinaryfile": "^5.0.0"
|
|
26
|
+
},
|
|
27
|
+
"keywords": ["cli", "repo", "combiner", "ai-context", "code-review", "mermaid", "dependency-graph"],
|
|
28
28
|
"author": "Susheel <susheelhbti@gmail.com>",
|
|
29
29
|
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=14.0.0"
|
|
32
|
+
},
|
|
30
33
|
"repository": {
|
|
31
34
|
"type": "git",
|
|
32
|
-
"url": "git+https://github.com/
|
|
35
|
+
"url": "git+https://github.com/susheel/repomeld.git"
|
|
33
36
|
},
|
|
34
37
|
"bugs": {
|
|
35
|
-
"url": "https://github.com/
|
|
36
|
-
},
|
|
37
|
-
"homepage": "https://github.com/sakshsky/repomeld#readme",
|
|
38
|
-
"dependencies": {
|
|
39
|
-
"archiver": "^7.0.1",
|
|
40
|
-
"commander": "^11.1.0",
|
|
41
|
-
"ignore": "^5.3.2",
|
|
42
|
-
"isbinaryfile": "^5.0.7"
|
|
43
|
-
},
|
|
44
|
-
"devDependencies": {
|
|
45
|
-
"eslint": "^8.56.0",
|
|
46
|
-
"prettier": "^3.1.1"
|
|
47
|
-
},
|
|
48
|
-
"engines": {
|
|
49
|
-
"node": ">=14.0.0"
|
|
38
|
+
"url": "https://github.com/susheel/repomeld/issues"
|
|
50
39
|
},
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
"README.md",
|
|
54
|
-
"LICENSE"
|
|
55
|
-
],
|
|
56
|
-
"publishConfig": {
|
|
57
|
-
"access": "public"
|
|
58
|
-
}
|
|
59
|
-
}
|
|
40
|
+
"homepage": "https://github.com/susheel/repomeld#readme"
|
|
41
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const fs = require("fs").promises;
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { normalizePath } = require("../utils/constants");
|
|
4
|
+
|
|
5
|
+
// Pattern to detect repomeld-related files/folders
|
|
6
|
+
const REPOMELD_PATTERN = /^repomeld/i; // Case-insensitive, matches anything starting with "repomeld"
|
|
7
|
+
|
|
8
|
+
async function getAllFilesWithIgnore(dirPath, ig, forceIncludePatterns) {
|
|
9
|
+
const fileList = [];
|
|
10
|
+
const stack = [{ dirPath, relativePath: '.' }];
|
|
11
|
+
|
|
12
|
+
// Pre-process force include patterns for faster matching
|
|
13
|
+
const processedPatterns = forceIncludePatterns?.map(pattern => ({
|
|
14
|
+
original: pattern,
|
|
15
|
+
clean: pattern.replace(/^\.\//, '').replace(/\/$/, ''),
|
|
16
|
+
isExact: !pattern.includes('*') && !pattern.includes('/')
|
|
17
|
+
})) || [];
|
|
18
|
+
|
|
19
|
+
while (stack.length) {
|
|
20
|
+
const { dirPath: currentDir, relativePath: currentRelative } = stack.pop();
|
|
21
|
+
|
|
22
|
+
let entries;
|
|
23
|
+
try {
|
|
24
|
+
entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
25
|
+
} catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
31
|
+
const relativePath = path.join(currentRelative, entry.name);
|
|
32
|
+
const normalizedPath = normalizePath(relativePath);
|
|
33
|
+
|
|
34
|
+
// HARD-CODED: Always ignore any file/folder starting with "repomeld"
|
|
35
|
+
// This prevents recursive inclusion and infinite loops
|
|
36
|
+
// Also ignores repomeld_output.txt, repomeld_zips/, etc.
|
|
37
|
+
if (REPOMELD_PATTERN.test(entry.name)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Fast force-include check
|
|
42
|
+
let isForceIncluded = false;
|
|
43
|
+
if (processedPatterns.length) {
|
|
44
|
+
for (const pattern of processedPatterns) {
|
|
45
|
+
if (pattern.isExact) {
|
|
46
|
+
// Exact match - fastest
|
|
47
|
+
if (entry.name === pattern.clean || normalizedPath === pattern.clean) {
|
|
48
|
+
isForceIncluded = true;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// Pattern matching
|
|
53
|
+
if (normalizedPath.includes(pattern.clean) ||
|
|
54
|
+
normalizedPath.startsWith(pattern.clean + '/') ||
|
|
55
|
+
entry.name.includes(pattern.clean)) {
|
|
56
|
+
isForceIncluded = true;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Only check ignore if not force-included
|
|
64
|
+
if (!isForceIncluded && ig.ignores(normalizedPath)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
stack.push({ dirPath: fullPath, relativePath });
|
|
70
|
+
} else if (entry.isFile()) {
|
|
71
|
+
fileList.push(fullPath);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return fileList;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { getAllFilesWithIgnore };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { formatSize, getLanguage } = require("../utils/helpers");
|
|
3
|
+
const { LANGUAGE_MAP } = require("../utils/constants");
|
|
4
|
+
|
|
5
|
+
function buildHeader(style, relativePath, filePath, lineCount, showMeta, stats) {
|
|
6
|
+
const lang = getLanguage(filePath, LANGUAGE_MAP);
|
|
7
|
+
const meta = showMeta ? ` [${lineCount} lines | ${formatSize(stats.size)}${lang ? ` | ${lang}` : ""}]` : "";
|
|
8
|
+
|
|
9
|
+
if (style === "markdown") {
|
|
10
|
+
return `\n## 📄 ${relativePath}${meta}\n\n\`\`\`${lang}\n`;
|
|
11
|
+
}
|
|
12
|
+
if (style === "minimal") {
|
|
13
|
+
return `\n# ${relativePath}\n`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const divider = "─".repeat(60);
|
|
17
|
+
return `\n${divider}\n FILE: ${relativePath}${meta}\n${divider}\n\n`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildFooter(style) {
|
|
21
|
+
return style === "markdown" ? "\n```\n" : "\n";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildTableOfContents(files, cwd) {
|
|
25
|
+
let toc = "TABLE OF CONTENTS\n" + "═".repeat(60) + "\n";
|
|
26
|
+
files.forEach((f, i) => {
|
|
27
|
+
let rel = path.relative(cwd, f);
|
|
28
|
+
// Normalize to forward slashes for cross-platform consistency
|
|
29
|
+
rel = rel.split(path.sep).join('/');
|
|
30
|
+
toc += ` ${String(i + 1).padStart(3, " ")}. ${rel}\n`;
|
|
31
|
+
});
|
|
32
|
+
toc += "═".repeat(60) + "\n\n";
|
|
33
|
+
return toc;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function printBanner(VERSION) {
|
|
37
|
+
console.log(`
|
|
38
|
+
╔══════════════════════════════════════════════════════╗
|
|
39
|
+
║ repomeld v${VERSION} ║
|
|
40
|
+
║ Meld your repo into one file 🔥 ║
|
|
41
|
+
╠══════════════════════════════════════════════════════╣
|
|
42
|
+
║ 💼 susheelhbti@gmail.com — Open for work ║
|
|
43
|
+
╚══════════════════════════════════════════════════════╝`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
buildHeader,
|
|
48
|
+
buildFooter,
|
|
49
|
+
buildTableOfContents,
|
|
50
|
+
printBanner,
|
|
51
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const fs = require("fs").promises;
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const ignore = require("ignore");
|
|
4
|
+
const { DEFAULT_IGNORE } = require("../utils/constants");
|
|
5
|
+
|
|
6
|
+
async function loadIgnoreConfig() {
|
|
7
|
+
const configPath = path.resolve(process.cwd(), "repomeld.ignore.json");
|
|
8
|
+
try {
|
|
9
|
+
const data = JSON.parse(await fs.readFile(configPath, "utf8"));
|
|
10
|
+
return Array.isArray(data.ignore) ? data.ignore : [];
|
|
11
|
+
} catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function buildIgnoreFilter(options) {
|
|
17
|
+
const ig = ignore();
|
|
18
|
+
ig.add(DEFAULT_IGNORE);
|
|
19
|
+
|
|
20
|
+
const customIgnores = await loadIgnoreConfig();
|
|
21
|
+
ig.add(customIgnores);
|
|
22
|
+
|
|
23
|
+
if (options.ignore?.length) ig.add(options.ignore);
|
|
24
|
+
|
|
25
|
+
if (!options.noGitignore) {
|
|
26
|
+
try {
|
|
27
|
+
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
28
|
+
const gitignoreContent = await fs.readFile(gitignorePath, "utf8");
|
|
29
|
+
ig.add(gitignoreContent);
|
|
30
|
+
} catch {
|
|
31
|
+
// No .gitignore file, ignore silently
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Return forceInclude as array for fileScanner
|
|
36
|
+
const forceIncludePatterns = options.forceInclude || [];
|
|
37
|
+
|
|
38
|
+
return { ig, forceIncludePatterns };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { buildIgnoreFilter };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const fs = require("fs").promises;
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
async function resolveOutputPath(desiredPath) {
|
|
5
|
+
try {
|
|
6
|
+
await fs.access(desiredPath);
|
|
7
|
+
const ext = path.extname(desiredPath);
|
|
8
|
+
const base = desiredPath.slice(0, -ext.length);
|
|
9
|
+
let counter = 2;
|
|
10
|
+
while (true) {
|
|
11
|
+
const candidate = `${base}__${counter}${ext}`;
|
|
12
|
+
try {
|
|
13
|
+
await fs.access(candidate);
|
|
14
|
+
counter++;
|
|
15
|
+
} catch {
|
|
16
|
+
return { path: candidate, number: counter };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
return { path: desiredPath, number: null };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { resolveOutputPath };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { formatDuration } = require("../utils/helpers");
|
|
2
|
+
|
|
3
|
+
class ProgressIndicator {
|
|
4
|
+
constructor(total, prefix = '') {
|
|
5
|
+
this.total = total;
|
|
6
|
+
this.prefix = prefix;
|
|
7
|
+
this.current = 0;
|
|
8
|
+
this.startTime = Date.now();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
update(current) {
|
|
12
|
+
this.current = current;
|
|
13
|
+
this.render();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
render() {
|
|
17
|
+
if (this.current % 80 !== 0 && this.current !== this.total) return;
|
|
18
|
+
const percent = (this.current / this.total * 100).toFixed(1);
|
|
19
|
+
const elapsed = Date.now() - this.startTime;
|
|
20
|
+
console.log(`\r${this.prefix} ${this.current}/${this.total} files (${percent}%) | ${formatDuration(elapsed)} elapsed`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
finish() {
|
|
24
|
+
const elapsed = Date.now() - this.startTime;
|
|
25
|
+
console.log(`\r${this.prefix} ✅ Completed ${this.current}/${this.total} files in ${formatDuration(elapsed)}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { ProgressIndicator };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const fs = require("fs").promises;
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { isBinaryFile } = require("isbinaryfile");
|
|
4
|
+
|
|
5
|
+
const { VERSION, BINARY_EXTENSIONS } = require("./utils/constants");
|
|
6
|
+
const { formatSize, formatDuration, matchesExtensions, matchesPattern } = require("./utils/helpers");
|
|
7
|
+
const { buildIgnoreFilter } = require("./core/ignoreBuilder");
|
|
8
|
+
const { getAllFilesWithIgnore } = require("./core/fileScanner");
|
|
9
|
+
const { ProgressIndicator } = require("./core/progress");
|
|
10
|
+
const { buildHeader, buildFooter, buildTableOfContents, printBanner } = require("./core/formatter");
|
|
11
|
+
const { createBackupZip } = require("./utils/backup");
|
|
12
|
+
const { resolveOutputPath } = require("./core/pathResolver");
|
|
13
|
+
const { checkForUpdates, showUpdateMessage } = require("./updates/updateChecker");
|
|
14
|
+
|
|
15
|
+
// Cache for binary detection
|
|
16
|
+
const binaryCache = new Map();
|
|
17
|
+
|
|
18
|
+
async function isBinaryFileFast(filePath) {
|
|
19
|
+
// Check cache first
|
|
20
|
+
if (binaryCache.has(filePath)) return binaryCache.get(filePath);
|
|
21
|
+
|
|
22
|
+
// Quick check by extension
|
|
23
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
24
|
+
if (BINARY_EXTENSIONS.has(ext)) {
|
|
25
|
+
binaryCache.set(filePath, true);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Fall back to full detection
|
|
30
|
+
const result = await isBinaryFile(filePath).catch(() => true);
|
|
31
|
+
binaryCache.set(filePath, result);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function repomeld(options) {
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
printBanner(VERSION);
|
|
38
|
+
const cwd = process.cwd();
|
|
39
|
+
|
|
40
|
+
const { path: outputFile, number: outputNumber } = await resolveOutputPath(path.resolve(cwd, options.output));
|
|
41
|
+
|
|
42
|
+
const { ig, forceIg } = await buildIgnoreFilter(options);
|
|
43
|
+
|
|
44
|
+
// Convert forceIg patterns to array for fileScanner
|
|
45
|
+
const forceIncludePatterns = options.forceInclude || [];
|
|
46
|
+
|
|
47
|
+
const filterExts = options.ext || [];
|
|
48
|
+
const maxFileSizeBytes = (parseFloat(options.maxSize) || 500) * 1024;
|
|
49
|
+
const headerStyle = options.style || "banner";
|
|
50
|
+
const showMeta = !options.noMeta;
|
|
51
|
+
const showToc = !options.noToc;
|
|
52
|
+
const dryRun = !!options.dryRun;
|
|
53
|
+
|
|
54
|
+
console.log(`\n 📂 Source : ${cwd}`);
|
|
55
|
+
console.log(` 📄 Output : ${path.relative(cwd, outputFile)}`);
|
|
56
|
+
console.log(` 🎨 Style : ${headerStyle}`);
|
|
57
|
+
if (filterExts.length) console.log(` 🔍 Filter : .${filterExts.join(", .")}`);
|
|
58
|
+
if (forceIncludePatterns.length) console.log(` 📌 Force include : ${forceIncludePatterns.join(", ")}`);
|
|
59
|
+
if (dryRun) console.log(` 🧪 Dry run mode`);
|
|
60
|
+
|
|
61
|
+
console.log(`\n 🔍 Scanning files...`);
|
|
62
|
+
let allFiles = await getAllFilesWithIgnore(cwd, ig, forceIncludePatterns);
|
|
63
|
+
|
|
64
|
+
// Apply filters
|
|
65
|
+
if (filterExts.length) allFiles = allFiles.filter(f => matchesExtensions(f, filterExts));
|
|
66
|
+
if (options.include?.length) allFiles = allFiles.filter(f => matchesPattern(f, options.include));
|
|
67
|
+
if (options.exclude?.length) allFiles = allFiles.filter(f => !matchesPattern(f, options.exclude));
|
|
68
|
+
|
|
69
|
+
// Remove previous repomeld outputs - improved pattern
|
|
70
|
+
const outputPattern = new RegExp(`^${path.basename(options.output).replace(/\.txt$/, '')}(_+?\\d+)?\\.txt$`);
|
|
71
|
+
allFiles = allFiles.filter(f => !outputPattern.test(path.basename(f)));
|
|
72
|
+
|
|
73
|
+
console.log(` ✅ Found ${allFiles.length} files\n`);
|
|
74
|
+
|
|
75
|
+
if (allFiles.length === 0) {
|
|
76
|
+
console.log(" ⚠️ No matching files found.");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Memory warning for large repos
|
|
81
|
+
const estimatedMemoryMB = (allFiles.length * 0.5) / 1024; // Rough estimate: 0.5KB per file path
|
|
82
|
+
if (estimatedMemoryMB > 100) {
|
|
83
|
+
console.log(` ⚠️ Large repository detected (~${allFiles.length} files). Memory usage may be high.\n`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let combinedContent = "";
|
|
87
|
+
let skipped = 0, included = 0, totalLines = 0;
|
|
88
|
+
const includedFiles = [];
|
|
89
|
+
|
|
90
|
+
const progress = new ProgressIndicator(allFiles.length, ' Processing:');
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < allFiles.length; i++) {
|
|
93
|
+
const filePath = allFiles[i];
|
|
94
|
+
const relativePath = path.relative(cwd, filePath);
|
|
95
|
+
progress.update(i + 1);
|
|
96
|
+
|
|
97
|
+
// Use fast binary detection with caching
|
|
98
|
+
if (await isBinaryFileFast(filePath)) {
|
|
99
|
+
skipped++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const stats = await fs.stat(filePath);
|
|
104
|
+
if (stats.size > maxFileSizeBytes) {
|
|
105
|
+
skipped++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
let content = await fs.readFile(filePath, "utf8");
|
|
111
|
+
|
|
112
|
+
if (options.trim) content = content.trim();
|
|
113
|
+
|
|
114
|
+
if (options.linesBefore || options.linesAfter) {
|
|
115
|
+
const lines = content.split("\n");
|
|
116
|
+
const start = Math.max(0, parseInt(options.linesBefore) || 0);
|
|
117
|
+
const end = options.linesAfter
|
|
118
|
+
? Math.max(start, lines.length - parseInt(options.linesAfter))
|
|
119
|
+
: lines.length;
|
|
120
|
+
content = lines.slice(start, end).join("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const lineCount = content.split("\n").length;
|
|
124
|
+
totalLines += lineCount;
|
|
125
|
+
includedFiles.push(filePath);
|
|
126
|
+
|
|
127
|
+
combinedContent += buildHeader(headerStyle, relativePath, filePath, lineCount, showMeta, stats);
|
|
128
|
+
combinedContent += content;
|
|
129
|
+
combinedContent += buildFooter(headerStyle);
|
|
130
|
+
|
|
131
|
+
included++;
|
|
132
|
+
} catch {
|
|
133
|
+
skipped++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
progress.finish();
|
|
138
|
+
|
|
139
|
+
// Final Output
|
|
140
|
+
let finalOutput = `# Generated by repomeld v${VERSION}\n`;
|
|
141
|
+
finalOutput += `# Date : ${new Date().toISOString()}\n`;
|
|
142
|
+
finalOutput += `# Source : ${cwd}\n`;
|
|
143
|
+
finalOutput += `# Files : ${included}\n`;
|
|
144
|
+
finalOutput += `# Lines : ${totalLines}\n\n`;
|
|
145
|
+
|
|
146
|
+
if (showToc) finalOutput += buildTableOfContents(includedFiles, cwd);
|
|
147
|
+
finalOutput += combinedContent;
|
|
148
|
+
|
|
149
|
+
if (!dryRun) {
|
|
150
|
+
await fs.writeFile(outputFile, finalOutput, "utf8");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const outputSize = formatSize(Buffer.byteLength(finalOutput, "utf8"));
|
|
154
|
+
const totalTime = Date.now() - startTime;
|
|
155
|
+
|
|
156
|
+
console.log(`
|
|
157
|
+
✨ repomeld complete!
|
|
158
|
+
─────────────────────────────────────────────────
|
|
159
|
+
✅ Included : ${included} files
|
|
160
|
+
⏭ Skipped : ${skipped} files
|
|
161
|
+
📏 Lines : ${totalLines}
|
|
162
|
+
💾 Size : ${outputSize}
|
|
163
|
+
⏱️ Time : ${formatDuration(totalTime)}
|
|
164
|
+
📄 Output : ${path.relative(cwd, outputFile)}${dryRun ? " (dry run)" : ""}
|
|
165
|
+
─────────────────────────────────────────────────`);
|
|
166
|
+
|
|
167
|
+
// Backup ZIP
|
|
168
|
+
if (!dryRun && included > 0 && options.backup !== false) {
|
|
169
|
+
console.log(`\n 📦 Creating backup zip...`);
|
|
170
|
+
try {
|
|
171
|
+
const { zipFilePath, size } = await createBackupZip(includedFiles, cwd, outputFile);
|
|
172
|
+
console.log(` ✅ Backup created: ${path.relative(cwd, zipFilePath)} (${size})`);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.log(` ⚠️ Backup failed: ${err.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(`\n 💼 Need a developer? susheelhbti@gmail.com`);
|
|
179
|
+
|
|
180
|
+
// Update check
|
|
181
|
+
if (!options.noUpdateCheck) {
|
|
182
|
+
const updateInfo = await checkForUpdates();
|
|
183
|
+
if (updateInfo.hasUpdate) {
|
|
184
|
+
showUpdateMessage(VERSION, updateInfo.latestVersion);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Clear binary cache to free memory
|
|
189
|
+
binaryCache.clear();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = { repomeld };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const https = require("https");
|
|
2
|
+
const { PACKAGE_NAME, VERSION } = require("../utils/constants");
|
|
3
|
+
|
|
4
|
+
async function checkForUpdates() {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const options = {
|
|
7
|
+
hostname: 'registry.npmjs.org',
|
|
8
|
+
path: `/${PACKAGE_NAME}/latest`,
|
|
9
|
+
method: 'GET',
|
|
10
|
+
timeout: 4000
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const req = https.request(options, (res) => {
|
|
14
|
+
let data = '';
|
|
15
|
+
res.on('data', chunk => data += chunk);
|
|
16
|
+
res.on('end', () => {
|
|
17
|
+
try {
|
|
18
|
+
const json = JSON.parse(data);
|
|
19
|
+
if (json.version && json.version !== VERSION) {
|
|
20
|
+
resolve({ hasUpdate: true, latestVersion: json.version });
|
|
21
|
+
} else {
|
|
22
|
+
resolve({ hasUpdate: false });
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
resolve({ hasUpdate: false });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
req.on('error', () => resolve({ hasUpdate: false }));
|
|
31
|
+
req.on('timeout', () => { req.destroy(); resolve({ hasUpdate: false }); });
|
|
32
|
+
req.end();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function showUpdateMessage(currentVersion, latestVersion) {
|
|
37
|
+
console.log(`\n${'═'.repeat(60)}`);
|
|
38
|
+
console.log(` ⭐ New version available: ${currentVersion} → ${latestVersion}`);
|
|
39
|
+
console.log(` 📦 Update: npm install -g ${PACKAGE_NAME}@latest`);
|
|
40
|
+
console.log(`${'═'.repeat(60)}\n`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { checkForUpdates, showUpdateMessage };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const archiver = require("archiver");
|
|
4
|
+
const { formatSize } = require("./helpers");
|
|
5
|
+
|
|
6
|
+
function resolveZipPath(outputPath) {
|
|
7
|
+
const outputFileName = path.basename(outputPath);
|
|
8
|
+
const outputBaseName = outputFileName.replace(/\.txt$/, '');
|
|
9
|
+
const zipDir = path.join(process.cwd(), "repomeld_zips");
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(zipDir)) {
|
|
12
|
+
fs.mkdirSync(zipDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return path.join(zipDir, `${outputBaseName}.zip`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function createBackupZip(files, cwd, outputFilePath) {
|
|
19
|
+
const zipFilePath = resolveZipPath(outputFilePath);
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const output = fs.createWriteStream(zipFilePath);
|
|
22
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
23
|
+
|
|
24
|
+
output.on('close', () => {
|
|
25
|
+
resolve({
|
|
26
|
+
zipFilePath,
|
|
27
|
+
size: formatSize(archive.pointer()),
|
|
28
|
+
fileCount: files.length
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
archive.on('error', reject);
|
|
33
|
+
archive.pipe(output);
|
|
34
|
+
|
|
35
|
+
for (const filePath of files) {
|
|
36
|
+
const relativePath = path.relative(cwd, filePath);
|
|
37
|
+
archive.file(filePath, { name: relativePath });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (fs.existsSync(outputFilePath)) {
|
|
41
|
+
archive.file(outputFilePath, { name: path.basename(outputFilePath) });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
archive.finalize();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { createBackupZip };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
// Normalize paths for cross-platform compatibility
|
|
4
|
+
const normalizePath = (p) => p.split(path.sep).join('/');
|
|
5
|
+
|
|
6
|
+
const VERSION = "1.2.0"; // Updated to match output
|
|
7
|
+
const PACKAGE_NAME = "repomeld";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_IGNORE = [
|
|
10
|
+
"node_modules", ".git", ".env", ".env.local", ".env.production",
|
|
11
|
+
".DS_Store", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
|
|
12
|
+
".next", ".nuxt", "dist", "build", ".cache"
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const LANGUAGE_MAP = {
|
|
16
|
+
js: "javascript", jsx: "javascript", ts: "typescript", tsx: "typescript",
|
|
17
|
+
py: "python", rb: "ruby", java: "java", cpp: "cpp", c: "c",
|
|
18
|
+
cs: "csharp", go: "go", rs: "rust", php: "php", swift: "swift",
|
|
19
|
+
kt: "kotlin", html: "html", css: "css", scss: "scss", json: "json",
|
|
20
|
+
yaml: "yaml", yml: "yaml", md: "markdown", sh: "bash", bash: "bash",
|
|
21
|
+
toml: "toml", xml: "xml", sql: "sql", graphql: "graphql",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Binary extensions cache for performance
|
|
25
|
+
const BINARY_EXTENSIONS = new Set([
|
|
26
|
+
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg',
|
|
27
|
+
'pdf', 'zip', 'tar', 'gz', '7z', 'rar',
|
|
28
|
+
'mp3', 'mp4', 'avi', 'mov', 'wav',
|
|
29
|
+
'exe', 'dll', 'so', 'dylib',
|
|
30
|
+
'woff', 'woff2', 'ttf', 'eot', 'otf'
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
normalizePath,
|
|
35
|
+
VERSION,
|
|
36
|
+
PACKAGE_NAME,
|
|
37
|
+
DEFAULT_IGNORE,
|
|
38
|
+
LANGUAGE_MAP,
|
|
39
|
+
BINARY_EXTENSIONS,
|
|
40
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
function formatSize(bytes) {
|
|
4
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
5
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
6
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
7
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function formatDuration(ms) {
|
|
11
|
+
return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function matchesExtensions(filePath, exts) {
|
|
15
|
+
if (!exts?.length) return true;
|
|
16
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
17
|
+
return exts.map(e => e.replace(/^\./, "").toLowerCase()).includes(ext);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function matchesPattern(filePath, patterns) {
|
|
21
|
+
if (!patterns?.length) return false;
|
|
22
|
+
const rel = path.relative(process.cwd(), filePath);
|
|
23
|
+
return patterns.some(p => rel.includes(p) || path.basename(filePath).includes(p));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getLanguage(filePath, LANGUAGE_MAP) {
|
|
27
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
28
|
+
return LANGUAGE_MAP[ext] || "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
formatSize,
|
|
33
|
+
formatDuration,
|
|
34
|
+
matchesExtensions,
|
|
35
|
+
matchesPattern,
|
|
36
|
+
getLanguage,
|
|
37
|
+
};
|