Skip to content

Commit bb34fb2

Browse files
committed
feat: add file sizes to tree, improve file headers, and add integration tests
1 parent 6cb5f83 commit bb34fb2

File tree

14 files changed

+333
-105
lines changed

14 files changed

+333
-105
lines changed

‎.github/workflows/publish-to-npm.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ jobs:
3232
- name: Install dependencies
3333
run: npm install
3434

35+
- name: Run Tests
36+
run: npm test
37+
3538
- name: Publish to npm
3639
run: npm publish --access public --no-git-checks
3740
env:

‎.github/workflows/publish-to-pypi.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ jobs:
3434
- name: Install dependencies
3535
run: python -m pip install --upgrade pip build
3636

37+
- name: Run Tests
38+
run: |
39+
pip install ".[test]"
40+
pytest
41+
3742
- name: Build package
3843
run: python -m build
3944

‎.release-please-manifest.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"combicode-py": "1.4.0",
3-
"combicode-js": "1.4.0"
2+
"combicode-py": "1.5.0",
3+
"combicode-js": "1.5.0"
44
}

‎combicode-js/.gitignore‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,7 @@ dist
111111
.pnp.*
112112

113113
# macOS
114-
.DS_Store
114+
.DS_Store
115+
116+
temp_env
117+
config

‎combicode-js/CHANGELOG.md‎

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,45 @@
11
# Changelog
22

3-
## [1.4.0](https://github.com/aaurelions/combicode/compare/combicode-js-v1.3.0...combicode-js-v1.4.0) (2025-08-13)
3+
## [1.5.0](https://github.com/aaurelions/combicode/compare/combicode-js-v1.4.0...combicode-js-v1.5.0) (2025-11-30)
4+
5+
### Features
46

7+
- **tree:** display file sizes in the generated file tree (e.g., `[1.2KB]`)
8+
- **output:** improve file header format (`### **FILE:**`) for better LLM parsing
9+
- **tests:** add integration test suite using native Node.js modules
10+
11+
## [1.4.0](https://github.com/aaurelions/combicode/compare/combicode-js-v1.3.0...combicode-js-v1.4.0) (2025-08-13)
512

613
### Features
714

