project2txt 1.1.1 → 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/index.js +142 -6
- package/package.json +1 -1
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');
|
|
@@ -21,8 +21,9 @@ let projectRoot = argv[0] || '.';
|
|
|
21
21
|
let outFile = 'project.txt';
|
|
22
22
|
let maxSizeMb = 5;
|
|
23
23
|
let outDir = 'output';
|
|
24
|
-
let maxLines = 0;
|
|
24
|
+
let maxLines = 0;
|
|
25
25
|
const keepFlag = argv.includes('--keep');
|
|
26
|
+
const compressFlag = argv.includes('--compress'); // НОВЫЙ ФЛАГ
|
|
26
27
|
|
|
27
28
|
const outFlag = argv.indexOf('--out');
|
|
28
29
|
const dirFlag = argv.indexOf('--dir');
|
|
@@ -34,6 +35,113 @@ if (dirFlag !== -1 && argv[dirFlag + 1]) outDir = argv[dirFlag + 1];
|
|
|
34
35
|
if (maxMbFlag !== -1 && argv[maxMbFlag + 1]) maxSizeMb= Number(argv[maxMbFlag + 1]);
|
|
35
36
|
if (maxLFlag !== -1 && argv[maxLFlag + 1]) maxLines = Number(argv[maxLFlag + 1]);
|
|
36
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
|
+
|
|
37
145
|
/* ========== clone GitHub repo if needed ========== */
|
|
38
146
|
function cloneIfNeeded(input) {
|
|
39
147
|
if (fs.existsSync(input)) return path.resolve(input);
|
|
@@ -126,11 +234,26 @@ function splitRespectingBoundaries(fullText, maxLines) {
|
|
|
126
234
|
const filtered = files.sort().filter(f => !ig.ignores(f));
|
|
127
235
|
if (!filtered.length) { logger.warn('no files found'); process.exit(0); }
|
|
128
236
|
|
|
129
|
-
|
|
237
|
+
logger.info({
|
|
238
|
+
totalFiles: filtered.length,
|
|
239
|
+
compress: compressFlag ? 'ON' : 'OFF',
|
|
240
|
+
maxSizeMb
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Добавляем суммари проекта в начало
|
|
244
|
+
let fullText = generateProjectSummary(filtered, projectRoot);
|
|
245
|
+
|
|
130
246
|
for (const rel of filtered) {
|
|
131
247
|
const head = `----- file: ${rel} -----\n`;
|
|
132
248
|
const tail = '-----------------------------------------\n';
|
|
133
|
-
|
|
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;
|
|
134
257
|
}
|
|
135
258
|
|
|
136
259
|
const chunks = splitRespectingBoundaries(fullText, maxLines);
|
|
@@ -150,9 +273,22 @@ function splitRespectingBoundaries(fullText, maxLines) {
|
|
|
150
273
|
}
|
|
151
274
|
fs.writeFileSync(target, chunk);
|
|
152
275
|
writtenBytes += buffer.length;
|
|
153
|
-
|
|
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');
|
|
154
282
|
});
|
|
155
|
-
|
|
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');
|
|
156
292
|
})().catch(err => {
|
|
157
293
|
logger.error(err);
|
|
158
294
|
process.exit(1);
|