combicode 1.5.3 → 1.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.7.1](https://github.com/aaurelions/combicode/compare/combicode-js-v1.7.0...combicode-js-v1.7.1) (2025-01-XX)
4
+
5
+ ### Bug Fixes
6
+
7
+ - **ci:** restore NPM_TOKEN for npm publishing to fix authentication issues
8
+
9
+ ## [1.7.0](https://github.com/aaurelions/combicode/compare/combicode-js-v1.6.0...combicode-js-v1.7.0) (2025-01-XX)
10
+
11
+ ### Maintenance
12
+
13
+ - **ci:** migrate to npm trusted publishing (OIDC), remove NPM_TOKEN requirement
14
+
15
+ ## [1.6.0](https://github.com/aaurelions/combicode/compare/combicode-js-v1.5.4...combicode-js-v1.6.0) (2025-01-XX)
16
+
17
+ ### Features
18
+
19
+ - **skip-content:** add `--skip-content` option to include files in tree but omit their content (useful for large test files)
20
+
21
+ ## [1.5.4](https://github.com/aaurelions/combicode/compare/combicode-js-v1.5.3...combicode-js-v1.5.4) (2025-12-03)
22
+
23
+ ### Features
24
+
25
+ - **ignore:** implement full support for nested `.gitignore` files, ensuring exclusion rules are applied correctly within subdirectories
26
+
27
+ ### Bug Fixes
28
+
29
+ - **deps:** replace `fast-glob` with `ignore` and recursive directory walking for accurate git-like pattern matching
30
+
3
31
  ## [1.5.3](https://github.com/aaurelions/combicode/compare/combicode-js-v1.5.2...combicode-js-v1.5.3) (2025-11-30)
4
32
 
5
33
  ### Bug Fixes
package/README.md CHANGED
@@ -16,6 +16,7 @@ The generated file starts with a system prompt and a file tree overview, priming
16
16
  - **Intelligent Priming:** Starts the output with a system prompt and a file tree, directing the LLM to analyze the entire codebase before responding.
17
17
  - **Intelligent Ignoring:** Automatically skips `node_modules`, `.venv`, `dist`, `.git`, binary files, and other common junk.
18
18
  - **`.gitignore` Aware:** Respects your project's existing `.gitignore` rules out of the box.
19
+ - **Nested Ignore Support:** Correctly handles `.gitignore` files located in subdirectories, ensuring local exclusion rules are respected.
19
20
  - **Zero-Install Usage:** Run it directly with `npx` or `pipx` without polluting your environment.
20
21
  - **Customizable:** Easily filter by file extension or add custom ignore patterns.
21
22
 
@@ -80,6 +81,20 @@ Use the `--exclude` or `-e` flag with comma-separated glob patterns.
80
81
  npx combicode -e "**/*_test.py,docs/**"
81
82
  ```
82
83
 
84
+ ### Skip content for specific files
85
+
86
+ Use the `--skip-content` flag to include files in the tree structure but omit their content. This is useful for large files (like test files) that you want visible in the project overview but don't need their full content.
87
+
88
+ ```bash
89
+ # Include .test.ts files in tree but skip their content
90
+ npx combicode --skip-content "**/*.test.ts"
91
+
92
+ # Skip content for multiple patterns
93
+ npx combicode --skip-content "**/*.test.ts,**/*.spec.ts,**/tests/**"
94
+ ```
95
+
96
+ Files with skipped content will be marked with `(content omitted)` in the file tree and will show a placeholder in the content section.
97
+
83
98
  ### Generating Context for `llms.txt`
84
99
 
85
100
  The `--llms.txt` or `-l` flag is designed for projects that use an [`llms.txt`](https://llmstxt.org/) file to specify important documentation. When this flag is used, Combicode inserts a specialized system prompt telling the LLM that the provided context is the project's definitive documentation for a specific version. This helps the LLM provide more accurate answers and avoid using deprecated functions.
@@ -97,6 +112,7 @@ npx combicode -l -i .md -o llms.txt
97
112
  | `--dry-run` | `-d` | Preview files without creating the output file. | `false` |
98
113
  | `--include-ext` | `-i` | Comma-separated list of extensions to exclusively include. | (include all) |
99
114
  | `--exclude` | `-e` | Comma-separated list of additional glob patterns to exclude. | (none) |
115
+ | `--skip-content` | | Comma-separated glob patterns for files to include in tree but omit content. | (none) |
100
116
  | `--llms-txt` | `-l` | Use a specialized system prompt for context generated from an `llms.txt` file. | `false` |
101
117
  | `--no-gitignore` | | Do not use patterns from the project's `.gitignore` file. | `false` |
102
118
  | `--no-header` | | Omit the introductory prompt and file tree from the output. | `false` |
package/index.js CHANGED
@@ -4,7 +4,7 @@ const fs = require("fs");
4
4
  const path = require("path");
5
5
  const yargs = require("yargs/yargs");
6
6
  const { hideBin } = require("yargs/helpers");
7
- const glob = require("fast-glob");
7
+ const ignore = require("ignore");
8
8
 
9
9
  const { version } = require("./package.json");
10
10
 
@@ -24,26 +24,14 @@ When answering questions or writing code, adhere strictly to the functions, vari
24
24
  A file tree of the documentation source is provided below for a high-level overview. The subsequent sections contain the full content of each file, clearly marked with a file header.
25
25
  `;
26
26
 
27
- function loadDefaultIgnorePatterns() {
28
- const configPath = path.resolve(__dirname, "config", "ignore.json");
29
- try {
30
- const rawConfig = fs.readFileSync(configPath, "utf8");
31
- return JSON.parse(rawConfig);
32
- } catch (err) {
33
- console.error(
34
- `❌ Critical: Could not read or parse bundled ignore config at ${configPath}`
35
- );
36
- process.exit(1);
37
- }
38
- }
39
-
40
- const DEFAULT_IGNORE_PATTERNS = loadDefaultIgnorePatterns();
27
+ // Minimal safety ignores that should always apply
28
+ const SAFETY_IGNORES = [".git", ".DS_Store"];
41
29
 
42
- function isLikelyBinary(file) {
30
+ function isLikelyBinary(filePath) {
43
31
  const buffer = Buffer.alloc(512);
44
32
  let fd;
45
33
  try {
46
- fd = fs.openSync(file, "r");
34
+ fd = fs.openSync(filePath, "r");
47
35
  const bytesRead = fs.readSync(fd, buffer, 0, 512, 0);
48
36
  return buffer.slice(0, bytesRead).includes(0);
49
37
  } catch (e) {
@@ -62,18 +50,137 @@ function formatBytes(bytes, decimals = 1) {
62
50
  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + "" + sizes[i];
63
51
  }
64
52
 
65
- function generateFileTree(filesWithSize, root) {
53
+ /**
54
+ * Recursively walks directories, respecting .gitignore files at each level.
55
+ */
56
+ function walkDirectory(
57
+ currentDir,
58
+ rootDir,
59
+ ignoreChain,
60
+ allowedExts,
61
+ absoluteOutputPath,
62
+ useGitIgnore,
63
+ stats // { scanned: 0, ignored: 0 }
64
+ ) {
65
+ let results = [];
66
+ let currentIgnoreManager = null;
67
+
68
+ // 1. Check for local .gitignore and add to chain for this scope
69
+ if (useGitIgnore) {
70
+ const gitignorePath = path.join(currentDir, ".gitignore");
71
+ if (fs.existsSync(gitignorePath)) {
72
+ try {
73
+ const content = fs.readFileSync(gitignorePath, "utf8");
74
+ const ig = ignore().add(content);
75
+ currentIgnoreManager = { manager: ig, root: currentDir };
76
+ } catch (e) {
77
+ // Warning could go here
78
+ }
79
+ }
80
+ }
81
+
82
+ // Create a new chain for this directory and its children
83
+ const nextIgnoreChain = currentIgnoreManager
84
+ ? [...ignoreChain, currentIgnoreManager]
85
+ : ignoreChain;
86
+
87
+ let entries;
88
+ try {
89
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
90
+ } catch (e) {
91
+ return [];
92
+ }
93
+
94
+ for (const entry of entries) {
95
+ const fullPath = path.join(currentDir, entry.name);
96
+
97
+ // SKIP CHECK: Output file
98
+ if (path.resolve(fullPath) === absoluteOutputPath) continue;
99
+
100
+ // SKIP CHECK: Ignore Chain
101
+ let shouldIgnore = false;
102
+ for (const item of nextIgnoreChain) {
103
+ // Calculate path relative to the specific ignore manager's root
104
+ // IMPORTANT: Normalize to POSIX slashes for 'ignore' package compatibility
105
+ let relToIgnoreRoot = path.relative(item.root, fullPath);
106
+
107
+ if (path.sep === "\\") {
108
+ relToIgnoreRoot = relToIgnoreRoot.replace(/\\/g, "/");
109
+ }
110
+
111
+ // If checking a directory, ensure trailing slash for proper 'ignore' directory matching
112
+ if (entry.isDirectory() && !relToIgnoreRoot.endsWith("/")) {
113
+ relToIgnoreRoot += "/";
114
+ }
115
+
116
+ if (item.manager.ignores(relToIgnoreRoot)) {
117
+ shouldIgnore = true;
118
+ break;
119
+ }
120
+ }
121
+
122
+ if (shouldIgnore) {
123
+ stats.ignored++;
124
+ continue;
125
+ }
126
+
127
+ if (entry.isDirectory()) {
128
+ // Recurse
129
+ results = results.concat(
130
+ walkDirectory(
131
+ fullPath,
132
+ rootDir,
133
+ nextIgnoreChain,
134
+ allowedExts,
135
+ absoluteOutputPath,
136
+ useGitIgnore,
137
+ stats
138
+ )
139
+ );
140
+ } else if (entry.isFile()) {
141
+ // SKIP CHECK: Binary
142
+ if (isLikelyBinary(fullPath)) {
143
+ stats.ignored++;
144
+ continue;
145
+ }
146
+
147
+ // SKIP CHECK: Extensions
148
+ if (allowedExts && !allowedExts.has(path.extname(entry.name))) {
149
+ stats.ignored++;
150
+ continue;
151
+ }
152
+
153
+ try {
154
+ const fileStats = fs.statSync(fullPath);
155
+ const relativeToRoot = path.relative(rootDir, fullPath);
156
+ stats.scanned++;
157
+ results.push({
158
+ path: fullPath,
159
+ relativePath: relativeToRoot,
160
+ size: fileStats.size,
161
+ formattedSize: formatBytes(fileStats.size),
162
+ });
163
+ } catch (e) {
164
+ // Skip inaccessible files
165
+ }
166
+ }
167
+ }
168
+
169
+ return results;
170
+ }
171
+
172
+ function generateFileTree(filesWithSize, root, skipContentSet = null) {
66
173
  let tree = `${path.basename(root)}/\n`;
67
174
  const structure = {};
68
175
 
69
- // Build the structure
70
176
  filesWithSize.forEach(({ relativePath, formattedSize }) => {
71
177
  const parts = relativePath.split(path.sep);
72
178
  let currentLevel = structure;
73
179
  parts.forEach((part, index) => {
74
180
  const isFile = index === parts.length - 1;
75
181
  if (isFile) {
76
- currentLevel[part] = formattedSize;
182
+ const shouldSkipContent = skipContentSet && skipContentSet.has(relativePath);
183
+ currentLevel[part] = { size: formattedSize, skipContent: shouldSkipContent };
77
184
  } else {
78
185
  if (!currentLevel[part]) {
79
186
  currentLevel[part] = {};
@@ -88,12 +195,12 @@ function generateFileTree(filesWithSize, root) {
88
195
  entries.forEach((entry, index) => {
89
196
  const isLast = index === entries.length - 1;
90
197
  const value = level[entry];
91
- const isFile = typeof value === "string";
92
-
198
+ const isFile = typeof value === "object" && value.size !== undefined;
93
199
  const connector = isLast ? "└── " : "├── ";
94
200
 
95
201
  if (isFile) {
96
- tree += `${prefix}${connector}[${value}] ${entry}\n`;
202
+ const marker = value.skipContent ? " (content omitted)" : "";
203
+ tree += `${prefix}${connector}[${value.size}] ${entry}${marker}\n`;
97
204
  } else {
98
205
  tree += `${prefix}${connector}${entry}\n`;
99
206
  buildTree(value, `${prefix}${isLast ? " " : "│ "}`);
@@ -112,7 +219,6 @@ async function main() {
112
219
  process.exit(0);
113
220
  }
114
221
 
115
- // Yargs singleton usage works correctly with arguments passed here
116
222
  const argv = yargs(rawArgv)
117
223
  .scriptName("combicode")
118
224
  .usage("$0 [options]")
@@ -154,43 +260,36 @@ async function main() {
154
260
  type: "boolean",
155
261
  default: false,
156
262
  })
263
+ .option("skip-content", {
264
+ describe: "Comma-separated glob patterns for files to include in tree but omit content",
265
+ type: "string",
266
+ })
157
267
  .version(version)
158
268
  .alias("v", "version")
159
269
  .help()
160
270
  .alias("h", "help").argv;
161
271
 
162
272
  const projectRoot = process.cwd();
163
- console.log(`✨ Running Combicode in: ${projectRoot}`);
273
+ console.log(`\n✨ Combicode v${version}`);
274
+ console.log(`📂 Root: ${projectRoot}`);
164
275
 
165
- const ignorePatterns = [...DEFAULT_IGNORE_PATTERNS];
276
+ const rootIgnoreManager = ignore();
166
277
 
167
- if (!argv.noGitignore) {
168
- const gitignorePath = path.join(projectRoot, ".gitignore");
169
- if (fs.existsSync(gitignorePath)) {
170
- console.log("🔎 Found and using .gitignore");
171
- const gitignoreContent = fs.readFileSync(gitignorePath, "utf8");
172
- ignorePatterns.push(
173
- ...gitignoreContent
174
- .split(/\r?\n/)
175
- .filter((line) => line && !line.startsWith("#"))
176
- );
177
- }
178
- }
278
+ // Only add minimal safety ignores + CLI excludes.
279
+ // No external JSON config is loaded.
280
+ rootIgnoreManager.add(SAFETY_IGNORES);
179
281
 
180
282
  if (argv.exclude) {
181
- ignorePatterns.push(...argv.exclude.split(","));
283
+ rootIgnoreManager.add(argv.exclude.split(","));
182
284
  }
183
285
 
184
- // Calculate the absolute path of the output file to prevent self-inclusion
185
- const absoluteOutputPath = path.resolve(projectRoot, argv.output);
286
+ // Create skip-content manager
287
+ const skipContentManager = ignore();
288
+ if (argv.skipContent) {
289
+ skipContentManager.add(argv.skipContent.split(","));
290
+ }
186
291
 
187
- let allFiles = await glob("**/*", {
188
- cwd: projectRoot,
189
- dot: true,
190
- ignore: ignorePatterns,
191
- absolute: true,
192
- stats: true,
193
- });
292
+ const absoluteOutputPath = path.resolve(projectRoot, argv.output);
194
293
 
195
294
  const allowedExtensions = argv.includeExt
196
295
  ? new Set(
@@ -200,73 +299,121 @@ async function main() {
200
299
  )
201
300
  : null;
202
301
 
203
- const includedFiles = allFiles
204
- .filter((fileObj) => {
205
- const file = fileObj.path;
302
+ // Initialize the ignore chain with the root manager
303
+ const ignoreChain = [{ manager: rootIgnoreManager, root: projectRoot }];
304
+
305
+ // Statistics container
306
+ const stats = { scanned: 0, ignored: 0 };
307
+
308
+ // Perform Recursive Walk
309
+ const includedFiles = walkDirectory(
310
+ projectRoot,
311
+ projectRoot,
312
+ ignoreChain,
313
+ allowedExtensions,
314
+ absoluteOutputPath,
315
+ !argv.noGitignore,
316
+ stats
317
+ );
206
318
 
207
- // Prevent the output file from being included in the list
208
- // We use path.normalize to handle potential differences in separators (e.g., / vs \)
209
- if (path.normalize(file) === absoluteOutputPath) return false;
319
+ includedFiles.sort((a, b) => a.path.localeCompare(b.path));
210
320
 
211
- if (!fileObj.stats || fileObj.stats.isDirectory()) return false;
212
- if (isLikelyBinary(file)) return false;
213
- if (allowedExtensions && !allowedExtensions.has(path.extname(file)))
214
- return false;
215
- return true;
216
- })
217
- .map((fileObj) => ({
218
- path: fileObj.path,
219
- relativePath: path.relative(projectRoot, fileObj.path),
220
- size: fileObj.stats.size,
221
- formattedSize: formatBytes(fileObj.stats.size),
222
- }))
223
- .sort((a, b) => a.path.localeCompare(b.path));
321
+ // Determine which files should have content skipped
322
+ const skipContentSet = new Set();
323
+ if (argv.skipContent) {
324
+ includedFiles.forEach((file) => {
325
+ const relativePath = file.relativePath.replace(/\\/g, "/");
326
+ if (skipContentManager.ignores(relativePath)) {
327
+ skipContentSet.add(file.relativePath);
328
+ }
329
+ });
330
+ }
331
+
332
+ // Calculate total size of included files
333
+ const totalSizeBytes = includedFiles.reduce(
334
+ (acc, file) => acc + file.size,
335
+ 0
336
+ );
224
337
 
225
338
  if (includedFiles.length === 0) {
226
- console.error("❌ No files to include. Check your path or filters.");
339
+ console.error(
340
+ "\n❌ No files to include. Check your path, .gitignore, or filters."
341
+ );
227
342
  process.exit(1);
228
343
  }
229
344
 
230
345
  if (argv.dryRun) {
231
346
  console.log("\n📋 Files to be included (Dry Run):\n");
232
- const tree = generateFileTree(includedFiles, projectRoot);
347
+ const tree = generateFileTree(includedFiles, projectRoot, skipContentSet);
233
348
  console.log(tree);
234
- console.log(`\nTotal: ${includedFiles.length} files.`);
349
+ console.log("\n📊 Summary (Dry Run):");
350
+ console.log(
351
+ ` • Included: ${includedFiles.length} files (${formatBytes(
352
+ totalSizeBytes
353
+ )})`
354
+ );
355
+ if (skipContentSet.size > 0) {
356
+ console.log(` • Content omitted: ${skipContentSet.size} files`);
357
+ }
358
+ console.log(` • Ignored: ${stats.ignored} files/dirs`);
235
359
  return;
236
360
  }
237
361
 
238
362
  const outputStream = fs.createWriteStream(argv.output);
363
+ let totalLines = 0;
239
364
 
240
365
  if (!argv.noHeader) {
241
366
  const systemPrompt = argv.llmsTxt
242
367
  ? LLMS_TXT_SYSTEM_PROMPT
243
368
  : DEFAULT_SYSTEM_PROMPT;
244
369
  outputStream.write(systemPrompt + "\n");
370
+ totalLines += systemPrompt.split("\n").length;
371
+
245
372
  outputStream.write("## Project File Tree\n\n");
246
373
  outputStream.write("```\n");
247
- const tree = generateFileTree(includedFiles, projectRoot);
374
+ const tree = generateFileTree(includedFiles, projectRoot, skipContentSet);
248
375
  outputStream.write(tree);
249
376
  outputStream.write("```\n\n");
250
377
  outputStream.write("---\n\n");
378
+
379
+ totalLines += tree.split("\n").length + 5;
251
380
  }
252
381
 
253
382
  for (const fileObj of includedFiles) {
254
383
  const relativePath = fileObj.relativePath.replace(/\\/g, "/");
384
+ const shouldSkipContent = skipContentSet.has(fileObj.relativePath);
385
+
255
386
  outputStream.write(`### **FILE:** \`${relativePath}\`\n`);
256
387
  outputStream.write("```\n");
257
- try {
258
- const content = fs.readFileSync(fileObj.path, "utf8");
259
- outputStream.write(content);
260
- } catch (e) {
261
- outputStream.write(`... (error reading file: ${e.message}) ...`);
388
+ if (shouldSkipContent) {
389
+ outputStream.write(`(Content omitted - file size: ${fileObj.formattedSize})\n`);
390
+ totalLines += 1;
391
+ } else {
392
+ try {
393
+ const content = fs.readFileSync(fileObj.path, "utf8");
394
+ outputStream.write(content);
395
+ totalLines += content.split("\n").length;
396
+ } catch (e) {
397
+ outputStream.write(`... (error reading file: ${e.message}) ...`);
398
+ }
262
399
  }
263
400
  outputStream.write("\n```\n\n");
401
+ totalLines += 4; // Headers/footers lines
264
402
  }
265
403
  outputStream.end();
266
404
 
405
+ console.log(`\n📊 Summary:`);
267
406
  console.log(
268
- `\n✅ Success! Combined ${includedFiles.length} files into '${argv.output}'.`
407
+ ` • Included: ${includedFiles.length} files (${formatBytes(
408
+ totalSizeBytes
409
+ )})`
269
410
  );
411
+ if (skipContentSet.size > 0) {
412
+ console.log(` • Content omitted: ${skipContentSet.size} files`);
413
+ }
414
+ console.log(` • Ignored: ${stats.ignored} files/dirs`);
415
+ console.log(` • Output: ${argv.output} (~${totalLines} lines)`);
416
+ console.log(`\n✅ Done!`);
270
417
  }
271
418
 
272
419
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "combicode",
3
- "version": "1.5.3",
3
+ "version": "1.7.1",
4
4
  "description": "A CLI tool to combine a project's codebase into a single file for LLM context.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -10,8 +10,6 @@
10
10
  "access": "public"
11
11
  },
12
12
  "scripts": {
13
- "prepack": "mkdir -p config && cp ../configs/ignore.json config/ignore.json",
14
- "pretest": "mkdir -p config && cp ../configs/ignore.json config/ignore.json",
15
13
  "test": "node test/test.js"
16
14
  },
17
15
  "repository": {
@@ -34,7 +32,7 @@
34
32
  "author": "A. Aurelions",
35
33
  "license": "MIT",
36
34
  "dependencies": {
37
- "fast-glob": "^3.3.3",
35
+ "ignore": "^5.3.0",
38
36
  "yargs": "^17.7.2"
39
37
  }
40
38
  }
package/test/test.js CHANGED
@@ -7,20 +7,17 @@ const CLI_PATH = path.resolve(__dirname, "../index.js");
7
7
  const TEST_DIR = path.resolve(__dirname, "temp_env");
8
8
  const OUTPUT_FILE = path.join(TEST_DIR, "combicode.txt");
9
9
 
10
- // Setup: Create a temp directory with dummy files
11
- function setup() {
12
- if (fs.existsSync(TEST_DIR)) {
13
- fs.rmSync(TEST_DIR, { recursive: true, force: true });
14
- }
15
- fs.mkdirSync(TEST_DIR);
16
-
17
- // Create a dummy JS file
18
- fs.writeFileSync(path.join(TEST_DIR, "alpha.js"), "console.log('alpha');");
19
-
20
- // Create a dummy text file in a subdir
21
- const subDir = path.join(TEST_DIR, "subdir");
22
- fs.mkdirSync(subDir);
23
- fs.writeFileSync(path.join(subDir, "beta.txt"), "Hello World");
10
+ // Helper to create directory structure
11
+ function createStructure(base, structure) {
12
+ Object.entries(structure).forEach(([name, content]) => {
13
+ const fullPath = path.join(base, name);
14
+ if (typeof content === "object") {
15
+ fs.mkdirSync(fullPath);
16
+ createStructure(fullPath, content);
17
+ } else {
18
+ fs.writeFileSync(fullPath, content);
19
+ }
20
+ });
24
21
  }
25
22
 
26
23
  // Teardown: Cleanup temp directory
@@ -34,53 +31,177 @@ function runTest() {
34
31
  console.log("🧪 Starting Node.js Integration Tests...");
35
32
 
36
33
  try {
37
- setup();
34
+ // Clean start
35
+ teardown();
36
+ fs.mkdirSync(TEST_DIR);
38
37
 
39
- // 1. Test Version Flag
40
- console.log(" Checking --version...");
38
+ // --- Scenario 1: Basic Functionality ---
39
+ console.log(" [1/4] Checking Basic Functionality & Version...");
41
40
  const versionOutput = execSync(`node ${CLI_PATH} --version`).toString();
42
41
  assert.match(versionOutput, /Combicode \(JavaScript\), version/);
43
42
 
44
- // 2. Test Dry Run
45
- console.log(" Checking --dry-run...");
43
+ createStructure(TEST_DIR, {
44
+ "alpha.js": "console.log('alpha');",
45
+ subdir: {
46
+ "beta.txt": "Hello World",
47
+ },
48
+ });
49
+
50
+ // Capture dry-run output to verify structure
46
51
  const dryRunOutput = execSync(`node ${CLI_PATH} --dry-run`, {
47
52
  cwd: TEST_DIR,
48
53
  }).toString();
49
54
  assert.match(dryRunOutput, /Files to be included \(Dry Run\)/);
50
- // Check for file size format in tree (e.g., [21B])
51
- assert.match(dryRunOutput, /\[\d+(\.\d+)?[KM]?B\]/);
55
+ assert.match(dryRunOutput, /\[\d+(\.\d+)?[KM]?B\]/); // Size check
52
56
 
53
- // 3. Test Actual Generation
54
- console.log(" Checking file generation...");
55
- execSync(`node ${CLI_PATH} --output combicode.txt`, { cwd: TEST_DIR });
57
+ // Run generation
58
+ execSync(`node ${CLI_PATH} --output combicode.txt`, {
59
+ cwd: TEST_DIR,
60
+ stdio: "inherit",
61
+ });
56
62
 
57
63
  assert.ok(fs.existsSync(OUTPUT_FILE), "Output file should exist");
64
+ let content = fs.readFileSync(OUTPUT_FILE, "utf8");
65
+ assert.ok(content.includes("### **FILE:** `alpha.js`"));
66
+ assert.ok(content.includes("### **FILE:** `subdir/beta.txt`"));
58
67
 
59
- const content = fs.readFileSync(OUTPUT_FILE, "utf8");
68
+ // --- Scenario 2: Nested .gitignore Support ---
69
+ console.log(" [2/4] Checking Nested .gitignore Support...");
70
+ teardown();
71
+ fs.mkdirSync(TEST_DIR);
72
+
73
+ createStructure(TEST_DIR, {
74
+ "root.js": "root",
75
+ "ignore_me_root.log": "log",
76
+ ".gitignore": "*.log",
77
+ nested: {
78
+ "child.js": "child",
79
+ "ignore_me_child.tmp": "tmp",
80
+ ".gitignore": "*.tmp",
81
+ deep: {
82
+ "deep.js": "deep",
83
+ "ignore_local.txt": "txt",
84
+ ".gitignore": "ignore_local.txt",
85
+ },
86
+ },
87
+ });
88
+
89
+ execSync(`node ${CLI_PATH} -o combicode.txt`, {
90
+ cwd: TEST_DIR,
91
+ stdio: "inherit",
92
+ });
93
+ content = fs.readFileSync(OUTPUT_FILE, "utf8");
60
94
 
61
- // Check for System Prompt
95
+ // Should include:
96
+ assert.ok(content.includes("### **FILE:** `root.js`"), "root.js missing");
62
97
  assert.ok(
63
- content.includes("You are an expert software architect"),
64
- "System prompt missing"
98
+ content.includes("### **FILE:** `nested/child.js`"),
99
+ "child.js missing"
100
+ );
101
+ assert.ok(
102
+ content.includes("### **FILE:** `nested/deep/deep.js`"),
103
+ "deep.js missing"
65
104
  );
66
105
 
67
- // Check for Tree structure
68
- assert.ok(content.includes("subdir"), "Tree should show subdirectory");
106
+ // Should exclude (Checking Headers, not content):
107
+ assert.ok(
108
+ !content.includes("### **FILE:** `ignore_me_root.log`"),
109
+ "Root gitignore failed (*.log)"
110
+ );
111
+ assert.ok(
112
+ !content.includes("### **FILE:** `nested/ignore_me_child.tmp`"),
113
+ "Nested gitignore failed (*.tmp)"
114
+ );
115
+ assert.ok(
116
+ !content.includes("### **FILE:** `nested/deep/ignore_local.txt`"),
117
+ "Deep nested gitignore failed (specific file)"
118
+ );
69
119
 
70
- // Check for new Header format
120
+ // --- Scenario 3: CLI Exclude Override ---
121
+ console.log(" [3/4] Checking CLI Exclude Flags...");
122
+ execSync(`node ${CLI_PATH} -o combicode.txt -e "**/deep.js"`, {
123
+ cwd: TEST_DIR,
124
+ stdio: "inherit",
125
+ });
126
+ content = fs.readFileSync(OUTPUT_FILE, "utf8");
71
127
  assert.ok(
72
- content.includes("### **FILE:** `alpha.js`"),
73
- "New header format missing for alpha.js"
128
+ !content.includes("### **FILE:** `nested/deep/deep.js`"),
129
+ "CLI exclude flag failed"
74
130
  );
131
+
132
+ // --- Scenario 4: Output File Self-Exclusion ---
133
+ console.log(" [4/4] Checking Output File Self-Exclusion...");
134
+ execSync(`node ${CLI_PATH} -o combicode.txt`, {
135
+ cwd: TEST_DIR,
136
+ stdio: "inherit",
137
+ });
138
+ content = fs.readFileSync(OUTPUT_FILE, "utf8");
139
+ assert.ok(
140
+ !content.includes("### **FILE:** `combicode.txt`"),
141
+ "Output file included itself"
142
+ );
143
+
144
+ // --- Scenario 5: Skip Content Feature ---
145
+ console.log(" [5/5] Checking Skip Content Feature...");
146
+ teardown();
147
+ fs.mkdirSync(TEST_DIR);
148
+
149
+ createStructure(TEST_DIR, {
150
+ "main.js": "console.log('main');",
151
+ "test.js": "describe('test', () => { it('works', () => {}); });",
152
+ "large.test.ts": "const data = " + '"x'.repeat(1000) + '";',
153
+ subdir: {
154
+ "spec.ts": "describe('spec', () => {});",
155
+ "utils.js": "export function util() {}",
156
+ },
157
+ });
158
+
159
+ execSync(`node ${CLI_PATH} -o combicode.txt --skip-content "**/*test.ts,**/*spec.ts"`, {
160
+ cwd: TEST_DIR,
161
+ stdio: "inherit",
162
+ });
163
+ content = fs.readFileSync(OUTPUT_FILE, "utf8");
164
+
165
+ // Files should appear in tree with (content omitted) marker
75
166
  assert.ok(
76
- content.includes("### **FILE:** `subdir/beta.txt`"),
77
- "New header format missing for beta.txt"
167
+ content.includes("large.test.ts (content omitted)"),
168
+ "Tree should show (content omitted) marker for large.test.ts"
78
169
  );
170
+ // Check for spec.ts - it might be in subdir/spec.ts path
171
+ assert.ok(
172
+ content.includes("spec.ts (content omitted)") || content.includes("subdir/spec.ts (content omitted)"),
173
+ "Tree should show (content omitted) marker for spec.ts"
174
+ );
175
+
176
+ // Files should have FILE headers
177
+ assert.ok(content.includes("### **FILE:** `large.test.ts`"), "File header should exist");
178
+ assert.ok(content.includes("### **FILE:** `subdir/spec.ts`"), "File header should exist");
179
+
180
+ // Content should be omitted (placeholder instead)
181
+ const largeTestMatch = content.match(/### \*\*FILE:\*\* `large\.test\.ts`[\s\S]*?```([\s\S]*?)```/);
182
+ assert.ok(largeTestMatch, "Should find large.test.ts content section");
183
+ assert.ok(
184
+ largeTestMatch[1].includes("Content omitted"),
185
+ "Content should be replaced with placeholder"
186
+ );
187
+ assert.ok(
188
+ largeTestMatch[1].includes("file size:"),
189
+ "Placeholder should include file size"
190
+ );
191
+
192
+ // Regular files should have full content
193
+ assert.ok(content.includes("console.log('main');"), "main.js should have full content");
194
+ assert.ok(content.includes("export function util() {}"), "utils.js should have full content");
195
+
196
+ // Dry run should show content omitted count
197
+ const skipContentDryRunOutput = execSync(`node ${CLI_PATH} --dry-run --skip-content "**/*.test.ts"`, {
198
+ cwd: TEST_DIR,
199
+ }).toString();
200
+ assert.match(skipContentDryRunOutput, /Content omitted:/, "Dry run should show content omitted count");
79
201
 
80
202
  console.log("✅ All Node.js tests passed!");
81
203
  } catch (error) {
82
204
  console.error("❌ Test Failed:", error.message);
83
- if (error.stdout) console.log(error.stdout.toString());
84
205
  process.exit(1);
85
206
  } finally {
86
207
  teardown();
@@ -1,53 +0,0 @@
1
- [
2
- "**/node_modules/**",
3
- "**/.git/**",
4
- "**/.vscode/**",
5
- "**/.idea/**",
6
- "**/*.log",
7
- "**/.env",
8
- "**/*.lock",
9
- "**/.venv/**",
10
- "**/venv/**",
11
- "**/env/**",
12
- "**/__pycache__/**",
13
- "**/*.pyc",
14
- "**/*.egg-info/**",
15
- "**/build/**",
16
- "**/dist/**",
17
- "**/.pytest_cache/**",
18
- "**/.npm/**",
19
- "**/pnpm-lock.yaml",
20
- "**/package-lock.json",
21
- "**/.next/**",
22
- "**/.DS_Store",
23
- "**/Thumbs.db",
24
- "**/*.png",
25
- "**/*.jpg",
26
- "**/*.jpeg",
27
- "**/*.gif",
28
- "**/*.ico",
29
- "**/*.svg",
30
- "**/*.webp",
31
- "**/*.mp3",
32
- "**/*.wav",
33
- "**/*.flac",
34
- "**/*.mp4",
35
- "**/*.mov",
36
- "**/*.avi",
37
- "**/*.zip",
38
- "**/*.tar.gz",
39
- "**/*.rar",
40
- "**/*.pdf",
41
- "**/*.doc",
42
- "**/*.docx",
43
- "**/*.xls",
44
- "**/*.xlsx",
45
- "**/*.dll",
46
- "**/*.exe",
47
- "**/*.so",
48
- "**/*.a",
49
- "**/*.lib",
50
- "**/*.o",
51
- "**/*.bin",
52
- "**/*.iso"
53
- ]