project2txt 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/README.md +62 -0
- package/index.js +158 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# project2txt
|
|
2
|
+
|
|
3
|
+
Concatenates any project (local folder or GitHub repo) into a single text file, ready for AI consumption.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -g project2txt # global install (run anywhere)
|
|
9
|
+
# or
|
|
10
|
+
npx project2txt <args> # no install required
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
project2txt <path_or_repo> [flags]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Флаги
|
|
20
|
+
|
|
21
|
+
- `--out NAME` — output file name (default: project.txt)
|
|
22
|
+
- `--dir FOLDER` — output folder (default: output)
|
|
23
|
+
- `--maxMb N` — max total size in MB (default: 5)
|
|
24
|
+
- `--maxLines N` — max lines per chunk; 0 = unlimited
|
|
25
|
+
- `--keep` — keep temporary clone folder (GitHub mode only)
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# local project
|
|
31
|
+
project2txt ./myProject --out bundle.txt --maxLines 5000
|
|
32
|
+
|
|
33
|
+
# short repo syntax
|
|
34
|
+
project2txt facebook/react --dir scans --name react.txt
|
|
35
|
+
|
|
36
|
+
# full URL
|
|
37
|
+
project2txt https://github.com/vercel/next.js --dir output --name next.txt --maxMb 10
|
|
38
|
+
|
|
39
|
+
# keep clone folder
|
|
40
|
+
project2txt user/repo --keep
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What it does
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
- Clones the repo if a GitHub URL is provided.
|
|
48
|
+
- Ignores:
|
|
49
|
+
`.git`, `node_modules`, `dist`, `build`, `*.log`, `*.yaml`,
|
|
50
|
+
`readme*`, `eslint.config.js`, `manifest.json`.
|
|
51
|
+
- Collects:
|
|
52
|
+
`ts`, `tsx`, `js`, `jsx`, `css`, `scss`, `sass`, `less`, `json`, `html`, `md`.
|
|
53
|
+
- Splits output only at file boundaries
|
|
54
|
+
(`----- file: … -----`).
|
|
55
|
+
- If a single file exceeds `maxLines`, it is split into `part-N` pieces.
|
|
56
|
+
- Saves the result to the specified label; on subsequent runs, `-N` is appended to the name.
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT License
|
|
61
|
+
© da-b1rmuda
|
|
62
|
+
Web2Bizz
|
package/index.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Склеивает проект в один текстовый файл для ИИ
|
|
4
|
+
* node project2singletxt.js [путь_или_репо] [--out имя] [--dir папка] [--maxMb 5] [--maxLines 5000] [--keep]
|
|
5
|
+
*/
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { glob } = require('glob');
|
|
9
|
+
const ignore = require('ignore');
|
|
10
|
+
const pino = require('pino');
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
const tmp = require('tmp');
|
|
13
|
+
tmp.setGracefulCleanup();
|
|
14
|
+
|
|
15
|
+
const logger = pino({ level: 'info' });
|
|
16
|
+
|
|
17
|
+
/* ========== CLI ========== */
|
|
18
|
+
const argv = process.argv.slice(2);
|
|
19
|
+
let projectRoot = argv[0] || '.';
|
|
20
|
+
let outFile = 'project.txt';
|
|
21
|
+
let maxSizeMb = 5;
|
|
22
|
+
let outDir = 'output';
|
|
23
|
+
let maxLines = 0; // 0 = без лимита строк
|
|
24
|
+
const keepFlag = argv.includes('--keep');
|
|
25
|
+
|
|
26
|
+
const outFlag = argv.indexOf('--out');
|
|
27
|
+
const dirFlag = argv.indexOf('--dir');
|
|
28
|
+
const maxMbFlag= argv.indexOf('--maxMb');
|
|
29
|
+
const maxLFlag = argv.indexOf('--maxLines');
|
|
30
|
+
|
|
31
|
+
if (outFlag !== -1 && argv[outFlag + 1]) outFile = argv[outFlag + 1];
|
|
32
|
+
if (dirFlag !== -1 && argv[dirFlag + 1]) outDir = argv[dirFlag + 1];
|
|
33
|
+
if (maxMbFlag !== -1 && argv[maxMbFlag + 1]) maxSizeMb= Number(argv[maxMbFlag + 1]);
|
|
34
|
+
if (maxLFlag !== -1 && argv[maxLFlag + 1]) maxLines = Number(argv[maxLFlag + 1]);
|
|
35
|
+
|
|
36
|
+
/* ========== clone GitHub repo if needed ========== */
|
|
37
|
+
function cloneIfNeeded(input) {
|
|
38
|
+
if (fs.existsSync(input)) return path.resolve(input);
|
|
39
|
+
const m = input.match(/(?:https?:\/\/github\.com\/|^)([^/]+\/[^/]+?)(?:\.git)?$/i);
|
|
40
|
+
if (!m) throw new Error('Unsupported URL or path: ' + input);
|
|
41
|
+
const repo = m[1];
|
|
42
|
+
const tmpDir = tmp.dirSync({ unsafeCleanup: !keepFlag });
|
|
43
|
+
const clonePath = path.join(tmpDir.name, repo.replace('/', '_'));
|
|
44
|
+
logger.info({ repo, clonePath }, 'cloning…');
|
|
45
|
+
execSync(`git clone --depth 1 https://github.com/${repo}.git "${clonePath}"`, { stdio: 'inherit' });
|
|
46
|
+
return clonePath;
|
|
47
|
+
}
|
|
48
|
+
projectRoot = cloneIfNeeded(projectRoot);
|
|
49
|
+
outDir = path.resolve(projectRoot, outDir);
|
|
50
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
51
|
+
|
|
52
|
+
const maxBytes = maxSizeMb * 1024 * 1024;
|
|
53
|
+
|
|
54
|
+
/* ========== ignore ========== */
|
|
55
|
+
const ig = ignore();
|
|
56
|
+
try { ig.add(fs.readFileSync(path.join(projectRoot, '.gitignore'), 'utf8')); } catch (e) {}
|
|
57
|
+
ig.add(['.git', 'node_modules', 'dist', 'build', 'out', '*.log', '*.yaml',
|
|
58
|
+
'readme*', 'eslint.config.js', 'manifest.json']);
|
|
59
|
+
|
|
60
|
+
/* ========== extensions ========== */
|
|
61
|
+
const EXT = ['ts','tsx','js','jsx','css','scss','sass','less','json','html','md'];
|
|
62
|
+
const pattern = `**/*.{${EXT.join(',')}}`;
|
|
63
|
+
|
|
64
|
+
/* ========== utils ========== */
|
|
65
|
+
function uniqueName(folder, name) {
|
|
66
|
+
const ext = path.extname(name);
|
|
67
|
+
const base = path.basename(name, ext);
|
|
68
|
+
let counter = 0;
|
|
69
|
+
let candidate = path.join(folder, name);
|
|
70
|
+
while (fs.existsSync(candidate)) {
|
|
71
|
+
counter++;
|
|
72
|
+
candidate = path.join(folder, `${base}-${counter}${ext}`);
|
|
73
|
+
}
|
|
74
|
+
return candidate;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function splitBigFile(fileBlock, maxLines) {
|
|
78
|
+
const lines = fileBlock.split(/\r?\n/);
|
|
79
|
+
const header = lines[0];
|
|
80
|
+
const body = lines.slice(1);
|
|
81
|
+
const parts = [];
|
|
82
|
+
for (let i = 0; i < body.length; i += maxLines - 1) {
|
|
83
|
+
parts.push([header, ...body.slice(i, i + maxLines - 1)].join('\n'));
|
|
84
|
+
}
|
|
85
|
+
return parts;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function splitRespectingBoundaries(fullText, maxLines) {
|
|
89
|
+
if (!maxLines) return [fullText];
|
|
90
|
+
const chunks = [];
|
|
91
|
+
let current = '';
|
|
92
|
+
let currentLines = 0;
|
|
93
|
+
|
|
94
|
+
const re = /(----- file:[^-]+ -----[\s\S]*?)(?=----- file:|$)/g;
|
|
95
|
+
let m;
|
|
96
|
+
while ((m = re.exec(fullText)) !== null) {
|
|
97
|
+
const block = m[1];
|
|
98
|
+
const lines = block.split(/\r?\n/).length;
|
|
99
|
+
if (lines > maxLines) {
|
|
100
|
+
splitBigFile(block, maxLines).forEach(ch => chunks.push(ch));
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (currentLines + lines > maxLines) {
|
|
104
|
+
chunks.push(current);
|
|
105
|
+
current = block;
|
|
106
|
+
currentLines = lines;
|
|
107
|
+
} else {
|
|
108
|
+
current += block;
|
|
109
|
+
currentLines += lines;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (current) chunks.push(current);
|
|
113
|
+
return chunks;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ========== main ========== */
|
|
117
|
+
(async () => {
|
|
118
|
+
const files = await glob(pattern, {
|
|
119
|
+
cwd: projectRoot,
|
|
120
|
+
dot: true,
|
|
121
|
+
markDirectories: true,
|
|
122
|
+
ignore: ['**/node_modules/**', '**/dist/**', '**/.git/**']
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const filtered = files.sort().filter(f => !ig.ignores(f));
|
|
126
|
+
if (!filtered.length) { logger.warn('no files found'); process.exit(0); }
|
|
127
|
+
|
|
128
|
+
let fullText = '';
|
|
129
|
+
for (const rel of filtered) {
|
|
130
|
+
const head = `----- file: ${rel} -----\n`;
|
|
131
|
+
const tail = '-----------------------------------------\n';
|
|
132
|
+
fullText += head + fs.readFileSync(path.join(projectRoot, rel), 'utf8') + tail;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const chunks = splitRespectingBoundaries(fullText, maxLines);
|
|
136
|
+
let writtenBytes = 0;
|
|
137
|
+
|
|
138
|
+
chunks.forEach((chunk, idx) => {
|
|
139
|
+
const ext = path.extname(outFile);
|
|
140
|
+
const base = path.basename(outFile, ext);
|
|
141
|
+
const name = `${base}-${idx + 1}${ext}`;
|
|
142
|
+
const target = uniqueName(outDir, name);
|
|
143
|
+
|
|
144
|
+
const buffer = Buffer.from(chunk);
|
|
145
|
+
if (writtenBytes + buffer.length > maxBytes) {
|
|
146
|
+
fs.writeFileSync(target, '\n[truncated by size limit]\n');
|
|
147
|
+
logger.warn('reached size limit');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
fs.writeFileSync(target, chunk);
|
|
151
|
+
writtenBytes += buffer.length;
|
|
152
|
+
logger.info({ chunk: target, lines: chunk.split(/\r?\n/).length }, 'written');
|
|
153
|
+
});
|
|
154
|
+
logger.info({ outDir, totalMb: (writtenBytes / 1024 / 1024).toFixed(2) }, 'done');
|
|
155
|
+
})().catch(err => {
|
|
156
|
+
logger.error(err);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "project2txt",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Concatenate any project (local or GitHub repo) into a single text file for AI",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"project2txt": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"preferGlobal": true,
|
|
10
|
+
"keywords": ["cli", "ai", "source", "concat"],
|
|
11
|
+
"author": {
|
|
12
|
+
"name": "da-b1rmuda",
|
|
13
|
+
"url": "https://github.com/da-b1rmuda"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"homepage": "https://github.com/da-b1rmuda/project2txt#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/da-b1rmuda/project2txt.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/da-b1rmuda/project2txt/issues"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"glob": "^13.0.0",
|
|
29
|
+
"ignore": "^7.0.5",
|
|
30
|
+
"pino": "^10.1.1"
|
|
31
|
+
}
|
|
32
|
+
}
|