project2txt 1.1.0 → 1.2.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 +2 -2
- package/index.js +144 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,10 +31,10 @@ project2txt <path_or_repo> [flags]
|
|
|
31
31
|
project2txt ./myProject --out bundle.txt --maxLines 5000
|
|
32
32
|
|
|
33
33
|
# short repo syntax
|
|
34
|
-
project2txt facebook/react --dir scans --
|
|
34
|
+
project2txt facebook/react --dir scans --out react.txt
|
|
35
35
|
|
|
36
36
|
# full URL
|
|
37
|
-
project2txt https://github.com/vercel/next.js --dir output --
|
|
37
|
+
project2txt https://github.com/vercel/next.js --dir output --out next.txt --maxMb 10
|
|
38
38
|
|
|
39
39
|
# keep clone folder
|
|
40
40
|
project2txt user/repo --keep
|
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Склеивает проект в один текстовый файл для ИИ
|
|
4
|
-
* node
|
|
4
|
+
* node project2txt.js [путь_или_репо] [--compress] [--out имя] [--dir папка] [--maxMb 5] [--maxLines 5000] [--keep]
|
|
5
5
|
*/
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
@@ -10,6 +10,7 @@ const ignore = require('ignore');
|
|
|
10
10
|
const pino = require('pino');
|
|
11
11
|
const { execSync } = require('child_process');
|
|
12
12
|
const tmp = require('tmp');
|
|
13
|
+
const CWD = process.cwd();
|
|
13
14
|
tmp.setGracefulCleanup();
|
|
14
15
|
|
|
15
16
|
const logger = pino({ level: 'info' });
|
|
@@ -20,8 +21,9 @@ let projectRoot = argv[0] || '.';
|
|
|
20
21
|
let outFile = 'project.txt';
|
|
21
22
|
let maxSizeMb = 5;
|
|
22
23
|
let outDir = 'output';
|
|
23
|
-
let maxLines = 0;
|
|
24
|
+
let maxLines = 0;
|
|
24
25
|
const keepFlag = argv.includes('--keep');
|
|
26
|
+
const compressFlag = argv.includes('--compress'); // НОВЫЙ ФЛАГ
|
|
25
27
|
|
|
26
28
|
const outFlag = argv.indexOf('--out');
|
|
27
29
|
const dirFlag = argv.indexOf('--dir');
|
|
@@ -33,6 +35,113 @@ if (dirFlag !== -1 && argv[dirFlag + 1]) outDir = argv[dirFlag + 1];
|
|
|
33
35
|
if (maxMbFlag !== -1 && argv[maxMbFlag + 1]) maxSizeMb= Number(argv[maxMbFlag + 1]);
|
|
34
36
|
if (maxLFlag !== -1 && argv[maxLFlag + 1]) maxLines = Number(argv[maxLFlag + 1]);
|
|
35
37
|
|
|
38
|
+
/* ========== ФУНКЦИЯ СЖАТИЯ ДЛЯ ИИ ========== */
|
|
39
|
+
function compressForAI(code, filePath) {
|
|
40
|
+
if (!compressFlag) return code; // Если флаг не указан - возвращаем как есть
|
|
41
|
+
|
|
42
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
43
|
+
|
|
44
|
+
// Базовое сжатие (для всех файлов)
|
|
45
|
+
let compressed = code
|
|
46
|
+
// Удаляем множественные пустые строки (max 2 подряд)
|
|
47
|
+
.replace(/\n\s*\n\s*\n+/g, '\n\n')
|
|
48
|
+
// Удаляем пробелы в конце строк
|
|
49
|
+
.replace(/[ \t]+$/gm, '')
|
|
50
|
+
// Удаляем однострочные комментарии
|
|
51
|
+
.replace(/\/\/.*$/gm, '')
|
|
52
|
+
// Удаляем блоки комментариев
|
|
53
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
54
|
+
// Удаляем debug-выражения
|
|
55
|
+
.replace(/^\s*(console\.(log|warn|error|info|time|timeEnd)\(.*\)|debugger);?\s*$/gm, '')
|
|
56
|
+
.replace(/\s*console\.(log|warn|error|info)\(.*\)\s*;/g, '');
|
|
57
|
+
|
|
58
|
+
// Специфичные оптимизации по типам файлов
|
|
59
|
+
switch (ext) {
|
|
60
|
+
case '.js':
|
|
61
|
+
case '.jsx':
|
|
62
|
+
case '.ts':
|
|
63
|
+
case '.tsx':
|
|
64
|
+
// Удаляем импорты типов TypeScript
|
|
65
|
+
compressed = compressed.replace(/import type .*?from .*?['"][^'"]+['"];?\n?/g, '');
|
|
66
|
+
// Удаляем пустые экспорты
|
|
67
|
+
compressed = compressed.replace(/export\s*{\s*};?\n?/g, '');
|
|
68
|
+
break;
|
|
69
|
+
|
|
70
|
+
case '.json':
|
|
71
|
+
// Минификация JSON (без пробелов)
|
|
72
|
+
try {
|
|
73
|
+
const obj = JSON.parse(compressed);
|
|
74
|
+
compressed = JSON.stringify(obj);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
// Если невалидный JSON, просто сжимаем пробелы
|
|
77
|
+
compressed = compressed
|
|
78
|
+
.replace(/\s*([\{\}\[\],:])\s*/g, '$1')
|
|
79
|
+
.replace(/{\s*/g, '{').replace(/\s*}/g, '}');
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
|
|
83
|
+
case '.html':
|
|
84
|
+
// Удаляем комментарии HTML
|
|
85
|
+
compressed = compressed.replace(/<!--[\s\S]*?-->/g, '');
|
|
86
|
+
// Сжимаем пробелы в тегах
|
|
87
|
+
compressed = compressed.replace(/>\s+</g, '><');
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
case '.css':
|
|
91
|
+
case '.scss':
|
|
92
|
+
case '.less':
|
|
93
|
+
// Удаляем CSS комментарии
|
|
94
|
+
compressed = compressed.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
95
|
+
// Сжимаем пробелы
|
|
96
|
+
compressed = compressed.replace(/\s*([\{\}:;,])\s*/g, '$1');
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Финальная очистка
|
|
101
|
+
compressed = compressed
|
|
102
|
+
.replace(/\n{3,}/g, '\n\n') // Не больше 2 пустых строк подряд
|
|
103
|
+
.trim();
|
|
104
|
+
|
|
105
|
+
return compressed;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* ========== ГЕНЕРАЦИЯ СУММАРИ ПРОЕКТА ========== */
|
|
109
|
+
function generateProjectSummary(files, projectRoot) {
|
|
110
|
+
const summary = [];
|
|
111
|
+
summary.push('=== PROJECT SUMMARY ===\n');
|
|
112
|
+
|
|
113
|
+
// Группируем файлы по типам
|
|
114
|
+
const fileTypes = {};
|
|
115
|
+
files.forEach(file => {
|
|
116
|
+
const ext = path.extname(file) || 'no-extension';
|
|
117
|
+
fileTypes[ext] = (fileTypes[ext] || 0) + 1;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
summary.push('File types:');
|
|
121
|
+
Object.entries(fileTypes).forEach(([ext, count]) => {
|
|
122
|
+
summary.push(` ${ext || '(no ext)'}: ${count} files`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Находим ключевые файлы
|
|
126
|
+
const keyFiles = files.filter(f =>
|
|
127
|
+
/^(package\.json|tsconfig\.json|webpack\.config|index\.|app\.|main\.|readme)/i.test(path.basename(f))
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (keyFiles.length) {
|
|
131
|
+
summary.push('\nKey files:');
|
|
132
|
+
keyFiles.forEach(file => {
|
|
133
|
+
try {
|
|
134
|
+
const content = fs.readFileSync(path.join(projectRoot, file), 'utf8');
|
|
135
|
+
const lines = content.split('\n').length;
|
|
136
|
+
summary.push(` ${file} (${lines} lines)`);
|
|
137
|
+
} catch (e) {}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
summary.push('\n=== END SUMMARY ===\n\n');
|
|
142
|
+
return summary.join('\n');
|
|
143
|
+
}
|
|
144
|
+
|
|
36
145
|
/* ========== clone GitHub repo if needed ========== */
|
|
37
146
|
function cloneIfNeeded(input) {
|
|
38
147
|
if (fs.existsSync(input)) return path.resolve(input);
|
|
@@ -46,7 +155,7 @@ function cloneIfNeeded(input) {
|
|
|
46
155
|
return clonePath;
|
|
47
156
|
}
|
|
48
157
|
projectRoot = cloneIfNeeded(projectRoot);
|
|
49
|
-
outDir
|
|
158
|
+
outDir = path.resolve(CWD, outDir);
|
|
50
159
|
fs.mkdirSync(outDir, { recursive: true });
|
|
51
160
|
|
|
52
161
|
const maxBytes = maxSizeMb * 1024 * 1024;
|
|
@@ -125,11 +234,26 @@ function splitRespectingBoundaries(fullText, maxLines) {
|
|
|
125
234
|
const filtered = files.sort().filter(f => !ig.ignores(f));
|
|
126
235
|
if (!filtered.length) { logger.warn('no files found'); process.exit(0); }
|
|
127
236
|
|
|
128
|
-
|
|
237
|
+
logger.info({
|
|
238
|
+
totalFiles: filtered.length,
|
|
239
|
+
compress: compressFlag ? 'ON' : 'OFF',
|
|
240
|
+
maxSizeMb
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Добавляем суммари проекта в начало
|
|
244
|
+
let fullText = generateProjectSummary(filtered, projectRoot);
|
|
245
|
+
|
|
129
246
|
for (const rel of filtered) {
|
|
130
247
|
const head = `----- file: ${rel} -----\n`;
|
|
131
248
|
const tail = '-----------------------------------------\n';
|
|
132
|
-
|
|
249
|
+
const fileContent = fs.readFileSync(path.join(projectRoot, rel), 'utf8');
|
|
250
|
+
|
|
251
|
+
// Применяем сжатие только если включен флаг --compress
|
|
252
|
+
const processedContent = compressFlag
|
|
253
|
+
? compressForAI(fileContent, rel)
|
|
254
|
+
: fileContent;
|
|
255
|
+
|
|
256
|
+
fullText += head + processedContent + tail;
|
|
133
257
|
}
|
|
134
258
|
|
|
135
259
|
const chunks = splitRespectingBoundaries(fullText, maxLines);
|
|
@@ -149,9 +273,22 @@ function splitRespectingBoundaries(fullText, maxLines) {
|
|
|
149
273
|
}
|
|
150
274
|
fs.writeFileSync(target, chunk);
|
|
151
275
|
writtenBytes += buffer.length;
|
|
152
|
-
|
|
276
|
+
const stats = {
|
|
277
|
+
chunk: path.basename(target),
|
|
278
|
+
lines: chunk.split(/\r?\n/).length,
|
|
279
|
+
sizeKB: (buffer.length / 1024).toFixed(1)
|
|
280
|
+
};
|
|
281
|
+
logger.info(stats, 'written');
|
|
153
282
|
});
|
|
154
|
-
|
|
283
|
+
|
|
284
|
+
const stats = {
|
|
285
|
+
outDir,
|
|
286
|
+
totalFiles: filtered.length,
|
|
287
|
+
totalChunks: chunks.length,
|
|
288
|
+
totalMB: (writtenBytes / 1024 / 1024).toFixed(2),
|
|
289
|
+
compressUsed: compressFlag
|
|
290
|
+
};
|
|
291
|
+
logger.info(stats, 'done');
|
|
155
292
|
})().catch(err => {
|
|
156
293
|
logger.error(err);
|
|
157
294
|
process.exit(1);
|