@vetterjulius/cagg 1.0.0
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/LICENSE +21 -0
- package/README.md +74 -0
- package/bin/cagg.js +4 -0
- package/package.json +24 -0
- package/src/cli.js +95 -0
- package/src/defaultIgnore.js +131 -0
- package/src/index.js +189 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Julius Vetter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# cagg — Code Aggregator
|
|
2
|
+
|
|
3
|
+
Bundles every relevant text/code file of a repository into a single output
|
|
4
|
+
file, each preceded by a clear header showing which file follows:
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
-------------- src/index.js -----------------
|
|
8
|
+
1: 'use strict';
|
|
9
|
+
2:
|
|
10
|
+
...
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Built for feeding a codebase to an LLM or for a quick, readable overview of
|
|
14
|
+
a project — the output is kept as small as possible (no unnecessary blank
|
|
15
|
+
lines, no binaries, no build artifacts).
|
|
16
|
+
|
|
17
|
+
## Example
|
|
18
|
+
An example aggregation of the source code for the tool is included. See [agg.txt](agg.txt).
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g cagg
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cagg # aggregate CWD into ./agg.txt
|
|
30
|
+
cagg -i path/to/project # aggregate a different folder
|
|
31
|
+
cagg -o context.md # change output name/extension
|
|
32
|
+
cagg -a # use absolute paths in headers instead of CWD-relative
|
|
33
|
+
cagg --no-line-numbers # don't prefix lines with numbers
|
|
34
|
+
cagg --max-size 1 # skip files larger than 1 MB
|
|
35
|
+
cagg --max-lines 2000 # skip files with more than 2000 lines
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Ignoring files
|
|
39
|
+
|
|
40
|
+
By default, `cagg` ignores an extensive built-in list of common clutter:
|
|
41
|
+
`.git`, `node_modules`, build/dist output, caches, lockfiles, IDE folders,
|
|
42
|
+
OS files, and common binary/media file types (images, fonts, archives,
|
|
43
|
+
documents, ...). Files that don't look like text (checked by content, not
|
|
44
|
+
just extension) are always skipped and reported.
|
|
45
|
+
|
|
46
|
+
- `--no-ignore` — disable all ignoring; every file is included. Prints a
|
|
47
|
+
warning since this will likely pull in dependencies and binaries.
|
|
48
|
+
- `--ignore-patterns "pattern1,pattern2"` — supply your own
|
|
49
|
+
`.gitignore`-style patterns. This **replaces** the built-in default list.
|
|
50
|
+
Can be repeated / combined with commas.
|
|
51
|
+
- `--ignore-file <path>` — same idea, but patterns come from a file (e.g.
|
|
52
|
+
point it at your real `.gitignore`). Also **replaces** the default list.
|
|
53
|
+
- If the current directory has a `.gitignore`, its entries are
|
|
54
|
+
automatically merged into whichever ignore list is active (default or
|
|
55
|
+
custom). Disable this with `--no-merge-gitignore`.
|
|
56
|
+
|
|
57
|
+
The output file itself is always excluded from its own aggregation.
|
|
58
|
+
|
|
59
|
+
## All options
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
-i, --input <path> input directory to scan (relative to CWD or absolute) (default: ".")
|
|
63
|
+
-o, --output <path> output file name/path (default: "agg.txt")
|
|
64
|
+
-a, --absolute-paths use absolute paths in file headers instead of CWD-relative paths
|
|
65
|
+
--no-ignore disable all ignoring; include every file (prints a warning)
|
|
66
|
+
--ignore-patterns <patterns> comma-separated gitignore-style patterns, replaces default ignore list
|
|
67
|
+
--ignore-file <path> file with gitignore-style patterns, replaces default ignore list
|
|
68
|
+
--no-merge-gitignore don't merge a CWD .gitignore into the active ignore list
|
|
69
|
+
--no-line-numbers do not prefix each line with its line number
|
|
70
|
+
--max-size <mb> skip files larger than this size in MB
|
|
71
|
+
--max-lines <n> skip files with more lines than this
|
|
72
|
+
-h, --help display help
|
|
73
|
+
-V, --version output the version number
|
|
74
|
+
```
|
package/bin/cagg.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vetterjulius/cagg",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Code Aggregator - bundles all text/code files of a repository into a single .txt file, with gitignore-aware filtering",
|
|
5
|
+
"keywords": ["cli", "aggregator", "code", "llm", "context", "gitignore"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"cagg": "./bin/cagg.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "src/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=16"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"commander": "^12.1.0",
|
|
21
|
+
"ignore": "^5.3.2",
|
|
22
|
+
"isbinaryfile": "^5.0.2"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { aggregate } = require('./index');
|
|
6
|
+
const pkg = require('../package.json');
|
|
7
|
+
|
|
8
|
+
function splitList(value, previous) {
|
|
9
|
+
const items = value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
10
|
+
return (previous || []).concat(items);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseFloatOpt(value) {
|
|
14
|
+
const n = parseFloat(value);
|
|
15
|
+
if (Number.isNaN(n) || n <= 0) {
|
|
16
|
+
throw new Error(`Invalid numeric value: ${value}`);
|
|
17
|
+
}
|
|
18
|
+
return n;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseIntOpt(value) {
|
|
22
|
+
const n = parseInt(value, 10);
|
|
23
|
+
if (Number.isNaN(n) || n <= 0) {
|
|
24
|
+
throw new Error(`Invalid integer value: ${value}`);
|
|
25
|
+
}
|
|
26
|
+
return n;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function run(argv) {
|
|
30
|
+
const program = new Command();
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.name('cagg')
|
|
34
|
+
.description(
|
|
35
|
+
'Code Aggregator: bundles all text/code files of a repository into a single ' +
|
|
36
|
+
'output file, separated by clear file-path headers.'
|
|
37
|
+
)
|
|
38
|
+
.version(pkg.version)
|
|
39
|
+
.option('-i, --input <path>', 'input directory to scan (relative to CWD or absolute)', '.')
|
|
40
|
+
.option('-o, --output <path>', 'output file name/path (extension can be changed)', 'agg.txt')
|
|
41
|
+
.option('-a, --absolute-paths', 'use absolute paths in file headers instead of CWD-relative paths', false)
|
|
42
|
+
.option('--no-ignore', 'disable all ignoring; include every file (prints a warning)')
|
|
43
|
+
.option(
|
|
44
|
+
'--ignore-patterns <patterns>',
|
|
45
|
+
'comma-separated .gitignore-style patterns; REPLACES the built-in default ignore list ' +
|
|
46
|
+
'(repeatable)',
|
|
47
|
+
splitList,
|
|
48
|
+
[]
|
|
49
|
+
)
|
|
50
|
+
.option(
|
|
51
|
+
'--ignore-file <path>',
|
|
52
|
+
'path to a file with .gitignore-style patterns (e.g. your real .gitignore); ' +
|
|
53
|
+
'REPLACES the built-in default ignore list'
|
|
54
|
+
)
|
|
55
|
+
.option(
|
|
56
|
+
'--no-merge-gitignore',
|
|
57
|
+
"don't additionally merge a .gitignore found in the CWD into the active ignore list"
|
|
58
|
+
)
|
|
59
|
+
.option('--no-line-numbers', 'do not prefix each line with its line number')
|
|
60
|
+
.option('--max-size <mb>', 'skip files larger than this size in MB', parseFloatOpt)
|
|
61
|
+
.option('--max-lines <n>', 'skip files with more lines than this', parseIntOpt);
|
|
62
|
+
|
|
63
|
+
let opts;
|
|
64
|
+
try {
|
|
65
|
+
program.parse(argv);
|
|
66
|
+
opts = program.opts();
|
|
67
|
+
|
|
68
|
+
const result = aggregate({
|
|
69
|
+
input: opts.input,
|
|
70
|
+
output: opts.output,
|
|
71
|
+
absolutePaths: Boolean(opts.absolutePaths),
|
|
72
|
+
noIgnore: opts.ignore === false,
|
|
73
|
+
ignorePatterns: opts.ignorePatterns,
|
|
74
|
+
ignoreFile: opts.ignoreFile,
|
|
75
|
+
mergeGitignore: opts.mergeGitignore !== false,
|
|
76
|
+
lineNumbers: opts.lineNumbers !== false,
|
|
77
|
+
maxSizeMb: opts.maxSize,
|
|
78
|
+
maxLines: opts.maxLines,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const relOut = path.relative(process.cwd(), result.outputPath) || result.outputPath;
|
|
82
|
+
console.log(`Aggregated ${result.includedCount} file(s) into ${relOut}`);
|
|
83
|
+
if (result.skipped.length) {
|
|
84
|
+
console.log(`Skipped ${result.skipped.length} file(s):`);
|
|
85
|
+
for (const { file, reason } of result.skipped) {
|
|
86
|
+
console.log(` - ${path.relative(process.cwd(), file)} (${reason})`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.error(`cagg: ${err.message}`);
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { run };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Extensive built-in default ignore list, gitignore-pattern-style.
|
|
4
|
+
// Covers common VCS/IDE/OS clutter, build artifacts for several ecosystems,
|
|
5
|
+
// dependency directories, lockfiles and binary/media file types that add
|
|
6
|
+
// bulk without helping a reader understand the source code.
|
|
7
|
+
module.exports = `
|
|
8
|
+
# --- version control ---
|
|
9
|
+
.git/
|
|
10
|
+
.svn/
|
|
11
|
+
.hg/
|
|
12
|
+
|
|
13
|
+
# --- OS clutter ---
|
|
14
|
+
.DS_Store
|
|
15
|
+
Thumbs.db
|
|
16
|
+
desktop.ini
|
|
17
|
+
|
|
18
|
+
# --- editors / IDEs ---
|
|
19
|
+
.vscode/
|
|
20
|
+
.idea/
|
|
21
|
+
*.iml
|
|
22
|
+
*.swp
|
|
23
|
+
*.swo
|
|
24
|
+
|
|
25
|
+
# --- node ---
|
|
26
|
+
node_modules/
|
|
27
|
+
npm-debug.log*
|
|
28
|
+
yarn-debug.log*
|
|
29
|
+
yarn-error.log*
|
|
30
|
+
.pnpm-debug.log*
|
|
31
|
+
package-lock.json
|
|
32
|
+
yarn.lock
|
|
33
|
+
pnpm-lock.yaml
|
|
34
|
+
dist/
|
|
35
|
+
build/
|
|
36
|
+
out/
|
|
37
|
+
coverage/
|
|
38
|
+
.next/
|
|
39
|
+
.nuxt/
|
|
40
|
+
.cache/
|
|
41
|
+
|
|
42
|
+
# --- python ---
|
|
43
|
+
__pycache__/
|
|
44
|
+
*.pyc
|
|
45
|
+
*.pyo
|
|
46
|
+
.venv/
|
|
47
|
+
venv/
|
|
48
|
+
env/
|
|
49
|
+
.mypy_cache/
|
|
50
|
+
.pytest_cache/
|
|
51
|
+
.tox/
|
|
52
|
+
poetry.lock
|
|
53
|
+
Pipfile.lock
|
|
54
|
+
*.egg-info/
|
|
55
|
+
|
|
56
|
+
# --- java / jvm ---
|
|
57
|
+
target/
|
|
58
|
+
*.class
|
|
59
|
+
*.jar
|
|
60
|
+
.gradle/
|
|
61
|
+
|
|
62
|
+
# --- go ---
|
|
63
|
+
vendor/
|
|
64
|
+
|
|
65
|
+
# --- rust ---
|
|
66
|
+
Cargo.lock
|
|
67
|
+
/target/
|
|
68
|
+
|
|
69
|
+
# --- ruby ---
|
|
70
|
+
.bundle/
|
|
71
|
+
Gemfile.lock
|
|
72
|
+
|
|
73
|
+
# --- dotnet ---
|
|
74
|
+
# scoped (not a bare "bin/") since Node CLI tools legitimately use a bin/ dir
|
|
75
|
+
[Bb]in/Debug/
|
|
76
|
+
[Bb]in/Release/
|
|
77
|
+
obj/
|
|
78
|
+
|
|
79
|
+
# --- logs / env ---
|
|
80
|
+
*.log
|
|
81
|
+
.env
|
|
82
|
+
.env.*
|
|
83
|
+
|
|
84
|
+
# --- archives ---
|
|
85
|
+
*.zip
|
|
86
|
+
*.tar
|
|
87
|
+
*.tar.gz
|
|
88
|
+
*.tgz
|
|
89
|
+
*.rar
|
|
90
|
+
*.7z
|
|
91
|
+
|
|
92
|
+
# --- compiled / binaries ---
|
|
93
|
+
*.exe
|
|
94
|
+
*.dll
|
|
95
|
+
*.so
|
|
96
|
+
*.dylib
|
|
97
|
+
*.o
|
|
98
|
+
*.a
|
|
99
|
+
|
|
100
|
+
# --- images / media / fonts ---
|
|
101
|
+
*.png
|
|
102
|
+
*.jpg
|
|
103
|
+
*.jpeg
|
|
104
|
+
*.gif
|
|
105
|
+
*.bmp
|
|
106
|
+
*.ico
|
|
107
|
+
*.webp
|
|
108
|
+
*.svg
|
|
109
|
+
*.mp3
|
|
110
|
+
*.mp4
|
|
111
|
+
*.mov
|
|
112
|
+
*.avi
|
|
113
|
+
*.woff
|
|
114
|
+
*.woff2
|
|
115
|
+
*.ttf
|
|
116
|
+
*.eot
|
|
117
|
+
|
|
118
|
+
# --- documents ---
|
|
119
|
+
*.pdf
|
|
120
|
+
*.doc
|
|
121
|
+
*.docx
|
|
122
|
+
*.xls
|
|
123
|
+
*.xlsx
|
|
124
|
+
*.ppt
|
|
125
|
+
*.pptx
|
|
126
|
+
|
|
127
|
+
# --- generated / minified ---
|
|
128
|
+
*.min.js
|
|
129
|
+
*.min.css
|
|
130
|
+
*.map
|
|
131
|
+
`;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const ignore = require('ignore');
|
|
6
|
+
const { isBinaryFileSync } = require('isbinaryfile');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_IGNORE = require('./defaultIgnore');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Reads a gitignore-pattern-style file and returns its non-empty lines.
|
|
12
|
+
* Missing files simply yield no extra patterns.
|
|
13
|
+
*/
|
|
14
|
+
function readPatternFile(filePath) {
|
|
15
|
+
try {
|
|
16
|
+
return fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
if (err.code === 'ENOENT') return [];
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Builds the `ignore` matcher according to the resolved options.
|
|
25
|
+
* Returns { ig, warnings } where warnings is a list of strings to print.
|
|
26
|
+
*/
|
|
27
|
+
function buildIgnoreMatcher(opts) {
|
|
28
|
+
const warnings = [];
|
|
29
|
+
|
|
30
|
+
if (opts.noIgnore) {
|
|
31
|
+
warnings.push(
|
|
32
|
+
'WARNING: ignoring is disabled (--no-ignore). Every file will be included, ' +
|
|
33
|
+
'including build artifacts, dependencies and binaries.'
|
|
34
|
+
);
|
|
35
|
+
return { ig: ignore(), warnings };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ig = ignore();
|
|
39
|
+
const hasCustomPatterns = opts.ignorePatterns.length > 0 || Boolean(opts.ignoreFile);
|
|
40
|
+
|
|
41
|
+
if (hasCustomPatterns) {
|
|
42
|
+
// Custom patterns replace the built-in default list ("Alternativ ...").
|
|
43
|
+
if (opts.ignoreFile) {
|
|
44
|
+
ig.add(readPatternFile(path.resolve(opts.ignoreFile)));
|
|
45
|
+
}
|
|
46
|
+
if (opts.ignorePatterns.length > 0) {
|
|
47
|
+
ig.add(opts.ignorePatterns);
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
ig.add(DEFAULT_IGNORE);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (opts.mergeGitignore) {
|
|
54
|
+
const projectGitignore = path.join(process.cwd(), '.gitignore');
|
|
55
|
+
const lines = readPatternFile(projectGitignore);
|
|
56
|
+
if (lines.some((l) => l.trim() && !l.trim().startsWith('#'))) {
|
|
57
|
+
ig.add(lines);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { ig, warnings };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Normalizes a path to forward slashes, as expected by the `ignore` package. */
|
|
65
|
+
function toPosix(p) {
|
|
66
|
+
return p.split(path.sep).join('/');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Recursively walks `dir`, collecting absolute file paths.
|
|
71
|
+
* Paths are checked against `ig` relative to `root` so whole directories
|
|
72
|
+
* (e.g. node_modules/) can be pruned without descending into them.
|
|
73
|
+
*/
|
|
74
|
+
function walk(dir, root, ig, results) {
|
|
75
|
+
let entries;
|
|
76
|
+
try {
|
|
77
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return; // unreadable directory (permissions, broken symlink, ...): skip
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const abs = path.join(dir, entry.name);
|
|
84
|
+
const rel = toPosix(path.relative(root, abs));
|
|
85
|
+
|
|
86
|
+
// ignore() expects directory patterns to be testable without a trailing
|
|
87
|
+
// slash mismatch; passing the relative dir path is enough for pruning.
|
|
88
|
+
if (rel && ig.ignores(entry.isDirectory() ? `${rel}/` : rel)) continue;
|
|
89
|
+
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
walk(abs, root, ig, results);
|
|
92
|
+
} else if (entry.isFile()) {
|
|
93
|
+
results.push(abs);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Formats a byte count as a human-readable MB string for messages. */
|
|
99
|
+
function toMb(bytes) {
|
|
100
|
+
return (bytes / (1024 * 1024)).toFixed(2);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Runs the aggregation with resolved options and writes the output file.
|
|
105
|
+
* `opts` fields:
|
|
106
|
+
* input, output, absolutePaths, noIgnore, ignorePatterns, ignoreFile,
|
|
107
|
+
* mergeGitignore, lineNumbers, maxSizeMb, maxLines
|
|
108
|
+
*/
|
|
109
|
+
function aggregate(opts) {
|
|
110
|
+
const cwd = process.cwd();
|
|
111
|
+
const inputRoot = path.resolve(opts.input);
|
|
112
|
+
const outputPath = path.resolve(opts.output);
|
|
113
|
+
|
|
114
|
+
const { ig, warnings } = buildIgnoreMatcher(opts);
|
|
115
|
+
warnings.forEach((w) => console.warn(w));
|
|
116
|
+
|
|
117
|
+
const allFiles = [];
|
|
118
|
+
walk(inputRoot, inputRoot, ig, allFiles);
|
|
119
|
+
|
|
120
|
+
// Never re-ingest our own output file, even across repeated runs.
|
|
121
|
+
const filtered = allFiles.filter((f) => path.resolve(f) !== outputPath);
|
|
122
|
+
|
|
123
|
+
const included = [];
|
|
124
|
+
const skipped = [];
|
|
125
|
+
const maxBytes = opts.maxSizeMb ? opts.maxSizeMb * 1024 * 1024 : null;
|
|
126
|
+
|
|
127
|
+
for (const file of filtered) {
|
|
128
|
+
let stat;
|
|
129
|
+
try {
|
|
130
|
+
stat = fs.statSync(file);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
skipped.push({ file, reason: 'unreadable' });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (maxBytes && stat.size > maxBytes) {
|
|
137
|
+
skipped.push({ file, reason: `exceeds max size (${toMb(stat.size)} MB)` });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let buffer;
|
|
142
|
+
try {
|
|
143
|
+
buffer = fs.readFileSync(file);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
skipped.push({ file, reason: 'unreadable' });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (isBinaryFileSync(buffer, buffer.length)) {
|
|
150
|
+
skipped.push({ file, reason: 'binary file' });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const content = buffer.toString('utf8');
|
|
155
|
+
const lines = content.length === 0 ? [] : content.split(/\r?\n/);
|
|
156
|
+
|
|
157
|
+
if (opts.maxLines && lines.length > opts.maxLines) {
|
|
158
|
+
skipped.push({ file, reason: `exceeds max lines (${lines.length})` });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
included.push({ file, lines });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Sort for deterministic, readable output.
|
|
166
|
+
included.sort((a, b) => a.file.localeCompare(b.file));
|
|
167
|
+
|
|
168
|
+
const parts = [];
|
|
169
|
+
for (const { file, lines } of included) {
|
|
170
|
+
const displayPath = toPosix(
|
|
171
|
+
opts.absolutePaths ? file : path.relative(cwd, file)
|
|
172
|
+
);
|
|
173
|
+
const header = `-------------- ${displayPath} -----------------`;
|
|
174
|
+
|
|
175
|
+
let body = lines
|
|
176
|
+
.map((line, i) => (opts.lineNumbers ? `${i + 1}: ${line}` : line))
|
|
177
|
+
.join('\n');
|
|
178
|
+
body = body.replace(/\n+$/, ''); // drop trailing blank lines, keep file small
|
|
179
|
+
|
|
180
|
+
parts.push(`${header}\n${body}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const output = parts.join('\n\n') + (parts.length ? '\n' : '');
|
|
184
|
+
fs.writeFileSync(outputPath, output, 'utf8');
|
|
185
|
+
|
|
186
|
+
return { outputPath, includedCount: included.length, skipped };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { aggregate, buildIgnoreMatcher, walk };
|