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 +28 -0
- package/README.md +16 -0
- package/index.js +224 -77
- package/package.json +2 -4
- package/test/test.js +157 -36
- package/config/ignore.json +0 -53
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
|
|
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
|
-
|
|
28
|
-
|
|
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(
|
|
30
|
+
function isLikelyBinary(filePath) {
|
|
43
31
|
const buffer = Buffer.alloc(512);
|
|
44
32
|
let fd;
|
|
45
33
|
try {
|
|
46
|
-
fd = fs.openSync(
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
92
|
-
|
|
198
|
+
const isFile = typeof value === "object" && value.size !== undefined;
|
|
93
199
|
const connector = isLast ? "└── " : "├── ";
|
|
94
200
|
|
|
95
201
|
if (isFile) {
|
|
96
|
-
|
|
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(
|
|
273
|
+
console.log(`\n✨ Combicode v${version}`);
|
|
274
|
+
console.log(`📂 Root: ${projectRoot}`);
|
|
164
275
|
|
|
165
|
-
const
|
|
276
|
+
const rootIgnoreManager = ignore();
|
|
166
277
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
283
|
+
rootIgnoreManager.add(argv.exclude.split(","));
|
|
182
284
|
}
|
|
183
285
|
|
|
184
|
-
//
|
|
185
|
-
const
|
|
286
|
+
// Create skip-content manager
|
|
287
|
+
const skipContentManager = ignore();
|
|
288
|
+
if (argv.skipContent) {
|
|
289
|
+
skipContentManager.add(argv.skipContent.split(","));
|
|
290
|
+
}
|
|
186
291
|
|
|
187
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
//
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
34
|
+
// Clean start
|
|
35
|
+
teardown();
|
|
36
|
+
fs.mkdirSync(TEST_DIR);
|
|
38
37
|
|
|
39
|
-
// 1
|
|
40
|
-
console.log(" Checking
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
assert.match(dryRunOutput, /\[\d+(\.\d+)?[KM]?B\]/);
|
|
55
|
+
assert.match(dryRunOutput, /\[\d+(\.\d+)?[KM]?B\]/); // Size check
|
|
52
56
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
95
|
+
// Should include:
|
|
96
|
+
assert.ok(content.includes("### **FILE:** `root.js`"), "root.js missing");
|
|
62
97
|
assert.ok(
|
|
63
|
-
content.includes("
|
|
64
|
-
"
|
|
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
|
-
//
|
|
68
|
-
assert.ok(
|
|
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
|
-
//
|
|
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:** `
|
|
73
|
-
"
|
|
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("
|
|
77
|
-
"
|
|
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();
|
package/config/ignore.json
DELETED
|
@@ -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
|
-
]
|