repomeld 2.0.5 → 3.0.1

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/bin/cli.js CHANGED
@@ -1,499 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require("fs").promises;
4
- const fsSync = require("fs");
5
- const path = require("path");
6
3
  const { program } = require("commander");
7
- const ignore = require("ignore");
8
- const { isBinaryFile } = require("isbinaryfile");
9
- const readline = require("readline");
10
- const os = require("os");
11
-
12
- const VERSION = "1.0.0";
13
- const PACKAGE_NAME = "repomeld";
14
-
15
- // Normalize paths for cross-platform compatibility
16
- const normalizePath = (p) => p.split(path.sep).join('/');
17
-
18
- const DEFAULT_IGNORE = [
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
-
34
- ];
35
-
36
- const LANGUAGE_MAP = {
37
- js: "javascript", jsx: "javascript", ts: "typescript", tsx: "typescript",
38
- py: "python", rb: "ruby", java: "java", cpp: "cpp", c: "c",
39
- cs: "csharp", go: "go", rs: "rust", php: "php", swift: "swift",
40
- kt: "kotlin", html: "html", css: "css", scss: "scss", json: "json",
41
- yaml: "yaml", yml: "yaml", md: "markdown", sh: "bash", bash: "bash",
42
- toml: "toml", xml: "xml", sql: "sql", graphql: "graphql",
43
- };
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
-
56
- function getLanguage(filePath) {
57
- const ext = path.extname(filePath).slice(1).toLowerCase();
58
- return LANGUAGE_MAP[ext] || "";
59
- }
60
-
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
- }
99
- }
100
-
101
- return fileList;
102
- }
103
-
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 };
131
- }
132
-
133
- function matchesExtensions(filePath, exts) {
134
- if (!exts || exts.length === 0) return true;
135
- const ext = path.extname(filePath).slice(1).toLowerCase();
136
- return exts.map((e) => e.replace(/^\./, "").toLowerCase()).includes(ext);
137
- }
138
-
139
- function matchesPattern(filePath, patterns) {
140
- if (!patterns || patterns.length === 0) return false;
141
- const rel = path.relative(process.cwd(), filePath);
142
- return patterns.some((p) => rel.includes(p) || path.basename(filePath).includes(p));
143
- }
144
-
145
- function formatSize(bytes) {
146
- if (bytes < 1024) return `${bytes} B`;
147
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
148
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
149
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
150
- }
151
-
152
- function formatDuration(ms) {
153
- if (ms < 1000) return `${ms}ms`;
154
- const seconds = (ms / 1000).toFixed(1);
155
- return `${seconds}s`;
156
- }
157
-
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)}`);
188
- }
189
- }
190
-
191
- function printBanner() {
192
- 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
- ╚══════════════════════════════════════════════════════╝`);
200
- }
201
-
202
- function buildHeader(style, relativePath, filePath, lineCount, showMeta, stats) {
203
- const lang = getLanguage(filePath);
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
- }
212
- const divider = "─".repeat(60);
213
- return `\n${divider}\n FILE: ${relativePath}${meta}\n${divider}\n\n`;
214
- }
215
-
216
- function buildFooter(style) {
217
- if (style === "markdown") return "\n```\n";
218
- return "\n";
219
- }
220
-
221
- function buildTableOfContents(files, cwd) {
222
- let toc = "TABLE OF CONTENTS\n" + "═".repeat(60) + "\n";
223
- files.forEach((f, i) => {
224
- const rel = path.relative(cwd, f);
225
- toc += ` ${String(i + 1).padStart(3, " ")}. ${rel}\n`;
226
- });
227
- toc += "═".repeat(60) + "\n\n";
228
- return toc;
229
- }
230
-
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 };
244
- }
245
- }
246
- } catch {
247
- return { path: desiredPath, number: null };
248
- }
249
- }
250
-
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);
257
- }
258
-
259
- function escapeRegex(str) {
260
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
261
- }
262
-
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
- });
297
- }
298
-
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`);
304
- }
305
-
306
- async function repomeld(options) {
307
- const startTime = Date.now();
308
- printBanner();
309
-
310
- const cwd = process.cwd();
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 || [];
317
- const maxFileSizeBytes = (parseFloat(options.maxSize) || 500) * 1024;
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}`);
328
- console.log(` 📄 Output : ${path.relative(cwd, outputFile)}`);
329
- console.log(` 🎨 Style : ${headerStyle}`);
330
- if (!options.noGitignore) console.log(` 📁 .gitignore respected`);
331
- if (filterExts.length) console.log(` 🔍 Filter : .${filterExts.join(", .")}`);
332
- if (forceInclude && forceInclude.length) console.log(` 📌 Force : ${forceInclude.join(", ")}`);
333
- if (dryRun) console.log(` 🧪 Dry run : no file will be written`);
334
- console.log();
335
-
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)}`);
340
-
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;
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
-
367
- let combinedContent = "";
368
- let skipped = 0;
369
- let included = 0;
370
- let totalLines = 0;
371
- const includedFiles = [];
372
-
373
- const progress = new ProgressIndicator(allFiles.length, ' ');
374
-
375
- for (let i = 0; i < allFiles.length; i++) {
376
- const filePath = allFiles[i];
377
- const relativePath = path.relative(cwd, filePath);
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
-
393
- try {
394
- let content = await fs.readFile(filePath, "utf8");
395
-
396
- if (options.trim) {
397
- content = content.trim();
398
- }
399
-
400
- if (linesBefore > 0 || linesAfter > 0) {
401
- const lines = content.split("\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");
405
- }
406
-
407
- const lineCount = content.split("\n").length;
408
- totalLines += lineCount;
409
- includedFiles.push(filePath);
410
-
411
- combinedContent += buildHeader(headerStyle, relativePath, filePath, lineCount, showMeta, stats);
412
- combinedContent += content;
413
- combinedContent += buildFooter(headerStyle);
414
-
415
- included++;
416
- } catch (err) {
417
- skipped++;
418
- }
419
- }
420
-
421
- progress.finish();
422
-
423
- // Build final output
424
- let finalOutput = "";
425
- const timestamp = new Date().toISOString();
426
- finalOutput += `# Generated by repomeld v${VERSION}\n`;
427
- finalOutput += `# Date : ${timestamp}\n`;
428
- finalOutput += `# Source : ${cwd}\n`;
429
- finalOutput += `# Files : ${included}\n`;
430
- finalOutput += `# Lines : ${totalLines}\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);
435
- }
436
-
437
- finalOutput += combinedContent;
438
-
439
- if (!dryRun && includedFiles.length > 0) {
440
- await fs.writeFile(outputFile, finalOutput, "utf8");
441
- }
442
-
443
- const outputSize = formatSize(Buffer.byteLength(finalOutput, "utf8"));
444
- const totalTime = Date.now() - startTime;
445
-
446
- console.log(`
447
- ✨ repomeld complete!
448
- ─────────────────────────────────────────────────
449
- ✅ Included : ${included} files
450
- ⏭ Skipped : ${skipped} files
451
- 📏 Lines : ${totalLines}
452
- 💾 Size : ${outputSize}
453
- ⏱️ Time : ${formatDuration(totalTime)}
454
- 📄 Output : ${path.relative(cwd, outputFile)}${dryRun ? " (dry run — not written)" : ""}
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
- }
466
- }
467
-
468
- // ─── CLI Definition ───────────────────────────────────────────
4
+ const { repomeld } = require("../src/index");
5
+ const { VERSION } = require("../src/utils/constants");
469
6
 