8-
* Add --llms-txt flag for llms.txt documentation context ([0817554](https://github.com/aaurelions/combicode/commit/081755435594b0ca5208609b2724eb47bd73c2dc))
15+
- Add --llms-txt flag for llms.txt documentation context ([0817554](https://github.com/aaurelions/combicode/commit/081755435594b0ca5208609b2724eb47bd73c2dc))
916

1017
## [1.3.0](https://github.com/aaurelions/combicode/compare/combicode-js-v1.2.1...combicode-js-v1.3.0) (2025-06-25)
1118

12-
1319
### Features
1420

15-
* Add package-lock.json for reproducible builds ([06a417a](https://github.com/aaurelions/combicode/commit/06a417a155e9b72e26d0e091d181cfb8c53f0d28))
16-
* **ci:** implement independent package versioning for monorepo ([d02cf23](https://github.com/aaurelions/combicode/commit/d02cf233239c7af8db19061f34b769178334b388))
17-
* improve CLI output and version reporting ([7963a10](https://github.com/aaurelions/combicode/commit/7963a10782c2626608750de53023d37d327d51b2))
18-
* improve CLI output and version reporting ([e74f6d8](https://github.com/aaurelions/combicode/commit/e74f6d8fbed4f9cdf8ad82f3dae87069f66f7bb6))
19-
21+
- Add package-lock.json for reproducible builds ([06a417a](https://github.com/aaurelions/combicode/commit/06a417a155e9b72e26d0e091d181cfb8c53f0d28))
22+
- **ci:** implement independent package versioning for monorepo ([d02cf23](https://github.com/aaurelions/combicode/commit/d02cf233239c7af8db19061f34b769178334b388))
23+
- improve CLI output and version reporting ([7963a10](https://github.com/aaurelions/combicode/commit/7963a10782c2626608750de53023d37d327d51b2))
24+
- improve CLI output and version reporting ([e74f6d8](https://github.com/aaurelions/combicode/commit/e74f6d8fbed4f9cdf8ad82f3dae87069f66f7bb6))
2025

2126
### Bug Fixes
2227

23-
* **ci:** permission error ([156b76d](https://github.com/aaurelions/combicode/commit/156b76d3ab1550123df2ded6b1da5d6e2e2cc008))
24-
* **npm:** Set public access for publishing and bump version to 1.0.1 ([6c91eb7](https://github.com/aaurelions/combicode/commit/6c91eb714c81ec0201bb0fcfad8ad9fb4124cd7e))
25-
* Use scoped npm package name and bump python version ([8a1b347](https://github.com/aaurelions/combicode/commit/8a1b347f6c54c9762acf354ef289c293d3ef21a3))
28+
- **ci:** permission error ([156b76d](https://github.com/aaurelions/combicode/commit/156b76d3ab1550123df2ded6b1da5d6e2e2cc008))
29+
- **npm:** Set public access for publishing and bump version to 1.0.1 ([6c91eb7](https://github.com/aaurelions/combicode/commit/6c91eb714c81ec0201bb0fcfad8ad9fb4124cd7e))
30+
- Use scoped npm package name and bump python version ([8a1b347](https://github.com/aaurelions/combicode/commit/8a1b347f6c54c9762acf354ef289c293d3ef21a3))
2631

2732
## [1.2.0](https://github.com/aaurelions/combicode/compare/combicode-js-v1.1.0...combicode-js-v1.2.0) (2025-06-25)
2833

29-
3034
### Features
3135

32-
* Add package-lock.json for reproducible builds ([06a417a](https://github.com/aaurelions/combicode/commit/06a417a155e9b72e26d0e091d181cfb8c53f0d28))
33-
* **ci:** implement independent package versioning for monorepo ([d02cf23](https://github.com/aaurelions/combicode/commit/d02cf233239c7af8db19061f34b769178334b388))
34-
* improve CLI output and version reporting ([7963a10](https://github.com/aaurelions/combicode/commit/7963a10782c2626608750de53023d37d327d51b2))
35-
* improve CLI output and version reporting ([e74f6d8](https://github.com/aaurelions/combicode/commit/e74f6d8fbed4f9cdf8ad82f3dae87069f66f7bb6))
36-
36+
- Add package-lock.json for reproducible builds ([06a417a](https://github.com/aaurelions/combicode/commit/06a417a155e9b72e26d0e091d181cfb8c53f0d28))
37+
- **ci:** implement independent package versioning for monorepo ([d02cf23](https://github.com/aaurelions/combicode/commit/d02cf233239c7af8db19061f34b769178334b388))
38+
- improve CLI output and version reporting ([7963a10](https://github.com/aaurelions/combicode/commit/7963a10782c2626608750de53023d37d327d51b2))
39+
- improve CLI output and version reporting ([e74f6d8](https://github.com/aaurelions/combicode/commit/e74f6d8fbed4f9cdf8ad82f3dae87069f66f7bb6))
3740

3841
### Bug Fixes
3942

40-
* **ci:** permission error ([156b76d](https://github.com/aaurelions/combicode/commit/156b76d3ab1550123df2ded6b1da5d6e2e2cc008))
41-
* **npm:** Set public access for publishing and bump version to 1.0.1 ([6c91eb7](https://github.com/aaurelions/combicode/commit/6c91eb714c81ec0201bb0fcfad8ad9fb4124cd7e))
42-
* Use scoped npm package name and bump python version ([8a1b347](https://github.com/aaurelions/combicode/commit/8a1b347f6c54c9762acf354ef289c293d3ef21a3))
43+
- **ci:** permission error ([156b76d](https://github.com/aaurelions/combicode/commit/156b76d3ab1550123df2ded6b1da5d6e2e2cc008))
44+
- **npm:** Set public access for publishing and bump version to 1.0.1 ([6c91eb7](https://github.com/aaurelions/combicode/commit/6c91eb714c81ec0201bb0fcfad8ad9fb4124cd7e))
45+
- Use scoped npm package name and bump python version ([8a1b347](https://github.com/aaurelions/combicode/commit/8a1b347f6c54c9762acf354ef289c293d3ef21a3))

‎combicode-js/index.js‎

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const { version } = require("./package.json");
1010

1111
const DEFAULT_SYSTEM_PROMPT = `You are an expert software architect. The user is providing you with the complete source code for a project, contained in a single file. Your task is to meticulously analyze the provided codebase to gain a comprehensive understanding of its structure, functionality, dependencies, and overall architecture.
1212
13-
A file tree is provided below to give you a high-level overview. The subsequent sections contain the full content of each file, clearly marked with "// FILE: <path>".
13+
A file tree is provided below to give you a high-level overview. The subsequent sections contain the full content of each file, clearly marked with a file header.
1414
1515
Your instructions are:
1616
1. **Analyze Thoroughly:** Read through every file to understand its purpose and how it interacts with other files.
@@ -21,7 +21,7 @@ const LLMS_TXT_SYSTEM_PROMPT = `You are an expert software architect. The user i
2121
2222
When answering questions or writing code, adhere strictly to the functions, variables, and methods described in this context. Do not use or suggest any deprecated or older functionalities that are not present here.
2323
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 "// FILE: <path>".
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.
2525
`;
2626

2727
function loadDefaultIgnorePatterns() {
@@ -53,28 +53,50 @@ function isLikelyBinary(file) {
5353
}
5454
}
5555

56-
function generateFileTree(files, root) {
56+
function formatBytes(bytes, decimals = 1) {
57+
if (bytes === 0) return "0 B";
58+
const k = 1024;
59+
const dm = decimals < 0 ? 0 : decimals;
60+
const sizes = ["B", "KB", "MB", "GB", "TB"];
61+
const i = Math.floor(Math.log(bytes) / Math.log(k));
62+
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + "" + sizes[i];
63+
}
64+
65+
function generateFileTree(filesWithSize, root) {
5766
let tree = `${path.basename(root)}/\n`;
5867
const structure = {};
5968

60-
files.forEach((file) => {
61-
const parts = file.split(path.sep);
69+
// Build the structure
70+
filesWithSize.forEach(({ relativePath, formattedSize }) => {
71+
const parts = relativePath.split(path.sep);
6272
let currentLevel = structure;
63-
parts.forEach((part) => {
64-
if (!currentLevel[part]) {
65-
currentLevel[part] = {};
73+
parts.forEach((part, index) => {
74+
const isFile = index === parts.length - 1;
75+
if (isFile) {
76+
currentLevel[part] = formattedSize;
77+
} else {
78+
if (!currentLevel[part]) {
79+
currentLevel[part] = {};
80+
}
81+
currentLevel = currentLevel[part];
6682
}
67-
currentLevel = currentLevel[part];
6883
});
6984
});
7085

7186
const buildTree = (level, prefix) => {
7287
const entries = Object.keys(level);
7388
entries.forEach((entry, index) => {
7489
const isLast = index === entries.length - 1;
75-
tree += `${prefix}${isLast ? "└── " : "├── "}${entry}\n`;
76-
if (Object.keys(level[entry]).length > 0) {
77-
buildTree(level[entry], `${prefix}${isLast ? " " : "│ "}`);
90+
const value = level[entry];
91+
const isFile = typeof value === "string";
92+
93+
const connector = isLast ? "└── " : "├── ";
94+
95+
if (isFile) {
96+
tree += `${prefix}${connector}[${value}] ${entry}\n`;
97+
} else {
98+
tree += `${prefix}${connector}${entry}\n`;
99+
buildTree(value, `${prefix}${isLast ? " " : "│ "}`);
78100
}
79101
});
80102
};
@@ -163,7 +185,7 @@ async function main() {
163185
dot: true,
164186
ignore: ignorePatterns,
165187
absolute: true,
166-
stats: false,
188+
stats: true,
167189
});
168190

169191
const allowedExtensions = argv.includeExt
@@ -175,28 +197,30 @@ async function main() {
175197
: null;
176198

177199
const includedFiles = allFiles
178-
.filter((file) => {
179-
const stats = fs.statSync(file, { throwIfNoEntry: false });
180-
if (!stats || stats.isDirectory()) return false;
200+
.filter((fileObj) => {
201+
const file = fileObj.path;
202+
if (!fileObj.stats || fileObj.stats.isDirectory()) return false;
181203
if (isLikelyBinary(file)) return false;
182204
if (allowedExtensions && !allowedExtensions.has(path.extname(file)))
183205
return false;
184206
return true;
185207
})
186-
.sort();
208+
.map((fileObj) => ({
209+
path: fileObj.path,
210+
relativePath: path.relative(projectRoot, fileObj.path),
211+
size: fileObj.stats.size,
212+
formattedSize: formatBytes(fileObj.stats.size),
213+
}))
214+
.sort((a, b) => a.path.localeCompare(b.path));
187215

188216
if (includedFiles.length === 0) {
189217
console.error("❌ No files to include. Check your path or filters.");
190218
process.exit(1);
191219
}
192220

193-
const relativeFiles = includedFiles.map((file) =>
194-
path.relative(projectRoot, file)
195-
);
196-
197221
if (argv.dryRun) {
198222
console.log("\n📋 Files to be included (Dry Run):\n");
199-
const tree = generateFileTree(relativeFiles, projectRoot);
223+
const tree = generateFileTree(includedFiles, projectRoot);
200224
console.log(tree);
201225
console.log(`\nTotal: ${includedFiles.length} files.`);
202226
return;
@@ -211,18 +235,18 @@ async function main() {
211235
outputStream.write(systemPrompt + "\n");
212236
outputStream.write("## Project File Tree\n\n");
213237
outputStream.write("```\n");
214-
const tree = generateFileTree(relativeFiles, projectRoot);
238+
const tree = generateFileTree(includedFiles, projectRoot);
215239
outputStream.write(tree);
216240
outputStream.write("```\n\n");
217241
outputStream.write("---\n\n");
218242
}
219243

220-
for (const file of includedFiles) {
221-
const relativePath = path.relative(projectRoot, file).replace(/\\/g, "/");
222-
outputStream.write(`// FILE: ${relativePath}` + "\n");
244+
for (const fileObj of includedFiles) {
245+
const relativePath = fileObj.relativePath.replace(/\\/g, "/");
246+
outputStream.write(`### **FILE:** \`${relativePath}\`\n`);
223247
outputStream.write("```\n");
224248
try {
225-
const content = fs.readFileSync(file, "utf8");
249+
const content = fs.readFileSync(fileObj.path, "utf8");
226250
outputStream.write(content);
227251
} catch (e) {
228252
outputStream.write(`... (error reading file: ${e.message}) ...`);

‎combicode-js/package.json‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "combicode",
3-
"version": "1.4.0",
3+
"version": "1.5.0",
44
"description": "A CLI tool to combine a project's codebase into a single file for LLM context.",
55
"main": "index.js",
66
"bin": {
@@ -10,7 +10,8 @@
1010
"access": "public"
1111
},
1212
"scripts": {
13-
"test": "echo \"Error: no test specified\" && exit 1"
13+
"pretest": "mkdir -p config && cp ../configs/ignore.json config/ignore.json",
14+
"test": "node test/test.js"
1415
},
1516
"repository": {
1617
"type": "git",
@@ -32,7 +33,7 @@
3233
"author": "A. Aurelions",
3334
"license": "MIT",
3435
"dependencies": {
35-
"fast-glob": "^3.3.1",
36-
"yargs": "^17.7.2"
36+
"fast-glob": "^3.3.3",
37+
"yargs": "^18.0.0"
3738
}
3839
}

‎combicode-js/test/test.js‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
const { execSync } = require("child_process");
4+
const assert = require("assert");
5+
6+
const CLI_PATH = path.resolve(__dirname, "../index.js");
7+
const TEST_DIR = path.resolve(__dirname, "temp_env");
8+
const OUTPUT_FILE = path.join(TEST_DIR, "combicode.txt");
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");
24+
}
25+
26+
// Teardown: Cleanup temp directory
27+
function teardown() {
28+
if (fs.existsSync(TEST_DIR)) {
29+
fs.rmSync(TEST_DIR, { recursive: true, force: true });
30+
}
31+
}
32+
33+
function runTest() {
34+
console.log("🧪 Starting Node.js Integration Tests...");
35+
36+
try {
37+
setup();
38+
39+
// 1. Test Version Flag
40+
console.log(" Checking --version...");
41+
const versionOutput = execSync(`node ${CLI_PATH} --version`).toString();
42+
assert.match(versionOutput, /Combicode \(JavaScript\), version/);
43+
44+
// 2. Test Dry Run
45+
console.log(" Checking --dry-run...");
46+
const dryRunOutput = execSync(`node ${CLI_PATH} --dry-run`, {
47+
cwd: TEST_DIR,
48+
}).toString();
49+
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\]/);
52+
53+
// 3. Test Actual Generation
54+
console.log(" Checking file generation...");
55+
execSync(`node ${CLI_PATH} --output combicode.txt`, { cwd: TEST_DIR });
56+
57+
assert.ok(fs.existsSync(OUTPUT_FILE), "Output file should exist");
58+
59+
const content = fs.readFileSync(OUTPUT_FILE, "utf8");
60+
61+
// Check for System Prompt
62+
assert.ok(
63+
content.includes("You are an expert software architect"),
64+
"System prompt missing"
65+
);
66+
67+
// Check for Tree structure
68+
assert.ok(content.includes("subdir"), "Tree should show subdirectory");
69+
70+
// Check for new Header format
71+
assert.ok(
72+
content.includes("### **FILE:** `alpha.js`"),
73+
"New header format missing for alpha.js"
74+
);
75+
assert.ok(
76+
content.includes("### **FILE:** `subdir/beta.txt`"),
77+
"New header format missing for beta.txt"
78+
);
79+
80+
console.log("✅ All Node.js tests passed!");
81+
} catch (error) {
82+
console.error("❌ Test Failed:", error.message);
83+
if (error.stdout) console.log(error.stdout.toString());
84+
process.exit(1);
85+
} finally {
86+
teardown();
87+
}
88+
}
89+
90+
runTest();

‎combicode-py/.gitignore‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,7 @@ coverage.xml
4141
.DS_Store
4242

4343
# Logs
44-
*.log
44+
*.log
45+
46+
tmp_path
47+
config

0 commit comments

Comments
 (0)