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.
Files changed (2) hide show
  1. package/index.js +142 -6
  2. 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 project2singletxt.js [путь_или_репо] [--out имя] [--dir папка] [--maxMb 5] [--maxLines 5000] [--keep]
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; // 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
- let fullText = '';
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
- fullText += head + fs.readFileSync(path.join(projectRoot, rel), 'utf8') + tail;
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
- logger.info({ chunk: target, lines: chunk.split(/\r?\n/).length }, 'written');
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
- logger.info({ outDir, totalMb: (writtenBytes / 1024 / 1024).toFixed(2) }, 'done');
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project2txt",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Concatenate any project (local or GitHub repo) into a single text file for AI",
5
5
  "main": "index.js",
6
6
  "bin": {