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.
Files changed (3) hide show
  1. package/README.md +62 -0
  2. package/index.js +158 -0
  3. 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
+ }