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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/index.js +144 -7
  3. 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 --name react.txt
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 --name next.txt --maxMb 10
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 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');
@@ -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; // 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 = path.resolve(projectRoot, 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
- 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
+
129
246
  for (const rel of filtered) {
130
247
  const head = `----- file: ${rel} -----\n`;
131
248
  const tail = '-----------------------------------------\n';
132
- 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;
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
- 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');
153
282
  });
154
- 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');
155
292
  })().catch(err => {
156
293
  logger.error(err);
157
294
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project2txt",
3
- "version": "1.1.0",
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": {