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 CHANGED
@@ -1,59 +1,41 @@
1
1
  {
2
2
  "name": "repomeld",
3
- "version": "3.0.0",
4
- "description": "Meld your entire repo into a single file — perfect for AI context, code reviews & sharing",
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
- "test": "node bin/cli.js --dry-run",
12
- "prepublishOnly": "npm run test",
13
- "postinstall": "node -e \"console.log('\\n📦 repomeld installed successfully! Run: repomeld --help\\n')\""
11
+ "dev": "node bin/cli.js --dry-run",
12
+ "test": "node bin/cli.js --dry-run --no-update-check"
13
+
14
14
  },
15
- "keywords": [
16
- "cli",
17
- "repo",
18
- "file",
19
- "combiner",
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/sakshsky/repomeld.git"
35
+ "url": "git+https://github.com/susheel/repomeld.git"
33
36
  },
34
37
  "bugs": {
35
- "url": "https://github.com/sakshsky/repomeld/issues"
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
- "files": [
52
- "bin/",
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
+ };