470
7
  program
471
8
  .name("repomeld")
472
- .description("Meld your entire repo into a single file — perfect for AI context, code reviews & sharing")
9
+ .description("Meld your entire repo into a single file — perfect for AI context & code reviews")
473
10
  .version(VERSION)
474
-
475
- .option("-o, --output <filename>", "Output file name", "repomeld_output.txt")
11
+ .option("-o, --output <filename>", "Output filename", "repomeld_output.txt")
476
12
  .option("-e, --ext <exts...>", "Only include specific extensions")
477
- .option("--include <patterns...>", "Only include files matching patterns")
13
+ .option("--include <patterns...>", "Force include files matching patterns")
478
14
  .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")
15
+ .option("-i, --ignore <names...>", "Additional ignore patterns")
16
+ .option("--force-include <names...>", "Force include even if ignored")
17
+ .option("--max-size <kb>", "Maximum file size in KB", "500")
18
+ .option("--no-gitignore", "Don't respect .gitignore")
483
19
  .option("-s, --style <style>", "Header style: banner | markdown | minimal", "banner")
484
20
  .option("--no-toc", "Disable table of contents")
485
21
  .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
-
22
+ .option("--trim", "Trim whitespace from each file")
23
+ .option("--lines-before <n>", "Skip first N lines", parseInt)
24
+ .option("--lines-after <n>", "Skip last N lines", parseInt)
25
+ .option("--dry-run", "Preview without writing files")
26
+ .option("--no-backup", "Skip backup zip creation")
27
+ .option("--no-update-check", "Skip update check")
492
28
  .action(async (options) => {
493
29
  try {
494
30
  await repomeld(options);
495
31
  } catch (error) {
496
- console.error(`\n ❌ Error: ${error.message}`);
32
+ console.error(`\n❌ Error: ${error.message}`);
497
33
  if (process.env.DEBUG) console.error(error);
498
34
  process.exit(1);
499
35
  }
package/package.json CHANGED
@@ -1,58 +1,41 @@
1
1
  {
2
2
  "name": "repomeld",
3
- "version": "2.0.5",
4
- "description": "Meld your entire repo into a single file — perfect for AI context, code reviews & sharing",
3
+ "version": "3.0.1",
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
  ],
28
- "author": "Susheel <susheelhbti@gmail.com>",
29
- "license": "MIT",
30
- "repository": {
31
- "type": "git",
32
- "url": "git+https://github.com/sakshsky/repomeld.git"
33
- },
34
- "bugs": {
35
- "url": "https://github.com/sakshsky/repomeld/issues"
36
- },
37
- "homepage": "https://github.com/sakshsky/repomeld#readme",
38
21
  "dependencies": {
22
+ "archiver": "^6.0.1",
39
23
  "commander": "^11.1.0",
40
- "ignore": "^5.3.2",
24
+ "ignore": "^5.3.0",
41
25
  "isbinaryfile": "^5.0.0"
42
26
  },
43
- "devDependencies": {
44
- "eslint": "^8.56.0",
45
- "prettier": "^3.1.1"
46
- },
27
+ "keywords": ["cli", "repo", "combiner", "ai-context", "code-review", "mermaid", "dependency-graph"],
28
+ "author": "Susheel <susheelhbti@gmail.com>",
29
+ "license": "MIT",
47
30
  "engines": {
48
31
  "node": ">=14.0.0"
49
32
  },
50
- "files": [
51
- "bin/",
52
- "README.md",
53
- "LICENSE"
54
- ],
55
- "publishConfig": {
56
- "access": "public"
57
- }
58
- }
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/susheel/repomeld.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/susheel/repomeld/issues"
39
+ },
40
+ "homepage": "https://github.com/susheel/repomeld#readme"
41
+ }
@@ -0,0 +1,47 @@
1
+ const fs = require("fs").promises;
2
+ const path = require("path");
3
+ const { normalizePath } = require("../utils/constants");
4
+
5
+ async function getAllFilesWithIgnore(dirPath, ig, forceIncludePatterns) {
6
+ const fileList = [];
7
+ const stack = [{ dirPath, relativePath: '.' }];
8
+
9
+ while (stack.length) {
10
+ const { dirPath: currentDir, relativePath: currentRelative } = stack.pop();
11
+
12
+ let entries;
13
+ try {
14
+ entries = await fs.readdir(currentDir, { withFileTypes: true });
15
+ } catch {
16
+ continue;
17
+ }
18
+
19
+ for (const entry of entries) {
20
+ const fullPath = path.join(currentDir, entry.name);
21
+ const relativePath = path.join(currentRelative, entry.name);
22
+ const normalizedPath = normalizePath(relativePath);
23
+
24
+ // Check force include first - improved pattern matching
25
+ const isForceIncluded = forceIncludePatterns?.some(pattern => {
26
+ // Support exact matches and path contains
27
+ const patternClean = pattern.replace(/^\.\//, '').replace(/\/$/, '');
28
+ return normalizedPath === patternClean ||
29
+ normalizedPath.includes(patternClean) ||
30
+ entry.name === patternClean ||
31
+ normalizedPath.startsWith(patternClean + '/');
32
+ });
33
+
34
+ // Only check ignore if not force-included
35
+ if (!isForceIncluded && ig.ignores(normalizedPath)) continue;
36
+
37
+ if (entry.isDirectory()) {
38
+ stack.push({ dirPath: fullPath, relativePath });
39
+ } else if (entry.isFile()) {
40
+ fileList.push(fullPath);
41
+ }
42
+ }
43
+ }
44
+ return fileList;
45
+ }
46
+
47
+ module.exports = { getAllFilesWithIgnore };